ruvnet-brain 3.9.134-dev → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/README.md +5 -5
  3. package/bin/install.mjs +382 -36
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/kb/zip-extract.mjs +53 -14
  25. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  26. package/package.json +14 -22
  27. package/plugin/.claude-plugin/marketplace.json +14 -0
  28. package/plugin/.claude-plugin/plugin.json +22 -0
  29. package/plugin/.codex-plugin/plugin.json +21 -0
  30. package/plugin/.mcp.json +8 -0
  31. package/plugin/commands/brain-console.md +16 -0
  32. package/plugin/commands/configure.md +33 -0
  33. package/plugin/commands/rvbc.md +79 -0
  34. package/plugin/commands/rvcb.md +16 -0
  35. package/plugin/commands/whats-new.md +57 -0
  36. package/plugin/hooks/codex-hooks.json +160 -0
  37. package/plugin/hooks/hook-contracts.json +77 -0
  38. package/plugin/hooks/hooks.json +202 -0
  39. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  40. package/plugin/mcp/server.mjs +56 -6
  41. package/plugin/scripts/anticipate.sh +534 -0
  42. package/plugin/scripts/codex-hook-adapter.mjs +96 -0
  43. package/plugin/scripts/continuation-gate.mjs +267 -0
  44. package/plugin/scripts/design-wall.sh +137 -0
  45. package/plugin/scripts/detach.mjs +182 -0
  46. package/plugin/scripts/first-session-worker.mjs +38 -0
  47. package/plugin/scripts/gate-receipt.sh +35 -0
  48. package/plugin/scripts/ground-before-write.sh +199 -0
  49. package/plugin/scripts/ground-ruvnet.sh +517 -0
  50. package/plugin/scripts/grounding-stamp.sh +113 -0
  51. package/plugin/scripts/grounding-substance.mjs +595 -0
  52. package/plugin/scripts/hijack-ruvnet.sh +81 -0
  53. package/plugin/scripts/hook-input.mjs +558 -0
  54. package/plugin/scripts/hook-shim-bash.mjs +55 -0
  55. package/plugin/scripts/hook-shim.mjs +303 -0
  56. package/plugin/scripts/host-update.mjs +58 -0
  57. package/plugin/scripts/kling-preflight.sh +146 -0
  58. package/plugin/scripts/learn-capture.sh +173 -0
  59. package/plugin/scripts/learn-flush.mjs +155 -0
  60. package/plugin/scripts/lesson-hooks.sh +213 -0
  61. package/plugin/scripts/md-stamp.mjs +219 -0
  62. package/plugin/scripts/protect-brain-state.sh +84 -0
  63. package/plugin/scripts/route-dispatch.sh +147 -0
  64. package/plugin/scripts/routing-outcome-capture.mjs +89 -0
  65. package/plugin/scripts/runtime-preferences.mjs +269 -0
  66. package/plugin/scripts/session-start-core.mjs +477 -0
  67. package/plugin/scripts/session-start.sh +13 -0
  68. package/plugin/scripts/signal-watch.mjs +193 -0
  69. package/plugin/scripts/unprompted-runtime.mjs +377 -0
  70. package/plugin/scripts/update-apply.mjs +419 -0
  71. package/plugin/scripts/verify-interface.sh +53 -0
  72. package/plugin/scripts/version-bump-gate.sh +112 -0
  73. package/plugin/skills/brain-build/SKILL.md +123 -0
  74. package/plugin/skills/brain-console/SKILL.md +22 -0
  75. package/plugin/skills/brain-prompt/SKILL.md +83 -0
  76. package/plugin/skills/brain-score/SKILL.md +101 -0
  77. package/plugin/skills/release-proof/SKILL.md +81 -0
  78. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  79. package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
  80. package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
  81. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +121 -0
  82. package/plugin/skills/ruvnet-brain/SKILL.md +234 -0
  83. package/plugin/skills/rvbc/SKILL.md +23 -0
  84. package/plugin/skills/savings/SKILL.md +46 -0
  85. package/plugin/skills/whats-new/SKILL.md +22 -0
  86. package/scripts/adr-backfill.mjs +107 -0
  87. package/scripts/advocacy-outcomes.mjs +808 -0
  88. package/scripts/agentdb-context.mjs +216 -0
  89. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  90. package/scripts/ascii-drift.mjs +236 -0
  91. package/scripts/behavioral-l1-l4.mjs +210 -0
  92. package/scripts/brain-capability-check.mjs +72 -0
  93. package/scripts/brain-grade-groundtruth.mjs +100 -0
  94. package/scripts/brain-latency-50.mjs +227 -0
  95. package/scripts/brain-novice-50.mjs +189 -0
  96. package/scripts/brain-stamp.mjs +94 -0
  97. package/scripts/brain-state.mjs +212 -0
  98. package/scripts/build-bundle.mjs +522 -0
  99. package/scripts/build-concepts.mjs +132 -0
  100. package/scripts/build-l2.mjs +71 -0
  101. package/scripts/build-primer.mjs +73 -0
  102. package/scripts/build-symbols.mjs +68 -0
  103. package/scripts/calibrate-router.mjs +97 -0
  104. package/scripts/capability-audit.mjs +321 -0
  105. package/scripts/capability-registry.mjs +876 -0
  106. package/scripts/check-indexation.mjs +108 -0
  107. package/scripts/check-legibility.mjs +189 -0
  108. package/scripts/ci/build-fixture-kb.mjs +67 -0
  109. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  110. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  111. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  112. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  113. package/scripts/ci/stranger-scenario.mjs +228 -0
  114. package/scripts/ci/stranger-timeout.mjs +25 -0
  115. package/scripts/ci-verdict.mjs +29 -0
  116. package/scripts/claims-verify.mjs +710 -0
  117. package/scripts/clear-claude-tmp.sh +31 -0
  118. package/scripts/console-engine.mjs +434 -0
  119. package/scripts/console-engine.test.mjs +125 -0
  120. package/scripts/corpus-qa.mjs +250 -0
  121. package/scripts/correction-detect-embed.mjs +346 -0
  122. package/scripts/correction-detect-measure.mjs +270 -0
  123. package/scripts/correction-detect.mjs +686 -0
  124. package/scripts/count-chunks.mjs +54 -0
  125. package/scripts/described-questions.json +30 -0
  126. package/scripts/design-grade.mjs +58 -0
  127. package/scripts/dev-plugin-link.sh +105 -0
  128. package/scripts/distill-project.mjs +200 -0
  129. package/scripts/doc-currency.mjs +801 -0
  130. package/scripts/eval-brain.mjs +244 -0
  131. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  132. package/scripts/full-hints.mjs +87 -0
  133. package/scripts/gate.sh +39 -0
  134. package/scripts/gates.mjs +146 -0
  135. package/scripts/gen-console-images.mjs +54 -0
  136. package/scripts/gen-images.mjs +47 -0
  137. package/scripts/git-clone-refresh.mjs +52 -0
  138. package/scripts/git-hooks/pre-push +126 -0
  139. package/scripts/goal-match.mjs +398 -0
  140. package/scripts/goldie-research.mjs +223 -0
  141. package/scripts/goldie-weekly.sh +67 -0
  142. package/scripts/health-repair.mjs +250 -0
  143. package/scripts/helix-scenario-questions.json +10 -0
  144. package/scripts/ingest-gists.mjs +230 -0
  145. package/scripts/ingest-meeting.mjs +115 -0
  146. package/scripts/ingest-repo.mjs +79 -0
  147. package/scripts/install-npx-witness.sh +49 -0
  148. package/scripts/issue-fix.mjs +639 -0
  149. package/scripts/issue-watch.mjs +276 -0
  150. package/scripts/issue4-close-note.md +31 -0
  151. package/scripts/key-canary.mjs +91 -0
  152. package/scripts/latency-to-surface.mjs +233 -0
  153. package/scripts/learning-enable.mjs +380 -0
  154. package/scripts/learning-replay.mjs +1570 -0
  155. package/scripts/learnings.mjs +62 -0
  156. package/scripts/lesson-gate.mjs +680 -0
  157. package/scripts/lesson-lifecycle.mjs +449 -0
  158. package/scripts/lesson-promote.mjs +262 -0
  159. package/scripts/lesson-ratify.mjs +98 -0
  160. package/scripts/lesson-seed.mjs +252 -0
  161. package/scripts/lesson-store.mjs +447 -0
  162. package/scripts/loop-checkpoint.mjs +86 -0
  163. package/scripts/memdb-health.sh +14 -0
  164. package/scripts/memory-doctor.mjs +271 -0
  165. package/scripts/model-catalog.mjs +79 -0
  166. package/scripts/nightly-controller.mjs +66 -0
  167. package/scripts/nightly-gists.sh +72 -0
  168. package/scripts/nightly-wrapper.sh +180 -0
  169. package/scripts/notify.sh +12 -0
  170. package/scripts/npx-witness.sh +56 -0
  171. package/scripts/onboarding-console.mjs +2749 -0
  172. package/scripts/private-fence.mjs +69 -0
  173. package/scripts/proactivity-metrics.mjs +118 -0
  174. package/scripts/proof-questions.json +56 -0
  175. package/scripts/prove.mjs +95 -0
  176. package/scripts/proxy/claude-proxied.sh +57 -0
  177. package/scripts/proxy/proxy-revert.sh +59 -0
  178. package/scripts/proxy/proxy-up.sh +60 -0
  179. package/scripts/proxy/proxy-verify.mjs +142 -0
  180. package/scripts/published-surface-probe.mjs +241 -0
  181. package/scripts/qe/card-lane-gate.mjs +162 -0
  182. package/scripts/qe/session-start-gate.mjs +229 -0
  183. package/scripts/qe/ux-suite.mjs +323 -0
  184. package/scripts/reconcile-project.mjs +0 -0
  185. package/scripts/record-lesson.mjs +113 -0
  186. package/scripts/refresh-model-catalog.mjs +99 -0
  187. package/scripts/release-proof.mjs +9 -0
  188. package/scripts/release-vector.mjs +281 -0
  189. package/scripts/release.mjs +395 -0
  190. package/scripts/remedy-registry.mjs +247 -0
  191. package/scripts/rerank-cap-eval.mjs +265 -0
  192. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  193. package/scripts/route-cheap.mjs +20 -15
  194. package/scripts/router-utilization.mjs +182 -0
  195. package/scripts/routing-flywheel.mjs +596 -0
  196. package/scripts/rvf-generation.mjs +104 -0
  197. package/scripts/rvf-index-audit.mjs +138 -0
  198. package/scripts/self-update.mjs +508 -0
  199. package/scripts/selfcheck.mjs +7 -1
  200. package/scripts/sign-bundle.mjs +69 -0
  201. package/scripts/signal-watch.mjs +171 -0
  202. package/scripts/stack-sync.mjs +469 -0
  203. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  204. package/scripts/stamp-sweep.mjs +144 -0
  205. package/scripts/status-honesty.mjs +102 -0
  206. package/scripts/sync-version.mjs +217 -0
  207. package/scripts/token-report.mjs +102 -0
  208. package/scripts/top100-benchmark.mjs +479 -0
  209. package/scripts/top100-corpus.mjs +112 -0
  210. package/scripts/top100-semantic-assertions.mjs +449 -0
  211. package/scripts/update-apply.mjs +9 -0
  212. package/scripts/upgrade-notice.mjs +14 -0
  213. package/scripts/verify-bundle.mjs +51 -0
  214. package/scripts/verify-channels.mjs +184 -0
  215. package/scripts/verify-model-catalog.mjs +104 -0
  216. package/scripts/verify-nightly-close-issue4.sh +31 -0
  217. package/scripts/version.mjs +40 -0
  218. package/scripts/wired-check.mjs +864 -0
package/console/app.js ADDED
@@ -0,0 +1,4189 @@
1
+ /* ============================================================================
2
+ RuvNet Brain — Onboarding Console (frontend)
3
+ ----------------------------------------------------------------------------
4
+ Vanilla ES module. No frameworks, no build step, no network beyond the
5
+ local API. Renders real machine state per console/CONTRACT.md:
6
+
7
+ Mirror → Explain → Recommend → (consent) Apply → Undo
8
+
9
+ Data flow: GET /api/state (fast) renders sections 2–6 immediately;
10
+ GET /api/stack (slow network audit) fills section 1 + late suggestions.
11
+ POSTs (/api/apply, /api/save-config, /api/undo) echo the launch token.
12
+ ============================================================================ */
13
+
14
+ 'use strict';
15
+
16
+ /* ------------------------------------------------------------------ setup */
17
+
18
+ const TOKEN = (typeof window !== 'undefined' && typeof window.__CONSOLE_TOKEN__ === 'string')
19
+ ? window.__CONSOLE_TOKEN__
20
+ : null; // tolerated: static preview has no token; GETs still work read-only
21
+
22
+ const MOCK = new URLSearchParams(location.search).has('mock'); // dev only, never default
23
+
24
+ let preStateHash = null; // echoed on /api/apply so the server can refuse a moved world
25
+ let lastMemory = null; // last rendered memory card, so the late fleet scan can merge into it
26
+ const renderedRecIds = new Set();
27
+ const renderedRecommendations = new Map();
28
+ let stateRecsSettled = false;
29
+ let stackRecsSettled = false;
30
+ // Health advocacy (ADR-027) hydrates from /api/memory, the slowest source. Until it has answered,
31
+ // the chip must NOT claim "none needed" — that would tell the user their machine is clean while the
32
+ // one check that actually looks at their learning state is still running. Silence read as an
33
+ // all-clear is the exact failure this ADR exists to end.
34
+ let healthRecsSettled = false;
35
+ const found = {}; // pieces of the "we looked at your computer" ribbon
36
+
37
+ /* --------------------------------------------------------------- helpers */
38
+
39
+ const $ = (sel, root = document) => root.querySelector(sel);
40
+
41
+ function el(tag, attrs, ...kids) {
42
+ const n = document.createElement(tag);
43
+ if (attrs) {
44
+ for (const [k, v] of Object.entries(attrs)) {
45
+ if (v == null || v === false) continue;
46
+ if (k === 'class') n.className = v;
47
+ else if (k === 'dataset') Object.assign(n.dataset, v);
48
+ else if (k.startsWith('on') && typeof v === 'function') n.addEventListener(k.slice(2), v);
49
+ else if (v === true) n.setAttribute(k, '');
50
+ else n.setAttribute(k, String(v));
51
+ }
52
+ }
53
+ for (const kid of kids.flat(Infinity)) {
54
+ if (kid == null || kid === false) continue;
55
+ n.append(kid.nodeType ? kid : document.createTextNode(String(kid)));
56
+ }
57
+ return n;
58
+ }
59
+
60
+ /* Parse a trusted (hand-authored, no interpolated data) SVG/HTML snippet. */
61
+ function frag(html) {
62
+ const t = document.createElement('template');
63
+ t.innerHTML = html.trim();
64
+ return t.content.firstElementChild;
65
+ }
66
+
67
+ function chip(text, tone, title) {
68
+ return el('span', { class: `chip tone-${tone || 'grey'}`, title: title || null }, text);
69
+ }
70
+
71
+ function setChips(id, chips) {
72
+ const c = document.getElementById(id);
73
+ if (c) c.replaceChildren(...chips);
74
+ }
75
+
76
+ function announce(msg) {
77
+ const r = $('#live-region');
78
+ if (r) { r.textContent = ''; r.textContent = msg; }
79
+ }
80
+
81
+ function illoBox(name) {
82
+ const tpl = document.getElementById('illo-' + name);
83
+ if (!tpl) return null;
84
+ return el('div', { class: 'illo-box', 'aria-hidden': 'true' }, tpl.content.firstElementChild.cloneNode(true));
85
+ }
86
+
87
+ /* Wrap section content beside its spot illustration.
88
+ The illo comes FIRST in DOM order so its mobile float lands beside the
89
+ opening text; the desktop grid re-places it via `order`. */
90
+ function withIllo(name, ...content) {
91
+ return el('div', { class: 'sect-body with-illo' },
92
+ illoBox(name),
93
+ el('div', { class: 'sect-main' }, ...content));
94
+ }
95
+
96
+ const fmtUsd = (n) => {
97
+ if (n == null || Number.isNaN(Number(n))) return '—';
98
+ const v = Number(n);
99
+ if (v > 0 && v < 0.01) return '<$0.01';
100
+ return '$' + v.toFixed(2);
101
+ };
102
+
103
+ const fmtMs = (ms) => {
104
+ if (ms == null || Number.isNaN(Number(ms))) return '—';
105
+ const v = Number(ms);
106
+ if (v < 1000) return `${Math.round(v)} ms`;
107
+ const s = v / 1000;
108
+ if (s < 60) return `${s.toFixed(1)} s`;
109
+ const m = Math.floor(s / 60);
110
+ return `${m} m ${Math.round(s % 60)} s`;
111
+ };
112
+
113
+ const fmtDate = (iso) => {
114
+ if (!iso) return '—';
115
+ try {
116
+ return new Date(iso).toLocaleString(undefined,
117
+ { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
118
+ } catch { return String(iso); }
119
+ };
120
+
121
+ const fmtInt = (n) => (n == null ? '—' : Number(n).toLocaleString());
122
+
123
+ /* ----------------------------------------------------------------- fetch */
124
+
125
+ async function getJSON(url) {
126
+ if (MOCK) return mockGet(url);
127
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
128
+ if (!res.ok) throw new Error(`${url} answered HTTP ${res.status}`);
129
+ return res.json();
130
+ }
131
+
132
+ async function postJSON(url, body) {
133
+ if (MOCK) return mockPost(url, body);
134
+ const res = await fetch(url, {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({ token: TOKEN, ...body }),
138
+ });
139
+ let data = {};
140
+ try { data = await res.json(); } catch { /* non-JSON error body */ }
141
+ return { status: res.status, ok: res.ok, data };
142
+ }
143
+
144
+ const TOKEN_MSG = 'The security token didn’t match — this page belongs to an older console launch. Restart the console and reload.';
145
+
146
+ /* ----------------------------------------------------------------- theme */
147
+
148
+ const THEME_KEY = 'rbc-theme';
149
+
150
+ function applyTheme(t) {
151
+ document.documentElement.dataset.theme = t;
152
+ const btn = $('#theme-toggle');
153
+ if (btn) btn.setAttribute('aria-label', t === 'dark' ? 'Switch to light theme' : 'Switch to dark theme');
154
+ }
155
+
156
+ function initTheme() {
157
+ applyTheme(document.documentElement.dataset.theme === 'light' ? 'light' : 'dark');
158
+ $('#theme-toggle')?.addEventListener('click', () => {
159
+ const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
160
+ try { localStorage.setItem(THEME_KEY, next); } catch { /* private mode */ }
161
+ applyTheme(next);
162
+ });
163
+ try {
164
+ matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
165
+ let stored = null;
166
+ try { stored = localStorage.getItem(THEME_KEY); } catch { /* ignore */ }
167
+ if (stored !== 'light' && stored !== 'dark') applyTheme(e.matches ? 'light' : 'dark');
168
+ });
169
+ } catch { /* older engines */ }
170
+ }
171
+
172
+ /* ---------------------------------------------------------------- errors */
173
+
174
+ function inlineError(bodyId, msg, retry) {
175
+ const body = document.getElementById(bodyId);
176
+ if (!body) return;
177
+ body.replaceChildren(
178
+ el('div', { class: 'inline-error', role: 'alert' },
179
+ el('p', { class: 'ie-title' }, 'Couldn’t read this section.'),
180
+ el('p', { class: 'ie-msg' }, msg),
181
+ retry ? el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: retry }, 'Try again') : null,
182
+ ),
183
+ );
184
+ }
185
+
186
+ function showGlobalError(err) {
187
+ const b = $('#global-error');
188
+ if (!b) return;
189
+ b.hidden = false;
190
+ b.replaceChildren(
191
+ el('p', {},
192
+ 'Couldn’t reach the console server (', el('code', {}, String(err.message || err)), '). ',
193
+ 'The page stays read-only either way — nothing was touched.'),
194
+ el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => { b.hidden = true; loadState(); } }, 'Try again'),
195
+ );
196
+ }
197
+
198
+ /* ---------------------------------------------------- the "found" ribbon */
199
+
200
+ /* One-time count-up for the found-strip numbers: each figure rolls in the first time it
201
+ appears, then stays static across the ribbon's later re-renders. Real values only — the
202
+ animation is a reveal, never an estimate — and prefers-reduced-motion gets the final
203
+ number immediately. */
204
+ const REDUCED_MOTION = (() => { try { return matchMedia('(prefers-reduced-motion: reduce)').matches; } catch { return true; } })();
205
+ const countedKeys = new Set();
206
+ function countUpNum(key, value, render = fmtInt) {
207
+ const target = Number(value);
208
+ const b = el('b', {}, render(target));
209
+ if (REDUCED_MOTION || countedKeys.has(key) || !Number.isFinite(target) || target <= 0) {
210
+ countedKeys.add(key);
211
+ return b;
212
+ }
213
+ countedKeys.add(key);
214
+ const dur = 700;
215
+ let t0 = null;
216
+ const tick = (t) => {
217
+ if (t0 == null) t0 = t;
218
+ const p = Math.min(1, (t - t0) / dur);
219
+ const eased = 1 - Math.pow(1 - p, 3); // ease-out cubic
220
+ b.textContent = render(Math.round(target * eased));
221
+ if (p < 1) requestAnimationFrame(tick);
222
+ else b.textContent = render(target);
223
+ };
224
+ requestAnimationFrame(tick);
225
+ return b;
226
+ }
227
+
228
+ function updateFoundStrip() {
229
+ const strip = $('#found-strip');
230
+ if (!strip || !found.host) return;
231
+ const bits = [];
232
+ if (found.pkgTotal != null) {
233
+ bits.push(el('span', {}, countUpNum('pkgTotal', found.pkgTotal), ' packages on your global stack',
234
+ found.pkgCurrent != null ? el('span', {}, ' (', countUpNum('pkgCurrent', found.pkgCurrent), ' current)') : ''));
235
+ }
236
+ if (found.npx != null) {
237
+ // THE SENTENCE USED TO CONTRADICT ITSELF IN THE SPACE OF SIX WORDS:
238
+ // "0 npx call sites across 0 projects — AI Retirement Analyzer, AMBULANCE_INVENTORY +63 more"
239
+ // `projectNames` is the list of projects we SCANNED, not the projects with npx call sites, and it
240
+ // was appended unconditionally to a count of npx sites. So the first data sentence on the page —
241
+ // the one immediately above "every number below traces to something we actually observed" —
242
+ // named 65 projects while asserting there were none. Nothing was miscounted; the two halves were
243
+ // about different things and were joined anyway. Found by Fable 5, 2026-07-24.
244
+ //
245
+ // Zero is the good outcome here (no npx drift), so it gets a sentence that reads as good news
246
+ // and credits the scan, instead of a contradiction that makes the reader distrust the number.
247
+ const scanned = found.projectNames?.length ?? 0;
248
+ if (found.npx === 0) {
249
+ bits.push(el('span', {}, 'no npx call sites',
250
+ scanned ? el('span', {}, ' in the ', countUpNum('projects', scanned), ' projects we scanned') : ''));
251
+ } else {
252
+ bits.push(el('span', {}, countUpNum('npx', found.npx), ' npx call sites across ',
253
+ countUpNum('projects', found.projects ?? 0), ' projects',
254
+ found.projectNames?.length
255
+ ? el('span', {}, ' — ', el('span', { class: 'fs-path' }, found.projectNames.slice(0, 2).join(', ')),
256
+ found.projectNames.length > 2 ? ` +${found.projectNames.length - 2} more` : '')
257
+ : ''));
258
+ }
259
+ }
260
+ if (found.memScore != null) {
261
+ // The qualifier travels WITH the number. A score whose asterisk lives in another card is an
262
+ // unqualified score to everyone who reads only this line — which is everyone, it is the ribbon.
263
+ bits.push(el('span', {}, 'memory quality ',
264
+ countUpNum('memScore', found.memScore, (v) => `${v}/100`),
265
+ found.memNotTested
266
+ ? el('span', { class: 'muted' },
267
+ ` (${found.memProbed} of ${found.memDims} dimensions checked)`)
268
+ : ''));
269
+ }
270
+ strip.replaceChildren(
271
+ el('span', {}, 'We looked around ', el('b', {}, found.host), '’s machine: '),
272
+ ...bits.flatMap((b, i) => (i ? [' · ', b] : [b])),
273
+ el('span', {}, '. Every number below traces to something we actually observed.'),
274
+ );
275
+ strip.hidden = false;
276
+ updateVerdict();
277
+ }
278
+
279
+ /**
280
+ * THE VERDICT LINE — the one sentence the page never said.
281
+ *
282
+ * Fable 5, 2026-07-24: "8,100px and the page never once says the one sentence the owner's success
283
+ * criterion demands: 'You're in good shape. One package needs a look; three suggestions below.'
284
+ * Every card makes you derive the verdict from chips." That is the whole gap between a dashboard and
285
+ * an answer, and it is why a careful reader can finish this page without ever learning how they are
286
+ * doing.
287
+ *
288
+ * THREE RULES IT OBEYS, all of them the product's existing rules applied to one sentence:
289
+ *
290
+ * 1. DERIVED, NEVER ASSERTED. Every number here is handed over by the card that measured it. This
291
+ * function computes nothing about the machine; it only decides which sentence the measurements
292
+ * support. That is also why it renders nothing until the counts arrive — an empty verdict is
293
+ * honest, an early one is a guess.
294
+ * 2. UNKNOWN IS NOT GOOD NEWS. Capabilities we could not check are stated separately and never
295
+ * folded into the good column, because "we could not tell" reported as "fine" is the exact lie
296
+ * the four-state model exists to prevent.
297
+ * 3. GOOD NEWS IS ALLOWED TO SOUND LIKE GOOD NEWS. The owner's criterion is that someone smiles and
298
+ * thinks "I have more tools than I realised." A page that can only ever hedge cannot deliver
299
+ * that. When the evidence says things are in good shape, this says so plainly — and names the
300
+ * exceptions in the same breath, which is what makes it believable rather than cheerful.
301
+ */
302
+ function updateVerdict() {
303
+ const v = $('#verdict');
304
+ if (!v || found.capsTotal == null) return; // nothing measured yet — say nothing
305
+
306
+ const behind = (found.pkgTotal != null && found.pkgCurrent != null)
307
+ ? found.pkgTotal - found.pkgCurrent : null;
308
+
309
+ // What actually wants the reader's attention, in blast-radius order.
310
+ const needsLook = [];
311
+ if (found.capsOff) needsLook.push(`${found.capsOff} capabilit${found.capsOff === 1 ? 'y is' : 'ies are'} off`);
312
+ if (behind) needsLook.push(`${behind} package${behind === 1 ? '' : 's'} behind`);
313
+
314
+ const caveats = [];
315
+ if (found.capsUnknown) caveats.push(`${found.capsUnknown} capabilit${found.capsUnknown === 1 ? "y" : "ies"} we could not check`);
316
+ if (found.memNotTested) caveats.push(`${found.memNotTested} memory dimension${found.memNotTested === 1 ? '' : 's'} not probed this session`);
317
+
318
+ const good = !needsLook.length;
319
+ const headline = good
320
+ ? 'You’re in good shape.'
321
+ : `Mostly good — ${needsLook.join(' and ')}.`;
322
+
323
+ const detail = `${found.capsOn} of ${found.capsTotal} capabilities are on`
324
+ + (found.capsAbsent ? `, ${found.capsAbsent} not installed` : '')
325
+ + (behind === 0 ? ', and every package on your stack is current' : '')
326
+ + '.';
327
+
328
+ v.replaceChildren(
329
+ el('span', { class: `verdict-mark ${good ? 'vm-good' : 'vm-look'}` }, good ? '✓' : '!'),
330
+ el('span', {},
331
+ el('b', {}, headline), ' ', detail,
332
+ // Caveats read as their own clause, not as a subordinate one. "We're not counting X, or Y as
333
+ // either" parsed badly out loud, and a sentence about honesty that the reader has to re-read
334
+ // is not doing its job.
335
+ caveats.length ? el('span', { class: 'muted' }, ` Not counted either way: ${caveats.join(' · ')}.`) : '',
336
+ !good ? el('span', { class: 'muted' }, ' Each one is named below, with its evidence and its undo.') : ''),
337
+ );
338
+ v.hidden = false;
339
+ }
340
+
341
+ /* ------------------------------------------------------------- section 0: host */
342
+
343
+ // Node reports the OS by its kernel codename — 'darwin' is the Unix core inside macOS. That is the
344
+ // machine's word for itself, not a person's, and this page is meant to read in plain English.
345
+ function osName(platform) {
346
+ return { darwin: 'macOS', win32: 'Windows', linux: 'Linux' }[platform] || platform || '—';
347
+ }
348
+ function renderHost(host, generatedAt) {
349
+ if (host && host.user) {
350
+ found.host = `${host.user}@${osName(host.platform)}`;
351
+ const hc = $('#host-chip');
352
+ if (hc) { hc.textContent = found.host; hc.hidden = false; }
353
+ // Header version chip (owner, 2026-07-24): the version is worn openly, next to the wordmark.
354
+ if (host.brainVersion) {
355
+ BRAIN_INSTALLED_VERSION = String(host.brainVersion);
356
+ const vc = $('#brain-ver');
357
+ if (vc) { vc.textContent = `v${BRAIN_INSTALLED_VERSION}`; vc.hidden = false; }
358
+ }
359
+ const meta = $('#host-meta');
360
+ if (meta) {
361
+ meta.replaceChildren(
362
+ el('span', {}, 'user ', el('b', {}, host.user)),
363
+ el('span', {}, 'platform ', el('b', {}, osName(host.platform))),
364
+ el('span', {}, 'node ', el('b', {}, host.node || '—')),
365
+ el('span', {}, 'npm prefix ', el('b', {}, host.npmPrefix || '—')),
366
+ );
367
+ meta.hidden = false;
368
+ }
369
+ }
370
+ const fg = $('#foot-generated');
371
+ if (fg && generatedAt) fg.textContent = `machine state read ${fmtDate(generatedAt)}`;
372
+ updateFoundStrip();
373
+ }
374
+
375
+ /* ------------------------------------------------------------ section 1: stack */
376
+
377
+ const STATE_ORDER = { BROKEN: 0, BEHIND: 1, UNRESOLVED: 2, AHEAD: 3, CURRENT: 4 };
378
+ const STATE_TONE = { CURRENT: 'green', BEHIND: 'warn', AHEAD: 'cyan', BROKEN: 'red', UNRESOLVED: 'grey' };
379
+ const STATE_TITLE = {
380
+ AHEAD: 'Newer than the target — a legal state, not an error.',
381
+ UNRESOLVED: 'We couldn’t check this one — reported honestly, not guessed.',
382
+ BROKEN: 'Present on disk but no readable version.',
383
+ };
384
+
385
+ function stackSkeleton() {
386
+ $('#body-stack').replaceChildren(
387
+ frag(`<div class="skeleton" aria-hidden="true">
388
+ <div class="sk-bar w35"></div><div class="sk-bar w90"></div>
389
+ <div class="sk-bar w85"></div><div class="sk-bar w88"></div><div class="sk-bar w60"></div></div>`),
390
+ el('p', { class: 'loading-note' },
391
+ 'Checking every global package against the npm registry, one by one — read-only, nothing changes. ',
392
+ 'Private by design: the registry only sees ordinary version lookups; nothing about you or your projects leaves this machine. ',
393
+ 'On a full stack the first look honestly takes 30–60 seconds; after that it’s instant from cache. ',
394
+ el('span', { class: 'elapsed', id: 'stack-elapsed' }, '')),
395
+ );
396
+ setChips('chips-stack', [chip('checking registry…', 'wait')]);
397
+ if (stackTicker) clearInterval(stackTicker);
398
+ const t0 = Date.now();
399
+ stackTicker = setInterval(() => {
400
+ const target = document.getElementById('stack-elapsed');
401
+ if (!target) { clearInterval(stackTicker); stackTicker = null; return; }
402
+ const s = Math.round((Date.now() - t0) / 1000);
403
+ target.textContent = `— ${s}s in, still working (the registry answers one package at a time)`;
404
+ }, 1000);
405
+ }
406
+
407
+ function pkgRow(p) {
408
+ const st = STATE_ORDER[p.state] != null ? p.state : 'UNRESOLVED';
409
+ // ISSUE #22 — a tool installed via the Claude Code plugin marketplace is first-class here; mark it
410
+ // so its "plugin" tag column isn't the only tell that it tracks a marketplace cadence, not npm.
411
+ const plugin = p.source === 'plugin';
412
+ return el('tr', {},
413
+ el('td', { class: 'cell-name' }, p.name || '—',
414
+ plugin ? el('span', { class: 'src-tag', title: `installed via the ${p.marketplace || 'Claude Code'} plugin marketplace` }, ' plugin') : null),
415
+ el('td', { class: 'cell-mono' },
416
+ p.installed != null ? p.installed : el('span', { style: 'color:var(--red-text)' }, 'unreadable')),
417
+ el('td', { class: 'cell-mono' }, p.target ?? '—'),
418
+ el('td', { class: 'cell-mono cell-dim' }, p.tag ?? '—'),
419
+ el('td', {}, chip(st, STATE_TONE[st], STATE_TITLE[st]),
420
+ p.state === 'BEHIND' ? el('button', {
421
+ class: 'btn-fix', type: 'button',
422
+ title: `Update ${p.name} to ${p.target ?? 'latest'} — one click below, undo recorded first`,
423
+ onclick: () => jumpToRec(`sync:${p.name}`),
424
+ }, `update → ${p.target ?? 'latest'}`) : null,
425
+ p.state === 'BROKEN' ? el('button', {
426
+ class: 'btn-fix', type: 'button', title: `Repair ${p.name} — one click below`,
427
+ onclick: () => jumpToRec(`repair:${p.name}`),
428
+ }, 'repair') : null),
429
+ );
430
+ }
431
+
432
+ /* ---- family grouping: roll sub-packages up under the tool people recognize ---- */
433
+ const STACK_FAMILIES = [
434
+ { name: 'ruflo', what: 'orchestration brain', test: (n) => n === 'ruflo' || n === '@claude-flow/cli' },
435
+ { name: 'AgentDB', what: 'memory that learns', test: (n) => n === '@claude-flow/memory' },
436
+ { name: 'AI Defence', what: 'prompt-injection / PII shield', test: (n) => n === '@claude-flow/aidefence' },
437
+ { name: 'RuVector', what: 'vector search + RVF storage', test: (n) => n.startsWith('@ruvector/') || n === 'ruvector' || n === 'ruvector-extensions' || n === 'ruvbot' },
438
+ { name: 'Agentic-Flow', what: 'multi-model / cheap routing', test: (n) => n === 'agentic-flow' },
439
+ { name: 'Agentic-QE', what: 'testing & quality fleet', test: (n) => n === 'agentic-qe' },
440
+ { name: 'MetaHarness', what: 'harness scoring & routing', test: (n) => n.startsWith('@metaharness/') },
441
+ { name: 'Agent-Browser', what: 'browser automation', test: (n) => n.startsWith('agent-browser') },
442
+ { name: 'Agentic Robotics', what: 'robot / agent control', test: (n) => n.startsWith('@agentic-robotics/') || n === 'agentic-robotics' },
443
+ ];
444
+ const STACK_MORE = { name: 'More RuvNet tools', what: 'flow-nexus, qudag, ruv-swarm, ruvi…' };
445
+
446
+ function familyOf(name) {
447
+ const f = STACK_FAMILIES.find((fam) => { try { return fam.test(String(name)); } catch { return false; } });
448
+ return f ? f.name : STACK_MORE.name;
449
+ }
450
+
451
+ function groupFamilies(pkgs) {
452
+ const map = new Map();
453
+ for (const p of pkgs) {
454
+ const fam = familyOf(p.name || '');
455
+ if (!map.has(fam)) map.set(fam, []);
456
+ map.get(fam).push(p);
457
+ }
458
+ const order = [...STACK_FAMILIES.map((f) => f.name), STACK_MORE.name];
459
+ return order.filter((n) => map.has(n)).map((n) => {
460
+ const meta = STACK_FAMILIES.find((f) => f.name === n) || STACK_MORE;
461
+ const items = map.get(n).slice().sort((a, b) =>
462
+ (STATE_ORDER[a.state] ?? 9) - (STATE_ORDER[b.state] ?? 9) || String(a.name).localeCompare(String(b.name)));
463
+ const attention = items.filter((p) => ['BROKEN', 'BEHIND', 'UNRESOLVED'].includes(p.state)).length;
464
+ return { name: n, what: meta.what, items, attention };
465
+ });
466
+ }
467
+
468
+ /* No status without a remedy: every "behind/broken" indicator carries a jump to its
469
+ one-click fix card (the consent-gated recommendation that already exists below). */
470
+ let stackTicker = null;
471
+ function jumpToRec(recId) {
472
+ const card = document.getElementById('card-recs');
473
+ if (card) card.open = true;
474
+ const rec = document.getElementById(`rec-${recId}`);
475
+ if (!rec) return;
476
+ rec.scrollIntoView({ behavior: 'smooth', block: 'center' });
477
+ rec.classList.add('rec-flash');
478
+ setTimeout(() => rec.classList.remove('rec-flash'), 2600);
479
+ // Land ready to act: the Apply button gets focus so the fix is one keystroke away —
480
+ // the jump must never feel like it WAS the fix.
481
+ setTimeout(() => rec.querySelector('.btn-apply')?.focus(), 650);
482
+ }
483
+
484
+ /* Re-mirror the machine — the header ↻ button, and auto-run after every apply/undo so the page shows
485
+ the AFTER state instead of a stale before.
486
+ *
487
+ * IT USED TO BE A PLACEBO (retired 2026-07-26, RVBC-INSTANT-SPEC #8). The old body re-fetched three
488
+ * endpoints and announced "Re-check complete." Every one of those endpoints is cache-first: it
489
+ * re-read the SAME cache it had just read, painted the same bytes, and declared the machine
490
+ * re-checked. Nothing was measured. Worse, it said so right after an apply — the one moment the user
491
+ * most needs to know whether the change actually took.
492
+ *
493
+ * There is now ONE refresh: expire the caches server-side, force a real measurement in the detached
494
+ * child, and report "done" only when a strictly newer measurement has actually landed. Both entry
495
+ * points (this button and the freshness pill) are the same function, because two controls that
496
+ * disagree about what "refreshed" means is how the placebo got written in the first place. */
497
+ const recheckMachine = doManualRefresh;
498
+
499
+ /* Jump-and-flash for a Settings row (provider chips land here) — same pattern as jumpToRec. */
500
+ function jumpToSetting(key) {
501
+ const card = document.getElementById('card-settings');
502
+ if (card) card.open = true;
503
+ const row = document.getElementById(`field-${key}`);
504
+ if (!row) return;
505
+ row.scrollIntoView({ behavior: 'smooth', block: 'center' });
506
+ row.classList.add('rec-flash');
507
+ setTimeout(() => row.classList.remove('rec-flash'), 2600);
508
+ }
509
+
510
+ /* WP5 — the page-level "stand by, this is private" line fades once the first card hydrates. */
511
+ function dismissStandby() {
512
+ const n = document.getElementById('standby-note');
513
+ if (!n || n.hidden) return;
514
+ n.classList.add('gone');
515
+ setTimeout(() => { n.hidden = true; }, 650);
516
+ }
517
+
518
+ /* ---------------------------------------- click-to-learn: ONE shared popover
519
+ Used by the Dev/Prod headers (WP3), every Settings row (WP4), and the wiring
520
+ lead (WP6). Anchored near its trigger, closes on Escape / click-outside /
521
+ scroll, never shifts the layout (position: fixed). */
522
+
523
+ let infoPopEl = null;
524
+ let infoPopOwner = null;
525
+
526
+ function closeInfoPop() {
527
+ if (!infoPopEl) return;
528
+ const owner = infoPopOwner;
529
+ infoPopEl.remove();
530
+ infoPopEl = null;
531
+ infoPopOwner = null;
532
+ document.removeEventListener('pointerdown', onInfoDocDown, true);
533
+ document.removeEventListener('keydown', onInfoKey, true);
534
+ window.removeEventListener('scroll', closeInfoPop, true);
535
+ window.removeEventListener('resize', closeInfoPop);
536
+ if (owner && document.contains(owner)) owner.focus({ preventScroll: true });
537
+ }
538
+
539
+ function onInfoDocDown(e) {
540
+ if (!infoPopEl) return;
541
+ if (infoPopEl.contains(e.target)) return;
542
+ if (infoPopOwner && (e.target === infoPopOwner || infoPopOwner.contains(e.target))) return;
543
+ closeInfoPop();
544
+ }
545
+
546
+ function onInfoKey(e) { if (e.key === 'Escape') closeInfoPop(); }
547
+
548
+ function openInfoPop(trigger, title, beats) {
549
+ if (infoPopEl && infoPopOwner === trigger) { closeInfoPop(); return; } // second click toggles off
550
+ closeInfoPop();
551
+ const pop = el('div', { class: 'info-pop', role: 'dialog', 'aria-label': title, tabindex: '-1' },
552
+ el('button', { class: 'ip-close', type: 'button', 'aria-label': 'Close', onclick: closeInfoPop }, '×'),
553
+ el('p', { class: 'ip-title' }, title),
554
+ (Array.isArray(beats) ? beats : [beats]).map((b) => (typeof b === 'string'
555
+ ? el('p', { class: 'ip-beat' }, b)
556
+ : el('p', { class: 'ip-beat' }, el('span', { class: 'ip-k' }, b.k), b.t))));
557
+ document.body.append(pop);
558
+ const r = trigger.getBoundingClientRect();
559
+ const pw = pop.offsetWidth;
560
+ const ph = pop.offsetHeight;
561
+ const left = Math.min(Math.max(12, r.left), Math.max(12, window.innerWidth - pw - 12));
562
+ let top = r.bottom + 8;
563
+ if (top + ph > window.innerHeight - 12) top = Math.max(12, r.top - ph - 8);
564
+ pop.style.left = `${Math.round(left)}px`;
565
+ pop.style.top = `${Math.round(top)}px`;
566
+ infoPopEl = pop;
567
+ infoPopOwner = trigger;
568
+ document.addEventListener('pointerdown', onInfoDocDown, true);
569
+ document.addEventListener('keydown', onInfoKey, true);
570
+ window.addEventListener('scroll', closeInfoPop, true);
571
+ window.addEventListener('resize', closeInfoPop);
572
+ pop.focus({ preventScroll: true });
573
+ }
574
+
575
+ function infoBtn(title, beats) {
576
+ return el('button', {
577
+ class: 'info-btn', type: 'button',
578
+ 'aria-label': `About “${title}” — what it is and why it matters`,
579
+ title: 'What is this — and why it matters',
580
+ onclick: (e) => { e.preventDefault(); e.stopPropagation(); openInfoPop(e.currentTarget, title, beats); },
581
+ }, 'i');
582
+ }
583
+
584
+ /* ── SECTION-LEVEL EXPLAINERS (2026-07-24) ───────────────────────────────────────────────────────
585
+ * The owner could not place several cards — "brain activity vs memory", "how it's wired", "trust &
586
+ * provenance", "what caught Claude". Every card now carries a one-click "i" that says, in the page's
587
+ * own quiet voice, what it shows and why it matters. One author wrote all of these so they read as
588
+ * one voice; each is attached via infoBtn() to a title node the render function already rebuilds. */
589
+ const STACK_INFO = ['Every rUv package installed globally on this computer, checked against the npm '
590
+ + 'registry and grouped by family (ruflo, AgentDB, RuVector, and so on). This is what’s on disk and '
591
+ + 'at what version — not whether it’s actually running; that’s the wiring card, further down.'];
592
+ const CAPABILITIES_INFO = ['Every capability this stack can offer — routing, learning, guardrails, and '
593
+ + 'the rest — with the state we actually observed on this machine: on, off, set up but idle, not '
594
+ + 'installed, or not checked. This card mostly reads. A few rows carry a tickable box, and even there '
595
+ + 'ticking only opens the consent-gated proposal in “What we’d suggest” below — nothing runs until '
596
+ + 'you confirm it there, with its evidence, its cost, and its undo.'];
597
+ const LEARNINGS_INFO = ['A separate, smaller stream from the memory below it: not what your projects '
598
+ + 'contain, but patterns in how you work — noticed across every project and reused everywhere. Your '
599
+ + 'project facts stay isolated; this strip is the one thing that deliberately crosses that boundary.'];
600
+ const MEMORY_INFO = [
601
+ { k: 'What is this?', t: 'A quality score for the memory system itself, not a feed of events. '
602
+ + '“What is happening in this project” shows individual things your AI did; this card asks whether '
603
+ + 'the memory behind every project is actually reliable — does it survive a compaction, does '
604
+ + 'recall really work, is it current.' },
605
+ { k: 'Why does it matter?', t: 'A memory store can be running and full of entries and still be '
606
+ + 'useless — the score is graded on real checks, never on whether the lights are on.' },
607
+ { k: 'How does it help me?', t: 'A dimension we couldn’t verify this session is shown grey and '
608
+ + 'excluded from the score, never guessed at — so a perfect number is never quietly covering for '
609
+ + 'something nobody tested.' },
610
+ ];
611
+ const ROUTER_ENGINE_INFO = ['The routing engine itself, opened up: which package is actually deciding '
612
+ + '(rUv’s @metaharness/router, not a lookalike), whether it’s still learning your patterns or routing '
613
+ + 'on real history, and its most recent real decisions straight from its own log — never a simulation.'];
614
+ const DISTRIBUTION_INFO = ['How your routed tasks split across mechanical/cheap/mid/frontier bands, '
615
+ + 'with the money saved for each stripe of the bar — from real receipts, matched one-for-one against '
616
+ + 'what running everything on the frontier model would have cost.'];
617
+ const PROVIDERS_INFO = ['Two separate choices, not one: “Your plan” is which subscription the router '
618
+ + 'treats as free (the biggest cost lever you have); “OpenRouter” is an optional second lane for '
619
+ + 'offloading plain-text tasks to even cheaper models. Turning one on never changes the other.'];
620
+ const SAVINGS_INFO = ['MetaHarness is explained just above; this card is its report card — real receipts '
621
+ + 'from tasks it has actually routed, never a projected or modelled number. Nothing here counts until '
622
+ + 'it’s actually happened.'];
623
+ const TRUST_CARD_INFO = [
624
+ { k: 'What is this?', t: '“Provenance” means: can you check that what’s running on your machine is '
625
+ + 'really what rUv published, instead of taking it on faith? Each row below is one such check — the '
626
+ + 'release fingerprint, the parts list, how you get updates, and how cautious the console is allowed to be.' },
627
+ { k: 'Why does it matter?', t: 'Software you can’t verify is software you can only trust — verified '
628
+ + 'beats trusted, and this card is where that gets proven, row by row.' },
629
+ { k: 'How does it help me?', t: 'Every row says plainly whether it’s live today or still coming — '
630
+ + 'nothing here is dressed up as measured when it wasn’t.' },
631
+ ];
632
+ const GATES_INFO = [
633
+ { k: 'What is this?', t: 'Gates are checks that run before your AI touches your machine — most just '
634
+ + 'add context, but some can refuse the action outright. This card is the ledger: what got refused, '
635
+ + 'by which gate, and why.' },
636
+ { k: 'Why does it matter?', t: '“Nothing caught yet” and “nothing is being checked” look identical '
637
+ + 'unless the count of armed gates is shown alongside it — this card gives you both, so silence '
638
+ + 'reads as silence, never as an all-clear.' },
639
+ { k: 'How does it help me?', t: 'Every catch names the gate, what it stopped, and when — recorded '
640
+ + 'since it was first armed, never guessed at for anything before that.' },
641
+ ];
642
+ const LESSONS_INFO = ['Rules your AI now follows on this machine — not memory (facts it recalls) and '
643
+ + 'not activity (things it did): specific behavioral corrections, most of them in your own words, that '
644
+ + 'it applies from now on. Every one has a switch, and turning one off never deletes the record of '
645
+ + 'where you taught it.'];
646
+
647
+ /* What/Why/How copy — three beats, every Settings row (WP4). */
648
+ const SETTING_INFO = {
649
+ qeFleet: [
650
+ { k: 'What is this?', t: 'A squad of test agents that spins up only when you ask — it can write tests for your code, measure what your tests miss, scan for security holes, and check accessibility.' },
651
+ { k: 'Why does it matter?', t: 'Untested code breaks in front of users.' },
652
+ { k: 'How does it help me?', t: 'Say “QE this” and the fleet does a quality pass no human has patience for.' },
653
+ ],
654
+ routing: [
655
+ { k: 'What is this?', t: 'Sends small mechanical tasks to small cheap models and saves the big model for hard work.' },
656
+ { k: 'Why does it matter?', t: 'Most AI work doesn’t need the expensive model.' },
657
+ { k: 'How does it help me?', t: 'Same quality where it counts, at a fraction of the spend — every routing decision is receipted.' },
658
+ ],
659
+ nightly: [
660
+ { k: 'What is this?', t: 'Rebuilds the knowledge base overnight so answers track the newest source.' },
661
+ { k: 'Why does it matter?', t: 'This ecosystem ships fast — stale knowledge means wrong answers.' },
662
+ { k: 'How does it help me?', t: 'You wake up current without doing anything.' },
663
+ ],
664
+ provider: [
665
+ { k: 'What is this?', t: 'Which AI stack is yours — sets your frontier model and what “savings” are measured against.' },
666
+ { k: 'Why does it matter?', t: 'The router should ride licenses you already pay for.' },
667
+ { k: 'How does it help me?', t: 'Click your house and routing adapts to your subscriptions automatically.' },
668
+ ],
669
+ openrouterKey: [
670
+ { k: 'What is this?', t: 'One key that unlocks many cheap models from many providers.' },
671
+ { k: 'Why does it matter?', t: 'The biggest savings come from models outside your main subscription.' },
672
+ { k: 'How does it help me?', t: 'Paste it once, the cheap lane lights up — stored only in your user folder.' },
673
+ ],
674
+ };
675
+
676
+ /* Dev-vs-Prod economics, in the owner's words (WP3). */
677
+ const PROFILE_INFO = {
678
+ Development: ['Development is you, building your app. You already pay for a subscription (Claude Max, Codex) — dev work rides it at no extra cost, so this table optimizes for speed on your license.'],
679
+ Production: ['Production is your app, serving other people. Your users can’t ride your personal subscription — production runs on metered API calls you pay per token, so this table optimizes for cost-per-quality on every call. Different economics — that’s why there are two tables.'],
680
+ };
681
+
682
+ /* The wiring card's click-to-learn (WP6). */
683
+ const WIRING_INFO = [
684
+ { k: 'What is this?', t: 'A live map of how each project launches the RuvNet tools — a fresh npx download on every call, or your one global install.' },
685
+ { k: 'Why does it matter?', t: 'npx keeps hidden private copies that can go stale — old code quietly answers while every command still “works”.' },
686
+ { k: 'How does it help me?', t: 'You see exactly where each style is in use, and every fix below is one click with the undo recorded first.' },
687
+ ];
688
+
689
+ function familyRow(fam) {
690
+ const tone = fam.attention ? 'warn' : 'green';
691
+ const statusText = fam.attention ? `${fam.attention} need${fam.attention === 1 ? 's' : ''} a look` : 'current';
692
+ const count = fam.items.length;
693
+ // Version on the row (Stuart 2026-07-17: "show the version numbers"). Healthy family → the
694
+ // flagship's version. Attention family → the problem AND its resolution on the same line:
695
+ // "installed → target" right beside the fix button.
696
+ // Exception (issue #23): "More RuvNet tools" is an explicit heterogeneous catch-all — its members
697
+ // are unrelated packages on independent version lines, so items[0]'s version does NOT represent the
698
+ // group yet reads as if it did. Suppress the flagship-version shorthand there (each package's real
699
+ // version is still shown in the expanded table below). A specific "installed → target" for ONE
700
+ // flagged package stays — it sits beside THAT package's fix button and is about it, not the group.
701
+ const isCatchAll = fam.name === STACK_MORE.name;
702
+ const first = fam.attention ? fam.items.find((i) => i.state === 'BEHIND' || i.state === 'BROKEN') : null;
703
+ const verText = first
704
+ ? `${first.installed ?? '?'} → ${first.target ?? 'latest'}`
705
+ : (!isCatchAll && fam.items[0]?.installed ? `v${fam.items[0].installed}` : '');
706
+ return el('details', { class: 'fam' },
707
+ el('summary', { class: 'fam-sum' },
708
+ el('span', { class: 'fam-name' }, fam.name),
709
+ el('span', { class: 'fam-what' }, fam.what),
710
+ el('span', { class: 'fam-status' },
711
+ verText ? el('span', { class: 'fam-ver' + (first ? ' is-behind' : '') }, verText) : null,
712
+ chip(statusText, tone),
713
+ fam.attention ? el('button', {
714
+ class: 'btn-fix', type: 'button', title: 'Jump to the one-click fix below (evidence, cost, and undo included)',
715
+ onclick: (e) => {
716
+ e.preventDefault(); e.stopPropagation();
717
+ if (first) jumpToRec(`${first.state === 'BROKEN' ? 'repair' : 'sync'}:${first.name}`);
718
+ },
719
+ }, 'fix ↓') : null,
720
+ el('span', { class: 'fam-count' }, count > 1 ? `${count} parts` : '1 pkg')),
721
+ el('span', { class: 'fam-chev', 'aria-hidden': 'true' }, '›')),
722
+ el('div', { class: 'fam-body' },
723
+ el('div', { class: 'scroll-x' },
724
+ el('table', { class: 'tb' },
725
+ el('thead', {}, el('tr', {},
726
+ el('th', { scope: 'col' }, 'Package'), el('th', { scope: 'col' }, 'Installed'),
727
+ el('th', { scope: 'col' }, 'Target'), el('th', { scope: 'col' }, 'Tag'),
728
+ el('th', { scope: 'col' }, 'State'))),
729
+ el('tbody', {}, fam.items.map(pkgRow))))));
730
+ }
731
+
732
+ function renderStack(data) {
733
+ if (stackTicker) { clearInterval(stackTicker); stackTicker = null; }
734
+ const body = $('#body-stack');
735
+ const sum = data.summary || {};
736
+ const pkgs = Array.isArray(data.packages) ? [...data.packages] : [];
737
+ const shadows = Array.isArray(data.shadows) ? data.shadows : [];
738
+
739
+ const total = sum.total ?? pkgs.length;
740
+ const current = sum.current ?? pkgs.filter((p) => p.state === 'CURRENT').length;
741
+ const behind = sum.behind ?? pkgs.filter((p) => p.state === 'BEHIND').length;
742
+ const ahead = sum.ahead ?? pkgs.filter((p) => p.state === 'AHEAD').length;
743
+ const broken = sum.broken ?? pkgs.filter((p) => p.state === 'BROKEN').length;
744
+ const stale = sum.stale ?? shadows.filter((s) => s.stale).length;
745
+
746
+ const chips = [chip(`${fmtInt(current)} current`, 'green')];
747
+ if (behind) chips.push(chip(`${fmtInt(behind)} behind`, 'warn'));
748
+ if (ahead) chips.push(chip(`${fmtInt(ahead)} ahead`, 'cyan', STATE_TITLE.AHEAD));
749
+ if (broken) chips.push(chip(`${fmtInt(broken)} broken`, 'red'));
750
+ if (stale) chips.push(chip(`${fmtInt(stale)} stale shadows`, 'warn'));
751
+ setChips('chips-stack', chips);
752
+
753
+ // The stack card leads the page but only EXPANDS when it has something to say (Stuart,
754
+ // 2026-07-16): an action to take (behind/broken/stale), or the user's first visit ever.
755
+ // A clean stack on a repeat visit stays collapsed — the green chip is the whole story.
756
+ const stackCard = $('#card-stack');
757
+ if (stackCard && !stackCard.dataset.userToggled) {
758
+ const firstVisit = !localStorage.getItem('rvbc-seen');
759
+ if (behind || broken || stale || firstVisit) stackCard.open = true;
760
+ }
761
+ try { localStorage.setItem('rvbc-seen', '1'); } catch { /* private mode */ }
762
+
763
+ found.pkgTotal = total;
764
+ found.pkgCurrent = current;
765
+ updateFoundStrip();
766
+
767
+ pkgs.sort((a, b) =>
768
+ (STATE_ORDER[a.state] ?? 9) - (STATE_ORDER[b.state] ?? 9) || String(a.name).localeCompare(String(b.name)));
769
+ const attention = pkgs.filter((p) => ['BROKEN', 'BEHIND', 'UNRESOLVED'].includes(p.state));
770
+
771
+ const main = [];
772
+ main.push(el('p', { class: 'lead-stat' },
773
+ 'We read ', el('b', {}, fmtInt(total)), ' packages on your global stack — ',
774
+ el('b', {}, fmtInt(current)), ' current',
775
+ ahead ? el('span', {}, ', ', el('b', {}, fmtInt(ahead)), ' ahead of the registry (which is legal)') : '',
776
+ broken ? el('span', {}, ', ', el('b', {}, fmtInt(broken)), ' broken') : '',
777
+ behind ? el('span', {}, ', ', el('b', {}, fmtInt(behind)), ' behind') : '',
778
+ '.', infoBtn('Your stack', STACK_INFO)));
779
+
780
+ if (pkgs.length) {
781
+ main.push(el('p', { class: 'impact-note' },
782
+ attention.length
783
+ ? el('span', {},
784
+ `${fmtInt(attention.length)} package${attention.length === 1 ? '' : 's'} need${attention.length === 1 ? 's' : ''} a look — every one has a one-click fix with evidence, cost, and undo. `,
785
+ el('button', {
786
+ class: 'btn-fix', type: 'button',
787
+ onclick: () => jumpToRec(attention[0].state === 'BROKEN' ? `repair:${attention[0].name}` : `sync:${attention[0].name}`),
788
+ }, 'take me to the fix ↓'))
789
+ : 'Nothing needs attention — every package matches its target, one copy each.'));
790
+ // Attention families bubble to the top (Stuart 2026-07-17); the healthy remainder keeps the
791
+ // curated blast-radius order (never alphabetical). Stable sort preserves it within each group.
792
+ main.push(el('div', { class: 'fam-list' },
793
+ groupFamilies(pkgs).sort((a, b) => (b.attention || 0) - (a.attention || 0)).map(familyRow)));
794
+ } else {
795
+ main.push(el('p', { class: 'muted' }, 'No stack packages detected on this machine yet.'));
796
+ }
797
+
798
+ if (shadows.length) {
799
+ // Problems only (Stuart 2026-07-17: "just tell me the ones I need to deal with — never an
800
+ // issue without its resolution on the same line"). Stale rows carry their one-click fix
801
+ // (the purge:shadows recommendation — evidence, cost, undo); the in-sync majority collapses
802
+ // to a single verified line with a peel-back for whoever wants the full inventory.
803
+ const staleRows = shadows.filter((s) => s.stale);
804
+ const syncCount = shadows.length - staleRows.length;
805
+ main.push(el('aside', { class: 'shadows' },
806
+ el('p', { class: 'shadows-title' }, 'Shadow copies in the npx cache',
807
+ staleRows.length ? chip(`${staleRows.length} stale`, 'warn') : chip('all in sync', 'green')),
808
+ staleRows.length ? el('p', { class: 'shadows-sub' },
809
+ 'npx keeps private copies in ', el('code', {}, '~/.npm/_npx'),
810
+ '. 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,
811
+ ...staleRows.map((s) => el('div', { class: 'shadow-row' },
812
+ el('span', { class: 'shadow-name' }, s.name || '—'),
813
+ el('span', { class: 'shadow-vers' }, `cache ${s.version ?? '?'} · global `, el('b', {}, s.global ?? '?')),
814
+ chip('stale', 'warn'),
815
+ el('button', {
816
+ class: 'btn-fix', type: 'button',
817
+ title: 'Jump to the one-click removal below — evidence, cost, and undo included',
818
+ onclick: () => jumpToRec('purge:shadows'),
819
+ }, 'remove it ↓'),
820
+ el('span', { class: 'shadow-dir' }, s.dir || ''),
821
+ )),
822
+ el('p', { class: 'shadows-ok' },
823
+ staleRows.length
824
+ ? `The other ${fmtInt(syncCount)} cached ${syncCount === 1 ? 'copy matches' : 'copies match'} your installs — re-checked on every audit.`
825
+ : shadows.length === 1
826
+ ? 'The 1 cached copy matches your install — re-checked on every audit; nothing is hiding stale.'
827
+ : `All ${fmtInt(shadows.length)} cached copies match your installs — re-checked on every audit; nothing is hiding stale.`),
828
+ el('details', { class: 'sub' },
829
+ el('summary', {}, `Peel it back — ${shadows.length === 1 ? 'the 1 cached copy' : `all ${fmtInt(shadows.length)} cached copies`}`),
830
+ el('div', { class: 'sub-body' },
831
+ shadows.map((s) => el('div', { class: 'shadow-row' },
832
+ el('span', { class: 'shadow-name' }, s.name || '—'),
833
+ el('span', { class: 'shadow-vers' }, `cache ${s.version ?? '?'} · global `, el('b', {}, s.global ?? '?')),
834
+ s.stale ? chip('stale', 'warn') : chip('in sync', 'green'),
835
+ el('span', { class: 'shadow-dir' }, s.dir || ''),
836
+ ))))));
837
+ }
838
+
839
+ body.replaceChildren(withIllo('stack', ...main));
840
+ }
841
+
842
+ /* ------------------------------------------- section 1b: what's on, what's off
843
+ THE MURK, ADDRESSED. Owner, 2026-07-22: "a ton of people don't know what is or isn't turned on
844
+ because it's very much a black box."
845
+
846
+ The console already knew plenty — capability-audit.mjs has had three working detectors since
847
+ 2026-07-22 and they report real, observed state — but every one of them surfaced only as a
848
+ RECOMMENDATION, i.e. only when something was wrong. There was no surface anywhere that answered
849
+ the plain question "what is on right now", so a machine with nothing wrong looked identical to a
850
+ machine nobody had checked. That is the black box: not missing data, missing a place to read it.
851
+
852
+ FOUR STATES, AND THE ONES THAT ARE NOT "OFF" ARE THE WHOLE POINT.
853
+ ON (green) · OFF (amber) · UNKNOWN (grey, dashed, "not checked") · ABSENT (grey, "not installed").
854
+ Rendering an unchecked capability as OFF would be the console telling the user a fact it does not
855
+ have — the precise lie this project bans (a detector that didn't answer is not a detector that
856
+ answered "no"). ABSENT is the second half of that: "you do not have this" and "you have it and it
857
+ is switched off" are different sentences with different actions behind them, and collapsing them
858
+ sends people to turn on software they never installed. So both are carried end-to-end and drawn in
859
+ a different visual CHANNEL from OFF, not just a different hue: dashed rail + dashed chip for
860
+ unknown, solid-but-muted for absent, so they survive greyscale, colour-blindness, and a squint from
861
+ six feet. (This header said THREE for one commit after ABSENT shipped — a comment claiming the code
862
+ below it was simpler than it is.)
863
+
864
+ DATA-DRIVEN, NO EXCEPTIONS. Not one capability name appears in this file or in index.html. Rows
865
+ come from /api/capabilities and only from there; a capability added server-side shows up here with
866
+ zero client change. The alternative — a hardcoded list — rots the week rUv ships again, which is
867
+ the same rot capability-audit.mjs explicitly refuses in its own header. */
868
+
869
+ const CAP_STATE = {
870
+ ON: {
871
+ tone: 'green', klass: 'is-on', label: 'on',
872
+ hint: 'Observed on this machine — the evidence beside it is what we saw.',
873
+ },
874
+ OFF: {
875
+ tone: 'amber', klass: 'is-off', label: 'off',
876
+ hint: 'We looked and it is not running. This is a measured "off", not an assumption.',
877
+ },
878
+ /* IDLE — the state this whole product exists to surface, and the card could not say it until now.
879
+ Owner, 2026-07-24: "people think something is 'On' only to find out it is not really running the
880
+ way they thought — that is exactly what this tool is for."
881
+
882
+ Its label is deliberately NOT "off". Off sends you to turn a thing on; this thing IS on, and
883
+ something that should be calling it stopped. Sending someone to re-enable an already-enabled
884
+ capability is how a true finding becomes a wasted afternoon. The distinct wording — "set up, not
885
+ running" — is the whole value of separating them. */
886
+ IDLE: {
887
+ tone: 'amber', klass: 'is-off', label: 'set up, not running',
888
+ hint: 'Configured and proven — it has worked here before — but nothing has invoked it recently. '
889
+ + 'This is usually a wiring gap, not a switch: something that should call it is missing or was never installed.',
890
+ },
891
+ UNKNOWN: {
892
+ tone: 'nt', klass: 'is-unknown', label: 'not checked',
893
+ hint: 'Nothing established this one either way, so nothing is claimed. Not checked is not off.',
894
+ },
895
+ /* ABSENT is a MEASURED ANSWER, and leaving it out was this card's own version of the lie it exists
896
+ to kill — inverted. The registry has emitted four states since it was written; this console knew
897
+ three, so every "we looked and it is not installed here" landed in the unknown bucket wearing the
898
+ hint "the server reported a state this console doesn't recognise". Hard facts, filed under
899
+ "we couldn't tell, and won't guess."
900
+
901
+ It bites hardest on precisely the machine the bar names: a brand-new install, where most rows are
902
+ legitimately `absent`. The newcomer whose console must be most trustworthy got the most rows
903
+ mislabelled as unanswered. "Not installed" is not ignorance — it is the answer, and it is the one
904
+ that tells them what to do next. */
905
+ ABSENT: {
906
+ tone: 'grey', klass: 'is-absent', label: 'not installed',
907
+ hint: 'We looked and this piece is not on this machine — a measured answer, not a guess. Installing it is what changes this row.',
908
+ },
909
+ };
910
+
911
+ /* ON / OFF / UNKNOWN / ABSENT, or null for a token this console has never heard of. A future
912
+ capability reporting some richer state must NOT be quietly folded into OFF — it lands in the
913
+ unknown bucket and shows its own raw word, which is honest about both the state and our ignorance
914
+ of it. Adding ABSENT here does not weaken that rule; it retires one specific known state from the
915
+ "we've never heard of it" pile, where it never belonged. */
916
+ function capState(raw) {
917
+ const t = String(raw ?? '').trim().toUpperCase();
918
+ // 'IDLE' added 2026-07-24. This whitelist is exactly where the ABSENT bug lived (see CAP_STATE):
919
+ // the registry gains a state, this line does not, and every row carrying it lands in UNKNOWN
920
+ // wearing a label that says we never looked — when in fact we looked and found something specific.
921
+ // Any future state must be added HERE, in the tally, in the chips, and in RANK — all four.
922
+ return (t === 'ON' || t === 'OFF' || t === 'IDLE' || t === 'UNKNOWN' || t === 'ABSENT') ? t : null;
923
+ }
924
+ const capBucket = (row) => capState(row && row.state) || 'UNKNOWN';
925
+
926
+ /* THE ONE GATE THAT DECIDES WHETHER A ROW EARNS A CHECKBOX AT ALL.
927
+ *
928
+ * A checkbox is a STRONGER promise than the plain-text turn-on line above it: a button says "you
929
+ * could run this"; a checkbox says "this is a switch, and I am telling you its current position." So
930
+ * the bar is at least as strict as the bar for text, and in one respect stricter — the server only
931
+ * ever stamps `row.recId` once it built a full, schema-gated Recommendation (evidence, cost, change,
932
+ * undo all present — see console-engine.mjs's makeRecommendation()) AND capability-registry.mjs's own
933
+ * proven-undo map accepted the row (buildCapabilityRecommendations() there — currently exactly
934
+ * memory-distillation, and only while OFF). See that file's header for the full "why".
935
+ *
936
+ * This is intentionally REDUNDANT with what the server already guarantees. A client-side check that
937
+ * merely trusted `row.recId` being truthy would work today and silently stop meaning anything the
938
+ * moment a future edit stamped it for the wrong reason — exactly the shape of "a field-name typo made
939
+ * one repo permanently unverified" (fix(3.9.69)). Re-derive every gate here; never just trust the flag. */
940
+ function capCheckboxEligible(row, known) {
941
+ if (known !== 'OFF') return false; // never ON / IDLE / UNKNOWN / ABSENT
942
+ if (String(row.scope || '').toLowerCase() === 'machine') return false; // machine-wide: full rec-card path only, never an inline tick
943
+ const cmd = row.turnOn && typeof row.turnOn === 'object' && typeof row.turnOn.cmd === 'string' ? row.turnOn.cmd : '';
944
+ if (!cmd || /<[^>]+>/.test(cmd)) return false; // no verified command, or one with a blank to fill in
945
+ if (typeof row.recId !== 'string' || !row.recId) return false; // the server never vouched for this one
946
+ return true;
947
+ }
948
+
949
+ /* Evidence arrives in whichever shape the detector that produced it already uses: a plain string,
950
+ a list of strings, or capability-audit.mjs's own [{ observed }] records. Accept all three rather
951
+ than force one, and silently drop nothing-shaped entries — an empty bullet is noise, not evidence. */
952
+ function capEvidence(ev) {
953
+ const src = Array.isArray(ev) ? ev : (ev == null ? [] : [ev]);
954
+ const out = [];
955
+ for (const e of src) {
956
+ const t = (typeof e === 'string') ? e
957
+ : (e && typeof e === 'object') ? (e.observed ?? e.text ?? e.detail ?? e.note) : null;
958
+ if (typeof t === 'string' && t.trim()) out.push(t.trim());
959
+ }
960
+ return out;
961
+ }
962
+
963
+ /* THE CAPABILITIES CARD'S ONLY INTERACTIVE CONTROL, and it does not itself apply anything — it points
964
+ * at the one place that does. The console already has exactly one tested state machine for "show
965
+ * evidence/cost/undo, get consent, apply, confirm, offer undo": the rec cards in "What we'd suggest"
966
+ * (buildRecCard/doApply/showConfirm/applied/doUndo above), fed by makeRecommendation()'s schema gate,
967
+ * which cannot construct a Recommendation missing evidence, cost, change, or undo. A checkbox that
968
+ * POSTed to /api/apply on its own, carrying its own payload, would be a SECOND, unaudited way to
969
+ * trigger a machine mutation — precisely the "two answers to one question" failure this project has
970
+ * already shipped more than once (capability-registry.mjs's learningEnable story, the hooks-list
971
+ * story). So this rides jumpToRec(recId) unchanged, the exact pattern the stack card's "attention" rows
972
+ * already use.
973
+ *
974
+ * The box must never visually move on its own click. It only ever shows checked once recheckMachine()
975
+ * has re-run the real detector and the row re-renders with state 'on' — at which point capRow() stops
976
+ * calling capCheckboxEligible entirely (known !== 'OFF') and this function is never called again for
977
+ * that row. There is no code path that sets `.checked = true` directly; clicking this box always
978
+ * preventDefault()s and only ever opens the real card. */
979
+ function capCheckbox(row) {
980
+ const id = `capbox-${row.key}`;
981
+ const humanAction = (row.turnOn && row.turnOn.human) || `Turn on ${row.label || row.key}`;
982
+
983
+ // GUARD FOR "the rec hasn't loaded yet". /api/state, /api/stack, and /api/memory settle at
984
+ // different times (see stateRecsSettled/stackRecsSettled/healthRecsSettled), and jumpToRec()
985
+ // silently no-ops if `#rec-<id>` isn't in the DOM yet. Rather than let a click do nothing with no
986
+ // explanation, an eligible row whose card has not rendered yet gets an honest, disabled placeholder
987
+ // instead of a clickable box that might fail silently.
988
+ const recNode = document.getElementById(`rec-${row.recId}`);
989
+ if (!recNode) {
990
+ return el('div', { class: 'cap-toggle-wrap cap-toggle-pending' },
991
+ el('span', { class: 'cap-toggle-text muted' }, `${humanAction} — still loading the full proposal…`));
992
+ }
993
+
994
+ const cb = el('input', {
995
+ type: 'checkbox', id, class: 'cap-toggle', checked: false, disabled: false,
996
+ 'aria-describedby': `${id}-note`,
997
+ onclick: (ev) => {
998
+ // preventDefault() keeps it unchecked; jumpToRec is what actually moves the world.
999
+ ev.preventDefault();
1000
+ jumpToRec(row.recId);
1001
+ announce(`Opening the one-click control for ${row.label || row.key} — nothing has changed yet.`);
1002
+ },
1003
+ });
1004
+ return el('div', { class: 'cap-toggle-wrap' },
1005
+ el('label', { class: 'cap-toggle-label', for: id }, cb,
1006
+ el('span', { class: 'cap-toggle-text' }, humanAction)),
1007
+ el('p', { class: 'cap-toggle-note', id: `${id}-note` },
1008
+ 'Ticking this opens the full proposal below — evidence, cost, and the undo — and applies ',
1009
+ 'nothing until you confirm it there.'));
1010
+ }
1011
+
1012
+ /* IDLE — never a checkbox (see capCheckboxEligible: known !== 'OFF' rules it out outright). Says so,
1013
+ once, so the absence of a control here reads as a decision rather than an oversight — the same
1014
+ "state one MORE fact" principle capLegend() already applies to the five state chips. */
1015
+ function capIdleNote() {
1016
+ return el('p', { class: 'cap-honest-mark is-idle' },
1017
+ el('span', { class: 'mark-glyph', 'aria-hidden': 'true' }, '◐'),
1018
+ 'Not a switch to flip — this ran before and something that should call it stopped. ',
1019
+ 'Fixing it means re-wiring the hook or gate named in the evidence above, not running a command.');
1020
+ }
1021
+
1022
+ /* UNKNOWN (and its ABSENT sibling handled inline in capRow) — never a checkbox; we do not know the
1023
+ current position, so there is nothing honest to show a box toggling from. Terse and low-weight on
1024
+ purpose: this is the least actionable of the five states and should take the least space. */
1025
+ function capUnknownNote() {
1026
+ return el('p', { class: 'cap-honest-mark is-unknown' },
1027
+ el('span', { class: 'mark-glyph', 'aria-hidden': 'true' }, '?'),
1028
+ 'Not checked — nothing to tick here until this can be read.');
1029
+ }
1030
+
1031
+ function capRow(row) {
1032
+ const known = capState(row.state);
1033
+ const st = CAP_STATE[known || 'UNKNOWN'];
1034
+ const raw = String(row.state ?? '').trim();
1035
+ // An unrecognised state shows its own word, not ours — relabelling it "not checked" would hide
1036
+ // that the server did answer, and calling it "off" would invent an answer it never gave.
1037
+ const label = known ? st.label : (raw ? raw.toLowerCase().slice(0, 24) : st.label);
1038
+ const hint = known ? st.hint
1039
+ : `The server reported the state “${raw}”, which this console doesn’t recognise — so it is grouped with the unchecked rather than guessed either way.`;
1040
+
1041
+ const buys = (typeof row.whatItBuysYou === 'string' && row.whatItBuysYou.trim())
1042
+ ? row.whatItBuysYou.trim() : null;
1043
+ const evidence = capEvidence(row.evidence);
1044
+
1045
+ /* THE RECOMMENDATION HALF OF THE QUESTION. The brief is "SHOULD it be on for this user, and IS it
1046
+ on" — the state chip answers IS, and until now nothing answered SHOULD. The registry has shipped
1047
+ a `turnOn: {human, cmd}` on every row it can verify one for, and this renderer dropped it on the
1048
+ floor, so the console displayed exactly half of what the server already knew.
1049
+
1050
+ Rendered as TEXT, never a button. There is no executor and no undo behind these commands here,
1051
+ and this project has already shipped one dead button; a control that looks live and does nothing
1052
+ is worse than a command you can read and decide about yourself.
1053
+
1054
+ A null turnOn is SAID OUT LOUD rather than omitted, because the registry's own header rule is
1055
+ that four capabilities have no verified enable command and silence would read as "nothing can be
1056
+ done about this" — which is a different, quieter falsehood. */
1057
+ const turnOn = (row.turnOn && typeof row.turnOn === 'object'
1058
+ && typeof row.turnOn.cmd === 'string' && row.turnOn.cmd.trim()) ? row.turnOn : null;
1059
+ const wantsAdvice = known === 'OFF' || known === 'ABSENT';
1060
+ // THE ONE ROW-LEVEL DECISION THAT CAN PROMOTE PLAIN TEXT TO A REAL CONTROL. See
1061
+ // capCheckboxEligible()'s own header for the full gate list; checked FIRST and narrowly, so a row
1062
+ // that fails even one gate falls straight through to the exact same honest text this card already
1063
+ // rendered before checkboxes existed — nothing about the non-eligible path changes.
1064
+ const checkboxEligible = capCheckboxEligible(row, known);
1065
+
1066
+ return el('div', { class: `cap-row ${st.klass}` },
1067
+ el('span', { class: 'cap-name' },
1068
+ String(row.label || row.key || 'unnamed capability'),
1069
+ row.scope ? el('span', { class: 'cap-scope', title: `where this applies: ${row.scope}` }, String(row.scope)) : null),
1070
+ el('div', { class: 'cap-val' },
1071
+ buys
1072
+ ? el('p', { class: 'cap-buys' }, buys)
1073
+ : el('p', { class: 'cap-buys cell-dim' },
1074
+ 'No plain-words description came with this one — inventing a benefit for it would be worse than leaving the line empty.'),
1075
+ /* EVIDENCE IS OPEN WHEN IT ASKS SOMETHING OF YOU, FOLDED WHEN IT ONLY REASSURES.
1076
+ *
1077
+ * Graded 88/100 on 2026-07-24 with the largest single deduction (-4) being that every row
1078
+ * carried a maintainer-altitude evidence paragraph inline. On this machine that is TEN rows of
1079
+ * proof-of-health expanded by default — the capabilities card became the longest thing on the
1080
+ * page, and the 12-to-4 restructure could not fix it because the weight was per-row, not
1081
+ * per-section.
1082
+ *
1083
+ * The wrong fix is hiding evidence. Evidence is what separates this page from a dashboard that
1084
+ * asserts, and the rule is that every claim carries what we observed. So nothing is removed —
1085
+ * it is RANKED. A row that wants something from you (off, idle, or unknown) keeps its evidence
1086
+ * open, because that is the row you are being asked to act on and the reasoning has to be right
1087
+ * there. A row that is simply working folds its proof behind one line you can open any time.
1088
+ *
1089
+ * Attention-first, the same law the row ORDER already follows. Rendering confirmation at the
1090
+ * same visual weight as a finding is how a page with ten healthy rows buries its one real one. */
1091
+ evidence.length
1092
+ ? (known === 'ON' || known === 'ABSENT'
1093
+ ? el('details', { class: 'cap-why' },
1094
+ el('summary', null, `Show the evidence (${evidence.length === 1 ? '1 observation' : `${evidence.length} observations`})`),
1095
+ el('ul', { class: 'cap-ev' }, ...evidence.map((e) => el('li', {}, e))))
1096
+ : el('ul', { class: 'cap-ev' }, ...evidence.map((e) => el('li', {}, e))))
1097
+ : el('p', { class: 'cap-ev-none cell-dim' }, 'No evidence was recorded for this row.'),
1098
+ checkboxEligible
1099
+ ? capCheckbox(row)
1100
+ : wantsAdvice
1101
+ ? (turnOn
1102
+ ? el('p', { class: 'cap-turnon' },
1103
+ el('span', { class: 'cap-turnon-lb' }, 'to turn it on'),
1104
+ String(turnOn.human || 'run'), ' — ', el('code', {}, String(turnOn.cmd)))
1105
+ : el('p', { class: 'cap-turnon cap-turnon-none' },
1106
+ el('span', { class: 'cap-turnon-lb' }, 'to turn it on'),
1107
+ 'No verified one-line command exists for this one, so none is offered — a command that ',
1108
+ 'sends you to a terminal to be told “unknown subcommand” would cost you trust in every ',
1109
+ 'other row on this page.'))
1110
+ // Neither "wants advice" nor eligible for a checkbox: IDLE and UNKNOWN still get an honest,
1111
+ // non-interactive mark rather than silence — the same "state one more fact" principle the
1112
+ // legend below already applies to the five state chips. ON says nothing further; its chip
1113
+ // already is the whole answer.
1114
+ : known === 'IDLE'
1115
+ ? capIdleNote()
1116
+ : known === 'UNKNOWN'
1117
+ ? capUnknownNote()
1118
+ : null),
1119
+ el('span', { class: 'cap-status' }, chip(label, st.tone, hint)));
1120
+ }
1121
+
1122
+ /* The colour key, stated once at the top of the card. The page already teaches its rail colours in
1123
+ the same shape (.stage-legend) — this borrows the grammar, not the class, because the two legends
1124
+ answer different questions and must be free to move apart. */
1125
+ function capLegend() {
1126
+ return el('p', { class: 'cap-legend mono' },
1127
+ // FIVE, not four. Caught by an independent grader on 2026-07-24, and it is the sharpest possible
1128
+ // defect for this particular card: the legend said "the four states" while renderCapabilities()
1129
+ // below already emitted a fifth chip ("set up, not running") for STATE.IDLE. A machine with an
1130
+ // idle capability displayed a state its own legend never defined — on the one card whose entire
1131
+ // job is "the black box, opened." The product told a small lie about itself, which is the exact
1132
+ // failure IDLE was added to expose in everything else. Adding the state without its legend row
1133
+ // was my omission, not a pre-existing gap.
1134
+ el('span', { class: 'cl-cap' }, 'the five states'),
1135
+ el('span', { class: 'cl' }, el('span', { class: 'cl-key k-on' }), 'on — observed here'),
1136
+ el('span', { class: 'cl' }, el('span', { class: 'cl-key k-idle' }), 'set up, not running — nothing is calling it'),
1137
+ el('span', { class: 'cl' }, el('span', { class: 'cl-key k-off' }), 'off — present, not running'),
1138
+ el('span', { class: 'cl' }, el('span', { class: 'cl-key k-absent' }), 'not installed — we looked, it isn’t here'),
1139
+ el('span', { class: 'cl' }, el('span', { class: 'cl-key k-unknown' }), 'not checked — we couldn’t tell, and won’t guess'),
1140
+ // NEW — states one MORE fact: a checkbox appearing at all is itself informative (state off, a
1141
+ // verified command, a proven undo), so say what its absence means too. Same principle as adding
1142
+ // IDLE to this legend in the first place: the reader should never have to infer "nothing to tick
1143
+ // here" from silence alone.
1144
+ el('span', { class: 'cl cl-checkbox-note' },
1145
+ '☐ a tickable box only appears where flipping it is single-step, reversible, and proven — everything else explains itself in words, on purpose'));
1146
+ }
1147
+
1148
+ function advocacyPrecisionSummary(advocacy) {
1149
+ const p = advocacy && advocacy.precision;
1150
+ if (!p || typeof p !== 'object') return null;
1151
+ const n = Number(p.offered) || 0;
1152
+ const applied = Number(p.applied) || 0;
1153
+ const interval = Array.isArray(p.interval) && p.interval.length === 2
1154
+ ? `${Math.round(Number(p.interval[0]) * 100)}–${Math.round(Number(p.interval[1]) * 100)}%`
1155
+ : null;
1156
+ if (!n || p.precision == null) {
1157
+ return el('section', { class: 'cap-precision', 'aria-label': 'Advocacy precision' },
1158
+ el('h3', {}, 'Advocacy precision · accruing'),
1159
+ el('p', {}, 'Not judgeable yet — no resolved offers have accumulated. This is a ',
1160
+ el('b', {}, 'post-launch metric'), ', never a fabricated launch score.'));
1161
+ }
1162
+ return el('section', { class: 'cap-precision', 'aria-label': 'Advocacy precision' },
1163
+ el('h3', {}, 'Advocacy precision · accruing'),
1164
+ el('p', {},
1165
+ el('b', {}, `${applied} of ${n}`), ' resolved offers were applied',
1166
+ interval ? ` · honest 95% interval ${interval}` : '',
1167
+ '. This remains a ', el('b', {}, 'post-launch metric'), ' while evidence accumulates.'));
1168
+ }
1169
+
1170
+ let lastCapabilities = null; // kept so the card can re-render once the rec cards its checkboxes point at have mounted
1171
+ function renderCapabilities(data) {
1172
+ lastCapabilities = data;
1173
+ const body = $('#body-capabilities');
1174
+ const rows = Array.isArray(data && data.rows)
1175
+ ? data.rows.filter((r) => r && typeof r === 'object')
1176
+ : null;
1177
+
1178
+ // 200 with a body we can't read is NOT an empty machine. Say which of the two happened.
1179
+ if (!rows) {
1180
+ setChips('chips-capabilities', [chip('not checked', 'nt')]);
1181
+ body.replaceChildren(withIllo('capabilities',
1182
+ el('p', { class: 'lead-stat' }, 'Not checked — the answer arrived in a shape this page can’t read.'),
1183
+ el('p', {}, 'The console asked for your capability states and got a reply without a readable ',
1184
+ el('code', {}, 'rows'), ' list. Rather than show you something invented from a malformed answer, ',
1185
+ 'this card shows nothing and says so.'),
1186
+ el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => { capsSkeleton(); loadCapabilities(); } }, 'Try again')));
1187
+ return;
1188
+ }
1189
+
1190
+ if (!rows.length) {
1191
+ // Empty-first: a fresh machine with nothing installed must read honestly and sensibly.
1192
+ setChips('chips-capabilities', [chip('nothing to report', 'grey')]);
1193
+ body.replaceChildren(withIllo('capabilities',
1194
+ el('p', { class: 'lead-stat' }, 'Nothing to report yet.'),
1195
+ el('p', {}, 'The audit ran and found no capabilities to describe on this machine — that is a real ',
1196
+ 'answer, not a blank card. As you install more of the stack, each piece appears here with its ',
1197
+ 'state and the evidence behind it.')));
1198
+ return;
1199
+ }
1200
+
1201
+ const on = rows.filter((r) => capBucket(r) === 'ON').length;
1202
+ const off = rows.filter((r) => capBucket(r) === 'OFF').length;
1203
+ const absent = rows.filter((r) => capBucket(r) === 'ABSENT').length;
1204
+ // Counted explicitly, and SUBTRACTED from unknown below. `unknown` is computed as the remainder, so
1205
+ // any state the tally forgets is silently reported as "we never checked" — the same shape of lie in
1206
+ // the summary line that CAP_STATE fixed in the rows.
1207
+ const idle = rows.filter((r) => capBucket(r) === 'IDLE').length;
1208
+ const unknown = rows.length - on - off - absent - idle;
1209
+
1210
+ const chips = [];
1211
+ if (on) chips.push(chip(`${fmtInt(on)} on`, 'green'));
1212
+ if (off) chips.push(chip(`${fmtInt(off)} off`, 'amber', CAP_STATE.OFF.hint));
1213
+ if (idle) chips.push(chip(`${fmtInt(idle)} set up, not running`, 'amber', CAP_STATE.IDLE.hint));
1214
+ if (absent) chips.push(chip(`${fmtInt(absent)} not installed`, 'grey', CAP_STATE.ABSENT.hint));
1215
+ if (unknown) chips.push(chip(`${fmtInt(unknown)} not checked`, 'nt', CAP_STATE.UNKNOWN.hint));
1216
+ setChips('chips-capabilities', chips);
1217
+ // Feed the verdict line. It needs the same counts this card just derived, and deriving them a
1218
+ // second time somewhere else is how two surfaces start disagreeing about one machine.
1219
+ found.capsOn = on; found.capsOff = off; found.capsAbsent = absent; found.capsUnknown = unknown;
1220
+ found.capsTotal = rows.length;
1221
+ updateFoundStrip();
1222
+
1223
+ // Attention first, same law as every other list on this page (never alphabetical, never arrival
1224
+ // order): what you own but aren't getting, then what we couldn't answer, then what isn't here at
1225
+ // all, then what's already working. ABSENT sits below UNKNOWN because an unanswered question is
1226
+ // more actionable than a piece of software the user has simply not installed — and above ON
1227
+ // because "not here" is still a gap. Array#sort is stable, so the server's own ordering survives
1228
+ // inside each bucket.
1229
+ const RANK = { OFF: 0, IDLE: 1, UNKNOWN: 2, ABSENT: 3, ON: 4 };
1230
+ const sorted = rows.slice().sort((a, b) => RANK[capBucket(a)] - RANK[capBucket(b)]);
1231
+
1232
+ // Built as clauses so a fresh machine — where `on` and `off` are both 0 and everything is absent —
1233
+ // reads as a sentence rather than as "0 on, 0 off". Empty-first is a rendering requirement, not
1234
+ // only a data one: the honest numbers still have to make sense out loud.
1235
+ const counts = [
1236
+ on ? el('span', {}, el('b', {}, fmtInt(on)), ' on') : null,
1237
+ off ? el('span', {}, el('b', {}, fmtInt(off)), ' off') : null,
1238
+ absent ? el('span', {}, el('b', {}, fmtInt(absent)), ' not installed') : null,
1239
+ unknown ? el('span', {}, el('b', {}, fmtInt(unknown)), ' we could not check') : null,
1240
+ ].filter(Boolean);
1241
+ const joined = [];
1242
+ counts.forEach((c, i) => {
1243
+ if (i) joined.push(i === counts.length - 1 ? ', and ' : ', ');
1244
+ joined.push(c);
1245
+ });
1246
+
1247
+ const main = [];
1248
+ main.push(el('p', { class: 'lead-stat' },
1249
+ 'We looked at ', el('b', {}, fmtInt(rows.length)),
1250
+ ` capabilit${rows.length === 1 ? 'y' : 'ies'} on this machine: `,
1251
+ ...joined, '.',
1252
+ unknown
1253
+ ? ' “Not checked” is its own answer here — no detector established those either way, so this card claims nothing about them.'
1254
+ : ' Every row below was established by something we observed, not assumed.'));
1255
+
1256
+ main.push(capLegend());
1257
+ const precisionSummary = advocacyPrecisionSummary(data && data.advocacy);
1258
+ if (precisionSummary) main.push(precisionSummary);
1259
+
1260
+ /* GROUPED BY SCOPE, not flat. `scope` has been in the registry since it was written
1261
+ (capability-registry.mjs — SCOPE.MACHINE / PROJECT / USER) and was rendered as a small grey
1262
+ badge nobody reads. As a badge it is trivia; as the GROUPING it answers the question the owner
1263
+ actually asks on opening this page: "what is switched on FOR THIS PROJECT, versus everywhere?"
1264
+ Order is most-specific-first — the project you are standing in is the one you can act on now,
1265
+ and the machine-wide rows are the ones you touch least often.
1266
+
1267
+ A scope with no rows renders nothing at all. An empty "In this project" heading would imply we
1268
+ looked and found none, when the truth is there was nothing of that scope to look at. */
1269
+ const SCOPE_GROUPS = [
1270
+ { key: 'project', title: 'In this project', blurb: 'Applies only where you are right now. Changing these affects this project and nothing else.' },
1271
+ { key: 'user', title: 'For you, in every project', blurb: 'Follows your user account across every project on this machine.' },
1272
+ { key: 'machine', title: 'On this machine', blurb: 'Machine-wide. These affect anyone using this computer, so they are the ones to read twice.' },
1273
+ ];
1274
+ const seen = new Set();
1275
+ const groups = [];
1276
+ for (const g of SCOPE_GROUPS) {
1277
+ const inGroup = sorted.filter((r) => String(r.scope || '').toLowerCase() === g.key);
1278
+ inGroup.forEach((r) => seen.add(r));
1279
+ if (!inGroup.length) continue;
1280
+ groups.push(el('section', { class: 'cap-group' },
1281
+ el('h3', { class: 'cap-group-h' }, g.title,
1282
+ el('span', { class: 'cap-group-n mono' }, `${inGroup.length}`)),
1283
+ el('p', { class: 'cap-group-blurb muted' }, g.blurb),
1284
+ el('div', { class: 'cap-list' }, ...inGroup.map(capRow))));
1285
+ }
1286
+ // Anything whose scope we do not recognise still gets shown — silently dropping a capability
1287
+ // because its scope string was unexpected would be the console lying by omission.
1288
+ const ungrouped = sorted.filter((r) => !seen.has(r));
1289
+ if (ungrouped.length) {
1290
+ groups.push(el('section', { class: 'cap-group' },
1291
+ el('h3', { class: 'cap-group-h' }, 'Everything else',
1292
+ el('span', { class: 'cap-group-n mono' }, `${ungrouped.length}`)),
1293
+ el('div', { class: 'cap-list' }, ...ungrouped.map(capRow))));
1294
+ }
1295
+ main.push(...groups);
1296
+
1297
+ // Almost no control that can't act: this card mostly reads. The one exception — a row whose
1298
+ // checkbox cleared capCheckboxEligible()'s full gate list — still never applies anything itself; it
1299
+ // only opens the real, consent-gated card below. Say where the acting happens either way, instead of
1300
+ // growing a button (or a checkbox) with no executor and no undo behind it.
1301
+ main.push(el('p', { class: 'fineprint' },
1302
+ 'This card mostly reads. A few rows carry a tickable box — even there, ticking only opens the ',
1303
+ 'consent-gated proposal below; nothing runs until you confirm it ', el('b', {}, 'there'),
1304
+ ', with its evidence, its cost, and its undo.',
1305
+ infoBtn('What’s on, what’s off', CAPABILITIES_INFO)));
1306
+
1307
+ body.replaceChildren(withIllo('capabilities', ...main));
1308
+ }
1309
+
1310
+ function capsSkeleton() {
1311
+ $('#body-capabilities').replaceChildren(
1312
+ frag('<div class="skeleton" aria-hidden="true"><div class="sk-bar w40"></div><div class="sk-bar w80"></div><div class="sk-bar w65"></div></div>'),
1313
+ el('p', { class: 'loading-note' },
1314
+ 'Checking each capability against this machine — local and read-only. ',
1315
+ 'Anything we can’t establish is reported as “not checked”, never as “off”.'));
1316
+ setChips('chips-capabilities', [chip('checking…', 'wait')]);
1317
+ }
1318
+
1319
+ /* The endpoint is NEW. A console binary older than this panel — or a plugin mid-update — simply has
1320
+ no /api/capabilities route, and the server's static handler answers 404 for it. That is a known,
1321
+ expected, non-broken condition and must not be dressed as a crash, so the status code is read
1322
+ directly here instead of being recovered from an error string. */
1323
+ async function fetchCapabilities() {
1324
+ if (MOCK) return { ok: true, status: 200, data: structuredClone(MOCK_CAPABILITIES) };
1325
+ const res = await fetch('/api/capabilities', { headers: { Accept: 'application/json' } });
1326
+ if (!res.ok) return { ok: false, status: res.status, data: null };
1327
+ return { ok: true, status: res.status, data: await res.json() };
1328
+ }
1329
+
1330
+ function capsMissingEndpoint() {
1331
+ setChips('chips-capabilities', [chip('not checked yet', 'nt')]);
1332
+ $('#body-capabilities').replaceChildren(withIllo('capabilities',
1333
+ el('p', { class: 'lead-stat' }, 'Not checked yet — this console can’t ask the question.'),
1334
+ el('p', {}, 'The page asks ', el('code', {}, '/api/capabilities'),
1335
+ ' for the state of each capability, and the server answering right now doesn’t have that ',
1336
+ 'endpoint — it was built before this panel existed. Nothing is inferred from that silence: ',
1337
+ 'an unanswered question is not an ', el('b', {}, 'off'), ', so no rows are shown at all.'),
1338
+ el('p', {}, 'Update the Brain and restart the console, and this card fills itself in.'),
1339
+ el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => { capsSkeleton(); loadCapabilities(); } }, 'Try again')));
1340
+ }
1341
+
1342
+ async function loadCapabilities(attempt = 0) {
1343
+ try {
1344
+ const r = await fetchCapabilities();
1345
+ if (r.status === 404) { capsMissingEndpoint(); return; }
1346
+ if (!r.ok) throw new Error(`/api/capabilities answered HTTP ${r.status}`);
1347
+ if (r.data && r.data.warming) {
1348
+ // "Not measured yet" is NOT "off" — this card's entire reason for existing. Rendering a
1349
+ // warming answer would produce an empty row list, which on this surface says "you own
1350
+ // nothing", the single most damaging thing this page could get wrong.
1351
+ capsSkeleton();
1352
+ setChips('chips-capabilities', [chip('checking each capability…', 'wait')]);
1353
+ if (attempt < WARM_RETRY_MAX) setTimeout(() => { void loadCapabilities(attempt + 1); }, WARM_RETRY_MS);
1354
+ else setChips('chips-capabilities', [chip('check is taking unusually long', 'warn')]);
1355
+ return;
1356
+ }
1357
+ renderCapabilities(r.data);
1358
+ } catch (err) {
1359
+ // A real failure (server down, unparseable JSON) gets the page's real failure treatment —
1360
+ // deliberately different from the 404 above, so "not built yet" never reads as "broken".
1361
+ setChips('chips-capabilities', [chip('unavailable', 'grey')]);
1362
+ inlineError('body-capabilities', String(err.message || err), () => { capsSkeleton(); loadCapabilities(); });
1363
+ }
1364
+ }
1365
+
1366
+ /* ----------------------------------------------------------- section 2: wiring */
1367
+
1368
+ const MECH_LABEL = { NPX: 'npx', GLOBAL_BINARY: 'global', PLUGIN: 'plugin', MCP: 'mcp' };
1369
+
1370
+ /* Verdict icons — same 2px round-cap stroke language as the page's other spot icons. */
1371
+ const WV_ICON_CLEAN = `
1372
+ <svg viewBox="0 0 24 24" aria-hidden="true">
1373
+ <circle cx="12" cy="12" r="8.6"/>
1374
+ <path d="M8.3 12.4l2.5 2.5 4.9-5.4"/>
1375
+ </svg>`;
1376
+ const WV_ICON_DRIFT = `
1377
+ <svg viewBox="0 0 24 24" aria-hidden="true">
1378
+ <path d="M12 4.2 2.9 19.3h18.2z"/>
1379
+ <path d="M12 10.2v4.2"/>
1380
+ <path d="M12 17.1v.02"/>
1381
+ </svg>`;
1382
+
1383
+ function renderWiring(w) {
1384
+ const body = $('#body-wiring');
1385
+ // Attach the "how it's wired" explainer to the card's static <h2> UNCONDITIONALLY. It used to live
1386
+ // only inside the verdict banner, which mounts only when `total > 0` — so on a machine with zero
1387
+ // wiring sites the whole explainer vanished, which is almost certainly why this card was the one the
1388
+ // owner could never place. Idempotent because <h2> is a static node, not one rebuilt each render.
1389
+ const wh2 = document.querySelector('#card-wiring h2');
1390
+ if (wh2 && !wh2.querySelector('.info-btn')) wh2.append(infoBtn('How it’s wired', WIRING_INFO));
1391
+ if (!w) {
1392
+ setChips('chips-wiring', [chip('no data', 'grey')]);
1393
+ body.replaceChildren(el('p', { class: 'muted' }, 'No wiring data received.'));
1394
+ return;
1395
+ }
1396
+ const s = w.summary || {};
1397
+ const sites = Array.isArray(w.sites) ? w.sites : [];
1398
+
1399
+ // The verdict this card exists for (Stuart 2026-07-17: "facts without purpose" — a census
1400
+ // is not an answer). One question, answered up top: when a rUv tool launches here, does the
1401
+ // version you installed actually run? Unpinned npx is the only lane that can lie — it keeps
1402
+ // a private copy in ~/.npm/_npx that quietly ages while every command still "works" (the
1403
+ // 3.25.6-vs-3.28.0 failure). Pinned npx (rUv's own style per his ruvector ADR, e.g.
1404
+ // `npx -y ruvector@0.2.25`) cannot drift — the villain is UNPINNED npx, not npx.
1405
+ // (renderGates answers "what stopped Claude"; this card answers "can what runs go stale".)
1406
+ const npxSites = sites.filter((x) => x.mechanism === 'NPX');
1407
+ const isPinned = (x) => /@\d+(\.\d+){0,2}([^\d.]|$)/.test(String(x.spec || ''));
1408
+ const driftSites = npxSites.filter((x) => !isPinned(x));
1409
+ // Summary counts npx but no rows arrived to inspect? Assume the worst, never the best.
1410
+ const driftN = (!npxSites.length && (s.npx ?? 0) > 0) ? (s.npx ?? 0) : driftSites.length;
1411
+ const pinnedN = npxSites.length - driftSites.length;
1412
+ const total = (s.npx ?? 0) + (s.global ?? 0) + (s.mcp ?? 0) + (s.plugin ?? 0);
1413
+ const projCount = new Set(sites.filter((x) => x.scope === 'project' && x.project).map((x) => x.project)).size;
1414
+
1415
+ setChips('chips-wiring', total
1416
+ ? (driftN
1417
+ ? [chip(`${fmtInt(driftN)} can drift stale`, 'warn'), chip(`${fmtInt(total - driftN)} pinned down`, 'green')]
1418
+ // "can" claimed the FUTURE from present-tense evidence: we observed that 0 of N launch sites
1419
+ // currently resolve via npx, which establishes that nothing IS drifting, not that nothing
1420
+ // COULD. One npx line added tomorrow falsifies the stronger claim, and this project's whole
1421
+ // trust position rests on never making a claim its evidence cannot carry. Fable 5, 2026-07-24.
1422
+ : [chip('nothing is drifting', 'green'), chip(`${fmtInt(total)} launch sites`, 'grey')])
1423
+ : [chip('nothing wired yet', 'grey')]);
1424
+
1425
+ found.npx = s.npx ?? 0;
1426
+ found.projects = s.projectsWithNpx ?? 0;
1427
+ found.projectNames = [...new Set(sites.filter((x) => x.scope === 'project' && x.project).map((x) => x.project))];
1428
+ updateFoundStrip();
1429
+
1430
+ const main = [];
1431
+ if (total) {
1432
+ // ---- the verdict banner: the answer first, evidence below it ----
1433
+ const title = driftN
1434
+ ? `${fmtInt(driftN)} launch site${driftN === 1 ? '' : 's'} can silently run a stale copy`
1435
+ : 'Every rUv tool here resolves to one known version';
1436
+ const sub = [];
1437
+ if (driftN) {
1438
+ sub.push('Unpinned npx keeps a private copy in ', el('code', {}, '~/.npm/_npx'),
1439
+ ' and runs that — every command still “works” while old code answers. Rewire ',
1440
+ driftN === 1 ? 'it' : 'each one', ' to the global binary, or pin the exact version the way rUv does.');
1441
+ } else {
1442
+ const bits = [];
1443
+ if (s.global) bits.push(el('span', {}, el('b', {}, fmtInt(s.global)), ' through your one global binary'));
1444
+ if (s.mcp) bits.push(el('span', {}, el('b', {}, fmtInt(s.mcp)), ' through a running MCP server'));
1445
+ if (s.plugin) bits.push(el('span', {}, el('b', {}, fmtInt(s.plugin)), ' inside Claude Code itself'));
1446
+ if (pinnedN) bits.push(el('span', {}, el('b', {}, fmtInt(pinnedN)), ' via npx pinned to an exact version, which cannot age'));
1447
+ const joined = [];
1448
+ bits.forEach((b, i) => { if (i) joined.push(i === bits.length - 1 ? ' and ' : ', '); joined.push(b); });
1449
+ sub.push('All ', el('b', {}, fmtInt(total)), ' launch sites',
1450
+ projCount ? el('span', {}, ' across ', el('b', {}, fmtInt(projCount)), ` project${projCount === 1 ? '' : 's'}`) : '',
1451
+ ' are accounted for: ', ...joined,
1452
+ '. Zero unpinned npx — nothing can silently drift stale.');
1453
+ }
1454
+ main.push(el('div', { class: 'wire-verdict' + (driftN ? ' is-drift' : ' is-clean') },
1455
+ el('span', { class: 'wv-icon', 'aria-hidden': 'true' }, frag(driftN ? WV_ICON_DRIFT : WV_ICON_CLEAN)),
1456
+ el('div', { class: 'wv-text' },
1457
+ el('p', { class: 'wv-title' }, title),
1458
+ el('p', { class: 'wv-sub' }, ...sub),
1459
+ driftN && driftSites.length ? el('ul', { class: 'wv-sites' },
1460
+ ...driftSites.slice(0, 8).map((x) => el('li', {},
1461
+ el('span', { class: 'site-where' }, x.scope === 'project' ? (x.project || 'unknown project') : 'machine-wide'),
1462
+ el('span', { class: 'cell-dim' }, x.file || '—'),
1463
+ el('span', { class: 'site-spec' }, x.spec || ''))),
1464
+ driftSites.length > 8 ? el('li', { class: 'cell-dim' },
1465
+ `+ ${fmtInt(driftSites.length - 8)} more — the full map is in the peel-back below`) : null) : null)));
1466
+
1467
+ // ---- lane legend, subordinate to the verdict: one quiet line per lane in use.
1468
+ // A lane at zero is omitted, not excused — a "0 plugin" row answers nothing.
1469
+ const npxMeaning = driftN === 0
1470
+ ? 'pinned to exact versions — deliberate, reproducible, cannot age'
1471
+ : pinnedN > 0
1472
+ ? `${fmtInt(driftN)} unpinned can drift stale · ${fmtInt(pinnedN)} pinned are safe`
1473
+ : 'downloads a private copy per call — can silently drift stale';
1474
+ const lanes = [
1475
+ { n: s.global ?? 0, label: 'global binary', tone: 'w-global', meaning: 'one path, one version — what runs is what you installed' },
1476
+ { n: s.mcp ?? 0, label: 'MCP server', tone: 'w-mcp', meaning: 'a running tool the AI calls directly — alive, not re-downloaded' },
1477
+ { n: s.npx ?? 0, label: 'npx', tone: 'w-npx', meaning: npxMeaning },
1478
+ { n: s.plugin ?? 0, label: 'plugin', tone: 'w-plugin', meaning: 'ships inside Claude Code itself' },
1479
+ ].filter((l) => l.n > 0);
1480
+ if (driftN) lanes.sort((a, b) => Number(b.label === 'npx') - Number(a.label === 'npx')); // risk leads
1481
+ main.push(el('div', { class: 'wire-legend' },
1482
+ ...lanes.map((l) => el('div', { class: 'wire-leg-row' },
1483
+ el('span', { class: 'wire-dot ' + l.tone, 'aria-hidden': 'true' }),
1484
+ el('b', { class: 'wire-leg-n' }, fmtInt(l.n)),
1485
+ el('span', { class: 'wire-leg-lab' }, l.label),
1486
+ el('span', { class: 'wire-leg-meaning cell-dim' }, l.meaning)))));
1487
+ }
1488
+
1489
+ if (sites.length) {
1490
+ const groups = new Map();
1491
+ for (const site of sites) {
1492
+ const key = site.scope === 'project' ? (site.project || 'unknown project') : 'global (your user settings)';
1493
+ if (!groups.has(key)) groups.set(key, []);
1494
+ groups.get(key).push(site);
1495
+ }
1496
+ const outer = el('details', { class: 'sub' },
1497
+ el('summary', {}, `Peel it back — all ${fmtInt(sites.length)} resolution sites, project by project`),
1498
+ el('div', { class: 'sub-body' },
1499
+ [...groups.entries()].map(([proj, list]) => el('details', { class: 'sub' },
1500
+ el('summary', {}, `${proj} — ${list.length} site${list.length === 1 ? '' : 's'}`),
1501
+ el('div', { class: 'sub-body' },
1502
+ el('div', { class: 'scroll-x' },
1503
+ el('table', { class: 'tb' },
1504
+ el('thead', {}, el('tr', {},
1505
+ el('th', { scope: 'col' }, 'File'), el('th', { scope: 'col' }, 'Event · matcher'),
1506
+ el('th', { scope: 'col' }, 'Via'), el('th', { scope: 'col' }, 'Spec'))),
1507
+ el('tbody', {}, list.map((site) => el('tr', {},
1508
+ el('td', { class: 'cell-mono' }, site.file || '—'),
1509
+ el('td', { class: 'cell-mono cell-dim' }, [site.event, site.matcher].filter(Boolean).join(' · ') || '—'),
1510
+ el('td', {}, chip(MECH_LABEL[site.mechanism] || String(site.mechanism || '?').toLowerCase(), 'grey')),
1511
+ el('td', { class: 'cell-mono cell-dim' }, site.spec || '—'),
1512
+ ))))))))));
1513
+ main.push(outer);
1514
+ } else {
1515
+ main.push(el('p', { class: 'muted' }, total
1516
+ ? 'The site-by-site list didn’t arrive with this audit — the counts above are from the summary.'
1517
+ : 'No resolution sites found — nothing is wired through hooks yet.'));
1518
+ }
1519
+
1520
+ body.replaceChildren(withIllo('wiring', ...main));
1521
+ }
1522
+
1523
+ /* -------------------------------------------------- section 3: recommendations */
1524
+
1525
+ const SEV_TONE = { INFO: 'grey', SUGGESTED: 'cyan', IMPORTANT: 'amber' };
1526
+
1527
+ const ICON_MACHINE = `
1528
+ <svg viewBox="0 0 24 24" aria-hidden="true">
1529
+ <rect x="4" y="5" width="16" height="11" rx="2"/>
1530
+ <path d="M2 19h20"/>
1531
+ </svg>`;
1532
+
1533
+ const BADGE_OK = `
1534
+ <svg class="applied-badge" viewBox="0 0 32 32" aria-hidden="true">
1535
+ <circle cx="16" cy="16" r="14"/>
1536
+ <path d="M10 16.8l4.1 4L22 12.2"/>
1537
+ </svg>`;
1538
+
1539
+ function updateRecsChip() {
1540
+ const n = renderedRecIds.size;
1541
+ if (!stateRecsSettled && !stackRecsSettled && !healthRecsSettled) { setChips('chips-recs', [chip('…', 'wait')]); return; }
1542
+ if (n === 0 && stateRecsSettled && stackRecsSettled && healthRecsSettled) {
1543
+ setChips('chips-recs', [chip('none needed', 'green')]);
1544
+ } else {
1545
+ setChips('chips-recs', [chip(`${n} proposal${n === 1 ? '' : 's'}`, n ? 'amber' : 'wait')]);
1546
+ }
1547
+ renderFixAll();
1548
+ }
1549
+
1550
+ function fixAllEligible() {
1551
+ return [...renderedRecommendations.values()].filter((rec) =>
1552
+ rec && Array.isArray(rec.evidence) && rec.evidence.length && rec.cost && rec.undo);
1553
+ }
1554
+
1555
+ function renderFixAll() {
1556
+ const slot = $('#recs-batch');
1557
+ if (!slot) return;
1558
+ const eligible = fixAllEligible();
1559
+ if (!eligible.length) { slot.replaceChildren(); return; }
1560
+
1561
+ const openConfirm = () => {
1562
+ const list = el('ul', { class: 'evidence-list' },
1563
+ eligible.map((rec) => el('li', {},
1564
+ el('b', {}, rec.title || rec.id),
1565
+ ` — ${rec.change?.human || 'apply the verified recommendation'}`,
1566
+ rec.undo?.human ? `; undo: ${rec.undo.human}` : '')));
1567
+ slot.replaceChildren(el('div', { class: 'confirm', role: 'group', 'aria-label': 'Confirm every verified recommendation' },
1568
+ el('p', { class: 'confirm-q' }, `Apply all ${eligible.length} verified fixes?`),
1569
+ el('p', { class: 'confirm-detail' },
1570
+ 'Each item is re-checked immediately before it runs. Resolved or changed items are skipped. ',
1571
+ 'Every successful item records its own undo before mutation.'),
1572
+ list,
1573
+ el('div', { class: 'confirm-btns' },
1574
+ el('button', { class: 'btn btn-apply', type: 'button', onclick: () => void applyAllVerified(eligible) }, 'Yes, fix all verified items'),
1575
+ el('button', { class: 'btn btn-ghost', type: 'button', onclick: renderFixAll }, 'Cancel'))));
1576
+ };
1577
+
1578
+ slot.replaceChildren(el('div', { class: 'save-row' },
1579
+ el('button', { class: 'btn btn-apply', type: 'button', onclick: openConfirm },
1580
+ `Fix all (${eligible.length})`),
1581
+ el('p', { class: 'save-note' },
1582
+ 'Only the evidence-backed, reversible proposals listed below. Unsupported settings and secrets are never included.')));
1583
+ }
1584
+
1585
+ async function undoBatchResult(result, btn) {
1586
+ if (!result.undoToken) return;
1587
+ btn.disabled = true;
1588
+ const response = await postJSON('/api/undo', { undoToken: result.undoToken });
1589
+ if (response.ok && response.data?.ok) {
1590
+ btn.replaceWith(el('span', { class: 'reverted' }, 'Reverted'));
1591
+ recheckMachine();
1592
+ } else {
1593
+ btn.disabled = false;
1594
+ announce(response.status === 403 ? TOKEN_MSG : 'Undo did not complete; the backup remains available.');
1595
+ }
1596
+ }
1597
+
1598
+ async function applyAllVerified(recs) {
1599
+ const slot = $('#recs-batch');
1600
+ slot.replaceChildren(el('p', { class: 'pending-note', role: 'status' }, `Applying ${recs.length} verified fixes…`));
1601
+ try {
1602
+ const response = await postJSON('/api/apply', { ids: recs.map((rec) => rec.id), preStateHash });
1603
+ if (response.status === 403) {
1604
+ slot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' }, TOKEN_MSG));
1605
+ return;
1606
+ }
1607
+ const results = Array.isArray(response.data?.results) ? response.data.results : [];
1608
+ const ok = results.filter((result) => result.ok);
1609
+ const skipped = results.filter((result) => !result.ok);
1610
+ slot.replaceChildren(el('div', { class: `form-note ${skipped.length ? 'n-err' : 'n-ok'}`, role: 'status' },
1611
+ el('div', { class: 'fn-body' },
1612
+ el('b', {}, `${ok.length} applied; ${skipped.length} skipped or failed.`),
1613
+ ' Every applied item has its own undo below.')));
1614
+ for (const result of results) {
1615
+ const card = document.getElementById(`rec-${result.id}`);
1616
+ const actions = card?.querySelector('.rec-actions');
1617
+ if (!actions) continue;
1618
+ if (result.ok) {
1619
+ card.classList.add('is-applied');
1620
+ const undoButton = el('button', {
1621
+ class: 'btn btn-undo',
1622
+ type: 'button',
1623
+ disabled: !result.undoToken,
1624
+ onclick: (event) => void undoBatchResult(result, event.currentTarget),
1625
+ }, 'Undo this change');
1626
+ actions.replaceChildren(el('div', { class: 'applied' },
1627
+ el('p', { class: 'applied-title' }, 'Applied by Fix all — and reversible.'),
1628
+ result.log ? el('pre', { class: 'log' }, String(result.log)) : null,
1629
+ undoButton));
1630
+ } else {
1631
+ actions.replaceChildren(el('div', { class: 'world-moved', role: 'alert' },
1632
+ el('p', {}, result.log || 'Skipped because the machine changed or the fix no longer applies.')));
1633
+ }
1634
+ }
1635
+ announce(`${ok.length} verified fixes applied; ${skipped.length} skipped or failed.`);
1636
+ recheckMachine();
1637
+ } catch (error) {
1638
+ slot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
1639
+ `Fix all could not reach the console server: ${error.message || error}.`));
1640
+ }
1641
+ }
1642
+
1643
+ function maybeRecsEmpty() {
1644
+ const emptyBox = $('#recs-empty');
1645
+ if (!emptyBox) return;
1646
+ if (stateRecsSettled && stackRecsSettled && healthRecsSettled && renderedRecIds.size === 0) {
1647
+ emptyBox.hidden = false;
1648
+ emptyBox.replaceChildren(el('div', { class: 'recs-empty' },
1649
+ withIllo('recs',
1650
+ el('p', { class: 'lead' }, 'Nothing to suggest.'),
1651
+ el('p', {}, 'Your setup looks the way you meant it to — and an advisor with nothing to say should say exactly that. If your machine changes, reload and we’ll look again.'))));
1652
+ } else {
1653
+ emptyBox.hidden = true;
1654
+ }
1655
+ }
1656
+
1657
+ function recsSettled(source, ok) {
1658
+ if (source === 'state') stateRecsSettled = true;
1659
+ if (source === 'health') healthRecsSettled = true;
1660
+ // A capability checkbox only renders once the rec card it opens (`#rec-enable:<key>`) is in the
1661
+ // DOM — before that it shows an honest "still loading" placeholder (see capCheckbox). Those rec
1662
+ // cards arrive with the recommendations, AFTER capabilities have already rendered once. Without
1663
+ // this, the placeholder never upgrades and the checkbox never appears. Re-render capabilities once
1664
+ // the recs that back them have settled, but ONLY if a pending placeholder is actually waiting —
1665
+ // no cards waiting means no reason to repaint.
1666
+ if ((source === 'state' || source === 'health') && lastCapabilities
1667
+ && document.querySelector('#card-capabilities .cap-toggle-pending')) {
1668
+ renderCapabilities(lastCapabilities);
1669
+ }
1670
+ if (source === 'stack') {
1671
+ stackRecsSettled = true;
1672
+ const pending = $('#recs-pending');
1673
+ if (pending) {
1674
+ if (ok) pending.remove();
1675
+ else {
1676
+ pending.textContent = 'The stack audit failed, so suggestions from it can’t appear this session.';
1677
+ pending.style.color = 'var(--warn-text)';
1678
+ }
1679
+ }
1680
+ }
1681
+ updateRecsChip();
1682
+ maybeRecsEmpty();
1683
+ }
1684
+
1685
+ function addRecommendations(recs, source) {
1686
+ const list = $('#recs-list');
1687
+ // Ordering explains itself (shown once): machine-wide first, then your active projects.
1688
+ if (!document.getElementById('recs-order-note')) {
1689
+ list.before(el('p', { class: 'impact-note', id: 'recs-order-note' },
1690
+ 'Ordered by what you’re working on — machine-wide updates first (they affect every project), then your most recently active projects.'));
1691
+ }
1692
+ let dropped = 0;
1693
+ const nodes = [];
1694
+ for (const rec of Array.isArray(recs) ? recs : []) {
1695
+ if (!rec || rec.id == null || renderedRecIds.has(rec.id)) continue;
1696
+ // The DDD invariant, honored in the UI too: no evidence/cost/undo → not rendered.
1697
+ if (!Array.isArray(rec.evidence) || !rec.evidence.length || !rec.cost || !rec.undo) { dropped += 1; continue; }
1698
+ renderedRecIds.add(rec.id);
1699
+ renderedRecommendations.set(rec.id, rec);
1700
+ nodes.push(buildRecCard(rec));
1701
+ }
1702
+ // Ordering is by BLAST RADIUS, not arrival time — the slow sources arrive last and matter most.
1703
+ //
1704
+ // 'health' outranks even stack updates. A store holding thousands of memories that teach your AI
1705
+ // nothing, or a corrupt memory index, is a bigger deal than a version being one behind — and it is
1706
+ // the one thing the user cannot discover for themselves, because nothing else on the machine says
1707
+ // it out loud. That is the entire premise of ADR-027: knowing which question to ask is the scarce
1708
+ // resource, so the answer nobody knew to ask for goes first.
1709
+ if ((source === 'health' || source === 'stack') && list.firstChild) list.prepend(...nodes);
1710
+ else list.append(...nodes);
1711
+ if (dropped) {
1712
+ list.append(el('p', { class: 'fineprint' },
1713
+ `${dropped} proposal${dropped === 1 ? '' : 's'} arrived without evidence, cost, or an undo and ${dropped === 1 ? 'was' : 'were'} not rendered — the contract requires all three.`));
1714
+ }
1715
+ updateRecsChip();
1716
+ }
1717
+
1718
+ /* The owner's "user-level vs per-project" question, answered per card: the blast radius of applying
1719
+ * this suggestion. `null` scope shows a muted "scope not stated" pill rather than being silently
1720
+ * folded into either side — the same honesty the rest of the console holds to (never guess a state).
1721
+ * A full grouped-sections layout is a deliberate follow-up: its group order and whether user/machine
1722
+ * split into two visible groups are product decisions the owner reserved. */
1723
+ const REC_SCOPE_LABEL = {
1724
+ project: { text: 'Just this project', tone: 'cyan', title: 'Applying this changes only the project you are in right now.' },
1725
+ user: { text: 'Every project · your account', tone: 'amber', title: 'Applying this changes behaviour for every project under your user account.' },
1726
+ machine: { text: 'Every project · this machine', tone: 'amber', title: 'Applying this changes behaviour for every project on this computer.' },
1727
+ };
1728
+ function recScopePill(scope) {
1729
+ const s = REC_SCOPE_LABEL[scope];
1730
+ if (!s) return chip('scope not stated', 'grey', 'We did not establish whether this is project-only or machine-wide — it is not guessed.');
1731
+ return chip(s.text, s.tone, s.title);
1732
+ }
1733
+
1734
+ function buildRecCard(rec) {
1735
+ const status = el('p', { class: 'rec-status', 'aria-live': 'polite' });
1736
+ const actions = el('div', { class: 'rec-actions' });
1737
+ const card = el('article', { class: 'rec', id: `rec-${rec.id}` });
1738
+
1739
+ /* impact surface — the load-bearing safety UX */
1740
+ let impact;
1741
+ if (rec.touchesMachine === true) {
1742
+ impact = el('div', { class: 'impact-banner', role: 'note' },
1743
+ el('span', { class: 'impact-icon' }, frag(ICON_MACHINE)),
1744
+ el('div', { class: 'impact-text' },
1745
+ el('p', { class: 'impact-title' }, 'This one touches your computer'),
1746
+ el('p', { class: 'impact-plain' }, rec.plainImpact ||
1747
+ 'It changes something the rest of your system uses. We’ll ask you to confirm before anything runs, and the exact reversal is recorded first.'),
1748
+ el('span', { class: 'impact-rev' }, 'reversible — undo recorded before it runs')));
1749
+ } else {
1750
+ impact = el('p', { class: 'impact-note' },
1751
+ 'Only writes RuvNet Brain’s own settings file in your user folder — nothing else on your computer changes.');
1752
+ }
1753
+
1754
+ /* evidence · cost · change · undo — all four, always */
1755
+ const facts = el('div', { class: 'rec-facts' },
1756
+ el('div', { class: 'fact' },
1757
+ el('span', { class: 'k k-evidence' }, 'Evidence'),
1758
+ el('div', { class: 'v' }, el('ul', { class: 'evidence-list' },
1759
+ rec.evidence.map((ev) => el('li', {},
1760
+ ev.observed || String(ev),
1761
+ ev.source ? el('span', { class: 'src' }, ` — ${ev.source}`) : null))))),
1762
+ el('div', { class: 'fact' },
1763
+ el('span', { class: 'k k-cost' }, 'Cost'),
1764
+ el('div', { class: 'v' }, el('div', { class: 'cost-row' },
1765
+ rec.cost.time != null ? el('span', { class: 'cost-item' }, el('span', { class: 'ck' }, 'time'), rec.cost.time) : null,
1766
+ rec.cost.latency != null ? el('span', { class: 'cost-item' }, el('span', { class: 'ck' }, 'latency'), rec.cost.latency) : null,
1767
+ rec.cost.usd != null ? el('span', { class: 'cost-item' }, el('span', { class: 'ck' }, 'cost'), fmtUsd(rec.cost.usd)) : null,
1768
+ rec.cost.risk != null ? el('span', { class: `cost-item risk-${rec.cost.risk}` }, el('span', { class: 'ck' }, 'risk'), rec.cost.risk) : null))),
1769
+ rec.change ? el('div', { class: 'fact' },
1770
+ el('span', { class: 'k k-change' }, 'Change'),
1771
+ el('div', { class: 'v' },
1772
+ rec.change.human || '',
1773
+ rec.change.cmd ? el('span', {}, ' — ', el('code', {}, rec.change.cmd)) : null)) : null,
1774
+ el('div', { class: 'fact' },
1775
+ el('span', { class: 'k k-undo' }, 'Undo'),
1776
+ el('div', { class: 'v' }, rec.undo.human || rec.undo.kind || 'recorded before the change runs')),
1777
+ );
1778
+
1779
+ function setIdleActions() {
1780
+ actions.replaceChildren(
1781
+ el('button', { class: 'btn btn-apply', type: 'button', onclick: onApply },
1782
+ rec.touchesMachine ? 'Apply…' : 'Apply'),
1783
+ el('button', { class: 'btn btn-ghost', type: 'button', onclick: onSkip }, 'Skip'),
1784
+ );
1785
+ }
1786
+
1787
+ function onApply() {
1788
+ if (rec.touchesMachine === true) showConfirm();
1789
+ else doApply();
1790
+ }
1791
+
1792
+ function showConfirm() {
1793
+ const cancel = el('button', { class: 'btn btn-ghost', type: 'button', onclick: setIdleActions }, 'Cancel');
1794
+ actions.replaceChildren(
1795
+ el('div', { class: 'confirm', role: 'group', 'aria-label': 'Confirm a change to your computer' },
1796
+ el('p', { class: 'confirm-q' }, 'Change your computer now?'),
1797
+ el('p', { class: 'confirm-detail' },
1798
+ rec.change?.human ? `It will ${rec.change.human}. ` : '',
1799
+ rec.undo?.human ? `If you change your mind: ${rec.undo.human}.` : 'The reversal is recorded before anything runs.'),
1800
+ el('div', { class: 'confirm-btns' },
1801
+ el('button', { class: 'btn btn-apply', type: 'button', onclick: doApply }, 'Yes, change my computer'),
1802
+ cancel)));
1803
+ cancel.focus();
1804
+ }
1805
+
1806
+ async function doApply() {
1807
+ actions.replaceChildren(el('button', { class: 'btn btn-apply', type: 'button', disabled: true }, 'Applying…'));
1808
+ status.textContent = '';
1809
+ status.dataset.tone = '';
1810
+ try {
1811
+ const { status: code, data } = await postJSON('/api/apply', { ids: [rec.id], preStateHash });
1812
+ if (code === 403) return fail(TOKEN_MSG);
1813
+ if (data && data.worldMoved) return worldMoved();
1814
+ const result = (data.results || []).find((r) => r.id === rec.id) || (data.results || [])[0];
1815
+ if (!result) return fail('The server returned no result for this change — nothing was assumed applied.');
1816
+ if (result.worldMoved || result.error === 'worldMoved') return worldMoved();
1817
+ if (result.ok) applied(result);
1818
+ else fail('The change didn’t complete. Nothing runs without its backup recorded first.', result.log);
1819
+ } catch (err) {
1820
+ fail(`Couldn’t reach the console server: ${err.message || err}`);
1821
+ }
1822
+ }
1823
+
1824
+ function applied(result) {
1825
+ card.classList.add('is-applied');
1826
+ const undoBtn = el('button', {
1827
+ class: 'btn btn-undo', type: 'button',
1828
+ onclick: () => doUndo(result.undoToken, undoBtn),
1829
+ }, 'Undo this change');
1830
+ actions.replaceChildren(
1831
+ el('div', { class: 'applied' },
1832
+ el('div', { class: 'applied-head' },
1833
+ frag(BADGE_OK),
1834
+ el('div', {},
1835
+ el('p', { class: 'applied-title' }, 'Applied — and reversible.'),
1836
+ el('p', { class: 'applied-sub' }, 'A backup was written before anything ran. The undo below restores it exactly.'))),
1837
+ result.log ? el('pre', { class: 'log' }, String(result.log)) : null,
1838
+ el('div', { class: 'applied-btns' }, undoBtn)));
1839
+ announce(`${rec.title} applied.`);
1840
+ undoBtn.focus();
1841
+ // Close the loop: re-mirror the machine so every card shows the AFTER state.
1842
+ recheckMachine();
1843
+ }
1844
+
1845
+ async function doUndo(undoToken, btn) {
1846
+ if (!undoToken) return fail('No undo token was returned for this change — undo it from the backup file noted in the log.');
1847
+ btn.disabled = true;
1848
+ btn.textContent = 'Undoing…';
1849
+ try {
1850
+ const { status: code, data } = await postJSON('/api/undo', { undoToken });
1851
+ if (code === 403) return fail(TOKEN_MSG);
1852
+ if (data && data.ok) {
1853
+ card.classList.remove('is-applied');
1854
+ actions.replaceChildren(el('p', { class: 'reverted' }, 'Reverted — your machine is back exactly the way it was.'));
1855
+ actions.append(el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: setIdleActions }, 'Offer it again'));
1856
+ announce(`${rec.title} reverted.`);
1857
+ recheckMachine(); // show the restored state everywhere, not just on this card
1858
+ } else {
1859
+ fail('Undo didn’t complete. The backup file still exists — nothing is lost.');
1860
+ }
1861
+ } catch (err) {
1862
+ fail(`Couldn’t reach the console server: ${err.message || err}`);
1863
+ }
1864
+ }
1865
+
1866
+ function worldMoved() {
1867
+ actions.replaceChildren(
1868
+ el('div', { class: 'world-moved', role: 'alert' },
1869
+ el('p', {}, 'Your machine changed since this page loaded — another session or a scheduled job got there first. Nothing was touched: we re-read the world before writing, and it had moved.'),
1870
+ el('button', { class: 'btn btn-ghost', type: 'button', onclick: () => location.reload() }, 'Reload and look again')));
1871
+ announce('Apply aborted: the machine changed since the page loaded.');
1872
+ }
1873
+
1874
+ function fail(msg, log) {
1875
+ status.dataset.tone = 'error';
1876
+ status.textContent = msg;
1877
+ setIdleActions();
1878
+ if (log) actions.before(el('pre', { class: 'log' }, String(log)));
1879
+ announce(msg);
1880
+ }
1881
+
1882
+ function onSkip() {
1883
+ card.classList.add('is-skipped');
1884
+ if (!card.querySelector('.skipped-row')) {
1885
+ card.append(el('div', { class: 'skipped-row' },
1886
+ el('span', { class: 'sk-title' }, `skipped · ${rec.title}`),
1887
+ el('button', {
1888
+ class: 'btn btn-ghost btn-sm', type: 'button',
1889
+ onclick: () => card.classList.remove('is-skipped'),
1890
+ }, 'Show again')));
1891
+ }
1892
+ }
1893
+
1894
+ card.append(
1895
+ el('div', { class: 'rec-top' },
1896
+ el('h3', {}, rec.title || rec.id),
1897
+ recScopePill(rec.scope),
1898
+ chip(rec.severity || 'INFO', SEV_TONE[rec.severity] || 'grey')),
1899
+ rec.rationale ? el('p', { class: 'rationale' }, rec.rationale) : null,
1900
+ impact,
1901
+ facts,
1902
+ actions,
1903
+ status,
1904
+ );
1905
+ setIdleActions();
1906
+ return card;
1907
+ }
1908
+
1909
+ /* ----------------------------------------------------------- section 4: memory */
1910
+
1911
+ const DIM_TONE = { ok: 'green', warn: 'warn', fail: 'red', notTested: 'nt' };
1912
+ const DIM_LABEL = { ok: 'ok', warn: 'warn', fail: 'fail', notTested: 'not checked this session' };
1913
+ const DIAL_ARC = 235.62; /* 270° arc, r=50 */
1914
+
1915
+ /* Constant markup only — the score is injected via textContent/attributes below,
1916
+ never interpolated into HTML. */
1917
+ const DIAL_SVG = `
1918
+ <svg class="dial" viewBox="0 0 120 108" role="img">
1919
+ <defs>
1920
+ <linearGradient id="dial-grad" x1="0" y1="1" x2="1" y2="0">
1921
+ <stop offset="0" stop-color="#f0a830"/><stop offset="0.32" stop-color="#ffce6a"/>
1922
+ <stop offset="0.68" stop-color="#5ad6ff"/><stop offset="1" stop-color="#5fd38a"/>
1923
+ </linearGradient>
1924
+ </defs>
1925
+ <path class="dial-track" d="M 24.64 95.36 A 50 50 0 1 1 95.36 95.36"/>
1926
+ <path class="dial-value" d="M 24.64 95.36 A 50 50 0 1 1 95.36 95.36"
1927
+ stroke="url(#dial-grad)" stroke-dasharray="0 235.62"/>
1928
+ <text class="dial-num" x="60" y="66" text-anchor="middle"></text>
1929
+ <text class="dial-sub" x="60" y="82" text-anchor="middle">of 100</text>
1930
+ </svg>`;
1931
+
1932
+ function dial(score) {
1933
+ const s = Math.max(0, Math.min(100, Math.round(Number(score) || 0)));
1934
+ const box = el('div', { class: 'dial-wrap' }, frag(DIAL_SVG));
1935
+ const svg = box.querySelector('.dial');
1936
+ svg.setAttribute('aria-label', `Memory quality score ${s} out of 100`);
1937
+ svg.querySelector('.dial-num').textContent = String(s);
1938
+ /* let the arc sweep in after first paint */
1939
+ requestAnimationFrame(() => requestAnimationFrame(() => {
1940
+ const v = box.querySelector('.dial-value');
1941
+ if (v) v.setAttribute('stroke-dasharray', `${((s / 100) * DIAL_ARC).toFixed(2)} ${DIAL_ARC.toFixed(2)}`);
1942
+ }));
1943
+ return box;
1944
+ }
1945
+
1946
+ // Compact, delightful "it's learning how you work" strip — the visible face of the recursive learning
1947
+ // loop (ADR-0017). Deliberately small; shows the win without dominating the card.
1948
+ function renderLearnings(l) {
1949
+ if (!l || !l.active) return null;
1950
+ const recent = (l.recentWorkflow || []).slice(0, 6);
1951
+ const when = l.daysSinceLastAdaptation === 0 ? 'updated today'
1952
+ : (l.daysSinceLastAdaptation != null ? `last updated ${l.daysSinceLastAdaptation}d ago` : '');
1953
+ return el('div', { class: 'learn-strip' },
1954
+ el('div', { class: 'learn-head' },
1955
+ el('span', { class: 'learn-spark', 'aria-hidden': 'true' }, '✦'),
1956
+ el('div', { class: 'learn-headtext' },
1957
+ el('div', { class: 'learn-title' }, 'Learning how you work', infoBtn('Learning how you work', LEARNINGS_INFO)),
1958
+ el('div', { class: 'learn-sub' },
1959
+ el('b', {}, fmtInt(l.patterns) + ' patterns'), ' from ', el('b', {}, fmtInt(l.trajectories) + ' workflows'),
1960
+ when ? ' · ' + when : ''))),
1961
+ recent.length ? el('div', { class: 'learn-recent' },
1962
+ el('span', { class: 'learn-recent-lab' }, 'recently observed'),
1963
+ ...recent.map((a) => el('span', { class: 'learn-chip' }, a.length > 30 ? a.slice(0, 28) + '…' : a))) : null,
1964
+ el('p', { class: 'learn-foot fineprint' },
1965
+ 'Shared across all your projects and getting smarter over time — but only ', el('b', {}, 'how you work'), '. Your project facts stay isolated; nothing here is project data.'));
1966
+ }
1967
+
1968
+ function renderMemory(mem) {
1969
+ const body = $('#body-memory');
1970
+ if (!mem || !mem.health) {
1971
+ setChips('chips-memory', [chip('no data', 'grey')]);
1972
+ body.replaceChildren(el('p', { class: 'muted' }, 'No memory-health data received.'));
1973
+ return;
1974
+ }
1975
+ const h = mem.health;
1976
+ const score = Math.max(0, Math.min(100, Math.round(Number(h.score) || 0)));
1977
+ const tone = score >= 85 ? 'green' : score >= 60 ? 'warn' : 'red';
1978
+
1979
+ // COUNT THE UNTESTED DIMENSIONS BEFORE ANYTHING PRINTS THE SCORE.
1980
+ //
1981
+ // These two lines used to sit BELOW the chip and the found-strip assignment, so both surfaces
1982
+ // published a bare "100/100" while a dimension of the five had never been probed — an untested
1983
+ // dimension silently contributing zero deduction inside a perfect-looking score. The server's own
1984
+ // summary string carries the qualifier ("across N probed dimensions; 1 not checked"); both
1985
+ // rendered surfaces dropped it. A perfect score is exactly the number that most needs its
1986
+ // asterisk, and this project's standing rule bans an unqualified score with an untested input.
1987
+ // Found by Fable 5, 2026-07-24.
1988
+ const dims = Array.isArray(h.dimensions) ? h.dimensions : [];
1989
+ const notTested = dims.filter((d) => d.status === 'notTested').length;
1990
+ const probed = dims.length - notTested;
1991
+ const qualifier = notTested
1992
+ ? `${probed} of ${dims.length} dimensions checked; ${notTested} not checked this session`
1993
+ : null;
1994
+
1995
+ setChips('chips-memory', [
1996
+ chip(notTested ? `${score}/100*` : `${score}/100`, tone, qualifier || undefined),
1997
+ ]);
1998
+ found.memScore = score;
1999
+ found.memNotTested = notTested;
2000
+ found.memProbed = probed;
2001
+ found.memDims = dims.length;
2002
+ updateFoundStrip();
2003
+
2004
+ const main = [];
2005
+ main.push(el('div', { class: 'memory-top' },
2006
+ dial(score),
2007
+ el('div', { class: 'mem-summary' },
2008
+ el('h3', {}, h.project ? `${h.project} — memory quality` : 'Memory quality', infoBtn('Memory quality', MEMORY_INFO)),
2009
+ el('p', { class: 'mem-line' }, h.summary ||
2010
+ 'A quality score, not a liveness light: a store can be up, populated, and still never surface the thing you need.'),
2011
+ el('p', { class: 'fineprint' },
2012
+ 'Dimensions we didn’t probe this session are excluded from the score — shown grey below, never assumed. A known-broken dimension caps the score.'))));
2013
+
2014
+ const learn = renderLearnings(mem.learnings);
2015
+ if (learn) main.push(learn);
2016
+
2017
+ if (dims.length) {
2018
+ main.push(el('div', { class: 'dims' }, dims.map((d) => {
2019
+ const st = DIM_TONE[d.status] ? d.status : 'notTested';
2020
+ const ded = Number(d.deduction) || 0;
2021
+ return el('div', { class: `dim${st === 'notTested' ? ' dim-nt' : ''}` },
2022
+ chip(DIM_LABEL[st], DIM_TONE[st]),
2023
+ el('span', { class: 'dim-name' }, d.label || d.key || '—'),
2024
+ el('span', { class: `dim-ded${ded > 0 ? ' has-ded' : ''}`,
2025
+ title: st === 'notTested' ? 'Not probed — contributes nothing to the score' : 'Deduction from the score' },
2026
+ st === 'notTested' ? '—' : (ded > 0 ? `−${ded}` : '0')),
2027
+ el('span', { class: 'dim-detail' }, d.detail || ''));
2028
+ })));
2029
+ if (notTested) {
2030
+ main.push(el('p', { class: 'fineprint', style: 'margin-top:10px' },
2031
+ `${notTested} dimension${notTested === 1 ? ' was' : 's were'} not probed this session — reported honestly rather than scored from an assumption.`));
2032
+ }
2033
+ }
2034
+
2035
+ const fleet = Array.isArray(mem.fleet) ? mem.fleet : [];
2036
+ if (fleet.length) {
2037
+ main.push(el('details', { class: 'sub' },
2038
+ el('summary', {}, `Across your ${fleet.length} project${fleet.length === 1 ? '' : 's'} — every memory store we found`),
2039
+ el('div', { class: 'sub-body' },
2040
+ el('div', { class: 'scroll-x' },
2041
+ el('table', { class: 'tb' },
2042
+ el('thead', {}, el('tr', {},
2043
+ el('th', { scope: 'col' }, 'Project'), el('th', { scope: 'col' }, 'Entries'),
2044
+ el('th', { scope: 'col' }, 'Embedded'), el('th', { scope: 'col' }, 'Patterns'),
2045
+ el('th', { scope: 'col' }, 'Learns'), el('th', { scope: 'col' }, 'Findings'))),
2046
+ el('tbody', {}, fleet.map((f) => el('tr', {},
2047
+ el('td', { class: 'cell-name' }, f.name || '—'),
2048
+ el('td', { class: 'cell-mono num' }, fmtInt(f.total)),
2049
+ el('td', { class: 'cell-mono num' }, f.coverPct != null ? `${f.coverPct}%` : '—'),
2050
+ el('td', { class: 'cell-mono num' }, fmtInt(f.patterns)),
2051
+ el('td', {}, f.learns ? chip('yes', 'green') : chip('no', 'grey')),
2052
+ el('td', { class: 'cell-dim' },
2053
+ Array.isArray(f.findings) && f.findings.length ? f.findings.join('; ') : '—'),
2054
+ ))))))));
2055
+ }
2056
+
2057
+ body.replaceChildren(withIllo('memory', ...main));
2058
+ }
2059
+
2060
+ /* ---------------------------------------------------------- section 5: savings */
2061
+
2062
+ /* Router panel (rebuilt 2026-07-16). The old panel displayed router-optimizer.mjs — a parallel,
2063
+ subscription-blind re-derivation of routing strategy that bypassed the real engine and told a
2064
+ Max subscriber to PAY for a worse model than the Sonnet 5 their plan covers. The replica is
2065
+ deleted. This panel renders only the ENGINE'S OWN truth: who decides (@metaharness/router —
2066
+ rUv's learned cost-optimal router — or a loudly-announced cold-start), the real candidate pool
2067
+ with THIS user's marginal prices ($0 where the subscription covers it), and the engine's own
2068
+ recent decisions from its append-only log. Nothing shown here can disagree with what routes. */
2069
+ function renderRouterEngine(re) {
2070
+ if (!re || !re.engine) return null;
2071
+ const money = (v) => (v == null ? '—' : v === 0 ? '$0' : '$' + v + '/Mtok');
2072
+ const eng = re.engine;
2073
+
2074
+ const modeChip =
2075
+ eng.mode === 'LEARNED' ? chip(`learned · ${eng.labels} real outcomes`, 'green')
2076
+ : eng.mode === 'COLD-START' ? chip(`cold-start · ${eng.labels} of ${eng.needed} labels`, 'warn')
2077
+ : chip('router package missing', 'red');
2078
+
2079
+ const engineLine = el('div', { class: 'rp-house' },
2080
+ el('span', { class: 'rp-house-tag' }, 'Who decides'),
2081
+ el('b', { class: 'rp-house-name' }, '@metaharness/router'),
2082
+ el('span', { class: 'rp-house-src' }, 'rUv’s learned cost-optimal router — the Brain adds only your constraints'),
2083
+ modeChip);
2084
+
2085
+ const modeNote =
2086
+ eng.mode === 'COLD-START' ? el('p', { class: 'rp-split' },
2087
+ 'It routes by learning from ', el('b', {}, 'your real outcomes'), ' — it has ',
2088
+ el('b', {}, String(eng.labels)), ' of the ', el('b', {}, String(eng.needed)),
2089
+ ' labelled examples it needs before its predictions count. Until then it says so and falls back — every routed task teaches it. This stops being a fallback with use.')
2090
+ : eng.mode === 'UNAVAILABLE' ? el('p', { class: 'rp-split' },
2091
+ 'The router package isn’t installed here — nothing is silently substituted in its place. ',
2092
+ el('span', { class: 'cell-mono' }, 'npm i @metaharness/router'), ' restores it.')
2093
+ : null;
2094
+
2095
+ // Dev/Prod are LENSES over the engine's one pool — a filter and a price column, never a second
2096
+ // strategy (Stuart 2026-07-16: "not sure I'm seeing dev vs production"). Development = you, in
2097
+ // Claude Code, where covered models are $0 marginal. Production = your deployed app on metered
2098
+ // APIs, where a personal subscription cannot apply and list price is the real cost.
2099
+ const pool = Array.isArray(re.pool) ? re.pool : [];
2100
+ const TIER_ORDER = { mechanical: 0, cheap: 1, mid: 2, frontier: 3 };
2101
+ const byTier = (a, b) => (TIER_ORDER[a.tier] ?? 9) - (TIER_ORDER[b.tier] ?? 9);
2102
+ const lensTable = (rows, costOf, costHead) => el('div', { class: 'scroll-x' },
2103
+ el('table', { class: 'tb rp-tb' },
2104
+ el('thead', {}, el('tr', {},
2105
+ el('th', { scope: 'col' }, 'Bucket'), el('th', { scope: 'col' }, 'Model'),
2106
+ el('th', { scope: 'col' }, costHead))),
2107
+ el('tbody', {}, rows.map((p) => el('tr', {},
2108
+ el('td', { class: 'rp-band' }, p.tier || '—'),
2109
+ el('td', {}, el('div', { class: 'rp-model' }, prettyModel(p.id))),
2110
+ el('td', { class: 'cell-mono num' }, costOf(p)),
2111
+ )))));
2112
+ // Development: only what this machine's harness can launch; best (cheapest-marginal) per bucket.
2113
+ const bestPerTier = (rows, price) => {
2114
+ const seen = {};
2115
+ for (const p of rows) {
2116
+ const k = p.tier || '?';
2117
+ if (!seen[k] || price(p) < price(seen[k])) seen[k] = p;
2118
+ }
2119
+ return Object.values(seen).sort(byTier);
2120
+ };
2121
+ const devRows = bestPerTier(
2122
+ pool.filter((p) => (p.harness || []).includes('claude-code')),
2123
+ (p) => (p.subscriptionCovered ? -1 : p.marginalPerMTok ?? Infinity));
2124
+ const prodRows = bestPerTier(
2125
+ pool.filter((p) => p.listPerMTok != null && p.provider !== 'local'),
2126
+ (p) => p.listPerMTok ?? Infinity);
2127
+ const devBlock = el('div', { class: 'rp-profile' },
2128
+ el('div', { class: 'rp-head' },
2129
+ el('span', { class: 'rp-name' }, 'Development'),
2130
+ el('span', { class: 'rp-obj' }, 'you, in Claude Code — models your plan covers win at $0 marginal')),
2131
+ lensTable(devRows,
2132
+ (p) => (p.subscriptionCovered ? el('b', { title: 'covered by your subscription — zero marginal cost' }, '$0 · yours') : money(p.marginalPerMTok)),
2133
+ 'Your cost'));
2134
+ const prodBlock = el('div', { class: 'rp-profile' },
2135
+ el('div', { class: 'rp-head' },
2136
+ el('span', { class: 'rp-name' }, 'Production'),
2137
+ el('span', { class: 'rp-obj' }, 'your deployed app on metered APIs — a personal plan can’t apply there')),
2138
+ lensTable(prodRows, (p) => money(p.listPerMTok), 'API price'));
2139
+ const lensGrid = el('div', { class: 'rp-grid' }, devBlock, prodBlock);
2140
+ const poolFoot = el('p', { class: 'fineprint' },
2141
+ re.catalogSource === 'built-in-fallback'
2142
+ ? `No personal catalog found — showing a minimal built-in set of ${pool.length}. Run \`node scripts/model-router-setup.mjs\` to build your real catalog, then the engine weighs yours on every call.`
2143
+ : `Best pick per bucket shown; the engine weighs all ${pool.length} candidates in its catalog on every call — nothing is retired by being off this summary.`);
2144
+
2145
+ // Decisions: dedupe consecutive identical picks, keep 3, humanize the reason head. The full
2146
+ // append-only log stays on disk — this is a pulse, not a table of record.
2147
+ const decisionsRaw = Array.isArray(re.decisions) ? re.decisions : [];
2148
+ const decisions = [];
2149
+ for (const d of decisionsRaw) {
2150
+ const prev = decisions[decisions.length - 1];
2151
+ if (prev && prev.model === d.model && prev.routedBy === d.routedBy) continue;
2152
+ decisions.push(d);
2153
+ if (decisions.length >= 3) break;
2154
+ }
2155
+ const humanReason = (r) => {
2156
+ const s = String(r || '');
2157
+ if (s.includes('predicted quality')) return s.match(/predicted quality [\d.]+/)?.[0] + (s.includes('clears') ? ' — clears the bar' : '');
2158
+ if (s.includes('NOT a tuned heuristic')) return 'starter policy while the router learns — prefers your covered models';
2159
+ return s.split('—')[0].split(';')[0].slice(0, 90);
2160
+ };
2161
+ const decRow = (d) => el('div', { class: 'dec-row' },
2162
+ el('div', { class: 'dec-top' },
2163
+ el('b', { class: 'dec-model' }, prettyModel(d.model)),
2164
+ (String(d.routedBy || '').startsWith('@metaharness/router')
2165
+ ? chip('rUv’s router', 'green') : chip('learning fallback', 'warn')),
2166
+ el('span', { class: 'dec-when cell-mono cell-dim' }, d.ts ? String(d.ts).slice(5, 16).replace('T', ' ') : '—')),
2167
+ el('div', { class: 'dec-why cell-dim' }, humanReason(d.reason)));
2168
+ const lastTs = decisionsRaw[0] && decisionsRaw[0].ts ? new Date(decisionsRaw[0].ts) : null;
2169
+ const daysQuiet = lastTs ? Math.floor((Date.now() - lastTs.getTime()) / 86400000) : null;
2170
+ const decisionsBlock = decisions.length ? el('div', { class: 'mh-dist' },
2171
+ el('p', { class: 'dist-ladder' }, 'Latest real decisions — from the engine’s own log, not simulated',
2172
+ daysQuiet > 1 ? el('span', { class: 'cell-dim' }, ` · quiet for ${daysQuiet} days — turn on smart routing above to feed it daily`) : null),
2173
+ ...decisions.map(decRow)) : null;
2174
+
2175
+ const keyLine = re.keys && re.keys.openrouter
2176
+ ? el('span', {}, 'OpenRouter key detected — metered cross-provider candidates are reachable.')
2177
+ : el('span', {}, 'No OpenRouter key — only subscription and local candidates are reachable. ',
2178
+ el('a', { class: 'rp-getkey', href: 'https://openrouter.ai/keys', target: '_blank', rel: 'noopener' }, 'Create one →'));
2179
+
2180
+ const constraintLine = re.profile && re.profile.present
2181
+ ? el('p', { class: 'rp-split' }, el('b', {}, 'Your constraints, applied as data: '),
2182
+ 'models your subscription covers enter the pool at ', el('b', {}, '$0 marginal'),
2183
+ ' — so the cost-optimal math prefers what you already pay for. Cheapest real cost first; frontier only when the work earns it.')
2184
+ : el('p', { class: 'rp-split' }, 'No personal profile yet — run ',
2185
+ el('span', { class: 'cell-mono' }, 'node scripts/model-router-setup.mjs'),
2186
+ ' so the router knows which models your plan already covers.');
2187
+
2188
+ return el('details', { class: 'mh-profiles' },
2189
+ el('summary', { class: 'rp-summary' },
2190
+ el('span', { class: 'rp-sum-t' }, 'Who routes your work — and with what', infoBtn('Who routes your work', ROUTER_ENGINE_INFO)),
2191
+ el('span', { class: 'rp-sum-s' }, `rUv’s learned router · your prices · ${eng.mode.toLowerCase().replace('-', ' ')}`),
2192
+ el('span', { class: 'rp-chev', 'aria-hidden': 'true' }, '›')),
2193
+ el('div', { class: 'rp-body' },
2194
+ engineLine,
2195
+ modeNote,
2196
+ constraintLine,
2197
+ lensGrid,
2198
+ poolFoot,
2199
+ decisionsBlock,
2200
+ el('p', { class: 'rp-foot fineprint' },
2201
+ 'Candidate pool = the engine’s own catalog × your profile (', re.profile ? re.profile.path : '', '). ',
2202
+ keyLine)));
2203
+ }
2204
+
2205
+ const MODEL_PRETTY = {
2206
+ 'claude-fable-5': 'Fable 5', 'claude-opus-4.8': 'Opus 4.8', 'claude-sonnet-5': 'Sonnet 5',
2207
+ 'claude-haiku-4.5': 'Haiku 4.5', 'agent-booster': 'Agent Booster',
2208
+ 'inclusionai/ling-2.6-flash': 'Ling 2.6 Flash', 'openai/gpt-4.1': 'GPT-4.1',
2209
+ 'meta-llama/llama-3.3-70b-instruct': 'Llama 3.3 70B', 'x-ai/grok-4.5': 'Grok 4.5', 'x-ai/grok-4.3': 'Grok 4.3',
2210
+ 'deepseek/deepseek-chat': 'DeepSeek Chat', 'deepseek/deepseek-v4-flash': 'DeepSeek v4 Flash',
2211
+ 'z-ai/glm-4.6': 'GLM 4.6', 'z-ai/glm-5': 'GLM 5',
2212
+ // house frontiers / ladders (per-provider personalization)
2213
+ 'openai/gpt-5.6-sol': 'GPT-5.6 Sol', 'openai/gpt-5.6-terra': 'GPT-5.6 Terra', 'openai/gpt-5.6-luna': 'GPT-5.6 Luna',
2214
+ 'google/gemini-3.1-pro-preview': 'Gemini 3.1 Pro', 'google/gemini-3.5-flash': 'Gemini 3.5 Flash', 'google/gemini-3.1-flash-lite': 'Gemini 3.1 Flash-Lite',
2215
+ };
2216
+ const HOUSE_SOURCE_NOTE = {
2217
+ config: 'you set this',
2218
+ env: 'detected from your API keys',
2219
+ default: 'you’re running Claude Code, so this is your dev house — set your production house in Settings',
2220
+ };
2221
+ // Friendly labels for the model-house selector — the stored value stays the id (anthropic/openai/…).
2222
+ const SEG_LABEL = {
2223
+ provider: { auto: 'Auto', anthropic: 'Claude', openai: 'ChatGPT', codex: 'Codex', google: 'Gemini', xai: 'Grok' },
2224
+ advocacy: { off: 'Off', 'important-only': 'Important only', all: 'All' },
2225
+ };
2226
+ const segLabel = (key, opt) => (SEG_LABEL[key] && SEG_LABEL[key][opt]) || opt;
2227
+ const prettyModel = (id) => {
2228
+ if (!id) return '—';
2229
+ if (MODEL_PRETTY[id]) return MODEL_PRETTY[id];
2230
+ // Fallback prettifier: drop provider prefix + date-pinned suffixes ("claude-haiku-4-5-20251001"
2231
+ // must never render raw — Stuart called the wall of ids a mess), title-case the words.
2232
+ const base = String(id).split('/').pop().replace(/-\d{8}$/, '');
2233
+ return base.split('-').map((w) => (/^\d/.test(w) ? w.replace(/-/g, '.') : w[0].toUpperCase() + w.slice(1)))
2234
+ .join(' ').replace(/(\d) (\d)/g, '$1.$2');
2235
+ };
2236
+
2237
+ // The ONGOING view: once real tasks have been routed, how many landed in each band and what that
2238
+ // saved vs sending them all to the frontier model. Driven entirely by measured receipts.
2239
+ function renderDistribution(u) {
2240
+ if (!u || !u.tasks) return null;
2241
+ const frontierName = prettyModel(u.frontierModel);
2242
+ const tone = { mechanical: 'b-mech', cheap: 'b-cheap', mid: 'b-mid', frontier: 'b-front' };
2243
+ const dist = Array.isArray(u.distribution) ? u.distribution : [];
2244
+ const active = dist.filter((d) => d.tasks > 0);
2245
+ const frontierBand = dist.find((d) => d.band === 'frontier');
2246
+ const frontierIdle = !!frontierBand && !frontierBand.tasks;
2247
+ const saved = (u.frontierUsd != null && u.realizedUsd != null) ? u.frontierUsd - u.realizedUsd : null;
2248
+
2249
+ // Verdict first: the money saved is the headline, everything else supports it.
2250
+ const hero = el('div', { class: 'dv-hero' },
2251
+ saved != null
2252
+ ? el('div', { class: 'dv-hero-num' }, fmtUsd(saved), el('span', { class: 'dv-hero-word' }, ' saved'))
2253
+ : null,
2254
+ el('p', { class: 'dv-hero-sub' },
2255
+ 'across ', el('b', {}, u.tasks + ' routed ' + (u.tasks === 1 ? 'task' : 'tasks')),
2256
+ frontierIdle ? el('span', {}, ' · frontier never fired') : null,
2257
+ (frontierBand && frontierBand.tasks > 0)
2258
+ ? el('span', {}, ' · ' + frontierBand.tasks + ' escalated to ' + frontierName) : null),
2259
+ (u.frontierUsd != null && u.realizedUsd != null)
2260
+ ? el('p', { class: 'dv-hero-math' },
2261
+ frontierName + ' for everything would have cost ' + fmtUsd(u.frontierUsd) +
2262
+ ' — you actually spent ' + fmtUsd(u.realizedUsd) + '.')
2263
+ : null);
2264
+
2265
+ // ONE continuous stacked bar — the mix in a single glance. Widths exactly
2266
+ // proportional to task counts (flex-grow), band colours carried by tone class.
2267
+ const bar = active.length
2268
+ ? el('div', {
2269
+ class: 'dv-bar', role: 'img',
2270
+ 'aria-label': 'Task mix: ' + active.map((d) => `${d.label} ${d.pctOfTasks}%`).join(', '),
2271
+ },
2272
+ ...active.map((d) => el('div', {
2273
+ class: 'dv-seg ' + tone[d.band],
2274
+ style: 'flex:' + d.tasks + ' 1 0%',
2275
+ title: `${d.label} — ${d.tasks} ${d.tasks === 1 ? 'task' : 'tasks'} (${d.pctOfTasks}%)` +
2276
+ (d.savedUsd > 0 ? ` · saved ${fmtUsd(d.savedUsd)}` : ''),
2277
+ },
2278
+ d.pctOfTasks >= 15 ? el('span', { class: 'dv-seg-lab' }, `${d.label} ${d.pctOfTasks}%`) : null)))
2279
+ : null;
2280
+
2281
+ // Compact legend: only bands that fired, each with its models + what it saved.
2282
+ const legendRows = active.map((d) => {
2283
+ const models = d.models.length
2284
+ ? d.models.map((m) => prettyModel(m.model) + (m.tasks > 1 ? ' ×' + m.tasks : '')).join(', ')
2285
+ : null;
2286
+ return el('div', { class: 'dv-leg-row' },
2287
+ el('span', { class: 'dv-dot ' + tone[d.band], 'aria-hidden': 'true' }),
2288
+ el('span', { class: 'dv-leg-band ' + tone[d.band] }, d.label),
2289
+ el('span', { class: 'dv-leg-meta cell-dim' },
2290
+ `${d.tasks} ${d.tasks === 1 ? 'task' : 'tasks'} · ${d.pctOfTasks}%` + (models ? ' — ' + models : '')),
2291
+ el('span', { class: 'dv-leg-saved num' }, d.savedUsd > 0 ? 'saved ' + fmtUsd(d.savedUsd) : ''));
2292
+ });
2293
+ // Frontier at zero is the punchline, not missing data — say so where its row would be.
2294
+ if (frontierIdle) {
2295
+ legendRows.push(el('div', { class: 'dv-leg-row dv-leg-punch' },
2296
+ el('span', { class: 'dv-check', 'aria-hidden': 'true' }, '✓'),
2297
+ el('span', { class: 'dv-leg-band b-front' }, frontierBand.label),
2298
+ el('span', { class: 'dv-leg-meta cell-dim' },
2299
+ el('b', {}, 'never fired'), ' — escalation is last resort by design'),
2300
+ el('span', { class: 'dv-leg-saved num' }, '')));
2301
+ }
2302
+
2303
+ return el('div', { class: 'mh-dist dv-wrap' },
2304
+ hero, bar,
2305
+ el('div', { class: 'dv-legend' }, ...legendRows),
2306
+ el('p', { class: 'fineprint' }, u.note));
2307
+ }
2308
+
2309
+ // Plan block (issue #24, applying sparkling's #21 redesign) — three genuinely different questions
2310
+ // used to render as one flat row of identical "chips": which subscription this runs on ("house" — a
2311
+ // word nobody outside the source knows), whether cheap-task routing is on, and which OTHER API keys
2312
+ // merely exist. Now two plainly-labelled boxes: YOUR PLAN (the one choice that changes cost, with a
2313
+ // real per-provider key checklist) and OPENROUTER (a separate switch, not another house).
2314
+ function renderProviders(sv) {
2315
+ const re = sv && sv.routerEngine;
2316
+ if (!re) return null;
2317
+ // House = the user's Settings choice (config.json `provider`), the single source of truth (issue
2318
+ // #21). re.house + re.keys come from onboarding-console.mjs's gatherRouterEngine(); re.keys now
2319
+ // carries a real per-provider credential check (issue #24), not a hardcoded false.
2320
+ const HOUSE_NAME = { anthropic: 'Claude Max', openai: 'ChatGPT', codex: 'Codex', google: 'Gemini', xai: 'Grok' };
2321
+ const KEY_NAME = { anthropic: 'Claude', openai: 'OpenAI', google: 'Gemini', xai: 'Grok' };
2322
+ const house = { provider: re.house && re.house.provider };
2323
+ const houseName = HOUSE_NAME[house.provider] || 'Your stack';
2324
+ const keys = re.keys || {};
2325
+
2326
+ const action = (label, target) => el('button', {
2327
+ class: 'plan-action', type: 'button', onclick: () => jumpToSetting(target),
2328
+ }, label);
2329
+ const head = (dotClass, name, actionBtn) => el('div', { class: 'plan-head' },
2330
+ el('span', { class: `plan-dot ${dotClass}`, 'aria-hidden': 'true' }),
2331
+ el('span', { class: 'plan-name' }, name), actionBtn);
2332
+
2333
+ // BOX 1 — YOUR PLAN. The one choice here that changes cost: which subscription MetaHarness treats
2334
+ // as $0. Footer checklist: which OTHER providers have a real API key on this machine — honest now.
2335
+ const others = ['anthropic', 'openai', 'google', 'xai'].filter((id) => id !== house.provider);
2336
+ const checklist = el('div', { class: 'plan-keys' },
2337
+ el('span', { class: 'plan-keys-lab' }, 'Other keys found:'),
2338
+ ...others.map((id) => {
2339
+ const ok = !!keys[id];
2340
+ return el('span', {
2341
+ class: `plan-key ${ok ? 'yes' : 'no'}`,
2342
+ title: ok ? `An API key for ${KEY_NAME[id]} is set on this machine`
2343
+ : `No API key for ${KEY_NAME[id]} found on this machine`,
2344
+ },
2345
+ el('span', { class: 'plan-key-mark', 'aria-hidden': 'true' }, ok ? '✓' : '✗'),
2346
+ ` ${KEY_NAME[id]}`);
2347
+ }));
2348
+ const planBox = el('div', { class: 'plan-box' },
2349
+ el('span', { class: 'plan-label' }, 'Your plan', infoBtn('Your plan and OpenRouter', PROVIDERS_INFO)),
2350
+ head('is-house', houseName, action('Change', 'provider')),
2351
+ el('p', { class: 'plan-sub' }, `MetaHarness's main work runs here at no extra cost.`),
2352
+ checklist);
2353
+
2354
+ // BOX 2 — OPENROUTER. A separate switch, not another house: Claude Code itself never leaves your
2355
+ // plan; this only offloads read-only text tasks (summarize, classify) to cheaper models.
2356
+ const routerOn = !!keys.openrouter;
2357
+ const laneBox = el('div', { class: 'plan-box' },
2358
+ el('span', { class: 'plan-label' }, 'OpenRouter'),
2359
+ head(`is-lane ${routerOn ? 'on' : 'off'}`, routerOn ? 'Active' : 'No key added',
2360
+ action(routerOn ? 'Manage' : 'Add a key', 'openrouterKey')),
2361
+ el('p', { class: 'plan-sub' }, routerOn
2362
+ ? `Text-only tasks like summarizing or classifying can go to cheaper models instead of using ${houseName}.`
2363
+ : `Add one so text-only tasks can skip ${houseName} and use cheaper models instead.`));
2364
+
2365
+ return el('div', { class: 'plan-group' }, planBox, laneBox);
2366
+ }
2367
+
2368
+ function renderSavings(sv) {
2369
+ const body = $('#body-savings');
2370
+ const totals = sv && sv.totals;
2371
+ const util = sv && sv.utilization && sv.utilization.tasks ? sv.utilization : null;
2372
+ const receipts = sv && Array.isArray(sv.receipts) ? sv.receipts : [];
2373
+
2374
+ // The pitch is the action — always shown, whether or not routing is on yet. issue #20: the CTA
2375
+ // must reflect the SAVED config (sv.routing), not a fresh pristine "Turn on" pitch on every render
2376
+ // — otherwise a successful click reads as a lie the moment the page reloads. ctaSlot is a stable
2377
+ // container; paintCta(on) repaints it for the current state and is called again after a successful
2378
+ // save, so the toggle is visibly live, not just correct after a reload.
2379
+ const ctaSlot = el('div', { class: 'mh-cta' });
2380
+ // A PREFERENCE IS NOT A CAPABILITY, and this chip claimed one while the Capabilities card measured
2381
+ // the other. On a fresh machine the page said "✓ Smart routing: ON" here, "cheap-model-routing:
2382
+ // absent — agentic-flow is not installed" there, and "Off by default" in the subtitle between them.
2383
+ // The server now sends whether the tool is actually installed, so the chip can only ever say what
2384
+ // is simultaneously true of the preference AND the machine.
2385
+ const installed = !!(sv && sv.routingInstalled);
2386
+
2387
+ // A failed save must never read as a success. postJSON does NOT throw on a non-2xx — a 403 from a
2388
+ // stale token (a known live case, handled explicitly elsewhere in this file) or an {ok:false} body
2389
+ // both landed in the `else` branch, which wrote "Saved." into an aria-live region and announced it
2390
+ // to screen readers while nothing had changed and the button re-enabled itself.
2391
+ const failed = (btn, note) => ({ status, data }) => {
2392
+ btn.disabled = false;
2393
+ note.textContent = status === 403
2394
+ ? 'The console was restarted — reload this page and try again.'
2395
+ : `Couldn’t save${data && data.log ? ` — ${data.log}` : ' — change it under Settings.'}`;
2396
+ };
2397
+
2398
+ function paintCta(state) {
2399
+ const note = el('span', { class: 'mh-enable-note', 'aria-live': 'polite' }, '');
2400
+ const save = (btn, values, onOk) => async () => {
2401
+ btn.disabled = true; note.textContent = 'saving…';
2402
+ try {
2403
+ const r = await postJSON('/api/save-config', values);
2404
+ if (r.ok && r.data && r.data.ok) onOk();
2405
+ else failed(btn, note)(r);
2406
+ } catch { btn.disabled = false; note.textContent = 'Couldn’t save — change it under Settings.'; }
2407
+ };
2408
+
2409
+ if (state === 'auto') {
2410
+ const offBtn = el('button', { class: 'btn btn-ghost btn-sm', type: 'button' }, 'Turn off');
2411
+ offBtn.addEventListener('click', save(offBtn, { values: { routing: 'off' } }, () => paintCta('off')));
2412
+ ctaSlot.replaceChildren(
2413
+ installed
2414
+ ? chip('✓ Smart routing: ON', 'green', 'You chose smart routing, and agentic-flow is installed to carry it out')
2415
+ : chip('Smart routing: chosen, not installed', 'wait', 'You chose smart routing, but agentic-flow is not installed on this machine, so nothing is being routed yet'),
2416
+ offBtn, note);
2417
+ return;
2418
+ }
2419
+
2420
+ const enableBtn = el('button', { class: 'mh-enable', type: 'button' }, 'Turn on smart routing');
2421
+ enableBtn.addEventListener('click', save(enableBtn, { values: { routing: 'auto' } }, () => paintCta('auto')));
2422
+ ctaSlot.replaceChildren(
2423
+ // "not chosen" and "chosen off" are different facts, and only one of them is a decision.
2424
+ state === 'off'
2425
+ ? chip('Smart routing: off', 'grey', 'You turned smart routing off')
2426
+ : chip('Smart routing: not chosen yet', 'wait', 'Nobody has answered this yet — it is neither on nor off'),
2427
+ enableBtn, note);
2428
+ }
2429
+ paintCta(sv && (sv.routing === 'auto' ? 'auto' : sv.routing === 'off' ? 'off' : null));
2430
+ // Stuart: "If you haven't ever seen MetaHarness, you have no idea what the word means, and you have
2431
+ // no idea what you should expect it to do. Saying 'do you want to use it or not' without any visual
2432
+ // explaining what it does is a little challenging." He is right — this card asked for a decision
2433
+ // before it earned understanding, and seven lines of prose is not how anyone learns a new word.
2434
+ // rUv called this exact risk in ADR-076: define the term in the first screen or it reads as jargon;
2435
+ // mitigation = the one-line gloss + the four-pillar framing. The diagram IS that mitigation, so the
2436
+ // paragraph it replaces is gone rather than sitting above it saying the same thing more slowly.
2437
+ const pitch = el('div', { class: 'mh-pitch' },
2438
+ el('p', { class: 'mh-lead' },
2439
+ el('b', {}, 'MetaHarness'), ' tunes everything wrapped around your model — the planning, the ',
2440
+ 'context, the retries, which model each task goes to — and keeps only the changes that ',
2441
+ el('b', {}, 'measurably win'), '. The model itself never changes. rUv leaves it ',
2442
+ el('b', {}, 'off by default'), ' on purpose: he’d rather you choose it than have it forced on you.',
2443
+ infoBtn('Smart model routing', SAVINGS_INFO)),
2444
+ el('figure', { class: 'mh-diagram' },
2445
+ el('img', {
2446
+ // NOT lazy: at 6.5KB this saves nothing, and the card sits far enough down the page that the
2447
+ // lazy threshold never fires — the diagram simply never appeared. Verified: no network
2448
+ // request at all with loading="lazy", even after scrolling the card into view.
2449
+ src: 'assets/metaharness.svg', width: '900', height: '470',
2450
+ alt: 'MetaHarness: the model sits frozen at the centre while seven policy surfaces around it — '
2451
+ + 'planner, contextBuilder, reviewer, retryPolicy, toolPolicy, memoryPolicy and scorePolicy — '
2452
+ + 'are each mutated and measured. Four pillars run underneath: route, evolve, orchestrate, '
2453
+ + 'verify. Per agentic-flow ADR-076: 28.5% cheaper at 98.1% bar-compliance.',
2454
+ })),
2455
+ ctaSlot);
2456
+
2457
+ const blocks = [];
2458
+ const prov = renderProviders(sv);
2459
+ if (prov) blocks.push(prov);
2460
+ blocks.push(pitch);
2461
+
2462
+ if (!util && !receipts.length) {
2463
+ setChips('chips-savings', [chip('nothing routed yet', 'wait')]);
2464
+ // WP2b — the first-run state is a confident promise, not an apology.
2465
+ blocks.push(el('div', { class: 'mh-empty' },
2466
+ el('p', { class: 'mh-empty-title' }, 'Nothing routed yet — that’s expected.'),
2467
+ el('p', { class: 'mh-empty-body' },
2468
+ 'Turn it on, work normally for a week, then come back. You’ll see exactly what you saved by not sending everything to the most expensive frontier model — every number here will be a ',
2469
+ el('b', {}, 'real receipt'), ', never a projection.')));
2470
+ // Even before any task runs, show what the router WOULD choose per bucket — the plan is real.
2471
+ const rp0 = renderRouterEngine(sv && sv.routerEngine);
2472
+ if (rp0) blocks.push(rp0);
2473
+ body.replaceChildren(withIllo('savings', ...blocks));
2474
+ return;
2475
+ }
2476
+
2477
+ // Headline numbers come from the measured utilization (recomputed vs the current frontier, Fable 5).
2478
+ const frontierName = util ? prettyModel(util.frontierModel) : 'the frontier';
2479
+ const pct = util ? util.pctSaved
2480
+ : (totals && totals.pctSaved != null ? totals.pctSaved
2481
+ : (totals && totals.baselineUsd ? Math.round((totals.usdSaved / totals.baselineUsd) * 100) : null));
2482
+ const savedUsd = util ? util.costOptimalitySaved : (totals ? totals.usdSaved : 0);
2483
+ const taskCount = util ? util.tasks : (totals ? totals.count : 0);
2484
+
2485
+ setChips('chips-savings', [
2486
+ pct != null ? chip(`${pct}% saved`, 'green') : chip(`${fmtUsd(savedUsd)} saved`, 'green'),
2487
+ chip(`${fmtInt(taskCount)} routed`, 'grey'),
2488
+ ]);
2489
+
2490
+ // The distribution hero states the same four numbers ($ saved, tasks, frontier-if-all, actual
2491
+ // spend) at a size you can read across the room, and the chip carries the %. Rendering the tiles
2492
+ // above it too would say $15.17 twice on one card — the "everything at the same weight" problem
2493
+ // this card was just rebuilt to fix, wearing a different hat. So the strip is now the FALLBACK:
2494
+ // it only appears when there are no receipts yet and the hero has nothing to say.
2495
+ const dist = renderDistribution(util);
2496
+ if (!dist) {
2497
+ blocks.push(el('div', { class: 'totals-strip' },
2498
+ el('div', { class: 'total-tile t-green' },
2499
+ el('div', { class: 'total-num' }, pct != null ? `${pct}%` : fmtUsd(savedUsd)),
2500
+ el('div', { class: 'total-lab' }, `saved vs ${frontierName}`)),
2501
+ el('div', { class: 'total-tile' },
2502
+ el('div', { class: 'total-num' }, fmtUsd(savedUsd)),
2503
+ el('div', { class: 'total-lab' }, '$ kept')),
2504
+ el('div', { class: 'total-tile' },
2505
+ el('div', { class: 'total-num' }, fmtInt(taskCount)),
2506
+ el('div', { class: 'total-lab' }, 'tasks routed')),
2507
+ el('div', { class: 'total-tile' },
2508
+ el('div', { class: 'total-num' }, util ? fmtUsd(util.frontierUsd) : (totals && totals.msSaved >= 0 ? fmtMs(totals.msSaved) : '—')),
2509
+ el('div', { class: 'total-lab' }, util ? `if all on ${frontierName}` : 'time saved'))));
2510
+ }
2511
+
2512
+ // WP2a — provenance, worn openly: these numbers are receipts, not projections.
2513
+ const receiptCount = util ? util.tasks : (totals && totals.count != null ? totals.count : receipts.length);
2514
+ blocks.push(el('p', { class: 'prov-badge' },
2515
+ el('span', { class: 'prov-dot', 'aria-hidden': 'true' }),
2516
+ el('span', {}, 'real numbers — recomputed from your ',
2517
+ el('b', {}, `${fmtInt(receiptCount)} receipt${receiptCount === 1 ? '' : 's'}`),
2518
+ ', never projected')));
2519
+
2520
+ // The distribution — how many tasks went to each bucket, and the saved-vs-frontier math.
2521
+ // (computed above, so the totals-strip can stand down when this hero is doing the talking)
2522
+ if (dist) blocks.push(dist);
2523
+
2524
+ // Full receipt detail, collapsed so the summary stays clean for a first-time reader.
2525
+ if (receipts.length) {
2526
+ blocks.push(el('details', { class: 'mh-receipts' },
2527
+ el('summary', { class: 'rp-summary' },
2528
+ el('span', { class: 'rp-sum-t' }, 'Every routed task'),
2529
+ el('span', { class: 'rp-sum-s' },
2530
+ totals && totals.count > receipts.length
2531
+ ? `${fmtInt(totals.count)} measured receipts — showing the ${fmtInt(receipts.length)} newest`
2532
+ : `${fmtInt(receipts.length)} measured receipt${receipts.length === 1 ? '' : 's'} · newest first`),
2533
+ el('span', { class: 'rp-chev', 'aria-hidden': 'true' }, '›')),
2534
+ el('div', { class: 'scroll-x scroll-y' },
2535
+ el('table', { class: 'tb' },
2536
+ el('thead', {}, el('tr', {},
2537
+ el('th', { scope: 'col' }, 'When'), el('th', { scope: 'col' }, 'Routed to'),
2538
+ el('th', { scope: 'col' }, 'Instead of'), el('th', { scope: 'col' }, 'Task'),
2539
+ el('th', { scope: 'col' }, 'Saved'))),
2540
+ el('tbody', {}, receipts.map((r) => el('tr', {},
2541
+ el('td', { class: 'cell-mono cell-dim' }, fmtDate(r.at)),
2542
+ el('td', { class: 'cell-mono' }, r.chosenTier || '—'),
2543
+ el('td', { class: 'cell-mono cell-dim' }, r.baselineTier || '—'),
2544
+ el('td', { class: 'cell-dim' }, (r.task && r.task.length > 60) ? r.task.slice(0, 58) + '…' : (r.task || '—')),
2545
+ el('td', { class: 'cell-mono num' }, fmtUsd(r.measuredUsd)),
2546
+ )))))));
2547
+ }
2548
+
2549
+ // (sv.note used to render here as 11px fineprint — the provenance badge above replaced it.)
2550
+
2551
+ const rp = renderRouterEngine(sv.routerEngine);
2552
+ if (rp) blocks.push(rp);
2553
+
2554
+ body.replaceChildren(withIllo('savings', ...blocks));
2555
+ }
2556
+
2557
+ /* --------------------------------------------------------- section 6: settings */
2558
+
2559
+ // Shown wherever a control is sitting on a recommendation rather than on a stored answer.
2560
+ const notChosenField = (rec) => el('span', { class: 'field-unset' },
2561
+ 'Not chosen yet — this shows the recommended setting (', el('b', {}, String(rec)), '), not your machine’s.');
2562
+
2563
+ /**
2564
+ * WP4 — every row answers What / Why / How on click. Fields with hand-written copy in SETTING_INFO
2565
+ * use it verbatim; everything else builds its beats from what the SERVER actually sent — `help`,
2566
+ * and, where the schema carries them (user-settings.mjs entries do), `whyItMatters`/`downside` —
2567
+ * rather than inventing new copy here. That is how the advocacy dial gets its info bubble without
2568
+ * this file duplicating a single sentence user-settings.mjs already owns.
2569
+ */
2570
+ function fieldBeats(f) {
2571
+ if (SETTING_INFO[f.key]) return SETTING_INFO[f.key];
2572
+ const beats = [{ k: 'What is this?', t: f.help || 'A RuvNet Brain option, stored in your settings file.' }];
2573
+ if (f.whyItMatters) beats.push({ k: 'Why does it matter?', t: f.whyItMatters });
2574
+ if (f.downside) beats.push({ k: 'What’s the downside?', t: f.downside });
2575
+ return beats;
2576
+ }
2577
+
2578
+ /**
2579
+ * Build ONE field's control + its label/info/help row. Factored out of renderSettings so the
2580
+ * advocacy dial (backed by user-settings.mjs, a different file, a different endpoint) gets the exact
2581
+ * same widget — same markup, same "not chosen" honesty rule, same info-bubble mechanics — as every
2582
+ * config.json field, rather than a bespoke control invented for one setting.
2583
+ */
2584
+ function buildSettingsField(f, values, defaults, refreshDirty) {
2585
+ const labId = `lab-${f.key}`;
2586
+ const helpId = `help-${f.key}`;
2587
+ const ctl = el('div', { class: 'field-ctl' });
2588
+ let collector;
2589
+ let initialValue;
2590
+
2591
+ if (f.type === 'secret' || f.secret) {
2592
+ const isSet = values[f.key] === true;
2593
+ let input = null;
2594
+ const buildInput = () => {
2595
+ input = el('input', {
2596
+ type: 'password', class: 'text-input', autocomplete: 'off',
2597
+ spellcheck: 'false', placeholder: isSet ? 'Enter a new key to replace it' : 'Enter key',
2598
+ 'aria-labelledby': labId, 'aria-describedby': helpId,
2599
+ oninput: refreshDirty,
2600
+ });
2601
+ const showBtn = el('button', {
2602
+ class: 'btn btn-ghost btn-sm', type: 'button',
2603
+ onclick: () => {
2604
+ const showing = input.type === 'text';
2605
+ input.type = showing ? 'password' : 'text';
2606
+ showBtn.textContent = showing ? 'Show' : 'Hide';
2607
+ },
2608
+ }, 'Show');
2609
+ const row = el('div', { class: 'secret-input-row' }, input, showBtn);
2610
+ if (isSet) {
2611
+ row.append(el('button', {
2612
+ class: 'btn btn-ghost btn-sm', type: 'button',
2613
+ onclick: () => { row.replaceWith(buildSetRow()); input = null; refreshDirty(); },
2614
+ }, 'Keep existing'));
2615
+ }
2616
+ return row;
2617
+ };
2618
+ const buildSetRow = () => el('div', { class: 'secret-set-row' },
2619
+ el('span', { class: 'chip tone-green secret-set', title: 'A value is stored; it is never sent to this page.' }, '•••• set'),
2620
+ el('button', {
2621
+ class: 'btn btn-ghost btn-sm', type: 'button', 'aria-describedby': helpId,
2622
+ onclick: (e) => { const r = buildInput(); e.currentTarget.parentElement.replaceWith(r); input.focus(); refreshDirty(); },
2623
+ }, 'Replace…'));
2624
+ ctl.append(isSet ? buildSetRow() : buildInput());
2625
+ collector = () => ({ secret: true, include: !!(input && input.value.trim()), value: input ? input.value.trim() : undefined });
2626
+ } else if (f.type === 'bool') {
2627
+ // NOT CHOSEN IS NOT OFF. The server sends null when the user has never answered this question,
2628
+ // and a bare unchecked switch states "off" — a claim about their machine that nobody made. The
2629
+ // control still has to sit somewhere, so it sits on the RECOMMENDED value and says, in words,
2630
+ // that this is a recommendation and not their current setting.
2631
+ const chosen = values[f.key] === true || values[f.key] === false;
2632
+ const rec = defaults[f.key] === true;
2633
+ const input = el('input', { type: 'checkbox', 'aria-labelledby': labId, 'aria-describedby': helpId, onchange: refreshDirty });
2634
+ input.checked = chosen ? values[f.key] === true : rec;
2635
+ initialValue = input.checked;
2636
+ ctl.append(el('label', { class: 'switch' }, input, el('span', { class: 'track', 'aria-hidden': 'true' })));
2637
+ if (!chosen) ctl.append(notChosenField(rec ? 'on' : 'off'));
2638
+ collector = () => ({ include: true, value: input.checked });
2639
+ } else if (f.type === 'enum' && Array.isArray(f.options)) {
2640
+ const name = `seg-${f.key}`;
2641
+ const seg = el('div', { class: 'seg', role: 'radiogroup', 'aria-labelledby': labId, 'aria-describedby': helpId });
2642
+ const inputs = [];
2643
+ const chosen = f.options.some((opt) => Object.is(values[f.key], opt));
2644
+ const rec = f.options.includes(defaults[f.key]) ? defaults[f.key] : f.options[0];
2645
+ for (const opt of f.options) {
2646
+ const input = el('input', { type: 'radio', name, value: String(opt), onchange: refreshDirty });
2647
+ input.optionValue = opt;
2648
+ input.checked = chosen ? Object.is(values[f.key], opt) : Object.is(opt, rec);
2649
+ inputs.push(input);
2650
+ seg.append(el('label', {}, input, el('span', { class: 'seg-lab' }, segLabel(f.key, opt))));
2651
+ }
2652
+ // Falling back to options[0] and saying nothing is how "routing: auto" appeared to be the
2653
+ // user's setting on a machine whose config file did not exist.
2654
+ if (!inputs.some((i) => i.checked) && inputs[0]) inputs[0].checked = true;
2655
+ initialValue = inputs.find((i) => i.checked)?.optionValue;
2656
+ if (f.key === 'advocacy' && !chosen) {
2657
+ ctl.append(el('p', { class: 'field-consult', role: 'note' },
2658
+ 'First-time choice: how much should RuvNet Brain jump in unprompted? Choose 1–5. ',
2659
+ el('b', {}, '3 · Balanced is recommended'), ' and you can change it any time.'));
2660
+ }
2661
+ ctl.append(seg);
2662
+ if (!chosen) ctl.append(notChosenField(segLabel(f.key, rec)));
2663
+ collector = () => ({ include: true, value: inputs.find((i) => i.checked)?.optionValue });
2664
+ } else {
2665
+ const input = el('input', {
2666
+ type: 'text', class: 'text-input', 'aria-labelledby': labId, 'aria-describedby': helpId, oninput: refreshDirty,
2667
+ });
2668
+ input.value = values[f.key] != null && values[f.key] !== true ? String(values[f.key]) : '';
2669
+ initialValue = input.value;
2670
+ ctl.append(input);
2671
+ collector = () => ({ include: true, value: input.value });
2672
+ }
2673
+
2674
+ const row = el('div', { class: 'field', id: `field-${f.key}` },
2675
+ el('div', {},
2676
+ el('span', { class: 'field-label', id: labId }, f.label || f.key, infoBtn(f.label || f.key, fieldBeats(f))),
2677
+ f.help ? el('p', { class: 'field-help', id: helpId }, f.help) : el('span', { id: helpId })),
2678
+ ctl);
2679
+
2680
+ return { row, collector, initialValue };
2681
+ }
2682
+
2683
+ /**
2684
+ * Build one COMPLETE settings form — every field in `cfg.schema`, a Save button, and a submit
2685
+ * handler that POSTs to `endpoint`. Used twice: once for config.json's five fields, once for the
2686
+ * single advocacy field backed by user-settings.mjs — same widget, same save/undo/error handling,
2687
+ * only the endpoint and the file it names in its own copy differ.
2688
+ */
2689
+ function buildSettingsForm(cfg, { endpoint }) {
2690
+ const values = cfg.values || {};
2691
+ // What the project would pick FOR you, kept strictly apart from what you actually picked. The
2692
+ // server sends these separately for exactly that reason — see gatherConfig / gatherAdvocacy.
2693
+ const defaults = cfg.defaults || {};
2694
+
2695
+ const form = el('form', { class: 'settings-form', novalidate: true });
2696
+ const collectors = {}; // key → () => ({ include, value })
2697
+ const initial = {};
2698
+ let saveBtn;
2699
+ const resultSlot = el('div');
2700
+
2701
+ function isDirty() {
2702
+ for (const [key, get] of Object.entries(collectors)) {
2703
+ const g = get();
2704
+ if (g.secret) { if (g.include) return true; continue; }
2705
+ if (g.value !== initial[key]) return true;
2706
+ }
2707
+ return false;
2708
+ }
2709
+ function refreshDirty() { if (saveBtn) saveBtn.disabled = !isDirty(); }
2710
+
2711
+ for (const f of cfg.schema) {
2712
+ const { row, collector, initialValue } = buildSettingsField(f, values, defaults, refreshDirty);
2713
+ collectors[f.key] = collector;
2714
+ if (f.type !== 'secret' && !f.secret) initial[f.key] = initialValue;
2715
+ form.append(row);
2716
+ }
2717
+
2718
+ saveBtn = el('button', { class: 'btn btn-apply', type: 'submit', disabled: true }, 'Save settings');
2719
+ form.append(el('div', { class: 'save-row' },
2720
+ saveBtn,
2721
+ el('p', { class: 'save-note' },
2722
+ 'Saves to ', el('code', {}, cfg.path || '~/.claude/ruvnet-brain/config.json'),
2723
+ ' in your user folder. Each choice is enforced by its named runtime; encrypted secrets and scheduler state never live in this browser response.')),
2724
+ resultSlot);
2725
+
2726
+ form.addEventListener('submit', async (e) => {
2727
+ e.preventDefault();
2728
+ const out = {};
2729
+ for (const [key, get] of Object.entries(collectors)) {
2730
+ const g = get();
2731
+ if (g.include) out[key] = g.value; // untouched secrets are simply absent
2732
+ }
2733
+ saveBtn.disabled = true;
2734
+ const prev = saveBtn.textContent;
2735
+ saveBtn.textContent = 'Saving…';
2736
+ resultSlot.replaceChildren();
2737
+ try {
2738
+ const { status: code, data } = await postJSON(endpoint, { values: out });
2739
+ if (code === 403) {
2740
+ resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' }, TOKEN_MSG));
2741
+ } else if (data && data.ok) {
2742
+ // Not every store behind this form has an undo token — saveAdvocacy() (user-settings.mjs)
2743
+ // returns a real backup path but no journalled undoToken the way saveConfig() does, so this
2744
+ // button simply does not appear for that form rather than pretending a capability exists.
2745
+ const undoBtn = data.undoToken ? el('button', {
2746
+ class: 'btn btn-undo btn-sm', type: 'button',
2747
+ // try/catch, because this is an async onclick with a network call in it. Without one, a
2748
+ // dropped connection rejected the promise, left the button permanently disabled and printed
2749
+ // NOTHING — the user is looking at a dead undo button with no idea whether it ran. The
2750
+ // server's own explanation is shown verbatim on failure: "this undo has already been used"
2751
+ // and "your settings were saved again after this point" are the two cases people will
2752
+ // actually hit, and both are worth reading.
2753
+ onclick: async (ev) => {
2754
+ const btn = ev.currentTarget;
2755
+ btn.disabled = true;
2756
+ try {
2757
+ const r = await postJSON('/api/undo', { undoToken: data.undoToken });
2758
+ const ok = r.ok && r.data?.ok;
2759
+ if (!ok) btn.disabled = false;
2760
+ resultSlot.replaceChildren(el('div', { class: `form-note ${ok ? 'n-ok' : 'n-err'}`, role: 'status' },
2761
+ ok
2762
+ ? 'Settings restored from the backup. Reload to see the restored values.'
2763
+ : (r.status === 403 ? TOKEN_MSG
2764
+ : `Undo didn’t complete — ${r.data?.log || 'the backup file still exists, nothing is lost.'}`)));
2765
+ } catch (err) {
2766
+ btn.disabled = false;
2767
+ resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
2768
+ `Undo couldn’t reach the console server: ${err.message || err}. Nothing was changed.`));
2769
+ }
2770
+ },
2771
+ }, 'Undo save') : null;
2772
+ resultSlot.replaceChildren(el('div', { class: 'form-note n-ok', role: 'status' },
2773
+ frag(BADGE_OK),
2774
+ el('div', { class: 'fn-body' },
2775
+ el('b', {}, 'Saved.'), ' Your choices are in ',
2776
+ el('span', { class: 'fn-path' }, cfg.path || 'your user folder'), '.',
2777
+ data.backup ? el('span', {}, ' Backup kept at ', el('span', { class: 'fn-path' }, data.backup), '.') : ''),
2778
+ undoBtn));
2779
+ for (const [key, get] of Object.entries(collectors)) {
2780
+ const g = get(); if (!g.secret) initial[key] = g.value;
2781
+ }
2782
+ announce('Settings saved.');
2783
+ } else {
2784
+ // The server says WHY — a rejected value names itself ("routing: expected one of auto, off").
2785
+ // Swallowing that left the user re-clicking a button that would fail identically every time.
2786
+ resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
2787
+ data && data.log
2788
+ ? `Save didn’t complete — ${data.log}`
2789
+ : 'Save didn’t complete. Your file was not changed without its backup.'));
2790
+ refreshDirty();
2791
+ }
2792
+ } catch (err) {
2793
+ resultSlot.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
2794
+ `Couldn’t reach the console server: ${err.message || err}`));
2795
+ refreshDirty();
2796
+ }
2797
+ saveBtn.textContent = prev;
2798
+ });
2799
+
2800
+ return form;
2801
+ }
2802
+
2803
+ /**
2804
+ * `cfg` is config.json's fields (issue's original section); `us` is user-settings.mjs's fields — for
2805
+ * now just the advocacy dial (ADR-032 §DDD-0004 "the three channels": this control is the volume knob
2806
+ * on the speech channel). Two stores, two forms, one shared widget — see buildSettingsField/Form.
2807
+ */
2808
+ function renderSettings(cfg, us, bp) {
2809
+ const body = $('#body-settings');
2810
+ // `bp` is NOT counted here any more: its field is rendered by #card-brain at the top of the page,
2811
+ // and counting a control this card does not show would make the "N options" chip a lie by one.
2812
+ const groups = [cfg, us].filter((c) => c && Array.isArray(c.schema) && c.schema.length);
2813
+ const unavailable = [cfg, us]
2814
+ .flatMap((c) => (c && Array.isArray(c.unavailable) ? c.unavailable : []));
2815
+ if (!groups.length) {
2816
+ setChips('chips-settings', [chip('no schema', 'grey')]);
2817
+ body.replaceChildren(el('p', { class: 'muted' }, 'No editable settings were received.'));
2818
+ return;
2819
+ }
2820
+
2821
+ const totalOptions = groups.reduce((n, c) => n + c.schema.length, 0);
2822
+ const unsetCount = groups.reduce((n, c) => {
2823
+ const values = c.values || {};
2824
+ return n + c.schema.filter((f) => !f.secret && f.type !== 'secret'
2825
+ && (values[f.key] === null || values[f.key] === undefined)).length;
2826
+ }, 0);
2827
+ setChips('chips-settings', [chip(`${totalOptions} option${totalOptions === 1 ? '' : 's'}`, 'grey'),
2828
+ unavailable.length ? chip(`${unavailable.length} unavailable here`, 'wait') : null,
2829
+ unsetCount ? chip(`${unsetCount} not chosen yet`, 'wait') : null,
2830
+ groups.some((c) => c.exists === false) ? chip('not created yet', 'wait') : null].filter(Boolean));
2831
+
2832
+ const main = [];
2833
+ // THE MASTER SWITCH MOVED OUT OF HERE (owner, 2026-07-27). It used to be rendered first in this
2834
+ // card, on the reasoning that "a volume knob below a power switch reads as more important than the
2835
+ // power switch". The reasoning was right and the placement was still wrong: this whole card sits
2836
+ // several sections down the page, behind a chevron, so the owner opened the console looking for
2837
+ // "a big on/off switch" and could not find it at all.
2838
+ //
2839
+ // It is now its own always-open card at the TOP of the page (#card-brain, renderBrainPower). This
2840
+ // card keeps ONE line — a pointer — because the second-worst outcome after "can't find it" is
2841
+ // "found two of them and they disagree".
2842
+ if (bp && typeof bp.off === 'boolean') {
2843
+ main.push(el('p', { class: 'fineprint' },
2844
+ 'The brain’s on/off switch is at the top of this page — it is ',
2845
+ el('b', {}, bp.off ? 'off' : 'on'), ' right now. ',
2846
+ el('button', {
2847
+ class: 'btn btn-ghost btn-sm', type: 'button',
2848
+ onclick: () => { document.getElementById('card-brain')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); },
2849
+ }, 'Take me to it')));
2850
+ }
2851
+ if (cfg && Array.isArray(cfg.schema) && cfg.schema.length) main.push(buildSettingsForm(cfg, { endpoint: '/api/save-config' }));
2852
+ if (us && Array.isArray(us.schema) && us.schema.length) main.push(buildSettingsForm(us, { endpoint: '/api/save-advocacy' }));
2853
+ if (unavailable.length) {
2854
+ main.push(el('section', { class: 'settings-unavailable', 'aria-labelledby': 'settings-unavailable-h' },
2855
+ el('h3', { id: 'settings-unavailable-h' }, 'Unavailable on this machine'),
2856
+ el('p', { class: 'fineprint' },
2857
+ 'These choices have runtime enforcement, but that runtime is not supported or reachable on this machine. ',
2858
+ 'They stay visible with the measured reason instead of becoming a dead switch.'),
2859
+ el('ul', { class: 'settings-unavailable-list' },
2860
+ unavailable.map((item) => el('li', {},
2861
+ el('b', {}, item.label || item.key),
2862
+ el('span', {}, ` — ${item.reason || 'No runtime consumer is implemented.'}`))))));
2863
+ }
2864
+
2865
+ body.replaceChildren(withIllo('settings', ...main));
2866
+ }
2867
+
2868
+ /* ═══════════════════════════════════════════════════════════ BRAIN POWER — the master switch
2869
+ Owner, 2026-07-27, after opening the console to look for it: "if it didn't show up in the console,
2870
+ how can it be implemented?" ADR-054's switch was real, saved correctly, and rendered — as one
2871
+ checkbox inside the Settings card, several sections below the things it governs, behind a chevron.
2872
+ From where the user stands that is indistinguishable from not existing.
2873
+
2874
+ FOUR RULES THIS RENDERER KEEPS:
2875
+ 1. ONE SWITCH ON THE PAGE. Settings no longer renders the field; it points here. Two controls for
2876
+ one machine state is how they end up disagreeing.
2877
+ 2. THE STATE IS READ, NEVER ASSUMED. Every paint comes from the server's resolved answer (the
2878
+ SENTINEL, not the settings mirror). A click does not flip the visual — it posts, waits, and
2879
+ re-reads. A switch that moves before the machine does is a lie with an animation on it.
2880
+ 3. THE COPY COMES FROM THE SCHEMA. whyItMatters/downside are rendered from the served field, never
2881
+ re-typed here, so this card cannot drift from what user-settings.mjs actually says.
2882
+ 4. CONSENT FOR THE DESTRUCTIVE DIRECTION ONLY. Turning it OFF opens a confirm step carrying the
2883
+ real consequences; turning it back ON is immediate, because there is nothing to warn about. */
2884
+
2885
+ let bpBusy = false;
2886
+
2887
+ function bpField(bp) {
2888
+ return (bp && Array.isArray(bp.schema) && bp.schema[0]) || null;
2889
+ }
2890
+
2891
+ function renderBrainPower(bp) {
2892
+ const body = $('#body-brain');
2893
+ if (!body) return;
2894
+ if (!bp || typeof bp.off !== 'boolean') {
2895
+ // NOT "on". An unreadable switch is reported as unreadable — the same rule the capabilities card
2896
+ // lives by, applied to the one control that governs it.
2897
+ setChips('chips-brain', [chip('can’t read the switch', 'nt')]);
2898
+ body.replaceChildren(el('p', { class: 'muted' },
2899
+ 'The console could not read the on/off state from this machine, so it is not showing one. ',
2900
+ 'Nothing has changed; the switch file is the authority and it is untouched.'));
2901
+ return;
2902
+ }
2903
+ const on = !bp.off;
2904
+ const f = bpField(bp);
2905
+ setChips('chips-brain', [chip(on ? 'ON' : 'OFF', on ? 'green' : 'amber')]);
2906
+
2907
+ const knob = el('span', { class: 'bp-track' }, el('span', { class: 'bp-knob' }));
2908
+ const switchBtn = el('button', {
2909
+ class: 'bp-switch', type: 'button', role: 'switch',
2910
+ 'aria-checked': on ? 'true' : 'false',
2911
+ 'aria-labelledby': 'card-brain-h',
2912
+ disabled: bpBusy || null,
2913
+ onclick: () => { if (on) bpAskToTurnOff(bp); else void bpFlip(true); },
2914
+ }, knob, el('span', { class: 'bp-switch-label' }, on ? 'Turn it off' : 'Turn it back on'));
2915
+
2916
+ // The description is the SCHEMA's own `help` line (rule 3), so this card cannot say something
2917
+ // different from what the settings store says about the same key.
2918
+ const state = el('div', { class: 'bp-state' },
2919
+ el('div', { class: 'bp-word' }, el('span', { class: 'bp-dot' }), on ? 'Your brain is ON' : 'Your brain is OFF'),
2920
+ el('p', { class: 'bp-sub' }, f && f.help ? f.help : 'Whether the brain is working at all.'),
2921
+ bp.off && bp.since
2922
+ ? el('p', { class: 'bp-sub' }, `Off since ${String(bp.since).slice(0, 10)}${bp.reason ? ` — ${bp.reason}` : ''}.`)
2923
+ : null);
2924
+
2925
+ const main = [
2926
+ el('div', { class: `bp-row ${on ? 'bp-on' : 'bp-off'}`, id: 'bp-row' }, state, switchBtn),
2927
+ el('div', { id: 'bp-confirm-slot' }),
2928
+ el('div', { id: 'bp-result-slot' }),
2929
+ ];
2930
+
2931
+ // What keeps running and what stops — from the SERVER's own list, so it can never drift from what
2932
+ // session-start.sh actually does.
2933
+ if (Array.isArray(bp.notes) && bp.notes.length) {
2934
+ main.push(el('ul', { class: 'bp-notes' }, bp.notes.map((n) => el('li', {}, n))));
2935
+ }
2936
+ if (bp.disagreement) main.push(el('p', { class: 'fineprint bp-warn' }, bp.disagreement.note));
2937
+ if (bp.switchPath) {
2938
+ // .fn-path, not <code>: an absolute path is reference detail, and a full-width monospace block
2939
+ // two lines tall out-shouts the switch it is a footnote to.
2940
+ main.push(el('p', { class: 'fineprint' },
2941
+ 'The switch is a file — ', el('span', { class: 'fn-path' }, bp.switchPath),
2942
+ ' — so it survives updates, and every part of the product reads the same one.'));
2943
+ }
2944
+ if (bp.profile) main.push(bpProfileControl(bp.profile));
2945
+ main.push(bpParts(bp));
2946
+ body.replaceChildren(...main);
2947
+ }
2948
+
2949
+ let bpProfileBusy = false;
2950
+ const bpSize = (bytes) => {
2951
+ if (!Number.isFinite(bytes)) return 'size measured after install';
2952
+ return bytes >= 1e9 ? `${(bytes / 1e9).toFixed(2)} GB` : `${Math.round(bytes / 1e6)} MB`;
2953
+ };
2954
+
2955
+ function bpProfileControl(profile) {
2956
+ const current = profile.values?.brainProfile;
2957
+ const result = el('div', { class: 'bp-profile-result' });
2958
+ const apply = el('button', { class: 'btn btn-apply', type: 'button', disabled: true }, 'Apply selection');
2959
+ const options = [
2960
+ {
2961
+ value: 'complete',
2962
+ title: 'Complete Brain',
2963
+ copy: 'Every public rUv repository in the release. Best for cross-repository answers and supporting evidence.',
2964
+ },
2965
+ {
2966
+ value: 'ruvector',
2967
+ title: 'RuVector Only',
2968
+ copy: 'Keeps the shared reader and RuVector RVF only. Smallest footprint, but no evidence from Ruflo, AgentDB, RuView, or the other repositories.',
2969
+ },
2970
+ ];
2971
+ const radios = [];
2972
+ const cards = options.map((option) => {
2973
+ const choice = profile.choices?.[option.value] || {};
2974
+ const input = el('input', {
2975
+ type: 'radio',
2976
+ name: 'brain-profile',
2977
+ value: option.value,
2978
+ disabled: choice.available === false || null,
2979
+ onchange: () => { apply.disabled = bpProfileBusy || input.value === current; },
2980
+ });
2981
+ input.checked = option.value === current;
2982
+ radios.push(input);
2983
+ return el('label', { class: `bp-profile-choice${input.disabled ? ' unavailable' : ''}` },
2984
+ input,
2985
+ el('span', { class: 'bp-profile-copy' },
2986
+ el('span', { class: 'bp-profile-title' }, option.title,
2987
+ option.value === current ? chip('active', 'green') : null),
2988
+ el('span', { class: 'bp-profile-meta' },
2989
+ `${choice.storeCount || (option.value === 'ruvector' ? 1 : '—')} ${choice.storeCount === 1 ? 'store' : 'stores'} · ${bpSize(choice.bytes)}`),
2990
+ el('span', { class: 'bp-profile-desc' }, option.copy),
2991
+ choice.available === false
2992
+ ? el('span', { class: 'bp-profile-desc bp-warn' }, 'The complete release bundle is not available on this machine; run the Brain update first.')
2993
+ : null));
2994
+ });
2995
+
2996
+ apply.onclick = async () => {
2997
+ const selected = radios.find((radio) => radio.checked)?.value;
2998
+ if (!selected || selected === current || bpProfileBusy) return;
2999
+ if (selected === 'ruvector' && !window.confirm(
3000
+ 'Switch to RuVector Only? This removes the other public repository RVFs from this machine. You can restore them from the complete signed bundle later.',
3001
+ )) return;
3002
+ bpProfileBusy = true;
3003
+ apply.disabled = true;
3004
+ apply.textContent = selected === 'complete' ? 'Restoring…' : 'Removing unselected stores…';
3005
+ result.replaceChildren();
3006
+ try {
3007
+ const response = await postJSON('/api/save-brain-profile', { values: { brainProfile: selected } });
3008
+ const data = response?.data || {};
3009
+ if (!response.ok || !data.ok) {
3010
+ result.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
3011
+ response?.status === 403 ? TOKEN_MSG : `The profile did not change — ${data.log || 'the console could not apply it.'}`));
3012
+ } else {
3013
+ result.replaceChildren(el('div', { class: 'form-note n-ok', role: 'status' },
3014
+ el('div', { class: 'fn-body' }, el('b', {}, data.profile === 'ruvector' ? 'RuVector Only is active.' : 'Complete Brain is active.'),
3015
+ ` ${data.log}.`,
3016
+ data.bytesFreed ? ` ${bpSize(data.bytesFreed)} released.` : '')));
3017
+ announce(data.log || 'Brain profile changed.');
3018
+ }
3019
+ } catch (error) {
3020
+ result.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
3021
+ `Couldn’t reach the console server: ${error.message || error}. Nothing was changed.`));
3022
+ } finally {
3023
+ bpProfileBusy = false;
3024
+ await loadState();
3025
+ }
3026
+ };
3027
+
3028
+ return el('section', { class: 'bp-profile', 'aria-labelledby': 'bp-profile-h' },
3029
+ el('div', { class: 'bp-profile-head' },
3030
+ el('h3', { id: 'bp-profile-h' }, 'How much of the brain is installed'),
3031
+ el('span', { class: 'bp-parts-hint' }, profile.path || 'installed brain')),
3032
+ el('p', { class: 'bp-profile-lead' },
3033
+ 'This changes the RVF files on disk, not just a preference. Nightly updates preserve the selection.'),
3034
+ el('div', { class: 'bp-profile-grid', role: 'radiogroup', 'aria-labelledby': 'bp-profile-h' }, ...cards),
3035
+ profile.disagreement
3036
+ ? el('p', { class: 'fineprint bp-warn' }, 'The stored preference did not match the RVFs on disk. The console is showing the files that actually exist.')
3037
+ : null,
3038
+ el('div', { class: 'bp-profile-actions' }, apply),
3039
+ result);
3040
+ }
3041
+
3042
+ /* The consent step, shown only for OFF. The downside text is the schema's own. */
3043
+ function bpAskToTurnOff(bp) {
3044
+ const slot = $('#bp-confirm-slot');
3045
+ if (!slot) return;
3046
+ const f = bpField(bp);
3047
+ slot.replaceChildren(el('div', { class: 'bp-confirm', role: 'group', 'aria-label': 'Confirm turning the brain off' },
3048
+ el('p', {}, f && f.downside
3049
+ ? f.downside
3050
+ : 'Turning it off stops retrieval, the grounding gate, everything it volunteers, and learning.'),
3051
+ el('div', { class: 'bp-actions' },
3052
+ el('button', { class: 'btn btn-apply', type: 'button', onclick: () => { void bpFlip(false); } }, 'Turn the brain off'),
3053
+ el('button', { class: 'btn btn-ghost', type: 'button', onclick: () => slot.replaceChildren() }, 'Keep it on'))));
3054
+ slot.querySelector('button')?.focus();
3055
+ }
3056
+
3057
+ /* POST, then RE-READ. The new state is whatever the server says after the write — never what we just
3058
+ asked for. loadState() repaints this card from the same measured payload every other card uses. */
3059
+ async function bpFlip(next) {
3060
+ if (bpBusy) return;
3061
+ bpBusy = true;
3062
+ const slot = $('#bp-result-slot');
3063
+ $('#bp-confirm-slot')?.replaceChildren();
3064
+ setChips('chips-brain', [chip(next ? 'switching on…' : 'switching off…', 'wait')]);
3065
+ announce(next ? 'Switching the brain on.' : 'Switching the brain off.');
3066
+ try {
3067
+ const r = await postJSON('/api/save-brain-power', { values: { brainEnabled: next } });
3068
+ const data = (r && r.data) || {};
3069
+ if (!r.ok || !data.ok) {
3070
+ slot?.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
3071
+ r.status === 403 ? TOKEN_MSG : `The switch did not move — ${data.log || `the console returned ${r.status}`}.`));
3072
+ } else {
3073
+ slot?.replaceChildren(el('div', { class: 'form-note n-ok', role: 'status' },
3074
+ el('div', { class: 'fn-body' }, el('b', {}, data.off ? 'The brain is off.' : 'The brain is on.'), ' ', data.log)));
3075
+ announce(data.log || 'Done.');
3076
+ }
3077
+ } catch (err) {
3078
+ slot?.replaceChildren(el('div', { class: 'form-note n-err', role: 'alert' },
3079
+ `Couldn’t reach the console server: ${err.message || err}. Nothing was changed.`));
3080
+ } finally {
3081
+ bpBusy = false;
3082
+ // Re-read rather than trusting the write's own echo: the sentinel on disk is the authority.
3083
+ await loadState();
3084
+ }
3085
+ }
3086
+
3087
+ /* ── "Parts of the brain" — the honest state of per-piece control ───────────────────────────────────
3088
+ The owner's second question, and the one a row of fake checkboxes would answer wrongly: where are
3089
+ the sub-piece switches? The answer today is "one is real, one lives in Settings, one is still being
3090
+ measured", and this block says exactly that. Every entry states its OWN control surface, so nobody
3091
+ has to wonder again whether a piece is missing or merely elsewhere.
3092
+
3093
+ THE RULE THAT KEEPS IT HONEST is user-settings.mjs's own: a switch is not shipped until something
3094
+ reads it. The remaining schema keys (learningScope, autoApply, newProjectDefaults) exist in the
3095
+ settings store and are deliberately NOT rendered as controls, because nothing enforces them yet —
3096
+ a row of checkboxes that govern nothing is the single fastest way to make every other switch on
3097
+ this page untrustworthy. */
3098
+ function bpParts(bp) {
3099
+ const off = !!bp.off;
3100
+ const part = (title, chipText, tone, ...text) => el('div', { class: 'bp-part' },
3101
+ el('div', { class: 'bp-part-h' }, el('b', {}, title), chip(chipText, tone)),
3102
+ el('div', { class: 'bp-part-t' }, ...text));
3103
+
3104
+ // COLLAPSED, BUT NAMED. The owner's complaint was that he could not find the sub-piece controls —
3105
+ // which a visible heading fixes. Leaving the whole block expanded would push the page's actual
3106
+ // content a full screen down on every visit, so the answer to "where are they?" is on screen and
3107
+ // the detail behind it is one click. It is NOT the switch itself: nothing about the power switch is
3108
+ // ever behind a click.
3109
+ return el('details', { class: 'bp-parts' },
3110
+ el('summary', {}, el('h3', {}, 'Parts of the brain'), el('span', { class: 'bp-parts-hint' }, 'what is separately controllable, and what is not yet')),
3111
+ el('p', { class: 'bp-parts-lead' },
3112
+ 'The switch above is all-or-nothing today. These are the pieces underneath it, and where each ',
3113
+ 'one is actually controlled — including the ones that are not controllable yet, and why.'),
3114
+
3115
+ part('Retrieval, grounding, learning', off ? 'off with the brain' : 'on with the brain', off ? 'grey' : 'green',
3116
+ 'The three things the switch above governs together. They are not separately switchable yet ',
3117
+ 'because each is enforced from the same sentinel file the switch writes.'),
3118
+
3119
+ part('Maintenance while it is off', 'in Settings', 'cyan',
3120
+ 'Version updates and the health alarm keep running even when the brain is off — an off machine ',
3121
+ 'has to be able to receive the fix for an off-state bug. The nightly refresh is the part of that ',
3122
+ 'you can pause: it is the ',
3123
+ el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => jumpToSetting('nightly') }, 'Nightly brain refresh'),
3124
+ ' switch in Settings.'),
3125
+
3126
+ part('Installed knowledge',
3127
+ bp.profile?.values?.brainProfile === 'ruvector'
3128
+ ? 'RuVector Only'
3129
+ : bp.profile?.values?.brainProfile === 'complete' ? 'Complete Brain' : 'not measured',
3130
+ 'cyan',
3131
+ 'The two-box selector above physically keeps or restores repository RVFs. Complete Brain is ',
3132
+ 'best for cross-repository reasoning; RuVector Only is the compact choice for people who only ',
3133
+ 'need the RuVector source. Nightly updates preserve whichever profile is selected.'),
3134
+
3135
+ el('p', { class: 'fineprint' },
3136
+ 'There are three more switches in the settings file — what it learns from, whether it may act ',
3137
+ 'on its own, and whether new projects inherit these choices. They are deliberately not shown ',
3138
+ 'as controls here: nothing enforces them yet, and a switch that governs nothing would make ',
3139
+ 'every real switch on this page worth less.'));
3140
+ }
3141
+
3142
+ /* -------------------------------------------------- section 7: trust & provenance
3143
+ v3.3 preview (PROVE stage). One row is REAL today — the release bundle's published
3144
+ sha256, read live from the latest GitHub release — plus the install channel read from
3145
+ the plugin cache on disk. The SBOM and Advisor Mode rows are honest empty states: each
3146
+ says exactly what will fill it and when. Nothing here is placeholder data. */
3147
+
3148
+ const TRUST_INFO = {
3149
+ signature: [
3150
+ { k: 'What is this?', t: 'The sha256 fingerprint of the release bundle, published as its own asset on every GitHub release.' },
3151
+ { 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.' },
3152
+ { k: 'How do I use it?', t: 'Run shasum -a 256 ruvnet-brain.zip on your download and compare. v3.3 adds a one-click local check right here.' },
3153
+ ],
3154
+ sbom: [
3155
+ { k: 'What is this?', t: 'A Software Bill of Materials — the complete, machine-readable list of every package inside the bundle.' },
3156
+ { 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.' },
3157
+ { k: 'How does it help me?', t: 'v3.3 attaches a CycloneDX SBOM to every release; this row will then show its package count and digest, measured from the published asset.' },
3158
+ ],
3159
+ channel: [
3160
+ { k: 'What is this?', t: 'How your plugin updates: riding the latest release, or pinned to a version you chose.' },
3161
+ { k: 'Why does it matter?', t: 'This stack ships fast — latest keeps you current. Pinning holds a known-good release when you need repeatable builds.' },
3162
+ { k: 'How does it help me?', t: 'Read from your plugin cache on disk, never assumed. Version pinning is planned — both choices will live on this row.' },
3163
+ ],
3164
+ advisor: [
3165
+ { k: 'What is this?', t: 'A coming mode switch. Full lets the console apply consent-gated, undoable fixes; Advisor makes every Apply button read-only — it shows the exact command and steps aside.' },
3166
+ { k: 'Why does it matter?', t: 'Some machines want eyes-only — work laptops, shared rigs, cautious first weeks. The right choice should be easy in both directions.' },
3167
+ { k: 'How does it help me?', t: 'Planned. Today the switch is a preview — it changes nothing, and says so.' },
3168
+ ],
3169
+ };
3170
+
3171
+ function trustRow({ name, info, coming, status, value }) {
3172
+ return el('div', { class: `trust-row${coming ? ' is-coming' : ''}` },
3173
+ el('span', { class: 'trust-name' }, name, infoBtn(name, info)),
3174
+ el('div', { class: 'trust-val' }, ...value),
3175
+ el('span', { class: 'trust-status' }, status));
3176
+ }
3177
+
3178
+ function renderTrust(t) {
3179
+ const body = $('#body-trust');
3180
+ const rel = t.release || {};
3181
+ const ch = t.channel || {};
3182
+ const sb = t.sbom || {};
3183
+ const liveCount = (rel.ok ? 1 : 0) + (sb.present ? 1 : 0);
3184
+
3185
+ setChips('chips-trust', [
3186
+ rel.ok ? chip('sha256 published ✓', 'green', 'The release bundle’s fingerprint is published and was read live this session')
3187
+ : chip('digest unreachable', 'warn', 'Couldn’t read the published digest this session'),
3188
+ sb.present ? chip(`SBOM · ${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'} ✓`, 'green', 'A local CycloneDX SBOM was found and read live this session')
3189
+ : chip('SBOM — v3.3', 'coming', 'A CycloneDX SBOM ships with every release from v3.3'),
3190
+ ch.installed ? chip(ch.channel === 'pinned' ? 'pinned' : 'latest channel', 'cyan') : chip('no plugin install', 'grey'),
3191
+ ]);
3192
+
3193
+ const rows = [];
3194
+
3195
+ /* 1 · bundle signature — the one REAL measurement today */
3196
+ rows.push(trustRow({
3197
+ name: 'Bundle signature', info: TRUST_INFO.signature,
3198
+ status: rel.ok ? chip('published ✓', 'green') : chip('unreachable', 'warn'),
3199
+ value: rel.ok ? [
3200
+ el('p', {}, 'Latest release ', el('b', {}, rel.tag || '—'),
3201
+ rel.asset ? el('span', {}, ' · ', el('span', { class: 'cell-mono' }, rel.asset)) : '',
3202
+ rel.publishedAt ? ` · published ${fmtDate(rel.publishedAt)}` : ''),
3203
+ el('code', { class: 'trust-hash', title: 'sha256 of the release bundle, as published' }, rel.sha256 || ''),
3204
+ el('p', {}, 'Check your download against it: ', el('code', {}, 'shasum -a 256 ruvnet-brain.zip'),
3205
+ ' — the 64 characters must match exactly.'),
3206
+ rel.sig ? el('p', {}, 'A detached signature (', el('span', { class: 'cell-mono' }, '.sig'),
3207
+ ') ships alongside — one-click signature verification is planned right here.') : null,
3208
+ el('span', { class: 'trust-src' }, 'read live · ', rel.source || 'github.com — latest release'),
3209
+ ] : [
3210
+ el('p', {}, 'Couldn’t reach GitHub this session', rel.error ? el('span', {}, ' (', el('span', { class: 'cell-mono' }, rel.error), ')') : '',
3211
+ ' — nothing is shown that wasn’t read. The digest is published on the latest release.'),
3212
+ el('button', { class: 'btn btn-ghost btn-sm', type: 'button', onclick: () => { trustSkeleton(); loadTrust(); } }, 'Try again'),
3213
+ ],
3214
+ }));
3215
+
3216
+ /* 2 · SBOM — real once `npm run sbom` has been run locally; honest empty state until then */
3217
+ rows.push(trustRow({
3218
+ name: 'SBOM', info: TRUST_INFO.sbom, coming: !sb.present,
3219
+ status: sb.present ? chip(`${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'}`, 'green') : chip('coming · v3.3', 'coming'),
3220
+ value: sb.present ? [
3221
+ el('p', {}, 'A ', el('b', {}, `CycloneDX ${sb.specVersion || ''}`.trim()), ' SBOM exists on this machine: ',
3222
+ el('b', {}, `${sb.componentCount} component${sb.componentCount === 1 ? '' : 's'}`),
3223
+ sb.mainComponent ? ` for ${sb.mainComponent}${sb.mainVersion ? `@${sb.mainVersion}` : ''}` : '',
3224
+ sb.generatedAt ? ` · generated ${fmtDate(sb.generatedAt)}` : ''),
3225
+ el('p', {}, 'This is the production dependency tree only (', el('code', {}, '--omit dev'),
3226
+ ') — the plugin and installer ship no other packages. Regenerate any time: ', el('code', {}, 'npm run sbom')),
3227
+ el('span', { class: 'trust-src' }, sb.path || 'sbom/ruvnet-brain.cdx.json'),
3228
+ ] : [
3229
+ el('p', {}, sb.error
3230
+ ? `Found sbom/ruvnet-brain.cdx.json but couldn’t read it (${sb.error}).`
3231
+ : 'Not generated yet on this machine — nothing to show, so nothing is shown.'),
3232
+ el('p', {}, 'Run ', el('code', {}, 'npm run sbom'),
3233
+ ' to produce a CycloneDX SBOM of the shipped dependency tree right now. From ', el('b', {}, 'v3.3'),
3234
+ ' every published release carries one too, measured from the release asset itself.'),
3235
+ ],
3236
+ }));
3237
+
3238
+ /* 3 · install channel — read from the plugin cache on disk */
3239
+ rows.push(trustRow({
3240
+ name: 'Install channel', info: TRUST_INFO.channel,
3241
+ status: ch.installed ? chip(ch.channel === 'pinned' ? 'pinned' : 'latest', 'cyan') : chip('not found', 'grey'),
3242
+ value: ch.installed ? [
3243
+ el('p', {}, el('b', {}, ch.channel === 'pinned' ? 'Pinned' : 'Latest'),
3244
+ ch.channel === 'pinned'
3245
+ ? ' — held at a version you chose.'
3246
+ : ' — auto-updates from GitHub, so you ride each release as it ships.'),
3247
+ el('p', {}, 'On disk right now: ', el('b', {}, `v${ch.version || '?'}`),
3248
+ ch.lastUpdated ? ` · updated ${fmtDate(ch.lastUpdated)}` : ''),
3249
+ el('span', { class: 'trust-src' }, ch.cacheDir || ''),
3250
+ ch.channel !== 'pinned' ? el('p', { style: 'margin-top:6px' },
3251
+ 'Prefer to hold a known-good release? ', el('b', {}, 'Version pinning is planned'),
3252
+ ' — you’ll choose it right here.') : null,
3253
+ ] : [
3254
+ el('p', {}, 'No plugin-cache install found on this machine — you may be running from a repo checkout. ',
3255
+ 'This row reads ', el('span', { class: 'cell-mono' }, '~/.claude/plugins'), ', never guesses.'),
3256
+ ],
3257
+ }));
3258
+
3259
+ /* 4 · advisor mode — display-only preview, clearly labeled */
3260
+ const advNote = el('p', { class: 'adv-note' },
3261
+ 'Full is on: every change stays consent-gated, with its undo recorded first. The switch itself is planned — today it’s a preview and changes nothing.');
3262
+ const advisorBtn = el('button', {
3263
+ class: 'adv-opt', type: 'button',
3264
+ title: 'Preview — Advisor Mode is planned; clicking changes nothing today',
3265
+ onclick: () => {
3266
+ advNote.textContent = 'Advisor Mode is planned — nothing changed just now. When it lands, this switch makes every Apply button read-only: the console shows the exact command and steps aside.';
3267
+ },
3268
+ }, el('span', { class: 'pc-dot', 'aria-hidden': 'true' }), 'Advisor — read-only');
3269
+ rows.push(trustRow({
3270
+ name: 'Advisor Mode', info: TRUST_INFO.advisor,
3271
+ status: chip('preview', 'coming'),
3272
+ value: [
3273
+ el('div', { class: 'adv-seg' },
3274
+ el('button', { class: 'adv-opt on', type: 'button', title: 'Your current behavior — consent-gated changes with undo' },
3275
+ el('span', { class: 'pc-dot', 'aria-hidden': 'true' }), 'Full — recommended'),
3276
+ advisorBtn,
3277
+ el('span', { class: 'preview-tag' }, 'preview · v3.3')),
3278
+ advNote,
3279
+ ],
3280
+ }));
3281
+
3282
+ body.replaceChildren(
3283
+ el('p', { class: 'lead-stat' },
3284
+ 'Provenance you can check, not take on faith — ', el('b', {}, String(liveCount)),
3285
+ ` measurement${liveCount === 1 ? ' is' : 's are'} live today; the rest of this card names exactly what v3.3 will measure.`,
3286
+ infoBtn('Trust & provenance', TRUST_CARD_INFO)),
3287
+ el('div', { class: 'trust-list', 'data-trust-ready': '1' }, ...rows),
3288
+ );
3289
+ }
3290
+
3291
+ function trustSkeleton() {
3292
+ $('#body-trust').replaceChildren(
3293
+ frag('<div class="skeleton" aria-hidden="true"><div class="sk-bar w45"></div><div class="sk-bar w70"></div></div>'),
3294
+ el('p', { class: 'loading-note' },
3295
+ 'Reading the published release fingerprint from GitHub (read-only metadata — the one network touch this card makes) and your plugin cache on disk.'));
3296
+ setChips('chips-trust', [chip('checking…', 'wait')]);
3297
+ }
3298
+
3299
+ /* ---------------------------------------------- header update gong (owner, 2026-07-24) */
3300
+
3301
+ let BRAIN_INSTALLED_VERSION = null; // set by renderHost from /api/state's host.brainVersion
3302
+
3303
+ // Numeric x.y.z only; -dev/-rc suffixes are deliberately ignored so a tie is NEVER "newer" —
3304
+ // a false gong is the nag this page promises not to be. Latest unreachable (null) is UNKNOWN,
3305
+ // never "update available" (same rule the trust section already lives by).
3306
+ function cmpVer(a, b) {
3307
+ const num = (v) => String(v).replace(/^v/, '').split('-')[0].split('.').map((n) => parseInt(n, 10) || 0);
3308
+ const pa = num(a); const pb = num(b);
3309
+ for (let i = 0; i < 3; i++) { if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0); }
3310
+ return 0;
3311
+ }
3312
+
3313
+ // An update has no verified undo, so per the intro contract the gong is INSTRUCTIONS, not a
3314
+ // switch: one click copies the exact command and says so — nothing runs behind your back.
3315
+ function renderUpdateGong(t) {
3316
+ const btn = $('#brain-update');
3317
+ if (!btn) return;
3318
+ const latest = t && t.release && t.release.tag ? String(t.release.tag).replace(/^v/, '') : null;
3319
+ if (!BRAIN_INSTALLED_VERSION || !latest || cmpVer(latest, BRAIN_INSTALLED_VERSION) <= 0) { btn.hidden = true; return; }
3320
+ const cmd = 'npx ruvnet-brain --update';
3321
+ btn.textContent = `⟳ update available · v${latest} — click to update`;
3322
+ btn.onclick = async () => {
3323
+ try { await navigator.clipboard.writeText(cmd); btn.textContent = `copied — paste in any terminal: ${cmd}`; }
3324
+ catch { btn.textContent = `run in any terminal: ${cmd}`; }
3325
+ };
3326
+ btn.hidden = false;
3327
+ }
3328
+
3329
+ async function loadTrust() {
3330
+ try {
3331
+ const t = await getJSON('/api/trust');
3332
+ renderTrust(t);
3333
+ renderUpdateGong(t);
3334
+ } catch (err) {
3335
+ setChips('chips-trust', [chip('unavailable', 'grey')]);
3336
+ inlineError('body-trust', String(err.message || err), () => { trustSkeleton(); loadTrust(); });
3337
+ }
3338
+ }
3339
+
3340
+ /* ═════════════════════════════════════════════════ freshness (owner directive 2026-07-26/27)
3341
+ THE PAGE CARRIES THE WAIT. The server never blocks: a cold or wrong-project read is answered
3342
+ instantly with `{warming:true}` while a detached child measures. That moves the whole burden of
3343
+ the wait here, to the only place that can carry it honestly — the visible page. So this block owns
3344
+ three promises, and each exists because its opposite was shipped and hurt someone:
3345
+
3346
+ 1. WARMING IS NARRATED, NEVER RENDERED AS EMPTY. A warming answer has no sections. Painting it
3347
+ would tell a first-time user "you have no hooks, no memory, nothing configured" about a
3348
+ machine nobody has looked at yet. Skeletons stay; the pill says what is happening.
3349
+ 2. THE AGE IS ALWAYS ON SCREEN. "Is this current?" must be a label, not a feeling.
3350
+ 3. ONE REFRESH STORY. One control (the header ↻ and this pill share one handler), one server
3351
+ job, one definition of done. The previous ↻ re-fetched three cache-first endpoints and
3352
+ announced "Re-check complete" — a placebo that measured nothing and said it had. */
3353
+
3354
+ let FRESH_BASE = null; // measuredAt of what is currently painted; null until something lands
3355
+ let freshPollTimer = null;
3356
+ let freshPollsLeft = 0;
3357
+
3358
+ function freshnessPill() {
3359
+ let pill = document.getElementById('freshness-pill');
3360
+ if (!pill) {
3361
+ // A BUTTON, not a label: the age and the way to change it are the same affordance, so there is
3362
+ // no second refresh control to disagree with the first.
3363
+ pill = el('button', {
3364
+ id: 'freshness-pill', class: 'chip tone-grey', type: 'button',
3365
+ title: 'How old this picture of your machine is — click to measure it again now',
3366
+ onclick: () => { void doManualRefresh(); },
3367
+ }, '…');
3368
+ const anchor = $('#recheck-btn');
3369
+ if (anchor && anchor.parentNode) anchor.parentNode.insertBefore(pill, anchor);
3370
+ }
3371
+ return document.getElementById('freshness-pill');
3372
+ }
3373
+
3374
+ function fmtAge(iso) {
3375
+ const ms = Date.now() - new Date(iso).getTime();
3376
+ if (!Number.isFinite(ms) || ms < 0) return 'just now';
3377
+ const m = Math.round(ms / 60000);
3378
+ if (ms < 45000) return 'seconds ago';
3379
+ if (m < 60) return `${Math.max(1, m)}m ago`;
3380
+ return `${Math.round(m / 60)}h ago`;
3381
+ }
3382
+
3383
+ /* A DELIBERATELY EXPIRED STAMP IS NOT AN AGE (found by looking at the rendered page, 2026-07-27).
3384
+ The server withdraws a claim by BACK-DATING it to the epoch — expire, never delete — and every
3385
+ write-path invalidation does it: /api/refresh on all four caches, setLesson on capabilities,
3386
+ publishBrainPowerToCache on the state cache. Flipping the brain switch and looking at the header
3387
+ produced, verbatim: "as of 495868h ago". That is a fabricated number on a user-facing surface,
3388
+ about the freshness of the very thing this pill exists to be honest about. An epoch stamp means
3389
+ "this reading was withdrawn on purpose"; a stamp we cannot parse means we do not know. Neither is
3390
+ a duration, and neither may be printed as one. Pure so the test can run the shipped rule. */
3391
+ function stampWithdrawn(at) {
3392
+ const t = Date.parse(at);
3393
+ if (!Number.isFinite(t)) return true;
3394
+ const age = Date.now() - t;
3395
+ // A couple of minutes of negative age is clock skew between the stamp and this tab, and fmtAge
3396
+ // renders it as "just now". A year old, or genuinely from the future, is not an age at all.
3397
+ return !(age >= -120_000 && age < 365 * 24 * 60 * 60 * 1000);
3398
+ }
3399
+
3400
+ /* One pill, five honest states: warming (nothing measured yet), withdrawn (a reading was expired on
3401
+ purpose), re-measuring (one is on its way), just-landed, and plain age. `warming` never sets
3402
+ FRESH_BASE — there is no measurement to base anything on, and pretending otherwise would make the
3403
+ next real one look like a repeat. */
3404
+ function renderFreshness(state, { polling = false, justLanded = false } = {}) {
3405
+ const pill = freshnessPill();
3406
+ if (!pill) return;
3407
+ if (state && state.warming) {
3408
+ pill.className = 'chip tone-wait';
3409
+ pill.textContent = 'measuring your machine… ~20s';
3410
+ return;
3411
+ }
3412
+ const at = state && (state.measuredAt || state.cachedAt || state.generatedAt);
3413
+ if (at) FRESH_BASE = at;
3414
+ if (justLanded) {
3415
+ pill.className = 'chip tone-green';
3416
+ pill.textContent = 'measured just now';
3417
+ return;
3418
+ }
3419
+ if (!at || stampWithdrawn(at)) {
3420
+ pill.className = polling ? 'chip tone-cyan' : 'chip tone-warn';
3421
+ pill.textContent = polling ? 're-measuring your machine…' : 'this reading was cleared — click to measure again';
3422
+ return;
3423
+ }
3424
+ pill.className = polling ? 'chip tone-cyan' : (state && state.stale ? 'chip tone-warn' : 'chip tone-grey');
3425
+ pill.textContent = `as of ${fmtAge(at)}${polling ? ' · re-measuring…' : ''}`;
3426
+ }
3427
+
3428
+ /* THE POLLER'S ONE RULE — pure, and named so it can be tested without a browser (see
3429
+ tests/unit/console-freshness-poller.test.mjs, which lifts this exact source and runs it).
3430
+
3431
+ Repaint only on a STRICTLY NEWER, SETTLED measurement. Every clause is a bug that shipped:
3432
+ • `!st.sections` — a warming answer has none; painting it empties the page.
3433
+ • `st.stale !== false` — /api/refresh back-dates the stamp to the epoch ON PURPOSE (expire, do
3434
+ not delete). The first poller compared `at !== FRESH_BASE`, so it read that withdrawal as an
3435
+ arrival: it would have painted the deliberately-withdrawn claim and then gone green.
3436
+ • strictly newer — a stamp that merely CHANGED can be older. Time only moves one way here. */
3437
+ function freshLanded(st, base) {
3438
+ if (!st || st.warming || st.stale !== false || !st.sections) return false;
3439
+ const at = st.measuredAt || st.cachedAt || st.generatedAt;
3440
+ const t = Date.parse(at);
3441
+ if (!Number.isFinite(t)) return false;
3442
+ if (!base) return true; // nothing painted yet — the first settled answer IS the news
3443
+ const b = Date.parse(base);
3444
+ return !Number.isFinite(b) || t > b;
3445
+ }
3446
+
3447
+ function startFreshnessPolling() {
3448
+ if (freshPollTimer) return; // one poller, ever
3449
+ freshPollsLeft = 60; // 60 x 3s = 3 minutes, then give up ALOUD (never silently)
3450
+ renderFreshness({ warming: !FRESH_BASE, measuredAt: FRESH_BASE, stale: true }, { polling: !!FRESH_BASE });
3451
+ freshPollTimer = setInterval(async () => {
3452
+ if (--freshPollsLeft <= 0) {
3453
+ clearInterval(freshPollTimer); freshPollTimer = null;
3454
+ // A poller that quits in silence leaves a "measuring…" pill sitting there forever, which reads
3455
+ // as "still working" — the exact dead air this whole change exists to end.
3456
+ const p = freshnessPill();
3457
+ if (p) { p.className = 'chip tone-warn'; p.textContent = 'still measuring — click to try again'; }
3458
+ announce('The background measurement has not landed yet. Click the freshness chip to try again.');
3459
+ return;
3460
+ }
3461
+ try {
3462
+ const st = await getJSON('/api/state?fast=1');
3463
+ if (freshLanded(st, FRESH_BASE)) {
3464
+ clearInterval(freshPollTimer); freshPollTimer = null;
3465
+ FRESH_BASE = st.measuredAt || st.cachedAt || st.generatedAt;
3466
+ // ONE painter — the same loadState() the first paint uses, so a polled repaint can never
3467
+ // drift from an initial one. It also refreshes preStateHash and the recommendations, which a
3468
+ // sections-only repaint silently left pointing at the pre-measurement machine.
3469
+ await loadState({ landed: true });
3470
+ // The other cards measure on their own clocks (stack, capabilities, fleet). Re-ask them here
3471
+ // rather than letting the page claim page-wide freshness on the strength of state alone.
3472
+ void loadStack();
3473
+ void loadCapabilities();
3474
+ announce('Your machine has been re-measured — the cards now show the new reading.');
3475
+ } else {
3476
+ renderFreshness(st, { polling: true });
3477
+ }
3478
+ } catch { /* transient — the next tick tries again */ }
3479
+ }, 3000);
3480
+ }
3481
+
3482
+ /* THE refresh. Expires all four caches server-side (scope-preservingly), force-kicks the measuring
3483
+ child past its debounce, then watches for the result. `started:false` is reported as such — a
3484
+ refresh that did not start must never render as one that did. */
3485
+ async function doManualRefresh() {
3486
+ const pill = freshnessPill();
3487
+ if (pill) { pill.className = 'chip tone-cyan'; pill.textContent = 're-measuring…'; }
3488
+ setChips('chips-stack', [chip('re-measuring…', 'wait')]);
3489
+ setChips('chips-capabilities', [chip('re-measuring…', 'wait')]);
3490
+ announce('Re-measuring your machine. Each card updates as its own measurement lands.');
3491
+ const r = await postJSON('/api/refresh', {});
3492
+ const body = (r && r.data) || {};
3493
+ if (!r || !r.ok || !body.ok) {
3494
+ if (pill) { pill.className = 'chip tone-warn'; pill.textContent = 'refresh failed — click to try again'; }
3495
+ announce('The refresh could not be started.');
3496
+ return;
3497
+ }
3498
+ if (body.started === false && pill) {
3499
+ // Honest, and not an error: a measurement was already running, so this click joined it instead
3500
+ // of starting a second full scan of the same machine.
3501
+ pill.className = 'chip tone-cyan';
3502
+ pill.textContent = 'a measurement is already running…';
3503
+ }
3504
+ if (freshPollTimer) { clearInterval(freshPollTimer); freshPollTimer = null; }
3505
+ startFreshnessPolling();
3506
+ }
3507
+
3508
+ /* ------------------------------------------------------------------ loaders */
3509
+
3510
+ /* ONE fetch, ONE painter (2026-07-26). This used to request /api/state TWICE — once with `?fast=1`
3511
+ for an instant cache paint, then again for "the live gather". The server strips the query string
3512
+ before routing, so both calls hit the identical cache-first handler: the second was a duplicate
3513
+ that painted the same bytes twice and cost a round-trip on every open. There is now one call, and
3514
+ the "live" reading arrives the only way it can without freezing the server — from the detached
3515
+ child, via the freshness poller. */
3516
+ async function loadState({ landed = false } = {}) {
3517
+ try {
3518
+ const state = await getJSON('/api/state');
3519
+ $('#global-error').hidden = true;
3520
+
3521
+ // WARMING: NOTHING HAS BEEN MEASURED FOR THIS PROJECT YET. Paint NOTHING. Every render* below
3522
+ // would turn a missing section into a positive claim — renderWiring(undefined) prints "No wiring
3523
+ // data received", renderGates(undefined) prints "No gate data received" — which is a page-wide
3524
+ // "your machine is empty" shown to someone whose machine simply has not been looked at yet. The
3525
+ // skeletons already on the page are the honest picture; the pill narrates the wait.
3526
+ if (state.warming) {
3527
+ renderFreshness(state);
3528
+ startFreshnessPolling();
3529
+ return;
3530
+ }
3531
+
3532
+ preStateHash = state.preStateHash ?? state.generatedAt ?? null;
3533
+ renderHost(state.host, state.generatedAt);
3534
+ const s = state.sections || {};
3535
+ renderBrainPower(s.brainPower);
3536
+ renderWiring(s.wiring);
3537
+ lastMemory = s.memory;
3538
+ renderMemory(s.memory);
3539
+ renderSavings(s.savings);
3540
+ renderSettings(s.config, s.userSettings, s.brainPower);
3541
+ renderGates(s.gates);
3542
+ addRecommendations(s.recommendations, 'state');
3543
+ recsSettled('state', true);
3544
+ dismissStandby(); // first cards are hydrated — the standby line has done its job
3545
+ renderFreshness(state, { justLanded: landed });
3546
+ // Chase the next measurement only when this one is actually OLD. Chasing a fresh reading would
3547
+ // never terminate: every warm serve kicks a background refresh, whose result is strictly newer,
3548
+ // which would repaint and kick again, forever.
3549
+ if (state.stale && !landed) startFreshnessPolling();
3550
+ void loadMemoryFleet(); // 100+ stores at ~90ms each — lands after the page is already usable
3551
+ } catch (err) {
3552
+ dismissStandby(); // don't say "stand by" over an error banner
3553
+ showGlobalError(err);
3554
+ const retry = () => loadState();
3555
+ inlineError('body-wiring', String(err.message || err), retry);
3556
+ inlineError('body-memory', String(err.message || err), retry);
3557
+ inlineError('body-savings', String(err.message || err), retry);
3558
+ inlineError('body-settings', String(err.message || err), retry);
3559
+ for (const id of ['chips-wiring', 'chips-memory', 'chips-savings', 'chips-settings']) {
3560
+ setChips(id, [chip('unavailable', 'grey')]);
3561
+ }
3562
+ recsSettled('state', false);
3563
+ }
3564
+ }
3565
+
3566
+ // The across-your-projects fleet list opens every memory store on the machine. It is the single
3567
+ // slowest thing the console does, so it is fetched on its own and merged into the memory card once
3568
+ // it lands — the health score and everything else are already on screen by then.
3569
+ /* ------------------------------------------------- what caught Claude (the gates + their receipts) */
3570
+
3571
+ // The verdict is the headline, never the inventory. "21 hooks are configured" is a census; "6 of them
3572
+ // can stop a tool call, and here is what they stopped" is the point. An empty ledger says so plainly —
3573
+ // it is the one number on this page that must never be guessed, because the whole claim rests on it.
3574
+ function renderGates(g) {
3575
+ const body = $('#body-gates');
3576
+ if (!g || !g.summary) {
3577
+ setChips('chips-gates', [chip('no data', 'grey')]);
3578
+ body.replaceChildren(el('p', { class: 'muted' }, 'No gate data received.'));
3579
+ return;
3580
+ }
3581
+ const s = g.summary;
3582
+ const caught = s.caughtTotal || 0;
3583
+ setChips('chips-gates', [
3584
+ chip(`${s.blocking} can block`, caught ? 'green' : 'cyan'),
3585
+ chip(caught ? `${caught} caught` : 'nothing caught yet', caught ? 'green' : 'grey'),
3586
+ ]);
3587
+
3588
+ const main = [];
3589
+ // The three numbers are now in one unit (wired entries), so this sentence adds up. It previously
3590
+ // mixed a deduplicated count with a raw one and lost a gate per duplicate wiring — see the note in
3591
+ // scripts/gates.mjs. Duplicates are now STATED rather than absorbed: a gate wired twice really does
3592
+ // run twice, which is a thing the reader would want to know and fix, not a rounding detail.
3593
+ const dupes = Array.isArray(s.duplicated) ? s.duplicated : [];
3594
+ main.push(el('p', { class: 'lead-stat' },
3595
+ 'Every move your AI makes here is read first. ', el('b', {}, String(s.armed)),
3596
+ ' gates are armed — ', el('b', {}, String(s.blocking)),
3597
+ ' of them can stop a tool call before it touches your machine. The other ',
3598
+ el('b', {}, String(s.advisory)), ' add context without ever blocking.',
3599
+ Number.isFinite(s.blockingDistinct) && s.blockingDistinct !== s.blocking
3600
+ ? ` Those ${s.blocking} blocking entries are ${s.blockingDistinct} distinct gates — some are wired more than once.`
3601
+ : '',
3602
+ dupes.length
3603
+ ? el('span', { class: 'muted' }, ` Wired twice, so it runs twice: ${dupes.join(', ')}.`)
3604
+ : '',
3605
+ infoBtn('What caught Claude', GATES_INFO)));
3606
+
3607
+ if (caught) {
3608
+ // Deliberately NOT the .wire-lane grid: its fixed columns are sized for (count, label, meaning)
3609
+ // and fling a gate name and its subject to opposite sides of a dead gap. A catch is a sentence —
3610
+ // who stopped what, and why — so it reads as one.
3611
+ // COLLAPSE IDENTICAL CATCHES — collapse, never hide.
3612
+ //
3613
+ // Measured 2026-07-24: of twelve rows, SIX were the same sentence ("design-wall — deliberate
3614
+ // override, wall skipped"). The card meant to show that the guardrails work instead read as a
3615
+ // log of one guardrail being walked past, six times, because repetition is what the eye counts.
3616
+ // Twelve near-identical amber rows is also simply unreadable.
3617
+ //
3618
+ // Grouping by (gate, reason) keeps every catch represented and every count exact — "× 6" states
3619
+ // the repetition plainly rather than letting six rows imply six different events. The most recent
3620
+ // timestamp is kept because "when did this last happen" is the actionable half. Fable 5, 2026-07-24.
3621
+ const groups = new Map();
3622
+ for (const c of g.catches) {
3623
+ const key = `${c.gate || 'gate'}${c.subject || ''}${c.reason || ''}`;
3624
+ const prev = groups.get(key);
3625
+ if (prev) { prev.n += 1; if (c.at && (!prev.at || Date.parse(c.at) > Date.parse(prev.at))) prev.at = c.at; }
3626
+ else groups.set(key, { ...c, n: 1 });
3627
+ }
3628
+ main.push(el('ul', { class: 'gate-catches' },
3629
+ ...[...groups.values()].map((c) => el('li', {},
3630
+ el('b', {}, c.gate || 'gate'),
3631
+ ' stopped ', el('b', {}, c.subject || 'a call'),
3632
+ c.n > 1 ? el('b', { class: 'catch-n' }, ` × ${c.n}`) : '',
3633
+ ' — ', el('span', { class: 'cell-dim' }, c.reason || ''),
3634
+ c.at ? el('span', { class: 'cell-dim' },
3635
+ (c.n > 1 ? ' · most recent ' : ' · ') + new Date(c.at).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })) : ''))));
3636
+ } else {
3637
+ // Honest empty state. The gates only started writing receipts on 2026-07-17; saying "0 blocks"
3638
+ // as though it were a measured safety record would be a lie of omission.
3639
+ main.push(el('p', { class: 'cell-dim' },
3640
+ 'Nothing caught yet. The gates began recording every refusal on 17 Jul — from here on, each ',
3641
+ 'time one stops your AI, the reason lands on this card. Silence here means silence, not proof.'));
3642
+ }
3643
+
3644
+ if (Array.isArray(s.duplicated) && s.duplicated.length) {
3645
+ main.push(el('p', { class: 'cell-dim' },
3646
+ '⚠ ', el('b', {}, s.duplicated.join(', ')),
3647
+ s.duplicated.length > 1 ? ' are wired twice' : ' is wired twice',
3648
+ ' — once machine-wide and once by the plugin, so they run twice on every matching call. ',
3649
+ 'Harmless, but it is duplicated work.'));
3650
+ }
3651
+ body.replaceChildren(...main);
3652
+ }
3653
+
3654
+ /* Re-ask an endpoint that answered `warming`, on the same 3s cadence as the freshness poller and
3655
+ with the same 3-minute ceiling. Shared by the two cards that load independently of /api/state, so
3656
+ there is one retry cadence on this page rather than three that drift. */
3657
+ const WARM_RETRY_MS = 3000;
3658
+ const WARM_RETRY_MAX = 60;
3659
+
3660
+ async function loadMemoryFleet(attempt = 0) {
3661
+ try {
3662
+ const m = await getJSON('/api/memory');
3663
+ if (m && m.warming) {
3664
+ // The fleet scan is the slowest thing the console does. Until it lands, the memory card keeps
3665
+ // whatever /api/state already gave it and simply does not claim a fleet — it must never render
3666
+ // "0 projects", which on this card would read as "nothing on your machine learns anything".
3667
+ if (attempt < WARM_RETRY_MAX) setTimeout(() => { void loadMemoryFleet(attempt + 1); }, WARM_RETRY_MS);
3668
+ else recsSettled('health', false); // gave up: say so, never let silence read as an all-clear
3669
+ return;
3670
+ }
3671
+ if (m && Array.isArray(m.fleet) && lastMemory) {
3672
+ lastMemory = { ...lastMemory, fleet: m.fleet };
3673
+ renderMemory(lastMemory);
3674
+ }
3675
+ // ADR-027: the brain advocates rather than waiting to be asked. These are the recommendations
3676
+ // derived from what the fleet scan just SAW — a corrupt index, a starving learner, stores full
3677
+ // of memories that have never been distilled into anything reusable. They were built and
3678
+ // schema-gated for a full day before anything rendered them.
3679
+ if (m && Array.isArray(m.recommendations)) addRecommendations(m.recommendations, 'health');
3680
+ recsSettled('health', true);
3681
+ } catch {
3682
+ // An advocacy failure must be VISIBLE as a failure, never as an all-clear.
3683
+ recsSettled('health', false);
3684
+ }
3685
+ }
3686
+
3687
+ /* ONE fetch, same reason as loadState: `?fast=1` and the bare URL route to the identical cache-first
3688
+ handler (the server strips the query string), so the old two-call sequence painted the same bytes
3689
+ twice. The stack audit is the endpoint that answered COLD in 23.6 SECONDS on the request path
3690
+ before the instant-open fix; it is now measured only in the detached child. */
3691
+ async function loadStack(attempt = 0) {
3692
+ try {
3693
+ const stack = await getJSON('/api/stack');
3694
+ if (stack && stack.warming) {
3695
+ setChips('chips-stack', [chip('auditing your stack…', 'wait')]);
3696
+ if (attempt < WARM_RETRY_MAX) setTimeout(() => { void loadStack(attempt + 1); }, WARM_RETRY_MS);
3697
+ else { setChips('chips-stack', [chip('audit is taking unusually long', 'warn')]); recsSettled('stack', false); }
3698
+ return; // skeleton stays: an empty package list would read as "nothing is installed"
3699
+ }
3700
+ renderStack(stack);
3701
+ addRecommendations(stack.recommendations, 'stack');
3702
+ recsSettled('stack', true);
3703
+ } catch (err) {
3704
+ if (stackTicker) { clearInterval(stackTicker); stackTicker = null; }
3705
+ setChips('chips-stack', [chip('couldn’t audit', 'grey')]);
3706
+ inlineError('body-stack', String(err.message || err), () => { stackSkeleton(); loadStack(); });
3707
+ recsSettled('stack', false);
3708
+ }
3709
+ }
3710
+
3711
+ /* ------------------------------------------------------------------- mock ---
3712
+ Development-only fixtures matching console/CONTRACT.md exactly.
3713
+ Active ONLY with ?mock=1 in the URL — never the default. */
3714
+
3715
+ const MOCK_STATE = {
3716
+ token: 'mock', generatedAt: new Date().toISOString(),
3717
+ preStateHash: 'mock-hash-1',
3718
+ host: { user: 'stuartkerr', platform: 'darwin', node: 'v22.14.0', npmPrefix: '~/.npm-global' },
3719
+ sections: {
3720
+ wiring: {
3721
+ summary: { npx: 190, global: 12, mcp: 6, plugin: 5, projectsWithNpx: 16 },
3722
+ sites: [
3723
+ { scope: 'project', project: 'ruvnet-brain', file: '.claude/settings.json', event: 'PreToolUse', matcher: 'Bash', spec: 'npx @claude-flow/cli@latest hooks pre-command', mechanism: 'NPX' },
3724
+ { scope: 'project', project: 'ruvnet-brain', file: '.claude/settings.json', event: 'PostToolUse', matcher: 'Write|Edit', spec: 'npx @claude-flow/cli@latest hooks post-edit', mechanism: 'NPX' },
3725
+ { scope: 'project', project: 'PowerPlatePulse', file: '.claude/settings.json', event: 'PreToolUse', matcher: 'Bash', spec: 'npx claude-flow@alpha hooks pre-command', mechanism: 'NPX' },
3726
+ { scope: 'global', file: '~/.claude/settings.json', event: 'SessionStart', matcher: '.*', spec: '~/.npm-global/bin/ruflo hooks session-start', mechanism: 'GLOBAL_BINARY' },
3727
+ ],
3728
+ },
3729
+ memory: {
3730
+ fleet: [
3731
+ { name: 'ruvnet-brain', total: 1023, embedded: 1021, coverPct: 99.8, patterns: 456, learns: true, findings: [] },
3732
+ { name: 'PowerPlatePulse', total: 214, embedded: 214, coverPct: 100, patterns: 88, learns: true, findings: ['no checkpoint yet'] },
3733
+ ],
3734
+ health: {
3735
+ project: 'ruvnet-brain', score: 92, summary: 'learns; recall-quality not probed',
3736
+ dimensions: [
3737
+ { key: 'liveness', label: 'Liveness', status: 'ok', detail: 'store→search round-trip works on the live path', deduction: 0 },
3738
+ { key: 'coverage', label: 'Coverage', status: 'ok', detail: 'checkpoint present, <1d old', deduction: 0 },
3739
+ { key: 'recallQuality', label: 'Recall quality', status: 'notTested', detail: 'no embedding round-trip run this session', deduction: 0 },
3740
+ { key: 'compactionSurvival', label: 'Compaction survival', status: 'warn', detail: 'last PreCompact snapshot is 9 days old', deduction: 8 },
3741
+ { key: 'sessionSurfacing', label: 'Session surfacing', status: 'ok', detail: 'SessionStart hook surfaces state', deduction: 0 },
3742
+ ],
3743
+ notTested: ['recallQuality'],
3744
+ },
3745
+ },
3746
+ savings: {
3747
+ totals: { count: 3, usdSaved: 0.42, msSaved: 18400 },
3748
+ note: 'receipts only — no modelled or projected savings',
3749
+ utilization: {
3750
+ frontierModel: 'claude-fable-5', tasks: 3, unpriced: 0,
3751
+ realizedUsd: 0.24, frontierUsd: 1.10, costOptimalitySaved: 0.86, pctSaved: 78,
3752
+ distribution: [
3753
+ { band: 'mechanical', label: 'Mechanical', tasks: 0, pctOfTasks: 0, realizedUsd: 0, frontierUsd: 0, savedUsd: 0, models: [] },
3754
+ { band: 'cheap', label: 'Cheap', tasks: 2, pctOfTasks: 67, realizedUsd: 0.05, frontierUsd: 0.50, savedUsd: 0.45, models: [{ model: 'claude-haiku-4.5', tasks: 2 }] },
3755
+ { band: 'mid', label: 'Mid', tasks: 1, pctOfTasks: 33, realizedUsd: 0.19, frontierUsd: 0.60, savedUsd: 0.41, models: [{ model: 'claude-sonnet-5', tasks: 1 }] },
3756
+ { band: 'frontier', label: 'Frontier', tasks: 0, pctOfTasks: 0, realizedUsd: 0, frontierUsd: 0, savedUsd: 0, models: [] },
3757
+ ],
3758
+ note: 'Offline demo — the live console recomputes this from your real receipts.',
3759
+ },
3760
+ receipts: [
3761
+ { at: '2026-07-13T14:20:00Z', capability: 'model-routing', task: 'changelog summarization', chosenTier: 'claude-haiku-4.5', baselineTier: 'claude-fable-5', measuredMs: 4200, measuredUsd: 0.14 },
3762
+ { at: '2026-07-13T15:02:00Z', capability: 'model-routing', task: 'commit message drafts', chosenTier: 'claude-haiku-4.5', baselineTier: 'claude-sonnet-5', measuredMs: 6100, measuredUsd: 0.09 },
3763
+ { at: '2026-07-14T09:41:00Z', capability: 'agentic-qe', task: 'regression triage', chosenTier: 'claude-sonnet-5', baselineTier: 'claude-fable-5', measuredMs: 8100, measuredUsd: 0.19 },
3764
+ ],
3765
+ },
3766
+ config: {
3767
+ path: '~/.claude/ruvnet-brain/config.json', exists: true,
3768
+ values: { openrouterKey: true, nightly: true, routing: 'auto', qeFleet: false },
3769
+ schema: [
3770
+ { key: 'openrouterKey', label: 'OpenRouter API key', type: 'secret', help: 'Unlocks cheap-model routing + the self-improvement loop', secret: true },
3771
+ { key: 'nightly', label: 'Nightly brain refresh', type: 'bool', help: 'Rebuild the KB from pinned SHAs overnight' },
3772
+ { key: 'routing', label: 'Token-smart routing', type: 'enum', options: ['auto', 'off'], help: 'Route cheap tasks to smaller models' },
3773
+ { key: 'qeFleet', label: 'On-demand QE fleet', type: 'bool', help: 'Agentic-QE test fleet, spun up on request' },
3774
+ ],
3775
+ },
3776
+ recommendations: [
3777
+ {
3778
+ id: 'save-preferences', title: 'Remember that you keep npx in helix-experiments on purpose',
3779
+ rationale: 'You told us this once — recording it stops us re-suggesting it forever.',
3780
+ severity: 'INFO', touchesMachine: false,
3781
+ evidence: [{ observed: '12 npx sites in helix-experiments marked "intentional" on 2026-07-10', source: 'operator-profile statedPreferences' }],
3782
+ cost: { time: '~0s', latency: 'none', usd: 0, risk: 'low' },
3783
+ change: { kind: 'write-config', human: 'record the preference in your RuvNet-Brain settings file' },
3784
+ undo: { kind: 'restore-file', human: 'the previous settings file is backed up and restorable' },
3785
+ },
3786
+ ],
3787
+ },
3788
+ };
3789
+
3790
+ const MOCK_STACK = {
3791
+ packages: [
3792
+ { name: 'ruflo', installed: '3.30.2', target: '3.30.2', tag: 'alpha', state: 'CURRENT' },
3793
+ { name: '@ruvector/rvf', installed: '0.2.3', target: '0.2.3', tag: 'latest', state: 'CURRENT' },
3794
+ { name: 'agentic-flow', installed: '1.9.1', target: '1.8.4', tag: 'latest', state: 'AHEAD' },
3795
+ { name: '@ruvector/edge-net', installed: null, target: '0.4.0', tag: 'latest', state: 'BROKEN' },
3796
+ { name: 'agentdb', installed: '3.0.0-alpha.17', target: '3.0.0-alpha.19', tag: 'alpha', state: 'BEHIND' },
3797
+ { name: 'qudag-cli', installed: '0.7.2', target: null, tag: 'latest', state: 'UNRESOLVED' },
3798
+ ],
3799
+ shadows: [
3800
+ { name: '@ruvector/rvf', version: '0.1.9', global: '0.2.3', dir: '~/.npm/_npx/a1b2c3d4e5f6/node_modules/@ruvector/rvf', stale: true },
3801
+ { name: 'claude-flow', version: '2.7.0', global: '2.7.0', dir: '~/.npm/_npx/f6e5d4c3b2a1/node_modules/claude-flow', stale: false },
3802
+ ],
3803
+ summary: { total: 6, behind: 1, broken: 1, ahead: 1, current: 2, shadows: 2, stale: 1 },
3804
+ recommendations: [
3805
+ {
3806
+ id: 'sync-stack', title: 'Sync 1 stale shadow of @ruvector/rvf',
3807
+ rationale: 'A second copy in the npx cache preempts your global binary and quietly serves 0.1.9.',
3808
+ severity: 'IMPORTANT', touchesMachine: true,
3809
+ plainImpact: 'This removes an extra, out-of-date copy of a tool sitting in a temporary folder on your computer. Your main copy is newer and stays untouched. Nothing you use will stop working — the temporary copy rebuilds itself automatically the next time it’s needed. Fully reversible.',
3810
+ evidence: [{ observed: '@ruvector/rvf@0.1.9 in ~/.npm/_npx while global is 0.2.3', source: 'stack-sync findShadows' }],
3811
+ cost: { time: '~0s', latency: 'none', usd: 0, risk: 'low' },
3812
+ change: { kind: 'run-script', human: 'purge the stale npx shadow', cmd: 'node scripts/stack-sync.mjs --sync' },
3813
+ undo: { kind: 'restore-dir', human: 'npx re-resolves on next use; backup kept at <dir>.bak-<ts>' },
3814
+ },
3815
+ ],
3816
+ };
3817
+
3818
+ /* Capability fixture — ?mock=1 ONLY, never a default. It exists so all FOUR states can be seen side
3819
+ by side while styling: the whole design claim is that "not checked" is visually unmistakable from
3820
+ "off" AND from "not installed", and that claim is unfalsifiable without every state on screen at
3821
+ once. Shapes match the contract exactly, including the [{ observed }] evidence records
3822
+ capability-audit.mjs emits and the {human, cmd} turnOn the registry ships.
3823
+
3824
+ THE FIRST ROW USED TO CARRY A RETIRED LIE. Its evidence read "26 hooks are registered on this
3825
+ machine and 0 are enabled" — the exact false finding that was traced to a field-name bug in
3826
+ ruflo's table renderer and removed from every detector in this repo. Left in a fixture, it was
3827
+ still the reference image someone would style against, and the sentence a screenshot would show.
3828
+ A retired falsehood preserved as a design sample is how it gets reintroduced. */
3829
+ const MOCK_CAPABILITIES = {
3830
+ rows: [
3831
+ {
3832
+ key: 'mock:cross-project-lessons', label: 'Cross-project lessons', state: 'OFF', scope: 'user',
3833
+ whatItBuysYou: 'A rule you have taught in three separate projects gets applied everywhere, instead of being re-taught project by project forever.',
3834
+ evidence: [{ observed: '4 processes you have taught in multiple separate projects are still trapped at project level' }],
3835
+ turnOn: { human: 'Promote the processes you have proven in several projects', cmd: 'node scripts/lesson-promote.mjs --apply' },
3836
+ },
3837
+ {
3838
+ key: 'mock:learning-hooks', label: 'Learning hooks', state: 'UNKNOWN', scope: 'machine',
3839
+ whatItBuysYou: 'Your AI writes down which approach actually worked and reuses it next time, instead of solving the same problem from scratch every session.',
3840
+ evidence: [{ observed: 'ruflo is installed, but whether its learning hooks are switched on cannot be read from it — its hook list is a static catalog, not a state readout' }],
3841
+ turnOn: null,
3842
+ },
3843
+ {
3844
+ key: 'mock:cheap-routing', label: 'Cheap-model routing', state: 'ABSENT', scope: 'machine',
3845
+ whatItBuysYou: 'Reading and summarising work runs on a model that costs a fraction of the top-tier one, and each run leaves a receipt showing what it saved.',
3846
+ evidence: [{ observed: 'agentic-flow is not installed and no routing receipts exist, so cheap routing has never been set up here' }],
3847
+ turnOn: { human: 'Route one read-only task through the cheap path', cmd: 'node scripts/route-cheap.mjs --task "<text>"' },
3848
+ },
3849
+ {
3850
+ key: 'mock:harness-champion', label: 'Harness champion policy', state: 'ON', scope: 'machine',
3851
+ whatItBuysYou: 'Runs your agents on the best-scoring policy found so far rather than the stock one.',
3852
+ evidence: [{ observed: 'a champion policy is active, applied 6 days ago' }],
3853
+ turnOn: null,
3854
+ },
3855
+ ],
3856
+ };
3857
+
3858
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3859
+
3860
+ /* MOCK PARITY WITH THE FRESHNESS CONTRACT (RVBC-INSTANT-SPEC #7).
3861
+ *
3862
+ * The mock existed so the page could be styled without a server — and it silently stopped matching
3863
+ * the server the day cache-first serving landed: it answered `/api/state` with no `fromCache`, no
3864
+ * `measuredAt`, no `stale`, and it had no answer at all for `?fast=1` (the poller's URL) or for the
3865
+ * warming state. So the three states the freshness work exists to make visible — warming, stale,
3866
+ * just-landed — were the exact three a designer could not see. A fixture that cannot show the new
3867
+ * states is a fixture that guarantees they will be styled by guesswork.
3868
+ *
3869
+ * `?mock=1&state=warming|stale|fresh` picks the variant; the default is a stale warm serve, which is
3870
+ * what a real re-open looks like. The stamps are computed at call time so ages read believably. */
3871
+ const MOCK_VARIANT = new URLSearchParams(location.search).get('state') || 'stale';
3872
+
3873
+ function mockState() {
3874
+ if (MOCK_VARIANT === 'warming') {
3875
+ return { warming: true, scope: '/Users/you/Code/your-project', kicked: true, fromCache: false, measuredAt: null, ageMs: null, stale: true };
3876
+ }
3877
+ const ageMs = MOCK_VARIANT === 'fresh' ? 4_000 : 46 * 60_000; // fresh: seconds; stale: past the 15m ceiling
3878
+ const at = new Date(Date.now() - ageMs).toISOString();
3879
+ return { ...structuredClone(MOCK_STATE), generatedAt: at, fromCache: true, cachedAt: at, measuredAt: at, ageMs, stale: ageMs > 15 * 60_000 };
3880
+ }
3881
+
3882
+ async function mockGet(url) {
3883
+ // The server routes on the path alone (it strips the query string), so the mock must too —
3884
+ // otherwise `?fast=1` throws "no mock" and the poller looks broken only in mock.
3885
+ const p = url.split('?')[0];
3886
+ if (p === '/api/state') { await sleep(250); return mockState(); }
3887
+ if (p === '/api/stack') { await sleep(2400); return structuredClone(MOCK_STACK); }
3888
+ if (p === '/api/memory') { await sleep(1200); return { fleet: MOCK_STATE.sections.memory.fleet, recommendations: [] }; }
3889
+ throw new Error(`no mock for ${url}`);
3890
+ }
3891
+
3892
+ async function mockPost(url, body) {
3893
+ await sleep(850);
3894
+ if (url === '/api/refresh') return { status: 200, ok: true, data: { ok: true, refreshing: true, started: true } };
3895
+ if (url === '/api/save-brain-power') {
3896
+ return { status: 200, ok: true, data: { ok: true, off: !!(body.values || {}).off, log: 'mock: the switch moved and was read back from disk' } };
3897
+ }
3898
+ if (url === '/api/save-brain-profile') {
3899
+ return { status: 200, ok: true, data: { ok: true, profile: body.values?.brainProfile, bytesFreed: 640_000_000, log: 'mock: profile applied' } };
3900
+ }
3901
+ if (url === '/api/apply') {
3902
+ return { status: 200, ok: true, data: { results: (body.ids || []).map((id) => ({
3903
+ id, ok: true, undoToken: `undo-${id}`,
3904
+ log: `[stack-sync] backup: ~/.npm/_npx/a1b2c3d4e5f6 → ~/.npm/_npx/a1b2c3d4e5f6.bak-1752500000\n[stack-sync] purged stale shadow @ruvector/rvf@0.1.9\n[stack-sync] verified: global 0.2.3 now answers`,
3905
+ })) } };
3906
+ }
3907
+ if (url === '/api/save-config') {
3908
+ return { status: 200, ok: true, data: { ok: true, backup: '~/.claude/ruvnet-brain/config.json.bak-1752500000', undoToken: 'undo-config-1' } };
3909
+ }
3910
+ if (url === '/api/undo') return { status: 200, ok: true, data: { ok: true } };
3911
+ return { status: 404, ok: false, data: {} };
3912
+ }
3913
+
3914
+ /* -------------------------------------------------------------------- init */
3915
+
3916
+ initTheme();
3917
+ loadState();
3918
+ loadStack();
3919
+ loadTrust();
3920
+ loadCapabilities();
3921
+ $('#recheck-btn')?.addEventListener('click', () => recheckMachine());
3922
+
3923
+ // Stack card leads (Stuart 2026-07-16): expand immediately on a true first visit so newcomers
3924
+ // watch it populate; afterwards only real drift opens it (renderStack). A manual toggle by the
3925
+ // user wins over both — mark it so the auto-open never fights a deliberate collapse.
3926
+ {
3927
+ const sc = $('#card-stack');
3928
+ if (sc) {
3929
+ sc.querySelector('summary')?.addEventListener('click', () => { sc.dataset.userToggled = '1'; });
3930
+ if (!localStorage.getItem('rvbc-seen')) sc.open = true;
3931
+ }
3932
+ }
3933
+
3934
+ /* ═══════════════════════════════════════════════════════════════════════════════════════════════
3935
+ LESSONS — "what it's learned from you"
3936
+
3937
+ THE GAP: sixteen lessons, thirteen of them the owner's own words, one enforcing at BLOCK level,
3938
+ all of them invisible on this page until now. Owner, 2026-07-24: "murky things in a .claude file
3939
+ nobody sees." A rule you cannot see is a rule you never consented to.
3940
+
3941
+ ONE CONTROL, NOT TWO. The obvious design is a checkbox AND an ✕ ("turn off" vs "remove"). The
3942
+ store has exactly one off-switch — demote() — which is reversible and KEEPS the record of where
3943
+ the lesson was taught. Shipping two controls that call one function is the precise flavour of
3944
+ fake granularity that makes people close a settings page. So: one checkbox, and the row says
3945
+ plainly what off means.
3946
+
3947
+ NOTHING IS ASSERTED. Every row's state is read from the store on load, and every write re-reads
3948
+ from disk and renders what actually changed. A toggle that reports success without re-reading is
3949
+ the failure user-settings.mjs exists to end.
3950
+ ═══════════════════════════════════════════════════════════════════════════════════════════════ */
3951
+
3952
+ const LESSON_ENF_TONE = { block: 'amber', checklist: 'cyan', review: 'grey' };
3953
+
3954
+ function lessonsSkeleton() {
3955
+ $('#body-lessons')?.replaceChildren(
3956
+ frag('<div class="skeleton" aria-hidden="true"><div class="sk-bar w45"></div><div class="sk-bar w85"></div></div>'));
3957
+ setChips('chips-lessons', [chip('checking…', 'wait')]);
3958
+ }
3959
+
3960
+ /* Turn a lesson ON. Two store verbs can stand between a lesson and "in force": it may be demoted
3961
+ (user switched it off) and/or unratified (never agreed to). Clear whichever apply, in that order,
3962
+ and let each call verify itself — rather than inventing a compound verb the store does not have. */
3963
+ /* postJSON returns {status, ok, data} where `ok` is the HTTP status — NOT the handler's verdict.
3964
+ Reading it directly meant a server-side refusal ({ok:false} sent with HTTP 200) rendered as
3965
+ success while the store never moved: the checkbox stays flipped, the note says it saved, and the
3966
+ rule is not actually off. That is the precise "every writer returned ok:true" failure this
3967
+ codebase already paid for once. This unwraps to the BODY and judges on the body's own verdict. */
3968
+ async function setLessonCall(id, action) {
3969
+ const r = await postJSON('/api/set-lesson', { id, action });
3970
+ const body = (r && r.data) || {};
3971
+ if (!r || !r.ok) return { ok: false, log: r && r.status === 403 ? TOKEN_MSG : `the console returned ${r ? r.status : '?'}` };
3972
+ return body;
3973
+ }
3974
+
3975
+ async function lessonOn(row) {
3976
+ let last = null;
3977
+ if (row.demoted) {
3978
+ last = await setLessonCall(row.id, 'restore');
3979
+ if (!last || !last.ok) return last;
3980
+ }
3981
+ if (!row.ratified) return setLessonCall(row.id, 'ratify');
3982
+ // Return the RESTORE's payload rather than a synthetic one. The first version discarded it and
3983
+ // fabricated `{ok, log}` with no `now`, so the row fell back to printing the raw lesson id —
3984
+ // internal jargon on the one line the user reads to find out what just happened.
3985
+ return last || { ok: true, now: { status: 'ratified', demoted: false } };
3986
+ }
3987
+
3988
+ function renderLessons(data) {
3989
+ const body = $('#body-lessons');
3990
+ if (!body) return;
3991
+
3992
+ if (!data || !data.ok) {
3993
+ setChips('chips-lessons', [chip('not checked', 'nt')]);
3994
+ body.replaceChildren(el('p', { class: 'loading-note' },
3995
+ data && data.error ? `Couldn't read the lesson store: ${data.error}` : 'Couldn’t read the lesson store.'));
3996
+ return;
3997
+ }
3998
+
3999
+ const rows = data.lessons || [];
4000
+ const c = data.counts || {};
4001
+
4002
+ if (!rows.length) {
4003
+ setChips('chips-lessons', [chip('nothing yet', 'grey')]);
4004
+ body.replaceChildren(el('p', { class: 'loading-note' },
4005
+ 'No lessons recorded yet. When you correct me and that correction proves durable, it shows up here — ' +
4006
+ 'with a switch, so you decide whether it stays.'));
4007
+ return;
4008
+ }
4009
+
4010
+ const chips = [];
4011
+ if (c.awaitingYou) chips.push(chip(`${c.awaitingYou} awaiting you`, 'amber', 'Recorded, but not yet agreed to by you. Until you decide, it does not enforce at full strength.'));
4012
+ if (c.active) chips.push(chip(`${c.active} on`, 'green'));
4013
+ if (c.off) chips.push(chip(`${c.off} off`, 'grey', 'Switched off by you. The record of where you taught it is kept.'));
4014
+ if (c.blocking) chips.push(chip(`${c.blocking} can stop me`, 'cyan', 'These interrupt me at their moment and I cannot continue until the check passes.'));
4015
+ setChips('chips-lessons', chips);
4016
+
4017
+ const list = el('div', { class: 'cap-list' });
4018
+
4019
+ for (const r of rows) {
4020
+ const isOn = r.ratified && !r.demoted;
4021
+
4022
+ const box = el('input', {
4023
+ type: 'checkbox', class: 'lesson-switch', id: `lsw-${r.id}`,
4024
+ 'aria-label': `${isOn ? 'Turn off' : 'Turn on'}: ${r.statement.slice(0, 60)}`,
4025
+ });
4026
+ box.checked = isOn;
4027
+
4028
+ const note = el('span', { class: 'form-note', role: 'status' });
4029
+
4030
+ box.addEventListener('change', async () => {
4031
+ const want = box.checked;
4032
+ box.disabled = true;
4033
+ note.textContent = want ? 'turning on…' : 'turning off…';
4034
+ try {
4035
+ const res = want ? await lessonOn(r) : await setLessonCall(r.id, 'demote');
4036
+ if (!res || !res.ok) {
4037
+ box.checked = !want; // the store did not move; neither does the UI
4038
+ note.textContent = (res && res.log) || 'that didn’t save — nothing changed';
4039
+ note.className = 'form-note n-err';
4040
+ return;
4041
+ }
4042
+ note.className = 'form-note';
4043
+ // Report the state read back from DISK, not the state we asked for.
4044
+ // Plain words only. `res.now.status` is "ratified" — a word about our data model, not about
4045
+ // the user's day. It never reaches the page.
4046
+ note.textContent = res.now
4047
+ ? (res.now.demoted ? 'off — the record of where you taught it is kept' : 'on — in force from now on')
4048
+ : 'saved';
4049
+ announce(`${r.id} ${want ? 'turned on' : 'turned off'}.`);
4050
+ // RE-READ THE WHOLE CARD FROM THE SERVER.
4051
+ //
4052
+ // Before this, a successful toggle updated exactly two things — the checkbox and this note —
4053
+ // while the header counts ("13 on", "3 awaiting you"), the row's on/off styling, and above
4054
+ // all the PARTITION kept describing the previous world. Concretely: switch a rule off and it
4055
+ // stayed filed under "The N rules already in force". That is not a stale number; it is a
4056
+ // false sentence about the user's machine, printed by the card whose whole purpose is to
4057
+ // tell them the truth about it. Turning a candidate ON left it under "needs your decision" —
4058
+ // the decision they had just made.
4059
+ //
4060
+ // Re-fetching is deliberately unconditional and unclever: the server already computes every
4061
+ // derived field, so anything patched up here would be a second implementation of the same
4062
+ // logic, free to drift from it. Found by GPT-5.6-Sol, 2026-07-24.
4063
+ loadLessons();
4064
+ } catch (e) {
4065
+ box.checked = !want;
4066
+ note.textContent = `that didn’t save — ${String(e.message || e)}`;
4067
+ note.className = 'form-note n-err';
4068
+ } finally { box.disabled = false; }
4069
+ });
4070
+
4071
+ const meta = [
4072
+ chip(r.enforcementLabel, LESSON_ENF_TONE[r.enforcement] || 'grey', r.enforcementDetail),
4073
+ chip(r.origin, r.userStated ? 'green' : 'nt',
4074
+ r.userStated
4075
+ ? 'You said this. Only lessons you stated yourself are allowed to reach the strongest level.'
4076
+ : '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.'),
4077
+ r.taughtCount ? chip(`taught ${r.taughtCount}×`, 'grey') : null,
4078
+ r.awaitingYou ? chip('awaiting your decision', 'amber', 'Recorded, but you have not agreed to it yet.') : null,
4079
+ ].filter(Boolean);
4080
+
4081
+ const why = el('details', { class: 'cap-why' },
4082
+ el('summary', null, 'What is this, and why is it here?'),
4083
+ el('div', { class: 'cap-why-body' },
4084
+ el('p', null, r.statement),
4085
+ el('p', null, el('strong', null, 'When it fires: '), r.when, '.'),
4086
+ el('p', null, el('strong', null, `${r.enforcementLabel}: `), r.enforcementDetail),
4087
+ // `evidence` is an ARRAY of {observed} records, not a string. Rendering it directly printed
4088
+ // "[object Object]" — the exact defect that got ADR-045 rejected, caught here only because
4089
+ // the endpoint was hit with real data instead of being reasoned about.
4090
+ Array.isArray(r.evidence) && r.evidence.length
4091
+ ? el('div', null,
4092
+ el('p', null, el('strong', null, r.evidence.length > 1 ? 'What I observed: ' : 'What I observed: ')),
4093
+ el('ul', { class: 'cap-ev' }, ...r.evidence
4094
+ .map((e) => (e && typeof e === 'object' ? e.observed : e))
4095
+ .filter((t) => typeof t === 'string' && t.trim())
4096
+ .map((t) => el('li', null, t))))
4097
+ : null,
4098
+ r.projects && r.projects.length
4099
+ ? el('p', null, el('strong', null, 'Learned in: '), r.projects.join(', ')) : null,
4100
+ el('p', { class: 'muted' },
4101
+ 'Turning this off hides the rule without deleting the record of where you taught it — ',
4102
+ 'you can switch it back on here at any time.')));
4103
+
4104
+ list.append(el('div', { class: `cap-row lesson-row${r.demoted ? ' is-off' : ''}` },
4105
+ // NO `for=` here. The label WRAPS its checkbox, which is already an implicit association; a
4106
+ // `for` pointing at the contained input makes the browser activate it twice on one click, so
4107
+ // `change` fired an even number of times and the confirmation text was overwritten back to
4108
+ // empty. Caught by clicking it in a real browser — the DOM structure and the API were both
4109
+ // correct, and the endpoint returned a perfect payload the whole time.
4110
+ el('label', { class: 'cap-row-head' },
4111
+ box,
4112
+ el('span', { class: 'cap-row-title' }, r.statement),
4113
+ ),
4114
+ el('div', { class: 'chips' }, ...meta),
4115
+ el('p', { class: 'cap-row-when muted' }, r.when),
4116
+ why, note));
4117
+ }
4118
+
4119
+ body.replaceChildren(
4120
+ el('p', { class: 'loading-note' },
4121
+ 'These are the rules I now work by on this machine. You can switch any of them off — ',
4122
+ 'nothing here is permanent, and off is reversible.',
4123
+ infoBtn('What it’s learned from you', LESSONS_INFO)),
4124
+ ...partitionLessons(list, rows));
4125
+ }
4126
+
4127
+ /* Split the rows into "needs you" and "already settled", and fold the settled ones away.
4128
+ *
4129
+ * WHY: graded 78 on 2026-07-24 with the single largest deduction (-8) being that this card ran to
4130
+ * roughly 40% of the whole page — sixteen uncapped rows that a first-time visitor had to scroll past
4131
+ * before reaching anything else. The content was right and the SHAPE was wrong, which is a distinct
4132
+ * failure: a card that answers its question honestly can still bury the six cards beneath it.
4133
+ *
4134
+ * The split is not arbitrary trimming. Exactly one group is a question being put to the user
4135
+ * (candidates awaiting ratification); the rest is a reference list they may audit whenever they like.
4136
+ * Showing a question and a reference list at the same visual weight is what made it a wall. */
4137
+ function partitionLessons(list, rows) {
4138
+ const kids = [...list.children];
4139
+ // THREE groups, not two. The first version split on `awaitingYou` alone and swept everything else
4140
+ // into "already in force" — so the moment a user switched a rule OFF, the page filed it under a
4141
+ // heading asserting it was ON. Not a stale count: a false sentence about their machine, printed by
4142
+ // the card whose job is to be the truth about it, and printed BECAUSE they used the control we
4143
+ // gave them. Found by GPT-5.6-Sol, 2026-07-24. A group's heading must be derivable from the state
4144
+ // of the rows inside it, which is why the counts below are computed from the split, never passed in.
4145
+ const asks = [], inForce = [], off = [];
4146
+ rows.forEach((r, i) => {
4147
+ if (r.demoted) off.push(kids[i]);
4148
+ else if (r.awaitingYou) asks.push(kids[i]);
4149
+ else inForce.push(kids[i]);
4150
+ });
4151
+ const out = [];
4152
+ if (asks.length) {
4153
+ out.push(el('p', { class: 'lessons-ask-h' },
4154
+ `${asks.length} ${asks.length === 1 ? 'rule needs' : 'rules need'} your decision`),
4155
+ el('p', { class: 'muted lessons-ask-b' },
4156
+ 'I noticed these myself, so they are not in force and cannot stop me until you agree. ',
4157
+ 'Leaving them off is a perfectly good answer.'),
4158
+ el('div', { class: 'cap-list' }, ...asks));
4159
+ }
4160
+ if (inForce.length) {
4161
+ out.push(el('details', { class: 'lessons-more' },
4162
+ el('summary', null,
4163
+ `The ${inForce.length} ${inForce.length === 1 ? 'rule' : 'rules'} already in force — open to review or switch any off`),
4164
+ el('div', { class: 'cap-list' }, ...inForce)));
4165
+ }
4166
+ if (off.length) {
4167
+ // Switched-off rules get their OWN fold rather than being hidden. Hiding them would make the
4168
+ // control feel like deletion, and the whole promise of the switch is that it is not.
4169
+ out.push(el('details', { class: 'lessons-more' },
4170
+ el('summary', null,
4171
+ `${off.length} ${off.length === 1 ? 'rule you switched off' : 'rules you switched off'} — not in force; switch back on any time`),
4172
+ el('div', { class: 'cap-list' }, ...off)));
4173
+ }
4174
+ return out;
4175
+ }
4176
+
4177
+ async function loadLessons() {
4178
+ lessonsSkeleton();
4179
+ try {
4180
+ const res = await fetch('/api/lessons', { headers: { Accept: 'application/json' } });
4181
+ if (!res.ok) throw new Error(`the console returned ${res.status}`);
4182
+ renderLessons(await res.json());
4183
+ } catch (err) {
4184
+ setChips('chips-lessons', [chip('not checked', 'nt')]);
4185
+ inlineError('body-lessons', String(err.message || err), () => loadLessons());
4186
+ }
4187
+ }
4188
+
4189
+ loadLessons();