@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.
- package/AGENTS.md +4 -2
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +328 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +284 -0
- package/FAQ.md +4 -4
- package/README.md +125 -184
- package/VERIFY.md +2 -2
- package/docs/external-mcp-host.md +3 -3
- package/docs/setup-clean-host.md +37 -12
- package/mcp/entwurf-bridge/dist/ENTWURF.json +284 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +46 -196
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +21 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +498 -29
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +23 -20
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +68 -21
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +53 -60
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +149 -327
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +8 -4
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +108 -9
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/overlay/upstream-pin.json +11 -5
- package/pi-extensions/lib/agent-guidance.ts +341 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +25 -31
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +725 -54
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-fresh-call.ts +23 -26
- package/pi-extensions/lib/mux-resume-call.ts +95 -23
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/remote-pair-send.ts +7 -4
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +52 -79
- package/run.sh +417 -654
- package/scripts/build-bridge.sh +10 -0
- package/scripts/check-capability-bundle-reach.ts +86 -0
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-self-address.ts +3 -3
- package/scripts/check-entwurf-v2-surface.ts +94 -98
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +24 -17
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +48 -3
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +160 -0
- package/scripts/mutants/bridge-boot-resume.json +6 -8
- package/scripts/mutants/capability-bundle-reach.json +124 -0
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +21 -4
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-fresh-call.json +14 -3
- package/scripts/mutants/mux-resume-call.json +89 -3
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +996 -6
- package/scripts/mutants/remote-pair-send.json +21 -2
- package/scripts/mutants/self-address.json +10 -10
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +50 -64
- package/scripts/mutants/v2-visible-resume.json +266 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- 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
|
|
17
|
-
*
|
|
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
|
|
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";
|
package/scripts/herdr-rails.mjs
CHANGED
|
@@ -31,17 +31,44 @@
|
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* The
|
|
35
|
-
* that is not evidence of support
|
|
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 `
|
|
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
|
|
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
|
-
*
|
|
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
|
-
"
|
|
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
|
}
|
package/scripts/meta-facts.ts
CHANGED
|
@@ -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("
|
|
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
|
|