@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
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * herdr-operator-link — the operator's ONE explicit link to the Herdr runtime's `entwurf` (#153).
4
+ *
5
+ * The plugin puts nothing on PATH (`herdr-runtime.mjs` header). This verb is the single exception,
6
+ * and only on request: `expose --bin-dir <abs>` makes exactly one symlink, `<bin-dir>/entwurf`, to
7
+ * the stable runtime's `node_modules/.bin/entwurf`; `withdraw` takes exactly that link back. Nothing
8
+ * else is named, no profile is edited, and no directory is chosen for the operator.
9
+ *
10
+ * THE RECEIPT IS THE SUB-OWNER. One file beside the activation ledger records the link — six keys,
11
+ * three phases — and is the only authority any inverse acts on. A link without a receipt is someone
12
+ * else's, whatever it points at, and nothing here goes looking along PATH for one.
13
+ *
14
+ * THE LEDGER FLOOR COMES FIRST. A receipt may exist only beside a schema-3 ledger: a reader that
15
+ * admits only 2 refuses that ledger before its first write, so an artifact that does not know this
16
+ * receipt can never tear down a runtime the link still names. The floor is raised here, and only
17
+ * here, from a settled host with no receipt; it is never lowered — a withdraw keeps it.
18
+ *
19
+ * ORDER IS THE CRASH CONTRACT: floor 3 → receipt `linking` → symlink → receipt `linked`. Each step
20
+ * is published atomically (write beside, rename) and judged again on the next call, so a process
21
+ * stopped between any two leaves a state the next call names and continues. Power loss is not
22
+ * certified (no fsync), as for the ledger itself.
23
+ *
24
+ * EVERY IMPORT IS STATIC. `herdr-plugin-deactivate` imports this module and then deletes the runtime
25
+ * it runs from; a lazy import afterwards would fail.
26
+ */
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+ import {
32
+ ActivationError,
33
+ certifyLedgerDescribesRuntime,
34
+ certifyPhaseForInverse,
35
+ certifyRootsAgainstLedger,
36
+ readCertifiedLedger,
37
+ resolveActivationLayout,
38
+ resolveComponentRoots,
39
+ runtimeIdentityOnDisk,
40
+ writeLedger,
41
+ } from "./herdr-activation.mjs";
42
+ import {
43
+ artifactCompleteness,
44
+ readCertifiedJournal,
45
+ resolveRuntimeLayout,
46
+ verifyInstalledRuntime,
47
+ } from "./herdr-runtime.mjs";
48
+
49
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
50
+
51
+ /** The one name this verb links, and the floor a receipt requires. */
52
+ const LINK_NAME = "entwurf";
53
+ const LEDGER_FLOOR_VERSION = 3;
54
+
55
+ export const OPERATOR_LINK_SCHEMA_VERSION = 1;
56
+ export const OPERATOR_LINK_KEYS = Object.freeze([
57
+ "schemaVersion",
58
+ "phase",
59
+ "binDir",
60
+ "linkPath",
61
+ "target",
62
+ "runtimeRoot",
63
+ ]);
64
+ export const OPERATOR_LINK_PHASES = Object.freeze(["linking", "linked", "unlinking"]);
65
+
66
+ function refuse(code, detail) {
67
+ throw new ActivationError(code, detail);
68
+ }
69
+
70
+ /** The receipt's fixed address: the ledger's own state root, its own file. */
71
+ export function resolveOperatorLinkLayout(env) {
72
+ const { stateRoot } = resolveActivationLayout(env);
73
+ return Object.freeze({ stateRoot, receiptPath: path.join(stateRoot, "operator-link.json") });
74
+ }
75
+
76
+ /** What the link points at: the stable runtime's own `entwurf`, lexically — never a resolved path. */
77
+ export function operatorLinkTarget(activeDir) {
78
+ return path.join(activeDir, "node_modules", ".bin", LINK_NAME);
79
+ }
80
+
81
+ /** The receipt, certified, or null when none stands at its address. */
82
+ export function readCertifiedOperatorLink(layout, activeDir) {
83
+ const kind = fs.lstatSync(layout.receiptPath, { throwIfNoEntry: false });
84
+ if (kind === undefined) return null;
85
+ const uncertified = (why) => refuse("operator-link-receipt-uncertified", `${layout.receiptPath}: ${why}`);
86
+ if (!kind.isFile()) uncertified("only a regular file may stand at the receipt address");
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(fs.readFileSync(layout.receiptPath, "utf8"));
90
+ } catch (err) {
91
+ uncertified(err.message);
92
+ }
93
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) uncertified("not a JSON object");
94
+ if (Object.keys(parsed).sort().join(",") !== [...OPERATOR_LINK_KEYS].sort().join(",")) {
95
+ uncertified(`key set: ${Object.keys(parsed).sort()}`);
96
+ }
97
+ if (parsed.schemaVersion !== OPERATOR_LINK_SCHEMA_VERSION) uncertified(`schemaVersion ${parsed.schemaVersion}`);
98
+ if (!OPERATOR_LINK_PHASES.includes(parsed.phase)) uncertified(`phase ${JSON.stringify(parsed.phase)}`);
99
+ for (const key of ["binDir", "linkPath", "target", "runtimeRoot"]) {
100
+ if (typeof parsed[key] !== "string" || !path.isAbsolute(parsed[key])) uncertified(`${key} is not an absolute path`);
101
+ }
102
+ if (parsed.linkPath !== path.join(parsed.binDir, LINK_NAME)) uncertified("linkPath is not <binDir>/entwurf");
103
+ if (parsed.target !== operatorLinkTarget(activeDir)) uncertified(`target is not ${operatorLinkTarget(activeDir)}`);
104
+ if (parsed.runtimeRoot !== activeDir) uncertified(`runtimeRoot is not this host's stable root ${activeDir}`);
105
+ return Object.freeze(parsed);
106
+ }
107
+
108
+ /** Receipts are replaced, never patched: write beside, then rename. */
109
+ function writeOperatorLink(layout, body) {
110
+ fs.mkdirSync(layout.stateRoot, { recursive: true });
111
+ const tmp = `${layout.receiptPath}.tmp`;
112
+ fs.writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`);
113
+ fs.renameSync(tmp, layout.receiptPath);
114
+ }
115
+
116
+ /**
117
+ * A receipt, in any phase, belongs to a certified schema-3 ledger for this runtime and these roots.
118
+ * Anything else is refused before a byte is written — it is never adopted, and the floor is never
119
+ * raised to make it fit.
120
+ */
121
+ function certifyReceiptBinding(receipt, ledger, roots, activeDir) {
122
+ if (receipt === null) return;
123
+ if (ledger === null) refuse("operator-link-orphan", "an operator-link receipt stands with no activation ledger");
124
+ if (ledger.schemaVersion !== LEDGER_FLOOR_VERSION)
125
+ refuse("operator-link-floor-mismatch", `the ledger is schema ${ledger.schemaVersion}, not ${LEDGER_FLOOR_VERSION}`);
126
+ certifyRootsAgainstLedger(ledger, roots, activeDir);
127
+ }
128
+
129
+ /** `real` (a physical directory that is its own realpath), `gone`, or `alias`. */
130
+ function assessBinDir(binDir) {
131
+ const kind = fs.lstatSync(binDir, { throwIfNoEntry: false });
132
+ if (kind === undefined) return "gone";
133
+ if (!kind.isDirectory()) return "alias";
134
+ if (fs.realpathSync(binDir) !== binDir) return "alias";
135
+ return "real";
136
+ }
137
+
138
+ /** `absent`, `ours` (a symlink whose text is exactly the target), or `foreign`. */
139
+ function assessLink(linkPath, target) {
140
+ const kind = fs.lstatSync(linkPath, { throwIfNoEntry: false });
141
+ if (kind === undefined) return "absent";
142
+ if (kind.isSymbolicLink() && fs.readlinkSync(linkPath) === target) return "ours";
143
+ return "foreign";
144
+ }
145
+
146
+ function isExecutableFile(file) {
147
+ try {
148
+ if (fs.statSync(file).isDirectory()) return false;
149
+ fs.accessSync(file, fs.constants.X_OK);
150
+ return true;
151
+ } catch {
152
+ // Bounded probe: missing or not executable is simply "not a candidate".
153
+ return false;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Where B stands in THIS caller's PATH, judged by path alone — no program is run. An empty entry is
159
+ * the cwd and a relative one is relative to it, which is why the run.sh arm does not cd. Aliases,
160
+ * functions, the shell's hash table and other shells' PATHs are outside this judgement.
161
+ */
162
+ function pathSelection(binDir, { PATH, cwd }) {
163
+ for (const entry of (PATH ?? "").split(path.delimiter)) {
164
+ const dir = path.resolve(cwd, entry === "" ? "." : entry);
165
+ if (dir === binDir) return { onPath: true, shadowedBy: null };
166
+ const candidate = path.join(dir, LINK_NAME);
167
+ if (isExecutableFile(candidate)) return { onPath: false, shadowedBy: candidate };
168
+ }
169
+ return { onPath: false, shadowedBy: null };
170
+ }
171
+
172
+ /** A public verb runs only as the active runtime's own package — never from a checkout or a copy. */
173
+ function admitSelf(runtime, verb) {
174
+ let same = false;
175
+ try {
176
+ same =
177
+ fs.realpathSync(PACKAGE_ROOT) ===
178
+ fs.realpathSync(path.join(runtime.activeDir, "node_modules", "@junghanacs", "entwurf"));
179
+ } catch {
180
+ // Bounded probe: no package at the stable address means this is not it.
181
+ same = false;
182
+ }
183
+ if (!same) {
184
+ refuse(
185
+ "operator-link-not-active-package",
186
+ `${verb} runs only from the Herdr runtime's own package (${runtime.activeDir}); this is ${PACKAGE_ROOT}`,
187
+ );
188
+ }
189
+ }
190
+
191
+ function usage(detail) {
192
+ refuse(
193
+ "operator-link-usage",
194
+ `${detail} — usage: herdr-plugin-expose-operator --bin-dir <absolute dir> | herdr-plugin-withdraw-operator`,
195
+ );
196
+ }
197
+
198
+ /** Exactly `--bin-dir <absolute>`; nothing else, in no other order. */
199
+ function parseExposeArgv(argv) {
200
+ if (argv.length !== 2 || argv[0] !== "--bin-dir")
201
+ usage(`expose takes exactly --bin-dir <dir>, got ${JSON.stringify(argv)}`);
202
+ if (!path.isAbsolute(argv[1])) usage(`--bin-dir must be absolute, got ${JSON.stringify(argv[1])}`);
203
+ return argv[1];
204
+ }
205
+
206
+ /**
207
+ * Make the one link, or say by name why not. Every judgement comes before the first write.
208
+ *
209
+ * @returns a one-line outcome for the operator.
210
+ */
211
+ export function expose(env, argv, { cwd }) {
212
+ const binDir = parseExposeArgv(argv);
213
+ const runtime = resolveRuntimeLayout(env);
214
+ const activation = resolveActivationLayout(env);
215
+ const roots = resolveComponentRoots(env);
216
+ const layout = resolveOperatorLinkLayout(env);
217
+ admitSelf(runtime, "expose");
218
+
219
+ const journal = readCertifiedJournal(runtime);
220
+ if (journal === null || journal.phase !== "runtime-ready") {
221
+ refuse("operator-link-runtime-not-ready", `${runtime.activeDir} has no certified runtime-ready journal behind it`);
222
+ }
223
+ verifyInstalledRuntime(runtime.activeDir, artifactCompleteness(journal.artifactIdentity));
224
+ const ledger = readCertifiedLedger(activation);
225
+ if (ledger === null || ledger.phase !== "active") {
226
+ refuse(
227
+ "operator-link-activation-not-settled",
228
+ `the activation ledger is ${ledger === null ? "absent" : ledger.phase}, not active`,
229
+ );
230
+ }
231
+ certifyRootsAgainstLedger(ledger, roots, runtime.activeDir);
232
+ certifyLedgerDescribesRuntime(ledger, runtimeIdentityOnDisk(journal));
233
+ if (
234
+ fs.lstatSync(runtime.stagingDir, { throwIfNoEntry: false }) ||
235
+ fs.lstatSync(runtime.previousDir, { throwIfNoEntry: false })
236
+ ) {
237
+ refuse(
238
+ "operator-link-runtime-not-settled",
239
+ "a staged or backed-up runtime stands beside the active one; finish or recover that install first",
240
+ );
241
+ }
242
+ const target = operatorLinkTarget(runtime.activeDir);
243
+ if (!isExecutableFile(target)) refuse("operator-link-target-missing", `${target} is not an executable file`);
244
+ if (assessBinDir(binDir) !== "real") {
245
+ refuse("operator-link-bin-dir-refused", `${binDir} is not an existing physical directory that is its own realpath`);
246
+ }
247
+ const selection = pathSelection(binDir, { PATH: env.PATH, cwd });
248
+ if (selection.shadowedBy !== null) {
249
+ refuse(
250
+ "operator-link-path-shadowed",
251
+ `${selection.shadowedBy} comes before ${binDir} on PATH; a link there would not be the one that runs`,
252
+ );
253
+ }
254
+ if (!selection.onPath) refuse("operator-link-bin-dir-off-path", `${binDir} is not an entry of the current PATH`);
255
+
256
+ const linkPath = path.join(binDir, LINK_NAME);
257
+ const receipt = readCertifiedOperatorLink(layout, runtime.activeDir);
258
+ certifyReceiptBinding(receipt, ledger, roots, runtime.activeDir);
259
+ const link = assessLink(linkPath, target);
260
+ const body = (phase) => ({
261
+ schemaVersion: OPERATOR_LINK_SCHEMA_VERSION,
262
+ phase,
263
+ binDir,
264
+ linkPath,
265
+ target,
266
+ runtimeRoot: runtime.activeDir,
267
+ });
268
+
269
+ if (receipt === null) {
270
+ if (link !== "absent")
271
+ refuse("operator-link-foreign", `${linkPath} exists and no receipt says it is ours; remove it yourself if it is`);
272
+ const { schemaVersion, ...entry } = ledger;
273
+ if (schemaVersion !== LEDGER_FLOOR_VERSION) writeLedger(activation, LEDGER_FLOOR_VERSION, entry);
274
+ writeOperatorLink(layout, body("linking"));
275
+ fs.symlinkSync(target, linkPath);
276
+ writeOperatorLink(layout, body("linked"));
277
+ return `linked ${linkPath} -> ${target}`;
278
+ }
279
+ if (receipt.binDir !== binDir) {
280
+ refuse(
281
+ "operator-link-other-bin-dir",
282
+ `the operator link already lives in ${receipt.binDir}; withdraw it before asking for ${binDir}`,
283
+ );
284
+ }
285
+ if (receipt.phase === "unlinking")
286
+ refuse("operator-link-unlinking", "a withdraw is half done; run herdr-plugin-withdraw-operator to finish it");
287
+ if (link === "foreign") refuse("operator-link-foreign", `${linkPath} is no longer the link the receipt names`);
288
+ if (receipt.phase === "linked") {
289
+ if (link === "absent")
290
+ refuse("operator-link-drift", `the receipt says ${linkPath} is linked but it is gone; withdraw, then ask again`);
291
+ return `already linked ${linkPath} -> ${target}`;
292
+ }
293
+ // `linking`: a run stopped after recording its intent. Our exact link is published as linked
294
+ // without being made again; an absent one is made now.
295
+ if (link === "absent") fs.symlinkSync(target, linkPath);
296
+ writeOperatorLink(layout, body("linked"));
297
+ return `linked ${linkPath} -> ${target} (resumed)`;
298
+ }
299
+
300
+ /**
301
+ * Before deactivate's first write: the receipt (or null) and every judgement its removal needs.
302
+ * No self admission here — deactivate's own tail must stay resumable after the runtime is gone.
303
+ */
304
+ export function preflightOperatorLink(env, ledger, roots, activeDir) {
305
+ const receipt = readCertifiedOperatorLink(resolveOperatorLinkLayout(env), activeDir);
306
+ if (receipt === null) return null;
307
+ certifyReceiptBinding(receipt, ledger, roots, activeDir);
308
+ assessForRemoval(receipt);
309
+ return receipt;
310
+ }
311
+
312
+ /** `ours` or `absent` (B gone counts as absent); a present B that drifted, or a foreign link, refuses. */
313
+ function assessForRemoval(receipt) {
314
+ const bin = assessBinDir(receipt.binDir);
315
+ if (bin === "alias")
316
+ refuse(
317
+ "operator-link-bin-dir-drifted",
318
+ `${receipt.binDir} is no longer a physical directory that is its own realpath`,
319
+ );
320
+ const link = bin === "gone" ? "absent" : assessLink(receipt.linkPath, receipt.target);
321
+ if (link === "foreign")
322
+ refuse("operator-link-foreign", `${receipt.linkPath} is not the link the receipt names; nothing was removed`);
323
+ return link;
324
+ }
325
+
326
+ /** Remove exactly the receipt's link, then the receipt. Shared by withdraw and deactivate. */
327
+ export function removeOperatorLink(env, receipt) {
328
+ const layout = resolveOperatorLinkLayout(env);
329
+ assessForRemoval(receipt);
330
+ writeOperatorLink(layout, { ...receipt, phase: "unlinking" });
331
+ if (assessForRemoval(receipt) === "ours") fs.unlinkSync(receipt.linkPath);
332
+ fs.rmSync(layout.receiptPath, { force: true });
333
+ }
334
+
335
+ /** The opt-out. The ledger floor stays where it is. */
336
+ export function withdraw(env, argv) {
337
+ if (argv.length !== 0) usage(`withdraw takes no arguments, got ${JSON.stringify(argv)}`);
338
+ const runtime = resolveRuntimeLayout(env);
339
+ admitSelf(runtime, "withdraw");
340
+ const receipt = readCertifiedOperatorLink(resolveOperatorLinkLayout(env), runtime.activeDir);
341
+ if (receipt === null) return "no operator link";
342
+ const ledger = readCertifiedLedger(resolveActivationLayout(env));
343
+ certifyReceiptBinding(receipt, ledger, resolveComponentRoots(env), runtime.activeDir);
344
+ certifyPhaseForInverse(ledger);
345
+ removeOperatorLink(env, receipt);
346
+ return `withdrew ${receipt.linkPath}`;
347
+ }
348
+
349
+ /** Without a ledger: what stands at the receipt address, and — only from a certified receipt — its link. */
350
+ export function operatorLinkLeftovers(env) {
351
+ const layout = resolveOperatorLinkLayout(env);
352
+ const kind = fs.lstatSync(layout.receiptPath, { throwIfNoEntry: false });
353
+ if (kind === undefined) return [];
354
+ const found = [`operator-link-receipt (${layout.receiptPath})`];
355
+ let receipt = null;
356
+ try {
357
+ receipt = readCertifiedOperatorLink(layout, resolveRuntimeLayout(env).activeDir);
358
+ } catch {
359
+ // A damaged receipt names only its own fixed address; no link address is trusted from it.
360
+ receipt = null;
361
+ }
362
+ if (receipt !== null && fs.lstatSync(receipt.linkPath, { throwIfNoEntry: false }) !== undefined) {
363
+ found.push(`operator-link (${receipt.linkPath})`);
364
+ }
365
+ return found;
366
+ }
367
+
368
+ function sameFile(a, b) {
369
+ try {
370
+ return fs.realpathSync(a) === fs.realpathSync(b);
371
+ } catch {
372
+ return false;
373
+ }
374
+ }
375
+
376
+ if (process.argv[1] && sameFile(process.argv[1], fileURLToPath(import.meta.url))) {
377
+ const [verb, ...rest] = process.argv.slice(2);
378
+ try {
379
+ let outcome;
380
+ if (verb === "expose") outcome = expose(process.env, rest, { cwd: process.cwd() });
381
+ else if (verb === "withdraw") outcome = withdraw(process.env, rest);
382
+ else usage(`unknown verb ${JSON.stringify(verb)}`);
383
+ process.stdout.write(`[herdr-operator-link] ${outcome}\n`);
384
+ process.exit(0);
385
+ } catch (err) {
386
+ process.stderr.write(`[herdr-operator-link] ${err.code ?? "operator-link-failed"}: ${err.detail ?? err.message}\n`);
387
+ process.exit(err.code === "operator-link-usage" ? 2 : 1);
388
+ }
389
+ }
@@ -32,6 +32,7 @@ import {
32
32
  certifyPhaseForForward,
33
33
  certifyRootsAgainstLedger,
34
34
  componentStates,
35
+ FRESH_ACTIVATION_SCHEMA_VERSION,
35
36
  ledgerBody,
36
37
  piStatePaths,
37
38
  planActivation,
@@ -248,9 +249,13 @@ export function activate(env, requested) {
248
249
  for (const backend of selected) {
249
250
  if (disposition === "rebind" || states[backend] !== "active") states[backend] = "pending";
250
251
  }
252
+ // The floor this host already carries is kept; only a ledger that does not exist yet is born at
253
+ // the fresh version. Activation neither raises nor lowers it.
254
+ const version = ledger?.schemaVersion ?? FRESH_ACTIVATION_SCHEMA_VERSION;
251
255
  const checkpoint = (phase) =>
252
256
  writeLedger(
253
257
  activation,
258
+ version,
254
259
  ledgerBody({ phase, runtimeRoot: runtime.activeDir, artifactIdentity: current, roots, states }),
255
260
  );
256
261
  checkpoint("activating");
@@ -13,8 +13,10 @@
13
13
  * ledger unretired, and nothing left to run the rest. So nothing here is loaded late, and the gate
14
14
  * asserts there is no dynamic import to defer.
15
15
  *
16
- * PREFLIGHT EVERY AUTHORITY, THEN MUTATE. The ledger, the resolved roots, and BOTH component
17
- * inverses are asked read-only first. Only when all of them are green does the first byte change.
16
+ * PREFLIGHT, THEN MUTATE. The ledger, the resolved roots, the operator link and the inverse of every
17
+ * component still left to undo are asked read-only first; only when all of them are green does the
18
+ * first byte change. Runtime ownership is not part of that preflight: `removeOwnedRuntime` judges it
19
+ * at its own step, after the components, and a refusal there stops the run with the ledger kept.
18
20
  * A component that then fails stops the run with the runtime and the ledger intact, because the
19
21
  * ledger is the retry authority and a teardown that deletes its own record of what is left is one
20
22
  * nobody can resume.
@@ -37,6 +39,7 @@ import {
37
39
  resolveComponentRoots,
38
40
  writeLedger,
39
41
  } from "./herdr-activation.mjs";
42
+ import { operatorLinkLeftovers, preflightOperatorLink, removeOperatorLink } from "./herdr-operator-link.mjs";
40
43
  import { removeOwnedRuntime, resolveRuntimeLayout } from "./herdr-runtime.mjs";
41
44
 
42
45
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -86,7 +89,7 @@ export function deactivate(env) {
86
89
  // recorded leaves a journal, a runtime or a cache behind, and saying "nothing to undo" over them
87
90
  // was a false success. Without a ledger nothing proves what they belong to or that no harness
88
91
  // wiring names them, so this verb removes nothing: it names what it found and exits non-zero.
89
- const leftovers = leftoversAtPluginAddresses(runtime);
92
+ const leftovers = [...leftoversAtPluginAddresses(runtime), ...operatorLinkLeftovers(env)];
90
93
  if (leftovers.length === 0) {
91
94
  process.stdout.write("[herdr-plugin-deactivate] no certified activation ledger — nothing to undo\n");
92
95
  return 0;
@@ -100,6 +103,9 @@ export function deactivate(env) {
100
103
  }
101
104
  certifyRootsAgainstLedger(ledger, roots, runtime.activeDir);
102
105
  certifyPhaseForInverse(ledger);
106
+ // The operator link (#153) is judged with everything else, before the first write: a link that is
107
+ // no longer the receipt's, or a receipt this ledger does not carry, stops the whole teardown here.
108
+ const operatorReceipt = preflightOperatorLink(env, ledger, roots, runtime.activeDir);
103
109
  // On a RETRY the plan is what is LEFT: a component already recorded `removed` is skipped, because
104
110
  // its inverse has no ownership state to be admitted by any more and re-running it is a permanent
105
111
  // refusal — which is exactly how a half-finished teardown used to become unfinishable.
@@ -107,7 +113,7 @@ export function deactivate(env) {
107
113
  const states = componentStates(ledger);
108
114
  const piState = piStatePaths(env);
109
115
 
110
- // ── PREFLIGHT: every remaining authority, zero writes ────────────────────
116
+ // ── PREFLIGHT: every remaining component inverse, zero writes ────────────
111
117
  const preflights = [];
112
118
  if (steps.includes("claude-code")) {
113
119
  preflights.push([
@@ -176,6 +182,7 @@ export function deactivate(env) {
176
182
  const checkpoint = () =>
177
183
  writeLedger(
178
184
  activation,
185
+ ledger.schemaVersion,
179
186
  ledgerBody({
180
187
  phase: "deactivating",
181
188
  runtimeRoot: runtime.activeDir,
@@ -187,6 +194,12 @@ export function deactivate(env) {
187
194
  checkpoint();
188
195
 
189
196
  const done = [];
197
+ // The link goes FIRST: it names the runtime, so it must be gone before the thing it names. Its
198
+ // receipt is its own retry authority, so the ledger records nothing new for it.
199
+ if (operatorReceipt !== null) {
200
+ removeOperatorLink(env, operatorReceipt);
201
+ done.push("operator-link");
202
+ }
190
203
  for (const step of steps) {
191
204
  if (step === "claude-code" || step === "pi") {
192
205
  const verb = step === "pi" ? "remove-user-scope" : "uninstall-meta-bridge";
@@ -195,7 +208,7 @@ export function deactivate(env) {
195
208
  process.stderr.write(`${(r.stderr || r.stdout || "").trim()}\n`);
196
209
  fail(
197
210
  "deactivate-component-failed",
198
- `${step} inverse failed — runtime and ledger preserved as retry authority (${done.length} component(s) already removed and recorded)`,
211
+ `${step} inverse failed — runtime and ledger preserved as retry authority (${done.filter((s) => s === "pi" || s === "claude-code").length} component(s) already removed and recorded)`,
199
212
  );
200
213
  }
201
214
  states[step] = "removed";
@@ -31,17 +31,44 @@
31
31
  */
32
32
 
33
33
  /**
34
- * The closed pilot set for the herdr launch rail. Herdr's own `--kind` enum is much larger and
35
- * that is not evidence of support: every other backend is a named pre-mutation reject.
34
+ * The backends the herdr rail opens through `tab create` + `agent start --kind`. Herdr's own
35
+ * `--kind` enum is much larger and that is not evidence of support. This set alone keys the
36
+ * agent-start maps (`HERDR_AGENT_KIND`, `HERDR_CANONICAL_EXECUTABLE`): a backend outside it has no
37
+ * herdr kind, and giving it one would start a different program under its name.
36
38
  *
37
39
  * The JSDoc cast is what keeps the TYPE half of this single source honest. Without it TypeScript
38
- * infers `readonly string[]` from a `.mjs` file and `HerdrFreshCallBackend` silently widens to
40
+ * infers `readonly string[]` from a `.mjs` file and `HerdrAgentStartBackend` silently widens to
39
41
  * `string` — the tuple would still be one source at runtime while the compiler had stopped
40
42
  * checking anything. A gate pins both halves.
41
43
  *
42
44
  * @type {readonly ["pi", "claude-code"]}
43
45
  */
44
- export const HERDR_FRESH_CALL_BACKENDS = Object.freeze(["pi", "claude-code"]);
46
+ export const HERDR_AGENT_START_BACKENDS = Object.freeze(["pi", "claude-code"]);
47
+
48
+ /**
49
+ * The backends the herdr rail opens through ONE fixed `layout.apply` request on the caller's own
50
+ * herdr socket (#157 A-L1). pi-durable has no herdr agent kind, so it is launched as the package's
51
+ * own verified `run.sh pi-durable …` argv vector in a new tab — never disguised as `--kind pi`.
52
+ *
53
+ * @type {readonly ["pi-durable"]}
54
+ */
55
+ export const HERDR_LAYOUT_LAUNCH_BACKENDS = Object.freeze(["pi-durable"]);
56
+
57
+ /**
58
+ * Everything `entwurf_fresh_call` actually opens inside herdr: the union of the two launch domains.
59
+ * Admission and refusal read THIS set, so every backend outside it is a named pre-mutation reject
60
+ * and no backend is admitted by one domain while another surface says it is refused.
61
+ *
62
+ * The status pane's RAILS block renders the two domains as separate groups — agent start, then
63
+ * layout — over this aggregate (#157 A-L2). The install narration covers only the integrations it
64
+ * activated, and pi-durable is never one: it has no herdr agent integration to activate.
65
+ *
66
+ * @type {readonly ["pi", "claude-code", "pi-durable"]}
67
+ */
68
+ export const HERDR_FRESH_CALL_BACKENDS = Object.freeze([
69
+ ...HERDR_AGENT_START_BACKENDS,
70
+ ...HERDR_LAYOUT_LAUNCH_BACKENDS,
71
+ ]);
45
72
 
46
73
  /**
47
74
  * ONE model string per backend, and it is an example of SYNTAX, never of choice.
@@ -49,13 +76,16 @@ export const HERDR_FRESH_CALL_BACKENDS = Object.freeze(["pi", "claude-code"]);
49
76
  * `[관측: GLG, 날것 PC, 2026-09-17]` "일단 정확한 모델명을 모른다" — after a green install the first
50
77
  * thing missing is a call that works, and the two runtimes spell a model differently enough that
51
78
  * guessing fails: pi wants a provider-qualified path, Claude Code wants the vendor id alone.
52
- * That difference is the whole content of this map.
79
+ * That difference is the whole content of this map. pi-durable takes pi's shape — its preflight
80
+ * refuses a model that is not provider-qualified (`pi-durable-model-not-provider-qualified`) — so its
81
+ * example is the same shape string, not a choice of model.
53
82
  *
54
- * @type {Readonly<Record<"pi" | "claude-code", string>>}
83
+ * @type {Readonly<Record<"pi" | "claude-code" | "pi-durable", string>>}
55
84
  */
56
85
  export const MODEL_SYNTAX_EXAMPLE = Object.freeze({
57
86
  pi: "openai-codex/gpt-5.6-terra",
58
87
  "claude-code": "claude-sonnet-5",
88
+ "pi-durable": "openai-codex/gpt-5.6-terra",
59
89
  });
60
90
 
61
91
  /**
@@ -67,9 +97,15 @@ export const MODEL_SYNTAX_EXAMPLE = Object.freeze({
67
97
  * citizen pi: the install says green, `pi` starts, and nothing is there. Citizenship is argv-gated
68
98
  * on purpose, and that gate is invisible unless something says it out loud.
69
99
  *
70
- * @type {Readonly<Record<"pi" | "claude-code", string>>}
100
+ * pi-durable's note says what it is NOT here as much as how it is reached: it is not an integration
101
+ * this plugin activates, and herdr has no agent kind for it, so nothing detects it or associates a
102
+ * session with it.
103
+ *
104
+ * @type {Readonly<Record<"pi" | "claude-code" | "pi-durable", string>>}
71
105
  */
72
106
  export const RAIL_ENTRY_NOTE = Object.freeze({
73
107
  pi: "start it as `pi --entwurf-control` to be a garden citizen — a plain `pi` loads the extension but has no garden id, no control socket and no entwurf tools",
74
108
  "claude-code": "nothing to add — an ordinary `claude` picks up the entwurf tools through MCP",
109
+ "pi-durable":
110
+ "not a Herdr integration this plugin activates — inside Herdr, `entwurf_fresh_call` opens it in a new tab through ONE layout request; Herdr has no agent kind for it, so it is neither detected nor associated with a session, and its peers placement reads unobserved",
75
111
  });
@@ -998,7 +998,18 @@ export function bootstrapRuntime({
998
998
  checkoutRoot = defaultCheckoutRoot(),
999
999
  acquire = acquireArtifact,
1000
1000
  resolveCommit = resolveCheckoutCommit,
1001
+ admitCandidate,
1001
1002
  }) {
1003
+ // ADMISSION (#153) is an optional, SYNCHRONOUS observation of the verified candidate, made before
1004
+ // the first rename: return nothing to admit it, throw to refuse it. This module never decides what
1005
+ // is asked — the caller that owns the question supplies it — so it imports nothing to ask it with.
1006
+ // A value that cannot be called is refused here, before the first byte.
1007
+ if (admitCandidate !== undefined && typeof admitCandidate !== "function") {
1008
+ throw new RuntimeBootstrapError(
1009
+ "runtime-candidate-admission-invalid",
1010
+ `admitCandidate is ${typeof admitCandidate}; it is a synchronous function or absent`,
1011
+ );
1012
+ }
1002
1013
  const requested = requestedArtifactIdentity({ lock, checkoutRoot, resolveCommit });
1003
1014
  const layout = resolveRuntimeLayout(env);
1004
1015
 
@@ -1114,6 +1125,22 @@ export function bootstrapRuntime({
1114
1125
  fs.mkdirSync(layout.stagingDir, { recursive: true });
1115
1126
  acquired = acquire({ identity: requested, prefix: layout.stagingDir, cacheDir: layout.cacheDir, env });
1116
1127
  verifyInstalledRuntime(layout.stagingDir, completenessOf(requested, acquired));
1128
+ // Inside this try, so a refused candidate is reclaimed exactly like a failed one: staging and
1129
+ // cache go, the active runtime and the `installing` journal (with its carried provenance) stay.
1130
+ // The same-artifact early return above never reaches this line, so it runs no probe. A
1131
+ // recovery's own move (restoring or discarding a tree) happens before it and is not observed;
1132
+ // a new candidate staged afterwards reaches this line like any other.
1133
+ if (admitCandidate !== undefined) {
1134
+ const verdict = admitCandidate(
1135
+ path.join(layout.stagingDir, "node_modules", completenessOf(requested, acquired).name),
1136
+ );
1137
+ if (verdict !== undefined) {
1138
+ throw new RuntimeBootstrapError(
1139
+ "runtime-candidate-admission-invalid",
1140
+ `admitCandidate returned ${typeof verdict}; admission is synchronous: return nothing to admit, throw to refuse`,
1141
+ );
1142
+ }
1143
+ }
1117
1144
  } catch (err) {
1118
1145
  removeTree(layout.stagingDir);
1119
1146
  // A half-fetched tarball left in the cache is the thing a retry would trip over: npm's own
@@ -127,33 +127,7 @@ import { probeSocketLiveness } from "../pi-extensions/lib/socket-probe.ts";
127
127
 
128
128
  function usage(code: number): never {
129
129
  console.error(
130
- [
131
- "usage: node --experimental-strip-types scripts/meta-bridge-fresh-cut.ts",
132
- " (dev clone: `./run.sh meta-bridge-fresh-cut` · installed package: `entwurf meta-bridge-fresh-cut`)",
133
- "",
134
- " Open a fresh meta-bridge generation: quiesce-check every live surface, then",
135
- " archive meta-sessions/ + meta-mailbox/ to `<dir>.archive-<timestamp>` siblings,",
136
- " clear dead sender/receiver markers and dead control sockets, and open empty",
137
- " live dirs. No migration, no restore: the archive is forensic bytes only.",
138
- "",
139
- "store = ENTWURF_META_SESSIONS_DIR || <PI_CODING_AGENT_DIR|~/.pi/agent>/meta-sessions",
140
- "mailbox = ENTWURF_META_MAILBOX_DIR || <PI_CODING_AGENT_DIR|~/.pi/agent>/meta-mailbox",
141
- "sockets = ENTWURF_DIR || ~/.pi/entwurf-control",
142
- "",
143
- "EXIT CONTRACT (#54) — what already moved, not how bad it was:",
144
- ` ${FRESH_CUT_EXIT.COMPLETE} cut complete: generation archived (or none existed), fresh v3 store open,`,
145
- " residue cleared. `&& setup` is correct here.",
146
- ` ${FRESH_CUT_EXIT.NO_MOVE} NOTHING MOVED: a live/unprovable surface, an occupied archive destination,`,
147
- " an unreadable surface, or a first rename that failed. The host is unchanged —",
148
- " fix the named cause and re-run. Do NOT run setup: the store it refused is still there.",
149
- ` ${FRESH_CUT_EXIT.USAGE} usage.`,
150
- ` ${FRESH_CUT_EXIT.HALF_CUT} CUT TRANSITION INCOMPLETE: at least one archive move happened,`,
151
- " but the fresh generation is not confirmed open. Inspect, or re-run to finish under a new stamp.",
152
- ` ${FRESH_CUT_EXIT.CLEANUP_INCOMPLETE} CUT COMPLETE, CLEANUP INCOMPLETE: the generation IS archived and the fresh`,
153
- " generation IS open — install and citizen birth are unblocked, so setup may run —",
154
- " but marker/socket residue survived. Prefer fixing it and re-running BEFORE setup; after new citizen birth,",
155
- " remove the named residue manually instead of archiving the new generation with another cut.",
156
- ].join("\n"),
130
+ "meta-bridge-fresh-cut: see `entwurf help meta-bridge-fresh-cut` (from that checkout's root: `./run.sh help meta-bridge-fresh-cut`)",
157
131
  );
158
132
  process.exit(code);
159
133
  }
@@ -31,7 +31,7 @@ function shellQuote(s: string): string {
31
31
 
32
32
  function usage(code: number): never {
33
33
  console.error(
34
- "usage: node --experimental-strip-types scripts/meta-bridge-prune.ts [meta-sessions-dir] [--ttl-days N]",
34
+ "meta-bridge-prune: see `entwurf help meta-bridge-prune` (from that checkout's root: `./run.sh help meta-bridge-prune`)",
35
35
  );
36
36
  process.exit(code);
37
37
  }
@@ -39,7 +39,7 @@ const arg = process.argv[2];
39
39
  // A dash argv is a flag this command does not have, not a store directory — treating
40
40
  // it as a path would answer `--help` with "empty store, exit 0", a silent wrong fact.
41
41
  if (process.argv.length > 3 || (arg !== undefined && arg.startsWith("-"))) {
42
- console.error("usage: entwurf meta-facts [meta-sessions-dir]");
42
+ console.error("meta-facts: see `entwurf help meta-facts` (from that checkout's root: `./run.sh help meta-facts`)");
43
43
  process.exit(2);
44
44
  }
45
45