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