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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +115 -44
  3. package/console/app.js +32 -11
  4. package/console/style.css +5 -0
  5. package/package.json +1 -1
  6. package/plugin/.claude-plugin/plugin.json +1 -1
  7. package/plugin/.codex-plugin/plugin.json +1 -1
  8. package/plugin/hooks/codex-hooks.json +17 -17
  9. package/plugin/scripts/codex-hook-wrapper.mjs +72 -1
  10. package/plugin/scripts/continuation-gate.mjs +41 -1
  11. package/plugin/scripts/learn-flush.mjs +37 -2
  12. package/plugin/scripts/project-identity.mjs +89 -0
  13. package/plugin/scripts/ruflo-bin.mjs +81 -0
  14. package/plugin/scripts/session-snapshot-hook.mjs +4 -1
  15. package/plugin/scripts/session-start-core.mjs +19 -2
  16. package/plugin/skills/release-proof/scripts/release-proof.mjs +12 -5
  17. package/scripts/candidate-host-evidence.mjs +49 -0
  18. package/scripts/ci/learning-replay-codex-adapter.mjs +13 -1
  19. package/scripts/console-runtime-identity.mjs +74 -0
  20. package/scripts/distill-project.mjs +9 -1
  21. package/scripts/doc-currency.mjs +15 -2
  22. package/scripts/health-repair.mjs +88 -11
  23. package/scripts/host-install-matrix.mjs +155 -0
  24. package/scripts/learning-replay-contract.mjs +14 -4
  25. package/scripts/learning-replay-execution.mjs +4 -3
  26. package/scripts/learning-replay-fixture.mjs +23 -2
  27. package/scripts/memory-doctor.mjs +26 -23
  28. package/scripts/model-router-catalog.mjs +34 -0
  29. package/scripts/onboarding-console.mjs +34 -12
  30. package/scripts/publication-receipt.mjs +135 -34
  31. package/scripts/release-evidence-aggregate.mjs +55 -0
  32. package/scripts/release-payload.mjs +100 -0
  33. package/scripts/release-transaction-provider.mjs +260 -55
  34. package/scripts/release-transaction.mjs +295 -105
  35. package/scripts/release.mjs +41 -12
  36. package/scripts/rvf-generation.mjs +17 -0
  37. package/scripts/selfcheck.mjs +20 -5
  38. package/scripts/staged-host-verifier.mjs +27 -57
  39. package/scripts/sync-version.mjs +10 -10
@@ -159,17 +159,31 @@ export function resolveInstalledSurface({ home = os.homedir(), repo = null } = {
159
159
  for (const v of fs.readdirSync(cache)) {
160
160
  const root = path.join(cache, v);
161
161
  if (fs.existsSync(path.join(root, 'hooks', 'hooks.json'))) {
162
- candidates.push({ root, source: `installed:${v}`, mtime: fs.statSync(path.join(root, 'hooks', 'hooks.json')).mtimeMs });
162
+ candidates.push({ root, source: `installed:${v}`, hooksFile: path.join(root, 'hooks', 'hooks.json'), mtime: fs.statSync(path.join(root, 'hooks', 'hooks.json')).mtimeMs });
163
163
  }
164
164
  }
165
165
  } catch { /* no packed install on this machine */ }
166
166
  candidates.sort((a, b) => b.mtime - a.mtime); // newest generation wins; several can coexist
167
167
  const clone = path.join(home, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin');
168
168
  if (fs.existsSync(path.join(clone, 'hooks', 'hooks.json'))) {
169
- candidates.push({ root: clone, source: 'marketplace-clone', mtime: 0 });
169
+ candidates.push({ root: clone, source: 'marketplace-clone', hooksFile: path.join(clone, 'hooks', 'hooks.json'), mtime: 0 });
170
170
  }
171
+ // Codex installs the same immutable payload under its own cache. A Codex-only machine has no
172
+ // Claude marketplace clone, so treating that layout as "no plugin" made the real installed
173
+ // Codex hooks impossible to self-check (the release host matrix caught this). Keep the same
174
+ // newest-generation rule and read the Codex hook registry from the installed payload itself.
175
+ const codexCache = path.join(home, '.codex', 'plugins', 'cache', 'ruvnet-brain', 'ruvnet-brain');
176
+ try {
177
+ for (const v of fs.readdirSync(codexCache)) {
178
+ const root = path.join(codexCache, v);
179
+ if (fs.existsSync(path.join(root, 'hooks', 'codex-hooks.json'))) {
180
+ candidates.push({ root, source: `codex-installed:${v}`, hooksFile: path.join(root, 'hooks', 'codex-hooks.json'), mtime: fs.statSync(path.join(root, 'hooks', 'codex-hooks.json')).mtimeMs, codex: true });
181
+ }
182
+ }
183
+ } catch { /* no Codex plugin cache on this machine */ }
171
184
  if (repo && fs.existsSync(path.join(repo, 'plugin', 'hooks', 'hooks.json'))) {
172
- candidates.push({ root: path.join(repo, 'plugin'), source: 'checkout', mtime: 0 });
185
+ const root = path.join(repo, 'plugin');
186
+ candidates.push({ root, source: 'checkout', hooksFile: path.join(root, 'hooks', 'hooks.json'), mtime: 0 });
173
187
  }
174
188
  const chosen = candidates[0];
175
189
  if (!chosen) return { ok: false, reason: 'no installed ruvnet-brain plugin payload found on this machine' };
@@ -177,8 +191,9 @@ export function resolveInstalledSurface({ home = os.homedir(), repo = null } = {
177
191
  ok: true,
178
192
  root: chosen.root,
179
193
  source: chosen.source,
180
- hooksFile: path.join(chosen.root, 'hooks', 'hooks.json'),
194
+ hooksFile: chosen.hooksFile,
181
195
  shimFile: path.join(chosen.root, 'scripts', 'hook-shim.mjs'),
196
+ codex: Boolean(chosen.codex),
182
197
  alternates: candidates.slice(1).map((c) => c.source),
183
198
  };
184
199
  }
@@ -480,7 +495,7 @@ export async function runBattery({ home = os.homedir(), repo = null, cwd = os.tm
480
495
  const shimId = reg.shimIdIn(r.command);
481
496
  const entry = shimId ? table[shimId] : null;
482
497
  const contract = entry ? null : contracts.find((c) => reg.contractMatches(c, { layer: 'plugin', event: r.event, matcher: r.matcher, command: r.command }));
483
- const mode = entry?.mode ?? contract?.mode ?? null;
498
+ const mode = entry?.mode ?? contract?.mode ?? (s.codex ? 'advisory' : null);
484
499
  const handler = entry?.file ?? reg.basenamesIn(r.command).filter((b) => b !== 'hook-shim.mjs').pop() ?? null;
485
500
  const rec = { ...r, handler, shimId, mode };
486
501
 
@@ -3,6 +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
7
 
7
8
  const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
8
9
  const locate = (name) => {
@@ -18,15 +19,9 @@ const run = (name, args, options) => {
18
19
  return result;
19
20
  };
20
21
 
21
- export function classifyDoctorResult(result) {
22
- const output = `${result.stdout || ''}\n${result.stderr || ''}`;
23
- if (!result.error && result.status === 0) return { accepted: true, status: 'PASS', output };
24
- const pendingReview = result.status === 1
25
- && /Codex installed the Brain, but \d+ lifecycle hooks await review/i.test(output)
26
- && /Grounding PROVEN/i.test(output)
27
- && !/reader MISSING|search_ruvnet MISSING|host convergence incomplete|receipt is invalid/i.test(output);
28
- return { accepted: pendingReview, status: pendingReview ? 'PENDING_REVIEW' : 'FAIL', output };
29
- }
22
+ // The doctor verdict has ONE rule, in host-install-matrix.mjs. This name is kept because
23
+ // tests/unit/staged-host-verifier.test.mjs pins it, but it is now an alias, not a second copy.
24
+ export { classifyDoctor as classifyDoctorResult } from './host-install-matrix.mjs';
30
25
 
31
26
  const preparePackage = ({ packagePath, bundlePath }) => {
32
27
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-staged-host-'));
@@ -43,61 +38,36 @@ const preparePackage = ({ packagePath, bundlePath }) => {
43
38
  return { temp, packageRoot };
44
39
  };
45
40
 
46
- const fixturePath = (mode, temp) => {
47
- const bin = path.join(temp, `bin-${mode}`);
48
- fs.mkdirSync(bin);
49
- const hosts = mode === 'claude' ? ['claude'] : mode === 'codex' ? ['codex'] : ['claude', 'codex'];
50
- const desired = ['node', 'npm', ...hosts];
51
- for (const name of desired) {
52
- const target = locate(name);
53
- if (!target) throw new Error(`${name} CLI unavailable for ${mode} host fixture`);
54
- fs.symlinkSync(target, path.join(bin, name));
55
- }
56
- return `${bin}:/usr/bin:/bin`;
57
- };
58
-
59
41
  export function stagedHostVerifier({ assets, identity }) {
60
42
  return {
61
43
  async verify({ source, assets: observedAssets = assets }) {
62
44
  const prepared = preparePackage(observedAssets);
63
- const results = {};
64
45
  try {
65
- for (const mode of ['claude', 'codex', 'dual']) {
66
- const home = path.join(prepared.temp, `home-${mode}`);
67
- const brainHome = path.join(home, '.cache', 'ruvnet-brain');
68
- fs.mkdirSync(path.join(home, '.claude'), { recursive: true });
69
- if (mode !== 'claude') fs.mkdirSync(path.join(home, '.codex'), { recursive: true });
70
- const env = {
71
- ...process.env,
72
- HOME: home,
73
- CODEX_HOME: path.join(home, '.codex'),
74
- RUVNET_BRAIN_HOME: brainHome,
75
- RUVNET_BRAIN_KB: path.join(brainHome, 'kb'),
76
- RUVNET_CLAUDE_MARKETPLACE_SOURCE: prepared.packageRoot,
77
- CI: 'true',
78
- PATH: fixturePath(mode, prepared.temp),
79
- };
80
- const installer = path.join(prepared.packageRoot, 'bin', 'install.mjs');
81
- run(process.execPath, [installer, '--local', '--yes', '--force', '--no-nightly-prompt',
82
- '--no-telemetry', '--no-stack', '--no-enhance', '--no-statusline', '--no-selfcheck'], {
83
- cwd: prepared.packageRoot, env, timeout: 1_200_000,
84
- });
85
- const doctor = spawnSync(process.execPath, [installer, '--doctor'], {
86
- cwd: prepared.packageRoot, env, timeout: 180_000,
87
- });
88
- const classified = classifyDoctorResult(doctor);
89
- if (!classified.accepted) {
90
- throw new Error(`doctor failed for ${mode}: ${classified.output.slice(-5000) || doctor.error?.message}`);
91
- }
92
- results[mode] = {
93
- status: classified.status,
94
- doctorExit: doctor.status,
95
- version: identity.version,
96
- };
46
+ // The loop, the mode names, the env and the doctor verdict all live in
47
+ // scripts/host-install-matrix.mjs, shared with the published-side check in
48
+ // publication-receipt.mjs. This file used to carry its own copy, which had drifted to
49
+ // different mode names (claude/codex/dual vs claudeOnly/codexOnly/dual) and a different
50
+ // install shape than the one that runs after publication — so the two halves of a release
51
+ // were judging different things and could not be compared. Only the STAGED-vs-PUBLISHED
52
+ // difference is real, and it is now a named variant rather than a second implementation.
53
+ const matrix = runHostMatrix({
54
+ packageRoot: prepared.packageRoot,
55
+ version: identity.version,
56
+ variant: 'staged',
57
+ locate,
58
+ temp: prepared.temp,
59
+ });
60
+ if (matrix.verdict !== 'PASS') {
61
+ return { verdict: 'FAIL', source, error: matrix.error, fixtures: matrix.fixtures };
97
62
  }
98
- return { verdict: 'PASS', source, artifactSha256: sha256(observedAssets.packagePath), fixtures: results };
63
+ return {
64
+ verdict: 'PASS',
65
+ source,
66
+ artifactSha256: sha256(observedAssets.packagePath),
67
+ fixtures: matrix.fixtures,
68
+ };
99
69
  } catch (error) {
100
- return { verdict: 'FAIL', error: error.message, fixtures: results };
70
+ return { verdict: 'FAIL', source, error: error.message, fixtures: {} };
101
71
  } finally {
102
72
  fs.rmSync(prepared.temp, { recursive: true, force: true });
103
73
  }
@@ -113,16 +113,16 @@ for (const rel of ['kb/RVF-GENERATIONS.json']) {
113
113
 
114
114
  if (CHECK && fs.existsSync(path.join(ROOT, 'kb/RVF-GENERATIONS.json'))) {
115
115
  const kbDir = path.join(ROOT, 'kb');
116
- const requiredStores = fs.readdirSync(kbDir)
117
- .filter((file) => file.endsWith('.big.rvf'))
118
- .map((file) => file.slice(0, -'.big.rvf'.length));
119
- // CI/source-only clones intentionally omit the gitignored RVF binaries. In that shape, validate
120
- // the committed ledger's version fields but defer byte checks to bundle assembly, where the RVFs
121
- // are present. A checkout containing any canonical RVF remains fail-closed for every ledger row.
122
- const { failures } = verifyRvfGenerations(kbDir, {
123
- requiredStores,
124
- allowMissingFiles: requiredStores.length === 0,
125
- });
116
+ // Validate the committed ledger's version fields ONLY. The previous form scanned the working
117
+ // directory for `*.big.rvf` and byte-compared every ledger row against whatever this machine
118
+ // happened to have — but those binaries are gitignored, so the verdict described the machine, not
119
+ // the commit. It was unsatisfiable in practice (ledger: 72 stores; a real checkout: 71, because
120
+ // the nightly rebuilds RVFs and regenerates the ledger as one step) and it blocked every push,
121
+ // tags included, while pointing at a remedy that cannot clear it. Byte verification now happens
122
+ // where the bytes are genuinely present and genuinely shipped: scripts/build-bundle.mjs:204-214.
123
+ // This is the same rule release.mjs:100 already applies — a verdict is only about the committed
124
+ // candidate.
125
+ const { failures } = verifyRvfGenerations(kbDir, { verifyBytes: false });
126
126
  for (const failure of failures) {
127
127
  console.error(`[version] RVF GENERATION DRIFT: ${failure}`);
128
128
  drift++;