ruvnet-brain 4.3.8 → 4.3.9

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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # 🧠 RuvNet Brain
6
6
 
7
- ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.8 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.8-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
7
+ ### 🧠 RuvNet Brain — [![RuvNet Brain version 4.3.9 — updated 2026-07-30 03:24 EDT](https://img.shields.io/badge/version_4.3.9-updated_2026--07--30_03:24_EDT-1E90FF?style=for-the-badge&labelColor=0757BA)](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
8
8
 
9
9
  **A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.8",
3
+ "version": "4.3.9",
4
4
  "description": "One-command installer for RuvNet Brain — a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
3
  "description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships an enforced UserPromptSubmit retrieve-and-inject grounding hook that sharply reduces drift.",
4
- "version": "4.3.8",
4
+ "version": "4.3.9",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ruvnet-brain",
3
- "version": "4.3.8",
3
+ "version": "4.3.9",
4
4
  "description": "Source-grounded RuvNet knowledge, lifecycle enforcement, and learning for Codex.",
5
5
  "author": {
6
6
  "name": "Stuart Kerr"
@@ -24,6 +24,32 @@ const HEX40 = /^[a-f0-9]{40}$/;
24
24
  const HEX64 = /^[a-f0-9]{64}$/;
25
25
  const normalizedDigest = (value) => String(value || '').replace(/^sha256:/, '');
26
26
 
27
+ function failedCanaryCases(retrieval) {
28
+ return retrieval.cases.filter(({ cohort, status, retrievalHit, citationResolved }) =>
29
+ status !== 'COMPLETED' || retrievalHit !== true || (cohort === 'delta' && citationResolved !== true));
30
+ }
31
+
32
+ function publicVerificationFailure({ os, identity, failures, completedLeaves }) {
33
+ const payload = {
34
+ schemaVersion: 1,
35
+ kind: 'ruvnet-brain-public-verification-failure',
36
+ os,
37
+ sourceSha: identity.candidateSha,
38
+ artifactSha256: identity.packageSha256,
39
+ bundleSha256: identity.bundleSha256,
40
+ failures: failures.map(({ mode, reason, retrieval }) => ({
41
+ mode,
42
+ reason,
43
+ metrics: retrieval.metrics,
44
+ failedCaseIds: failedCanaryCases(retrieval).map(({ id }) => id),
45
+ failedCases: failedCanaryCases(retrieval),
46
+ retrievalReceiptSha256: retrieval.receiptSha256,
47
+ })),
48
+ completedLeaves: completedLeaves.map(({ mode, leafSha256 }) => ({ mode, leafSha256 })),
49
+ };
50
+ return { ...payload, failureSha256: digest(payload) };
51
+ }
52
+
27
53
  function validateReleaseIdentity(identity) {
28
54
  if (identity?.repository !== 'stuinfla/ruvnet-brain' || identity?.package !== 'ruvnet-brain'
29
55
  || !HEX40.test(String(identity.candidateSha || '')) || !HEX64.test(String(identity.payloadId || ''))
@@ -118,6 +144,7 @@ export async function createPublicVerificationLane({
118
144
  releaseTransactionId: transactionIdFor(identity),
119
145
  };
120
146
  const leaves = [];
147
+ const failures = [];
121
148
  for (const mode of PUBLIC_VERIFICATION_MODES) {
122
149
  const installed = publication.installed?.[RECEIPT_MODE_NAMES[mode]];
123
150
  if (installed?.status !== 'PASS' || installed.doctorExit !== 0 || installed.version !== identity.version
@@ -132,10 +159,16 @@ export async function createPublicVerificationLane({
132
159
  search: ({ query, k }) => adapter.searchInstalled({ mode, query, k }),
133
160
  citationResolver: (matched, expected) => adapter.resolveInstalledCitation({ mode, matched, expected }),
134
161
  });
135
- const verifiedRetrieval = validateRetrievalCanaryReceipt(
136
- retrieval,
137
- { plan: retrievalPlan, requireAcceptance: true },
138
- );
162
+ let verifiedRetrieval;
163
+ try {
164
+ verifiedRetrieval = validateRetrievalCanaryReceipt(
165
+ retrieval,
166
+ { plan: retrievalPlan, requireAcceptance: true },
167
+ );
168
+ } catch (error) {
169
+ failures.push({ mode, reason: error.message, retrieval });
170
+ continue;
171
+ }
139
172
  leaves.push(createPublicVerificationLeaf({
140
173
  ...common,
141
174
  os,
@@ -152,6 +185,11 @@ export async function createPublicVerificationLane({
152
185
  unknown: 0,
153
186
  }));
154
187
  }
188
+ if (failures.length) {
189
+ const error = new Error(`retrieval canary acceptance failed for ${failures.map(({ mode }) => mode).join(', ')}`);
190
+ error.publicVerificationFailure = publicVerificationFailure({ os, identity, failures, completedLeaves: leaves });
191
+ throw error;
192
+ }
155
193
  if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
156
194
  throw new Error('public verification lane modes are incomplete');
157
195
  }
@@ -211,6 +249,13 @@ export async function generatePublicVerificationLane({
211
249
  fs.mkdirSync(path.dirname(output), { recursive: true });
212
250
  fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
213
251
  return receipt;
252
+ } catch (error) {
253
+ if (error.publicVerificationFailure && !fs.existsSync(output)) {
254
+ fs.mkdirSync(path.dirname(output), { recursive: true });
255
+ fs.writeFileSync(output, `${JSON.stringify(error.publicVerificationFailure, null, 2)}\n`,
256
+ { flag: 'wx', mode: 0o600 });
257
+ }
258
+ throw error;
214
259
  } finally {
215
260
  await adapter.dispose?.();
216
261
  fs.rmSync(temp, { recursive: true, force: true });
@@ -29,30 +29,62 @@ const DEADLINE_MS = 30_000;
29
29
  const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
30
30
  const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
31
31
  const receiptDigest = (candidate) => String(candidate?.artifact?.sha256 || '').replace(/^sha256:/, '');
32
-
32
+ export function commandInvocation(name, args, { platform = process.platform, env = process.env } = {}) {
33
+ if (platform === 'win32' && /^(?:npm|npx)$/i.test(name)) return {
34
+ executable: env.ComSpec || 'cmd.exe', args: ['/d', '/c', `${name}.cmd`, ...args],
35
+ };
36
+ return { executable: name, args };
37
+ }
33
38
  function command(name, args, options = {}) {
34
- const result = spawnSync(name, args, { encoding: 'utf8', ...options });
39
+ const { platform = process.platform, ...spawnOptions } = options;
40
+ const invocation = commandInvocation(name, args, { platform, env: spawnOptions.env || process.env });
41
+ const result = spawnSync(invocation.executable, invocation.args, { encoding: 'utf8', ...spawnOptions });
35
42
  if (result.error || result.status !== 0) {
36
43
  const detail = String(result.stderr || result.stdout || result.error?.message || '').trim();
37
44
  throw new Error(`${name} ${args.join(' ')} failed: ${detail || `exit ${result.status}`}`);
38
45
  }
39
46
  return String(result.stdout || '').trim();
40
47
  }
41
-
42
- function locate(name) {
43
- try { return command('which', [name]); } catch { return null; }
48
+ function locate(name, { platform = process.platform } = {}) {
49
+ const query = platform === 'win32' ? (name === 'node' ? 'node.exe' : `${name}.cmd`) : name;
50
+ try {
51
+ return command(platform === 'win32' ? 'where.exe' : 'which', [query], { platform })
52
+ .split(/\r?\n/).find(Boolean) || null;
53
+ } catch { return null; }
44
54
  }
45
-
46
- function isolatedPath(mode, temp) {
55
+ export function createIsolatedPath(mode, temp, { platform = process.platform, env = process.env,
56
+ resolve = (name) => locate(name, { platform }) } = {}) {
47
57
  const bin = path.join(temp, `bin-${mode}`);
48
58
  fs.mkdirSync(bin);
49
59
  const hosts = mode === 'claudeOnly' ? ['claude'] : mode === 'codexOnly' ? ['codex'] : ['claude', 'codex'];
50
- for (const name of ['node', 'npm', ...hosts]) {
51
- const target = locate(name);
60
+ const names = platform === 'win32' ? ['npm', ...hosts] : ['node', 'npm', ...hosts];
61
+ for (const name of names) {
62
+ const target = resolve(name);
52
63
  if (!target) throw new Error(`${name} CLI unavailable for ${mode} public host fixture`);
53
- fs.symlinkSync(target, path.join(bin, name));
64
+ if (platform === 'win32') {
65
+ fs.writeFileSync(path.join(bin, `${name}.cmd`), `@call "${target}" %*\r\n`, { flag: 'wx' });
66
+ } else {
67
+ fs.symlinkSync(target, path.join(bin, name));
68
+ }
69
+ }
70
+ if (platform !== 'win32') return `${bin}:/usr/bin:/bin`;
71
+ const node = resolve('node');
72
+ if (!node) throw new Error(`node CLI unavailable for ${mode} public host fixture`);
73
+ return [bin, path.dirname(node), path.join(env.SystemRoot || 'C:\\Windows', 'System32'),
74
+ path.join(env.ProgramFiles || 'C:\\Program Files', 'Git', 'usr', 'bin')].join(path.win32.delimiter);
75
+ }
76
+ export function stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot }) {
77
+ const source = path.resolve(bundlePath || '');
78
+ let stat;
79
+ try { stat = fs.lstatSync(source); } catch { throw new Error('verified public bundle is missing'); }
80
+ if (!stat.isFile() || stat.isSymbolicLink() || sha256(source) !== bundleSha256) {
81
+ throw new Error('verified bundle digest mismatch before installation');
54
82
  }
55
- return `${bin}:/usr/bin:/bin`;
83
+ const destination = path.join(packageRoot, 'dist', 'ruvnet-brain.zip');
84
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
85
+ fs.copyFileSync(source, destination, fs.constants.COPYFILE_EXCL);
86
+ if (sha256(destination) !== bundleSha256) throw new Error('staged verified bundle digest mismatch');
87
+ return destination;
56
88
  }
57
89
 
58
90
  async function download(url, destination, headers = {}) {
@@ -134,7 +166,8 @@ export function assertInstalledPayload(sourceRoot, installedRoot) {
134
166
  return checked;
135
167
  }
136
168
 
137
- function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
169
+ export function rpcSearch(server, env, query, k = 5, timeoutMs = DEADLINE_MS) {
170
+ if (!Number.isSafeInteger(k) || k < 1 || k > 50) throw new Error(`invalid search result count: ${k}`);
138
171
  return new Promise((resolve, reject) => {
139
172
  const child = spawn(process.execPath, [server], { env, stdio: ['pipe', 'pipe', 'pipe'] });
140
173
  let stdout = '';
@@ -175,7 +208,7 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
175
208
  const listed = await call(2, 'tools/list');
176
209
  if (!listed.result?.tools?.some((tool) => tool.name === 'search_ruvnet')) throw new Error('installed MCP does not advertise search_ruvnet');
177
210
  const started = performance.now();
178
- const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k: 5 } });
211
+ const searched = await call(3, 'tools/call', { name: 'search_ruvnet', arguments: { query, k } });
179
212
  const broadMs = Math.round(performance.now() - started);
180
213
  const text = (searched.result?.content || []).map((item) => item.text || '').join('\n');
181
214
  if (searched.error || searched.result?.isError || /search_ruvnet error:/i.test(text)) throw new Error(`installed Brain search failed: ${text.slice(0, 400)}`);
@@ -213,14 +246,17 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
213
246
  return { path: destination, tag: release.tag_name, sha: tagCommit(root, tag) };
214
247
  },
215
248
 
216
- async installHosts({ artifactPath, artifactSha256, version }) {
249
+ async installHosts({ artifactPath, artifactSha256, bundlePath, bundleSha256, version }) {
217
250
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-install-'));
218
251
  installTemp = temp;
219
252
  const packageRoot = path.join(temp, 'package');
220
253
  command('tar', ['-xzf', artifactPath, '-C', temp]);
254
+ stageVerifiedBundle({ bundlePath, bundleSha256, packageRoot });
221
255
  const sealedPlugin = path.join(packageRoot, 'plugin');
222
256
  const results = {};
223
257
  let bundle = null;
258
+ const sharedModelCache = path.join(temp, 'models-cache');
259
+ fs.mkdirSync(sharedModelCache, { recursive: true });
224
260
  // Derived from HOST_MODES, so a fourth host shape is added in ONE place and this loop
225
261
  // cannot fall behind the staged-side check the way it did.
226
262
  for (const mode of HOST_MODES.map((m) => RECEIPT_MODE_NAMES[m])) {
@@ -233,26 +269,27 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
233
269
  const env = {
234
270
  ...process.env,
235
271
  HOME: home,
272
+ ...(process.platform === 'win32' ? { USERPROFILE: home } : {}),
236
273
  CODEX_HOME: codexHome,
237
274
  RUVNET_BRAIN_HOME: brainHome,
238
275
  RUVNET_BRAIN_KB: kb,
276
+ KB_MODEL_CACHE: sharedModelCache,
239
277
  // Env comes from the ONE variant table (host-install-matrix VARIANTS.published), not a
240
278
  // second hand-written copy — this file's own header promises "ONE doctor rule and ONE mode
241
279
  // vocabulary, shared with the staged-side check", and the copy had already drifted: it
242
280
  // omitted the Codex hook-trust bypass the staged side carries, which failed the seal on
243
281
  // every release.
244
282
  ...VARIANTS.published.env({ packageRoot }),
283
+ RUVNET_STRICT_INSTALL: '0',
245
284
  CI: 'true',
246
- PATH: isolatedPath(mode, temp),
285
+ PATH: createIsolatedPath(mode, temp),
247
286
  };
248
287
  const installer = path.join(packageRoot, 'bin', 'install.mjs');
249
288
  command(process.execPath, [
250
289
  installer, '--yes', '--force', '--version', `v${version}`,
251
290
  '--no-nightly-prompt', '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline',
291
+ '--no-selfcheck', '--no-verify',
252
292
  ], { env, cwd: packageRoot, timeout: 1_200_000, maxBuffer: 32 * 1024 * 1024 });
253
- command(process.execPath, [installer, '--doctor', '--hooks'], {
254
- env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
255
- });
256
293
 
257
294
  const verified = {};
258
295
  if (mode !== 'codexOnly') {
@@ -270,7 +307,11 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
270
307
  throw new Error(`${mode} installed public Brain bundle version mismatch`);
271
308
  }
272
309
  bundle ||= { brainVersion: source.brainVersion, releaseTag: source.releaseTag };
273
- const searched = await rpcSearch(findMcpServer(home), env, 'How does RuvNet Brain prove a public release artifact?', DEADLINE_MS);
310
+ const searched = await rpcSearch(findMcpServer(home), env,
311
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
312
+ command(process.execPath, [installer, '--doctor', '--hooks'], {
313
+ env, cwd: packageRoot, timeout: 300_000, maxBuffer: 32 * 1024 * 1024,
314
+ });
274
315
  results[mode] = {
275
316
  status: 'PASS', doctorExit: 0, version, artifactSha256,
276
317
  functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
@@ -291,16 +332,17 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
291
332
  if (rvfs.length === 0) throw new Error('installed public Brain has no ruvnet-brain self RVF');
292
333
  const server = path.join(installContext.home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
293
334
  if (!fs.existsSync(server)) throw new Error('installed persistent MCP server is missing');
294
- const result = await rpcSearch(server, installContext.env, 'How does RuvNet Brain prove a public release artifact?', DEADLINE_MS);
335
+ const result = await rpcSearch(server, installContext.env,
336
+ 'How does RuvNet Brain prove a public release artifact?', 5, DEADLINE_MS);
295
337
  const readiness = readJson(path.join(installContext.brainHome, 'mcp-readiness.json'));
296
338
  if (readiness.state !== 'ready') throw new Error(`installed Brain readiness is ${readiness.state || 'missing'}`);
297
339
  return { status: 'PASS', selfStore: true, broadMs: result.broadMs, deadlineMs: DEADLINE_MS };
298
340
  },
299
341
 
300
- async searchInstalled({ mode, query }) {
342
+ async searchInstalled({ mode, query, k }) {
301
343
  const context = installContexts.get(mode);
302
344
  if (!context) throw new Error(`${mode} public host is not installed`);
303
- const result = await rpcSearch(findMcpServer(context.home), context.env, query, DEADLINE_MS);
345
+ const result = await rpcSearch(findMcpServer(context.home), context.env, query, k, DEADLINE_MS);
304
346
  return parseCitations(result.text).map((citation) => ({
305
347
  repo: citation.repo.toLowerCase(),
306
348
  path: citation.docPath,
@@ -400,7 +442,8 @@ export async function generatePublicationReceipt({
400
442
  if (npm.version !== candidate.version || (npm.sha && npm.sha !== candidate.sha)) throw new Error('npm public identity mismatch');
401
443
  if (github.tag !== candidate.tag || github.sha !== candidate.sha) throw new Error('GitHub public identity mismatch');
402
444
 
403
- const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest, version: candidate.version });
445
+ const installed = await adapter.installHosts({ artifactPath: npm.path, artifactSha256: digest,
446
+ bundlePath: githubBundle.path, bundleSha256: githubBundleDigest, version: candidate.version });
404
447
  const brain = await adapter.probeBrain({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
405
448
  const surface = await adapter.probePublishedSurface({ sha: candidate.sha, artifactSha256: digest, version: candidate.version });
406
449
  const publication = {
@@ -0,0 +1,101 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { canonicalJson, transactionIdFor, verifyReceipt } from './release-transaction.mjs';
5
+
6
+ const jsonBytes = (value) => Buffer.from(`${JSON.stringify(value, null, 2)}\n`);
7
+
8
+ function trustedEvidenceDirectory(root) {
9
+ const evidenceRoot = path.join(path.resolve(root), 'release-evidence');
10
+ let stat;
11
+ try { stat = fs.lstatSync(evidenceRoot); } catch { throw new Error('release-evidence directory is missing'); }
12
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
13
+ throw new Error('release-evidence must be a trusted directory');
14
+ }
15
+ return evidenceRoot;
16
+ }
17
+
18
+ export function resolvePublicationHandoffPaths({ root, identityPath, receiptPath } = {}) {
19
+ const evidenceRoot = trustedEvidenceDirectory(root);
20
+ const resolveTarget = (configured, label) => {
21
+ if (typeof configured !== 'string' || !configured.trim()) throw new Error(`${label} output is required`);
22
+ const target = path.resolve(root, configured);
23
+ if (path.dirname(target) !== evidenceRoot || path.extname(target) !== '.json') {
24
+ throw new Error(`${label} output must be a direct JSON file inside release-evidence`);
25
+ }
26
+ if (fs.existsSync(target)) throw new Error(`${label} output must be a new file`);
27
+ return target;
28
+ };
29
+ const identity = resolveTarget(identityPath, 'release identity');
30
+ const receipt = resolveTarget(receiptPath, 'channel receipt');
31
+ if (identity === receipt) throw new Error('release identity and channel receipt outputs must differ');
32
+ return { evidenceRoot, identity, receipt };
33
+ }
34
+
35
+ export function validatePublicationHandoff({ identity, receipt, publicKey } = {}) {
36
+ verifyReceipt(receipt, publicKey);
37
+ if (receipt.schemaVersion !== 3 || receipt.state !== 'channels-converged'
38
+ || receipt.observation?.verdict !== 'PUBLISHED_NOT_VERIFIED') {
39
+ throw new Error('channel receipt is not signed PUBLISHED_NOT_VERIFIED convergence evidence');
40
+ }
41
+ if (receipt.transactionId !== transactionIdFor(identity)
42
+ || canonicalJson(receipt.identity) !== canonicalJson(identity)) {
43
+ throw new Error('channel receipt identity differs from the release identity');
44
+ }
45
+ return receipt;
46
+ }
47
+
48
+ function writePrivateTemp(directory, label, bytes) {
49
+ const temp = path.join(directory, `.${label}.${process.pid}.${crypto.randomUUID()}.tmp`);
50
+ const fd = fs.openSync(temp, 'wx', 0o600);
51
+ let closed = false;
52
+ try {
53
+ fs.writeFileSync(fd, bytes);
54
+ fs.fsyncSync(fd);
55
+ fs.closeSync(fd);
56
+ closed = true;
57
+ return temp;
58
+ } catch (error) {
59
+ if (!closed) {
60
+ try { fs.closeSync(fd); } catch { /* preserve the write failure */ }
61
+ }
62
+ fs.rmSync(temp, { force: true });
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ function removeOwnedLink(destination, temporary) {
68
+ try {
69
+ const linked = fs.lstatSync(destination);
70
+ const source = fs.lstatSync(temporary);
71
+ if (linked.dev === source.dev && linked.ino === source.ino) fs.unlinkSync(destination);
72
+ } catch (error) {
73
+ if (error.code !== 'ENOENT') throw error;
74
+ }
75
+ }
76
+
77
+ export function materializePublicationHandoff({ paths, identity, receipt, publicKey } = {}) {
78
+ if (!paths?.evidenceRoot || path.dirname(paths.identity || '') !== paths.evidenceRoot
79
+ || path.dirname(paths.receipt || '') !== paths.evidenceRoot || paths.identity === paths.receipt) {
80
+ throw new Error('publication handoff paths were not validated');
81
+ }
82
+ validatePublicationHandoff({ identity, receipt, publicKey });
83
+ const temporary = [];
84
+ let identityLinked = false;
85
+ let receiptLinked = false;
86
+ try {
87
+ temporary.push(writePrivateTemp(paths.evidenceRoot, 'release-identity', jsonBytes(identity)));
88
+ temporary.push(writePrivateTemp(paths.evidenceRoot, 'channels-converged', jsonBytes(receipt)));
89
+ fs.linkSync(temporary[0], paths.identity);
90
+ identityLinked = true;
91
+ fs.linkSync(temporary[1], paths.receipt);
92
+ receiptLinked = true;
93
+ } catch (error) {
94
+ if (receiptLinked) removeOwnedLink(paths.receipt, temporary[1]);
95
+ if (identityLinked) removeOwnedLink(paths.identity, temporary[0]);
96
+ throw new Error(`publication handoff materialization failed: ${error.message}`);
97
+ } finally {
98
+ for (const temp of temporary) fs.rmSync(temp, { force: true });
99
+ }
100
+ return { identityPath: paths.identity, receiptPath: paths.receipt };
101
+ }
@@ -31,6 +31,7 @@ import fs from 'node:fs';
31
31
  import crypto from 'node:crypto';
32
32
  import { validateProtectedPublishEnvironment, validateProtectedPublishInvocation } from './protected-release-invocation.mjs';
33
33
  import { runReleaseTransaction } from './release-transaction.mjs';
34
+ import { materializePublicationHandoff, resolvePublicationHandoffPaths } from './release-publication-handoff.mjs';
34
35
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
35
36
  import { stagedHostVerifier } from './staged-host-verifier.mjs';
36
37
  import { verifyPayload } from './release-payload.mjs';
@@ -44,6 +45,7 @@ let publicationReceiptPath = null;
44
45
  let protectedReleaseMode = 'strict';
45
46
  let verifiedPayload = null;
46
47
  let aggregateEnvelope = null;
48
+ let publicationHandoffPaths = null;
47
49
  const c = { g: (s) => `\x1b[32m${s}\x1b[0m`, r: (s) => `\x1b[31m${s}\x1b[0m`, y: (s) => `\x1b[33m${s}\x1b[0m`, b: (s) => `\x1b[1m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m` };
48
50
  const V = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'plugin/.claude-plugin/plugin.json'), 'utf8')).version;
49
51
 
@@ -222,6 +224,11 @@ if (PUBLISH) {
222
224
  console.error(`\n${c.r('✗ PROTECTED RELEASE GATE FAILED')}\n publication receipt output must be a new file inside release-evidence\n`);
223
225
  process.exit(1);
224
226
  }
227
+ publicationHandoffPaths = resolvePublicationHandoffPaths({
228
+ root: ROOT,
229
+ identityPath: process.env.RUVNET_RELEASE_IDENTITY,
230
+ receiptPath: process.env.RUVNET_CHANNEL_RECEIPT,
231
+ });
225
232
  const payloadManifestPath = path.resolve(ROOT, process.env.RUVNET_CANDIDATE_PAYLOAD || '');
226
233
  const payloadSignaturePath = path.resolve(ROOT, process.env.RUVNET_CANDIDATE_PAYLOAD_SIGNATURE || '');
227
234
  const aggregatePath = path.resolve(ROOT, process.env.RUVNET_AGGREGATE_ENVELOPE || '');
@@ -375,6 +382,7 @@ if (PUBLISH) {
375
382
  };
376
383
  const privatePem = process.env.RUVNET_SIGNING_KEY;
377
384
  if (!privatePem) throw new Error('RUVNET_SIGNING_KEY is required for signed transaction receipts');
385
+ const publicKey = crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8'));
378
386
  const finalReceipt = await runReleaseTransaction({
379
387
  identity, assets, adapter: liveReleaseProvider({
380
388
  root: ROOT,
@@ -382,10 +390,11 @@ if (PUBLISH) {
382
390
  publicationReceipt: process.env.RUVNET_PUBLICATION_RECEIPT,
383
391
  }),
384
392
  privateKey: crypto.createPrivateKey(privatePem),
385
- publicKey: crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8')),
393
+ publicKey,
386
394
  hostVerifier: stagedHostVerifier({ assets, identity }),
387
395
  });
388
396
  if (finalReceipt.state !== 'channels-converged') throw new Error(`release transaction stopped at ${finalReceipt.state}`);
397
+ materializePublicationHandoff({ paths: publicationHandoffPaths, identity, receipt: finalReceipt, publicKey });
389
398
  } else {
390
399
  step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
391
400
  }