ruvnet-brain 4.3.1 → 4.3.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 (56) hide show
  1. package/README.md +12 -13
  2. package/bin/install.mjs +18 -8
  3. package/console/app.js +4 -4
  4. package/data/model-catalog.json +3 -3
  5. package/kb/verify-citation.mjs +32 -5
  6. package/package.json +13 -9
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/mcp/managed-cli-interface.mjs +4 -2
  10. package/plugin/scripts/codex-hook-adapter.mjs +12 -7
  11. package/plugin/scripts/codex-hook-events.mjs +39 -0
  12. package/plugin/scripts/coverage-integrity.mjs +4 -4
  13. package/plugin/scripts/hook-shim.mjs +9 -7
  14. package/plugin/scripts/learn-capture.sh +20 -1
  15. package/plugin/scripts/learn-flush.mjs +7 -1
  16. package/plugin/scripts/lesson-gate.mjs +17 -2
  17. package/plugin/scripts/lesson-hooks.sh +17 -2
  18. package/plugin/scripts/session-start-core.mjs +47 -11
  19. package/plugin/skills/release-proof/SKILL.md +19 -10
  20. package/plugin/skills/release-proof/references/receipt-contract.md +18 -8
  21. package/scripts/brain-stamp-resolve.mjs +22 -0
  22. package/scripts/brain-stamp.mjs +5 -3
  23. package/scripts/build-bundle.mjs +41 -4
  24. package/scripts/candidate-host-evidence.mjs +9 -2
  25. package/scripts/ci/step-watchdog.mjs +86 -0
  26. package/scripts/convergence-manifest.mjs +66 -0
  27. package/scripts/distill-project.mjs +22 -4
  28. package/scripts/eval-brain.mjs +13 -3
  29. package/scripts/execution-policy.mjs +111 -0
  30. package/scripts/execution-preflight.mjs +18 -0
  31. package/scripts/git-hooks/pre-push +16 -0
  32. package/scripts/health-repair.mjs +5 -1
  33. package/scripts/host-install-matrix.mjs +247 -40
  34. package/scripts/integration-evidence.mjs +73 -0
  35. package/scripts/nightly-wrapper.sh +4 -3
  36. package/scripts/onboarding-console.mjs +3 -3
  37. package/scripts/prepublication-evidence.mjs +174 -0
  38. package/scripts/protected-release-invocation.mjs +32 -4
  39. package/scripts/public-verification-aggregate.mjs +27 -54
  40. package/scripts/public-verification-inputs.mjs +5 -3
  41. package/scripts/qa-runner.mjs +72 -0
  42. package/scripts/qe/agentic-qe-4.3.mjs +195 -0
  43. package/scripts/qe/aggregate-4.3.mjs +42 -0
  44. package/scripts/record-lesson.mjs +28 -6
  45. package/scripts/refresh-model-catalog.mjs +32 -3
  46. package/scripts/release-authority.mjs +19 -7
  47. package/scripts/release-convergence-watchdog.mjs +10 -22
  48. package/scripts/release-projection.mjs +76 -32
  49. package/scripts/release-transaction.mjs +1 -1
  50. package/scripts/release.mjs +9 -15
  51. package/scripts/retrieval-canary.mjs +14 -6
  52. package/scripts/set-version.mjs +31 -0
  53. package/scripts/source-coverage.mjs +11 -2
  54. package/scripts/staged-host-verifier.mjs +2 -2
  55. package/scripts/verify-channels.mjs +1 -1
  56. package/scripts/wired-check.mjs +1 -0
@@ -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,13 +29,13 @@ 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
34
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
35
+ import { stagedHostVerifier } from './staged-host-verifier.mjs';
35
36
  import { verifyPayload } from './release-payload.mjs';
36
37
 
37
38
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
38
- const IS_MAIN = path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url);
39
39
  const PUBLISH = process.argv.includes('--publish');
40
40
  const CORPUS_SEED = process.argv.includes('--corpus-seed');
41
41
  let protectedCandidate = null;
@@ -87,9 +87,9 @@ export function runProtectedCorpusSeed({
87
87
  root = ROOT,
88
88
  run = (command, args, options) => spawnSync(command, args, { encoding: 'utf8', ...options }),
89
89
  } = {}) {
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');
90
+ const environmentFailures = validateProtectedPublishEnvironment(env);
91
+ if (environmentFailures.length) {
92
+ corpusFailure(environmentFailures.join('; '));
93
93
  }
94
94
 
95
95
  const tag = cliArg(argv, '--corpus-tag');
@@ -190,7 +190,7 @@ export function runProtectedCorpusSeed({
190
190
  return { tag, target, repository: repo, archiveSha256, receiptSha256: sha256File(receiptFile) };
191
191
  }
192
192
 
193
- if (IS_MAIN && CORPUS_SEED) {
193
+ if (CORPUS_SEED) {
194
194
  try {
195
195
  const result = runProtectedCorpusSeed();
196
196
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
@@ -198,7 +198,7 @@ if (IS_MAIN && CORPUS_SEED) {
198
198
  console.error(error.message);
199
199
  process.exitCode = 1;
200
200
  }
201
- } else if (IS_MAIN) {
201
+ } else {
202
202
 
203
203
  console.log(`\n${c.b('RuvNet Brain — release / definition-of-done')} ${c.dim('· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' · shipping ' + V())}\n`);
204
204
 
@@ -373,9 +373,6 @@ if (PUBLISH) {
373
373
  corpusSeedSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.corpusSeedPath)).digest('hex'),
374
374
  generationLedgerSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.generationLedgerPath)).digest('hex'),
375
375
  };
376
- const identityPath = path.resolve(ROOT, process.env.RUVNET_RELEASE_IDENTITY || 'release-evidence/release-identity.json');
377
- if (fs.existsSync(identityPath)) throw new Error(`refusing to overwrite release identity: ${identityPath}`);
378
- fs.writeFileSync(identityPath, `${JSON.stringify(identity, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
379
376
  const privatePem = process.env.RUVNET_SIGNING_KEY;
380
377
  if (!privatePem) throw new Error('RUVNET_SIGNING_KEY is required for signed transaction receipts');
381
378
  const finalReceipt = await runReleaseTransaction({
@@ -386,12 +383,9 @@ if (PUBLISH) {
386
383
  }),
387
384
  privateKey: crypto.createPrivateKey(privatePem),
388
385
  publicKey: crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8')),
386
+ hostVerifier: stagedHostVerifier({ assets, identity }),
389
387
  });
390
388
  if (finalReceipt.state !== 'channels-converged') throw new Error(`release transaction stopped at ${finalReceipt.state}`);
391
- const channelReceiptPath = path.resolve(ROOT,
392
- process.env.RUVNET_CHANNEL_RECEIPT || 'release-evidence/channels-converged-receipt.json');
393
- if (fs.existsSync(channelReceiptPath)) throw new Error(`refusing to overwrite channel receipt: ${channelReceiptPath}`);
394
- fs.writeFileSync(channelReceiptPath, `${JSON.stringify(finalReceipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
395
389
  } else {
396
390
  step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
397
391
  }
@@ -404,7 +398,7 @@ if (!PUBLISH) {
404
398
  }
405
399
 
406
400
  if (PUBLISH) {
407
- console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))} — npm and GitHub expose the sealed candidate; public install verification is still required.\n`);
401
+ console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}`);
408
402
  } else {
409
403
  console.log(`\n${c.g(c.b('✓✓✓ PREFLIGHT PASS — NOT PUBLISHED'))} — the committed candidate passed every check-only gate.\n`);
410
404
  }
@@ -108,14 +108,17 @@ export function validateRetrievalQueryEvidence(evidence) {
108
108
  return evidence;
109
109
  }
110
110
 
111
- export function verifyQueryOracleSource(queryEvidence, candidateSourceSha, { cwd = process.cwd(), run = spawnSync } = {}) {
111
+ export function verifyQueryOracleSource(queryEvidence, candidateSourceSha, {
112
+ cwd = process.cwd(), run = spawnSync, allowSquashedSource = false,
113
+ } = {}) {
112
114
  validateRetrievalQueryEvidence(queryEvidence);
113
115
  if (!HEX40.test(String(candidateSourceSha || '')) || queryEvidence.sourceCommit === candidateSourceSha) {
114
116
  throw new Error('query oracle source identity is invalid');
115
117
  }
116
118
  const ancestor = run('git', ['merge-base', '--is-ancestor', queryEvidence.sourceCommit, candidateSourceSha],
117
119
  { cwd, encoding: 'utf8' });
118
- if (ancestor.error || ancestor.status !== 0) throw new Error('query oracle source is not a strict candidate ancestor');
120
+ const strictAncestor = !ancestor.error && ancestor.status === 0;
121
+ if (!strictAncestor && !allowSquashedSource) throw new Error('query oracle source is not a strict candidate ancestor');
119
122
  const shown = run('git', ['show', `${queryEvidence.sourceCommit}:${queryEvidence.sourcePath}`],
120
123
  { cwd, encoding: null, maxBuffer: 64 * 1024 * 1024 });
121
124
  let sourcePayload;
@@ -184,7 +187,8 @@ export function validateRetrievalCanaryPlan(plan) {
184
187
  throw new Error('oracle denominator differs from eligible coverage');
185
188
  }
186
189
  if (new Set(ids).size !== ids.length) throw new Error('retrieval canary plan has duplicate case ids');
187
- if (!plan.cases.some(({ cohort }) => cohort === 'delta') || !plan.cases.some(({ cohort }) => cohort === 'legacy')) {
190
+ if ((!plan.cases.some(({ cohort }) => cohort === 'delta') && plan.noDelta !== true)
191
+ || !plan.cases.some(({ cohort }) => cohort === 'legacy')) {
188
192
  throw new Error('retrieval canary plan requires delta and legacy cohorts');
189
193
  }
190
194
  for (const row of plan.cases) {
@@ -236,7 +240,7 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
236
240
  return plan;
237
241
  }
238
242
  export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, coverageIdentity = null, queryEvidence, assetsDir = '.',
239
- readPassages = defaultReadPassages, legacySampleSize } = {}) {
243
+ readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false } = {}) {
240
244
  const checked = validateCoverageLedger(coverage);
241
245
  if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
242
246
  const coverageGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
@@ -294,12 +298,15 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
294
298
  const eligibleStores = ordered(eligible.map(storeOf));
295
299
  if (queryEvidence.queryStoreSetSha256 !== setDigest(eligibleStores)
296
300
  || canonicalJson(ordered(Object.keys(queryEvidence.queries))) !== canonicalJson(eligibleStores)) {
297
- throw new Error('independent query oracle does not cover the exact eligible store set');
301
+ const oracleStores = new Set(Object.keys(queryEvidence.queries));
302
+ const missing = eligibleStores.filter((store) => !oracleStores.has(store));
303
+ const extra = [...oracleStores].filter((store) => !eligibleStores.includes(store)).sort();
304
+ throw new Error(`independent query oracle does not cover the exact eligible store set (eligible=${eligibleStores.length}, oracle=${oracleStores.size}, missing=${missing.join(',') || 'none'}, extra=${extra.join(',') || 'none'})`);
298
305
  }
299
306
  const baselineStores = new Set(baseline.stores.map((name) => String(name).toLowerCase()));
300
307
  const delta = eligible.filter((row) => !baselineStores.has(storeOf(row)));
301
308
  const legacyPool = eligible.filter((row) => baselineStores.has(storeOf(row)));
302
- if (!delta.length || !legacyPool.length) throw new Error('coverage does not establish both failed-seed delta and legacy cohorts');
309
+ if ((!delta.length && !allowNoDelta) || !legacyPool.length) throw new Error('coverage does not establish both failed-seed delta and legacy cohorts');
303
310
  const passages = new Map(eligible.map((row) => [storeOf(row), readPassages(assetsDir, storeOf(row))]));
304
311
  const rankedLegacy = legacyPool.map((row) => ({ row, count: passages.get(storeOf(row)).length }))
305
312
  .sort((a, b) => a.count - b.count || storeOf(a.row).localeCompare(storeOf(b.row)));
@@ -376,6 +383,7 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
376
383
  queryStoreSetSha256: queryEvidence.queryStoreSetSha256,
377
384
  sourceCommit: queryEvidence.sourceCommit, sourceBlobSha256: queryEvidence.sourceBlobSha256,
378
385
  evidence: structuredClone(queryEvidence) },
386
+ noDelta: delta.length === 0,
379
387
  cohorts: { delta: delta.length, legacy: legacy.length,
380
388
  legacyStrata: [...strata.keys()].sort().map((stratum) => ({ stratum,
381
389
  population: rankedLegacy.filter((_entry, index) => Math.min(3, Math.floor(index * 4 / rankedLegacy.length)) === stratum).length,
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ // One release version command. The plugin manifest is the only editable source of truth;
3
+ // propagation and verification happen in the same process so a package-only bump cannot drift.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { spawnSync } from 'node:child_process';
8
+
9
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
+ const version = process.argv[2];
11
+ if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
12
+ console.error('usage: npm run version:set -- X.Y.Z[-channel]');
13
+ process.exit(2);
14
+ }
15
+ const manifest = path.join(root, 'plugin/.claude-plugin/plugin.json');
16
+ const doc = JSON.parse(fs.readFileSync(manifest, 'utf8'));
17
+ const previous = doc.version;
18
+ doc.version = version;
19
+ fs.writeFileSync(manifest, `${JSON.stringify(doc, null, 2)}\n`);
20
+ const sync = spawnSync(process.execPath, [path.join(root, 'scripts/sync-version.mjs')], {
21
+ cwd: root, encoding: 'utf8', stdio: 'inherit',
22
+ });
23
+ if (sync.status !== 0) {
24
+ console.error(`[version:set] propagation failed; source changed ${previous} -> ${version}, run version:sync after fixing the reported error`);
25
+ process.exit(sync.status || 1);
26
+ }
27
+ const check = spawnSync(process.execPath, [path.join(root, 'scripts/sync-version.mjs'), '--check'], {
28
+ cwd: root, encoding: 'utf8', stdio: 'inherit',
29
+ });
30
+ if (check.status !== 0) process.exit(check.status || 1);
31
+ console.log(`[version:set] ${previous} -> ${version}; all release surfaces agree`);
@@ -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',
@@ -97,7 +97,7 @@ async function run() {
97
97
  }
98
98
  check('npm latest matches shipping version', ok,
99
99
  ok ? `npm latest=${latest} · shipping=${V}`
100
- : `npm latest=${latest || '(view failed: ' + lastErr + ')'} · shipping=${V}${latest && latest !== V ? ' ← STILL STALE after ~' + (ATTEMPTS * 5) + 's: run `npm publish` + `npm dist-tag add ' + NPM_PKG + '@' + V + ' latest`' : ''}`);
100
+ : `npm latest=${latest || '(view failed: ' + lastErr + ')'} · shipping=${V}${latest && latest !== V ? ' ← STILL STALE after ~' + (ATTEMPTS * 5) + 's: dispatch the protected-release workflow for the exact candidate SHA' : ''}`);
101
101
  }
102
102
 
103
103
  // 2) the self-update manifest URL actually resolves (the 404 that stranded users)
@@ -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'],