ruvnet-brain 4.0.6 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +118 -9
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/codex-hooks.json +17 -17
  11. package/plugin/hooks/hooks.json +34 -1
  12. package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
  13. package/plugin/scripts/ground-ruvnet.sh +2 -1
  14. package/plugin/scripts/hook-shim.mjs +13 -6
  15. package/plugin/scripts/lesson-provenance.mjs +21 -0
  16. package/plugin/scripts/lesson-store.mjs +33 -3
  17. package/plugin/scripts/route-dispatch.sh +23 -49
  18. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  19. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  20. package/plugin/scripts/session-start-core.mjs +25 -7
  21. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  22. package/plugin/skills/release-proof/SKILL.md +4 -4
  23. package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
  24. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  25. package/scripts/candidate-host-evidence.mjs +49 -0
  26. package/scripts/capability-registry.mjs +3 -3
  27. package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
  28. package/scripts/learning-replay-contract.mjs +14 -4
  29. package/scripts/learning-replay-execution.mjs +4 -3
  30. package/scripts/learning-replay-fixture.mjs +23 -2
  31. package/scripts/lesson-lifecycle.mjs +2 -1
  32. package/scripts/lesson-ratify.mjs +6 -3
  33. package/scripts/lesson-seed.mjs +21 -16
  34. package/scripts/model-router-catalog.mjs +16 -0
  35. package/scripts/model-router-engine.mjs +4 -1
  36. package/scripts/onboarding-console.mjs +52 -14
  37. package/scripts/provider-availability.mjs +12 -0
  38. package/scripts/publication-receipt.mjs +126 -34
  39. package/scripts/release-authority.mjs +14 -2
  40. package/scripts/release-evidence-aggregate.mjs +55 -0
  41. package/scripts/release-payload.mjs +100 -0
  42. package/scripts/release-transaction-provider.mjs +413 -0
  43. package/scripts/release-transaction.mjs +404 -0
  44. package/scripts/release.mjs +97 -280
  45. package/scripts/selfcheck.mjs +20 -5
  46. package/scripts/session-snapshot-contract.mjs +1 -0
  47. package/scripts/staged-host-verifier.mjs +103 -0
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from './release-transaction.mjs';
7
+
8
+ export const REQUIRED_RELEASE_LEAVES = Object.freeze([
9
+ 'source-quality',
10
+ 'ux-qe',
11
+ 'release-qe',
12
+ 'integration-linux',
13
+ 'stranger-linux',
14
+ 'stranger-macos',
15
+ 'stranger-windows',
16
+ 'claude-only',
17
+ 'codex-only',
18
+ 'dual-host',
19
+ ]);
20
+
21
+ export function aggregateEvidence({ sha, payloadId, leaves }) {
22
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^[a-f0-9]{64}$/.test(payloadId || '')) {
23
+ throw new Error('aggregate identity is malformed');
24
+ }
25
+ const byName = new Map();
26
+ for (const leaf of leaves || []) {
27
+ if (!leaf?.name || byName.has(leaf.name)) throw new Error(`duplicate or unnamed evidence leaf: ${leaf?.name || '(missing)'}`);
28
+ if (leaf.sha !== sha || leaf.payloadId !== payloadId) throw new Error(`evidence identity mismatch: ${leaf.name}`);
29
+ if (leaf.status !== 'completed' || leaf.conclusion !== 'success' || leaf.verdict !== 'PASS') {
30
+ throw new Error(`evidence leaf is not fail-closed PASS: ${leaf.name}`);
31
+ }
32
+ byName.set(leaf.name, leaf);
33
+ }
34
+ const missing = REQUIRED_RELEASE_LEAVES.filter((name) => !byName.has(name));
35
+ if (missing.length) throw new Error(`required evidence leaves missing: ${missing.join(', ')}`);
36
+ const normalized = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
37
+ const envelope = { schemaVersion: 1, sha, payloadId, verdict: 'PASS', leaves: normalized };
38
+ return { ...envelope, evidenceDigest: crypto.createHash('sha256').update(canonicalJson(envelope)).digest('hex') };
39
+ }
40
+
41
+ const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
42
+ if (isCli) {
43
+ const arg = (name) => {
44
+ const index = process.argv.indexOf(name);
45
+ return index >= 0 ? process.argv[index + 1] : null;
46
+ };
47
+ const out = path.resolve(arg('--out') || 'release-evidence/aggregate-envelope.json');
48
+ const envelope = aggregateEvidence({
49
+ sha: arg('--sha'),
50
+ payloadId: arg('--payload-id'),
51
+ leaves: JSON.parse(fs.readFileSync(path.resolve(arg('--leaves')), 'utf8')),
52
+ });
53
+ fs.writeFileSync(out, `${canonicalJson(envelope)}\n`, { flag: 'wx', mode: 0o600 });
54
+ process.stdout.write(`${JSON.stringify({ verdict: 'PASS', evidenceDigest: envelope.evidenceDigest, out })}\n`);
55
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { canonicalJson } from './release-transaction.mjs';
7
+
8
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
9
+ const digestFile = (file) => sha256(fs.readFileSync(file));
10
+
11
+ export function createPayloadManifest({ version, tag, candidateSha, producer, members }) {
12
+ const normalized = members.map(({ name, file, role }) => {
13
+ const stat = fs.statSync(file);
14
+ if (!stat.isFile() || stat.size === 0) throw new Error(`payload member is absent or empty: ${file}`);
15
+ return { name, role, size: stat.size, sha256: digestFile(file) };
16
+ }).sort((a, b) => a.name.localeCompare(b.name));
17
+ const names = new Set(normalized.map(({ name }) => name));
18
+ if (names.size !== normalized.length) throw new Error('payload member names must be unique');
19
+ return { schemaVersion: 1, version, tag, candidateSha, producer, members: normalized };
20
+ }
21
+
22
+ export const payloadIdFor = (manifest) => sha256(Buffer.from(canonicalJson(manifest)));
23
+
24
+ export function signPayloadManifest(manifest, privateKey) {
25
+ return crypto.sign(null, Buffer.from(canonicalJson(manifest)), privateKey).toString('base64');
26
+ }
27
+
28
+ export function verifyPayloadMembers({ manifest, root }) {
29
+ if (!manifest || manifest.schemaVersion !== 1 || !Array.isArray(manifest.members)) {
30
+ throw new Error('invalid payload manifest');
31
+ }
32
+ for (const member of manifest.members) {
33
+ if (!member.name || path.basename(member.name) !== member.name) throw new Error(`unsafe payload member name: ${member.name}`);
34
+ const file = path.join(root, member.name);
35
+ const stat = fs.statSync(file);
36
+ if (stat.size !== member.size || digestFile(file) !== member.sha256) {
37
+ throw new Error(`payload member mismatch: ${member.name}`);
38
+ }
39
+ }
40
+ return { payloadId: payloadIdFor(manifest), manifest, root };
41
+ }
42
+
43
+ export function verifyPayload({ manifest, signature, publicKey, root }) {
44
+ if (!crypto.verify(null, Buffer.from(canonicalJson(manifest)), publicKey, Buffer.from(signature.trim(), 'base64'))) {
45
+ throw new Error('payload manifest signature mismatch');
46
+ }
47
+ return verifyPayloadMembers({ manifest, root });
48
+ }
49
+
50
+ const arg = (name) => {
51
+ const index = process.argv.indexOf(name);
52
+ return index >= 0 ? process.argv[index + 1] : null;
53
+ };
54
+ const command = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
55
+ ? process.argv[2]
56
+ : null;
57
+
58
+ if (command === 'create') {
59
+ const out = path.resolve(arg('--out') || 'release-evidence/payload-manifest.json');
60
+ const memberArgs = process.argv.filter((value, index) => process.argv[index - 1] === '--member');
61
+ const members = memberArgs.map((entry) => {
62
+ const [role, file] = entry.split('=', 2);
63
+ if (!role || !file) throw new Error(`invalid --member ${entry}; expected role=path`);
64
+ return { role, file: path.resolve(file), name: path.basename(file) };
65
+ });
66
+ const manifest = createPayloadManifest({
67
+ version: arg('--version'),
68
+ tag: arg('--tag'),
69
+ candidateSha: arg('--sha'),
70
+ producer: {
71
+ workflow: process.env.GITHUB_WORKFLOW || 'local',
72
+ runId: process.env.GITHUB_RUN_ID || 'local',
73
+ runAttempt: process.env.GITHUB_RUN_ATTEMPT || '1',
74
+ },
75
+ members,
76
+ });
77
+ fs.writeFileSync(out, `${canonicalJson(manifest)}\n`, { flag: 'wx', mode: 0o600 });
78
+ process.stdout.write(`${JSON.stringify({ payloadId: payloadIdFor(manifest), manifest: out })}\n`);
79
+ } else if (command === 'sign') {
80
+ const manifestPath = path.resolve(arg('--manifest'));
81
+ const out = path.resolve(arg('--out'));
82
+ if (fs.existsSync(out)) throw new Error(`refusing to overwrite persisted payload signature: ${out}`);
83
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
84
+ const pem = process.env.RUVNET_SIGNING_KEY;
85
+ if (!pem) throw new Error('RUVNET_SIGNING_KEY is required');
86
+ fs.writeFileSync(out, `${signPayloadManifest(manifest, crypto.createPrivateKey(pem))}\n`, { flag: 'wx', mode: 0o600 });
87
+ process.stdout.write(`${JSON.stringify({ payloadId: payloadIdFor(manifest), signature: out })}\n`);
88
+ } else if (command === 'verify' || command === 'verify-members') {
89
+ const manifestPath = path.resolve(arg('--manifest'));
90
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
91
+ const result = command === 'verify'
92
+ ? verifyPayload({
93
+ manifest,
94
+ signature: fs.readFileSync(path.resolve(arg('--signature')), 'utf8'),
95
+ publicKey: crypto.createPublicKey(fs.readFileSync(path.resolve(arg('--public-key')), 'utf8')),
96
+ root: path.dirname(manifestPath),
97
+ })
98
+ : verifyPayloadMembers({ manifest, root: path.dirname(manifestPath) });
99
+ process.stdout.write(`${JSON.stringify({ verdict: 'PASS', payloadId: result.payloadId })}\n`);
100
+ }
@@ -0,0 +1,413 @@
1
+ import { execFileSync, spawnSync } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {
7
+ pollObservation, RECEIPT_PREFIX, TERMINAL_STATES, transactionIdFor,
8
+ } from './release-transaction.mjs';
9
+
10
+ const REPO = 'stuinfla/ruvnet-brain';
11
+ const PACKAGE = 'ruvnet-brain';
12
+ export const ASSET_DOWNLOAD_TIMEOUT_MS = Number(process.env.RUVNET_RELEASE_ASSET_TIMEOUT_MS || 600_000);
13
+
14
+ const command = (name, args, options = {}) => execFileSync(name, args, {
15
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
16
+ }).trim();
17
+ const json = (name, args, options) => JSON.parse(command(name, args, options));
18
+ const maybe = (callback, fallback = null) => {
19
+ try { return callback(); } catch { return fallback; }
20
+ };
21
+
22
+ const tagSha = (tag, root) => {
23
+ const rows = command('git', ['ls-remote', 'origin', `refs/tags/${tag}`, `refs/tags/${tag}^{}`], { cwd: root })
24
+ .split('\n').filter(Boolean).map((line) => line.split(/\s+/));
25
+ return rows.find(([, ref]) => ref?.endsWith('^{}'))?.[0] || rows[0]?.[0] || '';
26
+ };
27
+
28
+ const assetBytes = (asset) => {
29
+ const result = spawnSync('gh', ['api', asset.url, '-H', 'Accept: application/octet-stream'], {
30
+ encoding: 'buffer', stdio: ['ignore', 'pipe', 'pipe'], timeout: ASSET_DOWNLOAD_TIMEOUT_MS,
31
+ });
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
+ }
35
+ return Buffer.from(result.stdout);
36
+ };
37
+ const assetReceipt = (asset) => JSON.parse(assetBytes(asset).toString('utf8'));
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
+ }
59
+
60
+ export function liveReleaseProvider({
61
+ root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
62
+ } = {}) {
63
+ let releases = [];
64
+ let activeDraft = null;
65
+ const assetDigestCache = new Map();
66
+ const refresh = () => {
67
+ const pages = json('gh', ['api', `repos/${REPO}/releases?per_page=100`, '--paginate', '--slurp']);
68
+ releases = (Array.isArray(pages?.[0]) ? pages : [pages]).flat();
69
+ return releases;
70
+ };
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) });
77
+ const receiptsFor = (release) => (release?.assets || [])
78
+ .filter(({ name }) => name.startsWith(RECEIPT_PREFIX) && name.endsWith('.json'))
79
+ .map(assetReceipt);
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) => {
101
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-staged-'));
102
+ try {
103
+ const release = hydratedRelease(releaseById(draft.id));
104
+ const required = [
105
+ ['bundlePath', 'ruvnet-brain.zip'],
106
+ ['bundleSignaturePath', 'ruvnet-brain.zip.sig'],
107
+ ['bundleDigestPath', 'ruvnet-brain.zip.sha256'],
108
+ ];
109
+ const assets = {};
110
+ for (const [key, name] of required) {
111
+ const remote = release.assets?.find((asset) => asset.name === name);
112
+ if (!remote) throw new Error(`staged GitHub asset missing: ${name}`);
113
+ const file = path.join(temp, name);
114
+ fs.writeFileSync(file, assetBytes(remote), { flag: 'wx', mode: 0o600 });
115
+ assets[key] = file;
116
+ }
117
+ if (sha256(fs.readFileSync(assets.bundlePath)) !== identity.bundleSha256) {
118
+ throw new Error('staged GitHub bundle digest does not match transaction identity');
119
+ }
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');
123
+ }
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 });
139
+ const integrity = `sha512-${crypto.createHash('sha512').update(fs.readFileSync(assets.packagePath)).digest('base64')}`;
140
+ if (integrity !== identity.packageIntegrity) throw new Error('staged npm package integrity mismatch');
141
+ return { assets, cleanup: () => fs.rmSync(temp, { recursive: true, force: true }) };
142
+ } catch (error) {
143
+ fs.rmSync(temp, { recursive: true, force: true });
144
+ throw error;
145
+ }
146
+ };
147
+
148
+ return {
149
+ observationPolicy: OBSERVATION_POLICY,
150
+
151
+ async discover(identity) {
152
+ const all = refresh();
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);
161
+ activeDraft = matchingDrafts[0] || null;
162
+ const receipts = matchingDrafts.length === 1
163
+ ? receiptsFor(hydratedRelease(releaseById(matchingDrafts[0].id))) : [];
164
+ const latestByTransaction = new Map();
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);
183
+ }
184
+ return { matchingDrafts, receipts, pending, legacySettled, prior: { npmLatest, githubLatest } };
185
+ },
186
+
187
+ async createDraft(identity) {
188
+ const release = json('gh', [
189
+ 'api', '-X', 'POST', `repos/${REPO}/releases`,
190
+ '-f', `tag_name=${identity.tag}`, '-f', `target_commitish=${identity.candidateSha}`,
191
+ '-f', `name=${identity.tag} staged candidate`, '-F', 'draft=true', '-F', 'prerelease=false',
192
+ ]);
193
+ activeDraft = { id: release.id, tag: release.tag_name, sha: release.target_commitish };
194
+ return activeDraft;
195
+ },
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
+
254
+ async appendReceipt(draft, receipt, name) {
255
+ const anchor = draft || activeDraft;
256
+ if (!anchor) throw new Error('no draft anchor for transaction receipt');
257
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-release-receipt-'));
258
+ const file = path.join(temp, name);
259
+ try {
260
+ fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
261
+ command('gh', ['release', 'upload', anchor.tag, file, '--repo', REPO]);
262
+ } finally {
263
+ fs.rmSync(temp, { recursive: true, force: true });
264
+ }
265
+ },
266
+
267
+ async readReceipt(draft, sequence) {
268
+ const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
269
+ const release = hydratedRelease(releaseById((draft || activeDraft).id));
270
+ const asset = release.assets?.find((item) => item.name === name);
271
+ if (!asset) throw new Error(`remote receipt ${name} is missing after upload`);
272
+ return assetReceipt(asset);
273
+ },
274
+
275
+ async uploadAssets(draft, assets) {
276
+ const paths = [assets.bundlePath, assets.bundleSignaturePath, assets.bundleDigestPath, assets.packagePath];
277
+ for (const file of paths) {
278
+ const release = hydratedRelease(releaseById(draft.id));
279
+ const existing = release.assets?.find(({ name }) => name === path.basename(file));
280
+ if (existing) {
281
+ if (sha256(assetBytes(existing)) !== sha256(fs.readFileSync(file))) {
282
+ throw new Error(`refusing to replace staged asset with different bytes: ${existing.name}`);
283
+ }
284
+ continue;
285
+ }
286
+ command('gh', ['release', 'upload', draft.tag, file, '--repo', REPO]);
287
+ }
288
+ },
289
+
290
+ async materializeStagedAssets(draft, identity) {
291
+ return materializeRemoteAssets(draft, identity);
292
+ },
293
+
294
+ async stageNpm(identity, packagePath) {
295
+ const existing = maybe(() => command('npm', ['view', `${PACKAGE}@${identity.version}`, 'dist.integrity']), null);
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
+ }
301
+ command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
302
+ },
303
+
304
+ async observeNpmCandidate(identity) {
305
+ const metadata = json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']);
306
+ return { version: metadata.version, integrity: metadata.dist?.integrity, tag: `candidate-v${identity.version}` };
307
+ },
308
+
309
+ async publishDraftNonLatest(draft) {
310
+ command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-F', 'draft=false', '-f', 'make_latest=false']);
311
+ },
312
+
313
+ async observeGithub(identity) {
314
+ const release = json('gh', ['api', `repos/${REPO}/releases/tags/${identity.tag}`]);
315
+ const latest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
316
+ return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
317
+ },
318
+
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
+ }
324
+ command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, 'latest']);
325
+ },
326
+ async observeNpmLatest() {
327
+ return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
328
+ },
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
+ }
334
+ command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
335
+ },
336
+ async observeGithubLatest() {
337
+ return { tag: json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name };
338
+ },
339
+ async restoreNpmLatest(prior, expected) {
340
+ const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
341
+ if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
342
+ command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
343
+ },
344
+ async finalize(identity, receipt, _hostVerifier) {
345
+ if (!candidateReceipt || !publicationReceipt) {
346
+ throw new Error('final convergence requires candidate and publication receipt paths');
347
+ }
348
+ if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
349
+ const publication = spawnSync(process.execPath, [
350
+ 'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
351
+ ], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
352
+ if (publication.error || publication.status !== 0) {
353
+ return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
354
+ }
355
+ }
356
+ const seal = spawnSync(process.execPath, [
357
+ 'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
358
+ ], { cwd: root, encoding: 'utf8', timeout: 300_000 });
359
+ if (seal.error || seal.status !== 0) {
360
+ return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
361
+ }
362
+ const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
363
+ const hosts = {
364
+ verdict: 'PASS',
365
+ claudeOnly: publication.installed?.claudeOnly,
366
+ codexOnly: publication.installed?.codexOnly,
367
+ dual: publication.installed?.dual,
368
+ };
369
+ const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
370
+ const result = {
371
+ verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
372
+ hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
373
+ };
374
+ if (result.verdict === 'PASS') {
375
+ const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
376
+ const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
377
+ schemaVersion: 1,
378
+ transactionId: receipt.transactionId,
379
+ version: identity.version,
380
+ tag: identity.tag,
381
+ candidateSha: identity.candidateSha,
382
+ verdict: 'PASS',
383
+ publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
384
+ hosts,
385
+ surface: observed,
386
+ }, null, 2)}\n`);
387
+ let remote = hydratedRelease(releaseById(activeDraft.id)).assets
388
+ ?.find((asset) => asset.name === 'current-release.json');
389
+ let currentReleaseBytes;
390
+ try {
391
+ currentReleaseBytes = selectCurrentReleaseBytes({
392
+ remoteBytes: remote ? assetBytes(remote) : null,
393
+ localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
394
+ generatedBytes: generatedCurrentRelease,
395
+ identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
396
+ });
397
+ } catch (error) {
398
+ return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
399
+ }
400
+ if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
401
+ if (!remote) {
402
+ command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
403
+ remote = hydratedRelease(releaseById(activeDraft.id)).assets
404
+ ?.find((asset) => asset.name === 'current-release.json');
405
+ }
406
+ if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
407
+ return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
408
+ }
409
+ }
410
+ return result;
411
+ },
412
+ };
413
+ }