ruvnet-brain 4.0.8 → 4.0.12
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 +1 -1
- package/bin/install.mjs +109 -42
- package/console/app.js +32 -11
- package/console/style.css +5 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/continuation-gate.mjs +41 -1
- package/plugin/scripts/learn-flush.mjs +37 -2
- package/plugin/scripts/project-identity.mjs +89 -0
- package/plugin/scripts/ruflo-bin.mjs +81 -0
- package/plugin/scripts/session-snapshot-hook.mjs +4 -1
- package/plugin/scripts/session-start-core.mjs +19 -2
- package/scripts/console-runtime-identity.mjs +74 -0
- package/scripts/distill-project.mjs +9 -1
- package/scripts/doc-currency.mjs +15 -2
- package/scripts/health-repair.mjs +88 -11
- package/scripts/host-install-matrix.mjs +155 -0
- package/scripts/memory-doctor.mjs +26 -23
- package/scripts/model-router-catalog.mjs +34 -0
- package/scripts/onboarding-console.mjs +34 -12
- package/scripts/publication-receipt.mjs +10 -1
- package/scripts/release-transaction-provider.mjs +30 -2
- package/scripts/rvf-generation.mjs +17 -0
- package/scripts/staged-host-verifier.mjs +27 -54
- package/scripts/sync-version.mjs +10 -10
|
@@ -32,6 +32,7 @@ import { buildStackRecommendations, buildWiringRecommendations, summarizeWiring,
|
|
|
32
32
|
import { planFor } from './remedy-registry.mjs';
|
|
33
33
|
import { auditAll as capabilityAuditAll } from './capability-registry.mjs';
|
|
34
34
|
import { getVersion } from './version.mjs';
|
|
35
|
+
import { consoleRuntimeDigest } from './console-runtime-identity.mjs';
|
|
35
36
|
// L5 (ADR-028): the audit is the one place that observes live capability state, so it is where an
|
|
36
37
|
// OFFERED-then-now-`on` transition becomes an APPLIED — the numerator of the precision metric that
|
|
37
38
|
// tells the owner whether advocacy is landing or nagging. Both are pure reads/appends and never throw.
|
|
@@ -67,6 +68,9 @@ import {
|
|
|
67
68
|
saveOpenRouterCredential,
|
|
68
69
|
} from '../plugin/scripts/runtime-preferences.mjs';
|
|
69
70
|
import { applyNightlyChoice, nightlyStatus } from './nightly-controller.mjs';
|
|
71
|
+
// One canonical answer to "which directory is this, and have I counted it already?" — shared with
|
|
72
|
+
// the PreCompact snapshot producer (#85) and with memory-doctor's root scan (#107).
|
|
73
|
+
import { canonicalPath, pathIdentity, projectDirectory } from '../plugin/scripts/project-identity.mjs';
|
|
70
74
|
|
|
71
75
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
72
76
|
const REPO = path.dirname(__dirname);
|
|
@@ -103,14 +107,19 @@ const RUNTIME_PRODUCT = 'ruvnet-brain-console';
|
|
|
103
107
|
const RUNTIME_SCHEMA = 1;
|
|
104
108
|
const RUNTIME_API_CONTRACT = 1;
|
|
105
109
|
const RUNTIME_SCRIPT = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
106
|
-
|
|
110
|
+
// The generation this process IS, derived from every byte it executes and serves — not from this one
|
|
111
|
+
// file. #79: a Console whose entrypoint was unchanged but whose imported modules and frontend had been
|
|
112
|
+
// replaced reported the same identity as the candidate that replaced it, so the launcher reused a
|
|
113
|
+
// pre-update router behind a post-update page. Same function, same surface, same list as the installer
|
|
114
|
+
// stages (scripts/console-runtime-identity.mjs), so the two halves of this fact cannot drift.
|
|
115
|
+
const RUNTIME_SOURCE_SHA256 = consoleRuntimeDigest(REPO);
|
|
107
116
|
|
|
108
117
|
function consoleCandidateRoots() {
|
|
109
118
|
return candidateRoots({ home: CONSOLE_ROOT, configPath: CONFIG_PATH });
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
function canonicalScope(cwd = process.cwd()) {
|
|
113
|
-
|
|
122
|
+
return canonicalPath(cwd) ?? path.resolve(cwd);
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
function runtimeReceiptPath(cwd = process.cwd()) {
|
|
@@ -381,7 +390,7 @@ function wiringSurvey() {
|
|
|
381
390
|
const projects = [];
|
|
382
391
|
for (const root of consoleCandidateRoots()) {
|
|
383
392
|
for (const proj of findProjects(root)) {
|
|
384
|
-
const resolved = path.resolve(proj);
|
|
393
|
+
const resolved = pathIdentity(proj) ?? path.resolve(proj);
|
|
385
394
|
if (seenProjects.has(resolved)) continue;
|
|
386
395
|
seenProjects.add(resolved);
|
|
387
396
|
projects.push({ proj, root });
|
|
@@ -494,8 +503,12 @@ function scanFleet() {
|
|
|
494
503
|
return fleet;
|
|
495
504
|
}
|
|
496
505
|
function gatherMemory(cwd, { fleet = true } = {}) {
|
|
497
|
-
// health = for the project the console was launched from (fall back to this repo)
|
|
498
|
-
|
|
506
|
+
// health = for the project the console was launched from (fall back to this repo). The scope is
|
|
507
|
+
// resolved through projectDirectory() — the same call the PreCompact producer makes — so a console
|
|
508
|
+
// launched from a subdirectory probes the project root the hook actually wrote to, instead of
|
|
509
|
+
// warning that a snapshot it can see on disk does not exist (#85).
|
|
510
|
+
const scope = projectDirectory({ cwd });
|
|
511
|
+
const project = fs.existsSync(path.join(scope, '.swarm/memory.db')) ? scope : REPO;
|
|
499
512
|
const projName = project.replace(CONSOLE_ROOT + '/Code/', '').replace(CONSOLE_ROOT + '/', '~/');
|
|
500
513
|
const health = scoreMemoryHealth({ project: projName, probes: probeMemory(project) });
|
|
501
514
|
return { fleet: fleet ? scanFleet() : null, health };
|
|
@@ -1588,8 +1601,11 @@ function refreshFleetCache() {
|
|
|
1588
1601
|
// machine whose projects live under ~/source instead of ~/Code.
|
|
1589
1602
|
for (const root of consoleCandidateRoots()) {
|
|
1590
1603
|
for (const s of findMemoryStores(root)) {
|
|
1591
|
-
|
|
1592
|
-
|
|
1604
|
+
// pathIdentity, not path.resolve: resolve() normalises `.`/`..` and nothing else, so a project
|
|
1605
|
+
// reached through a symlink OR through the other capitalisation of a case-insensitive volume
|
|
1606
|
+
// was two distinct strings for one directory, and its memories were summed twice (#107).
|
|
1607
|
+
const resolved = pathIdentity(s.project) ?? path.resolve(s.project);
|
|
1608
|
+
if (seen.has(resolved)) continue; // a project visible under two roots counts once
|
|
1593
1609
|
seen.add(resolved);
|
|
1594
1610
|
const n = Number(robustRead(s.db, "SELECT COUNT(*) FROM memory_entries WHERE status='active'").value || 0);
|
|
1595
1611
|
if (n > 0) {
|
|
@@ -1726,7 +1742,12 @@ function gatherRouterEngine() {
|
|
|
1726
1742
|
const subscriptions = detectSubscriptions();
|
|
1727
1743
|
let house;
|
|
1728
1744
|
let providerKeys = providerAvailability(null, subscriptions);
|
|
1729
|
-
|
|
1745
|
+
// `keysVerified` is the machine-readable half of `status`, and it is the field every consumer must
|
|
1746
|
+
// consult BEFORE presenting `keys` as a fact about the user's machine (issue #86). `status` alone
|
|
1747
|
+
// was published and then ignored: the Console rendered a confident "✗ no API key found" per
|
|
1748
|
+
// provider straight off `keys`, so a missing catalog asset — an internal packaging failure — was
|
|
1749
|
+
// shown to the user as a verified finding about their credentials. Unknown outranks off.
|
|
1750
|
+
let providerCatalog = { status: 'degraded', keysVerified: false, detail: 'provider catalog was not loaded; native boolean detections are shown' };
|
|
1730
1751
|
try {
|
|
1731
1752
|
const hcat = loadCatalog();
|
|
1732
1753
|
house = detectProvider(hcat, { provider: cfg.provider });
|
|
@@ -1736,10 +1757,10 @@ function gatherRouterEngine() {
|
|
|
1736
1757
|
// run-context markers (which are not credentials), exactly as detectProvider() itself filters them —
|
|
1737
1758
|
// so the UI's "key found / not found" is now true instead of decorative.
|
|
1738
1759
|
providerKeys = providerAvailability(hcat, subscriptions);
|
|
1739
|
-
providerCatalog = { status: 'ok', detail: 'verified provider catalog loaded' };
|
|
1760
|
+
providerCatalog = { status: 'ok', keysVerified: true, detail: 'verified provider catalog loaded' };
|
|
1740
1761
|
} catch (error) {
|
|
1741
1762
|
house = { provider: cfg.provider && cfg.provider !== 'auto' ? cfg.provider : 'anthropic', source: 'default' };
|
|
1742
|
-
providerCatalog = { status: 'degraded', detail: `provider catalog unavailable: ${String(error?.message || error)}` };
|
|
1763
|
+
providerCatalog = { status: 'degraded', keysVerified: false, detail: `provider catalog unavailable: ${String(error?.message || error)}` };
|
|
1743
1764
|
}
|
|
1744
1765
|
return {
|
|
1745
1766
|
engine: {
|
|
@@ -2211,7 +2232,7 @@ function currentValidIds(onlyId = null) {
|
|
|
2211
2232
|
// offering nothing to do about it — detection without a remedy, which ADR-027 prohibits.
|
|
2212
2233
|
if (validateAll || healthOnly) {
|
|
2213
2234
|
try {
|
|
2214
|
-
const project =
|
|
2235
|
+
const project = projectDirectory();
|
|
2215
2236
|
const health = scoreMemoryHealth({ project: path.basename(project), probes: probeMemory(project) });
|
|
2216
2237
|
for (const r of buildHealthRecommendations({ memory: health, learning: observeLearning() })) ids.add(r.id);
|
|
2217
2238
|
} catch { /* an advisory surface must never break the apply path */ }
|
|
@@ -2923,7 +2944,8 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('onboarding-consol
|
|
|
2923
2944
|
const fleet = scanFleet();
|
|
2924
2945
|
let recommendations = [];
|
|
2925
2946
|
try {
|
|
2926
|
-
const
|
|
2947
|
+
const project = projectDirectory();
|
|
2948
|
+
const health = scoreMemoryHealth({ project: path.basename(project), probes: probeMemory(project) });
|
|
2927
2949
|
recommendations = buildHealthRecommendations({ memory: health, learning: { ...observeLearning(), fleet } });
|
|
2928
2950
|
} catch { /* advisory only */ }
|
|
2929
2951
|
writeCache(MEMORY_CACHE, new Date().toISOString(), { fleet, recommendations }, process.cwd());
|
|
@@ -8,6 +8,13 @@ import path from 'node:path';
|
|
|
8
8
|
import { pipeline } from 'node:stream/promises';
|
|
9
9
|
import { Readable } from 'node:stream';
|
|
10
10
|
import { spawn, spawnSync } from 'node:child_process';
|
|
11
|
+
// ONE doctor rule and ONE mode vocabulary, shared with the staged-side check in
|
|
12
|
+
// scripts/staged-host-verifier.mjs. This file kept its own copies, spelled claudeOnly/
|
|
13
|
+
// codexOnly/dual against the other's claude/codex/dual, so `hosts.claudeOnly` and
|
|
14
|
+
// `fixtures.claude` described the same fixture and nothing could tell. The richer
|
|
15
|
+
// post-publication proofs below (payload assertions, MCP wiring, SOURCE.json, rpcSearch)
|
|
16
|
+
// stay here — they are this side's job, not duplication.
|
|
17
|
+
import { HOST_MODES, RECEIPT_MODE_NAMES, MODE_FROM_RECEIPT_NAME, classifyDoctor } from './host-install-matrix.mjs';
|
|
11
18
|
import { pathToFileURL } from 'node:url';
|
|
12
19
|
import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
|
|
13
20
|
import { verifyPayload } from './release-payload.mjs';
|
|
@@ -210,7 +217,9 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
|
|
|
210
217
|
const sealedPlugin = path.join(packageRoot, 'plugin');
|
|
211
218
|
const results = {};
|
|
212
219
|
let bundle = null;
|
|
213
|
-
|
|
220
|
+
// Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
|
|
221
|
+
// cannot fall behind the staged-side check the way it did.
|
|
222
|
+
for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
|
|
214
223
|
const home = path.join(temp, `home-${mode}`);
|
|
215
224
|
const codexHome = path.join(home, '.codex');
|
|
216
225
|
const brainHome = path.join(home, '.cache', 'ruvnet-brain');
|
|
@@ -341,10 +341,35 @@ export function liveReleaseProvider({
|
|
|
341
341
|
if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
|
|
342
342
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
|
|
343
343
|
},
|
|
344
|
-
async finalize(identity, receipt,
|
|
344
|
+
async finalize(identity, receipt, hostVerifier) {
|
|
345
345
|
if (!candidateReceipt || !publicationReceipt) {
|
|
346
346
|
throw new Error('final convergence requires candidate and publication receipt paths');
|
|
347
347
|
}
|
|
348
|
+
// THE HOST VERDICT IS MEASURED, NOT DECLARED — and it is measured FIRST.
|
|
349
|
+
//
|
|
350
|
+
// finalize() used to accept the verifier as `_hostVerifier` and ignore it, then build
|
|
351
|
+
// `hosts = { verdict: 'PASS', … }` as a literal. Every release therefore asserted host
|
|
352
|
+
// convergence with no host verified, while tests/helpers/release-transaction-fixture.mjs:153
|
|
353
|
+
// called the seam faithfully — a fault-injection suite certifying wiring the real publisher
|
|
354
|
+
// never ran. Fable 5 and GPT-5.6-Sol independently made this their #1 finding.
|
|
355
|
+
//
|
|
356
|
+
// It runs BEFORE publication and sealing because the order encodes the meaning: you seal a
|
|
357
|
+
// release you have verified, not the reverse. It is also the cheapest way to fail — an
|
|
358
|
+
// unusable artifact stops here instead of after a 20-minute publish.
|
|
359
|
+
if (!hostVerifier || typeof hostVerifier.verify !== 'function') {
|
|
360
|
+
return { verdict: 'FAIL', hostVerifierError: 'no host verifier was supplied to finalize' };
|
|
361
|
+
}
|
|
362
|
+
// No `assets` override: stagedHostVerifier is constructed in release.mjs:238 with the exact
|
|
363
|
+
// staged bytes for this candidate and defaults to them (staged-host-verifier.mjs:57), so
|
|
364
|
+
// passing nothing is what keeps production verifying the artifact actually being shipped.
|
|
365
|
+
const verified = await hostVerifier.verify({ source: 'final', identity });
|
|
366
|
+
if (verified.verdict !== 'PASS') {
|
|
367
|
+
return {
|
|
368
|
+
verdict: 'FAIL',
|
|
369
|
+
hosts: { verdict: verified.verdict, verifier: { error: verified.error, fixtures: verified.fixtures } },
|
|
370
|
+
previousReceiptDigest: receipt.receiptDigest,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
348
373
|
if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
|
|
349
374
|
const publication = spawnSync(process.execPath, [
|
|
350
375
|
'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
|
|
@@ -360,11 +385,14 @@ export function liveReleaseProvider({
|
|
|
360
385
|
return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
|
|
361
386
|
}
|
|
362
387
|
const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
|
|
388
|
+
|
|
389
|
+
// `verified` was measured at the top of finalize, before publication and sealing.
|
|
363
390
|
const hosts = {
|
|
364
|
-
verdict:
|
|
391
|
+
verdict: verified.verdict,
|
|
365
392
|
claudeOnly: publication.installed?.claudeOnly,
|
|
366
393
|
codexOnly: publication.installed?.codexOnly,
|
|
367
394
|
dual: publication.installed?.dual,
|
|
395
|
+
verifier: { artifactSha256: verified.artifactSha256, fixtures: verified.fixtures, error: verified.error },
|
|
368
396
|
};
|
|
369
397
|
const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
|
|
370
398
|
const result = {
|
|
@@ -83,14 +83,31 @@ export function verifyRvfGenerations(dir, {
|
|
|
83
83
|
releaseTag = getVersionTag(),
|
|
84
84
|
requiredStores = [],
|
|
85
85
|
allowMissingFiles = false,
|
|
86
|
+
verifyBytes = true,
|
|
86
87
|
} = {}) {
|
|
87
88
|
const manifest = readRvfGenerations(dir);
|
|
88
89
|
const failures = [];
|
|
89
90
|
if (manifest.brainVersion !== version) failures.push(`brainVersion=${manifest.brainVersion}, expected ${version}`);
|
|
90
91
|
if (manifest.releaseTag !== releaseTag) failures.push(`releaseTag=${manifest.releaseTag}, expected ${releaseTag}`);
|
|
92
|
+
// `verifyBytes: false` verifies ONLY the committed ledger fields above.
|
|
93
|
+
//
|
|
94
|
+
// WHY: the `.rvf` binaries are gitignored (.gitignore:28 `kb/*.rvf`), so a byte comparison is a
|
|
95
|
+
// statement about the machine running the check, not about the commit being pushed. This ledger
|
|
96
|
+
// records 72 stores; a working checkout routinely has a different set — the nightly rebuilds RVFs
|
|
97
|
+
// and regenerates the ledger together (kb/forge-refresh.mjs:242), so between those two moments any
|
|
98
|
+
// developer tree disagrees. Wired into the pre-push gate this was unsatisfiable: it blocked EVERY
|
|
99
|
+
// push, including tags, and its own remedy line ("Run: node scripts/sync-version.mjs") could not
|
|
100
|
+
// clear it because the byte check lives under `if (CHECK)` and write mode never regenerates. That
|
|
101
|
+
// is what forced the 4.0.7 emergency promotion.
|
|
102
|
+
//
|
|
103
|
+
// release.mjs:100 already states the governing principle for this repo: "A verdict is only about
|
|
104
|
+
// the exact committed candidate." Bytes are verified where they are actually present and actually
|
|
105
|
+
// shipped — scripts/build-bundle.mjs:204-214, at bundle assembly — which is exactly what the
|
|
106
|
+
// deferral comment in sync-version.mjs promised but never had a caller for.
|
|
91
107
|
for (const store of requiredStores) {
|
|
92
108
|
if (!manifest.stores[store]) failures.push(`${store}: no generation record`);
|
|
93
109
|
}
|
|
110
|
+
if (!verifyBytes) return { manifest, failures };
|
|
94
111
|
for (const [store, generation] of Object.entries(manifest.stores)) {
|
|
95
112
|
const file = path.join(dir, generation.file || `${store}.big.rvf`);
|
|
96
113
|
if (!fs.existsSync(file)) {
|
|
@@ -3,6 +3,7 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { runHostMatrix } from './host-install-matrix.mjs';
|
|
6
7
|
|
|
7
8
|
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
8
9
|
const locate = (name) => {
|
|
@@ -18,11 +19,9 @@ const run = (name, args, options) => {
|
|
|
18
19
|
return result;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return { accepted: false, status: 'FAIL', output };
|
|
25
|
-
}
|
|
22
|
+
// The doctor verdict has ONE rule, in host-install-matrix.mjs. This name is kept because
|
|
23
|
+
// tests/unit/staged-host-verifier.test.mjs pins it, but it is now an alias, not a second copy.
|
|
24
|
+
export { classifyDoctor as classifyDoctorResult } from './host-install-matrix.mjs';
|
|
26
25
|
|
|
27
26
|
const preparePackage = ({ packagePath, bundlePath }) => {
|
|
28
27
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-staged-host-'));
|
|
@@ -39,62 +38,36 @@ const preparePackage = ({ packagePath, bundlePath }) => {
|
|
|
39
38
|
return { temp, packageRoot };
|
|
40
39
|
};
|
|
41
40
|
|
|
42
|
-
const fixturePath = (mode, temp) => {
|
|
43
|
-
const bin = path.join(temp, `bin-${mode}`);
|
|
44
|
-
fs.mkdirSync(bin);
|
|
45
|
-
const hosts = mode === 'claude' ? ['claude'] : mode === 'codex' ? ['codex'] : ['claude', 'codex'];
|
|
46
|
-
const desired = ['node', 'npm', ...hosts];
|
|
47
|
-
for (const name of desired) {
|
|
48
|
-
const target = locate(name);
|
|
49
|
-
if (!target) throw new Error(`${name} CLI unavailable for ${mode} host fixture`);
|
|
50
|
-
fs.symlinkSync(target, path.join(bin, name));
|
|
51
|
-
}
|
|
52
|
-
return `${bin}:/usr/bin:/bin`;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
41
|
export function stagedHostVerifier({ assets, identity }) {
|
|
56
42
|
return {
|
|
57
43
|
async verify({ source, assets: observedAssets = assets }) {
|
|
58
44
|
const prepared = preparePackage(observedAssets);
|
|
59
|
-
const results = {};
|
|
60
45
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
const installer = path.join(prepared.packageRoot, 'bin', 'install.mjs');
|
|
78
|
-
run(process.execPath, [installer, '--local', '--yes', '--force', '--no-nightly-prompt',
|
|
79
|
-
'--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'], {
|
|
80
|
-
cwd: prepared.packageRoot, env, timeout: 1_200_000,
|
|
81
|
-
});
|
|
82
|
-
const doctor = spawnSync(process.execPath, [installer, '--doctor', '--hooks'], {
|
|
83
|
-
cwd: prepared.packageRoot, env, timeout: 180_000,
|
|
84
|
-
});
|
|
85
|
-
const classified = classifyDoctorResult(doctor);
|
|
86
|
-
if (!classified.accepted) {
|
|
87
|
-
throw new Error(`doctor failed for ${mode}: ${classified.output.slice(-5000) || doctor.error?.message}`);
|
|
88
|
-
}
|
|
89
|
-
results[mode] = {
|
|
90
|
-
status: classified.status,
|
|
91
|
-
doctorExit: doctor.status,
|
|
92
|
-
version: identity.version,
|
|
93
|
-
};
|
|
46
|
+
// The loop, the mode names, the env and the doctor verdict all live in
|
|
47
|
+
// scripts/host-install-matrix.mjs, shared with the published-side check in
|
|
48
|
+
// publication-receipt.mjs. This file used to carry its own copy, which had drifted to
|
|
49
|
+
// different mode names (claude/codex/dual vs claudeOnly/codexOnly/dual) and a different
|
|
50
|
+
// install shape than the one that runs after publication — so the two halves of a release
|
|
51
|
+
// were judging different things and could not be compared. Only the STAGED-vs-PUBLISHED
|
|
52
|
+
// difference is real, and it is now a named variant rather than a second implementation.
|
|
53
|
+
const matrix = runHostMatrix({
|
|
54
|
+
packageRoot: prepared.packageRoot,
|
|
55
|
+
version: identity.version,
|
|
56
|
+
variant: 'staged',
|
|
57
|
+
locate,
|
|
58
|
+
temp: prepared.temp,
|
|
59
|
+
});
|
|
60
|
+
if (matrix.verdict !== 'PASS') {
|
|
61
|
+
return { verdict: 'FAIL', source, error: matrix.error, fixtures: matrix.fixtures };
|
|
94
62
|
}
|
|
95
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
verdict: 'PASS',
|
|
65
|
+
source,
|
|
66
|
+
artifactSha256: sha256(observedAssets.packagePath),
|
|
67
|
+
fixtures: matrix.fixtures,
|
|
68
|
+
};
|
|
96
69
|
} catch (error) {
|
|
97
|
-
return { verdict: 'FAIL', error: error.message, fixtures:
|
|
70
|
+
return { verdict: 'FAIL', source, error: error.message, fixtures: {} };
|
|
98
71
|
} finally {
|
|
99
72
|
fs.rmSync(prepared.temp, { recursive: true, force: true });
|
|
100
73
|
}
|
package/scripts/sync-version.mjs
CHANGED
|
@@ -113,16 +113,16 @@ for (const rel of ['kb/RVF-GENERATIONS.json']) {
|
|
|
113
113
|
|
|
114
114
|
if (CHECK && fs.existsSync(path.join(ROOT, 'kb/RVF-GENERATIONS.json'))) {
|
|
115
115
|
const kbDir = path.join(ROOT, 'kb');
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
// the
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
116
|
+
// Validate the committed ledger's version fields ONLY. The previous form scanned the working
|
|
117
|
+
// directory for `*.big.rvf` and byte-compared every ledger row against whatever this machine
|
|
118
|
+
// happened to have — but those binaries are gitignored, so the verdict described the machine, not
|
|
119
|
+
// the commit. It was unsatisfiable in practice (ledger: 72 stores; a real checkout: 71, because
|
|
120
|
+
// the nightly rebuilds RVFs and regenerates the ledger as one step) and it blocked every push,
|
|
121
|
+
// tags included, while pointing at a remedy that cannot clear it. Byte verification now happens
|
|
122
|
+
// where the bytes are genuinely present and genuinely shipped: scripts/build-bundle.mjs:204-214.
|
|
123
|
+
// This is the same rule release.mjs:100 already applies — a verdict is only about the committed
|
|
124
|
+
// candidate.
|
|
125
|
+
const { failures } = verifyRvfGenerations(kbDir, { verifyBytes: false });
|
|
126
126
|
for (const failure of failures) {
|
|
127
127
|
console.error(`[version] RVF GENERATION DRIFT: ${failure}`);
|
|
128
128
|
drift++;
|