ruvnet-brain 4.3.20 → 4.3.25
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 +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
package/console/scope.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/* scope.js — "what's in the brain". Reads /api/scope once, renders four verdict buckets and two
|
|
2
|
+
sortable, searchable tables. Every number on this page comes from that payload; nothing is typed
|
|
3
|
+
here. With JS off the page shows the as-of rule and an honest "needs the console server" line. */
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
/* ── theme toggle — same key and mechanism as the console ───────────────── */
|
|
8
|
+
var KEY = 'rbc-theme';
|
|
9
|
+
var btn = document.getElementById('theme-toggle');
|
|
10
|
+
if (btn) {
|
|
11
|
+
btn.addEventListener('click', function () {
|
|
12
|
+
var t = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
|
|
13
|
+
document.documentElement.dataset.theme = t;
|
|
14
|
+
try { localStorage.setItem(KEY, t); } catch (e) { /* private mode is fine */ }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var $ = function (id) { return document.getElementById(id); };
|
|
19
|
+
var esc = function (s) {
|
|
20
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
21
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
var day = function (iso) { return iso ? String(iso).slice(0, 10) : null; };
|
|
25
|
+
var BUCKET = {
|
|
26
|
+
current: { cls: 'b-current', label: 'in the brain · current' },
|
|
27
|
+
behind: { cls: 'b-behind', label: 'in the brain · behind' },
|
|
28
|
+
unverified: { cls: 'b-unverified', label: 'in the brain · currency unverified' },
|
|
29
|
+
'not-in-brain': { cls: 'b-notin', label: 'not in the brain' }
|
|
30
|
+
};
|
|
31
|
+
var RANK = { current: 0, behind: 1, unverified: 2, 'not-in-brain': 3 }; // column sort on "verdict"
|
|
32
|
+
var RANK_BEHIND = { behind: 0, unverified: 1, 'not-in-brain': 2, current: 3 }; // the "Behind first" view
|
|
33
|
+
|
|
34
|
+
/* ── three views + one search, as pure functions (owner, 2026-09-12: "alphabetically, or
|
|
35
|
+
thematically, or by date"). Published on window.RBScope so the page and its test share them. ── */
|
|
36
|
+
function byName(a, b) {
|
|
37
|
+
return String(a.name || '').localeCompare(String(b.name || ''), undefined, { sensitivity: 'base' });
|
|
38
|
+
}
|
|
39
|
+
function newestFirst(a, b) {
|
|
40
|
+
var x = a.ruvChangedAt, y = b.ruvChangedAt;
|
|
41
|
+
if (x == null && y == null) return byName(a, b);
|
|
42
|
+
if (x == null) return 1; // unknown dates sink to the bottom
|
|
43
|
+
if (y == null) return -1;
|
|
44
|
+
return x < y ? 1 : x > y ? -1 : byName(a, b);
|
|
45
|
+
}
|
|
46
|
+
/** Which comparator a named view uses. An unknown view reads as "newest", never throws. */
|
|
47
|
+
function sortFor(view) {
|
|
48
|
+
if (view === 'az') return byName;
|
|
49
|
+
if (view === 'behind') {
|
|
50
|
+
return function (a, b) {
|
|
51
|
+
var ra = RANK_BEHIND[a.bucket], rb = RANK_BEHIND[b.bucket];
|
|
52
|
+
var d = (ra == null ? 9 : ra) - (rb == null ? 9 : rb);
|
|
53
|
+
return d !== 0 ? d : newestFirst(a, b);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return newestFirst;
|
|
57
|
+
}
|
|
58
|
+
/** Does a row answer the query — by name, or by what it does (desc)? An empty query matches everything. */
|
|
59
|
+
function match(row, q) {
|
|
60
|
+
var needle = String(q == null ? '' : q).trim().toLowerCase();
|
|
61
|
+
if (!needle) return true;
|
|
62
|
+
return String(row.name || '').toLowerCase().indexOf(needle) !== -1
|
|
63
|
+
|| String(row.desc || '').toLowerCase().indexOf(needle) !== -1;
|
|
64
|
+
}
|
|
65
|
+
window.RBScope = { match: match, sortFor: sortFor };
|
|
66
|
+
|
|
67
|
+
var VIEW_SORT = { newest: ['ruvChangedAt', 'desc'], az: ['name', 'asc'], behind: ['bucket', 'asc'] };
|
|
68
|
+
var state = { data: null, q: '', view: 'newest', sort: { repos: ['ruvChangedAt', 'desc'], gists: ['ruvChangedAt', 'desc'] } };
|
|
69
|
+
|
|
70
|
+
function cmp(key, dir) {
|
|
71
|
+
var m = dir === 'asc' ? 1 : -1;
|
|
72
|
+
return function (a, b) {
|
|
73
|
+
var x = a[key], y = b[key];
|
|
74
|
+
if (key === 'bucket') { x = RANK[x]; y = RANK[y]; }
|
|
75
|
+
// unknown dates always sink to the bottom, whichever direction is chosen
|
|
76
|
+
if (x == null && y == null) return a.name.localeCompare(b.name);
|
|
77
|
+
if (x == null) return 1;
|
|
78
|
+
if (y == null) return -1;
|
|
79
|
+
if (x < y) return -1 * m;
|
|
80
|
+
if (x > y) return 1 * m;
|
|
81
|
+
return a.name.localeCompare(b.name);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rowHtml(r) {
|
|
86
|
+
var b = BUCKET[r.bucket] || BUCKET.unverified;
|
|
87
|
+
var gap = r.bucket === 'behind' && r.behindDays != null ? '<span class="gap">behind ' + r.behindDays + ' d</span>' : '';
|
|
88
|
+
var d1 = day(r.ruvChangedAt), d2 = day(r.brainReadAt);
|
|
89
|
+
return '<tr>' +
|
|
90
|
+
'<td class="name"><a href="' + esc(r.url) + '" target="_blank" rel="noopener">' + esc(r.name) + '</a>' +
|
|
91
|
+
(r.desc ? '<small class="desc" title="' + esc(r.desc) + '">' + esc(r.desc) + '</small>' : '') + '</td>' +
|
|
92
|
+
'<td class="date' + (d1 ? '' : ' unknown') + '">' + (d1 || 'unknown') + '</td>' +
|
|
93
|
+
'<td class="date' + (d2 ? '' : ' unknown') + '">' + (d2 || (r.bucket === 'not-in-brain' ? '—' : 'unknown')) + '</td>' +
|
|
94
|
+
'<td class="verdict"><span class="badge ' + b.cls + '">' + b.label + '</span>' + gap + '</td>' +
|
|
95
|
+
'<td class="sha" title="brain holds ' + esc(r.brainSha || '—') + ' · upstream ' + esc(r.upstreamSha || '—') + '">' +
|
|
96
|
+
esc((r.brainSha || '—').slice(0, 7)) + ' → ' + esc((r.upstreamSha || '—').slice(0, 7)) + '</td>' +
|
|
97
|
+
'</tr>';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function renderTable(kind) {
|
|
101
|
+
var rows = state.data[kind] || [];
|
|
102
|
+
var q = state.q.trim().toLowerCase();
|
|
103
|
+
if (q) rows = rows.filter(function (r) { return match(r, q); });
|
|
104
|
+
var s = state.sort[kind];
|
|
105
|
+
// a named view sorts both tables the same way; a column click switches to a per-table sort
|
|
106
|
+
rows = rows.slice().sort(state.view === 'custom' ? cmp(s[0], s[1]) : sortFor(state.view));
|
|
107
|
+
var body = $(kind + '-body');
|
|
108
|
+
var t0 = performance.now();
|
|
109
|
+
body.innerHTML = rows.length ? rows.map(rowHtml).join('') : '<tr><td colspan="5" class="empty">no ' + kind + ' match “' + esc(state.q) + '”</td></tr>';
|
|
110
|
+
$(kind + '-count').textContent = rows.length + (q ? ' of ' + (state.data[kind] || []).length : '');
|
|
111
|
+
var ths = $(kind + '-table').querySelectorAll('thead th[data-key]');
|
|
112
|
+
for (var i = 0; i < ths.length; i++) {
|
|
113
|
+
var k = ths[i].getAttribute('data-key');
|
|
114
|
+
ths[i].setAttribute('aria-sort', k === s[0] ? (s[1] === 'asc' ? 'ascending' : 'descending') : 'none');
|
|
115
|
+
}
|
|
116
|
+
if (window.console && rows.length > 200) console.debug('[scope] rendered', kind, rows.length, 'rows in', Math.round(performance.now() - t0), 'ms');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderBuckets(d) {
|
|
120
|
+
var c = d.counts || {};
|
|
121
|
+
var r = c.repos || {}, g = c.gists || {};
|
|
122
|
+
var tile = function (cls, key, label, desc) {
|
|
123
|
+
var n = (r[key] || 0) + (g[key] || 0);
|
|
124
|
+
return '<div class="bucket ' + cls + '"><div class="n">' + n + '<small>' + (r[key] || 0) + ' repos · ' + (g[key] || 0) + ' gists</small></div>' +
|
|
125
|
+
'<div class="l">' + label + '</div><div class="d">' + desc + '</div></div>';
|
|
126
|
+
};
|
|
127
|
+
$('buckets').innerHTML =
|
|
128
|
+
tile('b-current', 'current', 'in the brain · current', 'The brain holds exactly the commit (or gist revision) that is live upstream.') +
|
|
129
|
+
tile('b-behind', 'behind', 'in the brain · behind', 'rUv changed it after the brain last read it. The gap is shown per row.') +
|
|
130
|
+
tile('b-unverified', 'unverified', 'in the brain · unverified', 'Installed here, but no source commit was recorded, so currency cannot be proven either way.') +
|
|
131
|
+
tile('b-notin', 'notInBrain', 'not in the brain', 'Eligible upstream, no store on this machine.');
|
|
132
|
+
var inel = (r.ineligible || 0) + (g.ineligible || 0);
|
|
133
|
+
var foot = [];
|
|
134
|
+
if (inel) foot.push(inel + ' upstream item' + (inel === 1 ? '' : 's') + ' excluded by policy (forks, archived, empty) — not counted above');
|
|
135
|
+
if (d.installedOutsideCoverage && d.installedOutsideCoverage.length) {
|
|
136
|
+
foot.push(d.installedOutsideCoverage.length + ' store' + (d.installedOutsideCoverage.length === 1 ? '' : 's') + ' installed locally, outside release coverage: <code>' + d.installedOutsideCoverage.map(esc).join('</code>, <code>') + '</code>');
|
|
137
|
+
}
|
|
138
|
+
$('bucket-foot').innerHTML = foot.join(' · ');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderAsOf(d) {
|
|
142
|
+
var el = $('asof');
|
|
143
|
+
var age = d.ageDays == null ? null : Math.round(d.ageDays);
|
|
144
|
+
el.className = 'asof' + (d.stale ? ' is-old' : '');
|
|
145
|
+
el.innerHTML = '<span class="dot"></span>as of <strong>' + esc(day(d.observedAt) || 'unknown') + '</strong>' +
|
|
146
|
+
(age == null ? '' : ' · ' + age + ' day' + (age === 1 ? '' : 's') + ' ago') +
|
|
147
|
+
(d.releaseTag ? ' · brain ' + esc(d.releaseTag) : '') +
|
|
148
|
+
' · ' + (d.installedStoreCount != null ? d.installedStoreCount + ' stores installed' : '');
|
|
149
|
+
$('asof-note').textContent = d.stale
|
|
150
|
+
? 'This is the coverage record that shipped with the installed brain, not a live probe. Repos rUv changed since this date are not reflected until the brain updates.'
|
|
151
|
+
: 'This is the coverage record that shipped with the installed brain.';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function render() {
|
|
155
|
+
var d = state.data;
|
|
156
|
+
renderAsOf(d); renderBuckets(d); renderTable('repos'); renderTable('gists');
|
|
157
|
+
$('foot-root').textContent = d.root || '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function setView(view) {
|
|
161
|
+
state.view = view;
|
|
162
|
+
var buttons = document.querySelectorAll('.scope-views .view');
|
|
163
|
+
for (var i = 0; i < buttons.length; i++) {
|
|
164
|
+
buttons[i].setAttribute('aria-pressed', buttons[i].getAttribute('data-view') === view ? 'true' : 'false');
|
|
165
|
+
}
|
|
166
|
+
if (VIEW_SORT[view]) { state.sort.repos = VIEW_SORT[view].slice(); state.sort.gists = VIEW_SORT[view].slice(); }
|
|
167
|
+
renderTable('repos'); renderTable('gists');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function wire() {
|
|
171
|
+
$('search').addEventListener('input', function (e) { state.q = e.target.value; renderTable('repos'); renderTable('gists'); });
|
|
172
|
+
var buttons = document.querySelectorAll('.scope-views .view');
|
|
173
|
+
for (var b = 0; b < buttons.length; b++) {
|
|
174
|
+
buttons[b].addEventListener('click', function (ev) { setView(ev.currentTarget.getAttribute('data-view')); });
|
|
175
|
+
}
|
|
176
|
+
['repos', 'gists'].forEach(function (kind) {
|
|
177
|
+
var ths = $(kind + '-table').querySelectorAll('thead th[data-key]');
|
|
178
|
+
for (var i = 0; i < ths.length; i++) {
|
|
179
|
+
ths[i].addEventListener('click', function (ev) {
|
|
180
|
+
var k = ev.currentTarget.getAttribute('data-key');
|
|
181
|
+
var s = state.sort[kind];
|
|
182
|
+
state.sort[kind] = [k, s[0] === k && s[1] === 'desc' ? 'asc' : 'desc'];
|
|
183
|
+
state.view = 'custom'; // a column click is a per-table sort; no view button is pressed
|
|
184
|
+
var vb = document.querySelectorAll('.scope-views .view');
|
|
185
|
+
for (var j = 0; j < vb.length; j++) vb[j].setAttribute('aria-pressed', 'false');
|
|
186
|
+
renderTable(kind);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fetch('/api/scope', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (d) {
|
|
193
|
+
var st = $('state');
|
|
194
|
+
if (!d || d.available === false) {
|
|
195
|
+
st.className = 'state error';
|
|
196
|
+
st.textContent = 'The installed brain has no coverage record on this machine' + (d && d.reason ? ' — ' + d.reason : '') + '. Nothing to list; nothing is being guessed.';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
st.hidden = true;
|
|
200
|
+
$('content').hidden = false;
|
|
201
|
+
state.data = d;
|
|
202
|
+
wire();
|
|
203
|
+
render();
|
|
204
|
+
}).catch(function (e) {
|
|
205
|
+
var st = $('state');
|
|
206
|
+
st.className = 'state error';
|
|
207
|
+
st.textContent = 'Could not reach /api/scope on the console server: ' + (e && e.message ? e.message : e);
|
|
208
|
+
});
|
|
209
|
+
})();
|
package/console/tips.html
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
<span class="brand-text">RuvNet Brain <span class="brand-sub">the other 90%</span></span>
|
|
75
75
|
</div>
|
|
76
76
|
<div class="head-right">
|
|
77
|
+
<a class="back-chip mono" href="scope.html">what's in the brain ›</a>
|
|
77
78
|
<a class="back-chip mono" href="./">‹ console</a>
|
|
78
79
|
<button class="theme-toggle" id="theme-toggle" type="button" aria-label="Switch color theme">
|
|
79
80
|
<svg class="ic-sun" viewBox="0 0 24 24" aria-hidden="true">
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// kb/corpus-release-identity.mjs — the customer side of a corpus-only release (ADR-086 step 16).
|
|
2
|
+
//
|
|
3
|
+
// TWO IDENTITY DOMAINS, AND WHY CONFLATING THEM COST A NIGHTLY HALF-GIGABYTE.
|
|
4
|
+
//
|
|
5
|
+
// A customer bundle carries a RUNTIME identity: `brainVersion` / `releaseTag` in SOURCE.json,
|
|
6
|
+
// stamped by scripts/build-bundle.mjs:333-334 as the semver of the code release that assembled it.
|
|
7
|
+
// A CORPUS release carries a TRANSPORT identity: the GitHub tag `corpus-sha256-<64 hex>`, which is
|
|
8
|
+
// a content address of the corpus archive and is not a version of anything.
|
|
9
|
+
//
|
|
10
|
+
// kb/forge-update.mjs's isBehind() treated release-tag identity as authoritative with one string
|
|
11
|
+
// inequality. Measured on this fixture before this module existed (4 consecutive `--apply` runs
|
|
12
|
+
// against ONE unchanged corpus release):
|
|
13
|
+
//
|
|
14
|
+
// apply#1: exit=0 zipDownloads=1 behindReported=true
|
|
15
|
+
// apply#2: exit=0 zipDownloads=2 behindReported=true
|
|
16
|
+
// apply#3: exit=0 zipDownloads=3 behindReported=true
|
|
17
|
+
// apply#4: exit=0 zipDownloads=4 behindReported=true
|
|
18
|
+
// check: exit=10 zipDownloads=4 behindReported=true
|
|
19
|
+
//
|
|
20
|
+
// `'corpus-sha256-aaa…' !== 'v4.9.0'` is true, and it stays true after a perfectly successful
|
|
21
|
+
// install, because the landed bundle re-stamps `releaseTag: v4.9.0`. The comparison could never
|
|
22
|
+
// converge. So the transport identity is recorded in its OWN field, `corpusReleaseTag`, written
|
|
23
|
+
// atomically with the installation, and compared only against a corpus tag.
|
|
24
|
+
//
|
|
25
|
+
// THE COMPATIBILITY BOUNDARY (Dual, verbatim): "One releases/latest pointer cannot represent
|
|
26
|
+
// independent newest corpora for multiple incompatible runtimes. Either explicitly support the
|
|
27
|
+
// current approved runtime with safe rejection for older clients, or add version-aware discovery.
|
|
28
|
+
// Never silently install incompatible code."
|
|
29
|
+
//
|
|
30
|
+
// And on how the pin must hold (Dual, verbatim): "Pinning survives only through enforced equality
|
|
31
|
+
// to the approved shipped runtime and its executable hashes. Copying current-main package.json or
|
|
32
|
+
// preserving a version string alone is insufficient."
|
|
33
|
+
//
|
|
34
|
+
// Hence RUNTIME-IDENTITY.json. It is written by bin/install.mjs — the owner-gated code release —
|
|
35
|
+
// and names the sha256 of the executables that release placed into the KB tree. It is NEVER
|
|
36
|
+
// produced by a bundle: build-bundle.mjs cannot see coverage-integrity.mjs (it is behind the
|
|
37
|
+
// updater's dynamic load), which is precisely why that file is the right thing to pin to. The
|
|
38
|
+
// updater re-hashes those bytes at update time; a declared version alone proves nothing and is
|
|
39
|
+
// never sufficient on its own.
|
|
40
|
+
//
|
|
41
|
+
// FAIL-CLOSED, ALWAYS. Missing identity file, unreadable identity file, missing executable,
|
|
42
|
+
// changed executable, or a corpus release built by a different runtime → the corpus release is
|
|
43
|
+
// REFUSED. A client that cannot prove which approved runtime it is running does not get a corpus
|
|
44
|
+
// release; it keeps the bundle it has and is told to re-run the installer.
|
|
45
|
+
|
|
46
|
+
import fs from 'node:fs';
|
|
47
|
+
import path from 'node:path';
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
|
|
50
|
+
/** The corpus transport tag shape published by scripts/corpus-seed-publish.mjs's corpusSeedTag(). */
|
|
51
|
+
export const CORPUS_TAG_PATTERN = /^corpus-sha256-[0-9a-f]{64}$/;
|
|
52
|
+
|
|
53
|
+
/** The installer-written record of which approved runtime this KB belongs to. */
|
|
54
|
+
export const INSTALLED_RUNTIME_FILE = 'RUNTIME-IDENTITY.json';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Executables the runtime pin is bound to, relative to the KB root.
|
|
58
|
+
*
|
|
59
|
+
* `coverage-integrity.mjs` is the trusted coverage validator. It is placed by the installer
|
|
60
|
+
* (bin/install.mjs placeTrustedCoverageValidator) and CANNOT be supplied by a bundle, so its bytes
|
|
61
|
+
* are a real, non-spoofable fingerprint of the approved shipped runtime on this machine. Adding
|
|
62
|
+
* more installer-placed executables here widens the pin; nothing else needs to change.
|
|
63
|
+
*/
|
|
64
|
+
export const RUNTIME_PINNED_EXECUTABLES = Object.freeze(['coverage-integrity.mjs']);
|
|
65
|
+
|
|
66
|
+
/** Where a refused release is remembered — OUTSIDE the governed tree, so an update cannot erase it. */
|
|
67
|
+
export const REJECTED_RELEASE_FILE = '.ruvnet-brain-rejected-release.json';
|
|
68
|
+
|
|
69
|
+
export function isCorpusReleaseTag(tag) {
|
|
70
|
+
return typeof tag === 'string' && CORPUS_TAG_PATTERN.test(tag);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function fileIdentity(file) {
|
|
74
|
+
try {
|
|
75
|
+
const bytes = fs.readFileSync(file);
|
|
76
|
+
return { sha256: createHash('sha256').update(bytes).digest('hex'), bytes: bytes.length };
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function atomicJson(file, value) {
|
|
83
|
+
const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
|
84
|
+
fs.writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`);
|
|
85
|
+
fs.renameSync(temp, file);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Stamp "this tree belongs to approved runtime X, and here are X's executable hashes" into `kbDir`.
|
|
90
|
+
*
|
|
91
|
+
* Called by the installer immediately after it places those executables, so the declared version and
|
|
92
|
+
* the measured bytes are written from the same moment and cannot disagree.
|
|
93
|
+
*
|
|
94
|
+
* @returns {{written: boolean, path: string, missing: string[]}}
|
|
95
|
+
*/
|
|
96
|
+
export function writeInstalledRuntimeIdentity(kbDir, { brainVersion }) {
|
|
97
|
+
if (!brainVersion || typeof brainVersion !== 'string') {
|
|
98
|
+
throw new Error('writeInstalledRuntimeIdentity requires the approved runtime brainVersion');
|
|
99
|
+
}
|
|
100
|
+
const executables = {};
|
|
101
|
+
const missing = [];
|
|
102
|
+
for (const relative of RUNTIME_PINNED_EXECUTABLES) {
|
|
103
|
+
const identity = fileIdentity(path.join(kbDir, relative));
|
|
104
|
+
if (!identity) { missing.push(relative); continue; }
|
|
105
|
+
executables[relative] = identity;
|
|
106
|
+
}
|
|
107
|
+
const target = path.join(kbDir, INSTALLED_RUNTIME_FILE);
|
|
108
|
+
// A pin naming an executable that is not there would be a pin to nothing. Refuse to write one.
|
|
109
|
+
if (missing.length) return { written: false, path: target, missing };
|
|
110
|
+
atomicJson(target, {
|
|
111
|
+
schemaVersion: 1,
|
|
112
|
+
kind: 'ruvnet-brain-installed-runtime',
|
|
113
|
+
brainVersion,
|
|
114
|
+
stampedUtc: new Date().toISOString(),
|
|
115
|
+
executables,
|
|
116
|
+
});
|
|
117
|
+
return { written: true, path: target, missing };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read the installed approved runtime and RE-HASH every executable it pins.
|
|
122
|
+
*
|
|
123
|
+
* The declared `brainVersion` is only ever returned alongside proof that the bytes it was written
|
|
124
|
+
* against are still the bytes on disk — that is the whole difference between an enforced pin and a
|
|
125
|
+
* copied version string.
|
|
126
|
+
*
|
|
127
|
+
* @returns {{ok: boolean, brainVersion: string|null, reason: string|null}}
|
|
128
|
+
*/
|
|
129
|
+
export function readInstalledRuntime(kbDir) {
|
|
130
|
+
const file = path.join(kbDir, INSTALLED_RUNTIME_FILE);
|
|
131
|
+
let doc;
|
|
132
|
+
try { doc = JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
133
|
+
catch (error) {
|
|
134
|
+
return { ok: false, brainVersion: null,
|
|
135
|
+
reason: `no installed runtime identity at ${INSTALLED_RUNTIME_FILE} (${error.code === 'ENOENT' ? 'absent' : error.message})` };
|
|
136
|
+
}
|
|
137
|
+
if (doc?.schemaVersion !== 1 || doc?.kind !== 'ruvnet-brain-installed-runtime'
|
|
138
|
+
|| typeof doc.brainVersion !== 'string' || !doc.brainVersion || !doc.executables) {
|
|
139
|
+
return { ok: false, brainVersion: null, reason: `${INSTALLED_RUNTIME_FILE} is not a readable installed-runtime record` };
|
|
140
|
+
}
|
|
141
|
+
const declared = Object.keys(doc.executables).sort();
|
|
142
|
+
const expected = [...RUNTIME_PINNED_EXECUTABLES].sort();
|
|
143
|
+
if (declared.join('\0') !== expected.join('\0')) {
|
|
144
|
+
return { ok: false, brainVersion: null,
|
|
145
|
+
reason: `${INSTALLED_RUNTIME_FILE} pins [${declared.join(', ')}], this runtime requires [${expected.join(', ')}]` };
|
|
146
|
+
}
|
|
147
|
+
for (const relative of expected) {
|
|
148
|
+
const measured = fileIdentity(path.join(kbDir, relative));
|
|
149
|
+
const pinned = doc.executables[relative];
|
|
150
|
+
if (!measured) {
|
|
151
|
+
return { ok: false, brainVersion: null, reason: `pinned runtime executable ${relative} is missing` };
|
|
152
|
+
}
|
|
153
|
+
if (measured.sha256 !== pinned?.sha256 || measured.bytes !== pinned?.bytes) {
|
|
154
|
+
return { ok: false, brainVersion: null,
|
|
155
|
+
reason: `pinned runtime executable ${relative} does not match its approved bytes `
|
|
156
|
+
+ `(approved ${String(pinned?.sha256).slice(0, 12)}…/${pinned?.bytes}B, on disk ${measured.sha256.slice(0, 12)}…/${measured.bytes}B)` };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { ok: true, brainVersion: doc.brainVersion, reason: null };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Decide whether this client may install the release now staged/offered.
|
|
164
|
+
*
|
|
165
|
+
* `offeredRuntimeVersion` is the brainVersion the candidate bundle declares — read from a STAGED
|
|
166
|
+
* tree that has already passed signature verification, never from the release payload's prose.
|
|
167
|
+
*
|
|
168
|
+
* @returns {{ok: boolean, reason: string|null, installedRuntime: string|null}}
|
|
169
|
+
*/
|
|
170
|
+
export function assertCorpusReleaseCompatible({ kbDir, offeredRuntimeVersion = null }) {
|
|
171
|
+
const runtime = readInstalledRuntime(kbDir);
|
|
172
|
+
if (!runtime.ok) return { ok: false, reason: runtime.reason, installedRuntime: null };
|
|
173
|
+
if (offeredRuntimeVersion == null) return { ok: true, reason: null, installedRuntime: runtime.brainVersion };
|
|
174
|
+
if (offeredRuntimeVersion !== runtime.brainVersion) {
|
|
175
|
+
return { ok: false, installedRuntime: runtime.brainVersion,
|
|
176
|
+
reason: `this corpus release was built by runtime ${offeredRuntimeVersion}; `
|
|
177
|
+
+ `this brain runs approved runtime ${runtime.brainVersion}` };
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, reason: null, installedRuntime: runtime.brainVersion };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Record the authenticated transport identity of the bytes now being installed, IN the tree being
|
|
184
|
+
* installed — so promotion carries it or nothing is promoted. The runtime identity fields
|
|
185
|
+
* (`brainVersion` / `releaseTag`) are never touched: they describe the code that built the bundle,
|
|
186
|
+
* and a corpus tag is not a version of that code.
|
|
187
|
+
*/
|
|
188
|
+
export function recordCorpusTransportIdentity(treeDir, { releaseTag }) {
|
|
189
|
+
const file = path.join(treeDir, 'SOURCE.json');
|
|
190
|
+
const source = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
191
|
+
// STRICTLY IDEMPOTENT, AND THAT IS NOT A STYLE CHOICE. A promoted tree's byte identity is what
|
|
192
|
+
// the storage transaction compares to decide `noop` vs `applied` — the distinction issue #106 and
|
|
193
|
+
// #108 were fought over. Rewriting SOURCE.json to set a field to the value it already holds (or
|
|
194
|
+
// to delete keys that were never there) re-serializes the file and changes its bytes, which turns
|
|
195
|
+
// a genuine no-op into a spurious "applied" and strands a rollback copy. So: touch the file only
|
|
196
|
+
// when a field actually changes. For the same reason there is no install timestamp here — a
|
|
197
|
+
// clock-valued field would differ on every run and could never be byte-identical. The tag IS the
|
|
198
|
+
// identity; `builtUtc` already carries the time.
|
|
199
|
+
if (isCorpusReleaseTag(releaseTag)) {
|
|
200
|
+
if (source.corpusReleaseTag === releaseTag) return source;
|
|
201
|
+
source.corpusReleaseTag = releaseTag;
|
|
202
|
+
} else {
|
|
203
|
+
// An ordinary code release supersedes whatever corpus generation preceded it: its bundle IS the
|
|
204
|
+
// corpus. Leaving a stale corpus tag behind would make the next check compare against a corpus
|
|
205
|
+
// this tree no longer holds.
|
|
206
|
+
if (!Object.hasOwn(source, 'corpusReleaseTag')) return source;
|
|
207
|
+
delete source.corpusReleaseTag;
|
|
208
|
+
}
|
|
209
|
+
atomicJson(file, source);
|
|
210
|
+
return source;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── the anti-loop ledger ────────────────────────────────────────────────────────────────────────
|
|
214
|
+
// A refusal that is rediscovered every night is a download loop with extra steps. The refused tag
|
|
215
|
+
// is remembered next to the KB (never inside it, where an update would replace it), so the very
|
|
216
|
+
// next run refuses BEFORE fetching a byte. It is cleared the moment a different release is offered.
|
|
217
|
+
|
|
218
|
+
export function rejectedReleasePath(kbDir) {
|
|
219
|
+
return path.join(path.dirname(path.resolve(kbDir)), REJECTED_RELEASE_FILE);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function readRejectedRelease(kbDir) {
|
|
223
|
+
try {
|
|
224
|
+
const doc = JSON.parse(fs.readFileSync(rejectedReleasePath(kbDir), 'utf8'));
|
|
225
|
+
return typeof doc?.tag === 'string' && doc.tag ? doc : null;
|
|
226
|
+
} catch { return null; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function writeRejectedRelease(kbDir, { tag, reason, installedRuntime = null }) {
|
|
230
|
+
try {
|
|
231
|
+
atomicJson(rejectedReleasePath(kbDir), { schemaVersion: 1, kind: 'ruvnet-brain-rejected-release',
|
|
232
|
+
tag, reason, installedRuntime, rejectedUtc: new Date().toISOString() });
|
|
233
|
+
return true;
|
|
234
|
+
} catch { return false; } // never let bookkeeping mask the refusal itself
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function clearRejectedRelease(kbDir) {
|
|
238
|
+
try { fs.rmSync(rejectedReleasePath(kbDir), { force: true }); return true; } catch { return false; }
|
|
239
|
+
}
|
|
@@ -48,12 +48,29 @@ export function treeIdentity(dir) {
|
|
|
48
48
|
fileCount: entries.filter((entry) => entry.type === 'file').length, entries };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// SYMLINK POLICY (issues #130/#131, applied here 2026-09-11). This threw on ANY symbolic link, and
|
|
52
|
+
// the installed brain always carries one: npm's `node_modules/.bin/semver`. So the already-current
|
|
53
|
+
// path in forge-update.mjs (managedStorageInventory at :1141) died on every machine, and the same
|
|
54
|
+
// link inside a `kb.bak-*` sibling wedged its inventory. A link that cannot be a store file and
|
|
55
|
+
// stays inside the tree is inventory, reported as `symlinkCount`. Two cases still throw, because
|
|
56
|
+
// they are what the hardening exists for: a link standing in for a store file, and a link that
|
|
57
|
+
// escapes the tree (its target is what a receipt would then silently be measuring).
|
|
51
58
|
function trustedTreeSummary(dir, kind) {
|
|
52
59
|
const identity = treeIdentity(dir);
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const root = path.resolve(dir);
|
|
61
|
+
let symlinkCount = 0;
|
|
62
|
+
for (const entry of identity.entries) {
|
|
63
|
+
if (entry.type !== 'symlink') continue;
|
|
64
|
+
const relative = entry.path.split('/').join(path.sep);
|
|
65
|
+
const link = path.join(dir, relative);
|
|
66
|
+
if (/\.rvf$/i.test(relative)) throw new Error(`managed ${kind} tree contains a symbolic link in place of a store file: ${link}`);
|
|
67
|
+
const resolved = path.resolve(root, path.dirname(relative), entry.target);
|
|
68
|
+
if (path.isAbsolute(entry.target) || !resolved.startsWith(`${root}${path.sep}`)) {
|
|
69
|
+
throw new Error(`managed ${kind} tree contains a symbolic link that escapes the tree: ${link} -> ${entry.target}`);
|
|
70
|
+
}
|
|
71
|
+
symlinkCount++;
|
|
55
72
|
}
|
|
56
|
-
return { kind, path: dir, sha256: identity.sha256, bytes: identity.bytes, fileCount: identity.fileCount };
|
|
73
|
+
return { kind, path: dir, sha256: identity.sha256, bytes: identity.bytes, fileCount: identity.fileCount, symlinkCount };
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
// Installer-retained paths are observations, not transaction-owned cleanup candidates. Count link
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.25",
|
|
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": {
|
|
@@ -33,10 +33,13 @@
|
|
|
33
33
|
"qe:ux": "node scripts/qe/ux-suite.mjs",
|
|
34
34
|
"test:cov": "vitest run tests/unit --coverage",
|
|
35
35
|
"coverage:source": "node scripts/source-coverage.mjs --check",
|
|
36
|
+
"private-overlay": "node scripts/private-overlay.mjs",
|
|
37
|
+
"seal-gist-receipt": "node scripts/seal-gist-receipt.mjs",
|
|
36
38
|
"test:all": "npm run test:unit && npm run test:mesh && npm run test:mutation && npm run test:regression && npm run test:integration && npm test",
|
|
37
39
|
"metaharness:receipts": "node scripts/metaharness-receipts.mjs",
|
|
38
40
|
"route:cheap": "node scripts/route-cheap.mjs",
|
|
39
41
|
"route:receipt": "node scripts/dispatch-receipt.mjs",
|
|
42
|
+
"trismart": "node scripts/trismart.mjs",
|
|
40
43
|
"metaharness:fix": "node scripts/fix-metaharness-memretrieve.mjs --apply",
|
|
41
44
|
"metaharness:check": "node scripts/fix-metaharness-memretrieve.mjs --check",
|
|
42
45
|
"eval": "node scripts/eval-brain.mjs",
|
|
@@ -64,7 +67,8 @@
|
|
|
64
67
|
"cap:report": "node scripts/rerank-cap-eval.mjs --report",
|
|
65
68
|
"cap:ab": "node scripts/rerank-cap-warm-ab.mjs",
|
|
66
69
|
"hooks:check": "node scripts/hook-retirement-check.mjs",
|
|
67
|
-
"release:qualify": "node scripts/release-qualification.mjs"
|
|
70
|
+
"release:qualify": "node scripts/release-qualification.mjs",
|
|
71
|
+
"rehearse:corpus": "node scripts/rehearse-corpus-pipeline.mjs"
|
|
68
72
|
},
|
|
69
73
|
"files": [
|
|
70
74
|
".claude-plugin/marketplace.json",
|
|
@@ -84,10 +88,12 @@
|
|
|
84
88
|
"LICENSE",
|
|
85
89
|
"console/",
|
|
86
90
|
"config/model-router/",
|
|
91
|
+
"tri-smart-skill/",
|
|
87
92
|
"data/model-catalog.json",
|
|
88
93
|
"scripts/",
|
|
89
94
|
"kb/verify-citation.mjs",
|
|
90
95
|
"kb/retrieval-result.mjs",
|
|
96
|
+
"kb/corpus-release-identity.mjs",
|
|
91
97
|
"kb/zip-extract.mjs",
|
|
92
98
|
"kb/brain-profile.mjs",
|
|
93
99
|
"kb/lifecycle-evidence-retention.mjs",
|
|
@@ -125,6 +131,7 @@
|
|
|
125
131
|
"@metaharness/darwin": "~0.9.2",
|
|
126
132
|
"@ruvector/rvf": "^0.3.4",
|
|
127
133
|
"@vitest/coverage-v8": "^4.1.10",
|
|
134
|
+
"mdast-util-from-markdown": "2.0.3",
|
|
128
135
|
"playwright": "^1.62.0",
|
|
129
136
|
"vitest": "^4.1.10"
|
|
130
137
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
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
|
|
4
|
-
"version": "4.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 a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
+
"version": "4.3.25",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Write a full project checkpoint to this project's canonical AgentDB store — the goal, the acceptance contract, the decisions, what is done, what is blocked, and the exact next action — so the next session (in Claude Code or Codex) resumes from a verified record instead of from nothing. Prints a read-back receipt."
|
|
3
|
+
updated: 2026-09-11
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# RuvNet-Brain: checkpoint
|
|
7
|
+
|
|
8
|
+
The user is asking you to **persist the state of this project** so the next session can resume it.
|
|
9
|
+
|
|
10
|
+
Automatic capture already runs at Stop, PreCompact and SessionEnd, and it records everything that can
|
|
11
|
+
be READ from the machine: the git tree, their work ledger, their newest `project-state-current` note,
|
|
12
|
+
a bounded transcript reference. **It cannot read what only you know** — the acceptance contract you
|
|
13
|
+
are working to, the decision you just made and why, the exact next action. That is what this command
|
|
14
|
+
is for, and it is why a checkpoint is worth more than another automatic snapshot.
|
|
15
|
+
|
|
16
|
+
## Do this
|
|
17
|
+
|
|
18
|
+
**1. Write down the real state — from THIS session, not from a template.**
|
|
19
|
+
|
|
20
|
+
Build a JSON object with the fields you can honestly fill. Every field is optional; set at least one.
|
|
21
|
+
Empty is better than invented: a fabricated "next action" is worse than no next action, because the
|
|
22
|
+
next session will act on it.
|
|
23
|
+
|
|
24
|
+
| field | what goes in it |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `currentGoal` | one sentence: what this work is actually for |
|
|
27
|
+
| `acceptanceContract` | what would make it DONE — the conditions, not the vibe |
|
|
28
|
+
| `nextAction` | the single next concrete step, specific enough to start cold |
|
|
29
|
+
| `decisions` | decisions made and WHY; include the ones you'd otherwise have to re-derive |
|
|
30
|
+
| `completed` / `inProgress` / `blockers` | honest status; a blocker named is a blocker halved |
|
|
31
|
+
| `failures` | what was tried and did not work, so it is not tried again |
|
|
32
|
+
| `changedFiles` | the files this work touched |
|
|
33
|
+
| `proofArtifacts` | commands run, outputs seen, receipts — the evidence, not the claim |
|
|
34
|
+
| `untested` | what was built but NOT verified. Do not omit this one. |
|
|
35
|
+
|
|
36
|
+
**2. Store it.**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node "${CLAUDE_PLUGIN_ROOT}/scripts/project-progression-checkpoint.mjs" --json '<your JSON>'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For anything longer than a line or two, write the JSON to a temp file first and use
|
|
43
|
+
`--json-file <path>` — a large single-quoted argument is how a shell mangles a checkpoint.
|
|
44
|
+
|
|
45
|
+
**3. Report the receipt, not a claim.**
|
|
46
|
+
|
|
47
|
+
The command prints the exact event key, the payload digest, and the digest of the row **read back
|
|
48
|
+
from the store by that exact key**. Tell the user it is stored only when `readbackVerified` is
|
|
49
|
+
`true`. If the command exits non-zero, say exactly what it printed — an unstored checkpoint that is
|
|
50
|
+
reported as stored is the specific failure this whole system exists to prevent.
|
|
51
|
+
|
|
52
|
+
If it reports `replayedPending`, say so: those were snapshots a previously interrupted session had
|
|
53
|
+
made durable but never committed, and this checkpoint settled them.
|
|
54
|
+
|
|
55
|
+
## What NOT to do
|
|
56
|
+
|
|
57
|
+
- **Do not** invent progress, decisions, or proof to make the checkpoint look complete.
|
|
58
|
+
- **Do not** write the user's secrets into it. Redaction runs anyway, but do not rely on it.
|
|
59
|
+
- **Do not** hand-write rows into `.swarm/memory.db` or call `ruflo memory store` yourself. This
|
|
60
|
+
command is the one writer; a second path is how two stores drift apart.
|
|
61
|
+
- **Do not** claim the next session "will remember" anything the receipt did not confirm.
|