@junghanacs/entwurf 0.45.0 → 0.51.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 (116) hide show
  1. package/AGENTS.md +5 -3
  2. package/BASELINE.md +110 -1
  3. package/CHANGELOG.md +249 -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 +132 -188
  9. package/VERIFY.md +18 -11
  10. package/demo/README.md +1 -1
  11. package/docs/acp-backend-rail.md +1 -1
  12. package/docs/external-mcp-host.md +4 -4
  13. package/docs/setup-clean-host.md +94 -22
  14. package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
  15. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +4 -4
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
  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/resume-launch-identity.js +18 -11
  28. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
  29. package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
  30. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
  31. package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
  32. package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
  33. package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
  34. package/mcp/entwurf-bridge/src/index.ts +28 -8
  35. package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
  36. package/native/win32-x64/manifest.json +1 -1
  37. package/package.json +16 -20
  38. package/pi/pi-durable/bootstrap.mjs +179 -6
  39. package/pi/pi-durable/carrier/runtime.js +327 -57
  40. package/pi/pi-durable/carrier/session-name.js +11 -0
  41. package/pi/pi-durable/carrier/sessions.js +96 -20
  42. package/pi/pi-durable/carrier/subagent.js +5 -2
  43. package/pi/pi-durable/carrier/tui.js +60 -22
  44. package/pi/pi-durable/overlay/upstream-pin.json +26 -19
  45. package/pi-extensions/lib/entwurf-v2-contract-schema.ts +73 -69
  46. package/pi-extensions/lib/entwurf-v2-contract.ts +4 -4
  47. package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
  48. package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
  49. package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
  50. package/pi-extensions/lib/herdr-resume-call.ts +810 -0
  51. package/pi-extensions/lib/meta-session.ts +13 -8
  52. package/pi-extensions/lib/mux-resume-call.ts +77 -4
  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/resume-launch-identity.ts +31 -12
  58. package/pi-extensions/meta-bridge-pi-durable.ts +30 -16
  59. package/run.sh +552 -699
  60. package/scripts/check-claude-launch.ts +341 -0
  61. package/scripts/check-entwurf-v2-contract.ts +4 -4
  62. package/scripts/check-entwurf-v2-surface.ts +2 -2
  63. package/scripts/check-fresh-call-dispatch.ts +70 -9
  64. package/scripts/check-fresh-cut-gate.sh +23 -9
  65. package/scripts/check-gate-qualification.ts +19 -14
  66. package/scripts/check-herdr-activation.ts +301 -23
  67. package/scripts/check-herdr-fresh-call.ts +161 -25
  68. package/scripts/check-herdr-plugin-build.ts +173 -1
  69. package/scripts/check-herdr-plugin.ts +28 -2
  70. package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
  71. package/scripts/check-install-surface.ts +1236 -6
  72. package/scripts/check-meta-doctor-oracle.sh +1 -1
  73. package/scripts/check-meta-facts.ts +20 -1
  74. package/scripts/check-mux-resume-call.ts +28 -0
  75. package/scripts/check-peer-facts.ts +39 -2
  76. package/scripts/cli-help.mjs +222 -0
  77. package/scripts/dev-bin.sh +39 -2
  78. package/scripts/herdr-activation.mjs +28 -6
  79. package/scripts/herdr-operator-link.mjs +389 -0
  80. package/scripts/herdr-plugin-activate.mjs +5 -0
  81. package/scripts/herdr-plugin-deactivate.mjs +18 -5
  82. package/scripts/herdr-rails.mjs +43 -7
  83. package/scripts/herdr-runtime.mjs +27 -0
  84. package/scripts/meta-bridge-fresh-cut.ts +1 -27
  85. package/scripts/meta-bridge-prune.ts +1 -1
  86. package/scripts/meta-facts.ts +1 -1
  87. package/scripts/mutants/agent-guidance.json +4 -4
  88. package/scripts/mutants/claude-launch.json +142 -0
  89. package/scripts/mutants/cli-help-source.json +420 -0
  90. package/scripts/mutants/control-send-receipt.json +1 -1
  91. package/scripts/mutants/dev-gate-dispatch.json +109 -0
  92. package/scripts/mutants/fresh-call-dispatch.json +38 -2
  93. package/scripts/mutants/fresh-cut.json +15 -0
  94. package/scripts/mutants/herdr-activation.json +128 -0
  95. package/scripts/mutants/herdr-fresh-call.json +278 -5
  96. package/scripts/mutants/herdr-operator-link.json +318 -0
  97. package/scripts/mutants/herdr-plugin-build.json +55 -0
  98. package/scripts/mutants/herdr-plugin.json +17 -0
  99. package/scripts/mutants/herdr-resume-call.json +288 -0
  100. package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
  101. package/scripts/mutants/meta-facts.json +15 -0
  102. package/scripts/mutants/mux-resume-call.json +63 -2
  103. package/scripts/mutants/pack-install.json +4 -4
  104. package/scripts/mutants/peer-facts.json +15 -0
  105. package/scripts/mutants/pi-durable.json +1167 -3
  106. package/scripts/mutants/pi-floor.json +22 -5
  107. package/scripts/mutants/source-install.json +35 -0
  108. package/scripts/mutants/v2-surface.json +2 -2
  109. package/scripts/mutants/v2-visible-resume.json +255 -6
  110. package/scripts/peer-facts.ts +1 -1
  111. package/scripts/smoke-agy-install-state.sh +66 -0
  112. package/scripts/smoke-meta-prune.sh +23 -0
  113. package/scripts/smoke-omp-fresh-live.ts +12 -3
  114. package/scripts/smoke-omp-receive-live.ts +7 -1
  115. package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
  116. package/team.json.example +39 -0
@@ -14,7 +14,9 @@
14
14
  // fails and closes itself retires that doorbell at once;
15
15
  // - a launcher's first input, when given, is admitted to the root ONCE, after that birth;
16
16
  // - the TUI is handed the native view with the garden id on every conversation label
17
- // (`withGardenIdentity`).
17
+ // (`withGardenIdentity`);
18
+ // - every open takes the owner fence (#157 B3) — built once here from the compiled closure — inside
19
+ // the session directory before its native lease; a refusal opens nothing.
18
20
  // Nothing of the native registry, controller or TUI is rebuilt here.
19
21
  //
20
22
  // The durable app itself is this package's CARRIER (./carrier/, emitted from Entwurf's owned source
@@ -27,6 +29,8 @@
27
29
  // <entwurf>/pi/pi-durable/bootstrap.mjs \
28
30
  // --provider <provider> --model <model id> --width task-wide [--entwurf-bootstrap <json>]
29
31
  // ... bootstrap.mjs --continue
32
+ // ... bootstrap.mjs --resume
33
+ // ... bootstrap.mjs --session <native id>
30
34
  //
31
35
  // Explicit means the native API, not a vendor argv: upstream's durable main takes only
32
36
  // `--continue`, so `--provider`/`--model` reach pi's own `findInitialAgentModel` through the
@@ -45,9 +49,17 @@
45
49
  // address rides the process env only.
46
50
  // `--continue` opens the newest session for this directory as upstream does (not a chosen
47
51
  // record), so every flag that only a new session can honour is refused beside it by name.
52
+ // `--session <native id>` (#157 G3 B-L1) reopens exactly that native session of this directory as it
53
+ // was saved, through the owned runtime's `session` option, with no interactive session picker or
54
+ // picker-specific TTY check; the normal TUI still runs. The id is the native session directory name, never a garden id: a native CLI selector, not an address. It is
55
+ // held to the owned `isSessionName` grammar before operator-native module evaluation, TUI load, open
56
+ // or birth; that scope is lexical (the selection stays under this cwd's session root by name), not
57
+ // physical symlink confinement. A well-formed id this directory does not hold is the runtime's own
58
+ // refusal, after the TUI module loads and before any TUI runs. `--continue`, `--resume` and
59
+ // `--session` are mutually exclusive, and every new-session flag is refused beside each of them.
48
60
  //
49
- // `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new or a continued
50
- // session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
61
+ // `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new, a continued, a
62
+ // resumed or a named session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
51
63
  // its initialization, and its default export is a native durable Extension, handed to `openDurable`
52
64
  // by reference after the contact. Initialization runs before the TUI or the runtime is imported —
53
65
  // the runtime creates the provider runtime before it installs any extension (carrier/runtime.js
@@ -68,16 +80,28 @@
68
80
  // REPLACE the first extension or keep both tools. A refusal at module k leaves modules before it
69
81
  // evaluated: nothing is rolled back. No watch, no recursion, nothing persisted.
70
82
  //
83
+ // `--entwurf-adopt <json>` (#157 B3 I-3) opens the one native session a parent reserved for this process. The
84
+ // payload is closed: exactly `{v:1, gardenId, metaRoot, sourceGeneration}`, and any other key or value is refused. It
85
+ // is exclusive with `--continue`, `--resume`, `--session` and every new-session flag. The garden record under
86
+ // `metaRoot` is the address. Its native session id is the session, held to the same grammar as `--session`.
87
+ // Before anything loads, the four `ENTWURF_META_*_DIR` values this process actually received must be `metaRoot`'s
88
+ // standard children (the launch's fixed projection). The contact's roots are those four, whatever a seam passes.
89
+ // The open takes the adoption fence of that context, which re-reads the record and its location inside its own
90
+ // holds. The selection read here is never handed to the fence as evidence.
91
+ //
71
92
  // Without that resolver the carrier's first pi-coding-agent internal import fails by name (an
72
93
  // unsupported entwurf-pi-dist: URL scheme), and that failure is the honest one.
73
94
  import { readdirSync, realpathSync, statSync } from "node:fs";
74
95
  import { isAbsolute, join } from "node:path";
75
96
  import { fileURLToPath, pathToFileURL } from "node:url";
97
+ import { readAddressableMetaIdentity } from "../../mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js";
98
+ import { createPiDurableOwnerFence } from "../../mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js";
76
99
  import { decodeOmpBootstrapPayload } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js";
77
100
  import {
78
101
  closeAll,
79
102
  createPiDurableContact,
80
103
  failAfterCleanup,
104
+ PI_DURABLE_BACKEND,
81
105
  PI_DURABLE_EXTENSION_NAME,
82
106
  PI_DURABLE_TOOL_NAMES,
83
107
  piDurableGardenIds,
@@ -87,6 +111,14 @@ import {
87
111
  const RUNTIME = "./carrier/runtime.js";
88
112
  const TUI = "./carrier/tui.js";
89
113
  const SESSIONS = "./carrier/sessions.js";
114
+ /** The session-name grammar alone: no imports and no I/O, so reading it loads nothing an operator module may prepare. */
115
+ const SESSION_NAME = "./carrier/session-name.js";
116
+
117
+ /**
118
+ * The owner fence every ordinary open of this process hands the runtime (#157 B3); an adoption hands its own (below).
119
+ * Building it reads and writes nothing.
120
+ */
121
+ const OWNER_FENCE = createPiDurableOwnerFence();
90
122
 
91
123
  /** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
92
124
  export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
@@ -102,9 +134,20 @@ const VALUE_FLAGS = [
102
134
  "--entwurf-bootstrap",
103
135
  "--native-module",
104
136
  "--native-module-dir",
137
+ "--session",
138
+ "--entwurf-adopt",
105
139
  ];
106
140
  /** Flags only a NEW session can honour. */
107
141
  const FRESH_ONLY_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
142
+ /** The closed adopt context's keys, sorted: nothing else rides it (#157 B3 I-3). */
143
+ const ADOPT_KEYS = ["gardenId", "metaRoot", "sourceGeneration", "v"];
144
+ /** Each contact root of an adoption: the env name its launch carries it in, and its standard child of `metaRoot`. */
145
+ const ADOPT_ROOTS = {
146
+ sessionsDir: ["ENTWURF_META_SESSIONS_DIR", "meta-sessions"],
147
+ mailboxDir: ["ENTWURF_META_MAILBOX_DIR", "meta-mailbox"],
148
+ sendersDir: ["ENTWURF_META_SENDERS_DIR", "meta-senders"],
149
+ receiversDir: ["ENTWURF_META_RECEIVERS_DIR", "meta-receivers"],
150
+ };
108
151
 
109
152
  /**
110
153
  * Extension names the durable app installs itself — the contact, then the carrier's own
@@ -124,6 +167,68 @@ function refuse(reason, detail) {
124
167
 
125
168
  const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
126
169
 
170
+ const messageOf = (error) => (error instanceof Error ? error.message : String(error));
171
+
172
+ /**
173
+ * `--entwurf-adopt`'s closed context, decoded before anything loads. The key set and `v` are this grammar's own.
174
+ * The garden id, the generation and the absolute root are the adoption fence's checks, run here by building the
175
+ * fence, which reads and writes nothing.
176
+ */
177
+ function decodeAdoptContext(text) {
178
+ let value;
179
+ try {
180
+ value = JSON.parse(text);
181
+ } catch (error) {
182
+ throw Object.assign(refuse("adopt-context-invalid", "--entwurf-adopt is not JSON"), { cause: error });
183
+ }
184
+ if (
185
+ typeof value !== "object" ||
186
+ value === null ||
187
+ Array.isArray(value) ||
188
+ Object.keys(value).sort().join(",") !== ADOPT_KEYS.join(",")
189
+ ) {
190
+ throw refuse("adopt-context-invalid", "--entwurf-adopt is exactly {v, gardenId, metaRoot, sourceGeneration}");
191
+ }
192
+ if (value.v !== 1) throw refuse("adopt-context-invalid", "--entwurf-adopt is version 1");
193
+ const { gardenId, metaRoot, sourceGeneration } = value;
194
+ let fence;
195
+ try {
196
+ fence = createPiDurableOwnerFence({ adoption: { gardenId, metaRoot, sourceGeneration } });
197
+ } catch (error) {
198
+ throw Object.assign(refuse("adopt-context-invalid", messageOf(error)), { cause: error });
199
+ }
200
+ return { gardenId, metaRoot, sourceGeneration, fence };
201
+ }
202
+
203
+ /** An adoption's contact roots: the standard children of its `metaRoot`, the shape the parent's reserve required. */
204
+ function adoptRoots(metaRoot) {
205
+ return Object.fromEntries(Object.entries(ADOPT_ROOTS).map(([key, [, child]]) => [key, join(metaRoot, child)]));
206
+ }
207
+
208
+ /** The four meta roots this process actually received must be the context's; no ambient root stands in for them. */
209
+ function requireAdoptRootEnv(metaRoot) {
210
+ const mismatched = Object.values(ADOPT_ROOTS)
211
+ .filter(([name, child]) => process.env[name] !== join(metaRoot, child))
212
+ .map(([name]) => name);
213
+ if (mismatched.length > 0) {
214
+ throw refuse("adopt-root-mismatch", `${mismatched.join(", ")} must be the standard children of ${metaRoot}`);
215
+ }
216
+ }
217
+
218
+ /** The native session the garden record names. The record is the address, read through the addressable reader. */
219
+ function adoptedSession({ gardenId, metaRoot }) {
220
+ let record;
221
+ try {
222
+ record = readAddressableMetaIdentity(gardenId, join(metaRoot, ADOPT_ROOTS.sessionsDir[1]));
223
+ } catch (error) {
224
+ throw Object.assign(refuse("adopt-record-unaddressable", `${gardenId}: ${messageOf(error)}`), { cause: error });
225
+ }
226
+ if (record.backend !== PI_DURABLE_BACKEND) {
227
+ throw refuse("adopt-record-not-pi-durable", `${gardenId} is a ${record.backend} record`);
228
+ }
229
+ return record.nativeSessionId;
230
+ }
231
+
127
232
  /** The identity carriers present now: names and values, compared in memory and never printed. */
128
233
  function identitySnapshot() {
129
234
  return Object.fromEntries(
@@ -291,6 +396,8 @@ async function loadNativeModules(files) {
291
396
  * checkout-only gates, which drive a private bundle) passes another one — never the launch argv.
292
397
  * `extensions` are the operator's native extensions (`--native-module` or `--native-module-dir`),
293
398
  * installed after the contact, by reference.
399
+ * `adopt` is a decoded `--entwurf-adopt` context (#157 B3 I-3). Its fence admits the open, in place of the ordinary
400
+ * one, and its `metaRoot`'s four standard roots are the contact's, spread last so no seam replaces them.
294
401
  *
295
402
  * @param {{
296
403
  * bridgeEntry?: string,
@@ -301,7 +408,8 @@ async function loadNativeModules(files) {
301
408
  * firstInput?: string,
302
409
  * extensions?: readonly object[],
303
410
  * contact?: Record<string, unknown>,
304
- * open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
411
+ * adopt?: { gardenId: string, metaRoot: string, sourceGeneration: string, fence: object },
412
+ * open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string }, fence: object }) => Promise<any>,
305
413
  * }} options
306
414
  * @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
307
415
  * (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
@@ -316,6 +424,7 @@ export async function openDurableCitizen({
316
424
  firstInput,
317
425
  extensions = [],
318
426
  contact: contactOptions = {},
427
+ adopt,
319
428
  open,
320
429
  }) {
321
430
  if (continueSession && (model !== undefined || firstInput !== undefined)) {
@@ -337,7 +446,12 @@ export async function openDurableCitizen({
337
446
  throw refuse("first-input-empty", "a first input is a non-empty task");
338
447
  }
339
448
  const openDurable = open ?? (await import(RUNTIME)).openDurable;
340
- const contact = createPiDurableContact({ ...contactOptions, bridgeEntry });
449
+ const fence = adopt === undefined ? OWNER_FENCE : adopt.fence;
450
+ const contact = createPiDurableContact({
451
+ ...contactOptions,
452
+ bridgeEntry,
453
+ ...(adopt === undefined ? {} : adoptRoots(adopt.metaRoot)),
454
+ });
341
455
  let durable;
342
456
  try {
343
457
  durable = await openDurable({
@@ -346,6 +460,7 @@ export async function openDurableCitizen({
346
460
  ...(session === undefined ? {} : { session }),
347
461
  extensions: [contact.extension, ...extensions],
348
462
  ...(model === undefined ? {} : { model }),
463
+ fence,
349
464
  });
350
465
  } catch (error) {
351
466
  contact.fail(error);
@@ -426,7 +541,42 @@ function parseArgs(argv) {
426
541
  "--resume chooses a session and --continue opens the newest one; give one of them",
427
542
  );
428
543
  }
544
+ const session = values.get("--session");
545
+ if (session !== undefined && continueSession) {
546
+ throw refuse(
547
+ "session-continue-exclusive",
548
+ "--session names the session to reopen and --continue opens the newest one; give one of them",
549
+ );
550
+ }
551
+ if (session !== undefined && resume) {
552
+ throw refuse(
553
+ "session-resume-exclusive",
554
+ "--session names the session to reopen and --resume chooses one from a list; give one of them",
555
+ );
556
+ }
429
557
  const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
558
+ const adoptArg = values.get("--entwurf-adopt");
559
+ if (adoptArg !== undefined) {
560
+ const others = [
561
+ ...(continueSession ? ["--continue"] : []),
562
+ ...(resume ? ["--resume"] : []),
563
+ ...(session === undefined ? [] : ["--session"]),
564
+ ...fresh,
565
+ ];
566
+ if (others.length > 0) {
567
+ throw refuse(
568
+ "adopt-exclusive",
569
+ `${others.join(", ")} cannot be given with --entwurf-adopt: an adoption opens exactly the reserved session`,
570
+ );
571
+ }
572
+ }
573
+ const adopt = adoptArg === undefined ? undefined : decodeAdoptContext(adoptArg);
574
+ if (session !== undefined && fresh.length > 0) {
575
+ throw refuse(
576
+ "fresh-only-argument-with-session",
577
+ `${fresh.join(", ")} apply only to a new session; --session reopens the named session as it was saved`,
578
+ );
579
+ }
430
580
  if (resume && fresh.length > 0) {
431
581
  throw refuse(
432
582
  "fresh-only-argument-with-resume",
@@ -455,6 +605,8 @@ function parseArgs(argv) {
455
605
  : {};
456
606
  if (continueSession) return { continueSession, ...nativeModule };
457
607
  if (resume) return { resume, ...nativeModule };
608
+ if (session !== undefined) return { session, ...nativeModule };
609
+ if (adopt !== undefined) return { adopt, ...nativeModule };
458
610
  const provider = values.get("--provider");
459
611
  if (provider === undefined) {
460
612
  throw refuse(
@@ -543,8 +695,29 @@ async function chooseSession(cwd, sessionsDir, pick) {
543
695
  * }} [seams]
544
696
  */
545
697
  export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry, pick } = {}) {
546
- // Every argument refusal is decided here, before any module, the TUI or the app loads.
698
+ // Every argument refusal but the `--session` grammar is decided here, before any module, the TUI or
699
+ // the app loads.
547
700
  const { nativeModules = [], resume = false, ...args } = parseArgs(argv);
701
+ // `--entwurf-adopt` (#157 B3 I-3): the roots this process received are checked against the context before
702
+ // anything else is read. The session is the one the garden record names, under the `--session` grammar below.
703
+ if (args.adopt !== undefined) {
704
+ requireAdoptRootEnv(args.adopt.metaRoot);
705
+ args.session = adoptedSession(args.adopt);
706
+ }
707
+ // `--session` is held to the owned session-name grammar the runtime selects by, before operator-native
708
+ // module evaluation, TUI load, open or birth. Only the import-free grammar file is read for it — never
709
+ // the session store, whose SDK config import fixes package state from the environment an operator
710
+ // module may still prepare. The shape is lexical — it names a directory under this cwd's session root
711
+ // by name only, which is not physical symlink confinement.
712
+ if (args.session !== undefined) {
713
+ const { isSessionName } = await import(SESSION_NAME);
714
+ if (!isSessionName(args.session)) {
715
+ throw refuse(
716
+ "session-id-malformed",
717
+ `${JSON.stringify(args.session)} is not a durable session id (<13-digit ms>-<uuid>)`,
718
+ );
719
+ }
720
+ }
548
721
  // The caller's directory, fixed before any operator code runs and handed to the app explicitly.
549
722
  const cwd = process.cwd();
550
723
  const extensions = await loadNativeModules(nativeModules);