ruvnet-brain 4.0.7 → 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 +115 -44
- 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/hooks/codex-hooks.json +17 -17
- package/plugin/scripts/codex-hook-wrapper.mjs +72 -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/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
- package/scripts/candidate-host-evidence.mjs +49 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
- 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/learning-replay-contract.mjs +14 -4
- package/scripts/learning-replay-execution.mjs +4 -3
- package/scripts/learning-replay-fixture.mjs +23 -2
- 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 +135 -34
- package/scripts/release-evidence-aggregate.mjs +55 -0
- package/scripts/release-payload.mjs +100 -0
- package/scripts/release-transaction-provider.mjs +260 -55
- package/scripts/release-transaction.mjs +295 -105
- package/scripts/release.mjs +41 -12
- package/scripts/rvf-generation.mjs +17 -0
- package/scripts/selfcheck.mjs +20 -5
- package/scripts/staged-host-verifier.mjs +27 -57
- package/scripts/sync-version.mjs +10 -10
|
@@ -3,13 +3,16 @@ 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 {
|
|
6
|
+
import {
|
|
7
|
+
pollObservation, RECEIPT_PREFIX, TERMINAL_STATES, transactionIdFor,
|
|
8
|
+
} from './release-transaction.mjs';
|
|
7
9
|
|
|
8
10
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
9
11
|
const PACKAGE = 'ruvnet-brain';
|
|
12
|
+
export const ASSET_DOWNLOAD_TIMEOUT_MS = Number(process.env.RUVNET_RELEASE_ASSET_TIMEOUT_MS || 600_000);
|
|
10
13
|
|
|
11
14
|
const command = (name, args, options = {}) => execFileSync(name, args, {
|
|
12
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...options,
|
|
15
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
|
|
13
16
|
}).trim();
|
|
14
17
|
const json = (name, args, options) => JSON.parse(command(name, args, options));
|
|
15
18
|
const maybe = (callback, fallback = null) => {
|
|
@@ -24,31 +27,80 @@ const tagSha = (tag, root) => {
|
|
|
24
27
|
|
|
25
28
|
const assetBytes = (asset) => {
|
|
26
29
|
const result = spawnSync('gh', ['api', asset.url, '-H', 'Accept: application/octet-stream'], {
|
|
27
|
-
encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: ASSET_DOWNLOAD_TIMEOUT_MS,
|
|
28
31
|
});
|
|
29
|
-
if (result.error || result.status !== 0)
|
|
32
|
+
if (result.error || result.signal || result.status !== 0) {
|
|
33
|
+
throw new Error(`cannot download transaction asset ${asset.name}: ${result.error?.message || result.signal || `exit ${result.status}`}`);
|
|
34
|
+
}
|
|
30
35
|
return Buffer.from(result.stdout);
|
|
31
36
|
};
|
|
32
37
|
const assetReceipt = (asset) => JSON.parse(assetBytes(asset).toString('utf8'));
|
|
33
38
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
39
|
+
const OBSERVATION_POLICY = {
|
|
40
|
+
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 180_000),
|
|
41
|
+
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 14),
|
|
42
|
+
initialDelayMs: 1_000,
|
|
43
|
+
maxDelayMs: 15_000,
|
|
44
|
+
multiplier: 1.8,
|
|
45
|
+
jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
|
|
49
|
+
const bytes = remoteBytes || localBytes || generatedBytes;
|
|
50
|
+
let value;
|
|
51
|
+
try { value = JSON.parse(Buffer.from(bytes).toString('utf8')); } catch { throw new Error('public receipt is invalid JSON'); }
|
|
52
|
+
if (value.transactionId !== identity.transactionId || value.version !== identity.version
|
|
53
|
+
|| value.candidateSha !== identity.candidateSha || value.verdict !== 'PASS'
|
|
54
|
+
|| value.hosts?.verdict !== 'PASS') {
|
|
55
|
+
throw new Error('public receipt identity conflict');
|
|
56
|
+
}
|
|
57
|
+
return Buffer.from(bytes);
|
|
58
|
+
}
|
|
34
59
|
|
|
35
60
|
export function liveReleaseProvider({
|
|
36
61
|
root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
|
|
37
62
|
} = {}) {
|
|
38
63
|
let releases = [];
|
|
39
64
|
let activeDraft = null;
|
|
65
|
+
const assetDigestCache = new Map();
|
|
40
66
|
const refresh = () => {
|
|
41
|
-
|
|
67
|
+
const pages = json('gh', ['api', `repos/${REPO}/releases?per_page=100`, '--paginate', '--slurp']);
|
|
68
|
+
releases = (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
|
|
42
69
|
return releases;
|
|
43
70
|
};
|
|
44
71
|
const releaseById = (id) => json('gh', ['api', `repos/${REPO}/releases/${id}`]);
|
|
72
|
+
const assetsForRelease = (id) => {
|
|
73
|
+
const pages = json('gh', ['api', `repos/${REPO}/releases/${id}/assets?per_page=100`, '--paginate', '--slurp']);
|
|
74
|
+
return (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
|
|
75
|
+
};
|
|
76
|
+
const hydratedRelease = (release) => ({ ...release, assets: assetsForRelease(release.id) });
|
|
45
77
|
const receiptsFor = (release) => (release?.assets || [])
|
|
46
78
|
.filter(({ name }) => name.startsWith(RECEIPT_PREFIX) && name.endsWith('.json'))
|
|
47
79
|
.map(assetReceipt);
|
|
48
|
-
const
|
|
80
|
+
const expectedAssets = (identity) => ({
|
|
81
|
+
'ruvnet-brain.zip': identity.bundleSha256,
|
|
82
|
+
'ruvnet-brain.zip.sig': identity.bundleSignatureSha256,
|
|
83
|
+
'ruvnet-brain.zip.sha256': identity.bundleDigestSha256,
|
|
84
|
+
[identity.packageAssetName || '']: identity.packageSha256 || null,
|
|
85
|
+
});
|
|
86
|
+
const digestAsset = (asset, force = false) => {
|
|
87
|
+
const key = `${asset.id}:${asset.size}`;
|
|
88
|
+
if (!force && assetDigestCache.has(key)) return assetDigestCache.get(key);
|
|
89
|
+
const digest = sha256(assetBytes(asset));
|
|
90
|
+
assetDigestCache.set(key, digest);
|
|
91
|
+
return digest;
|
|
92
|
+
};
|
|
93
|
+
const assetsExactFor = (release, identity, force = false) => {
|
|
94
|
+
const expected = expectedAssets(identity);
|
|
95
|
+
return Object.entries(expected).filter(([name, digest]) => name && digest).every(([name, digest]) => {
|
|
96
|
+
const asset = release?.assets?.find((item) => item.name === name);
|
|
97
|
+
return asset && digestAsset(asset, force) === digest;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
const materializeRemoteAssets = async (draft, identity) => {
|
|
49
101
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-staged-'));
|
|
50
102
|
try {
|
|
51
|
-
const release = releaseById(draft.id);
|
|
103
|
+
const release = hydratedRelease(releaseById(draft.id));
|
|
52
104
|
const required = [
|
|
53
105
|
['bundlePath', 'ruvnet-brain.zip'],
|
|
54
106
|
['bundleSignaturePath', 'ruvnet-brain.zip.sig'],
|
|
@@ -65,13 +117,25 @@ export function liveReleaseProvider({
|
|
|
65
117
|
if (sha256(fs.readFileSync(assets.bundlePath)) !== identity.bundleSha256) {
|
|
66
118
|
throw new Error('staged GitHub bundle digest does not match transaction identity');
|
|
67
119
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (!Array.isArray(packed) || packed.length !== 1 || !packed[0].filename) {
|
|
72
|
-
throw new Error('npm candidate pack did not return exactly one artifact');
|
|
120
|
+
if (identity.bundleSignatureSha256
|
|
121
|
+
&& sha256(fs.readFileSync(assets.bundleSignaturePath)) !== identity.bundleSignatureSha256) {
|
|
122
|
+
throw new Error('staged GitHub bundle signature bytes do not match transaction identity');
|
|
73
123
|
}
|
|
74
|
-
|
|
124
|
+
if (identity.bundleDigestSha256
|
|
125
|
+
&& sha256(fs.readFileSync(assets.bundleDigestPath)) !== identity.bundleDigestSha256) {
|
|
126
|
+
throw new Error('staged GitHub digest sidecar bytes do not match transaction identity');
|
|
127
|
+
}
|
|
128
|
+
const { value: metadata } = await pollObservation(
|
|
129
|
+
async () => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']),
|
|
130
|
+
(value) => value?.version === identity.version
|
|
131
|
+
&& value?.dist?.integrity === identity.packageIntegrity
|
|
132
|
+
&& typeof value?.dist?.tarball === 'string',
|
|
133
|
+
OBSERVATION_POLICY,
|
|
134
|
+
);
|
|
135
|
+
const response = await fetch(metadata.dist.tarball, { signal: AbortSignal.timeout(30_000) });
|
|
136
|
+
if (!response.ok) throw new Error(`npm candidate tarball download failed: HTTP ${response.status}`);
|
|
137
|
+
assets.packagePath = path.join(temp, identity.packageAssetName || `${PACKAGE}-${identity.version}.tgz`);
|
|
138
|
+
fs.writeFileSync(assets.packagePath, Buffer.from(await response.arrayBuffer()), { flag: 'wx', mode: 0o600 });
|
|
75
139
|
const integrity = `sha512-${crypto.createHash('sha512').update(fs.readFileSync(assets.packagePath)).digest('base64')}`;
|
|
76
140
|
if (integrity !== identity.packageIntegrity) throw new Error('staged npm package integrity mismatch');
|
|
77
141
|
return { assets, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
|
|
@@ -82,24 +146,42 @@ export function liveReleaseProvider({
|
|
|
82
146
|
};
|
|
83
147
|
|
|
84
148
|
return {
|
|
149
|
+
observationPolicy: OBSERVATION_POLICY,
|
|
150
|
+
|
|
85
151
|
async discover(identity) {
|
|
86
152
|
const all = refresh();
|
|
87
|
-
const matchingDrafts = all.filter((release) => release.tag_name === identity.tag
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
153
|
+
const matchingDrafts = all.filter((release) => release.tag_name === identity.tag)
|
|
154
|
+
.map((release) => ({
|
|
155
|
+
id: release.id,
|
|
156
|
+
tag: release.tag_name,
|
|
157
|
+
sha: release.draft ? release.target_commitish : tagSha(identity.tag, root),
|
|
158
|
+
draft: release.draft,
|
|
159
|
+
}))
|
|
160
|
+
.filter((release) => release.sha === identity.candidateSha);
|
|
91
161
|
activeDraft = matchingDrafts[0] || null;
|
|
92
|
-
const receipts = matchingDrafts.length === 1
|
|
162
|
+
const receipts = matchingDrafts.length === 1
|
|
163
|
+
? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
|
|
93
164
|
const latestByTransaction = new Map();
|
|
94
|
-
for (const
|
|
95
|
-
const
|
|
96
|
-
|
|
165
|
+
for (const release of all) {
|
|
166
|
+
for (const receipt of receiptsFor(hydratedRelease(release))) {
|
|
167
|
+
const prior = latestByTransaction.get(receipt.transactionId);
|
|
168
|
+
if (!prior || receipt.sequence > prior.receipt.sequence) {
|
|
169
|
+
latestByTransaction.set(receipt.transactionId, { receipt, release });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
174
|
+
const githubLatest = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
175
|
+
const legacySettled = [];
|
|
176
|
+
const pending = [];
|
|
177
|
+
for (const { receipt, release } of latestByTransaction.values()) {
|
|
178
|
+
if (TERMINAL_STATES.has(receipt.state)) continue;
|
|
179
|
+
const settled = receipt.schemaVersion === 1 && release.draft === false
|
|
180
|
+
&& receipt.identity?.version === npmLatest && receipt.identity?.tag === githubLatest;
|
|
181
|
+
if (settled) legacySettled.push(receipt.transactionId);
|
|
182
|
+
else pending.push(receipt);
|
|
97
183
|
}
|
|
98
|
-
|
|
99
|
-
.filter((receipt) => !TERMINAL_STATES.has(receipt.state));
|
|
100
|
-
const npmLatest = maybe(() => command('npm', ['view', `${PACKAGE}@latest`, 'version']), null);
|
|
101
|
-
const githubLatest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
|
|
102
|
-
return { matchingDrafts, receipts, pending, prior: { npmLatest, githubLatest } };
|
|
184
|
+
return { matchingDrafts, receipts, pending, legacySettled, prior: { npmLatest, githubLatest } };
|
|
103
185
|
},
|
|
104
186
|
|
|
105
187
|
async createDraft(identity) {
|
|
@@ -112,6 +194,63 @@ export function liveReleaseProvider({
|
|
|
112
194
|
return activeDraft;
|
|
113
195
|
},
|
|
114
196
|
|
|
197
|
+
async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
|
|
198
|
+
try {
|
|
199
|
+
const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
|
|
200
|
+
const latestTag = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
201
|
+
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
202
|
+
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
203
|
+
const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
204
|
+
const resolvedSha = release && !release.draft ? tagSha(identity.tag, root) : release?.target_commitish || draft?.sha || null;
|
|
205
|
+
const publicReceipt = release?.assets?.find((asset) => asset.name === 'current-release.json');
|
|
206
|
+
let publicReceiptExact = false;
|
|
207
|
+
let publicHostsExact = false;
|
|
208
|
+
if (publicReceipt) {
|
|
209
|
+
const value = assetReceipt(publicReceipt);
|
|
210
|
+
publicReceiptExact = value?.transactionId === transactionIdFor(identity)
|
|
211
|
+
&& value?.version === identity.version
|
|
212
|
+
&& value?.candidateSha === identity.candidateSha
|
|
213
|
+
&& /^[a-f0-9]{64}$/.test(value?.publicationReceiptSha256 || '');
|
|
214
|
+
publicHostsExact = value?.verdict === 'PASS' && value?.hosts?.verdict === 'PASS'
|
|
215
|
+
&& ['claudeOnly', 'codexOnly', 'dual'].every((mode) => value.hosts?.[mode]?.status === 'PASS'
|
|
216
|
+
&& value.hosts?.[mode]?.doctorExit === 0
|
|
217
|
+
&& value.hosts?.[mode]?.artifactSha256 === identity.packageSha256);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
npm: {
|
|
221
|
+
candidateVersion: candidate?.version || exactVersion?.version || null,
|
|
222
|
+
candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
|
|
223
|
+
candidateTagVersion: candidate?.version || null,
|
|
224
|
+
latestVersion,
|
|
225
|
+
},
|
|
226
|
+
github: release ? {
|
|
227
|
+
tag: release.tag_name,
|
|
228
|
+
sha: resolvedSha,
|
|
229
|
+
draft: release.draft === true,
|
|
230
|
+
published: release.draft === false,
|
|
231
|
+
latest: latestTag === identity.tag,
|
|
232
|
+
latestTag,
|
|
233
|
+
assetsExact: assetsExactFor(release, identity, forceAssets),
|
|
234
|
+
} : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
|
|
235
|
+
publicReceiptExact,
|
|
236
|
+
publicHostsExact,
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { readError: error.message };
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
async observeNpm(identity) {
|
|
244
|
+
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
245
|
+
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
246
|
+
return {
|
|
247
|
+
candidateVersion: candidate?.version || exactVersion?.version || null,
|
|
248
|
+
candidateIntegrity: candidate?.dist?.integrity || exactVersion?.dist?.integrity || null,
|
|
249
|
+
candidateTagVersion: candidate?.version || null,
|
|
250
|
+
latestVersion: command('npm', ['view', `${PACKAGE}@latest`, 'version']),
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
|
|
115
254
|
async appendReceipt(draft, receipt, name) {
|
|
116
255
|
const anchor = draft || activeDraft;
|
|
117
256
|
if (!anchor) throw new Error('no draft anchor for transaction receipt');
|
|
@@ -127,7 +266,7 @@ export function liveReleaseProvider({
|
|
|
127
266
|
|
|
128
267
|
async readReceipt(draft, sequence) {
|
|
129
268
|
const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
|
|
130
|
-
const release = releaseById((draft || activeDraft).id);
|
|
269
|
+
const release = hydratedRelease(releaseById((draft || activeDraft).id));
|
|
131
270
|
const asset = release.assets?.find((item) => item.name === name);
|
|
132
271
|
if (!asset) throw new Error(`remote receipt ${name} is missing after upload`);
|
|
133
272
|
return assetReceipt(asset);
|
|
@@ -136,7 +275,7 @@ export function liveReleaseProvider({
|
|
|
136
275
|
async uploadAssets(draft, assets) {
|
|
137
276
|
const paths = [assets.bundlePath, assets.bundleSignaturePath, assets.bundleDigestPath, assets.packagePath];
|
|
138
277
|
for (const file of paths) {
|
|
139
|
-
const release = releaseById(draft.id);
|
|
278
|
+
const release = hydratedRelease(releaseById(draft.id));
|
|
140
279
|
const existing = release.assets?.find(({ name }) => name === path.basename(file));
|
|
141
280
|
if (existing) {
|
|
142
281
|
if (sha256(assetBytes(existing)) !== sha256(fs.readFileSync(file))) {
|
|
@@ -154,7 +293,11 @@ export function liveReleaseProvider({
|
|
|
154
293
|
|
|
155
294
|
async stageNpm(identity, packagePath) {
|
|
156
295
|
const existing = maybe(() => command('npm', ['view', `${PACKAGE}@${identity.version}`, 'dist.integrity']), null);
|
|
157
|
-
if (existing)
|
|
296
|
+
if (existing) {
|
|
297
|
+
if (existing !== identity.packageIntegrity) throw new Error('existing npm version has different immutable bytes');
|
|
298
|
+
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
158
301
|
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
|
|
159
302
|
},
|
|
160
303
|
|
|
@@ -173,13 +316,21 @@ export function liveReleaseProvider({
|
|
|
173
316
|
return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
|
|
174
317
|
},
|
|
175
318
|
|
|
176
|
-
async promoteNpm(identity) {
|
|
319
|
+
async promoteNpm(identity, expectedPrior) {
|
|
320
|
+
const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
321
|
+
if (current !== expectedPrior && current !== identity.version) {
|
|
322
|
+
throw new Error(`refusing npm promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
323
|
+
}
|
|
177
324
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, 'latest']);
|
|
178
325
|
},
|
|
179
326
|
async observeNpmLatest() {
|
|
180
327
|
return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
|
|
181
328
|
},
|
|
182
|
-
async makeGithubLatest(draft) {
|
|
329
|
+
async makeGithubLatest(draft, identity, expectedPrior) {
|
|
330
|
+
const current = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
331
|
+
if (current !== expectedPrior && current !== identity.tag) {
|
|
332
|
+
throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
333
|
+
}
|
|
183
334
|
command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
|
|
184
335
|
},
|
|
185
336
|
async observeGithubLatest() {
|
|
@@ -191,46 +342,100 @@ export function liveReleaseProvider({
|
|
|
191
342
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
|
|
192
343
|
},
|
|
193
344
|
async finalize(identity, receipt, hostVerifier) {
|
|
194
|
-
const staged = materializeRemoteAssets(activeDraft, identity);
|
|
195
|
-
let hosts;
|
|
196
|
-
try {
|
|
197
|
-
hosts = await hostVerifier.verify({ source: 'final', identity, assets: staged.assets });
|
|
198
|
-
} finally {
|
|
199
|
-
staged.cleanup();
|
|
200
|
-
}
|
|
201
|
-
if (hosts.verdict !== 'PASS') return { verdict: 'FAIL', hosts };
|
|
202
345
|
if (!candidateReceipt || !publicationReceipt) {
|
|
203
346
|
throw new Error('final convergence requires candidate and publication receipt paths');
|
|
204
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
|
+
}
|
|
205
373
|
if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
|
|
206
374
|
const publication = spawnSync(process.execPath, [
|
|
207
375
|
'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
|
|
208
376
|
], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
|
|
209
377
|
if (publication.error || publication.status !== 0) {
|
|
210
|
-
return { verdict: 'FAIL',
|
|
378
|
+
return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
|
|
211
379
|
}
|
|
212
380
|
}
|
|
213
381
|
const seal = spawnSync(process.execPath, [
|
|
214
382
|
'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
|
|
215
383
|
], { cwd: root, encoding: 'utf8', timeout: 300_000 });
|
|
216
384
|
if (seal.error || seal.status !== 0) {
|
|
217
|
-
return { verdict: 'FAIL',
|
|
218
|
-
}
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
verdict:
|
|
385
|
+
return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
|
|
386
|
+
}
|
|
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.
|
|
390
|
+
const hosts = {
|
|
391
|
+
verdict: verified.verdict,
|
|
392
|
+
claudeOnly: publication.installed?.claudeOnly,
|
|
393
|
+
codexOnly: publication.installed?.codexOnly,
|
|
394
|
+
dual: publication.installed?.dual,
|
|
395
|
+
verifier: { artifactSha256: verified.artifactSha256, fixtures: verified.fixtures, error: verified.error },
|
|
396
|
+
};
|
|
397
|
+
const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
|
|
398
|
+
const result = {
|
|
399
|
+
verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
|
|
232
400
|
hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
|
|
233
401
|
};
|
|
402
|
+
if (result.verdict === 'PASS') {
|
|
403
|
+
const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
|
|
404
|
+
const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
|
|
405
|
+
schemaVersion: 1,
|
|
406
|
+
transactionId: receipt.transactionId,
|
|
407
|
+
version: identity.version,
|
|
408
|
+
tag: identity.tag,
|
|
409
|
+
candidateSha: identity.candidateSha,
|
|
410
|
+
verdict: 'PASS',
|
|
411
|
+
publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
|
|
412
|
+
hosts,
|
|
413
|
+
surface: observed,
|
|
414
|
+
}, null, 2)}\n`);
|
|
415
|
+
let remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
416
|
+
?.find((asset) => asset.name === 'current-release.json');
|
|
417
|
+
let currentReleaseBytes;
|
|
418
|
+
try {
|
|
419
|
+
currentReleaseBytes = selectCurrentReleaseBytes({
|
|
420
|
+
remoteBytes: remote ? assetBytes(remote) : null,
|
|
421
|
+
localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
|
|
422
|
+
generatedBytes: generatedCurrentRelease,
|
|
423
|
+
identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
|
|
424
|
+
});
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
|
|
427
|
+
}
|
|
428
|
+
if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
|
|
429
|
+
if (!remote) {
|
|
430
|
+
command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
|
|
431
|
+
remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
432
|
+
?.find((asset) => asset.name === 'current-release.json');
|
|
433
|
+
}
|
|
434
|
+
if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
|
|
435
|
+
return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return result;
|
|
234
439
|
},
|
|
235
440
|
};
|
|
236
441
|
}
|