johns-harness 2026.9.17 → 2026.9.18
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/README.md +3 -3
- package/dist/gateway-cli-B7fBU7gD.js +6 -0
- package/dist/gateway-cli-BCNMwzNq.js +6 -0
- package/dist/runtime-integrity.js +53 -0
- package/dist/runtime-path.js +9 -3
- package/dist/update-cli-Cm_l3W29.js +2 -0
- package/dist/update-command.js +30 -6
- package/dist/volume-update.js +208 -0
- package/docs/PATCHES.md +59 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,8 +73,8 @@ node johnness.mjs --help
|
|
|
73
73
|
### Verify
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
npm test #
|
|
77
|
-
bash scripts/verify-patches.sh #
|
|
76
|
+
npm test # 850 tests against the installed dependency tree
|
|
77
|
+
bash scripts/verify-patches.sh # 417 checks across every patch family
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
## Launch a swarm with one command
|
|
@@ -187,7 +187,7 @@ The log is a [`RULES.md`](RULES.md) workspace convention enforced through the ac
|
|
|
187
187
|
|
|
188
188
|
## Patch families
|
|
189
189
|
|
|
190
|
-
The runtime carries 65 patch families. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs
|
|
190
|
+
The runtime carries 65 patch families plus the 65.1 safe-update amendment. Each is a production fix applied at the source with a regression test, a durable patch marker, and a rollback path. A verifier runs 417 checks against the package tree and installed dependencies, and 850 tests run against the installed dependency tree.
|
|
191
191
|
|
|
192
192
|
Every family follows the same six steps: reproduce the failure, trace the exact runtime path, make the smallest source-level change that restores the invariant, add a regression test and a patch marker, run the verifier, and retain rollback artifacts. The full index: [`docs/PATCHES.md`](docs/PATCHES.md).
|
|
193
193
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
2
|
+
import { readInstalledVersion as readUpdatedRuntimeVersion } from "./update-command.js";
|
|
1
3
|
// JOHNNESS_PATCH_CATALOG_REFRESH_65
|
|
2
4
|
import { refreshJohnnessCatalog65 } from "./catalog-refresh.js";
|
|
3
5
|
import { g as resolveStateDir, i as isNixMode, l as resolveGatewayLockDir, o as resolveConfigPath, r as STATE_DIR, t as CONFIG_PATH, u as resolveGatewayPort } from "./paths-BJV7vkaX.js";
|
|
@@ -21754,6 +21756,10 @@ async function scheduleRestartSentinelWake(_params) {
|
|
|
21754
21756
|
const sentinel = await consumeRestartSentinel();
|
|
21755
21757
|
if (!sentinel) return;
|
|
21756
21758
|
const payload = sentinel.payload;
|
|
21759
|
+
if (payload.stats?.mode === "chat.update") {
|
|
21760
|
+
const running = readUpdatedRuntimeVersion();
|
|
21761
|
+
payload.message = running === payload.stats.targetVersion ? `johns-harness is now on ${running}. The new runtime has booted.` : `Update restart complete. Running johns-harness ${running}; requested ${payload.stats.targetVersion ?? "unknown"}.`;
|
|
21762
|
+
}
|
|
21757
21763
|
const sessionKey = payload.sessionKey?.trim();
|
|
21758
21764
|
const message = formatRestartSentinelMessage(payload);
|
|
21759
21765
|
const summary = summarizeRestartSentinel(payload);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
2
|
+
import { readInstalledVersion as readUpdatedRuntimeVersion } from "./update-command.js";
|
|
1
3
|
// JOHNNESS_PATCH_CATALOG_REFRESH_65
|
|
2
4
|
import { refreshJohnnessCatalog65 } from "./catalog-refresh.js";
|
|
3
5
|
import { a as logVerbose, d as colorize, f as isRich, g as getResolvedLoggerSettings, h as getLogger, m as getChildLogger, p as theme, s as setVerbose } from "./globals-Bv4ZcVWM.js";
|
|
@@ -21756,6 +21758,10 @@ async function scheduleRestartSentinelWake(_params) {
|
|
|
21756
21758
|
const sentinel = await consumeRestartSentinel();
|
|
21757
21759
|
if (!sentinel) return;
|
|
21758
21760
|
const payload = sentinel.payload;
|
|
21761
|
+
if (payload.stats?.mode === "chat.update") {
|
|
21762
|
+
const running = readUpdatedRuntimeVersion();
|
|
21763
|
+
payload.message = running === payload.stats.targetVersion ? `johns-harness is now on ${running}. The new runtime has booted.` : `Update restart complete. Running johns-harness ${running}; requested ${payload.stats.targetVersion ?? "unknown"}.`;
|
|
21764
|
+
}
|
|
21759
21765
|
const sessionKey = payload.sessionKey?.trim();
|
|
21760
21766
|
const message = formatRestartSentinelMessage(payload);
|
|
21761
21767
|
const summary = summarizeRestartSentinel(payload);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
export const RECEIPT = '.johnness-runtime-integrity.json';
|
|
6
|
+
export const packageRootForPrefix = (prefix, platform = process.platform) => path.join(prefix, platform === 'win32' ? '' : 'lib', 'node_modules', 'johns-harness');
|
|
7
|
+
const hash = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
8
|
+
const safeRelative = name => typeof name === 'string' && name.length > 0 && !path.isAbsolute(name) && !name.startsWith('node_modules/') && name !== 'node_modules' && !name.split(/[\\/]/).some(p => p === '..' || p === '');
|
|
9
|
+
export function inventoryPackage(root) {
|
|
10
|
+
const files = [];
|
|
11
|
+
function walk(dir) {
|
|
12
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) {
|
|
13
|
+
if ((e.name === 'node_modules' && dir === root) || e.name === '.git') continue;
|
|
14
|
+
const file = path.join(dir, e.name);
|
|
15
|
+
if (e.isSymbolicLink()) throw new Error('Package artifact contains a symlink');
|
|
16
|
+
if (e.isDirectory()) walk(file);
|
|
17
|
+
else if (e.isFile()) { const bytes = fs.readFileSync(file); files.push([path.relative(root, file).split(path.sep).join('/'), bytes.length, hash(bytes)]); }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
walk(root);
|
|
21
|
+
return files;
|
|
22
|
+
}
|
|
23
|
+
export function validateRuntimeTree(root, { receipt, expectedVersion, legacy = true } = {}) {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
26
|
+
if (pkg.name !== 'johns-harness' || !pkg.version || (expectedVersion && pkg.version !== expectedVersion)) throw new Error('package version mismatch');
|
|
27
|
+
for (const rel of ['johnness.mjs', 'dist/entry.js', 'extensions/telegram/index.ts']) {
|
|
28
|
+
const stat = fs.lstatSync(path.join(root, rel));
|
|
29
|
+
if (!stat.isFile() || stat.size === 0) throw new Error(`missing or empty entry: ${rel}`);
|
|
30
|
+
}
|
|
31
|
+
if (!receipt && !legacy) throw new Error('integrity receipt missing');
|
|
32
|
+
if (receipt) {
|
|
33
|
+
if (receipt.version !== pkg.version || !Array.isArray(receipt.files) || !receipt.files.length) throw new Error('invalid integrity receipt');
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const [rel, size, digest] of receipt.files) {
|
|
36
|
+
if (!safeRelative(rel) || seen.has(rel) || !Number.isSafeInteger(size) || size < 0 || !/^[a-f0-9]{64}$/.test(digest)) throw new Error('unsafe integrity entry');
|
|
37
|
+
seen.add(rel);
|
|
38
|
+
const stat = fs.lstatSync(path.join(root, rel));
|
|
39
|
+
if (!stat.isFile() || stat.size !== size) throw new Error(`incomplete artifact: ${rel}`);
|
|
40
|
+
if (hash(fs.readFileSync(path.join(root, rel))) !== digest) throw new Error(`artifact hash mismatch: ${rel}`);
|
|
41
|
+
}
|
|
42
|
+
for (const rel of ['package.json','johnness.mjs','dist/entry.js','extensions/telegram/index.ts']) if (!seen.has(rel)) throw new Error('incomplete receipt coverage');
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, version: pkg.version };
|
|
45
|
+
} catch (error) { return { ok: false, reason: error.code ?? error.message }; }
|
|
46
|
+
}
|
|
47
|
+
export function inspectPrefix(prefix, platform = process.platform) {
|
|
48
|
+
let receipt;
|
|
49
|
+
const receiptFile = path.join(prefix, RECEIPT);
|
|
50
|
+
try { if (fs.existsSync(receiptFile)) receipt = JSON.parse(fs.readFileSync(receiptFile, 'utf8')); }
|
|
51
|
+
catch { return { ok: false, reason: 'invalid integrity receipt' }; }
|
|
52
|
+
return validateRuntimeTree(packageRootForPrefix(prefix, platform), { receipt });
|
|
53
|
+
}
|
package/dist/runtime-path.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
3
|
+
import { inspectPrefix } from "./runtime-integrity.js";
|
|
2
4
|
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
3
5
|
// `johnness-runtime-path`: print the johnness executable an entrypoint should exec.
|
|
4
6
|
// Prefers the volume runtime installed by `/update` at
|
|
@@ -45,9 +47,13 @@ export function resolveRuntimePath({ env = process.env, imageRoot = path.resolve
|
|
|
45
47
|
const volumeRoot = path.join(prefix, platform === "win32" ? "" : "lib", "node_modules", PACKAGE);
|
|
46
48
|
const volumeBin = path.join(volumeRoot, "johnness.mjs");
|
|
47
49
|
const volumeVersion = readVersion(volumeRoot);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const candidates = [[prefix, "volume"], [prefix + ".previous", "previous"]];
|
|
51
|
+
for (const [candidate, source] of candidates) {
|
|
52
|
+
const checked = inspectPrefix(candidate, platform);
|
|
53
|
+
if (!checked.ok || (imageVersion && compare(checked.version, imageVersion) < 0)) continue;
|
|
54
|
+
return { bin: path.join(candidate, platform === "win32" ? "" : "lib", "node_modules", PACKAGE, "johnness.mjs"), source, version: checked.version };
|
|
55
|
+
}
|
|
56
|
+
return { bin: imageBin, source: "image", version: imageVersion, ...(volumeVersion ? { skipped: volumeVersion } : {}) };
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
1
2
|
import { p as theme } from "./globals-Bv4ZcVWM.js";
|
|
2
3
|
import { g as resolveStateDir, n as DEFAULT_GATEWAY_PORT, u as resolveGatewayPort } from "./paths-BfR2LXbA.js";
|
|
3
4
|
import { f as defaultRuntime } from "./subsystem-Cf9yS0UI.js";
|
|
@@ -1049,6 +1050,7 @@ async function maybeRestartService(params) {
|
|
|
1049
1050
|
}
|
|
1050
1051
|
}
|
|
1051
1052
|
async function updateCommand(opts) {
|
|
1053
|
+
if (await (await import("./update-command.js")).maybeHandleVolumeCliUpdate(opts, { exit: code => { process.exitCode = code; } })) return;
|
|
1052
1054
|
suppressDeprecations();
|
|
1053
1055
|
const timeoutMs = parseTimeoutMsOrExit(opts.timeout);
|
|
1054
1056
|
const shouldRestart = opts.restart !== false;
|
package/dist/update-command.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
2
|
+
import { installVolumeRuntime } from "./volume-update.js";
|
|
1
3
|
// JOHNNESS_PATCH_UPDATE_COMMAND_62
|
|
2
4
|
// Owner-only `/update` chat command: install a newer johns-harness on the host the
|
|
3
5
|
// gateway runs on, then restart into it. Two install targets, chosen automatically:
|
|
@@ -102,6 +104,7 @@ export function installArgv(target, spec) {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
export function manualCommand(target, spec) {
|
|
107
|
+
if (target.mode === "volume") return `node ${JSON.stringify(path.join(path.dirname(installedPackageJson(target)), "johnness.mjs"))} update --tag ${spec.slice(spec.lastIndexOf("@") + 1)} --no-restart`;
|
|
105
108
|
return installArgv(target, spec).join(" ");
|
|
106
109
|
}
|
|
107
110
|
|
|
@@ -143,7 +146,10 @@ export async function runChatInstall({ tag, env = process.env, packageRoot, time
|
|
|
143
146
|
const manual = manualCommand(target, spec);
|
|
144
147
|
if (!target.prefix) return { ok: false, target, spec, manual, tail: "Could not detect an npm prefix for the running install." };
|
|
145
148
|
const runUpdateStep = deps.runUpdateStep ?? (await loadUpdater()).runUpdateStep;
|
|
146
|
-
if (target.mode === "volume")
|
|
149
|
+
if (target.mode === "volume") {
|
|
150
|
+
const result = await (deps.installVolumeRuntime ?? installVolumeRuntime)({ target, tag, packageRoot, timeoutMs, runUpdateStep, env, deps });
|
|
151
|
+
return { ...result, target, spec, manual };
|
|
152
|
+
}
|
|
147
153
|
const step = await runUpdateStep({ name: "npm install", argv: installArgv(target, spec), timeoutMs });
|
|
148
154
|
if (step.exitCode !== 0) return { ok: false, target, spec, manual, tail: redactTail(`${step.stdoutTail ?? ""}\n${step.stderrTail ?? ""}`) };
|
|
149
155
|
let installedVersion = null;
|
|
@@ -154,6 +160,24 @@ export async function runChatInstall({ tag, env = process.env, packageRoot, time
|
|
|
154
160
|
return { ok: true, target, spec, manual, installedVersion };
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
export async function maybeHandleVolumeCliUpdate(opts, deps = {}) {
|
|
164
|
+
const env = deps.env ?? process.env;
|
|
165
|
+
const target = resolveUpdatePrefix({ env, packageRoot: deps.packageRoot });
|
|
166
|
+
if (target.mode !== "volume") return false;
|
|
167
|
+
const log = deps.log ?? console.log, error = deps.error ?? console.error;
|
|
168
|
+
const tag = opts.tag ?? "latest";
|
|
169
|
+
if ((opts.channel && opts.channel !== "stable") || (tag !== "latest" && !VERSION_RE.test(tag))) {
|
|
170
|
+
error("Volume updates require --tag latest or an exact release version; the serving runtime was not changed.");
|
|
171
|
+
(deps.exit ?? (code => { process.exitCode = code; }))(1); return true;
|
|
172
|
+
}
|
|
173
|
+
if (opts.dryRun) { log(JSON.stringify({ dryRun: true, mode: "volume", prefix: target.prefix, stagesAndVerifies: true, ephemeralCache: true, keepsPreviousTree: false })); return true; }
|
|
174
|
+
const result = await runChatInstall({ tag, env, packageRoot: deps.packageRoot, deps, timeoutMs: deps.timeoutMs });
|
|
175
|
+
if (!result.ok) { error(redactTail(result.tail)); (deps.exit ?? (code => { process.exitCode = code; }))(1); return true; }
|
|
176
|
+
if (opts.json) log(JSON.stringify({ ok: true, version: result.installedVersion, prefix: target.prefix, restartRequired: true }));
|
|
177
|
+
else log(`Installed johns-harness ${result.installedVersion} safely. Restart the gateway supervisor to use it; no configuration was changed.`);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
function buildRoute(params) {
|
|
158
182
|
return {
|
|
159
183
|
channel: params.ctx?.OriginatingChannel || params.command?.channel,
|
|
@@ -175,7 +199,7 @@ function buildRoute(params) {
|
|
|
175
199
|
export async function restartIntoUpdate({ target, deps, log = console.info, exit = (code) => process.exit(code) }) {
|
|
176
200
|
if (target.mode === "volume") {
|
|
177
201
|
log("[update] volume runtime installed; exiting 75 for supervisor restart into the new runtime");
|
|
178
|
-
setTimeout(() => exit(75), 1500).unref?.();
|
|
202
|
+
setTimeout(() => (deps.exit ?? exit)(75), 1500).unref?.();
|
|
179
203
|
return "supervisor";
|
|
180
204
|
}
|
|
181
205
|
try {
|
|
@@ -249,10 +273,10 @@ export async function handleUpdateCommand(params, allowTextCommands, { routeRepl
|
|
|
249
273
|
status: "ok",
|
|
250
274
|
ts: Date.now(),
|
|
251
275
|
sessionKey: params.sessionKey,
|
|
252
|
-
deliveryContext: delivery?.deliveryContext,
|
|
253
|
-
threadId: delivery?.threadId,
|
|
276
|
+
deliveryContext: delivery?.deliveryContext ?? { channel: route.channel, to: route.to, accountId: route.accountId },
|
|
277
|
+
threadId: delivery?.threadId ?? route.threadId,
|
|
254
278
|
message: `Updated johns-harness ${current} -> ${result.installedVersion} (${result.target.mode} prefix ${result.target.prefix}).`,
|
|
255
|
-
stats: { mode: "chat.update", reason: "/update" },
|
|
279
|
+
stats: { mode: "chat.update", reason: "/update", targetVersion: result.installedVersion },
|
|
256
280
|
});
|
|
257
281
|
} catch {}
|
|
258
282
|
}
|
|
@@ -260,7 +284,7 @@ export async function handleUpdateCommand(params, allowTextCommands, { routeRepl
|
|
|
260
284
|
const restartText = how === "supervisor"
|
|
261
285
|
? "Restarting through the supervisor; the entrypoint will start the new runtime."
|
|
262
286
|
: how === "service" ? "Restarting the gateway service." : how === "gateway" ? "Restarting the gateway." : "Restart the gateway to finish (johnness gateway restart).";
|
|
263
|
-
await deliver(`Installed johns-harness ${result.installedVersion} into ${result.target.prefix}. ${restartText} I will confirm the running version when I am back.`);
|
|
287
|
+
if (result.target.mode !== "volume") await deliver(`Installed johns-harness ${result.installedVersion} into ${result.target.prefix}. ${restartText} I will confirm the running version when I am back.`);
|
|
264
288
|
} catch (error) {
|
|
265
289
|
await deliver(`Update failed: ${redactTail(error?.message ?? String(error), 5)}\nFix by hand: ${manualCommand(target, `${PACKAGE}@${targetVersion}`)}`);
|
|
266
290
|
} finally {
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1
|
|
2
|
+
// Stage and verify first. Never run npm against the serving prefix.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import fsp from 'node:fs/promises';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
8
|
+
import { Readable, Transform } from 'node:stream';
|
|
9
|
+
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { x as extract, t as listTar } from 'tar';
|
|
11
|
+
import { RECEIPT, inventoryPackage, packageRootForPrefix, validateRuntimeTree } from './runtime-integrity.js';
|
|
12
|
+
|
|
13
|
+
const MB = 1024 * 1024;
|
|
14
|
+
const sha = value => createHash('sha256').update(value).digest('hex');
|
|
15
|
+
const exists = file => fs.existsSync(file);
|
|
16
|
+
const within = (parent, child) => { const r = path.relative(path.resolve(parent), path.resolve(child)); return r === '' || (!r.startsWith('..'+path.sep) && r !== '..' && !path.isAbsolute(r)); };
|
|
17
|
+
const readJson = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
|
|
19
|
+
export async function scanDependencies(root) {
|
|
20
|
+
const result = { bytes: 0, files: 0, dirs: 0, empty: new Set() };
|
|
21
|
+
if (!exists(root)) return result;
|
|
22
|
+
async function walk(dir) {
|
|
23
|
+
for (const e of await fsp.readdir(dir, { withFileTypes: true })) {
|
|
24
|
+
const file = path.join(dir, e.name);
|
|
25
|
+
if (e.isDirectory()) { result.dirs++; await walk(file); }
|
|
26
|
+
else if (e.isFile()) { const st = await fsp.stat(file); result.files++; result.bytes += st.size; if (!st.size) result.empty.add(path.relative(root, file)); }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
await walk(root);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export async function verifyEmptyDependencies(root, empty, { fetchImpl = fetch, scratch, signal } = {}) {
|
|
33
|
+
const manifests = new Map();
|
|
34
|
+
for (const rel of empty) {
|
|
35
|
+
let dir = path.dirname(path.join(root, rel));
|
|
36
|
+
while (within(root, dir) && !exists(path.join(dir, 'package.json'))) dir = path.dirname(dir);
|
|
37
|
+
if (!within(root, dir)) throw new Error(`Unexpected empty dependency file: ${rel}`);
|
|
38
|
+
const pkg = readJson(path.join(dir, 'package.json')), member = 'package/' + path.relative(dir, path.join(root, rel)).split(path.sep).join('/');
|
|
39
|
+
const key = pkg.name + '@' + pkg.version;
|
|
40
|
+
if (!manifests.has(key)) {
|
|
41
|
+
const metaRes = await fetchImpl(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/${encodeURIComponent(pkg.version)}`, { signal });
|
|
42
|
+
if (!metaRes.ok) throw new Error('Cannot authenticate empty dependency fixtures');
|
|
43
|
+
const meta = await metaRes.json(), url = new URL(meta.dist?.tarball);
|
|
44
|
+
if (meta.name !== pkg.name || meta.version !== pkg.version || url.origin !== 'https://registry.npmjs.org' || !/^sha512-[A-Za-z0-9+/]+=*$/.test(meta.dist?.integrity ?? '')) throw new Error('Invalid dependency fixture metadata');
|
|
45
|
+
const res = await fetchImpl(url.href, { signal });
|
|
46
|
+
if (!res.ok) throw new Error('Cannot fetch dependency fixture manifest');
|
|
47
|
+
const archive = path.join(scratch, 'dependency-' + sha(key) + '.tgz');
|
|
48
|
+
const digest = createHash('sha512'); let received = 0;
|
|
49
|
+
await pipeline(Readable.fromWeb(res.body), new Transform({ transform(chunk, _enc, cb) { received += chunk.length; if (received > 512*MB) return cb(new Error('Dependency archive too large')); digest.update(chunk); cb(null, chunk); } }), fs.createWriteStream(archive, { flags: 'wx', mode: 0o600 }));
|
|
50
|
+
if ('sha512-' + digest.digest('base64') !== meta.dist.integrity) throw new Error('Dependency fixture integrity mismatch');
|
|
51
|
+
const allowed = new Set();
|
|
52
|
+
await listTar({ file: archive, strict: true, onReadEntry: entry => { if (entry.type === 'File' && entry.size === 0) allowed.add(entry.path); } });
|
|
53
|
+
await fsp.unlink(archive); manifests.set(key, allowed);
|
|
54
|
+
}
|
|
55
|
+
if (!manifests.get(key).has(member)) throw new Error(`Unexpected empty dependency file: ${rel}`);
|
|
56
|
+
}
|
|
57
|
+
return [...empty].sort();
|
|
58
|
+
}
|
|
59
|
+
export function requiredVolumeBytes({ tarballBytes, unpackedBytes, packageFiles = 0, dependencyBytes = 0, dependencyFiles = 0, dependencyDirs = 0, reuse = false }) {
|
|
60
|
+
return Math.ceil(Math.max(tarballBytes * 3, unpackedBytes + packageFiles * 4096 + 16 * MB + (reuse ? dependencyFiles * 512 + dependencyDirs * 4096 : dependencyBytes * 1.25)));
|
|
61
|
+
}
|
|
62
|
+
function matchingContract(oldRoot, nextRoot) {
|
|
63
|
+
try {
|
|
64
|
+
const a = readJson(path.join(oldRoot, 'package.json')), b = readJson(path.join(nextRoot, 'package.json'));
|
|
65
|
+
for (const key of ['dependencies','optionalDependencies','peerDependencies','engines']) if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) return false;
|
|
66
|
+
// Only the known, identical postinstall may run against a link-cloned dependency tree.
|
|
67
|
+
for (const rel of ['scripts/postinstall.mjs','dist/migrate-plugin-adapters.js','dist/migrate-plan.js']) if (sha(fs.readFileSync(path.join(oldRoot, rel))) !== sha(fs.readFileSync(path.join(nextRoot, rel)))) return false;
|
|
68
|
+
return true;
|
|
69
|
+
} catch { return false; }
|
|
70
|
+
}
|
|
71
|
+
function mutableDependencyFiles(artifact) {
|
|
72
|
+
const mutable = new Set();
|
|
73
|
+
const vendor = path.join(artifact, 'vendor/patched-deps');
|
|
74
|
+
if (!exists(vendor)) return mutable;
|
|
75
|
+
function walk(dir) {
|
|
76
|
+
const manifest = path.join(dir, 'package.json');
|
|
77
|
+
if (exists(manifest)) {
|
|
78
|
+
const pkg = readJson(manifest);
|
|
79
|
+
for (const [rel] of inventoryPackage(dir)) if (rel !== 'package.json') mutable.add(path.join(pkg.name, rel));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) if (e.isDirectory()) walk(path.join(dir, e.name));
|
|
83
|
+
}
|
|
84
|
+
walk(vendor);
|
|
85
|
+
return mutable;
|
|
86
|
+
}
|
|
87
|
+
export async function cloneDependencies(source, target, mutable = new Set()) {
|
|
88
|
+
async function walk(dir, dest) {
|
|
89
|
+
await fsp.mkdir(dest, { recursive: true });
|
|
90
|
+
for (const e of await fsp.readdir(dir, { withFileTypes: true })) {
|
|
91
|
+
const from = path.join(dir, e.name), to = path.join(dest, e.name), rel = path.relative(source, from);
|
|
92
|
+
if (e.isDirectory()) await walk(from, to);
|
|
93
|
+
else if (e.isSymbolicLink()) {
|
|
94
|
+
if (rel === 'mr-memory' || [...mutable].some(p => p === rel || p.startsWith(rel+path.sep))) throw new Error('Mutable dependency is a symlink; fresh dependency install required');
|
|
95
|
+
await fsp.symlink(await fsp.readlink(from), to);
|
|
96
|
+
} else if (e.isFile()) {
|
|
97
|
+
const memoryAdapterFile = rel.startsWith('mr-memory'+path.sep) && !rel.startsWith(path.join('mr-memory','node_modules')+path.sep);
|
|
98
|
+
if (mutable.has(rel) || memoryAdapterFile) await fsp.copyFile(from, to);
|
|
99
|
+
else await fsp.link(from, to);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await walk(source, target);
|
|
104
|
+
}
|
|
105
|
+
function assertHealthy(root, receipt, version) {
|
|
106
|
+
const result = validateRuntimeTree(root, { receipt, expectedVersion: version, legacy: false });
|
|
107
|
+
if (!result.ok) throw new Error(`Incomplete staged runtime: ${result.reason}`);
|
|
108
|
+
}
|
|
109
|
+
function assertStep(step) {
|
|
110
|
+
if (step.exitCode !== 0 || /ENOSPC|TAR_ENTRY_ERROR|no space left on device/i.test(`${step.stderrTail??''}\n${step.stdoutTail??''}`)) throw new Error('Install step failed or reported incomplete extraction; serving runtime preserved');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function installVolumeRuntime({ target, tag, packageRoot, timeoutMs = 20*60000, runUpdateStep, env = process.env, deps = {} }) {
|
|
114
|
+
const prefix = path.resolve(target.prefix), parent = path.dirname(prefix);
|
|
115
|
+
const current = packageRootForPrefix(prefix), sourceRoot = exists(current) ? current : packageRoot;
|
|
116
|
+
const stage = prefix + '.stage-' + randomUUID(), previous = prefix + '.previous';
|
|
117
|
+
const lockPath = prefix + '.update.lock';
|
|
118
|
+
const rename = deps.rename ?? fsp.rename, remove = deps.remove ?? fsp.rm;
|
|
119
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
120
|
+
let lock, scratch, cache, response, promoted = false, previousMoved = false, recordWritten = false, cleanupStarted = false;
|
|
121
|
+
const recordPath = prefix + ".previous-version.json";
|
|
122
|
+
const previousRecord = exists(recordPath) ? await fsp.readFile(recordPath) : null;
|
|
123
|
+
try {
|
|
124
|
+
await fsp.mkdir(parent, { recursive: true });
|
|
125
|
+
lock = await fsp.open(lockPath, 'wx', 0o600);
|
|
126
|
+
await lock.writeFile(JSON.stringify({ pid: process.pid, startedAt: Date.now(), version: tag }));
|
|
127
|
+
if (exists(previous)) throw new Error('A previous interrupted update needs recovery before another install');
|
|
128
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
129
|
+
const metadataResponse = await fetchImpl(`https://registry.npmjs.org/johns-harness/${encodeURIComponent(tag)}`, { signal });
|
|
130
|
+
if (!metadataResponse.ok) throw new Error(`Registry metadata HTTP ${metadataResponse.status} (E${metadataResponse.status})`);
|
|
131
|
+
const meta = await metadataResponse.json();
|
|
132
|
+
if (meta.name !== 'johns-harness' || !/^\d{4}\.\d{1,2}\.\d{1,2}(?:-[\w.-]+)?$/.test(meta.version) || (tag !== 'latest' && meta.version !== tag)) throw new Error('Registry package/version mismatch');
|
|
133
|
+
const url = new URL(meta.dist?.tarball);
|
|
134
|
+
if (url.origin !== 'https://registry.npmjs.org' || !/^sha512-[A-Za-z0-9+/]+=*$/.test(meta.dist?.integrity??'')) throw new Error('Registry artifact integrity metadata invalid');
|
|
135
|
+
response = await fetchImpl(url.href, { signal, headers: { 'accept-encoding': 'identity' } });
|
|
136
|
+
if (!response.ok) throw new Error(`Registry artifact HTTP ${response.status}`);
|
|
137
|
+
const tarballBytes = Number(response.headers.get('content-length'));
|
|
138
|
+
if (!Number.isSafeInteger(tarballBytes) || tarballBytes <= 0 || tarballBytes > 512*MB) throw new Error('Cannot establish a safe tarball size');
|
|
139
|
+
const available = async () => { const s = await (deps.statfs ?? fsp.statfs)(parent); return Number(s.bavail)*Number(s.bsize); };
|
|
140
|
+
const insistSpace = async required => { const free = await available(); if (free < required) throw new Error(`Insufficient volume space: need ${Math.ceil(required/MB)} MiB free, have ${Math.floor(free/MB)} MiB. Free at least ${Math.ceil((required-free)/MB)} MiB; live runtime untouched.`); };
|
|
141
|
+
await insistSpace(tarballBytes * 3);
|
|
142
|
+
scratch = await fsp.mkdtemp(path.join(deps.tempRoot ?? os.tmpdir(), 'johnness-update-'));
|
|
143
|
+
if (within(parent, scratch) || (env.JOHNNESS_STATE_DIR && within(env.JOHNNESS_STATE_DIR, scratch))) throw new Error('Update scratch/cache must be outside the persistent state volume');
|
|
144
|
+
cache = path.join(scratch, 'npm-cache');
|
|
145
|
+
await fsp.mkdir(cache);
|
|
146
|
+
const archive = path.join(scratch, 'package.tgz'), artifact = path.join(scratch, 'artifact');
|
|
147
|
+
const digest = createHash('sha512'); let received = 0;
|
|
148
|
+
await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, _enc, cb) { received += chunk.length; if (received > tarballBytes) return cb(new Error('Artifact exceeded declared size')); digest.update(chunk); cb(null, chunk); } }), fs.createWriteStream(archive, { flags: 'wx', mode: 0o600 }));
|
|
149
|
+
if (received !== tarballBytes || 'sha512-'+digest.digest('base64') !== meta.dist.integrity) throw new Error('Downloaded artifact integrity mismatch');
|
|
150
|
+
await fsp.mkdir(artifact);
|
|
151
|
+
await extract({ file: archive, cwd: artifact, strip: 1, strict: true, preservePaths: false, filter: (name, entry) => {
|
|
152
|
+
if (!name.startsWith('package/') || name.split('/').includes('..') || ['SymbolicLink','Link'].includes(entry.type)) throw new Error('Unsafe package archive member');
|
|
153
|
+
return true;
|
|
154
|
+
} });
|
|
155
|
+
const files = inventoryPackage(artifact), receipt = { managedBy: 'johnness-65.1', version: meta.version, integrity: meta.dist.integrity, files };
|
|
156
|
+
assertHealthy(artifact, receipt, meta.version);
|
|
157
|
+
const sourceDeps = sourceRoot && path.join(sourceRoot, 'node_modules');
|
|
158
|
+
const scanned = await scanDependencies(sourceDeps ?? path.join(scratch, 'none'));
|
|
159
|
+
let reuse = process.platform !== 'win32' && !!sourceDeps && exists(sourceDeps) && matchingContract(sourceRoot, artifact) && (await fsp.stat(sourceDeps)).dev === (await fsp.stat(parent)).dev;
|
|
160
|
+
const mutable = mutableDependencyFiles(artifact);
|
|
161
|
+
if (reuse) for (const name of ['mr-memory', ...new Set([...mutable].map(p => p.startsWith('@') ? p.split(path.sep).slice(0,2).join(path.sep) : p.split(path.sep)[0]))]) if (exists(path.join(sourceDeps,name)) && (await fsp.lstat(path.join(sourceDeps,name))).isSymbolicLink()) reuse = false;
|
|
162
|
+
await insistSpace(requiredVolumeBytes({ tarballBytes, unpackedBytes: files.reduce((n,f)=>n+f[1],0), packageFiles: files.length, dependencyBytes: scanned.bytes, dependencyFiles: scanned.files, dependencyDirs: scanned.dirs, reuse }));
|
|
163
|
+
const stagedRoot = packageRootForPrefix(stage);
|
|
164
|
+
await fsp.mkdir(path.dirname(stagedRoot), { recursive: true });
|
|
165
|
+
if (reuse) {
|
|
166
|
+
await fsp.cp(artifact, stagedRoot, { recursive: true, errorOnExist: true, force: false });
|
|
167
|
+
await cloneDependencies(sourceDeps, path.join(stagedRoot,'node_modules'), mutable);
|
|
168
|
+
assertHealthy(stagedRoot, receipt, meta.version);
|
|
169
|
+
assertStep(await runUpdateStep({ name: 'staged dependency patches', argv: [process.execPath, path.join(stagedRoot,'scripts/postinstall.mjs')], cwd: stagedRoot, timeoutMs }));
|
|
170
|
+
await fsp.mkdir(path.join(stage,'bin'), { recursive: true });
|
|
171
|
+
await fsp.symlink('../lib/node_modules/johns-harness/johnness.mjs', path.join(stage,'bin/johnness'));
|
|
172
|
+
} else {
|
|
173
|
+
assertStep(await runUpdateStep({ name: 'staged npm install', argv: ['env',`npm_config_cache=${cache}`,'npm','i','-g','--prefix',stage,'--cache',cache,'--prefer-offline','--no-audit','--no-fund',archive], timeoutMs }));
|
|
174
|
+
}
|
|
175
|
+
assertHealthy(stagedRoot, receipt, meta.version);
|
|
176
|
+
const afterDeps = await scanDependencies(path.join(stagedRoot,'node_modules'));
|
|
177
|
+
receipt.emptyDependencies = await (deps.verifyEmptyDependencies ?? verifyEmptyDependencies)(path.join(stagedRoot,'node_modules'), afterDeps.empty, { fetchImpl, scratch, signal });
|
|
178
|
+
await fsp.writeFile(path.join(stage, RECEIPT), JSON.stringify(receipt)+'\n', { mode: 0o600 });
|
|
179
|
+
const oldVersion = exists(current) ? readJson(path.join(current,'package.json')).version : null;
|
|
180
|
+
if (exists(prefix)) { await rename(prefix, previous); previousMoved = true; }
|
|
181
|
+
await rename(stage, prefix); promoted = true;
|
|
182
|
+
await deps.afterPromotion?.(prefix);
|
|
183
|
+
assertHealthy(packageRootForPrefix(prefix), receipt, meta.version);
|
|
184
|
+
// Commit is complete only after the old tree is gone. Keep version metadata, not code.
|
|
185
|
+
await fsp.writeFile(recordPath, JSON.stringify({ previousVersion: oldVersion, currentVersion: meta.version, rollback: oldVersion ? `johnness update --tag ${oldVersion}` : null })+'\n', { mode: 0o600 });
|
|
186
|
+
recordWritten = true;
|
|
187
|
+
if (previousMoved) { cleanupStarted = true; await remove(previous, { recursive: true, force: true }); }
|
|
188
|
+
return { ok: true, installedVersion: meta.version, reusedDependencies: reuse, verifiedFiles: files.length };
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (cleanupStarted) return { ok: false, installedVersion: tag, committed: true, tail: "New verified runtime committed, but old-tree cleanup failed. Serving the new runtime; remove the .previous directory before retrying. Never restore its partially deleted contents." };
|
|
191
|
+
if (previousMoved && exists(previous)) {
|
|
192
|
+
if (promoted && exists(prefix)) await rename(prefix, stage);
|
|
193
|
+
await rename(previous, prefix);
|
|
194
|
+
promoted = false;
|
|
195
|
+
}
|
|
196
|
+
if (promoted && !previousMoved && exists(prefix)) { await rename(prefix, stage); promoted = false; }
|
|
197
|
+
if (recordWritten && !promoted) {
|
|
198
|
+
if (previousRecord) await fsp.writeFile(recordPath, previousRecord);
|
|
199
|
+
else await fsp.unlink(recordPath).catch(()=>{});
|
|
200
|
+
}
|
|
201
|
+
return { ok: false, tail: String(error.code ?? error.message).replace(/(?:npm_[A-Za-z0-9]+|Bearer\s+\S+)/g, '<redacted>') };
|
|
202
|
+
} finally {
|
|
203
|
+
try { await response?.body?.cancel(); } catch {}
|
|
204
|
+
if (!promoted) await fsp.rm(stage, { recursive: true, force: true }).catch(()=>{});
|
|
205
|
+
if (scratch) await fsp.rm(scratch, { recursive: true, force: true }).catch(()=>{});
|
|
206
|
+
if (lock) { await lock.close(); await fsp.unlink(lockPath).catch(()=>{}); }
|
|
207
|
+
}
|
|
208
|
+
}
|
package/docs/PATCHES.md
CHANGED
|
@@ -17,7 +17,7 @@ they predate ~2026-06-06).
|
|
|
17
17
|
|
|
18
18
|
## Index
|
|
19
19
|
|
|
20
|
-
Family 01 is retired. Families 02 through 41 and 43 through 65 are live on `main`. Family 42 is omitted; native Codex OAuth ownership is Family 58.
|
|
20
|
+
Family 01 is retired. Families 02 through 41 and 43 through 65, including the 65.1 amendment, are live on `main`. Family 42 is omitted; native Codex OAuth ownership is Family 58.
|
|
21
21
|
|
|
22
22
|
1. **01, announce queue TTL, retired:** Removed the five-minute age filter that could discard delayed sub-agent completion announcements. [Details](#01--announce-ttl-retired-2026-07-30)
|
|
23
23
|
2. **02, failover rotation:** Returns model selection to the highest-priority profile after its cooldown ends. [Details](#02--failover-rotation-undocumented)
|
|
@@ -85,6 +85,8 @@ Family 01 is retired. Families 02 through 41 and 43 through 65 are live on `main
|
|
|
85
85
|
64. **64, Codex to API sibling fallback:** Any `openai-codex/<model>` failover tries `openai/<model>` first, before configured fallbacks, when an OpenAI API key is present. [Details](#64-codex-to-api-sibling-fallback)
|
|
86
86
|
65. **65, catalog refresh and selection policy:** API cache defaults cannot create a model allowlist; shipped models remain authoritative and every boot refreshes version-stamped per-agent catalogs while preserving custom entries. [Details](#65-catalog-refresh-and-selection-policy)
|
|
87
87
|
|
|
88
|
+
65.1. **65.1, safe staged volume updater:** Same-volume verified promotion, ephemeral npm cache, one final runtime tree and actual-version confirmation after boot. [Details](#651-safe-staged-in-place-volume-updater)
|
|
89
|
+
|
|
88
90
|
---
|
|
89
91
|
|
|
90
92
|
## 01 - announce-ttl (RETIRED 2026-07-30)
|
|
@@ -2464,3 +2466,59 @@ The durable choice is both a merged in-memory catalog and a refreshed on-disk sn
|
|
|
2464
2466
|
Baseline: 812 tests and 395 verifier checks. Release: 827 tests and 405 verifier checks, plus the alias contract. Earlier families retain exact byte-identical replay; overlapping historical reconstructions apply Family 65 chronologically, not a relaxed comparison. The Family 59 README counters and pinned CLI artifact are synchronized.
|
|
2465
2467
|
|
|
2466
2468
|
Rex-only deployment procedure: back up the existing state, install 2026.9.17 into the existing volume prefix, restart, inspect the boot/provider/stamp evidence, and use gateway `sessions.patch` on an isolated probe session for both Astra references. Preserve Rex's current OpenAI primary and sub-agent model. Rollback: `npm i -g --prefix /home/rex/.clawdbot/runtime/johnness johns-harness@2026.9.16`, then restart his machine. No other fleet agent is part of this release deployment.
|
|
2469
|
+
|
|
2470
|
+
|
|
2471
|
+
## 65.1 Safe staged in-place volume updater
|
|
2472
|
+
|
|
2473
|
+
Release: **johns-harness 2026.9.18**. Marker: `JOHNNESS_PATCH_SAFE_VOLUME_UPDATE_65_1`.
|
|
2474
|
+
Patcher: `scripts/patch-safe-volume-update.py [root] [--check]`, seven runtime files,
|
|
2475
|
+
idempotent and fail-closed across every anchor. Verifier: **65.1.1 through 65.1.12**.
|
|
2476
|
+
|
|
2477
|
+
- Chat `/update` and volume-mode CLI `johnness update --tag VERSION` use the same staged
|
|
2478
|
+
installer. A cross-process lock guards the prefix. Download/cache live in an ephemeral
|
|
2479
|
+
OS temp directory, never under persistent state. npm receives both `npm_config_cache`
|
|
2480
|
+
and `--cache`. Network, npm, extraction and preflight failures preserve the live tree.
|
|
2481
|
+
- Preflight requires at least **3x compressed archive bytes**, then the greater of that
|
|
2482
|
+
minimum and unpacked files, directory metadata and dependency staging costs. Refusals
|
|
2483
|
+
report required/free MiB and exactly how much more to free.
|
|
2484
|
+
- The registry tarball is SHA-512 authenticated. An exact path/size/SHA-256 inventory
|
|
2485
|
+
verifies every package file before and after installation and promotion, plus package
|
|
2486
|
+
version and required entrypoints, including Telegram. One intentional package JSONL
|
|
2487
|
+
and legitimate dependency fixtures may be empty only when publisher manifests say so.
|
|
2488
|
+
Dependency empty fixtures are authenticated against their own integrity-checked tarballs.
|
|
2489
|
+
- Identical dependency and postinstall contracts can hardlink unchanged dependencies on
|
|
2490
|
+
the same device. Vendored patch targets and mr-memory JavaScript receive private copies
|
|
2491
|
+
before postinstall, so staging cannot mutate live shared inodes. Changed contracts use
|
|
2492
|
+
a fresh npm install and the larger space requirement. No full dependency copy is retained.
|
|
2493
|
+
- Promotion uses same-volume renames, verifies the promoted tree, writes a small previous
|
|
2494
|
+
version/reinstall record, then deletes the old tree. Success leaves exactly one runtime
|
|
2495
|
+
tree. Chat does not try to send its final volume completion before exit; the existing
|
|
2496
|
+
restart sentinel is consumed after channels start, and both gateway copies report the
|
|
2497
|
+
actual booted package version rather than blindly claiming the requested version.
|
|
2498
|
+
|
|
2499
|
+
### Failure boundaries and recovery
|
|
2500
|
+
|
|
2501
|
+
Before old-tree deletion begins, caught failures restore the original live prefix. Renames
|
|
2502
|
+
are individually atomic, not a portable two-directory exchange transaction. The selector
|
|
2503
|
+
recognizes a valid `.previous` tree if interrupted between renames; a stale lock or previous
|
|
2504
|
+
path deliberately blocks another updater until the operator inspects/reconciles it. This
|
|
2505
|
+
release does not claim automatic recovery from every power-loss or filesystem failure.
|
|
2506
|
+
Once deletion starts, its contents cannot safely be rolled back: an exceptional cleanup
|
|
2507
|
+
failure reports `committed: true`, retains the verified NEW runtime, and requests removal
|
|
2508
|
+
of the partial `.previous` directory. It never restores a partially deleted old tree.
|
|
2509
|
+
Final confirmation uses the existing startup sentinel delivery lifecycle; transport failure
|
|
2510
|
+
is recorded as a system event, not guaranteed exactly-once delivery to an offline service.
|
|
2511
|
+
CLI volume installation reports the supervisor restart command; it does not kill another
|
|
2512
|
+
process. `--dry-run` displays the in-place plan without staging or modifying the prefix.
|
|
2513
|
+
|
|
2514
|
+
Model defaults and fleet entrypoint config rewriting are unchanged. A 9.17 runtime cannot
|
|
2515
|
+
provide this new installer until bootstrapped from the 9.18 artifact. Once installed, the
|
|
2516
|
+
next `/update` uses this path natively.
|
|
2517
|
+
|
|
2518
|
+
### Verification
|
|
2519
|
+
|
|
2520
|
+
Full suite **850/850**, source and isolated npm-install verifier **417/417**, 23 focused
|
|
2521
|
+
safety tests. A fake volume exercises private staging, artifact verification, atomic rename
|
|
2522
|
+
promotion, old-tree removal and corrupt-stage rollback. Historical families 51/54/60/61/62/
|
|
2523
|
+
63/64/65 and alias contracts remain green; Family 65 replay layers the new amendment before
|
|
2524
|
+
byte-exact comparisons. Family 59's README snapshot keeps its exact counts synchronized.
|