ruvnet-brain 4.0.12 → 4.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/advocacy-outcomes.mjs +808 -0
- package/plugin/scripts/anticipate.sh +80 -14
- package/plugin/scripts/capability-registry.mjs +994 -0
- package/plugin/scripts/codex-hook-wrapper.mjs +1 -0
- package/plugin/scripts/continuation-gate.mjs +129 -1
- package/plugin/scripts/gates.mjs +146 -0
- package/plugin/scripts/goal-match.mjs +398 -0
- package/plugin/scripts/hijack-ruvnet.sh +69 -1
- package/plugin/scripts/hook-registry.mjs +616 -0
- package/plugin/scripts/hook-shim.mjs +13 -2
- package/plugin/scripts/learning-enable.mjs +382 -0
- package/plugin/scripts/lesson-promote.mjs +262 -0
- package/plugin/scripts/lesson-provenance.mjs +43 -0
- package/plugin/scripts/lesson-store.mjs +67 -56
- package/plugin/scripts/memory-doctor.mjs +345 -0
- package/plugin/scripts/nightly-controller.mjs +98 -0
- package/plugin/scripts/runtime-preferences.mjs +18 -0
- package/plugin/scripts/unprompted-runtime.mjs +22 -7
- package/plugin/scripts/user-settings.mjs +672 -0
- package/plugin/skills/ruvnet-brain/SKILL.md +2 -2
- package/scripts/advocacy-outcomes.mjs +4 -808
- package/scripts/capability-registry.mjs +4 -876
- package/scripts/corpus-qa.mjs +44 -6
- package/scripts/doc-currency.mjs +30 -2
- package/scripts/gates.mjs +4 -146
- package/scripts/goal-match.mjs +4 -398
- package/scripts/hook-registry.mjs +4 -567
- package/scripts/issue-watch.mjs +108 -0
- package/scripts/learning-enable.mjs +4 -380
- package/scripts/lesson-promote.mjs +4 -262
- package/scripts/memory-doctor.mjs +4 -345
- package/scripts/nightly-controller.mjs +4 -66
- package/scripts/nightly-wrapper.sh +23 -1
- package/scripts/proactivity-metrics.mjs +8 -1
- package/scripts/qe/ux-suite.mjs +72 -1
- package/scripts/release-abort-stale.mjs +111 -0
- package/scripts/release-convergence-watchdog.mjs +119 -0
- package/scripts/release-transaction-provider.mjs +46 -6
- package/scripts/release-transaction.mjs +55 -17
- package/scripts/self-update.mjs +63 -10
- package/scripts/user-settings.mjs +4 -640
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* capability-registry.mjs — the data model behind "the top things you own and don't use".
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS, and why it is a REGISTRY rather than more detectors.
|
|
6
|
+
*
|
|
7
|
+
* `capability-audit.mjs` answers "what is dormant?" and it answers it well, but it only speaks up
|
|
8
|
+
* when a detector decides something is WRONG. That shape cannot answer the flat question a person
|
|
9
|
+
* actually asks — "is X on?" — because a healthy capability produces no finding at all, and silence
|
|
10
|
+
* is indistinguishable from "I never looked." The console needs a row per capability whether the
|
|
11
|
+
* news is good, bad, or unavailable.
|
|
12
|
+
*
|
|
13
|
+
* THE ONE RULE THIS FILE EXISTS TO ENFORCE: 'unknown' is a first-class state, and it outranks
|
|
14
|
+
* 'off' every single time a probe could not run. Reporting "off" for something you failed to
|
|
15
|
+
* measure is not a rounding error — it is the exact lie the whole project was built to kill, and
|
|
16
|
+
* it is *easy* to commit here because every underlying helper has a falsy default.
|
|
17
|
+
*
|
|
18
|
+
* That is not a hypothetical. While this file was being written (2026-07-22, ~00:22) a live probe of
|
|
19
|
+
* this repo's own `.swarm/memory.db` came back `{unreadable: 'unable to open database file (14)',
|
|
20
|
+
* learns: false}` — and a naive `learns ? 'on' : 'off'` would have reported "memory distillation is
|
|
21
|
+
* OFF". Re-running the identical query 90 seconds later returned 1201 memories, 99.8% embedded, 596
|
|
22
|
+
* distilled patterns: the store was fully healthy and the first read had simply lost a race with a
|
|
23
|
+
* concurrent writer holding the WAL. One transient lock, and the console would have told its owner
|
|
24
|
+
* to fix a system that was already working. Every detector below therefore maps "could not read" to
|
|
25
|
+
* 'unknown' WITH THE REASON, and only ever says 'off' about a value it genuinely observed.
|
|
26
|
+
*
|
|
27
|
+
* THE SECOND RULE: `turnOn` is null unless the exact command was run with `--help` and the
|
|
28
|
+
* subcommand confirmed present. A confidently-wrong command is worse than no command — it sends a
|
|
29
|
+
* person to a shell to be told "unknown subcommand", which costs them trust in every other row on
|
|
30
|
+
* the page. Six of the eleven capabilities below have `turnOn: null` for that reason, and each one
|
|
31
|
+
* records the negative check that produced the null, so nobody re-litigates it from memory.
|
|
32
|
+
*
|
|
33
|
+
* (That count said FOUR until 2026-08-05 and the real number was seven — stale by three, in the
|
|
34
|
+
* paragraph explaining why nulls must be re-checked. Counted, not remembered:
|
|
35
|
+
* `grep -c '^ turnOn: null,'`. Issue #116 removed one, leaving six.)
|
|
36
|
+
*
|
|
37
|
+
* learning-hooks `ruflo hooks --help` lists list/route/metrics/pretrain/... and NO
|
|
38
|
+
* enable|disable subcommand (grep for "enable" exits 1). There is no CLI
|
|
39
|
+
* that flips them on; inventing one would be fabrication. Deeper still,
|
|
40
|
+
* that capability's own detector proves there is no readable on/off state
|
|
41
|
+
* to flip — see the long note on it before trusting any hook table.
|
|
42
|
+
* harness-evolution CORRECTED 2026-08-05 (issue #116). This block used to read "No `evolve`"
|
|
43
|
+
* and called it VERIFIED NULL. That measurement DRIFTED. Re-measured live
|
|
44
|
+
* against ruflo v3.34.0, which is what a user actually has:
|
|
45
|
+
* --subcommand One of: score | genome | mcp-scan | threat-model |
|
|
46
|
+
* oia-audit | audit-list | audit-trend | similarity | drift-from-history |
|
|
47
|
+
* mint | redblue | learn | gepa | evolve | bench | flywheel
|
|
48
|
+
* `evolve` is there, and so are `bench` and `flywheel`.
|
|
49
|
+
*
|
|
50
|
+
* The stale claim was LOAD-BEARING, not commentary: it justified
|
|
51
|
+
* `turnOn: null`, so the console could never offer an action that had since
|
|
52
|
+
* started existing. A null justified by a measurement must be re-measured,
|
|
53
|
+
* or it silently becomes a lie — the same failure mode as every other
|
|
54
|
+
* drifted assertion in this repo, sitting inside the registry whose whole
|
|
55
|
+
* job is to describe what is actually available.
|
|
56
|
+
*
|
|
57
|
+
* The offer names its precondition. plugin/skills/brain-score/SKILL.md:97 is
|
|
58
|
+
* explicit that the WRITE layer needs OPENROUTER_API_KEY and that we must
|
|
59
|
+
* never claim the evolve loop "just works" without it, so the human text
|
|
60
|
+
* says so rather than handing someone a command that will fail.
|
|
61
|
+
* lessons-in-force Deliberate, not missing: `lesson-seed.mjs --apply` stores CANDIDATES only,
|
|
62
|
+
* because "the model does not get to ratify its own rules." A turnOn here
|
|
63
|
+
* would hand the model the pen it was explicitly denied.
|
|
64
|
+
* session-capture,
|
|
65
|
+
* write-gates,
|
|
66
|
+
* nightly-refresh Turning these on means editing settings.json / loading a launchd plist —
|
|
67
|
+
* multi-step machine mutation with no single verified command, and global
|
|
68
|
+
* Rule 10 forbids handing out system-mutating one-liners unprompted.
|
|
69
|
+
*
|
|
70
|
+
* Everything here is READ-ONLY. It observes; it never installs, enables, or writes.
|
|
71
|
+
*/
|
|
72
|
+
import fs from 'node:fs';
|
|
73
|
+
import path from 'node:path';
|
|
74
|
+
import os from 'node:os';
|
|
75
|
+
import { execFileSync } from 'node:child_process';
|
|
76
|
+
import { fileURLToPath } from 'node:url';
|
|
77
|
+
// Two facts this file must NOT restate in its own words, because it already did and both were wrong
|
|
78
|
+
// (issues #112, #113): the name of the nightly job the installer loads, and which hooks a session
|
|
79
|
+
// really has wired. Both are imported from the modules that own them, statically — a missing sibling
|
|
80
|
+
// here is a broken build caught by tests, not a runtime degradation to paper over.
|
|
81
|
+
import { NIGHTLY_LABEL } from './nightly-controller.mjs';
|
|
82
|
+
import { buildRegistry, REPO } from './hook-registry.mjs';
|
|
83
|
+
|
|
84
|
+
const HOME = os.homedir();
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* REPO is WHERE THIS CODE IS INSTALLED. It is NOT the user's project, and confusing the two was the
|
|
88
|
+
* single most damaging bug this file has shipped.
|
|
89
|
+
*
|
|
90
|
+
* Every `scope: PROJECT` detector used to read REPO, and `auditAll()` took no argument, so the two
|
|
91
|
+
* project-scoped rows always described the ruvnet-brain package directory no matter where the person
|
|
92
|
+
* running the console actually stood. Proven in both directions, and the second one is the harmful one:
|
|
93
|
+
*
|
|
94
|
+
* from an empty folder: "write-gates | ON | 6 gates can refuse a write, and 203 refusals have been
|
|
95
|
+
* recorded" — ruvnet-brain's own numbers, presented as the user's.
|
|
96
|
+
* from a real project
|
|
97
|
+
* holding a healthy
|
|
98
|
+
* 16MB memory store: "memory-distillation | ABSENT | no memory store exists for this project
|
|
99
|
+
* yet" — plus a turnOn button offering to fix a problem they do not have.
|
|
100
|
+
*
|
|
101
|
+
* Anyone not standing inside a ruvnet-brain checkout — which is every user — got one of those two.
|
|
102
|
+
* `capability-audit.mjs` had this right from the start (process.cwd(), with a --repo override); the
|
|
103
|
+
* registry was the file that disagreed, so the registry is the file that changed.
|
|
104
|
+
*
|
|
105
|
+
* REPO survives for exactly one honest purpose: it is the root `dispatchGateWiring()` hands to
|
|
106
|
+
* hook-registry's buildRegistry(). It is IMPORTED from that module rather than recomputed here,
|
|
107
|
+
* because `..` from this file stopped meaning "the repo root" when this file moved into the payload
|
|
108
|
+
* (2026-08-06) — and hook-registry.mjs is the module that owns resolving that root across both
|
|
109
|
+
* shipped layouts. One answer, in the one place that already had to know it.
|
|
110
|
+
*
|
|
111
|
+
* Resolving the scripts THIS package ships is a SEPARATE job, and it is SCRIPTS_DIR's — see below.
|
|
112
|
+
*/
|
|
113
|
+
const DAY = 86_400_000;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A turnOn command must name its script ABSOLUTELY (a relative `node scripts/x.mjs` only runs for
|
|
117
|
+
* someone standing inside a checkout), and it must name a script that EXISTS — the house rule is
|
|
118
|
+
* "never render a control without a real executor", and capability-registry.test.mjs enforces it.
|
|
119
|
+
*
|
|
120
|
+
* Two candidate homes, probed in that order, because this package ships in two shapes:
|
|
121
|
+
* 1. SCRIPTS_DIR — a sibling inside the payload. True in EVERY shipped layout (the Spine's
|
|
122
|
+
* versions/<gen>/scripts, the plugin cache's <ver>/scripts, and this file's
|
|
123
|
+
* own <src>/plugin/scripts), so payload tools resolve everywhere.
|
|
124
|
+
* 2. <root>/scripts — the repo-root scripts/ dir, which exists in a git checkout and in the npm
|
|
125
|
+
* tarball but NOT in the flattened plugin payload. Tools that live only
|
|
126
|
+
* there (distill-project.mjs, route-cheap.mjs — both wired into the console's
|
|
127
|
+
* remedy registry and the installer's router-tools copy, so relocating them
|
|
128
|
+
* is a different change with a different blast radius) resolve here.
|
|
129
|
+
*
|
|
130
|
+
* NULL WHEN NEITHER HOLDS IT. `turnOn: null` is an established, tested shape in this file — it is how
|
|
131
|
+
* every capability with no verified command already renders — and both consumers (console-engine's
|
|
132
|
+
* offer builder and anticipate.sh's one line) already treat a null/blank cmd as "no button". Emitting
|
|
133
|
+
* a plausible-looking `node …/route-cheap.mjs` that ENOENTs on a Spine install would be strictly
|
|
134
|
+
* worse than saying nothing: the whole point of this registry is that it does not claim what it did
|
|
135
|
+
* not check.
|
|
136
|
+
*/
|
|
137
|
+
const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
138
|
+
function selfScript(name, args) {
|
|
139
|
+
const home = [SCRIPTS_DIR, path.join(REPO, 'scripts')].find((d) => fs.existsSync(path.join(d, name)));
|
|
140
|
+
if (!home) return null;
|
|
141
|
+
// plain quotes, not JSON.stringify: JSON doubles every backslash on Windows and users copy-paste this
|
|
142
|
+
return `node "${path.join(home, name)}"${args ? ` ${args}` : ''}`;
|
|
143
|
+
}
|
|
144
|
+
/** `{human, cmd}` only when the executor is really there; otherwise null. See selfScript() above. */
|
|
145
|
+
const selfTurnOn = (human, name, args) => {
|
|
146
|
+
const cmd = selfScript(name, args);
|
|
147
|
+
return cmd ? { human, cmd } : null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** The four states. 'absent' means "not installed here", which is NOT the same as "installed and off". */
|
|
151
|
+
/**
|
|
152
|
+
* IDLE — "you think this is on; it is set up and it is not running."
|
|
153
|
+
*
|
|
154
|
+
* THE STATE THIS PRODUCT EXISTS FOR, and it was missing. Owner, 2026-07-24: "this is exactly what we
|
|
155
|
+
* mean by people thinking something is 'On' only to find out it is not really running and working the
|
|
156
|
+
* way they thought it would — that is exactly what this tool is for."
|
|
157
|
+
*
|
|
158
|
+
* It was found on ourselves. `cheap-model-routing` reported ON off a receipt count alone: any n > 0
|
|
159
|
+
* meant on, forever. The router had 38 receipts, an active policy and a current catalog — and had not
|
|
160
|
+
* routed anything in 4.8 days, because the PreToolUse gate that would invoke it was written on
|
|
161
|
+
* 2026-07-13 and never wired into settings.json. Configured, proven, and inert. The age was even
|
|
162
|
+
* PRINTED in the evidence string and did not touch the verdict, which is the tell: we had the fact and
|
|
163
|
+
* threw it away at the moment of judgement.
|
|
164
|
+
*
|
|
165
|
+
* IDLE is deliberately NOT a flavour of OFF. Off means "we looked and it is not running" and points at
|
|
166
|
+
* turnOn. Idle means "it ran, it works, nothing is calling it now" and points at a WIRING question —
|
|
167
|
+
* usually a hook that was built and never installed. Collapsing the two would send someone to
|
|
168
|
+
* re-enable a thing that is already enabled, which is how a diagnosis becomes a wild goose chase.
|
|
169
|
+
*
|
|
170
|
+
* The horizon is a property of the capability, not a constant: a nightly job idle for 2 days is
|
|
171
|
+
* broken, a router idle for 2 days may just be a quiet weekend. Each detector passes its own.
|
|
172
|
+
*/
|
|
173
|
+
export const STATE = Object.freeze({ ON: 'on', OFF: 'off', IDLE: 'idle', UNKNOWN: 'unknown', ABSENT: 'absent' });
|
|
174
|
+
export const SCOPE = Object.freeze({ PROJECT: 'project', USER: 'user', MACHINE: 'machine' });
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Sibling helpers are loaded ONCE, lazily, and a load failure degrades to 'unknown' instead of
|
|
178
|
+
* taking the whole registry down. Top-level await keeps every detect() synchronous, which matters:
|
|
179
|
+
* a sync detector cannot be half-awaited by a caller that forgot, and the console renders these
|
|
180
|
+
* rows during a request. The repo has been bitten by a silent import landmine before, so a helper
|
|
181
|
+
* that vanishes must produce an honest "could not load", never a confident zero.
|
|
182
|
+
*/
|
|
183
|
+
const helpers = {};
|
|
184
|
+
for (const [name, spec] of Object.entries({
|
|
185
|
+
memoryDoctor: './memory-doctor.mjs',
|
|
186
|
+
lessonStore: './lesson-store.mjs',
|
|
187
|
+
lessonPromote: './lesson-promote.mjs',
|
|
188
|
+
gates: './gates.mjs',
|
|
189
|
+
// learning-enable.mjs owns the ONE reading of the learner's state file. It is imported rather than
|
|
190
|
+
// re-implemented because the two used to disagree out loud: on a stats.json whose counters had been
|
|
191
|
+
// renamed upstream, this registry said "off — 0 trajectories, 0 patterns, nothing has been learned"
|
|
192
|
+
// while learning-enable, reading the identical bytes, said "UNKNOWN — no recognisable counters".
|
|
193
|
+
// Both shipped, on one machine, in the same minute. Two answers to one question is worse than
|
|
194
|
+
// either answer alone, so the second implementation is gone rather than merely corrected.
|
|
195
|
+
learningEnable: './learning-enable.mjs',
|
|
196
|
+
})) {
|
|
197
|
+
try { helpers[name] = await import(spec); }
|
|
198
|
+
catch (e) { helpers[name] = null; helpers[`${name}Err`] = String(e?.message || e).split('\n')[0].slice(0, 90); }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const row = (state, evidence) => ({ state, evidence });
|
|
202
|
+
const daysSince = (ms) => (ms ? Math.round((Date.now() - ms) / DAY) : null);
|
|
203
|
+
|
|
204
|
+
/** Read+parse JSON, distinguishing "absent" from "unreadable" — collapsing them hides real corruption. */
|
|
205
|
+
function readJSON(file) {
|
|
206
|
+
if (!fs.existsSync(file)) return { missing: true };
|
|
207
|
+
try { return { value: JSON.parse(fs.readFileSync(file, 'utf8')) }; }
|
|
208
|
+
catch (e) { return { err: String(e?.message || e).split('\n')[0].slice(0, 80) }; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Newest mtime of a file, or null when it does not exist / cannot be stat'd. Never 0 — 0 reads as 1970. */
|
|
212
|
+
function mtimeOf(file) {
|
|
213
|
+
try { return fs.statSync(file).mtimeMs; } catch { return null; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Count non-blank lines. Returns null (unknown) rather than 0 when the file cannot be read. */
|
|
217
|
+
function lineCount(file) {
|
|
218
|
+
try { return fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim()).length; }
|
|
219
|
+
catch { return null; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Is a PreToolUse gate on subagent dispatch wired to the cheap-model router?
|
|
224
|
+
*
|
|
225
|
+
* READ THE MERGED REGISTRY, NOT ONE FILE (issue #112). This used to scan `~/.claude/settings.json`
|
|
226
|
+
* for the literal string `route-dispatch.sh`, which is how the LEGACY standalone install wires the
|
|
227
|
+
* gate — and is invisible to the way most people now have it. A plugin-marketplace install wires it
|
|
228
|
+
* in the plugin's own `hooks.json` as `hook-shim.mjs route-dispatch`, never touching settings.json,
|
|
229
|
+
* so `gateWired` was false for every plugin user no matter how correctly the hook was installed. The
|
|
230
|
+
* console then told them "nothing can invoke it" about a gate that was wired.
|
|
231
|
+
*
|
|
232
|
+
* hook-registry.mjs already enumerates every registry a session loads and resolves each command to
|
|
233
|
+
* its HANDLER through hook-shim.mjs's own dispatch table — so `route-dispatch.sh` is recognised
|
|
234
|
+
* whether it is named directly or reached through the shim, and the wiring is found in whichever
|
|
235
|
+
* layer holds it. That module is the authority; this one asks it rather than describing hooks again.
|
|
236
|
+
*
|
|
237
|
+
* WHICH COPY COUNTS, and this is the whole care of the function. The question is what THIS MACHINE
|
|
238
|
+
* loads, so the two code copies nothing boots are excluded: the repo's own `plugin/hooks/hooks.json`
|
|
239
|
+
* is the PREIMAGE (a checkout can be ahead of the installed plugin, and reading the preimage instead
|
|
240
|
+
* of the booted copy is the adjacent-door defect ADR-055 F16 names), and the marketplace clone is
|
|
241
|
+
* where installs are fetched FROM. What Claude Code actually booted on a marketplace install is the
|
|
242
|
+
* plugin-cache copy — and because a cache directory outlives the plugin being switched off, that one
|
|
243
|
+
* counts only while the plugin is enabled.
|
|
244
|
+
*/
|
|
245
|
+
export function dispatchGateWiring({ repo = REPO, home = HOME } = {}) {
|
|
246
|
+
const PREIMAGE = new Set(['plugin', 'marketplace-clone']);
|
|
247
|
+
let records;
|
|
248
|
+
try { records = buildRegistry({ repo, home }).records; }
|
|
249
|
+
catch { return { wired: false, layer: null, unreadable: true }; }
|
|
250
|
+
const hits = records.filter((r) => r.event === 'PreToolUse'
|
|
251
|
+
&& r.handler === 'route-dispatch.sh'
|
|
252
|
+
&& r.tools.some((t) => t === 'Task' || t === 'Agent' || t === '*')
|
|
253
|
+
&& !PREIMAGE.has(r.layer));
|
|
254
|
+
const external = hits.find((r) => r.layer !== 'plugin-installed');
|
|
255
|
+
if (external) return { wired: true, layer: external.layer, unreadable: false };
|
|
256
|
+
const enabled = Object.entries(readJSON(path.join(home, '.claude/settings.json')).value?.enabledPlugins || {})
|
|
257
|
+
.some(([k, v]) => k.startsWith('ruvnet-brain@') && v === true);
|
|
258
|
+
const ours = enabled ? hits[0] : null;
|
|
259
|
+
return { wired: Boolean(ours), layer: ours ? ours.layer : null, unreadable: false };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Locate the ONE global ruflo (global Rule 21 — never npx, which masks a stale global install).
|
|
264
|
+
*
|
|
265
|
+
* LOCATES. NEVER EXECUTES. That distinction is load-bearing and was learned the expensive way: an
|
|
266
|
+
* earlier version of the learning-hooks detector ran `ruflo hooks list` to count rows, and ruflo
|
|
267
|
+
* responds to ANY invocation by auto-starting its daemon and adopting the caller's cwd as its
|
|
268
|
+
* workspace. Measured on a scratch HOME: one call to auditAll() left a live `node cli.js daemon
|
|
269
|
+
* start --foreground` process running after the script exited, plus four files written into HOME
|
|
270
|
+
* (.claude-flow/daemon.pid, daemon-state.json, logs/daemon.log, update-state.json).
|
|
271
|
+
*
|
|
272
|
+
* This is a READ-ONLY status page. Hundreds of people opening it must not each acquire an
|
|
273
|
+
* unrequested long-lived process and a polluted home directory as the price of asking a question.
|
|
274
|
+
* `command -v` is safe because it resolves a name without running the program behind it.
|
|
275
|
+
*/
|
|
276
|
+
function rufloBin() {
|
|
277
|
+
const p = path.join(HOME, '.npm-global/bin/ruflo');
|
|
278
|
+
if (fs.existsSync(p)) return p;
|
|
279
|
+
|
|
280
|
+
// NO LOGIN SHELL. This used to run `sh -lc 'command -v ruflo'`, and the `-l` sources the user's
|
|
281
|
+
// entire profile — every export, nvm/rbenv shim, and one-off line anyone has ever pasted into
|
|
282
|
+
// .profile — as the price of answering "is ruflo installed?". Arbitrary startup code executed by a
|
|
283
|
+
// page whose defining promise, stated four lines above, is that it only observes. Milder than the
|
|
284
|
+
// daemon spawn already removed from this file, and the same category of mistake.
|
|
285
|
+
//
|
|
286
|
+
// PATH lookup does the same job with no shell at all: resolving a name against directories, which
|
|
287
|
+
// is all `command -v` was ever wanted for here.
|
|
288
|
+
const exts = process.platform === 'win32' ? ['.cmd', '.exe', ''] : [''];
|
|
289
|
+
for (const dir of String(process.env.PATH || '').split(path.delimiter)) {
|
|
290
|
+
if (!dir) continue;
|
|
291
|
+
for (const ext of exts) {
|
|
292
|
+
const cand = path.join(dir, `ruflo${ext}`);
|
|
293
|
+
try { if (fs.existsSync(cand) && fs.statSync(cand).isFile()) return cand; } catch { /* unreadable PATH entry */ }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Count hook entries that carry an actual command, across a settings.json hook group array.
|
|
301
|
+
*
|
|
302
|
+
* Counting the GROUPS instead — `hooks.PreCompact.length` — is the bug this replaces. A matcher
|
|
303
|
+
* group is a container; `[{matcher:'.*',hooks:[]}]` has length 1 and runs nothing at all. Verified:
|
|
304
|
+
* a settings.json holding exactly that for both boundaries made this registry report session capture
|
|
305
|
+
* "on — both boundaries are covered", which is a fabricated status about a machine that would lose
|
|
306
|
+
* every session. Only a non-empty `command` string is evidence that anything executes.
|
|
307
|
+
*/
|
|
308
|
+
function countHookCommands(groups) {
|
|
309
|
+
if (!Array.isArray(groups)) return 0;
|
|
310
|
+
let n = 0;
|
|
311
|
+
for (const g of groups) {
|
|
312
|
+
for (const h of Array.isArray(g?.hooks) ? g.hooks : []) {
|
|
313
|
+
if (typeof h?.command === 'string' && h.command.trim()) n += 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return n;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Commands at a session boundary that plausibly PERSIST STATE — the only ones "session capture" is a
|
|
321
|
+
* true statement about.
|
|
322
|
+
*
|
|
323
|
+
* Deliberately a whitelist of named mechanisms rather than "any command": the boundary tells you when
|
|
324
|
+
* something runs, never what it does, and this row claims what it does. `echo done` at SessionEnd is
|
|
325
|
+
* a registered hook and captures nothing. Each pattern below is a real writer — the global autocapture
|
|
326
|
+
* hook, ruflo/claude-flow's own session and memory subcommands, agentdb, or a script whose name says
|
|
327
|
+
* it captures/persists — so a match is evidence, not a guess.
|
|
328
|
+
*
|
|
329
|
+
* Returns null (never 0) when any part of the structure is unparseable. See the caller: an incomplete
|
|
330
|
+
* count rendered as a complete one is the failure this whole file exists to refuse.
|
|
331
|
+
*/
|
|
332
|
+
const CAPTURE_COMMAND = /(agentdb|autocapture|auto-capture|session-end|session_end|sessionend|precompact|pre-compact|memory[\s_-]*(store|save|persist)|\bruflo\b[^"]*\b(memory|session|hooks)\b|claude-flow[^"]*\b(memory|session|hooks)\b|(capture|persist|snapshot|checkpoint)[\w-]*\.(mjs|js|sh|py))/i;
|
|
333
|
+
|
|
334
|
+
function countCaptureCommands(groups) {
|
|
335
|
+
if (groups === undefined) return 0; // nothing registered at this boundary is a real answer
|
|
336
|
+
if (!Array.isArray(groups)) return null; // present but unreadable — not the same as absent
|
|
337
|
+
let n = 0;
|
|
338
|
+
for (const g of groups) {
|
|
339
|
+
if (g?.hooks !== undefined && !Array.isArray(g.hooks)) return null;
|
|
340
|
+
for (const h of Array.isArray(g?.hooks) ? g.hooks : []) {
|
|
341
|
+
const cmd = typeof h?.command === 'string' ? h.command.trim() : '';
|
|
342
|
+
if (!cmd) continue;
|
|
343
|
+
if (CAPTURE_COMMAND.test(cmd)) n += 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return n;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── The capabilities ─────────────────────────────────────────────────────────────────────────────
|
|
350
|
+
// Ordered by blast radius: the ones whose dormancy costs the most sit at the top, because this list
|
|
351
|
+
// is rendered in order and nobody reads to the bottom.
|
|
352
|
+
|
|
353
|
+
export const CAPABILITIES = [
|
|
354
|
+
{
|
|
355
|
+
key: 'learning-hooks',
|
|
356
|
+
label: 'Learning hooks',
|
|
357
|
+
whatItBuysYou: 'Your AI writes down which approach actually worked and reuses it next time, instead of solving the same problem from scratch every session.',
|
|
358
|
+
scope: SCOPE.MACHINE,
|
|
359
|
+
// VERIFIED NULL: there is no enable command, and — more importantly — no readable state to flip.
|
|
360
|
+
turnOn: null,
|
|
361
|
+
/**
|
|
362
|
+
* THIS DETECTOR RETURNS 'unknown' ON PURPOSE, AND THE FIRST VERSION OF IT WAS A LIE.
|
|
363
|
+
*
|
|
364
|
+
* It originally parsed the `Enabled` column of `ruflo hooks list` and reported, confidently:
|
|
365
|
+
* "all 26 registered hooks report Enabled: No … nothing is being learned from your sessions."
|
|
366
|
+
* That is the single most alarming sentence this registry could print, and it was false. It was
|
|
367
|
+
* caught within the hour by cross-checking against the installed ruflo source, and every step
|
|
368
|
+
* was then re-verified here rather than taken on trust:
|
|
369
|
+
*
|
|
370
|
+
* · `ruflo hooks list --format json` returns rows of {name, type, status:"active"} — there is
|
|
371
|
+
* NO `enabled` key in the payload at all.
|
|
372
|
+
* · The CLI renderer draws a column keyed `enabled`, so `v` is `undefined` for every row and
|
|
373
|
+
* the formatter prints its falsy branch: "No", 26 times. Same artifact empties Priority and
|
|
374
|
+
* Executions and makes Last Executed read "Never" for everything.
|
|
375
|
+
* · `ruflo hooks list --enabled`, documented as "Show only enabled hooks", returns the exact
|
|
376
|
+
* same 27 lines as the unfiltered call — the filter is passed to a handler that takes no
|
|
377
|
+
* arguments.
|
|
378
|
+
* · The handler itself (@claude-flow/cli .../mcp-tools/hooks-tools.js, `export const
|
|
379
|
+
* hooksList`) contains zero reads of any file, database, or env var, and the string
|
|
380
|
+
* "enabled" does not appear in it. It is a hardcoded catalog of which subcommands exist.
|
|
381
|
+
*
|
|
382
|
+
* So `ruflo hooks list` is a MENU, not a dashboard, and BOTH readings of it are worthless:
|
|
383
|
+
* "Enabled: No" is a field-name bug, and status:"active" is a literal in a static array. It
|
|
384
|
+
* cannot answer "is learning on?" in either direction, which makes 'unknown' the only honest
|
|
385
|
+
* state available from this source — and 'off' the precise false accusation the header warns
|
|
386
|
+
* about, committed against rUv's own tooling.
|
|
387
|
+
*
|
|
388
|
+
* The MEASURED answer lives in the `workflow-pattern-learning` row, which counts trajectories
|
|
389
|
+
* and patterns actually recorded. Outcomes are evidence; a catalog of subcommands is not.
|
|
390
|
+
*/
|
|
391
|
+
/**
|
|
392
|
+
* AND IT NO LONGER RUNS `ruflo hooks list` AT ALL — which is the second lesson, layered on the
|
|
393
|
+
* first. Having established above that the table cannot answer the question in either direction,
|
|
394
|
+
* the old code still SHELLED OUT to fetch it, purely to print a row count in a sentence whose
|
|
395
|
+
* substance is "this number tells you nothing." That cost a daemon and four files in the user's
|
|
396
|
+
* home directory (see rufloBin) for a fact we then disclaim in the same breath.
|
|
397
|
+
*
|
|
398
|
+
* A probe whose result you have already decided to disregard should not be run. So presence is
|
|
399
|
+
* established from the binary on disk — a fact a status page is entitled to read — and the state
|
|
400
|
+
* stays honestly unknown, pointing at the row that measures OUTCOMES instead.
|
|
401
|
+
*/
|
|
402
|
+
detect() {
|
|
403
|
+
const bin = rufloBin();
|
|
404
|
+
if (!bin) return row(STATE.ABSENT, 'ruflo is not installed on this machine, so there are no learning hooks to enable');
|
|
405
|
+
return row(STATE.UNKNOWN, 'ruflo is installed, but whether its learning hooks are switched on cannot be read from it: `ruflo hooks list` is a static catalog of available subcommands, not a state readout (its own --enabled filter returns every row unchanged, and its handler reads no file, database, or env var). Rather than run a command whose answer we would have to disclaim — and which starts a background daemon to produce it — nothing is claimed here. Measured learning activity is reported by the workflow-learning row instead.');
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
{
|
|
410
|
+
key: 'memory-distillation',
|
|
411
|
+
label: 'Memory distillation',
|
|
412
|
+
whatItBuysYou: 'Loose notes from past sessions get mined into reusable patterns, so your AI recalls the lesson instead of re-reading every old note to find it.',
|
|
413
|
+
scope: SCOPE.PROJECT,
|
|
414
|
+
// The offer points at scripts/distill-project.mjs, NOT at bare `ruflo memory distill run`, and the
|
|
415
|
+
// difference is the whole reason ADR-047 was rejected. Both duelists found the same hole: the
|
|
416
|
+
// registry offers `turnOn` commands whose promised undo lives on a DIFFERENT execution path than
|
|
417
|
+
// the action actually handed to the user. Here that was literal — the inverse advertised for
|
|
418
|
+
// distillation restores snapshots that `health-repair.mjs --distill-fleet` takes, while this line
|
|
419
|
+
// used to hand over the raw command, which (verified against `--help`) takes no snapshot at all.
|
|
420
|
+
// Run it, dislike the result, and there was nothing to go back to.
|
|
421
|
+
//
|
|
422
|
+
// The wrapper sequences rUv's own commands so the operation is reversible: WAL-safe
|
|
423
|
+
// `ruflo memory backup` FIRST (cp on a live WAL DB silently amputates the newest transactions —
|
|
424
|
+
// this project has lost data that way), a durable fsync'd receipt fail-closed BEFORE any mutation,
|
|
425
|
+
// `distill run --db` scoped to THIS project rather than whatever the cwd implies, and a verified
|
|
426
|
+
// pattern delta reported as a measurement. `--restore` is the tested inverse.
|
|
427
|
+
//
|
|
428
|
+
// PROVEN end to end against the real store, 2026-07-24: 644 → 648 patterns (+4), restore → 644,
|
|
429
|
+
// re-run → 648, five durable receipts, $0.0000. This is the ONE capability whose undo has actually
|
|
430
|
+
// been run rather than merely promised — which is precisely what makes it the only one offerable.
|
|
431
|
+
turnOn: selfTurnOn(
|
|
432
|
+
'Mine this project\'s stored memories into reusable patterns (snapshots first; reversible)',
|
|
433
|
+
'distill-project.mjs',
|
|
434
|
+
),
|
|
435
|
+
detect({ project = process.cwd() } = {}) {
|
|
436
|
+
const db = path.join(project, '.swarm/memory.db');
|
|
437
|
+
if (!fs.existsSync(db)) return row(STATE.ABSENT, `no memory store exists for this project yet (${path.join(path.basename(project), '.swarm/memory.db')} is not present)`);
|
|
438
|
+
if (!helpers.memoryDoctor) return row(STATE.UNKNOWN, `the memory diagnostic could not be loaded (${helpers.memoryDoctorErr}) — distillation state not checked`);
|
|
439
|
+
|
|
440
|
+
let d;
|
|
441
|
+
try { d = helpers.memoryDoctor.diagnose(db); }
|
|
442
|
+
catch (e) { return row(STATE.UNKNOWN, `the memory store could not be diagnosed: ${String(e?.message || e).slice(0, 60)}`); }
|
|
443
|
+
|
|
444
|
+
// THE UNREADABLE CASE, and the entire reason this file states its rule twice. `learns` is false
|
|
445
|
+
// in BOTH the dead case and the could-not-open case, so trusting it blindly turns a failed read
|
|
446
|
+
// into a false accusation. The STATE here was always right; the REASON was not.
|
|
447
|
+
//
|
|
448
|
+
// What this used to say, to every unreadable store without distinction: "this is often a passing
|
|
449
|
+
// lock from another session, not a fault; re-check before acting." That sentence generalised ONE
|
|
450
|
+
// real observation — a store that read unreadable and then healthy 90 seconds later, which was a
|
|
451
|
+
// genuine concurrent writer — into a blanket explanation for every failure mode. It was wrong on
|
|
452
|
+
// this very repo, whose WAL sidecars had been renamed to .CORRUPT-*: that store was structurally
|
|
453
|
+
// unopenable, re-checking would never have cleared it, and the console told its owner to wait.
|
|
454
|
+
// Advice that cannot work is worse than no advice, because the person takes it.
|
|
455
|
+
//
|
|
456
|
+
// The open failure itself is now handled properly in memory-doctor's q() (resting-WAL fallback),
|
|
457
|
+
// so what reaches here is a real lock or a real fault — and it says which it can distinguish
|
|
458
|
+
// rather than asserting one of them.
|
|
459
|
+
if (d.unreadable) {
|
|
460
|
+
const locked = /lock|busy|writer/i.test(String(d.unreadable));
|
|
461
|
+
return row(STATE.UNKNOWN, locked
|
|
462
|
+
? `the memory store is currently held by another process (${d.unreadable}) — that is a passing lock, not a fault; re-checking in a moment should clear it`
|
|
463
|
+
: `the memory store could not be read (${d.unreadable}) — this is not a transient lock, so re-checking will not clear it; the store or its journal files need attention before distillation state can be established`);
|
|
464
|
+
}
|
|
465
|
+
if (d.schemaless) return row(STATE.UNKNOWN, 'the store exists but has no memory_entries table (pre-AgentDB schema, never initialised) — nothing to distill yet, and nothing is broken');
|
|
466
|
+
if (typeof d.total !== 'number') return row(STATE.UNKNOWN, 'the store opened but returned no countable rows — distillation state not established');
|
|
467
|
+
|
|
468
|
+
if (d.total === 0) return row(STATE.ABSENT, 'the memory store is empty, so there is nothing to distill yet');
|
|
469
|
+
if (d.learns) return row(STATE.ON, `${d.patterns} reusable patterns distilled from ${d.real} memories (${(d.cover * 100).toFixed(1)}% embedded)`);
|
|
470
|
+
if (d.patterns === 0) return row(STATE.OFF, `${d.total} memories stored and ${(d.cover * 100).toFixed(1)}% embedded, but 0 have been distilled into patterns — the store records and forgets`);
|
|
471
|
+
// "BARELY RUN" IS NOT "NOT RUNNING". This returned STATE.OFF while its own sentence says the
|
|
472
|
+
// thing has produced patterns — used-and-weak reported as never-used. OFF is a claim that we
|
|
473
|
+
// looked and found it stopped; here we looked and found it working, thinly. Reporting a working
|
|
474
|
+
// capability as off sends the user to switch on something already on, and it corrupts the
|
|
475
|
+
// dormancy predicate that ADR-047 wants to build offers from: a capability that HAS run is not
|
|
476
|
+
// a dormancy finding, whatever its ratio. The weak ratio is still said out loud — it belongs in
|
|
477
|
+
// the evidence, which is where a concern with no action attached should live.
|
|
478
|
+
// Found by Fable 5 in the ADR-047 duel, 2026-07-24.
|
|
479
|
+
return row(STATE.ON, `only ${d.patterns} patterns from ${d.real} memories — distillation has run, but thinly`);
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
{
|
|
484
|
+
key: 'workflow-pattern-learning',
|
|
485
|
+
label: 'Workflow learning',
|
|
486
|
+
whatItBuysYou: 'Your AI picks up how you personally like work done and carries that across every project, rather than starting each one as a stranger.',
|
|
487
|
+
scope: SCOPE.USER,
|
|
488
|
+
// VERIFIED: `ruflo hooks pretrain --help` exists (4-step pipeline + embeddings, --path default '.').
|
|
489
|
+
turnOn: { human: 'Bootstrap the learner from this repository', cmd: 'ruflo hooks pretrain' },
|
|
490
|
+
/**
|
|
491
|
+
* DELEGATED to learning-enable.mjs, which is the only place the learner's state file is read.
|
|
492
|
+
* The hand-rolled version this replaces committed BOTH of the mistakes this file warns about:
|
|
493
|
+
*
|
|
494
|
+
* SCHEMA DRIFT READ AS A MEASUREMENT. `Number(r.value?.trajectoriesRecorded) || 0` turns
|
|
495
|
+
* NaN into 0, so the day rUv renames that field every user is simultaneously told "the learner
|
|
496
|
+
* file exists but records 0 trajectories and 0 patterns — nothing has been learned yet."
|
|
497
|
+
* Reproduced on a stats.json carrying 457 real trajectories under `trajectories_recorded`:
|
|
498
|
+
* this row said OFF; learning-enable, on the same bytes, said UNKNOWN. Its `num()` returns
|
|
499
|
+
* null rather than 0 precisely so an unreadable counter can never masquerade as a measured
|
|
500
|
+
* zero — which is the header's rule, implemented once, correctly, in the other file.
|
|
501
|
+
*
|
|
502
|
+
* NO STALENESS. A learner last adapted 400 days ago reported "on — 457 sessions recorded",
|
|
503
|
+
* while learning-enable called the same file "IDLE — nothing in 400 days". Freshness is part
|
|
504
|
+
* of the verdict, not a footnote, and STALE_DAYS now has exactly one definition.
|
|
505
|
+
*/
|
|
506
|
+
detect() {
|
|
507
|
+
if (!helpers.learningEnable) return row(STATE.UNKNOWN, `the learner probe could not be loaded (${helpers.learningEnableErr}) — learning state not checked`);
|
|
508
|
+
let learner;
|
|
509
|
+
let v;
|
|
510
|
+
try {
|
|
511
|
+
learner = helpers.learningEnable.readLearnerState({ home: HOME });
|
|
512
|
+
v = helpers.learningEnable.verdict(learner);
|
|
513
|
+
} catch (e) { return row(STATE.UNKNOWN, `the learner state could not be read (${String(e?.message || e).slice(0, 60)}) — learning state not checked`); }
|
|
514
|
+
|
|
515
|
+
const traj = learner.trajectories;
|
|
516
|
+
const pat = learner.patterns;
|
|
517
|
+
const days = learner.ageMinutes === null ? null : Math.floor(learner.ageMinutes / 1440);
|
|
518
|
+
switch (v.code) {
|
|
519
|
+
case 'NO_LEARNER_STATE':
|
|
520
|
+
return row(STATE.ABSENT, 'no learner state exists yet (~/.claude-flow/neural/stats.json has never been written)');
|
|
521
|
+
case 'CORRUPT':
|
|
522
|
+
return row(STATE.UNKNOWN, 'the learner state file exists but could not be parsed — counts not checked, and nothing is concluded from an unreadable file');
|
|
523
|
+
case 'UNKNOWN_SHAPE':
|
|
524
|
+
// The drift case, stated as the obstacle it is. NEVER "0 trajectories" — that is a claim
|
|
525
|
+
// about the learner; this is a claim about our ability to read it.
|
|
526
|
+
return row(STATE.UNKNOWN, 'the learner state file exists but carries no counters this version recognises — the field names have probably changed upstream, so whether it has learned anything cannot be read here');
|
|
527
|
+
case 'UNKNOWN_PARTIAL':
|
|
528
|
+
// HALF-DRIFT, and the half we cannot read decides the answer. Rendering the readable half
|
|
529
|
+
// as though it settled the question is how "null work sessions recorded and 457 patterns
|
|
530
|
+
// learned" reached a user's screen. One unread counter, one honest unknown.
|
|
531
|
+
return row(STATE.UNKNOWN, `the learner state file is only half-readable — ${v.missingField} is not a number this version recognises, so the counters cannot be compared and no verdict is drawn from the half that did parse`);
|
|
532
|
+
case 'INITIALISED_EMPTY':
|
|
533
|
+
return row(STATE.OFF, 'the learner file exists and genuinely records 0 trajectories and 0 patterns — it has been created but never fed');
|
|
534
|
+
case 'IDLE':
|
|
535
|
+
// WAS STATE.OFF UNTIL 2026-07-24, AND THAT WAS THE SAME BUG THIS FILE ADDED STATE.IDLE TO END.
|
|
536
|
+
//
|
|
537
|
+
// The verdict is literally named IDLE and its own sentence says "ran before and has gone
|
|
538
|
+
// quiet" — the textbook definition of the state added to the top of this file hours earlier.
|
|
539
|
+
// It kept returning OFF because STATE.IDLE was wired into exactly ONE detector
|
|
540
|
+
// (cheap-model-routing) and no others. One bug, found once, fixed once, left everywhere else.
|
|
541
|
+
//
|
|
542
|
+
// WHY IT MATTERS BEYOND TIDINESS: OFF means "we looked and it is not running" and points the
|
|
543
|
+
// user at turnOn. A learner holding hundreds of trajectories is not off — it worked, and
|
|
544
|
+
// something stopped calling it. Offering to "turn on" an already-populated learner is the
|
|
545
|
+
// category error that put "457 patterns learned" next to an invitation to enable it.
|
|
546
|
+
// Found by Fable 5 in the ADR-047 duel, one file over from where I had just fixed it.
|
|
547
|
+
return row(STATE.IDLE, `${traj} work sessions and ${pat} patterns were recorded, but nothing in ${days} days — the learner ran before and has gone quiet. It is not off; something that fed it stopped.`);
|
|
548
|
+
default: {
|
|
549
|
+
// TWO IDENTICAL NUMBERS ARE ONE FACT, NOT TWO ACHIEVEMENTS.
|
|
550
|
+
//
|
|
551
|
+
// Measured live 2026-07-24: trajectoriesRecorded 1114, patternsLearned 1114 — exactly 1:1.
|
|
552
|
+
// Rendered as "1114 work sessions recorded AND 1114 patterns learned", that reads as two
|
|
553
|
+
// independent wins and implies a distillation step. Fable 5's verdict, and it is right: a
|
|
554
|
+
// sharp reader sees the 1:1 instantly and concludes the counter is counting itself.
|
|
555
|
+
//
|
|
556
|
+
// WHAT I DID NOT CONCLUDE: that ruflo's learner is fake. Grounded in rUv's own source
|
|
557
|
+
// (ruflo/v3/@claude-flow/memory/src/persistent-sona.ts), extractPatternsFromTrajectory()
|
|
558
|
+
// stores a pattern ONLY when findSimilarPatterns() finds no near-duplicate — so patterns
|
|
559
|
+
// ARE deduplicated by design and the ratio should sit below 1:1. I cannot explain an exact
|
|
560
|
+
// 1:1 from the code I have read, and the counters in ~/.claude-flow/neural/stats.json may
|
|
561
|
+
// be written by a different path than that module. Unexplained is not the same as false.
|
|
562
|
+
//
|
|
563
|
+
// So this says only what is observed. When the two counts are equal we report ONE number
|
|
564
|
+
// and name the identity out loud, which is both honest and the more interesting signal —
|
|
565
|
+
// it tells the reader something is worth asking about instead of quietly inflating.
|
|
566
|
+
const when = days === null ? '' : `, last updated ${days} day${days === 1 ? '' : 's'} ago`;
|
|
567
|
+
if (traj === pat && traj > 0) {
|
|
568
|
+
return row(STATE.ON, `${traj} work sessions recorded, and the pattern count matches it exactly (${pat}) — one pattern per session, with no reduction between them${when}`);
|
|
569
|
+
}
|
|
570
|
+
return row(STATE.ON, `${traj} work sessions recorded and ${pat} patterns learned${when}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
{
|
|
577
|
+
key: 'cheap-model-routing',
|
|
578
|
+
label: 'Cheap-model routing',
|
|
579
|
+
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.',
|
|
580
|
+
scope: SCOPE.MACHINE,
|
|
581
|
+
// VERIFIED: `node scripts/route-cheap.mjs` prints its usage line requiring --task; script present in repo.
|
|
582
|
+
// ABSOLUTE, via selfScript(). `node scripts/route-cheap.mjs` is copy-pasteable only by someone
|
|
583
|
+
// already standing in a ruvnet-brain checkout; everyone else got `Cannot find module`. A real
|
|
584
|
+
// executor behind an unreachable path is a dead button with extra steps.
|
|
585
|
+
turnOn: selfTurnOn('Route one read-only task through the cheap path', 'route-cheap.mjs', '--task "<text>"'),
|
|
586
|
+
detect() {
|
|
587
|
+
const bin = path.join(HOME, '.npm-global/bin/agentic-flow');
|
|
588
|
+
const installed = fs.existsSync(bin);
|
|
589
|
+
const receipts = process.env.METAHARNESS_RECEIPTS || path.join(HOME, '.claude/metaharness/routing-receipts.jsonl');
|
|
590
|
+
const n = lineCount(receipts);
|
|
591
|
+
|
|
592
|
+
// Receipts are the proof, and they outrank installation: a receipt file with lines means this
|
|
593
|
+
// genuinely ran, even if the binary later moved. Absence of the binary AND of receipts is the
|
|
594
|
+
// only honest 'absent'.
|
|
595
|
+
if (n === null && !fs.existsSync(receipts)) {
|
|
596
|
+
return installed
|
|
597
|
+
? row(STATE.OFF, 'agentic-flow is installed but no routing receipt has ever been written — the cheap path exists and has never been used')
|
|
598
|
+
: row(STATE.ABSENT, 'agentic-flow is not installed and no routing receipts exist, so cheap routing has never been set up here');
|
|
599
|
+
}
|
|
600
|
+
if (n === null) return row(STATE.UNKNOWN, 'the routing receipt ledger exists but could not be read — usage not checked');
|
|
601
|
+
if (n === 0) return row(STATE.OFF, 'the routing receipt ledger is present but empty — no task has been routed to a cheaper model');
|
|
602
|
+
const age = daysSince(mtimeOf(receipts));
|
|
603
|
+
|
|
604
|
+
// THE AGE NOW DECIDES, INSTEAD OF DECORATING. This line used to return ON for any n > 0 and
|
|
605
|
+
// merely MENTION the age in the evidence — so a router with 38 receipts and nothing invoking it
|
|
606
|
+
// for a fortnight read as healthy. We were holding the disproving fact and printing it politely.
|
|
607
|
+
//
|
|
608
|
+
// 7 days: this path should fire on ordinary sessions, so a full quiet week means something
|
|
609
|
+
// upstream stopped calling it — not that the user had a light week. Measured on this machine
|
|
610
|
+
// 2026-07-24: 38 receipts, last one 4.8 days old, and the PreToolUse gate that invokes it
|
|
611
|
+
// (plugin/scripts/route-dispatch.sh, written 2026-07-13) had never been added to settings.json.
|
|
612
|
+
// Built, correct, and unwired — which no state in this registry could previously express.
|
|
613
|
+
// MEASURE THE CAUSE, NOT A SYMPTOM. An age threshold alone is a proxy and it FAILED on the real
|
|
614
|
+
// case: measured 2026-07-24, the last receipt was 5 days old — under any sane horizon — while the
|
|
615
|
+
// router was in fact never being consulted at all. A quiet week and a severed wire look identical
|
|
616
|
+
// from the receipt file, so read the wire directly.
|
|
617
|
+
//
|
|
618
|
+
// Two things must both be true for the host-limited dispatch audit to record anything: a
|
|
619
|
+
// PreToolUse hook on subagent dispatch and the opt-in profile it refuses to act without
|
|
620
|
+
// (route-dispatch.sh exits 0 when
|
|
621
|
+
// profile.json is absent). Either missing ⇒ the router cannot fire, regardless of how healthy
|
|
622
|
+
// the receipt ledger looks.
|
|
623
|
+
const profile = fs.existsSync(path.join(HOME, '.claude/model-router/profile.json'));
|
|
624
|
+
// ONE READING OF THE WIRING, from the module that owns it — see dispatchGateWiring(). Scanning
|
|
625
|
+
// settings.json here was a second, narrower implementation of that question, and it answered
|
|
626
|
+
// "not wired" for every plugin-marketplace install (issue #112).
|
|
627
|
+
const gate = dispatchGateWiring();
|
|
628
|
+
const gateWired = gate.wired;
|
|
629
|
+
// THE ONE RULE OF THIS FILE. A census we could not take is not a gate we observed to be
|
|
630
|
+
// missing, and "nothing can invoke it" is a claim about the user's machine.
|
|
631
|
+
if (gate.unreadable) return row(STATE.UNKNOWN, `${n} routing receipt${n === 1 ? '' : 's'} recorded, but the hook registries on this machine could not be read — whether anything is wired to invoke the router was not checked`);
|
|
632
|
+
|
|
633
|
+
if (!gateWired || !profile) {
|
|
634
|
+
const missing = [!gateWired && 'no PreToolUse gate on Task|Agent is wired to route-dispatch.sh',
|
|
635
|
+
!profile && 'no ~/.claude/model-router/profile.json (the opt-in the gate requires)'].filter(Boolean).join('; and ');
|
|
636
|
+
return row(STATE.IDLE,
|
|
637
|
+
`set up and proven — ${n} routing receipt${n === 1 ? '' : 's'} recorded — but nothing can invoke it: ${missing}. `
|
|
638
|
+
+ 'Every receipt so far came from someone running the router by hand. Until the gate is wired, subagents keep '
|
|
639
|
+
+ 'inheriting this session\'s model, which is the single largest cost leak in the harness.');
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const IDLE_AFTER_DAYS = 7;
|
|
643
|
+
if (age !== null && age > IDLE_AFTER_DAYS) {
|
|
644
|
+
return row(STATE.IDLE,
|
|
645
|
+
`set up and proven — ${n} routing receipt${n === 1 ? '' : 's'} recorded — but nothing has routed through it in ${age} days. `
|
|
646
|
+
+ 'It is configured; something that should be calling it is not. Check that the subagent-dispatch gate is wired '
|
|
647
|
+
+ '(a PreToolUse hook on Task|Agent) and that ~/.claude/model-router/profile.json exists — without either, the router is never consulted.');
|
|
648
|
+
}
|
|
649
|
+
return row(STATE.ON, `${n} routing receipt${n === 1 ? '' : 's'} recorded${age === null ? '' : `, most recent ${age} day${age === 1 ? '' : 's'} ago`}`);
|
|
650
|
+
},
|
|
651
|
+
},
|
|
652
|
+
|
|
653
|
+
{
|
|
654
|
+
key: 'cross-project-lessons',
|
|
655
|
+
label: 'Cross-project lessons',
|
|
656
|
+
whatItBuysYou: 'A rule you have taught in three separate projects gets applied everywhere, instead of being re-taught project by project forever.',
|
|
657
|
+
scope: SCOPE.USER,
|
|
658
|
+
// VERIFIED: `lesson-promote.mjs --apply` exists (backs up first, reversible — see its header).
|
|
659
|
+
turnOn: selfTurnOn('Promote the processes you have proven in several projects', 'lesson-promote.mjs', '--apply'),
|
|
660
|
+
detect() {
|
|
661
|
+
if (!helpers.lessonPromote) return row(STATE.UNKNOWN, `the cross-project scanner could not be loaded (${helpers.lessonPromoteErr}) — promotion state not checked`);
|
|
662
|
+
let result;
|
|
663
|
+
try { result = helpers.lessonPromote.analyze(helpers.lessonPromote.collectLessons()); }
|
|
664
|
+
catch (e) { return row(STATE.UNKNOWN, `the cross-project lesson scan failed (${String(e?.message || e).slice(0, 60)}) — promotion state not checked`); }
|
|
665
|
+
|
|
666
|
+
const scanned = result?.scanned || {};
|
|
667
|
+
const promotable = result?.promotable || [];
|
|
668
|
+
if (!scanned.lessons) return row(STATE.ABSENT, 'no per-project lessons were found to compare, so there is nothing to promote yet');
|
|
669
|
+
|
|
670
|
+
// EFFECT IN FORCE, not backlog remaining. REJECTED by both duelists 2026-07-24: the old rule was
|
|
671
|
+
// ON iff promotable.length === 0, so teaching two new lessons anywhere flipped a WORKING capability
|
|
672
|
+
// to OFF — permanently, since the backlog always re-arms. Measured on this machine: it read OFF
|
|
673
|
+
// while the promoted block was sitting in the user's global CLAUDE.md, put there the same day.
|
|
674
|
+
// Worse, the evidence string carries a live counter and stateHashOf() hashes that prose, so every
|
|
675
|
+
// tick minted a fresh "the world changed, you may speak again" token — a perpetual-nag engine.
|
|
676
|
+
// Dormant must mean INSTALLED, USABLE, NEVER USED. Promotion writes a marked block into the user's
|
|
677
|
+
// global instructions; the presence of that block is the only honest evidence it is in use.
|
|
678
|
+
let promotedInForce = false;
|
|
679
|
+
try {
|
|
680
|
+
promotedInForce = fs.readFileSync(path.join(HOME, '.claude', 'CLAUDE.md'), 'utf8')
|
|
681
|
+
.includes('BEGIN ruvnet-brain: promoted-lessons');
|
|
682
|
+
} catch { promotedInForce = false; }
|
|
683
|
+
|
|
684
|
+
if (promotedInForce) {
|
|
685
|
+
return row(STATE.ON, promotable.length
|
|
686
|
+
? `cross-project promotion is in force in your global instructions; ${promotable.length} further process${promotable.length === 1 ? '' : 'es'} ${promotable.length === 1 ? 'has' : 'have'} since become eligible (from ${scanned.lessons} lessons across ${scanned.projects} projects)`
|
|
687
|
+
: `cross-project promotion is in force in your global instructions, and nothing further is waiting (from ${scanned.lessons} lessons across ${scanned.projects} projects)`);
|
|
688
|
+
}
|
|
689
|
+
if (promotable.length === 0) return row(STATE.ON, `${scanned.lessons} lessons across ${scanned.projects} projects scanned, and none are stuck at project level`);
|
|
690
|
+
return row(STATE.OFF, `promotion has never been applied on this machine, and ${promotable.length} process${promotable.length === 1 ? '' : 'es'} you have taught in multiple separate projects ${promotable.length === 1 ? 'is' : 'are'} still trapped at project level (from ${scanned.lessons} lessons across ${scanned.projects} projects)`);
|
|
691
|
+
},
|
|
692
|
+
},
|
|
693
|
+
|
|
694
|
+
{
|
|
695
|
+
key: 'lessons-in-force',
|
|
696
|
+
label: 'Lessons in force',
|
|
697
|
+
whatItBuysYou: 'The corrections you have given your AI actually constrain what it does next, rather than sitting in a file it never consults.',
|
|
698
|
+
scope: SCOPE.USER,
|
|
699
|
+
// VERIFIED NULL, BY DESIGN: ratification is deliberately withheld from the model (see header).
|
|
700
|
+
turnOn: null,
|
|
701
|
+
detect() {
|
|
702
|
+
if (!helpers.lessonStore) return row(STATE.UNKNOWN, `the lesson store could not be loaded (${helpers.lessonStoreErr}) — enforcement state not checked`);
|
|
703
|
+
const file = helpers.lessonStore.STORE_PATH;
|
|
704
|
+
if (!fs.existsSync(file)) return row(STATE.ABSENT, 'no lessons have been recorded yet on this machine');
|
|
705
|
+
let lessons;
|
|
706
|
+
try { lessons = helpers.lessonStore.loadLessons(); }
|
|
707
|
+
catch (e) { return row(STATE.UNKNOWN, `the lesson store could not be read (${String(e?.message || e).slice(0, 60)}) — enforcement state not checked`); }
|
|
708
|
+
if (!Array.isArray(lessons) || lessons.length === 0) return row(STATE.ABSENT, 'the lesson store exists but holds no lessons yet');
|
|
709
|
+
|
|
710
|
+
const S = helpers.lessonStore.STATUS || {};
|
|
711
|
+
const inForce = lessons.filter((l) => l?.status === S.RATIFIED || l?.status === S.ACTIVE).length;
|
|
712
|
+
const candidates = lessons.filter((l) => l?.status === S.CANDIDATE).length;
|
|
713
|
+
// A candidate can never block (lesson-store.mjs). Counting all 12 as "your lessons" would be
|
|
714
|
+
// the flattering number; the honest one is how many can actually affect a decision.
|
|
715
|
+
if (inForce > 0) return row(STATE.ON, `${inForce} of ${lessons.length} lessons are ratified and can affect what your AI does`);
|
|
716
|
+
return row(STATE.OFF, `all ${candidates} recorded lessons are still candidates awaiting your ratification — none of them can influence anything yet`);
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
{
|
|
721
|
+
key: 'harness-evolution',
|
|
722
|
+
label: 'Harness self-improvement',
|
|
723
|
+
// Issue #116: this was `turnOn: null`, justified by a "VERIFIED NULL: evolve is not among them"
|
|
724
|
+
// measurement that has since drifted — ruflo v3.34.0 ships evolve, bench and flywheel. The
|
|
725
|
+
// precondition is named in the human text because brain-score/SKILL.md:97 requires the WRITE
|
|
726
|
+
// layer's OPENROUTER_API_KEY to be disclosed rather than discovered on failure.
|
|
727
|
+
turnOn: {
|
|
728
|
+
human: 'Evolve the harness and keep only measured winners (needs OPENROUTER_API_KEY; without it, `--subcommand score` is the free read-only layer)',
|
|
729
|
+
cmd: 'ruflo metaharness --subcommand evolve',
|
|
730
|
+
},
|
|
731
|
+
whatItBuysYou: 'The rules your AI works by get tested against each other, and the version that measurably does better becomes the new default.',
|
|
732
|
+
scope: SCOPE.MACHINE,
|
|
733
|
+
// VERIFIED NULL: `ruflo metaharness --help` enumerates its subcommands and `evolve` is not among them.
|
|
734
|
+
turnOn: null,
|
|
735
|
+
detect({ project = process.cwd() } = {}) {
|
|
736
|
+
const policy = path.join(HOME, '.claude-flow/harness-active-policy.json');
|
|
737
|
+
// The archive is a per-project artifact even though the ACTIVE POLICY it feeds is machine-wide,
|
|
738
|
+
// so it is read from where the user stands. Reading it from REPO is what made a fresh machine
|
|
739
|
+
// appear to have run self-improvement it had never run.
|
|
740
|
+
const archive = path.join(project, '.metaharness/archive.json');
|
|
741
|
+
const p = readJSON(policy);
|
|
742
|
+
const haveArchive = fs.existsSync(archive);
|
|
743
|
+
|
|
744
|
+
if (p.err) return row(STATE.UNKNOWN, `the active-policy file exists but could not be parsed (${p.err}) — cannot tell whether an evolved policy is in force`);
|
|
745
|
+
if (!p.missing && p.value?.championId) {
|
|
746
|
+
const age = p.value.appliedAt ? daysSince(p.value.appliedAt) : null;
|
|
747
|
+
const tier = p.value.provenanceTier || 'unknown provenance';
|
|
748
|
+
return row(STATE.ON, `an evolved policy is active machine-wide (${String(p.value.championId).slice(0, 20)}…, provenance ${tier}${age === null ? '' : `, applied ${age} day${age === 1 ? '' : 's'} ago`})`);
|
|
749
|
+
}
|
|
750
|
+
// IDLE, not OFF: the sentence says it HAS RUN. Off means never used and points at turnOn;
|
|
751
|
+
// this ran and stopped, which is a wiring question. Same class as the learner fix above.
|
|
752
|
+
// Found by GPT-5.6-Sol in the ADR-047 duel, 2026-07-24.
|
|
753
|
+
if (haveArchive) return row(STATE.IDLE, 'self-improvement has run in this repo but no evolved policy is currently in force — nothing it discovered is being used');
|
|
754
|
+
return row(STATE.ABSENT, 'self-improvement has never run here and no evolved policy is active');
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
{
|
|
759
|
+
key: 'write-gates',
|
|
760
|
+
label: 'Write gates',
|
|
761
|
+
whatItBuysYou: 'Your AI is stopped before it writes something you have already told it not to, instead of you catching it in review.',
|
|
762
|
+
scope: SCOPE.PROJECT,
|
|
763
|
+
// Turning a gate on means hand-editing settings.json hook arrays — no single verified command.
|
|
764
|
+
turnOn: null,
|
|
765
|
+
detect({ project = process.cwd() } = {}) {
|
|
766
|
+
if (!helpers.gates) return row(STATE.UNKNOWN, `the gate survey could not be loaded (${helpers.gatesErr}) — gate state not checked`);
|
|
767
|
+
let survey;
|
|
768
|
+
try { survey = helpers.gates.gatesSurvey({ repo: project }); }
|
|
769
|
+
catch (e) { return row(STATE.UNKNOWN, `the gate survey failed (${String(e?.message || e).slice(0, 60)}) — gate state not checked`); }
|
|
770
|
+
|
|
771
|
+
const s = survey?.summary || {};
|
|
772
|
+
if (!s.armed) return row(STATE.ABSENT, 'no gates are wired on this machine or in this project');
|
|
773
|
+
// "1 gates are wired" shipped, because the plural on `refusal` was handled and the one on
|
|
774
|
+
// `gate` beside it was not. Small, but this surface is read by people deciding whether to
|
|
775
|
+
// trust it, and sloppy copy reads as sloppy measurement.
|
|
776
|
+
const gates = (n) => `${n} gate${n === 1 ? '' : 's'}`;
|
|
777
|
+
// ON, not OFF: gates ARE wired and ARE reading every move — they just cannot refuse one. That
|
|
778
|
+
// is a weaker MODE of running, not an absence of running. Reporting it OFF tells the user to
|
|
779
|
+
// switch on something already on, and hides that they have advisory coverage today.
|
|
780
|
+
// Found by GPT-5.6-Sol in the ADR-047 duel, 2026-07-24.
|
|
781
|
+
if (!s.blocking) return row(STATE.ON, `${gates(s.armed)} ${s.armed === 1 ? 'is' : 'are'} wired but ${s.armed === 1 ? 'it cannot' : 'none of them can'} actually refuse anything — ${s.armed === 1 ? 'it is' : 'they are'} advisory`);
|
|
782
|
+
// Receipts began only once the ledger was added, so "0 caught" is genuinely ambiguous between
|
|
783
|
+
// "never fired" and "fired before we were counting". Say armed, and say the caveat.
|
|
784
|
+
const caught = s.caughtTotal || 0;
|
|
785
|
+
return row(STATE.ON, caught > 0
|
|
786
|
+
? `${gates(s.blocking)} can refuse a write, and ${caught} refusal${caught === 1 ? ' has' : 's have'} been recorded (${s.caughtThisWeek || 0} this week)`
|
|
787
|
+
: `${gates(s.blocking)} can refuse a write; no refusals are recorded yet, which may mean nothing has warranted one`);
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
{
|
|
792
|
+
key: 'session-capture',
|
|
793
|
+
label: 'Session capture',
|
|
794
|
+
whatItBuysYou: 'What you worked out in a long session survives when the conversation is compacted or ends, instead of being lost with the window.',
|
|
795
|
+
scope: SCOPE.MACHINE,
|
|
796
|
+
// Registering hooks means editing settings.json by hand — no single verified command.
|
|
797
|
+
turnOn: null,
|
|
798
|
+
detect() {
|
|
799
|
+
const r = readJSON(path.join(HOME, '.claude/settings.json'));
|
|
800
|
+
if (r.missing) return row(STATE.ABSENT, 'no Claude Code settings file exists on this machine yet');
|
|
801
|
+
if (r.err) return row(STATE.UNKNOWN, `the settings file could not be parsed (${r.err}) — capture hooks not checked`);
|
|
802
|
+
const hooksRoot = r.value?.hooks;
|
|
803
|
+
if (hooksRoot !== undefined && (!hooksRoot || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot))) {
|
|
804
|
+
return row(STATE.UNKNOWN, 'the settings file has a hooks section this version cannot interpret — capture hooks not counted');
|
|
805
|
+
}
|
|
806
|
+
const hooks = hooksRoot || {};
|
|
807
|
+
// COUNT COMMANDS, NOT MATCHER GROUPS. See countHookCommands: `[{matcher:'.*',hooks:[]}]` has
|
|
808
|
+
// length 1 and executes nothing, and the old `.length` check called that "both boundaries are
|
|
809
|
+
// covered" — a fabricated ON on a machine that saves nothing.
|
|
810
|
+
//
|
|
811
|
+
// AND COUNT *CAPTURE* COMMANDS, NOT ANY COMMAND. The old count accepted whatever was wired at
|
|
812
|
+
// those two boundaries, so a shell logger and a terminal beep — neither of which saves a byte of
|
|
813
|
+
// session state — produced "Session capture: ON". MEASURED with exactly that pair. The boundary
|
|
814
|
+
// a command is attached to says WHEN it runs, never WHAT it does, and this row's whole claim is
|
|
815
|
+
// about what it does. A command is only counted when it names a mechanism known to persist state.
|
|
816
|
+
//
|
|
817
|
+
// A MALFORMED GROUP POISONS THE COUNT rather than being skipped — the same rule, and the same
|
|
818
|
+
// words, as learning-enable.readSettingsWiring, which documents at length why skipping an
|
|
819
|
+
// unparseable entry and reporting the remainder as a total is this project's signature lie.
|
|
820
|
+
// MEASURED: a PreCompact written as an object instead of an array was silently skipped and the
|
|
821
|
+
// row reported OFF — "nothing is saved when a session compacts" — about a machine whose capture
|
|
822
|
+
// hook we simply failed to parse. Identical structure to the bug fixed in that file, opposite
|
|
823
|
+
// treatment, same commit.
|
|
824
|
+
const pre = countCaptureCommands(hooks.PreCompact);
|
|
825
|
+
const end = countCaptureCommands(hooks.SessionEnd);
|
|
826
|
+
if (pre === null || end === null) {
|
|
827
|
+
return row(STATE.UNKNOWN, `the ${pre === null ? 'pre-compaction' : 'session-end'} hook list could not be parsed, so whether anything is registered there cannot be read — no conclusion is drawn from the half that did parse`);
|
|
828
|
+
}
|
|
829
|
+
// "registered", never "capturing" — the same standard the MCP row holds itself to twenty lines
|
|
830
|
+
// below. A settings entry proves a command is wired to fire; no local artifact proves it ever
|
|
831
|
+
// ran or that it succeeded when it did, and claiming captured state from a config file would be
|
|
832
|
+
// exactly the fabricated status this registry exists to refuse.
|
|
833
|
+
if (pre && end) return row(STATE.ON, 'a state-saving hook is registered at both boundaries: one before compaction and one at session end — registered, which is not the same as proven to have captured anything');
|
|
834
|
+
// ON, not OFF: one boundary IS covered. Partially configured is not never-used — half the
|
|
835
|
+
// sessions are being saved today, and calling that "off" both understates what they have and
|
|
836
|
+
// invites them to re-enable a thing already running. The gap is named in the evidence, which is
|
|
837
|
+
// where a real but partial shortfall belongs. Found by GPT-5.6-Sol, 2026-07-24.
|
|
838
|
+
if (pre || end) return row(STATE.ON, `a state-saving hook is registered only at ${pre ? 'the pre-compaction' : 'the session-end'} boundary — the other one loses its state`);
|
|
839
|
+
return row(STATE.OFF, 'no hook that saves session state is registered at either boundary, so nothing is kept when a session compacts or closes');
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
{
|
|
844
|
+
key: 'mcp-servers',
|
|
845
|
+
label: 'Connected tools (MCP)',
|
|
846
|
+
whatItBuysYou: 'Your AI can reach the services you have hooked up — your notes, your browser, your deployment host — instead of only what is in the chat.',
|
|
847
|
+
scope: SCOPE.USER,
|
|
848
|
+
// VERIFIED: `claude mcp --help` lists `add <name> <commandOrUrl> [args...]`.
|
|
849
|
+
turnOn: { human: 'Connect a tool', cmd: 'claude mcp add <name> <commandOrUrl>' },
|
|
850
|
+
detect() {
|
|
851
|
+
const r = readJSON(path.join(HOME, '.claude.json'));
|
|
852
|
+
if (r.missing) return row(STATE.ABSENT, 'no Claude Code config file exists on this machine yet');
|
|
853
|
+
if (r.err) return row(STATE.UNKNOWN, `the config file could not be parsed (${r.err}) — connected tools not counted`);
|
|
854
|
+
const names = Object.keys(r.value?.mcpServers || {});
|
|
855
|
+
if (!names.length) return row(STATE.OFF, 'no tools are configured');
|
|
856
|
+
// "configured", NEVER "connected". No local artifact proves a server answered, and claiming a
|
|
857
|
+
// live connection from a config entry would be a fabricated status.
|
|
858
|
+
return row(STATE.ON, `${names.length} tools are configured (${names.slice(0, 4).join(', ')}${names.length > 4 ? ', …' : ''}) — configured, which is not the same as currently reachable`);
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
{
|
|
863
|
+
key: 'nightly-refresh',
|
|
864
|
+
label: 'Nightly refresh',
|
|
865
|
+
whatItBuysYou: 'Your knowledge base updates itself overnight, so what your AI knows about your tools does not quietly go stale.',
|
|
866
|
+
scope: SCOPE.MACHINE,
|
|
867
|
+
// Loading a launchd job is machine mutation with no single verified command; global Rule 10.
|
|
868
|
+
turnOn: null,
|
|
869
|
+
detect() {
|
|
870
|
+
// launchd is macOS-only. On any other platform this is UNCHECKABLE, not off — this repo has
|
|
871
|
+
// already shipped a macOS-only assumption that went red the moment it met the Linux CI runner,
|
|
872
|
+
// and reporting "your nightly job is off" to a Linux user would be that same bug with worse
|
|
873
|
+
// consequences, because it reads as an actionable fault rather than a test failure.
|
|
874
|
+
if (process.platform !== 'darwin') return row(STATE.UNKNOWN, `scheduled jobs are managed by launchd, which does not exist on ${process.platform} — this cannot be checked here`);
|
|
875
|
+
let out;
|
|
876
|
+
try { out = execFileSync('launchctl', ['list'], { encoding: 'utf8', timeout: 15_000 }); }
|
|
877
|
+
catch (e) { return row(STATE.UNKNOWN, `could not list scheduled jobs (${String(e?.message || e).split('\n')[0].slice(0, 60)}) — nightly state not checked`); }
|
|
878
|
+
|
|
879
|
+
// THIS ROW IS ABOUT THE NIGHTLY KNOWLEDGE-BASE REFRESH, so it counts the nightly refresh — not
|
|
880
|
+
// every launchd job whose label happens to start com.ruvnet. MEASURED on this machine: that
|
|
881
|
+
// prefix match reported "11 refresh jobs are loaded and every one last exited cleanly" while
|
|
882
|
+
// sweeping in goldie-weekly, npx-witness, issue-fix, npm-token-renew, issue-watch,
|
|
883
|
+
// routing-flywheel, brain-gists, npx-72h-verdict and nightly-watchdog. Exactly ONE of the
|
|
884
|
+
// eleven (brain-nightly) was the thing the sentence claimed to describe. Ten unrelated jobs
|
|
885
|
+
// were being offered as evidence for a capability none of them implements.
|
|
886
|
+
//
|
|
887
|
+
// AND THE UNDER-COUNTING TWIN, which cost more (issue #113). The pattern below is a guess at
|
|
888
|
+
// what a refresh job is CALLED, and the one job this row is actually about is not called that:
|
|
889
|
+
// the installer loads `com.ruvnet.brain-update`, which contains neither "nightly" nor
|
|
890
|
+
// "refresh". So the console reported "no nightly refresh job is loaded" about a job that was
|
|
891
|
+
// loaded, scheduled for 03:47 and running nightly — a detector blind to its own installer.
|
|
892
|
+
// The label is now taken from nightly-controller.mjs, the module the console already uses to
|
|
893
|
+
// turn this job on and off, instead of being described a second time as a pattern here.
|
|
894
|
+
const NIGHTLY = /^com\.ruvnet\.[\w.-]*(nightly|refresh)/i;
|
|
895
|
+
const all = out.split('\n')
|
|
896
|
+
.map((l) => l.split('\t'))
|
|
897
|
+
.filter((c) => c.length >= 3 && /^com\.ruvnet\./.test(c[2] || ''))
|
|
898
|
+
.map((c) => ({ label: c[2].trim(), exit: c[1] }));
|
|
899
|
+
// The watchdog watches the refresh; it is not the refresh, and counting it inflates the answer.
|
|
900
|
+
const jobs = all.filter((j) => j.label === NIGHTLY_LABEL
|
|
901
|
+
|| (NIGHTLY.test(j.label) && !/watchdog/i.test(j.label)));
|
|
902
|
+
if (!jobs.length) {
|
|
903
|
+
return row(STATE.ABSENT, all.length
|
|
904
|
+
? `no nightly refresh job is loaded on this machine (${all.length} other RuvNet job${all.length === 1 ? '' : 's'} are scheduled, but none of them is the knowledge-base refresh)`
|
|
905
|
+
: 'no scheduled refresh jobs are loaded on this machine');
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const name = (j) => j.label.replace('com.ruvnet.', '');
|
|
909
|
+
// FAILING IS NOT DORMANT. REJECTED by both duelists 2026-07-24: a job that is loaded, scheduled and
|
|
910
|
+
// has RUN is installed and IN USE — a non-zero exit is a HEALTH problem belonging to the alarm
|
|
911
|
+
// channel, never a "you should switch this on" offer. Reporting it OFF is a category error, and it
|
|
912
|
+
// fired here for the worst possible reason: brain-nightly exited non-zero because the publish guard
|
|
913
|
+
// CORRECTLY refused to release from a non-main branch. A working safety guard was being reported as
|
|
914
|
+
// a dormant capability the user should go turn on.
|
|
915
|
+
const failing = jobs.filter((j) => j.exit !== '0' && j.exit !== '-');
|
|
916
|
+
if (failing.length) return row(STATE.ON, `${jobs.length} nightly refresh job${jobs.length === 1 ? '' : 's'} loaded and running, but ${failing.length} last exited non-zero (${failing.slice(0, 3).map((j) => `${name(j)}=${j.exit}`).join(', ')}) — installed and in use, so this is a health problem to look into, not a capability to switch on`);
|
|
917
|
+
|
|
918
|
+
// "-" IS NOT "0". launchd prints "-" for a job that has never run in this boot, and the old
|
|
919
|
+
// check lumped it in with success — so "every one last exited cleanly" could describe a job
|
|
920
|
+
// that has never executed once. That is the silence-reads-as-health failure the positive-
|
|
921
|
+
// confirmation standing order exists to kill, stated on the surface that is supposed to enforce it.
|
|
922
|
+
const neverRan = jobs.filter((j) => j.exit === '-');
|
|
923
|
+
if (neverRan.length === jobs.length) {
|
|
924
|
+
return row(STATE.UNKNOWN, `${jobs.length} nightly refresh job${jobs.length === 1 ? ' is' : 's are'} loaded (${jobs.map(name).slice(0, 3).join(', ')}) but ${jobs.length === 1 ? 'it has' : 'none has'} run since this machine last booted, so whether the refresh actually works here has not been demonstrated`);
|
|
925
|
+
}
|
|
926
|
+
if (neverRan.length) {
|
|
927
|
+
return row(STATE.ON, `${jobs.length} nightly refresh jobs are loaded; ${jobs.length - neverRan.length} last exited cleanly and ${neverRan.length} (${neverRan.map(name).slice(0, 3).join(', ')}) have not run since boot`);
|
|
928
|
+
}
|
|
929
|
+
return row(STATE.ON, `${jobs.length} nightly refresh job${jobs.length === 1 ? '' : 's'} loaded (${jobs.map(name).slice(0, 3).join(', ')}), and every one last exited cleanly`);
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
];
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Run every detect() and return one row per capability. NEVER throws: this feeds an advisory
|
|
936
|
+
* surface, and a surface that can crash the page it advises on is worse than no surface. A detector
|
|
937
|
+
* that throws is reported as 'unknown' with the thrown message — the failure becomes visible data
|
|
938
|
+
* rather than a missing row, because a silently dropped capability is indistinguishable from one
|
|
939
|
+
* that does not exist.
|
|
940
|
+
*/
|
|
941
|
+
export function auditAll({ project = process.cwd() } = {}) {
|
|
942
|
+
// The default is the CALLER'S directory, not this package's. See the note on REPO: taking no
|
|
943
|
+
// argument at all is what made every project-scoped row describe the wrong folder.
|
|
944
|
+
const ctx = { project: path.resolve(project), home: HOME };
|
|
945
|
+
return CAPABILITIES.map((c) => {
|
|
946
|
+
let r;
|
|
947
|
+
try { r = c.detect(ctx); }
|
|
948
|
+
catch (e) { r = row(STATE.UNKNOWN, `this check failed to run (${String(e?.message || e).split('\n')[0].slice(0, 70)})`); }
|
|
949
|
+
// A detector returning something malformed must not silently become 'undefined' on the page.
|
|
950
|
+
const state = Object.values(STATE).includes(r?.state) ? r.state : STATE.UNKNOWN;
|
|
951
|
+
const evidence = typeof r?.evidence === 'string' && r.evidence.trim()
|
|
952
|
+
? r.evidence
|
|
953
|
+
: 'this check returned no evidence, so its state is unknown';
|
|
954
|
+
return {
|
|
955
|
+
key: c.key,
|
|
956
|
+
label: c.label,
|
|
957
|
+
whatItBuysYou: c.whatItBuysYou,
|
|
958
|
+
scope: c.scope,
|
|
959
|
+
turnOn: c.turnOn,
|
|
960
|
+
state,
|
|
961
|
+
evidence,
|
|
962
|
+
// WHICH project a project-scoped row is about, named rather than assumed. "no memory store
|
|
963
|
+
// exists for this project" is only checkable by a reader who can see which folder was read.
|
|
964
|
+
...(c.scope === SCOPE.PROJECT ? { project: ctx.project } : {}),
|
|
965
|
+
};
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────────────────────────
|
|
970
|
+
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]).endsWith('capability-registry.mjs');
|
|
971
|
+
if (invokedDirectly) {
|
|
972
|
+
// --project mirrors capability-audit.mjs's --repo: the project-scoped rows are about a directory,
|
|
973
|
+
// and the person running this must be able to say which one rather than inferring it.
|
|
974
|
+
const pi = process.argv.indexOf('--project');
|
|
975
|
+
const project = pi >= 0 && process.argv[pi + 1] ? path.resolve(process.argv[pi + 1]) : process.cwd();
|
|
976
|
+
const rows = auditAll({ project });
|
|
977
|
+
if (process.argv.includes('--json')) { console.log(JSON.stringify(rows, null, 2)); process.exit(0); }
|
|
978
|
+
|
|
979
|
+
const MARK = { on: '●', off: '○', unknown: '?', absent: '·' };
|
|
980
|
+
const off = rows.filter((r) => r.state === STATE.OFF);
|
|
981
|
+
console.log(`\n ${rows.length} capabilities checked on this machine`);
|
|
982
|
+
console.log(` (project-scoped rows describe ${project.replace(HOME, '~')})\n`);
|
|
983
|
+
for (const r of rows) {
|
|
984
|
+
console.log(` ${MARK[r.state]} ${r.label.padEnd(24)} ${r.state.toUpperCase()} [${r.scope}]`);
|
|
985
|
+
console.log(` ${r.evidence}`);
|
|
986
|
+
if (r.state === STATE.OFF) {
|
|
987
|
+
console.log(` buys you: ${r.whatItBuysYou}`);
|
|
988
|
+
// No verified command is stated as exactly that. Silence would read as "nothing can be done".
|
|
989
|
+
console.log(r.turnOn ? ` turn on: ${r.turnOn.cmd}` : ` turn on: no verified one-line command exists for this`);
|
|
990
|
+
}
|
|
991
|
+
console.log('');
|
|
992
|
+
}
|
|
993
|
+
console.log(` ${off.length} of ${rows.length} are installed and switched off.\n`);
|
|
994
|
+
}
|