ruvnet-brain 4.4.1 → 4.5.1
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 +3 -3
- package/bin/install.mjs +612 -113
- package/console/app.js +178 -81
- package/console/index.html +1 -1
- package/console/install-architecture.html +1 -0
- package/console/scope.css +4 -1
- package/console/style.css +13 -0
- package/console/tips.html +4 -4
- package/kb/brain-profile.mjs +1 -0
- package/kb/corpus-release-identity.mjs +1 -1
- package/kb/forge-update.mjs +41 -17
- package/kb/model-requirements.mjs +4 -1
- package/kb/update-storage-transaction.mjs +79 -0
- package/kb/zip-extract.mjs +22 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +5 -4
- package/plugin/commands/configure.md +5 -4
- package/plugin/commands/rnb-brief.md +41 -0
- package/plugin/commands/rnb.md +80 -0
- package/plugin/commands/rnbc.md +80 -0
- package/plugin/commands/rvbc.md +5 -4
- package/plugin/commands/rvcb.md +5 -4
- package/plugin/commands/whats-new.md +4 -4
- package/plugin/mcp/server.mjs +10 -2
- package/plugin/scripts/advocacy-route.mjs +59 -23
- package/plugin/scripts/anticipate.sh +4 -0
- package/plugin/scripts/brain-confirmation.mjs +263 -0
- package/plugin/scripts/brain-footprint.mjs +494 -0
- package/plugin/scripts/brain-location.mjs +47 -0
- package/plugin/scripts/capability-registry.mjs +11 -1
- package/plugin/scripts/continuity-brief.mjs +324 -0
- package/plugin/scripts/continuity-events.mjs +327 -0
- package/plugin/scripts/continuity-journal.mjs +500 -0
- package/plugin/scripts/decision-gate.mjs +56 -3
- package/plugin/scripts/footprint-io.mjs +186 -0
- package/plugin/scripts/ground-before-write.sh +8 -1
- package/plugin/scripts/ground-ruvnet.sh +103 -12
- package/plugin/scripts/grounding-answer.mjs +2 -1
- package/plugin/scripts/grounding-stamp.sh +3 -0
- package/plugin/scripts/grounding-substance.mjs +1 -1
- package/plugin/scripts/grounding-turn-evidence.mjs +17 -2
- package/plugin/scripts/hook-input.mjs +78 -4
- package/plugin/scripts/kb-copy-proof.mjs +148 -0
- package/plugin/scripts/lesson-bridge.mjs +6 -2
- package/plugin/scripts/nightly-controller.mjs +8 -1
- package/plugin/scripts/node-sqlite.mjs +41 -0
- package/plugin/scripts/package-cards.json +797 -0
- package/plugin/scripts/package-cards.rvf +0 -0
- package/plugin/scripts/package-cards.rvf.idmap.json +1 -0
- package/plugin/scripts/package-cards.rvf.meta.json +1 -0
- package/plugin/scripts/package-recommender-client.mjs +138 -0
- package/plugin/scripts/package-recommender-flag.mjs +30 -0
- package/plugin/scripts/package-recommender.mjs +391 -0
- package/plugin/scripts/project-progression-outbox.mjs +26 -8
- package/plugin/scripts/project-progression-reader.mjs +4 -2
- package/plugin/scripts/protect-brain-state.sh +4 -1
- package/plugin/scripts/session-snapshot-hook.mjs +27 -3
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +34 -4
- package/plugin/scripts/session-start-health.mjs +7 -1
- package/plugin/scripts/session-start-update-plane.mjs +35 -0
- package/plugin/scripts/turn-outcome-capture.mjs +12 -1
- package/plugin/scripts/unprompted-runtime.mjs +2 -2
- package/plugin/skills/brain-console/SKILL.md +3 -3
- package/plugin/skills/rnbc/SKILL.md +24 -0
- package/plugin/skills/rvbc/SKILL.md +2 -2
- package/scripts/approved-runtime.mjs +2 -2
- package/scripts/ci/warm-brain-models.mjs +28 -0
- package/scripts/codex-hook-trust.mjs +94 -0
- package/scripts/console-runtime-identity.mjs +5 -0
- package/scripts/corpus-canary.mjs +46 -6
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +1 -1
- package/scripts/hook-qualify-hosts.mjs +15 -3
- package/scripts/host-install-matrix.mjs +63 -2
- package/scripts/human-approval-phrases.mjs +46 -0
- package/scripts/installed-brain-health.mjs +53 -0
- package/scripts/move-brain.mjs +310 -0
- package/scripts/onboarding-console.mjs +93 -10
- package/scripts/oracle/abstain-threshold-sweep.mjs +62 -0
- package/scripts/oracle/abstain-trace.mjs +139 -0
- package/scripts/oracle/doc2query-generate.mjs +162 -0
- package/scripts/oracle/doc2query-reach.mjs +110 -0
- package/scripts/oracle/judge-train.mjs +158 -0
- package/scripts/oracle/need-set-split.mjs +48 -0
- package/scripts/oracle/sona-query-adapter-eval.mjs +139 -0
- package/scripts/package-cards.mjs +374 -0
- package/scripts/publication-receipt.mjs +37 -9
- package/scripts/recommendation-e2e.mjs +110 -0
- package/scripts/recommendation-eval.mjs +105 -0
- package/scripts/recommendation-floor.mjs +56 -0
- package/scripts/recommendation-judge-score.mjs +74 -0
- package/scripts/recommendation-latency.mjs +95 -0
- package/scripts/recommendation-real-host-score.mjs +76 -0
- package/scripts/recommendation-real-host.mjs +137 -0
- package/scripts/release-channel-kind.mjs +1 -1
- package/scripts/release-environment-policy.mjs +33 -0
- package/scripts/single-source-check.mjs +15 -10
- package/scripts/sync-commands.mjs +5 -2
- package/scripts/wired-check.mjs +17 -2
package/console/app.js
CHANGED
|
@@ -305,8 +305,7 @@ function updateVerdict() {
|
|
|
305
305
|
const v = $('#verdict');
|
|
306
306
|
if (!v || found.capsTotal == null) return; // nothing measured yet — say nothing
|
|
307
307
|
|
|
308
|
-
const behind =
|
|
309
|
-
? found.pkgTotal - found.pkgCurrent : null;
|
|
308
|
+
const behind = found.pkgNeedsLook != null ? found.pkgNeedsLook : null;
|
|
310
309
|
|
|
311
310
|
// What actually wants the reader's attention, in blast-radius order.
|
|
312
311
|
const needsLook = [];
|
|
@@ -316,6 +315,7 @@ function updateVerdict() {
|
|
|
316
315
|
const caveats = [];
|
|
317
316
|
if (found.capsUnknown) caveats.push(`${found.capsUnknown} capabilit${found.capsUnknown === 1 ? "y" : "ies"} we could not check`);
|
|
318
317
|
if (found.memNotTested) caveats.push(`${found.memNotTested} memory dimension${found.memNotTested === 1 ? '' : 's'} not probed this session`);
|
|
318
|
+
if (found.pkgUnchecked) caveats.push(`${found.pkgUnchecked} package${found.pkgUnchecked === 1 ? '' : 's'} whose version we could not check`);
|
|
319
319
|
|
|
320
320
|
const good = !needsLook.length;
|
|
321
321
|
const headline = good
|
|
@@ -323,8 +323,10 @@ function updateVerdict() {
|
|
|
323
323
|
: `Mostly good — ${needsLook.join(' and ')}.`;
|
|
324
324
|
|
|
325
325
|
const detail = `${found.capsOn} of ${found.capsTotal} capabilities are on`
|
|
326
|
+
+ (found.capsIdle ? `, ${found.capsIdle} set up but not running` : '')
|
|
326
327
|
+ (found.capsAbsent ? `, ${found.capsAbsent} not installed` : '')
|
|
327
|
-
+ (behind === 0 ? ', and every package on your stack is current' : '')
|
|
328
|
+
+ (behind === 0 && !found.pkgUnchecked ? ', and every package on your stack is current' : '')
|
|
329
|
+
+ (behind === 0 && found.pkgUnchecked ? ', and no checked package is behind' : '')
|
|
328
330
|
+ '.';
|
|
329
331
|
|
|
330
332
|
v.replaceChildren(
|
|
@@ -357,6 +359,7 @@ function renderHost(host, generatedAt) {
|
|
|
357
359
|
BRAIN_INSTALLED_VERSION = String(host.brainVersion);
|
|
358
360
|
const vc = $('#brain-ver');
|
|
359
361
|
if (vc) { vc.textContent = `v${BRAIN_INSTALLED_VERSION}`; vc.hidden = false; }
|
|
362
|
+
if (LAST_TRUST) renderUpdateGong(LAST_TRUST);
|
|
360
363
|
}
|
|
361
364
|
const meta = $('#host-meta');
|
|
362
365
|
if (meta) {
|
|
@@ -764,6 +767,12 @@ function renderStack(data) {
|
|
|
764
767
|
|
|
765
768
|
found.pkgTotal = total;
|
|
766
769
|
found.pkgCurrent = current;
|
|
770
|
+
// RNBC QA 2026-10-01: the verdict used to compute "behind" as total − current, so 34 plugin
|
|
771
|
+
// packages whose marketplace revision is simply not checked were announced as "34 packages
|
|
772
|
+
// behind" directly above a stack card saying "Nothing needs attention". Behind is what the audit
|
|
773
|
+
// MEASURED as behind or broken; not-checked is its own caveat.
|
|
774
|
+
found.pkgNeedsLook = behind + broken;
|
|
775
|
+
found.pkgUnchecked = Math.max(0, total - current - behind - broken - ahead);
|
|
767
776
|
updateFoundStrip();
|
|
768
777
|
|
|
769
778
|
pkgs.sort((a, b) =>
|
|
@@ -777,6 +786,8 @@ function renderStack(data) {
|
|
|
777
786
|
ahead ? el('span', {}, ', ', el('b', {}, fmtInt(ahead)), ' ahead of the registry (which is legal)') : '',
|
|
778
787
|
broken ? el('span', {}, ', ', el('b', {}, fmtInt(broken)), ' broken') : '',
|
|
779
788
|
behind ? el('span', {}, ', ', el('b', {}, fmtInt(behind)), ' behind') : '',
|
|
789
|
+
found.pkgUnchecked ? el('span', {}, ', ', el('b', {}, fmtInt(found.pkgUnchecked)),
|
|
790
|
+
' installed but not version-checked (plugin marketplace revisions are not compared)') : '',
|
|
780
791
|
'.', infoBtn('Your stack', STACK_INFO)));
|
|
781
792
|
|
|
782
793
|
if (pkgs.length) {
|
|
@@ -803,10 +814,17 @@ function renderStack(data) {
|
|
|
803
814
|
// (the purge:shadows recommendation — evidence, cost, undo); the in-sync majority collapses
|
|
804
815
|
// to a single verified line with a peel-back for whoever wants the full inventory.
|
|
805
816
|
const staleRows = shadows.filter((s) => s.stale);
|
|
806
|
-
|
|
817
|
+
// A cached copy with NO global install has nothing to "match" — RNBC QA 2026-10-01 measured
|
|
818
|
+
// four "cache 0.1.12 · global ?" rows each wearing a green "in sync" chip under "All 8 cached
|
|
819
|
+
// copies match your installs". They are counted and labelled for what they are.
|
|
820
|
+
const orphanRows = shadows.filter((s) => !s.stale && !s.global);
|
|
821
|
+
const syncCount = shadows.length - staleRows.length - orphanRows.length;
|
|
822
|
+
const orphanNote = orphanRows.length
|
|
823
|
+
? ` ${fmtInt(orphanRows.length)} more ${orphanRows.length === 1 ? 'is a cached copy' : 'are cached copies'} of a package with no global install to compare against.`
|
|
824
|
+
: '';
|
|
807
825
|
main.push(el('aside', { class: 'shadows' },
|
|
808
826
|
el('p', { class: 'shadows-title' }, 'Shadow copies in the npx cache',
|
|
809
|
-
staleRows.length ? chip(`${staleRows.length} stale`, 'warn') : chip('all in sync', 'green')),
|
|
827
|
+
staleRows.length ? chip(`${staleRows.length} stale`, 'warn') : chip(orphanRows.length ? 'none stale' : 'all in sync', 'green')),
|
|
810
828
|
staleRows.length ? el('p', { class: 'shadows-sub' },
|
|
811
829
|
'npx keeps private copies in ', el('code', {}, '~/.npm/_npx'),
|
|
812
830
|
'. A stale one quietly answers instead of your newer install — every command still “works”, which is exactly why it’s invisible. These need dealing with:') : null,
|
|
@@ -822,18 +840,18 @@ function renderStack(data) {
|
|
|
822
840
|
el('span', { class: 'shadow-dir' }, s.dir || ''),
|
|
823
841
|
)),
|
|
824
842
|
el('p', { class: 'shadows-ok' },
|
|
825
|
-
staleRows.length
|
|
826
|
-
?
|
|
843
|
+
(staleRows.length || orphanRows.length
|
|
844
|
+
? `${staleRows.length ? 'The other ' : ''}${fmtInt(syncCount)} cached ${syncCount === 1 ? 'copy matches' : 'copies match'} your installs — re-checked on every audit.`
|
|
827
845
|
: shadows.length === 1
|
|
828
846
|
? 'The 1 cached copy matches your install — re-checked on every audit; nothing is hiding stale.'
|
|
829
|
-
: `All ${fmtInt(shadows.length)} cached copies match your installs — re-checked on every audit; nothing is hiding stale.`),
|
|
847
|
+
: `All ${fmtInt(shadows.length)} cached copies match your installs — re-checked on every audit; nothing is hiding stale.`) + orphanNote),
|
|
830
848
|
el('details', { class: 'sub' },
|
|
831
849
|
el('summary', {}, `Peel it back — ${shadows.length === 1 ? 'the 1 cached copy' : `all ${fmtInt(shadows.length)} cached copies`}`),
|
|
832
850
|
el('div', { class: 'sub-body' },
|
|
833
851
|
shadows.map((s) => el('div', { class: 'shadow-row' },
|
|
834
852
|
el('span', { class: 'shadow-name' }, s.name || '—'),
|
|
835
853
|
el('span', { class: 'shadow-vers' }, `cache ${s.version ?? '?'} · global `, el('b', {}, s.global ?? '?')),
|
|
836
|
-
s.stale ? chip('stale', 'warn') : chip('in sync', 'green'),
|
|
854
|
+
s.stale ? chip('stale', 'warn') : s.global ? chip('in sync', 'green') : chip('no global install', 'grey', 'Nothing installed globally to compare this cached copy with'),
|
|
837
855
|
el('span', { class: 'shadow-dir' }, s.dir || ''),
|
|
838
856
|
))))));
|
|
839
857
|
}
|
|
@@ -1100,7 +1118,14 @@ function capRow(row) {
|
|
|
1100
1118
|
checkboxEligible
|
|
1101
1119
|
? capCheckbox(row)
|
|
1102
1120
|
: wantsAdvice
|
|
1103
|
-
? (
|
|
1121
|
+
? (row.setting
|
|
1122
|
+
? el('p', { class: 'cap-turnon' },
|
|
1123
|
+
el('span', { class: 'cap-turnon-lb' }, 'to turn it on'),
|
|
1124
|
+
'use the switch in Settings — ',
|
|
1125
|
+
el('button', { class: 'btn btn-ghost btn-sm cap-setting-jump', type: 'button', onclick: () => jumpToSetting(String(row.setting)) },
|
|
1126
|
+
'take me to it'),
|
|
1127
|
+
' (saved with an undo)')
|
|
1128
|
+
: turnOn
|
|
1104
1129
|
? el('p', { class: 'cap-turnon' },
|
|
1105
1130
|
el('span', { class: 'cap-turnon-lb' }, 'to turn it on'),
|
|
1106
1131
|
String(turnOn.human || 'run'), ' — ', el('code', {}, String(turnOn.cmd)))
|
|
@@ -1218,7 +1243,7 @@ function renderCapabilities(data) {
|
|
|
1218
1243
|
setChips('chips-capabilities', chips);
|
|
1219
1244
|
// Feed the verdict line. It needs the same counts this card just derived, and deriving them a
|
|
1220
1245
|
// second time somewhere else is how two surfaces start disagreeing about one machine.
|
|
1221
|
-
found.capsOn = on; found.capsOff = off; found.capsAbsent = absent; found.capsUnknown = unknown;
|
|
1246
|
+
found.capsOn = on; found.capsOff = off; found.capsAbsent = absent; found.capsUnknown = unknown; found.capsIdle = idle;
|
|
1222
1247
|
found.capsTotal = rows.length;
|
|
1223
1248
|
updateFoundStrip();
|
|
1224
1249
|
|
|
@@ -1237,6 +1262,9 @@ function renderCapabilities(data) {
|
|
|
1237
1262
|
const counts = [
|
|
1238
1263
|
on ? el('span', {}, el('b', {}, fmtInt(on)), ' on') : null,
|
|
1239
1264
|
off ? el('span', {}, el('b', {}, fmtInt(off)), ' off') : null,
|
|
1265
|
+
// RNBC QA 2026-10-01: idle rows were in the chips but missing from this sentence, so "11
|
|
1266
|
+
// capabilities: 7 on, 1 not installed, and 1 we could not check" silently lost two.
|
|
1267
|
+
idle ? el('span', {}, el('b', {}, fmtInt(idle)), ' set up but not running') : null,
|
|
1240
1268
|
absent ? el('span', {}, el('b', {}, fmtInt(absent)), ' not installed') : null,
|
|
1241
1269
|
unknown ? el('span', {}, el('b', {}, fmtInt(unknown)), ' we could not check') : null,
|
|
1242
1270
|
].filter(Boolean);
|
|
@@ -1723,7 +1751,10 @@ function addRecommendations(recs, source) {
|
|
|
1723
1751
|
* A full grouped-sections layout is a deliberate follow-up: its group order and whether user/machine
|
|
1724
1752
|
* split into two visible groups are product decisions the owner reserved. */
|
|
1725
1753
|
const REC_SCOPE_LABEL = {
|
|
1726
|
-
project
|
|
1754
|
+
// "One project", not "Just this project": a reconcile proposal names and changes a DIFFERENT project
|
|
1755
|
+
// from the one the console is serving (RNBC QA 2026-10-01 — the pill said "the project you are in
|
|
1756
|
+
// right now" over a card that rewires npx-project while the console served qa-project).
|
|
1757
|
+
project: { text: 'One project only', tone: 'cyan', title: 'Applying this changes only the one project this proposal names — nothing machine-wide.' },
|
|
1727
1758
|
user: { text: 'Every project · your account', tone: 'amber', title: 'Applying this changes behaviour for every project under your user account.' },
|
|
1728
1759
|
machine: { text: 'Every project · this machine', tone: 'amber', title: 'Applying this changes behaviour for every project on this computer.' },
|
|
1729
1760
|
};
|
|
@@ -2641,7 +2672,7 @@ function fieldBeats(f) {
|
|
|
2641
2672
|
* same widget — same markup, same "not chosen" honesty rule, same info-bubble mechanics — as every
|
|
2642
2673
|
* config.json field, rather than a bespoke control invented for one setting.
|
|
2643
2674
|
*/
|
|
2644
|
-
function buildSettingsField(f, values, defaults, refreshDirty) {
|
|
2675
|
+
function buildSettingsField(f, values, defaults, refreshDirty, overrides = null) {
|
|
2645
2676
|
const labId = `lab-${f.key}`;
|
|
2646
2677
|
const helpId = `help-${f.key}`;
|
|
2647
2678
|
const ctl = el('div', { class: 'field-ctl' });
|
|
@@ -2690,20 +2721,27 @@ function buildSettingsField(f, values, defaults, refreshDirty) {
|
|
|
2690
2721
|
// that this is a recommendation and not their current setting.
|
|
2691
2722
|
const chosen = values[f.key] === true || values[f.key] === false;
|
|
2692
2723
|
const rec = defaults[f.key] === true;
|
|
2693
|
-
|
|
2724
|
+
// A NOT-CHOSEN field is sent only once the person has touched it. RNBC QA 2026-10-01: every field
|
|
2725
|
+
// was posted on every save, so changing ONLY "Your model house" on a fresh machine also saved the
|
|
2726
|
+
// pre-filled recommendation nightly:true — and saveConfig() installed the nightly LaunchAgent the
|
|
2727
|
+
// person never touched. A recommendation is not a choice until someone makes it.
|
|
2728
|
+
let touched = false;
|
|
2729
|
+
const input = el('input', { type: 'checkbox', 'aria-labelledby': labId, 'aria-describedby': helpId,
|
|
2730
|
+
onchange: () => { touched = true; refreshDirty(); } });
|
|
2694
2731
|
input.checked = chosen ? values[f.key] === true : rec;
|
|
2695
2732
|
initialValue = input.checked;
|
|
2696
2733
|
ctl.append(el('label', { class: 'switch' }, input, el('span', { class: 'track', 'aria-hidden': 'true' })));
|
|
2697
2734
|
if (!chosen) ctl.append(notChosenField(rec ? 'on' : 'off'));
|
|
2698
|
-
collector = () => ({ include:
|
|
2735
|
+
collector = () => ({ include: chosen || touched, touched: !chosen && touched, value: input.checked });
|
|
2699
2736
|
} else if (f.type === 'enum' && Array.isArray(f.options)) {
|
|
2700
2737
|
const name = `seg-${f.key}`;
|
|
2701
2738
|
const seg = el('div', { class: 'seg', role: 'radiogroup', 'aria-labelledby': labId, 'aria-describedby': helpId });
|
|
2702
2739
|
const inputs = [];
|
|
2703
2740
|
const chosen = f.options.some((opt) => Object.is(values[f.key], opt));
|
|
2704
2741
|
const rec = f.options.includes(defaults[f.key]) ? defaults[f.key] : f.options[0];
|
|
2742
|
+
let touched = false;
|
|
2705
2743
|
for (const opt of f.options) {
|
|
2706
|
-
const input = el('input', { type: 'radio', name, value: String(opt), onchange: refreshDirty });
|
|
2744
|
+
const input = el('input', { type: 'radio', name, value: String(opt), onchange: () => { touched = true; refreshDirty(); } });
|
|
2707
2745
|
input.optionValue = opt;
|
|
2708
2746
|
input.checked = chosen ? Object.is(values[f.key], opt) : Object.is(opt, rec);
|
|
2709
2747
|
inputs.push(input);
|
|
@@ -2720,7 +2758,7 @@ function buildSettingsField(f, values, defaults, refreshDirty) {
|
|
|
2720
2758
|
}
|
|
2721
2759
|
ctl.append(seg);
|
|
2722
2760
|
if (!chosen) ctl.append(notChosenField(segLabel(f.key, rec)));
|
|
2723
|
-
collector = () => ({ include:
|
|
2761
|
+
collector = () => ({ include: chosen || touched, touched: !chosen && touched, value: inputs.find((i) => i.checked)?.optionValue });
|
|
2724
2762
|
} else {
|
|
2725
2763
|
const input = el('input', {
|
|
2726
2764
|
type: 'text', class: 'text-input', 'aria-labelledby': labId, 'aria-describedby': helpId, oninput: refreshDirty,
|
|
@@ -2731,6 +2769,15 @@ function buildSettingsField(f, values, defaults, refreshDirty) {
|
|
|
2731
2769
|
collector = () => ({ include: true, value: input.value });
|
|
2732
2770
|
}
|
|
2733
2771
|
|
|
2772
|
+
// A project file that overrides this key for the runtime that reads it (see projectOverrides()
|
|
2773
|
+
// on the server). Saving here still changes the user-level choice; the note says why this project
|
|
2774
|
+
// will not follow it.
|
|
2775
|
+
const ov = overrides && overrides.values && Object.hasOwn(overrides.values, f.key) ? overrides : null;
|
|
2776
|
+
if (ov) {
|
|
2777
|
+
ctl.append(el('p', { class: 'field-override bp-warn', role: 'note' },
|
|
2778
|
+
'This project overrides this setting: ', el('b', {}, String(typeof ov.values[f.key] === 'boolean' ? (ov.values[f.key] ? 'on' : 'off') : segLabel(f.key, ov.values[f.key]))),
|
|
2779
|
+
' (from ', el('code', {}, ov.path), '). A change saved here applies to your other projects, not this one.'));
|
|
2780
|
+
}
|
|
2734
2781
|
const row = el('div', { class: 'field', id: `field-${f.key}` },
|
|
2735
2782
|
el('div', {},
|
|
2736
2783
|
el('span', { class: 'field-label', id: labId }, f.label || f.key, infoBtn(f.label || f.key, fieldBeats(f))),
|
|
@@ -2746,6 +2793,11 @@ function buildSettingsField(f, values, defaults, refreshDirty) {
|
|
|
2746
2793
|
* single advocacy field backed by user-settings.mjs — same widget, same save/undo/error handling,
|
|
2747
2794
|
* only the endpoint and the file it names in its own copy differ.
|
|
2748
2795
|
*/
|
|
2796
|
+
// The last save's undo, per form, kept OUTSIDE the form so it survives a repaint. RNBC QA 2026-10-01:
|
|
2797
|
+
// the form is rebuilt whenever a background measurement lands, and the "Undo save" button vanished
|
|
2798
|
+
// with it — the undo was still in the server's journal, with no way left to reach it.
|
|
2799
|
+
const PENDING_SAVE_UNDO = new Map();
|
|
2800
|
+
|
|
2749
2801
|
function buildSettingsForm(cfg, { endpoint }) {
|
|
2750
2802
|
const values = cfg.values || {};
|
|
2751
2803
|
// What the project would pick FOR you, kept strictly apart from what you actually picked. The
|
|
@@ -2762,14 +2814,20 @@ function buildSettingsForm(cfg, { endpoint }) {
|
|
|
2762
2814
|
for (const [key, get] of Object.entries(collectors)) {
|
|
2763
2815
|
const g = get();
|
|
2764
2816
|
if (g.secret) { if (g.include) return true; continue; }
|
|
2817
|
+
if (g.touched) return true;
|
|
2765
2818
|
if (g.value !== initial[key]) return true;
|
|
2766
2819
|
}
|
|
2767
2820
|
return false;
|
|
2768
2821
|
}
|
|
2769
|
-
function refreshDirty() {
|
|
2822
|
+
function refreshDirty() {
|
|
2823
|
+
const dirty = isDirty();
|
|
2824
|
+
if (saveBtn) saveBtn.disabled = !dirty;
|
|
2825
|
+
// read by renderSettings: a repaint must not discard choices the person has not saved yet
|
|
2826
|
+
form.dataset.dirty = dirty ? '1' : '';
|
|
2827
|
+
}
|
|
2770
2828
|
|
|
2771
2829
|
for (const f of cfg.schema) {
|
|
2772
|
-
const { row, collector, initialValue } = buildSettingsField(f, values, defaults, refreshDirty);
|
|
2830
|
+
const { row, collector, initialValue } = buildSettingsField(f, values, defaults, refreshDirty, cfg.projectOverrides);
|
|
2773
2831
|
collectors[f.key] = collector;
|
|
2774
2832
|
if (f.type !== 'secret' && !f.secret) initial[f.key] = initialValue;
|
|
2775
2833
|
form.append(row);
|
|
@@ -2783,6 +2841,45 @@ function buildSettingsForm(cfg, { endpoint }) {
|
|
|
2783
2841
|
' in your user folder. Each choice is enforced by its named runtime; encrypted secrets and scheduler state never live in this browser response.')),
|
|
2784
2842
|
resultSlot);
|
|
2785
2843
|
|
|
2844
|
+
// Renders the saved note and its Undo. Called on a successful save AND whenever this form is
|
|
2845
|
+
// rebuilt while an undo is still pending, so a repaint cannot strand the undo.
|
|
2846
|
+
function showSaved(data) {
|
|
2847
|
+
// Not every store behind this form has an undo token — so the button only appears when one exists.
|
|
2848
|
+
const undoBtn = data.undoToken ? el('button', {
|
|
2849
|
+
class: 'btn btn-undo btn-sm', type: 'button',
|
|
2850
|
+
// try/catch, because this is an async onclick with a network call in it. The server's own
|
|
2851
|
+
// explanation is shown verbatim on failure: "this undo has already been used" and "your
|
|
2852
|
+
// settings were saved again after this point" are the two cases people will actually hit.
|
|
2853
|
+
onclick: async (ev) => {
|
|
2854
|
+
const btn = ev.currentTarget;
|
|
2855
|
+
btn.disabled = true;
|
|
2856
|
+
try {
|
|
2857
|
+
const r = await postJSON('/api/undo', { undoToken: data.undoToken });
|
|
2858
|
+
const ok = r.ok && r.data?.ok;
|
|
2859
|
+
if (!ok) btn.disabled = false;
|
|
2860
|
+
if (ok || /already been used|saved again after/.test(r.data?.log || '')) PENDING_SAVE_UNDO.delete(endpoint);
|
|
2861
|
+
resultSlot.replaceChildren(el('div', { class: `form-note ${ok ? 'n-ok' : 'n-err'}`, role: 'status' },
|
|
2862
|
+
ok
|
|
2863
|
+
? 'Settings restored from the backup. Reload to see the restored values.'
|
|
2864
|
+
: (r.status === 403 ? TOKEN_MSG
|
|
2865
|
+
: `Undo didn’t complete — ${r.data?.log || 'the backup file still exists, nothing is lost.'}`)));
|
|
2866
|
+
} catch (err) {
|
|
2867
|
+
btn.disabled = false;
|
|
2868
|
+
resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
|
|
2869
|
+
`Undo couldn’t reach the console server: ${err.message || err}. Nothing was changed.`));
|
|
2870
|
+
}
|
|
2871
|
+
},
|
|
2872
|
+
}, 'Undo save') : null;
|
|
2873
|
+
resultSlot.replaceChildren(el('div', { class: 'form-note n-ok', role: 'status' },
|
|
2874
|
+
frag(BADGE_OK),
|
|
2875
|
+
el('div', { class: 'fn-body' },
|
|
2876
|
+
el('b', {}, 'Saved.'), ' Your choices are in ',
|
|
2877
|
+
el('span', { class: 'fn-path' }, cfg.path || 'your user folder'), '.',
|
|
2878
|
+
data.backup ? el('span', {}, ' Backup kept at ', el('span', { class: 'fn-path' }, data.backup), '.') : ''),
|
|
2879
|
+
undoBtn));
|
|
2880
|
+
}
|
|
2881
|
+
if (PENDING_SAVE_UNDO.has(endpoint)) showSaved(PENDING_SAVE_UNDO.get(endpoint));
|
|
2882
|
+
|
|
2786
2883
|
form.addEventListener('submit', async (e) => {
|
|
2787
2884
|
e.preventDefault();
|
|
2788
2885
|
const out = {};
|
|
@@ -2799,46 +2896,12 @@ function buildSettingsForm(cfg, { endpoint }) {
|
|
|
2799
2896
|
if (code === 403) {
|
|
2800
2897
|
resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' }, TOKEN_MSG));
|
|
2801
2898
|
} else if (data && data.ok) {
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
// button simply does not appear for that form rather than pretending a capability exists.
|
|
2805
|
-
const undoBtn = data.undoToken ? el('button', {
|
|
2806
|
-
class: 'btn btn-undo btn-sm', type: 'button',
|
|
2807
|
-
// try/catch, because this is an async onclick with a network call in it. Without one, a
|
|
2808
|
-
// dropped connection rejected the promise, left the button permanently disabled and printed
|
|
2809
|
-
// NOTHING — the user is looking at a dead undo button with no idea whether it ran. The
|
|
2810
|
-
// server's own explanation is shown verbatim on failure: "this undo has already been used"
|
|
2811
|
-
// and "your settings were saved again after this point" are the two cases people will
|
|
2812
|
-
// actually hit, and both are worth reading.
|
|
2813
|
-
onclick: async (ev) => {
|
|
2814
|
-
const btn = ev.currentTarget;
|
|
2815
|
-
btn.disabled = true;
|
|
2816
|
-
try {
|
|
2817
|
-
const r = await postJSON('/api/undo', { undoToken: data.undoToken });
|
|
2818
|
-
const ok = r.ok && r.data?.ok;
|
|
2819
|
-
if (!ok) btn.disabled = false;
|
|
2820
|
-
resultSlot.replaceChildren(el('div', { class: `form-note ${ok ? 'n-ok' : 'n-err'}`, role: 'status' },
|
|
2821
|
-
ok
|
|
2822
|
-
? 'Settings restored from the backup. Reload to see the restored values.'
|
|
2823
|
-
: (r.status === 403 ? TOKEN_MSG
|
|
2824
|
-
: `Undo didn’t complete — ${r.data?.log || 'the backup file still exists, nothing is lost.'}`)));
|
|
2825
|
-
} catch (err) {
|
|
2826
|
-
btn.disabled = false;
|
|
2827
|
-
resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
|
|
2828
|
-
`Undo couldn’t reach the console server: ${err.message || err}. Nothing was changed.`));
|
|
2829
|
-
}
|
|
2830
|
-
},
|
|
2831
|
-
}, 'Undo save') : null;
|
|
2832
|
-
resultSlot.replaceChildren(el('div', { class: 'form-note n-ok', role: 'status' },
|
|
2833
|
-
frag(BADGE_OK),
|
|
2834
|
-
el('div', { class: 'fn-body' },
|
|
2835
|
-
el('b', {}, 'Saved.'), ' Your choices are in ',
|
|
2836
|
-
el('span', { class: 'fn-path' }, cfg.path || 'your user folder'), '.',
|
|
2837
|
-
data.backup ? el('span', {}, ' Backup kept at ', el('span', { class: 'fn-path' }, data.backup), '.') : ''),
|
|
2838
|
-
undoBtn));
|
|
2899
|
+
if (data.undoToken) PENDING_SAVE_UNDO.set(endpoint, data); else PENDING_SAVE_UNDO.delete(endpoint);
|
|
2900
|
+
showSaved(data);
|
|
2839
2901
|
for (const [key, get] of Object.entries(collectors)) {
|
|
2840
2902
|
const g = get(); if (!g.secret) initial[key] = g.value;
|
|
2841
2903
|
}
|
|
2904
|
+
form.dataset.dirty = ''; // saved: nothing left to protect from a repaint
|
|
2842
2905
|
announce('Settings saved.');
|
|
2843
2906
|
} else {
|
|
2844
2907
|
// The server says WHY — a rejected value names itself ("routing: expected one of auto, off").
|
|
@@ -2878,14 +2941,26 @@ function nightlyFactsLine(f) {
|
|
|
2878
2941
|
const run = f.lastRun === 'ok' ? (f.lastRunAt ? fmtDate(f.lastRunAt) : 'completed')
|
|
2879
2942
|
: f.lastRun === 'never-ran' ? 'never'
|
|
2880
2943
|
: f.lastRun === 'failed' ? 'failed' : String(f.lastRun || 'unknown');
|
|
2944
|
+
// Each fact carries ITS OWN evidence. RNBC QA 2026-10-01: the agent's evidence ("No LaunchAgent
|
|
2945
|
+
// plist at …") was printed after "last completed run: failed", reading as the reason the run
|
|
2946
|
+
// failed — while the run's real failure (a preflight refusal, a week earlier) was never shown.
|
|
2947
|
+
const runWhen = f.lastRun === 'failed' && f.lastRunAt ? ` ${fmtDate(f.lastRunAt)}` : '';
|
|
2881
2948
|
return el('p', { class: `fineprint field-facts${f.agree === false ? ' bp-warn' : ''}` },
|
|
2882
|
-
'switch: ', el('b', {}, choice),
|
|
2883
|
-
|
|
2884
|
-
f.enforcementEvidence ? el('span', { class: 'muted' }, ` (${f.enforcementEvidence})`) : ''
|
|
2949
|
+
'switch: ', el('b', {}, choice),
|
|
2950
|
+
' · agent: ', el('b', {}, agent),
|
|
2951
|
+
f.enforcementEvidence ? el('span', { class: 'muted nf-agent-ev' }, ` (${f.enforcementEvidence})`) : '',
|
|
2952
|
+
' · last completed run: ', el('b', {}, run + runWhen),
|
|
2953
|
+
f.lastRunEvidence && f.lastRun !== 'ok' ? el('span', { class: 'muted nf-run-ev' }, ` (${f.lastRunEvidence})`) : '',
|
|
2954
|
+
f.agree === false ? ' — these disagree, so the toggle above shows no single state until they do.' : '');
|
|
2885
2955
|
}
|
|
2886
2956
|
|
|
2887
2957
|
function renderSettings(cfg, us, bp) {
|
|
2888
2958
|
const body = $('#body-settings');
|
|
2959
|
+
// UNSAVED CHOICES SURVIVE A REPAINT. RNBC QA 2026-10-01: a background measurement landing while
|
|
2960
|
+
// someone was mid-way through the Settings form rebuilt it and silently threw their selections
|
|
2961
|
+
// away. While any form here has unsaved changes the card is left exactly as the person has it;
|
|
2962
|
+
// the next repaint after they save (or undo their edits) brings it current.
|
|
2963
|
+
if (body && body.querySelector('form.settings-form[data-dirty="1"]')) return;
|
|
2889
2964
|
// `bp` is NOT counted here any more: its field is rendered by #card-brain at the top of the page,
|
|
2890
2965
|
// and counting a control this card does not show would make the "N options" chip a lie by one.
|
|
2891
2966
|
const groups = [cfg, us].filter((c) => c && Array.isArray(c.schema) && c.schema.length);
|
|
@@ -2986,15 +3061,21 @@ function renderInventory(inv) {
|
|
|
2986
3061
|
inv && inv.reason ? inv.reason : 'The console could not read RVF-GENERATIONS.json from the installed brain, so it is not guessing a list.'));
|
|
2987
3062
|
return;
|
|
2988
3063
|
}
|
|
2989
|
-
|
|
3064
|
+
// "stores", not "repositories": gist and concept stores are in this list too (RNBC QA 2026-10-01).
|
|
3065
|
+
const chips = [chip(`${fmtInt(inv.count)} stores`, 'green')];
|
|
2990
3066
|
if (inv.privateCount > 0) chips.push(chip(`${fmtInt(inv.privateCount)} private`, 'cyan', 'Fenced private stores — never leave this machine'));
|
|
2991
3067
|
if (inv.releaseTag) chips.push(chip(`generation ${inv.releaseTag}`, 'grey'));
|
|
2992
3068
|
setChips('chips-inventory', chips);
|
|
2993
3069
|
|
|
2994
|
-
|
|
3070
|
+
// totalBytes sums each store's .rvf only. It was labelled "on disk" beside a Brain-power card
|
|
3071
|
+
// reporting the same 207 stores as 1.25 GB installed (RVF + passages + sidecars) — both true, but
|
|
3072
|
+
// only one of them is the disk footprint. Say which bytes this is.
|
|
3073
|
+
const mb = inv.totalBytes ? `${(inv.totalBytes / 1e6).toFixed(0)} MB of vector files` : null;
|
|
2995
3074
|
const summary = el('p', { class: 'muted' },
|
|
2996
3075
|
`${fmtInt(inv.count)} searchable stores`,
|
|
2997
|
-
|
|
3076
|
+
// withProvenance counts rows whose SOURCE.json names an upstream repository — not commits (nearly
|
|
3077
|
+
// every row shows its build commit below).
|
|
3078
|
+
inv.withProvenance != null ? `, ${fmtInt(inv.withProvenance)} with a named upstream repository` : '',
|
|
2998
3079
|
mb ? `, ${mb}.` : '.');
|
|
2999
3080
|
|
|
3000
3081
|
const rows = (inv.stores || []).map((s) => el('li', { class: 'inv-row', 'data-name': s.name.toLowerCase() },
|
|
@@ -3281,11 +3362,16 @@ function bpParts(bp) {
|
|
|
3281
3362
|
'best for cross-repository reasoning; RuVector Only is the compact choice for people who only ',
|
|
3282
3363
|
'need the RuVector source. Nightly updates preserve whichever profile is selected.'),
|
|
3283
3364
|
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3365
|
+
// RNBC QA 2026-10-01: this used to say these switches were "deliberately not shown … nothing
|
|
3366
|
+
// enforces them yet" — while the Settings card rendered all of them and learn-capture/learn-flush,
|
|
3367
|
+
// the console's guarded remedy loop and SessionStart seeding each read one. A sentence about the
|
|
3368
|
+
// page that the page itself contradicts is the failure this card exists to prevent.
|
|
3369
|
+
el('p', { class: 'fineprint bp-more-settings' },
|
|
3370
|
+
'Four more choices shape what the brain does while it is on — what it learns from, how much it ',
|
|
3371
|
+
'jumps in, whether it may act on its own, and whether new projects inherit these choices. They ',
|
|
3372
|
+
'are in ',
|
|
3373
|
+
el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => jumpToSetting('learningScope') }, 'Settings'),
|
|
3374
|
+
', and each one names the runtime that enforces it.'));
|
|
3289
3375
|
}
|
|
3290
3376
|
|
|
3291
3377
|
/* -------------------------------------------------- section 7: trust & provenance
|
|
@@ -3298,12 +3384,12 @@ const TRUST_INFO = {
|
|
|
3298
3384
|
signature: [
|
|
3299
3385
|
{ k: 'What is this?', t: 'The sha256 fingerprint of the release bundle, published as its own asset on every GitHub release.' },
|
|
3300
3386
|
{ k: 'Why does it matter?', t: 'If your download’s fingerprint matches the published one, the bundle is byte-identical to what was released — nothing altered, nothing truncated.' },
|
|
3301
|
-
{ k: 'How do I use it?', t: 'Run shasum -a 256 ruvnet-brain.zip on your download and compare.
|
|
3387
|
+
{ k: 'How do I use it?', t: 'Run shasum -a 256 ruvnet-brain.zip on your download and compare. A one-click local check here is planned.' },
|
|
3302
3388
|
],
|
|
3303
3389
|
sbom: [
|
|
3304
3390
|
{ k: 'What is this?', t: 'A Software Bill of Materials — the complete, machine-readable list of every package inside the bundle.' },
|
|
3305
3391
|
{ k: 'Why does it matter?', t: 'You can see what’s in the box without unzipping it, and scanners can watch it for known vulnerabilities.' },
|
|
3306
|
-
{ k: 'How does it help me?', t: '
|
|
3392
|
+
{ k: 'How does it help me?', t: 'Attaching a CycloneDX SBOM to every release is planned; this row will then show its package count and digest, measured from the published asset.' },
|
|
3307
3393
|
],
|
|
3308
3394
|
channel: [
|
|
3309
3395
|
{ k: 'What is this?', t: 'How your plugin updates: riding the latest release, or pinned to a version you chose.' },
|
|
@@ -3340,7 +3426,7 @@ function renderTrust(t) {
|
|
|
3340
3426
|
rel.ok ? chip('sha256 published ✓', 'green', 'The release bundle’s fingerprint is published and was read live this session')
|
|
3341
3427
|
: chip('digest unreachable', 'warn', 'Couldn’t read the published digest this session'),
|
|
3342
3428
|
sb.present ? chip(`SBOM · ${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'} ✓`, 'green', 'A local CycloneDX SBOM was found and read live this session')
|
|
3343
|
-
: chip('SBOM —
|
|
3429
|
+
: chip('SBOM — planned', 'coming', 'Releases do not carry a CycloneDX SBOM yet; it is planned'),
|
|
3344
3430
|
ch.installed ? chip(ch.channel === 'pinned' ? 'pinned' : 'latest channel', 'cyan') : chip('no plugin install', 'grey'),
|
|
3345
3431
|
]);
|
|
3346
3432
|
|
|
@@ -3370,7 +3456,7 @@ function renderTrust(t) {
|
|
|
3370
3456
|
/* 2 · SBOM — real once `npm run sbom` has been run locally; honest empty state until then */
|
|
3371
3457
|
rows.push(trustRow({
|
|
3372
3458
|
name: 'SBOM', info: TRUST_INFO.sbom, coming: !sb.present,
|
|
3373
|
-
status: sb.present ? chip(`${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'}`, 'green') : chip('
|
|
3459
|
+
status: sb.present ? chip(`${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'}`, 'green') : chip('planned', 'coming'),
|
|
3374
3460
|
value: sb.present ? [
|
|
3375
3461
|
el('p', {}, 'A ', el('b', {}, `CycloneDX ${sb.specVersion || ''}`.trim()), ' SBOM exists on this machine: ',
|
|
3376
3462
|
el('b', {}, `${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'}`),
|
|
@@ -3383,9 +3469,8 @@ function renderTrust(t) {
|
|
|
3383
3469
|
el('p', {}, sb.error
|
|
3384
3470
|
? `Found sbom/ruvnet-brain.cdx.json but couldn’t read it (${sb.error}).`
|
|
3385
3471
|
: 'Not generated yet on this machine — nothing to show, so nothing is shown.'),
|
|
3386
|
-
el('p', {}, '
|
|
3387
|
-
'
|
|
3388
|
-
' every published release carries one too, measured from the release asset itself.'),
|
|
3472
|
+
el('p', {}, 'Published releases do not carry an SBOM yet — attaching one, measured from the release asset itself, is planned. ',
|
|
3473
|
+
'From a source checkout, ', el('code', {}, 'npm run sbom'), ' produces a CycloneDX SBOM of the shipped dependency tree.'),
|
|
3389
3474
|
],
|
|
3390
3475
|
}));
|
|
3391
3476
|
|
|
@@ -3451,7 +3536,7 @@ function renderTrust(t) {
|
|
|
3451
3536
|
el('button', { class: 'adv-opt on', type: 'button', title: 'Your current behavior — consent-gated changes with undo' },
|
|
3452
3537
|
el('span', { class: 'pc-dot', 'aria-hidden': 'true' }), 'Full — recommended'),
|
|
3453
3538
|
advisorBtn,
|
|
3454
|
-
el('span', { class: 'preview-tag' }, 'preview ·
|
|
3539
|
+
el('span', { class: 'preview-tag' }, 'preview · planned')),
|
|
3455
3540
|
advNote,
|
|
3456
3541
|
],
|
|
3457
3542
|
}));
|
|
@@ -3459,7 +3544,7 @@ function renderTrust(t) {
|
|
|
3459
3544
|
body.replaceChildren(
|
|
3460
3545
|
el('p', { class: 'lead-stat' },
|
|
3461
3546
|
'Provenance you can check, not take on faith — ', el('b', {}, String(liveCount)),
|
|
3462
|
-
` measurement${liveCount === 1 ? ' is' : 's are'} live today; the rest of this card names exactly what
|
|
3547
|
+
` measurement${liveCount === 1 ? ' is' : 's are'} live today; the rest of this card names exactly what is planned and not yet measured.`,
|
|
3463
3548
|
infoBtn('Trust & provenance', TRUST_CARD_INFO)),
|
|
3464
3549
|
el('div', { class: 'trust-list', 'data-trust-ready': '1' }, ...rows),
|
|
3465
3550
|
);
|
|
@@ -3476,6 +3561,10 @@ function trustSkeleton() {
|
|
|
3476
3561
|
/* ---------------------------------------------- header update gong (owner, 2026-07-24) */
|
|
3477
3562
|
|
|
3478
3563
|
let BRAIN_INSTALLED_VERSION = null; // set by renderHost from /api/state's host.brainVersion
|
|
3564
|
+
// The last /api/trust payload. State and trust load in parallel; when trust lands first the gong is
|
|
3565
|
+
// decided with no installed version, so renderHost decides it again once the version is known
|
|
3566
|
+
// (RNBC 2026-10-01: under load the gong never appeared).
|
|
3567
|
+
let LAST_TRUST = null;
|
|
3479
3568
|
|
|
3480
3569
|
// Numeric x.y.z only; -dev/-rc suffixes are deliberately ignored so a tie is NEVER "newer" —
|
|
3481
3570
|
// a false gong is the nag this page promises not to be. Latest unreachable (null) is UNKNOWN,
|
|
@@ -3495,10 +3584,15 @@ function renderUpdateGong(t) {
|
|
|
3495
3584
|
const latest = t && t.release && t.release.tag ? String(t.release.tag).replace(/^v/, '') : null;
|
|
3496
3585
|
if (!BRAIN_INSTALLED_VERSION || !latest || cmpVer(latest, BRAIN_INSTALLED_VERSION) <= 0) { btn.hidden = true; return; }
|
|
3497
3586
|
const cmd = 'npx ruvnet-brain --update';
|
|
3498
|
-
|
|
3587
|
+
// SHORT ON PURPOSE. RNBC QA 2026-10-01: the long label ("⟳ update available · vX — click to update",
|
|
3588
|
+
// then "copied — paste in any terminal: npx …") overflowed the brand block and sat UNDER the
|
|
3589
|
+
// "what's in the brain" link at 1440px, so clicking the gong navigated to the scope page instead.
|
|
3590
|
+
// The full instruction lives in the title and the live region.
|
|
3591
|
+
btn.textContent = `⟳ v${latest} available`;
|
|
3592
|
+
btn.title = `A newer release (v${latest}) is out — click to copy the update command: ${cmd}`;
|
|
3499
3593
|
btn.onclick = async () => {
|
|
3500
|
-
try { await navigator.clipboard.writeText(cmd); btn.textContent =
|
|
3501
|
-
catch { btn.textContent = `run in any terminal: ${cmd}
|
|
3594
|
+
try { await navigator.clipboard.writeText(cmd); btn.textContent = '✓ update command copied'; announce(`Copied: ${cmd} — paste it in any terminal.`); }
|
|
3595
|
+
catch { btn.textContent = `run: ${cmd}`; announce(`Run in any terminal: ${cmd}`); }
|
|
3502
3596
|
};
|
|
3503
3597
|
btn.hidden = false;
|
|
3504
3598
|
}
|
|
@@ -3506,6 +3600,7 @@ function renderUpdateGong(t) {
|
|
|
3506
3600
|
async function loadTrust() {
|
|
3507
3601
|
try {
|
|
3508
3602
|
const t = await getJSON('/api/trust');
|
|
3603
|
+
LAST_TRUST = t;
|
|
3509
3604
|
renderTrust(t);
|
|
3510
3605
|
renderUpdateGong(t);
|
|
3511
3606
|
} catch (err) {
|
|
@@ -4288,8 +4383,10 @@ function renderLessons(data) {
|
|
|
4288
4383
|
chip(r.origin, r.userStated ? 'green' : 'nt',
|
|
4289
4384
|
r.userStated
|
|
4290
4385
|
? 'You said this. Only lessons you stated yourself are allowed to reach the strongest level.'
|
|
4291
|
-
: r.
|
|
4292
|
-
?
|
|
4386
|
+
: (r.sourceClass === 'imported-owner' || r.sourceClass === 'demonstration')
|
|
4387
|
+
? (r.quarantined
|
|
4388
|
+
? 'This came from bundled maintainer or demonstration history. It is not your statement and cannot be turned into your personal policy.'
|
|
4389
|
+
: 'This came from imported maintainer history and was ratified, so it is in force here. You can switch it off; it cannot be raised to "Stops me".')
|
|
4293
4390
|
: 'I inferred this from what happened. A lesson I inferred can never be raised to "Stops me", however often it fires — the model does not get to ratify its own rules.'),
|
|
4294
4391
|
r.taughtCount ? chip(`taught ${r.taughtCount}×`, 'grey') : null,
|
|
4295
4392
|
r.awaitingYou ? chip('awaiting your decision', 'amber', 'Recorded, but you have not agreed to it yet.') : null,
|
package/console/index.html
CHANGED
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
card in this section is L1 doing its job; anything with an undo behind it is L2. Full definitions
|
|
478
478
|
and the tests each level has to pass: <code>docs/adr/0028</code>.</p>
|
|
479
479
|
<p class="fineprint">The one thing on this list you can actually set is the volume on advocacy
|
|
480
|
-
itself: the <a href="#field-advocacy"><b>“How much it
|
|
480
|
+
itself: the <a href="#field-advocacy"><b>“How much it jumps in”</b></a> dial, in the Settings
|
|
481
481
|
card in this same section. Everything above is a description of the software, not a switch.</p>
|
|
482
482
|
</div>
|
|
483
483
|
</details>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>RuvNet Brain — install architecture</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 64 64%22><rect x=%222%22 y=%222%22 width=%2260%22 height=%2260%22 rx=%2216%22 fill=%22%230b0d12%22 stroke=%22%232c3850%22 stroke-width=%221.5%22/><path d=%22M25 48 C17 48 11.5 41.5 13.5 34.5 C10 29 13 21 19 19 C21 12.5 29 9.5 35 12 C43 9 50.5 14.5 50.5 22 C55 26 55 34.5 50 38 C49 43.5 43 47.5 37 46.5 L31 47.5 Z%22 fill=%22none%22 stroke=%22%23f0a830%22 stroke-width=%223.4%22 stroke-linejoin=%22round%22/><path d=%22M31 47 L31 33 M31 33 L24 27 M31 33 L40 24.5 M31 33 L41 36.5%22 fill=%22none%22 stroke=%22%23f0a830%22 stroke-width=%222.4%22 stroke-linecap=%22round%22 opacity=%22.9%22/><circle cx=%2224%22 cy=%2227%22 r=%223.4%22 fill=%22%235ad6ff%22/><circle cx=%2240%22 cy=%2224.5%22 r=%223.4%22 fill=%22%23f4b13a%22/><circle cx=%2241%22 cy=%2236.5%22 r=%223.4%22 fill=%22%235fd38a%22/></svg>">
|
|
7
8
|
<style>
|
|
8
9
|
:root {
|
|
9
10
|
--bg: #f4f1ea; --card: #fbfaf6; --ink: #26221b; --ink-2: #5b5648; --muted: #8a8474;
|
package/console/scope.css
CHANGED
|
@@ -100,8 +100,11 @@ table.scope thead th {
|
|
|
100
100
|
position: sticky; top: 58px; z-index: 2;
|
|
101
101
|
background: color-mix(in srgb, var(--surface-2) 92%, var(--bg));
|
|
102
102
|
font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted);
|
|
103
|
-
|
|
103
|
+
user-select: none; white-space: nowrap;
|
|
104
104
|
}
|
|
105
|
+
/* Only sortable headers look clickable (RNBC QA 2026-10-01: the commits/revisions headers wore a
|
|
106
|
+
pointer cursor and did nothing when clicked). */
|
|
107
|
+
table.scope thead th[data-key] { cursor: pointer; }
|
|
105
108
|
table.scope thead th[aria-sort="descending"]::after { content: " ▾"; color: var(--amber-text); }
|
|
106
109
|
table.scope thead th[aria-sort="ascending"]::after { content: " ▴"; color: var(--amber-text); }
|
|
107
110
|
table.scope tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--surface-2) 40%, transparent); }
|
package/console/style.css
CHANGED
|
@@ -229,6 +229,12 @@ p:last-child { margin-bottom: 0; }
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
.head-right { display: inline-flex; align-items: center; gap: 10px; }
|
|
232
|
+
/* The header is full at desktop widths. When the update gong appears it needs ~155px; the host chip
|
|
233
|
+
(also shown in the intro's host line) gives way so the gong never slides under the header links
|
|
234
|
+
(RNBC QA 2026-10-01: it did, and clicking it opened the scope page). The brand text truncates. */
|
|
235
|
+
.brand > * { flex: none; }
|
|
236
|
+
.brand > .brand-text { flex: 0 1 auto; min-width: 0; }
|
|
237
|
+
.head-inner:has(#brain-update:not([hidden])) .host-chip { display: none; }
|
|
232
238
|
.host-chip {
|
|
233
239
|
font-size: 12.5px; color: var(--ink-2);
|
|
234
240
|
border: 1px solid var(--hair); border-radius: 100px; padding: 4px 11px;
|
|
@@ -247,6 +253,9 @@ p:last-child { margin-bottom: 0; }
|
|
|
247
253
|
color: #7c3f00; background: rgba(217, 119, 6, .14);
|
|
248
254
|
border: 1px solid rgba(217, 119, 6, .55); border-radius: 100px; padding: 3px 10px;
|
|
249
255
|
margin-left: 6px; animation: gong-pulse 2.2s ease-in-out infinite;
|
|
256
|
+
/* Kept short in app.js and never squeezed: the brand text truncates before the gong does. A long
|
|
257
|
+
gong once overflowed under the header links, so clicking it opened the scope page. */
|
|
258
|
+
flex: none;
|
|
250
259
|
}
|
|
251
260
|
:root[data-theme="dark"] .update-gong { color: #fbbf24; background: rgba(217, 119, 6, .18); }
|
|
252
261
|
.update-gong:hover { animation-play-state: paused; }
|
|
@@ -1137,6 +1146,10 @@ details.sub .sub-body > *:first-child { margin-top: 4px; }
|
|
|
1137
1146
|
page-wide horizontal scrollbar. Cap + ellipsis keeps the click target and the full text in the
|
|
1138
1147
|
title tooltip; nothing about the update claim is hidden, only its overflow. */
|
|
1139
1148
|
.update-gong { max-width: 46vw; overflow: hidden; text-overflow: ellipsis; }
|
|
1149
|
+
/* RNBC QA 2026-10-01, measured at 390px: the header did not fit one row — the version chip sat
|
|
1150
|
+
under the scope link and the theme button was pushed off-screen. Let it wrap to two rows. */
|
|
1151
|
+
.head-inner { flex-wrap: wrap; height: auto; min-height: 58px; row-gap: 6px; padding-block: 8px; }
|
|
1152
|
+
.head-right { flex-wrap: wrap; justify-content: flex-end; row-gap: 6px; margin-left: auto; }
|
|
1140
1153
|
.fact { grid-template-columns: minmax(0, 1fr); gap: 3px; }
|
|
1141
1154
|
.dim { grid-template-columns: auto minmax(0, 1fr); }
|
|
1142
1155
|
.dim-ded { grid-row: auto; grid-column: 2; }
|
package/console/tips.html
CHANGED
|
@@ -492,11 +492,11 @@
|
|
|
492
492
|
<div class="peg">
|
|
493
493
|
<h3 class="peg-cap mono">slash commands</h3>
|
|
494
494
|
<ul class="peg-list">
|
|
495
|
-
<li><code>/
|
|
496
|
-
<li><code>/
|
|
497
|
-
<li><code>/
|
|
495
|
+
<li><code>/rnbc</code></li>
|
|
496
|
+
<li><code>/rnb</code></li>
|
|
497
|
+
<li><code>/rvbc</code></li>
|
|
498
498
|
</ul>
|
|
499
|
-
<p class="peg-note">All three open the same place — the console, the live mirror of your stack.</p>
|
|
499
|
+
<p class="peg-note">All three open the same place — the console, the live mirror of your stack (so do <code>/brain-console</code> and <code>/ruvnet-brain:configure</code>).</p>
|
|
500
500
|
</div>
|
|
501
501
|
<div class="peg">
|
|
502
502
|
<h3 class="peg-cap mono">skills</h3>
|