ruvnet-brain 4.0.1 → 4.0.2
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/.claude-plugin/marketplace.json +1 -0
- package/README.md +4 -4
- package/bin/install.mjs +100 -5
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +8 -22
- package/plugin/.claude-plugin/marketplace.json +1 -0
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +2 -2
- package/plugin/commands/configure.md +3 -2
- package/plugin/commands/rvbc.md +4 -3
- package/plugin/commands/rvcb.md +2 -2
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +21 -0
- package/plugin/scripts/detach.mjs +14 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -6
- package/plugin/scripts/hook-shim.mjs +7 -7
- package/plugin/scripts/learn-capture.sh +22 -3
- package/plugin/scripts/learn-flush.mjs +21 -4
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +477 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +81 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +522 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +250 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +639 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +271 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +180 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2749 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +395 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +508 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +864 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
package/console/tips.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/* RuvNet Brain — How to use it (page two).
|
|
2
|
+
Two behaviors, both progressive enhancement — with JS off the page reads exactly as before:
|
|
3
|
+
1. The same theme toggle as the console (same 'rbc-theme' key, same data-theme mechanism).
|
|
4
|
+
2. Collapsible sections (owner, 2026-07-24: the page "gets too overwhelming as a wall of
|
|
5
|
+
text"). Every .depth.card collapses behind its own header; only the inventory — the
|
|
6
|
+
section that IS the page — starts open. Open state persists per browser; a #hash
|
|
7
|
+
deep-link always opens its target. */
|
|
8
|
+
(function () {
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
/* ── theme toggle (unchanged) ─────────────────────────────────────────────── */
|
|
12
|
+
var KEY = 'rbc-theme';
|
|
13
|
+
var btn = document.getElementById('theme-toggle');
|
|
14
|
+
function applyTheme(t) {
|
|
15
|
+
document.documentElement.dataset.theme = t;
|
|
16
|
+
try { localStorage.setItem(KEY, t); } catch (e) { /* private mode is fine */ }
|
|
17
|
+
}
|
|
18
|
+
if (btn) {
|
|
19
|
+
btn.addEventListener('click', function () {
|
|
20
|
+
applyTheme(document.documentElement.dataset.theme === 'light' ? 'dark' : 'light');
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* ── collapsible sections ─────────────────────────────────────────────────── */
|
|
25
|
+
var OPEN_KEY = 'rbc-tips-open';
|
|
26
|
+
var DEFAULT_OPEN = ['inventory']; // the lead section IS the page; everything else waits
|
|
27
|
+
var sections = Array.prototype.slice.call(document.querySelectorAll('section.depth.card'));
|
|
28
|
+
if (!sections.length) return;
|
|
29
|
+
|
|
30
|
+
function savedOpen() {
|
|
31
|
+
try {
|
|
32
|
+
var raw = localStorage.getItem(OPEN_KEY);
|
|
33
|
+
return raw ? JSON.parse(raw) : null;
|
|
34
|
+
} catch (e) { return null; }
|
|
35
|
+
}
|
|
36
|
+
function persist() {
|
|
37
|
+
try {
|
|
38
|
+
var open = sections.filter(function (s) { return !s.classList.contains('is-collapsed'); })
|
|
39
|
+
.map(function (s) { return s.id; });
|
|
40
|
+
localStorage.setItem(OPEN_KEY, JSON.stringify(open));
|
|
41
|
+
} catch (e) { /* private mode is fine */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setOpen(sec, open) {
|
|
45
|
+
sec.classList.toggle('is-collapsed', !open);
|
|
46
|
+
var head = sec.querySelector(':scope > .depth-head');
|
|
47
|
+
if (head) head.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var open = savedOpen() || DEFAULT_OPEN;
|
|
51
|
+
sections.forEach(function (sec) {
|
|
52
|
+
var head = sec.querySelector(':scope > .depth-head');
|
|
53
|
+
if (!head || !sec.id) return;
|
|
54
|
+
|
|
55
|
+
// Wrap EVERYTHING after the header — several sections carry an art <figure> as a sibling of
|
|
56
|
+
// .depth-body, and a collapsed section that still shows its full-bleed art has not collapsed.
|
|
57
|
+
// grid-template-rows animates the height without measuring it (no reflow jank).
|
|
58
|
+
var dc = document.createElement('div'); dc.className = 'dc';
|
|
59
|
+
var dci = document.createElement('div'); dci.className = 'dci';
|
|
60
|
+
var after = [];
|
|
61
|
+
var walk = head.nextElementSibling;
|
|
62
|
+
while (walk) { after.push(walk); walk = walk.nextElementSibling; }
|
|
63
|
+
if (!after.length) return;
|
|
64
|
+
sec.appendChild(dc); dc.appendChild(dci);
|
|
65
|
+
after.forEach(function (n) { dci.appendChild(n); });
|
|
66
|
+
|
|
67
|
+
// The header is the disclosure control: chevron, ARIA, keyboard.
|
|
68
|
+
var chev = document.createElement('span');
|
|
69
|
+
chev.className = 'dc-chev';
|
|
70
|
+
chev.setAttribute('aria-hidden', 'true');
|
|
71
|
+
var svgNS = 'http://www.w3.org/2000/svg';
|
|
72
|
+
var csvg = document.createElementNS(svgNS, 'svg'); csvg.setAttribute('viewBox', '0 0 24 24');
|
|
73
|
+
var cpath = document.createElementNS(svgNS, 'path'); cpath.setAttribute('d', 'M6 9l6 6 6-6');
|
|
74
|
+
csvg.appendChild(cpath); chev.appendChild(csvg);
|
|
75
|
+
head.appendChild(chev);
|
|
76
|
+
head.setAttribute('role', 'button');
|
|
77
|
+
head.setAttribute('tabindex', '0');
|
|
78
|
+
head.setAttribute('aria-controls', sec.id + '-body');
|
|
79
|
+
dc.id = sec.id + '-body';
|
|
80
|
+
sec.classList.add('dc-ready');
|
|
81
|
+
|
|
82
|
+
function toggle() { setOpen(sec, sec.classList.contains('is-collapsed')); persist(); }
|
|
83
|
+
head.addEventListener('click', function (e) {
|
|
84
|
+
if (e.target.closest && e.target.closest('a, button')) return; // links in headers stay links
|
|
85
|
+
toggle();
|
|
86
|
+
});
|
|
87
|
+
head.addEventListener('keydown', function (e) {
|
|
88
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); }
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
setOpen(sec, open.indexOf(sec.id) !== -1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// One small master switch, right-aligned above the first section.
|
|
95
|
+
var all = document.createElement('p');
|
|
96
|
+
all.className = 'dc-all';
|
|
97
|
+
var allBtn = document.createElement('button');
|
|
98
|
+
allBtn.type = 'button';
|
|
99
|
+
function anyCollapsed() {
|
|
100
|
+
return sections.some(function (s) { return s.classList.contains('is-collapsed'); });
|
|
101
|
+
}
|
|
102
|
+
function labelAll() { allBtn.textContent = anyCollapsed() ? 'expand all' : 'collapse all'; }
|
|
103
|
+
allBtn.addEventListener('click', function () {
|
|
104
|
+
var opening = anyCollapsed();
|
|
105
|
+
sections.forEach(function (s) { setOpen(s, opening); });
|
|
106
|
+
persist(); labelAll();
|
|
107
|
+
});
|
|
108
|
+
all.appendChild(allBtn);
|
|
109
|
+
sections[0].parentNode.insertBefore(all, sections[0]);
|
|
110
|
+
labelAll();
|
|
111
|
+
sections.forEach(function (s) {
|
|
112
|
+
var h = s.querySelector(':scope > .depth-head');
|
|
113
|
+
if (!h) return;
|
|
114
|
+
h.addEventListener('click', function () { setTimeout(labelAll, 0); });
|
|
115
|
+
h.addEventListener('keydown', function () { setTimeout(labelAll, 0); });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// A #hash deep-link must always land on an OPEN section — from the console's cards, the
|
|
119
|
+
// metaharness cross-reference, or a shared URL.
|
|
120
|
+
function openHashTarget() {
|
|
121
|
+
var id = (location.hash || '').replace(/^#/, '');
|
|
122
|
+
if (!id) return;
|
|
123
|
+
var sec = document.getElementById(id);
|
|
124
|
+
if (sec && sec.classList.contains('depth')) { setOpen(sec, true); persist(); labelAll(); }
|
|
125
|
+
}
|
|
126
|
+
window.addEventListener('hashchange', openHashTarget);
|
|
127
|
+
openHashTarget();
|
|
128
|
+
}());
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# RuvNet-Brain 4.0 line — what's new (the major-release highlights)
|
|
2
|
+
|
|
3
|
+
Updated: 2026-07-30
|
|
4
|
+
|
|
5
|
+
> **Source of truth** for the `/whats-new` command and the first-run upgrade message. Curated, honest,
|
|
6
|
+
> major-only — not the point-release churn. If a claim here isn't true of the shipping build, it does not
|
|
7
|
+
> belong here. The self-measurement claims are deliberately hedged: they are *new and filling*, not
|
|
8
|
+
> *proven*.
|
|
9
|
+
>
|
|
10
|
+
> **VERSION STATUS:** the public release line is now 4.x. Publication does not erase the remaining
|
|
11
|
+
> acceptance obligations: the exact public artifact must still pass the fail-closed release contract
|
|
12
|
+
> through fresh Claude Code and Codex hosts. Anything not proven through that boundary is listed as a
|
|
13
|
+
> limitation, never converted into a capability claim.
|
|
14
|
+
|
|
15
|
+
**One line:** the 4.0 line is where the brain got **honest, legible, fast, and self-measuring** — and
|
|
16
|
+
it's landing now.
|
|
17
|
+
|
|
18
|
+
## The big things
|
|
19
|
+
|
|
20
|
+
### Release proof is fail-closed
|
|
21
|
+
The 4.0 release path now separates a clean candidate seal from a post-publication seal. Dirty
|
|
22
|
+
lineage, zero/skipped/todo tests, open issues, red or pending exact-SHA workflows, a missing
|
|
23
|
+
`ruvnet-brain` self-RVF store, weak query-deadline margin, missing independent graders,
|
|
24
|
+
host/artifact mismatches, and public-byte drift are release failures rather than warnings.
|
|
25
|
+
`npm run release:proof -- --status --quick` shows the current live blockers.
|
|
26
|
+
|
|
27
|
+
### 1. The Console is the front door
|
|
28
|
+
Type `/rvbc` and your whole RuvNet stack is on one live local page: what's installed, what the AI has
|
|
29
|
+
actually learned from *your* projects (real memories + distilled lessons, drill-down to the verbatim
|
|
30
|
+
cards), which subscription pays for what, and one-click **reversible** fixes for anything stale. New in
|
|
31
|
+
4.0:
|
|
32
|
+
- a plain-English **explainer on every card** (no more guessing what "trust & provenance" means),
|
|
33
|
+
- every suggestion carries its **blast radius** — *just this project* vs *every project · this machine*,
|
|
34
|
+
- **safe on/off checkboxes** that appear *only* where the undo is proven,
|
|
35
|
+
- a **terminal-first install** — the granular "here's exactly what I'll do, uncheck any of it" flow runs
|
|
36
|
+
in your terminal, where an `npx` user expects it.
|
|
37
|
+
|
|
38
|
+
### 2. It will not lie about your machine
|
|
39
|
+
Every number is measured live from your setup. **"We couldn't check" never renders as "off."** One
|
|
40
|
+
project's state can never leak into another project's view (a real bug 4.0 fixed in the console itself).
|
|
41
|
+
Empty-first, honest-always.
|
|
42
|
+
|
|
43
|
+
### 3. Fast — and it tells you when it's ready
|
|
44
|
+
The console and tips page paint in **well under a second** (measured, with a QE suite that runs every
|
|
45
|
+
time). On a first scan it shows a **countdown** and then says *"it's live — take a look at your page,"*
|
|
46
|
+
so you're never staring at a blank screen wondering if it hung.
|
|
47
|
+
|
|
48
|
+
### 4. It measures itself now
|
|
49
|
+
The brain records when it offered help and whether you acted on it — so over time it can **prove** it's
|
|
50
|
+
improving instead of asserting it. **Honest caveat:** this instrumentation is *new* and has only just
|
|
51
|
+
started collecting. 4.0 is not a claim of "proven better in the field" — it is the release that makes
|
|
52
|
+
that proof *possible*, and the evidence accrues as you use it.
|
|
53
|
+
|
|
54
|
+
### 5. It learns across your projects
|
|
55
|
+
A lesson proven in one project can be **promoted to your global brain** and applied everywhere — and it
|
|
56
|
+
now **survives an update** (tested against the real updater, not argued).
|
|
57
|
+
|
|
58
|
+
### 6. Runs on your account, cheapest capable model
|
|
59
|
+
The QE suite and model routing use **your Claude account, not an API key**, at the least-powerful model
|
|
60
|
+
that does the job. Nothing bills silently.
|
|
61
|
+
|
|
62
|
+
### 7. Claude Code and Codex share one active runtime
|
|
63
|
+
Both hosts are wired to the same Stable Spine generation: the MCP search shell, lifecycle hooks,
|
|
64
|
+
skills and update behavior advance as one versioned runtime instead of being installed as unrelated
|
|
65
|
+
copies.
|
|
66
|
+
|
|
67
|
+
### 8. Source grounding is an RVF-native product surface
|
|
68
|
+
The Brain searches per-repository RuVector RVF stores, joins hits to their source passages, and
|
|
69
|
+
returns cited repository paths. Model memory is not accepted as evidence for rUv-stack claims.
|
|
70
|
+
|
|
71
|
+
### 9. Quality and harness workflows are explicit
|
|
72
|
+
Agentic-QE is the testing fleet. Harness scoring evaluates the orchestration layer, and cost-aware
|
|
73
|
+
routing can select cheaper capable models when the required provider access is configured. These are
|
|
74
|
+
named workflows a user can request, not silent substitutes.
|
|
75
|
+
|
|
76
|
+
## What 4.0 deliberately does NOT claim
|
|
77
|
+
Stated up front because overclaiming is the one thing this product cannot do:
|
|
78
|
+
- **Not** "proven X% better" — the outcome ledger is still filling (see #4).
|
|
79
|
+
- **Not** "fully proactive / anticipatory" — the brain still mostly speaks when you open the console or
|
|
80
|
+
ask; the in-session, unprompted surface is the next frontier, not a shipped 4.0 guarantee.
|
|
81
|
+
- **Not** independently graded ≥95 on the exact public artifact. A score is not a release substitute.
|
|
82
|
+
- **Not** fully accepted while any critical exact-artifact, host, retrieval, version-convergence or
|
|
83
|
+
recovery invariant is FAIL, UNKNOWN, skipped or mocked-only.
|
|
84
|
+
|
|
85
|
+
## For upgraders
|
|
86
|
+
On the first real major-version transition, the installer/session experience presents the concise
|
|
87
|
+
highlights once. Run `npx ruvnet-brain --whats-new` at any time to read this full list again, or
|
|
88
|
+
`npx ruvnet-brain --what-changed` to inspect the exact machine footprint and undo path for each piece.
|
|
@@ -19,19 +19,50 @@ export function modelCacheReady(modelCache, model, revision = null) {
|
|
|
19
19
|
].every((file) => fs.existsSync(file));
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function acquireMaterializeLock(destination, timeoutMs = 30_000) {
|
|
23
|
+
const lock = `${destination}.materialize-lock`;
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
26
|
+
while (true) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(lock);
|
|
29
|
+
return () => fs.rmSync(lock, { recursive: true, force: true });
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
32
|
+
try {
|
|
33
|
+
const ageMs = Date.now() - fs.statSync(lock).mtimeMs;
|
|
34
|
+
if (ageMs > timeoutMs) {
|
|
35
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
} catch { /* another process released it between checks */ }
|
|
39
|
+
if (Date.now() - started >= timeoutMs) {
|
|
40
|
+
throw new Error(`timed out waiting to materialize model cache: ${destination}`);
|
|
41
|
+
}
|
|
42
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
// Revision-pinned downloads live below <model>/<revision>/, while strict offline reads resolve
|
|
23
48
|
// <model>/ directly. Promote only the exact pinned files into that canonical offline location.
|
|
24
49
|
export function materializeModelRevision(modelCache, model, revision) {
|
|
25
50
|
if (!revision || !modelCacheReady(modelCache, model, revision)) return false;
|
|
26
51
|
const source = path.join(modelPath(modelCache, model), revision);
|
|
27
52
|
const destination = modelPath(modelCache, model);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
53
|
+
const release = acquireMaterializeLock(destination);
|
|
54
|
+
try {
|
|
55
|
+
if (modelCacheReady(modelCache, model)) return true;
|
|
56
|
+
for (const entry of fs.readdirSync(source)) {
|
|
57
|
+
fs.cpSync(path.join(source, entry), path.join(destination, entry), {
|
|
58
|
+
recursive: true,
|
|
59
|
+
force: true,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return modelCacheReady(modelCache, model);
|
|
63
|
+
} finally {
|
|
64
|
+
release();
|
|
33
65
|
}
|
|
34
|
-
return modelCacheReady(modelCache, model);
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
// The RVF sidecar is the source of truth for the query embedder. Only sidecars with a matching
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
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": {
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node plugin/test/run-tests.mjs",
|
|
11
|
+
"release:proof": "node scripts/release-proof.mjs",
|
|
12
|
+
"benchmark:brain50": "node scripts/brain-latency-50.mjs",
|
|
13
|
+
"benchmark:novice50": "node scripts/brain-novice-50.mjs",
|
|
11
14
|
"version:check": "node scripts/sync-version.mjs --check",
|
|
12
15
|
"claims:verify": "node scripts/claims-verify.mjs",
|
|
13
16
|
"claims:fix": "node scripts/claims-verify.mjs --fix",
|
|
@@ -61,33 +64,16 @@
|
|
|
61
64
|
"!plugin/scripts/.ruvnet-brain/",
|
|
62
65
|
"!plugin/test/",
|
|
63
66
|
"README.md",
|
|
67
|
+
"docs/RELEASE-NOTES-4.0.md",
|
|
64
68
|
"LICENSE",
|
|
69
|
+
"console/",
|
|
65
70
|
"config/model-router/",
|
|
71
|
+
"scripts/",
|
|
66
72
|
"kb/verify-citation.mjs",
|
|
67
73
|
"kb/zip-extract.mjs",
|
|
68
74
|
"kb/brain-profile.mjs",
|
|
69
75
|
"kb/model-requirements.mjs",
|
|
70
|
-
"
|
|
71
|
-
"scripts/model-router-setup.mjs",
|
|
72
|
-
"scripts/model-router-status.mjs",
|
|
73
|
-
"scripts/model-router-outcome.mjs",
|
|
74
|
-
"scripts/subscription-hosts.mjs",
|
|
75
|
-
"scripts/dual-host-deliberation.mjs",
|
|
76
|
-
"scripts/dual-host-suggest.mjs",
|
|
77
|
-
"scripts/route-cheap.mjs",
|
|
78
|
-
"scripts/dispatch-receipt.mjs",
|
|
79
|
-
"scripts/metaharness-receipts.mjs",
|
|
80
|
-
"scripts/codex-routed.sh",
|
|
81
|
-
"scripts/metaharness-router.mjs",
|
|
82
|
-
"scripts/no-silent-substitution.mjs",
|
|
83
|
-
"scripts/user-settings.mjs",
|
|
84
|
-
"scripts/selfcheck.mjs",
|
|
85
|
-
"scripts/hook-registry.mjs",
|
|
86
|
-
"scripts/install-scope.mjs",
|
|
87
|
-
"scripts/upgrade-notice.mjs",
|
|
88
|
-
"scripts/falsify.mjs",
|
|
89
|
-
"scripts/nightly-watchdog.mjs",
|
|
90
|
-
"scripts/job-heartbeat.sh"
|
|
76
|
+
"keys/ruvnet-brain-signing.pub.pem"
|
|
91
77
|
],
|
|
92
78
|
"engines": {
|
|
93
79
|
"node": ">=18"
|
|
@@ -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 69 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.0.
|
|
4
|
+
"version": "4.0.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -18,6 +18,5 @@
|
|
|
18
18
|
"brain",
|
|
19
19
|
"mcp",
|
|
20
20
|
"claude-code"
|
|
21
|
-
]
|
|
22
|
-
"updated": "2026-07-30"
|
|
21
|
+
]
|
|
23
22
|
}
|
|
@@ -7,8 +7,8 @@ updated: 2026-07-20
|
|
|
7
7
|
correct the user's spelling.
|
|
8
8
|
|
|
9
9
|
1. Say: "Opening it now; it scans live while you watch."
|
|
10
|
-
2.
|
|
11
|
-
|
|
10
|
+
2. Use `${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
11
|
+
A current-repository copy is allowed only for an explicit developer checkout.
|
|
12
12
|
3. Run `node <resolved-script> --serve --open` in the background.
|
|
13
13
|
4. Give the URL immediately. Never promise a duration; the page reports its own progress.
|
|
14
14
|
|
|
@@ -8,8 +8,9 @@ their actual machine and lets them configure and (with consent) fix it.
|
|
|
8
8
|
|
|
9
9
|
Do this:
|
|
10
10
|
|
|
11
|
-
1. **Find the
|
|
12
|
-
|
|
11
|
+
1. **Find the installed runtime.** Use
|
|
12
|
+
`${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
13
|
+
A current-repository copy is allowed only for an explicit developer checkout.
|
|
13
14
|
|
|
14
15
|
2. **Start the server in the BACKGROUND** (it is a long-running process) so this turn keeps going, and
|
|
15
16
|
open the browser:
|
package/plugin/commands/rvbc.md
CHANGED
|
@@ -26,10 +26,11 @@ opening it now and it will scan itself live in their browser. Something in the s
|
|
|
26
26
|
Say it like a person, not a status bar. **Do not** promise 20 seconds, a minute, or any duration: the
|
|
27
27
|
page carries its own timing now, and a number invented here is a number the page will contradict.
|
|
28
28
|
|
|
29
|
-
## 2. Find the
|
|
29
|
+
## 2. Find the installed runtime
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
Use `${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
32
|
+
A current-repository copy is allowed only in an explicit developer checkout. Never guess a
|
|
33
|
+
`~/Code/ruvnet-brain` path: the installer persists this runtime for clean users.
|
|
33
34
|
|
|
34
35
|
## 3. Start it in the BACKGROUND and open the browser
|
|
35
36
|
|
package/plugin/commands/rvcb.md
CHANGED
|
@@ -7,8 +7,8 @@ updated: 2026-07-20
|
|
|
7
7
|
correct the user's spelling.
|
|
8
8
|
|
|
9
9
|
1. Say: "Opening it now; it scans live while you watch."
|
|
10
|
-
2.
|
|
11
|
-
|
|
10
|
+
2. Use `${RUVNET_BRAIN_KB:-$HOME/.cache/ruvnet-brain/kb}/.console-runtime/scripts/onboarding-console.mjs`.
|
|
11
|
+
A current-repository copy is allowed only for an explicit developer checkout.
|
|
12
12
|
3. Run `node <resolved-script> --serve --open` in the background.
|
|
13
13
|
4. Give the URL immediately. Never promise a duration; the page reports its own progress.
|
|
14
14
|
|
package/plugin/hooks/hooks.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"_platform": "posix",
|
|
2
|
+
"description": "POSIX Stable Spine (ADR-023): every hook routes through hook-shim.mjs, which resolves the active generation per invocation. Advisory hooks fail open; blocking hooks preserve their exit-code contract.",
|
|
4
3
|
"hooks": {
|
|
5
4
|
"SessionStart": [
|
|
6
5
|
{
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { loadRuntimePreferences, runtimeChildEnv } from '../scripts/runtime-preferences.mjs';
|
|
5
6
|
|
|
6
7
|
export const MANAGED_EXECUTABLES = Object.freeze([
|
|
7
8
|
'ruflo',
|
|
@@ -142,9 +143,21 @@ function writeStamps(executable, argv, env) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
export function resolveManagedExecutable(executable, env = process.env) {
|
|
147
|
+
if (executable !== 'ruflo') return executable;
|
|
148
|
+
const home = env.HOME || os.homedir();
|
|
149
|
+
const canonical = path.join(home, '.npm-global', 'bin', 'ruflo');
|
|
150
|
+
try {
|
|
151
|
+
fs.accessSync(canonical, fs.constants.X_OK);
|
|
152
|
+
return canonical;
|
|
153
|
+
} catch {
|
|
154
|
+
return executable;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
function execute(executable, argv, env) {
|
|
146
159
|
return new Promise((resolve) => {
|
|
147
|
-
const child = spawn(executable, argv, {
|
|
160
|
+
const child = spawn(resolveManagedExecutable(executable, env), argv, {
|
|
148
161
|
env,
|
|
149
162
|
shell: false,
|
|
150
163
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -186,8 +199,9 @@ function execute(executable, argv, env) {
|
|
|
186
199
|
|
|
187
200
|
function resultOf(executable, argv, result) {
|
|
188
201
|
const output = [result.stdout, result.stderr].filter(Boolean).join(result.stdout && result.stderr ? '\n' : '');
|
|
189
|
-
|
|
190
|
-
|
|
202
|
+
const contradictoryFailure = /(?:^|\n)\s*(?:❌|\[ERROR\])|invalid pragma command|key not found/i.test(output);
|
|
203
|
+
if (result.error || result.code !== 0 || contradictoryFailure) {
|
|
204
|
+
const reason = result.error || (contradictoryFailure ? 'fatal output despite exit 0' : `exit ${result.code}`);
|
|
191
205
|
return {
|
|
192
206
|
content: [{ type: 'text', text: output || `${executable} ${argv.join(' ')} failed: ${reason}` }],
|
|
193
207
|
isError: true,
|
|
@@ -220,7 +234,36 @@ export async function callManagedCli(toolName, args, env = process.env) {
|
|
|
220
234
|
isError: true,
|
|
221
235
|
};
|
|
222
236
|
}
|
|
223
|
-
|
|
237
|
+
const policy = loadRuntimePreferences({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() });
|
|
238
|
+
if (executable === 'agentic-flow' && policy.values.routing !== 'auto') {
|
|
239
|
+
return {
|
|
240
|
+
content: [{
|
|
241
|
+
type: 'text',
|
|
242
|
+
text: policy.values.routing === 'off'
|
|
243
|
+
? 'Token-smart routing is off in RuvNet Brain Console; agentic-flow was not started.'
|
|
244
|
+
: 'Token-smart routing has not been enabled in RuvNet Brain Console; agentic-flow was not started.',
|
|
245
|
+
}],
|
|
246
|
+
isError: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const isFleetMutation = executable === 'agentic-qe'
|
|
250
|
+
&& argv[0] === 'fleet'
|
|
251
|
+
&& ['init', 'spawn', 'run'].includes(argv[1]);
|
|
252
|
+
if (isFleetMutation && policy.values.qeFleet !== true) {
|
|
253
|
+
return {
|
|
254
|
+
content: [{
|
|
255
|
+
type: 'text',
|
|
256
|
+
text: policy.values.qeFleet === false
|
|
257
|
+
? 'The Agentic-QE fleet is off in RuvNet Brain Console; no QE agents were started.'
|
|
258
|
+
: 'The Agentic-QE fleet has not been enabled in RuvNet Brain Console; no QE agents were started.',
|
|
259
|
+
}],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const childEnv = (executable === 'agentic-flow' || executable === 'agentic-qe')
|
|
264
|
+
? runtimeChildEnv({ env, cwd: env.RUVNET_BRAIN_PROJECT_DIR || process.cwd() })
|
|
265
|
+
: env;
|
|
266
|
+
return resultOf(executable, argv, await execute(executable, argv, childEnv));
|
|
224
267
|
}
|
|
225
268
|
|
|
226
269
|
return {
|
package/plugin/mcp/server.mjs
CHANGED
|
@@ -147,6 +147,10 @@ function killChild(reason) {
|
|
|
147
147
|
async function ensureChild() {
|
|
148
148
|
const gen = currentGeneration();
|
|
149
149
|
if (child && child.generation !== gen && pendingCount === 0) await killChild('superseded by a newer generation');
|
|
150
|
+
// A concurrent initialize/tools-list/tools-call must join the same readiness promise. Returning
|
|
151
|
+
// `child` while its private initialize or model warmup is still running advertises a half-ready
|
|
152
|
+
// worker and charges cold-start to the first user call.
|
|
153
|
+
if (childStartup) return childStartup;
|
|
150
154
|
if (child) return child;
|
|
151
155
|
await childRetirement;
|
|
152
156
|
if (!fs.existsSync(CHILD_MCP)) return null;
|
|
@@ -177,6 +181,17 @@ async function ensureChild() {
|
|
|
177
181
|
capabilities: {},
|
|
178
182
|
clientInfo: { name: 'ruvnet-brain-shell', version: SERVER_INFO.version },
|
|
179
183
|
}, CHILD_INIT_TIMEOUT_MS);
|
|
184
|
+
const warmed = await childRequest(c, 'brain/warmup', {}, CHILD_INIT_TIMEOUT_MS);
|
|
185
|
+
// Rolling-upgrade compatibility: 4.0.1 workers predate the private warmup extension. The
|
|
186
|
+
// stable shell is installed before the 700MB KB swap completes, so rejecting -32601 here
|
|
187
|
+
// makes every query fail during the exact in-place upgrade window the shell exists to hide.
|
|
188
|
+
// An older initialized worker is usable (its first query may be cold); any other warmup
|
|
189
|
+
// failure still fails closed.
|
|
190
|
+
const warmupUnsupported = warmed.error?.code === -32601
|
|
191
|
+
|| /method not found/i.test(String(warmed.error?.message || ''));
|
|
192
|
+
if (!warmupUnsupported && (warmed.error || warmed.result?.ready !== true)) {
|
|
193
|
+
throw new Error(warmed.error?.message || 'brain worker did not confirm warm readiness');
|
|
194
|
+
}
|
|
180
195
|
} catch (e) {
|
|
181
196
|
await killChild('failed initialize');
|
|
182
197
|
throw new Error(`brain worker failed to initialize: ${e.message}`);
|
|
@@ -241,6 +256,12 @@ async function handleClient(msg) {
|
|
|
241
256
|
if (id === undefined || id === null) return; // notifications need no answer
|
|
242
257
|
switch (method) {
|
|
243
258
|
case 'initialize':
|
|
259
|
+
// MCP initialize itself stays protocol-fast; normal clients request tools/list immediately
|
|
260
|
+
// afterwards, which joins this readiness promise and does not receive the tool declaration
|
|
261
|
+
// until the worker's query embedder and cross-encoder are warm.
|
|
262
|
+
void ensureChild().catch((e) => {
|
|
263
|
+
console.error(`[ruvnet-brain] worker warmup failed during initialize: ${e.message}`);
|
|
264
|
+
});
|
|
244
265
|
return clientOk(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
|
|
245
266
|
case 'ping':
|
|
246
267
|
return clientOk(id, {});
|
|
@@ -107,11 +107,25 @@ if (!SUPERVISOR) {
|
|
|
107
107
|
? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
108
108
|
: 'powershell.exe';
|
|
109
109
|
const quotePs = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
110
|
+
// Start-Process has its own standard-stream boundary. On GitHub's packed PowerShell install
|
|
111
|
+
// path the SessionStart body finished but the external checker never received `close` before
|
|
112
|
+
// its 5s watchdog — consistent with a descendant retaining a capture handle, though that job
|
|
113
|
+
// did not instrument exact handle ownership. Redirect both streams to distinct files at the
|
|
114
|
+
// native boundary so the supervisor cannot inherit the hook's capture streams; redirecting
|
|
115
|
+
// only this short Node wrapper's stdio does not establish that contract for its grandchild.
|
|
116
|
+
const supervisorOut = logPath && logPath !== '-'
|
|
117
|
+
? `${logPath}.supervisor.stdout`
|
|
118
|
+
: path.join(os.tmpdir(), `ruvnet-brain-detach-supervisor-${process.pid}.stdout`);
|
|
119
|
+
const supervisorErr = logPath && logPath !== '-'
|
|
120
|
+
? `${logPath}.supervisor.stderr`
|
|
121
|
+
: path.join(os.tmpdir(), `ruvnet-brain-detach-supervisor-${process.pid}.stderr`);
|
|
110
122
|
const launch = [
|
|
111
123
|
'Start-Process',
|
|
112
124
|
'-FilePath', quotePs(process.execPath),
|
|
113
125
|
'-ArgumentList', `@(${quotePs(SELF)},${quotePs('--payload-env')})`,
|
|
114
126
|
'-WindowStyle', 'Hidden',
|
|
127
|
+
'-RedirectStandardOutput', quotePs(supervisorOut),
|
|
128
|
+
'-RedirectStandardError', quotePs(supervisorErr),
|
|
115
129
|
].join(' ');
|
|
116
130
|
child = spawn(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', launch], {
|
|
117
131
|
detached: true,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One detached first-session lifecycle worker: seed the Stable Spine, then perform the update
|
|
3
|
+
// heartbeat the seed used to race. Keeping both operations in one worker preserves ADR-054's
|
|
4
|
+
// "Brain OFF still receives fixes" contract without making SessionStart launch two detachers.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const [updateApply, hostUpdate, versionLog] = process.argv.slice(2);
|
|
10
|
+
if (![updateApply, hostUpdate, versionLog].every((value) => typeof value === 'string' && value)) {
|
|
11
|
+
process.stderr.write('usage: first-session-worker.mjs <update-apply.mjs> <host-update.mjs> <version-log>\n');
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const seed = spawnSync(process.execPath, [updateApply, '--seed'], {
|
|
16
|
+
env: process.env,
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
});
|
|
19
|
+
if (seed.error || seed.status !== 0) {
|
|
20
|
+
process.stderr.write(`[ruvnet-brain] first-session seed failed: ${seed.error?.message || `exit ${seed.status}`}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const check = spawnSync(process.execPath, [hostUpdate, '--check'], {
|
|
25
|
+
env: process.env,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
});
|
|
28
|
+
const version = String(check.stdout || '').trim();
|
|
29
|
+
if (!check.error && check.status === 0 && /^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version)) {
|
|
30
|
+
fs.mkdirSync(path.dirname(versionLog), { recursive: true });
|
|
31
|
+
const temporary = `${versionLog}.tmp-${process.pid}`;
|
|
32
|
+
fs.writeFileSync(temporary, `${version}\n`);
|
|
33
|
+
fs.renameSync(temporary, versionLog);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Network failure must not undo a successful seed. The 15-minute heartbeat stamp lets the normal
|
|
37
|
+
// SessionStart path retry later, while the current runtime remains valid and usable.
|
|
38
|
+
process.exit(0);
|