ruvnet-brain 4.3.3 → 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.
@@ -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
+ }
@@ -565,7 +565,7 @@ export async function finalizeReleaseTransaction({
565
565
  lanes: aggregate.metrics.leaves,
566
566
  recallAt10: aggregate.metrics.recallAt10,
567
567
  deltaCitationRate: aggregate.metrics.deltaCitationRate,
568
- reviewReceipts: aggregate.reviews,
568
+ reviewReceipts: aggregate.reviews || [],
569
569
  },
570
570
  npm: snapshot.npm,
571
571
  github: snapshot.github,
@@ -29,8 +29,9 @@ import { fileURLToPath } from 'node:url';
29
29
  import { spawnSync, execFileSync } from 'node:child_process';
30
30
  import fs from 'node:fs';
31
31
  import crypto from 'node:crypto';
32
- import { validateProtectedPublishInvocation } from './protected-release-invocation.mjs';
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
 
@@ -87,9 +89,9 @@ export function runProtectedCorpusSeed({
87
89
  root = ROOT,
88
90
  run = (command, args, options) => spawnSync(command, args, { encoding: 'utf8', ...options }),
89
91
  } = {}) {
90
- if (env.GITHUB_ACTIONS !== 'true' || env.GITHUB_WORKFLOW !== 'protected-release'
91
- || env.GITHUB_REPOSITORY !== 'stuinfla/ruvnet-brain') {
92
- corpusFailure('publication is allowed only inside the protected-release GitHub workflow for stuinfla/ruvnet-brain');
92
+ const environmentFailures = validateProtectedPublishEnvironment(env);
93
+ if (environmentFailures.length) {
94
+ corpusFailure(environmentFailures.join('; '));
93
95
  }
94
96
 
95
97
  const tag = cliArg(argv, '--corpus-tag');
@@ -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
  }
@@ -399,7 +408,6 @@ if (!PUBLISH) {
399
408
 
400
409
  if (PUBLISH) {
401
410
  console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}`);
402
- console.log(`\n${c.g(c.b('✓✓✓ SHIPPED'))} — every gate passed and every live channel is current. ${c.dim('A user on any path (npm, npx, explainer, --update) gets the working, current build.')}\n`);
403
411
  } else {
404
412
  console.log(`\n${c.g(c.b('✓✓✓ PREFLIGHT PASS — NOT PUBLISHED'))} — the committed candidate passed every check-only gate.\n`);
405
413
  }
@@ -6,6 +6,7 @@ import { spawnSync } from 'node:child_process';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { readRvfGenerations, sha256File } from './rvf-generation.mjs';
8
8
  import { canonicalJson, coverageGenerationFor, digest, validateGistAggregateReceipt } from './coverage-integrity.mjs';
9
+ import { repositoryNames } from '../kb/card-lane.mjs';
9
10
 
10
11
  export { canonicalJson, digest } from './coverage-integrity.mjs';
11
12
 
@@ -272,7 +273,15 @@ export function classifyGist(gist, evidence) {
272
273
  };
273
274
  }
274
275
 
275
- function artifactEvidence(kbDir, ledger, cardStores, name) {
276
+ // ALIAS-AWARE, BECAUSE THE ROUTER IS. `cardStores` holds card HEADINGS; a store reachable only
277
+ // under an alias (e.g. `metaharness` via its `## agent-harness-generator` card) has no heading of
278
+ // its own, so a direct `cardStores.has(store)` reports it card-absent even though the router finds
279
+ // it. `kb/store-root.mjs`'s `darkStores()` and `scripts/brain-score.mjs`'s `readCoverage()` were
280
+ // already fixed for this exact conflation (ADR-058; metaharness is the standing example in both);
281
+ // this sibling computation, which feeds the committed `data/source-coverage.json` and
282
+ // `docs/RUVNET-COVERAGE.md` (ADR-069), never received it. `repositoryNames` is the router's own
283
+ // resolver (kb/card-lane.mjs), imported rather than reimplemented.
284
+ export function artifactEvidence(kbDir, ledger, cardStores, name) {
276
285
  const store = storeName(name);
277
286
  const receipt = Object.entries(ledger.stores).find(([key]) => key.toLowerCase() === store)?.[1] || null;
278
287
  const rvfFile = receipt?.file || `${store}.big.rvf`;
@@ -284,7 +293,7 @@ function artifactEvidence(kbDir, ledger, cardStores, name) {
284
293
  bytesVerified: Boolean(rvfPresent && receipt?.sha256 && sha256File(rvfPath) === receipt.sha256),
285
294
  passagesPresent: fs.existsSync(path.join(kbDir, `${store}.passages.jsonl`))
286
295
  && fs.statSync(path.join(kbDir, `${store}.passages.jsonl`)).size > 0,
287
- cardPresent: cardStores.has(store),
296
+ cardPresent: repositoryNames(store, kbDir).some((alias) => cardStores.has(storeName(alias))),
288
297
  };
289
298
  }
290
299
 
@@ -3,7 +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
+ import { runHostMatrixAsync } from './host-install-matrix.mjs';
7
7
 
8
8
  const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
9
9
  const locate = (name) => {
@@ -50,7 +50,7 @@ export function stagedHostVerifier({ assets, identity }) {
50
50
  // install shape than the one that runs after publication — so the two halves of a release
51
51
  // were judging different things and could not be compared. Only the STAGED-vs-PUBLISHED
52
52
  // difference is real, and it is now a named variant rather than a second implementation.
53
- const matrix = runHostMatrix({
53
+ const matrix = await runHostMatrixAsync({
54
54
  packageRoot: prepared.packageRoot,
55
55
  version: identity.version,
56
56
  variant: 'staged',
@@ -95,6 +95,7 @@ const STANDALONE = [
95
95
  + 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
96
96
  + 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
97
97
  ['release', 'the ship path, run by a human'],
98
+ ['execution-preflight', 'external orchestration boundary — invoked by the host before consequential Ruflo/Codex execution; no in-repo caller exists because the host supplies the live Brain and AgentDB receipts'],
98
99
  ['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
99
100
  + 'isolated writing agent to start and hand off a fix lane. Scheduling it would violate its safety '
100
101
  + 'boundary: it prepares evidence but never merges, pushes, publishes, deletes, or cleans worktrees'],