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.
Files changed (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/README.md +4 -4
  3. package/bin/install.mjs +303 -24
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  25. package/package.json +8 -22
  26. package/plugin/.claude-plugin/marketplace.json +1 -0
  27. package/plugin/.claude-plugin/plugin.json +2 -3
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/commands/brain-console.md +2 -2
  30. package/plugin/commands/configure.md +3 -2
  31. package/plugin/commands/rvbc.md +4 -3
  32. package/plugin/commands/rvcb.md +2 -2
  33. package/plugin/commands/whats-new.md +6 -6
  34. package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
  35. package/plugin/hooks/hooks.json +1 -2
  36. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  37. package/plugin/mcp/server.mjs +90 -32
  38. package/plugin/scripts/detach.mjs +14 -0
  39. package/plugin/scripts/first-session-worker.mjs +38 -0
  40. package/plugin/scripts/ground-ruvnet.sh +16 -6
  41. package/plugin/scripts/hook-shim.mjs +34 -29
  42. package/plugin/scripts/learn-capture.sh +22 -3
  43. package/plugin/scripts/learn-flush.mjs +21 -4
  44. package/plugin/scripts/runtime-preferences.mjs +269 -0
  45. package/plugin/scripts/session-start-core.mjs +503 -0
  46. package/plugin/scripts/session-start.sh +3 -858
  47. package/plugin/scripts/whats-new.mjs +42 -0
  48. package/plugin/skills/brain-console/SKILL.md +4 -2
  49. package/plugin/skills/release-proof/SKILL.md +98 -0
  50. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  51. package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
  52. package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
  54. package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
  55. package/plugin/skills/rvbc/SKILL.md +9 -6
  56. package/plugin/skills/whats-new/SKILL.md +4 -4
  57. package/scripts/adr-backfill.mjs +107 -0
  58. package/scripts/advocacy-outcomes.mjs +808 -0
  59. package/scripts/agentdb-context.mjs +216 -0
  60. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  61. package/scripts/ascii-drift.mjs +236 -0
  62. package/scripts/behavioral-l1-l4.mjs +210 -0
  63. package/scripts/brain-capability-check.mjs +72 -0
  64. package/scripts/brain-grade-groundtruth.mjs +100 -0
  65. package/scripts/brain-latency-50.mjs +227 -0
  66. package/scripts/brain-novice-50.mjs +189 -0
  67. package/scripts/brain-stamp.mjs +94 -0
  68. package/scripts/brain-state.mjs +212 -0
  69. package/scripts/build-bundle.mjs +531 -0
  70. package/scripts/build-concepts.mjs +132 -0
  71. package/scripts/build-l2.mjs +71 -0
  72. package/scripts/build-primer.mjs +73 -0
  73. package/scripts/build-symbols.mjs +68 -0
  74. package/scripts/calibrate-router.mjs +97 -0
  75. package/scripts/capability-audit.mjs +321 -0
  76. package/scripts/capability-registry.mjs +876 -0
  77. package/scripts/check-indexation.mjs +108 -0
  78. package/scripts/check-legibility.mjs +189 -0
  79. package/scripts/ci/build-fixture-kb.mjs +67 -0
  80. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  81. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  82. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  83. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  84. package/scripts/ci/stranger-scenario.mjs +228 -0
  85. package/scripts/ci/stranger-timeout.mjs +25 -0
  86. package/scripts/ci-verdict.mjs +29 -0
  87. package/scripts/claims-verify.mjs +710 -0
  88. package/scripts/clear-claude-tmp.sh +31 -0
  89. package/scripts/console-engine.mjs +434 -0
  90. package/scripts/console-engine.test.mjs +125 -0
  91. package/scripts/corpus-qa.mjs +250 -0
  92. package/scripts/correction-detect-embed.mjs +346 -0
  93. package/scripts/correction-detect-measure.mjs +270 -0
  94. package/scripts/correction-detect.mjs +686 -0
  95. package/scripts/count-chunks.mjs +54 -0
  96. package/scripts/described-questions.json +30 -0
  97. package/scripts/design-grade.mjs +58 -0
  98. package/scripts/dev-plugin-link.sh +105 -0
  99. package/scripts/distill-project.mjs +200 -0
  100. package/scripts/doc-currency.mjs +801 -0
  101. package/scripts/eval-brain.mjs +244 -0
  102. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  103. package/scripts/fix-workstream.mjs +291 -0
  104. package/scripts/full-hints.mjs +87 -0
  105. package/scripts/gate.sh +39 -0
  106. package/scripts/gates.mjs +146 -0
  107. package/scripts/gen-console-images.mjs +54 -0
  108. package/scripts/gen-images.mjs +47 -0
  109. package/scripts/git-clone-refresh.mjs +52 -0
  110. package/scripts/git-hooks/pre-push +126 -0
  111. package/scripts/goal-match.mjs +398 -0
  112. package/scripts/goldie-research.mjs +223 -0
  113. package/scripts/goldie-weekly.sh +67 -0
  114. package/scripts/health-repair.mjs +237 -0
  115. package/scripts/helix-scenario-questions.json +10 -0
  116. package/scripts/ingest-gists.mjs +230 -0
  117. package/scripts/ingest-meeting.mjs +115 -0
  118. package/scripts/ingest-repo.mjs +79 -0
  119. package/scripts/install-npx-witness.sh +49 -0
  120. package/scripts/issue-fix.mjs +558 -0
  121. package/scripts/issue-watch.mjs +276 -0
  122. package/scripts/issue4-close-note.md +31 -0
  123. package/scripts/key-canary.mjs +91 -0
  124. package/scripts/latency-to-surface.mjs +233 -0
  125. package/scripts/learning-enable.mjs +380 -0
  126. package/scripts/learning-replay.mjs +1570 -0
  127. package/scripts/learnings.mjs +62 -0
  128. package/scripts/lesson-gate.mjs +680 -0
  129. package/scripts/lesson-lifecycle.mjs +449 -0
  130. package/scripts/lesson-promote.mjs +262 -0
  131. package/scripts/lesson-ratify.mjs +98 -0
  132. package/scripts/lesson-seed.mjs +252 -0
  133. package/scripts/lesson-store.mjs +447 -0
  134. package/scripts/loop-checkpoint.mjs +86 -0
  135. package/scripts/memdb-health.sh +14 -0
  136. package/scripts/memory-doctor.mjs +326 -0
  137. package/scripts/model-catalog.mjs +79 -0
  138. package/scripts/nightly-controller.mjs +66 -0
  139. package/scripts/nightly-gists.sh +72 -0
  140. package/scripts/nightly-wrapper.sh +172 -0
  141. package/scripts/notify.sh +12 -0
  142. package/scripts/npx-witness.sh +56 -0
  143. package/scripts/onboarding-console.mjs +2922 -0
  144. package/scripts/private-fence.mjs +69 -0
  145. package/scripts/proactivity-metrics.mjs +118 -0
  146. package/scripts/proof-questions.json +56 -0
  147. package/scripts/protected-release-invocation.mjs +76 -0
  148. package/scripts/prove.mjs +95 -0
  149. package/scripts/proxy/claude-proxied.sh +57 -0
  150. package/scripts/proxy/proxy-revert.sh +59 -0
  151. package/scripts/proxy/proxy-up.sh +60 -0
  152. package/scripts/proxy/proxy-verify.mjs +142 -0
  153. package/scripts/publication-receipt.mjs +307 -0
  154. package/scripts/published-surface-probe.mjs +241 -0
  155. package/scripts/qe/card-lane-gate.mjs +162 -0
  156. package/scripts/qe/session-start-gate.mjs +229 -0
  157. package/scripts/qe/ux-suite.mjs +323 -0
  158. package/scripts/reconcile-project.mjs +0 -0
  159. package/scripts/record-lesson.mjs +113 -0
  160. package/scripts/refresh-model-catalog.mjs +99 -0
  161. package/scripts/release-authority.mjs +93 -0
  162. package/scripts/release-proof.mjs +9 -0
  163. package/scripts/release-vector.mjs +281 -0
  164. package/scripts/release.mjs +439 -0
  165. package/scripts/remedy-registry.mjs +247 -0
  166. package/scripts/rerank-cap-eval.mjs +265 -0
  167. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  168. package/scripts/route-cheap.mjs +20 -15
  169. package/scripts/router-utilization.mjs +182 -0
  170. package/scripts/routing-flywheel.mjs +596 -0
  171. package/scripts/rvf-generation.mjs +104 -0
  172. package/scripts/rvf-index-audit.mjs +138 -0
  173. package/scripts/self-update.mjs +296 -0
  174. package/scripts/selfcheck.mjs +7 -1
  175. package/scripts/sign-bundle.mjs +69 -0
  176. package/scripts/signal-watch.mjs +171 -0
  177. package/scripts/stabilization-receipt.mjs +108 -0
  178. package/scripts/stack-sync.mjs +469 -0
  179. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  180. package/scripts/stamp-sweep.mjs +144 -0
  181. package/scripts/status-honesty.mjs +102 -0
  182. package/scripts/sync-version.mjs +217 -0
  183. package/scripts/token-report.mjs +102 -0
  184. package/scripts/top100-benchmark.mjs +479 -0
  185. package/scripts/top100-corpus.mjs +112 -0
  186. package/scripts/top100-semantic-assertions.mjs +449 -0
  187. package/scripts/update-apply.mjs +9 -0
  188. package/scripts/upgrade-notice.mjs +14 -0
  189. package/scripts/verify-bundle.mjs +51 -0
  190. package/scripts/verify-channels.mjs +184 -0
  191. package/scripts/verify-model-catalog.mjs +104 -0
  192. package/scripts/verify-nightly-close-issue4.sh +31 -0
  193. package/scripts/version.mjs +40 -0
  194. package/scripts/wired-check.mjs +867 -0
  195. package/plugin/scripts/finalize-token-meter.mjs +0 -25
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lesson-promote.mjs — mine project-scoped lessons, find the UNIVERSAL ones, promote them.
4
+ *
5
+ * THE PROBLEM, MEASURED (2026-07-22, on the owner's own machine — this is not hypothetical):
6
+ *
7
+ * 736 lessons across 48 project memory stores.
8
+ * 284 of them are `type: feedback` — "how I want you to WORK", which is almost never
9
+ * project-specific — and they are scattered across 33 separate stores.
10
+ *
11
+ * "Test before claiming done" taught 87 times across 19 projects
12
+ * "Versioning / release discipline" taught 52 times across 14 projects
13
+ * "Never fabricate / be honest" taught 37 times across 14 projects
14
+ *
15
+ * The owner did not repeat himself because he forgot. He repeated himself because a lesson learned
16
+ * in project A physically cannot reach project B: Claude Code scopes memory to
17
+ * ~/.claude/projects/<project>/memory/, and nothing promotes upward. His words: "I shouldn't ever
18
+ * have to tell you twice." He has had to tell us 87 times.
19
+ *
20
+ * THE PROMOTION RULE IS NOT OURS. It is rUv's, from ruflo ADR-G008 ("Win Twice to Promote",
21
+ * Accepted/implemented): a rule may not enter the constitution on one good result, because one
22
+ * result is noise. We apply the same test with the strongest evidence available here — INDEPENDENT
23
+ * REDISCOVERY. A lesson the user taught in two or more separate projects has already won twice, in
24
+ * the only arena that matters: he needed it more than once, in places that could not see each other.
25
+ *
26
+ * That is deliberately NOT a similarity score or an LLM judgment call. It is a count of how many
27
+ * times a human independently arrived at the same instruction. Cheap, explainable, and impossible
28
+ * to fudge — which matters, because a promotion engine that guesses will pollute the global rules
29
+ * that govern every project, and a bad global rule is far more expensive than a missing one.
30
+ *
31
+ * READ-ONLY BY DEFAULT. Promotion writes to the user's global instructions, which is the highest
32
+ * blast-radius write this project performs. It requires --apply, backs up first, and is reversible.
33
+ *
34
+ * Usage:
35
+ * node scripts/lesson-promote.mjs # report only — what WOULD be promoted, and why
36
+ * node scripts/lesson-promote.mjs --json # machine-readable, for the console
37
+ * node scripts/lesson-promote.mjs --apply # write the promotion block (backs up first)
38
+ * node scripts/lesson-promote.mjs --min-projects 3
39
+ */
40
+ import fs from 'node:fs';
41
+ import path from 'node:path';
42
+ import os from 'node:os';
43
+
44
+ const HOME = os.homedir();
45
+ const PROJECTS = path.join(HOME, '.claude', 'projects');
46
+ const argv = process.argv.slice(2);
47
+ const has = (f) => argv.includes(f);
48
+ const arg = (f, d) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
49
+
50
+ // A lesson must have been independently learned in at least this many DISTINCT projects to be
51
+ // considered universal. 2 is ADR-G008's "win twice"; the flag exists so a cautious user can demand
52
+ // more evidence, never less — the floor is enforced below.
53
+ const MIN_PROJECTS = Math.max(2, parseInt(arg('--min-projects', '2'), 10) || 2);
54
+
55
+ /**
56
+ * Themes are the unit of promotion, not individual files.
57
+ *
58
+ * Promoting 87 near-identical "test first" lessons verbatim would be worse than promoting none —
59
+ * it would bury the global instructions under duplicates and make them unreadable, which is how a
60
+ * constitution stops being read. We cluster to the PROCESS, then promote one canonical statement of
61
+ * it, citing the projects that independently discovered it as the evidence.
62
+ *
63
+ * Deliberately keyword-based rather than embedding-based. An embedding cluster is a black box the
64
+ * user cannot audit, and this writes to the file that governs every project he owns. He must be able
65
+ * to read the rule that decided, disagree with it, and edit it. Legibility beats cleverness here.
66
+ */
67
+ const THEMES = [
68
+ { key: 'release-discipline', label: 'Versioning and release discipline',
69
+ match: /version|semver|bump|release|ship|deploy|publish|rollback/i },
70
+ { key: 'proof-before-done', label: 'Prove it works before calling it done',
71
+ match: /test|verify|prove|validat|\bqa\b|gate|green|passes/i },
72
+ { key: 'honesty', label: 'Never fabricate, never assume, never inflate',
73
+ match: /honest|lie|fabricat|assum|guess|placeholder|inflat|real data|made up/i },
74
+ { key: 'docs-upkeep', label: 'Keep docs and README current with the code',
75
+ match: /readme|document|changelog|\bdocs?\b|narrative/i },
76
+ { key: 'people', label: 'How to communicate with people',
77
+ match: /thank|contributor|personal|tone|nudge|deferential|communicat/i },
78
+ { key: 'tooling-discipline', label: 'Use the real tool; never hand-roll a substitute',
79
+ match: /hand-roll|impersonat|substitut|reinvent|use the tool|existing tool|ruvnet wins/i },
80
+ { key: 'cost-routing', label: 'Route work to the cheapest capable model',
81
+ match: /cheap|cost|route|routing|model selection|budget|spend/i },
82
+ ];
83
+
84
+ /** Every lesson file on this machine, with its project, type, and text. */
85
+ export function collectLessons(root = PROJECTS) {
86
+ const out = [];
87
+ let dirs = [];
88
+ try { dirs = fs.readdirSync(root); } catch { return out; }
89
+ for (const p of dirs) {
90
+ const md = path.join(root, p, 'memory');
91
+ if (!fs.existsSync(md)) continue;
92
+ let files = [];
93
+ try { files = fs.readdirSync(md); } catch { continue; }
94
+ for (const f of files) {
95
+ if (!f.endsWith('.md') || f === 'MEMORY.md') continue;
96
+ let s = '';
97
+ try { s = fs.readFileSync(path.join(md, f), 'utf8'); } catch { continue; }
98
+ const type = (s.match(/^\s*type:\s*(\w+)/m) || [])[1] || 'unknown';
99
+ const desc = (s.match(/^description:\s*"?(.*?)"?\s*$/m) || [])[1] || '';
100
+ out.push({
101
+ project: p.replace(/^-Users-[^-]+-/, ''),
102
+ file: f.replace(/\.md$/, ''),
103
+ type, desc,
104
+ // name + description only — never the body. The body can hold project specifics (paths,
105
+ // client names, URLs); the identity of a PROCESS lives in its title. Classifying on the body
106
+ // would drag project facts into a global rule, which is the one thing promotion must not do.
107
+ text: `${f} ${desc}`,
108
+ });
109
+ }
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /**
115
+ * Cluster lessons into themes and decide which have won often enough to be universal.
116
+ *
117
+ * Only `feedback` lessons are eligible. `project` lessons are, by their own declared type, about one
118
+ * codebase; promoting them would be a category error and would leak one client's details into every
119
+ * other project's context.
120
+ */
121
+ /**
122
+ * Themes the user has explicitly rejected. Read from the lesson store's demoted rows.
123
+ *
124
+ * WITHOUT THIS, DEMOTION WAS THEATRE. `lesson-ratify.mjs --demote` set a flag the miner never
125
+ * looked at, so the next mining run would re-propose the exact rule the user had just deleted.
126
+ * ADR-030 §5 states the requirement plainly — "a one-click demote that the next nightly silently
127
+ * undoes is worse than no demote at all, because the user stops trusting the control and, correctly,
128
+ * stops using it" — and the code did not implement it. Verified 2026-07-22: zero references to
129
+ * `demoted` in this file.
130
+ *
131
+ * Read defensively: the store may be absent, locked, or from a newer schema. A miner that throws
132
+ * because it could not read an optional file is worse than one that proposes a rejected theme.
133
+ */
134
+ function demotedThemeKeys() {
135
+ try {
136
+ const file = process.env.RUVNET_LESSON_STORE
137
+ || path.join(os.homedir(), '.config', 'ruvnet-brain', 'lessons.json');
138
+ const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
139
+ return new Set(
140
+ (raw.lessons || [])
141
+ .filter((l) => l && l.demoted === true && typeof l.themeKey === 'string')
142
+ .map((l) => l.themeKey),
143
+ );
144
+ } catch { return new Set(); }
145
+ }
146
+
147
+ export function analyze(lessons, { minProjects = MIN_PROJECTS, rejected = null } = {}) {
148
+ // Injectable for tests; defaults to the real store so the CLI honours real demotions.
149
+ const demoted = rejected instanceof Set ? rejected : demotedThemeKeys();
150
+ const eligible = lessons.filter((l) => l.type === 'feedback');
151
+ const themes = [];
152
+ for (const t of THEMES) {
153
+ const hits = eligible.filter((l) => t.match.test(l.text));
154
+ if (!hits.length) continue;
155
+ const projects = [...new Set(hits.map((h) => h.project))].sort();
156
+ // A theme the user has demoted is NEVER re-proposed. Sticky across every future run.
157
+ if (demoted.has(t.key)) continue;
158
+ themes.push({
159
+ key: t.key,
160
+ label: t.label,
161
+ lessons: hits.length,
162
+ projects,
163
+ projectCount: projects.length,
164
+ // The whole verdict, in one line anyone can check by hand.
165
+ universal: projects.length >= minProjects,
166
+ evidence: `taught ${hits.length} time${hits.length === 1 ? '' : 's'} across ${projects.length} independent project${projects.length === 1 ? '' : 's'}`,
167
+ examples: hits.slice(0, 4).map((h) => `${h.project}: ${h.file}`),
168
+ });
169
+ }
170
+ themes.sort((a, b) => b.projectCount - a.projectCount || b.lessons - a.lessons);
171
+
172
+ const promotable = themes.filter((t) => t.universal);
173
+ return {
174
+ scanned: { projects: new Set(lessons.map((l) => l.project)).size, lessons: lessons.length, feedback: eligible.length },
175
+ minProjects,
176
+ themes,
177
+ promotable,
178
+ // The headline the console should say out loud, computed rather than written.
179
+ headline: promotable.length
180
+ ? `${promotable.length} process${promotable.length === 1 ? '' : 'es'} you have taught in ${minProjects}+ separate projects are still trapped at project level`
181
+ : 'no cross-project process has met the promotion bar yet',
182
+ };
183
+ }
184
+
185
+ /** Render the promotion block. Idempotent, fenced, and safe to regenerate. */
186
+ export function renderBlock(result, now) {
187
+ const lines = [];
188
+ lines.push(BEGIN);
189
+ lines.push('<!-- Generated by scripts/lesson-promote.mjs. Regeneration REPLACES this fenced block');
190
+ lines.push(' wholesale on the next --apply — do NOT hand-edit between the markers, those changes');
191
+ lines.push(' are overwritten. Everything OUTSIDE the markers is left untouched. -->');
192
+ lines.push('');
193
+ lines.push(`## Cross-project lessons (promoted ${now})`);
194
+ lines.push('');
195
+ lines.push('These processes were learned independently in multiple projects. Per ruflo ADR-G008');
196
+ lines.push('("win twice to promote"), independent rediscovery IS the evidence — each one below was');
197
+ lines.push('needed more than once, in places that could not see each other.');
198
+ lines.push('');
199
+ for (const t of result.promotable) {
200
+ lines.push(`- **${t.label}** — ${t.evidence}.`);
201
+ lines.push(` <sub>projects: ${t.projects.slice(0, 6).join(', ')}${t.projects.length > 6 ? `, +${t.projects.length - 6} more` : ''}</sub>`);
202
+ }
203
+ lines.push('');
204
+ lines.push(END);
205
+ return lines.join('\n');
206
+ }
207
+
208
+ const BEGIN = '<!-- BEGIN ruvnet-brain: promoted-lessons -->';
209
+ const END = '<!-- END ruvnet-brain: promoted-lessons -->';
210
+
211
+ /** Write the block into the user's global CLAUDE.md, backing up first. Reversible by design. */
212
+ export function applyPromotion(result, { file, now }) {
213
+ if (!result.promotable.length) return { ok: true, noop: true, log: 'nothing met the promotion bar — nothing written' };
214
+ let existing = '';
215
+ try { existing = fs.readFileSync(file, 'utf8'); } catch { return { ok: false, log: `cannot read ${file}` }; }
216
+
217
+ const backup = `${file}.bak-promote-${now.replace(/[:.]/g, '-')}`;
218
+ try { fs.copyFileSync(file, backup); } catch (e) { return { ok: false, log: `refusing to write — backup failed: ${e.message}` }; }
219
+
220
+ const block = renderBlock(result, now);
221
+ const next = existing.includes(BEGIN)
222
+ ? existing.replace(new RegExp(`${BEGIN}[\\s\\S]*?${END}`), block) // replace ONLY our fence
223
+ : `${existing.trimEnd()}\n\n${block}\n`; // first run: append
224
+
225
+ try { fs.writeFileSync(file, next); } catch (e) { return { ok: false, log: `write failed: ${e.message}; backup at ${backup}` }; }
226
+ return { ok: true, backup, promoted: result.promotable.length, log: `promoted ${result.promotable.length} process(es) into ${file.replace(HOME, '~')}` };
227
+ }
228
+
229
+ // ── CLI ──────────────────────────────────────────────────────────────────────────────────────────
230
+ const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]).endsWith('lesson-promote.mjs');
231
+ if (invokedDirectly) {
232
+ const result = analyze(collectLessons());
233
+ if (has('--json')) { console.log(JSON.stringify(result, null, 2)); process.exit(0); }
234
+
235
+ console.log(`\n Scanned ${result.scanned.lessons} lessons across ${result.scanned.projects} projects `
236
+ + `(${result.scanned.feedback} are about how you want work done).\n`);
237
+ console.log(` ${result.headline}.\n`);
238
+ const w = 42;
239
+ for (const t of result.themes) {
240
+ const mark = t.universal ? ' ⬆ PROMOTE ' : ' · project ';
241
+ console.log(`${mark}${t.label.padEnd(w)} ${String(t.lessons).padStart(3)} lessons · ${t.projectCount} projects`);
242
+ }
243
+ if (result.promotable.length) {
244
+ console.log(`\n Evidence for each (independent rediscovery — ADR-G008 "win twice"):`);
245
+ for (const t of result.promotable) {
246
+ console.log(`\n ${t.label}`);
247
+ console.log(` ${t.evidence}`);
248
+ for (const ex of t.examples) console.log(` · ${ex}`);
249
+ }
250
+ }
251
+
252
+ if (has('--apply')) {
253
+ const file = arg('--file', path.join(HOME, '.claude', 'CLAUDE.md'));
254
+ const res = applyPromotion(result, { file, now: new Date().toISOString().slice(0, 10) });
255
+ console.log(`\n ${res.ok ? '✓' : '✗'} ${res.log}`);
256
+ if (res.backup) console.log(` backup: ${res.backup.replace(HOME, '~')}`);
257
+ process.exit(res.ok ? 0 : 1);
258
+ } else {
259
+ console.log(`\n This was a REPORT — nothing was written.`);
260
+ console.log(` To promote these into your global instructions: node scripts/lesson-promote.mjs --apply\n`);
261
+ }
262
+ }
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lesson-ratify.mjs — the human control over what the machine is allowed to enforce.
4
+ *
5
+ * The owner's requirement, verbatim (2026-07-22): "I should be able to see them all at a global
6
+ * level, and I should be able to go delete any ones on a global level that you thought were global
7
+ * but are really project-based."
8
+ *
9
+ * That is not a nice-to-have. ADR-029's promotion bar is evidence-based but not infallible — a
10
+ * keyword cluster can absolutely lift something local, and ADR-031's trust boundary exists because
11
+ * an adversarial review found that a hallucinated session summary could otherwise reach the
12
+ * objective function. A rule the user cannot see, audit, and delete is a rule imposed on them.
13
+ *
14
+ * Three verbs, and the asymmetry between them is the design:
15
+ *
16
+ * --list every lesson, its trigger, force, provenance, and evidence
17
+ * --ratify <id> a human agrees: raise it to the enforcement it was proposed at
18
+ * --demote <id> a human disagrees: it stops firing, PERMANENTLY
19
+ *
20
+ * Ratification is the ONLY path from candidate to enforcement, and `ratify()` refuses to raise a
21
+ * model-inferred lesson to `block` no matter what is asked of it. If the model could ratify its own
22
+ * inferences, the trust boundary would be a comment rather than a control.
23
+ *
24
+ * Demotion is STICKY — it survives every future mining run. A one-click reject that the next
25
+ * nightly quietly undoes is worse than no control at all, because the user stops trusting it and,
26
+ * correctly, stops using it.
27
+ */
28
+ import os from 'node:os';
29
+ import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, TRIGGERS } from './lesson-store.mjs';
30
+
31
+ const argv = process.argv.slice(2);
32
+ const arg = (f) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
33
+ const has = (f) => argv.includes(f);
34
+
35
+ const lessons = loadLessons();
36
+ if (!lessons.length) {
37
+ console.log('\n No lessons stored yet. Seed them with: node scripts/lesson-seed.mjs --apply\n');
38
+ process.exit(0);
39
+ }
40
+
41
+ const FORCE = { block: '⛔ BLOCKS', checklist: '☑ checklist', inject: '· context', review: '👁 review only' };
42
+
43
+ function list() {
44
+ const pend = pending(lessons);
45
+ console.log(`\n ${lessons.length} lessons — ${pend.length} awaiting your decision.\n`);
46
+ console.log(' Nothing here refuses your work until YOU ratify it. The model does not get to');
47
+ console.log(' ratify its own rules — that is the whole trust boundary.\n');
48
+
49
+ for (const t of Object.values(TRIGGERS)) {
50
+ const group = lessons.filter((l) => l.trigger === t.key);
51
+ if (!group.length) continue;
52
+ console.log(` ▸ WHEN ${t.label}`);
53
+ for (const l of group) {
54
+ const now = FORCE[l.enforcement] || l.enforcement;
55
+ const becomes = l.intendedEnforcement && l.intendedEnforcement !== l.enforcement
56
+ ? ` → ${FORCE[l.intendedEnforcement]} once ratified` : '';
57
+ const flag = l.demoted ? ' [DEMOTED — will never fire]' : '';
58
+ const who = l.origin === ORIGIN.USER_STATED ? 'you said it' : `${l.origin} — quarantined, can never block`;
59
+ console.log(` ${l.id}${flag}`);
60
+ console.log(` ${l.statement.slice(0, 110)}${l.statement.length > 110 ? '…' : ''}`);
61
+ console.log(` ${now}${becomes} · ${who} · taught ${l.repeatCount}× · weight ${weightOf(l)}`);
62
+ }
63
+ console.log('');
64
+ }
65
+ console.log(' node scripts/lesson-ratify.mjs --ratify <id> # agree: let it enforce');
66
+ console.log(' node scripts/lesson-ratify.mjs --demote <id> # disagree: silence it for good');
67
+ console.log(' node scripts/lesson-ratify.mjs --ratify-all-user-stated\n');
68
+ }
69
+
70
+ function show(next, id, verb) {
71
+ const l = next.find((x) => x.id === id);
72
+ if (!l) { console.log(`\n No lesson with id "${id}". Run --list to see them.\n`); process.exit(1); }
73
+ saveLessons(next);
74
+ console.log(`\n ✓ ${verb} ${l.id}`);
75
+ console.log(` now: ${FORCE[l.enforcement] || l.enforcement}${l.demoted ? ' (demoted — will never fire again, including after future mining runs)' : ''}`);
76
+ console.log(` stored at ${(process.env.RUVNET_LESSON_STORE || '~/.config/ruvnet-brain/lessons.json').replace(os.homedir(), '~')}\n`);
77
+ }
78
+
79
+ if (has('--ratify')) {
80
+ const id = arg('--ratify');
81
+ show(ratify(id, lessons), id, 'ratified');
82
+ } else if (has('--demote')) {
83
+ const id = arg('--demote');
84
+ show(demote(id, lessons), id, 'demoted');
85
+ } else if (has('--ratify-all-user-stated')) {
86
+ // Bulk convenience, deliberately scoped: it can only touch lessons the USER stated. Model-inferred
87
+ // lessons are never swept up by a bulk action — that would be exactly the hole the boundary closes.
88
+ let next = lessons;
89
+ const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED && l.status === STATUS.CANDIDATE && !l.demoted);
90
+ for (const l of targets) next = ratify(l.id, next);
91
+ saveLessons(next);
92
+ const nowBlocking = next.filter((l) => l.enforcement === ENFORCEMENT.BLOCK).length;
93
+ console.log(`\n ✓ ratified ${targets.length} lesson(s) you stated yourself.`);
94
+ console.log(` ${nowBlocking} now BLOCK at their decision point. Model-inferred lessons were left`);
95
+ console.log(` as candidates — a bulk action may never promote something the model inferred.\n`);
96
+ } else {
97
+ list();
98
+ }
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ // lesson-seed.mjs — the lessons of 2026-07-21/22, as executable objects.
3
+ //
4
+ // Every one has a dated, measured failure behind it from a single session, and each is classified by
5
+ // what could ACTUALLY have caught it. Several are declared `review`/`checklist`, meaning no hook can
6
+ // fully observe them — saying so is the honest move; claiming otherwise would be the
7
+ // under-enumeration failure (L10) committed while recording L10.
8
+ //
9
+ // PROVENANCE IS REAL HERE, not decorative. Lessons where the owner's words can be quoted are
10
+ // `user-stated`; lessons the model inferred about its OWN behaviour are `model-inferred` and are
11
+ // quarantined — they can never block, no matter how convincing they sound. Nothing here is ratified,
12
+ // because the model does not get to ratify its own rules. That is the point of the boundary.
13
+ //
14
+ // node scripts/lesson-seed.mjs # preview: what fires when, and at what force
15
+ // node scripts/lesson-seed.mjs --apply # store as CANDIDATES awaiting ratification
16
+
17
+ import os from 'node:os';
18
+ import {
19
+ makeLesson, saveLessons, loadLessons, lessonsFor, unenforceable, pending, weightOf,
20
+ TRIGGERS as T, ENFORCEMENT as E, ORIGIN as O,
21
+ } from './lesson-store.mjs';
22
+
23
+ // Shipped at CHECKLIST; `intendedEnforcement` records what it becomes once a human ratifies it.
24
+ const blocking = { enforcement: E.CHECKLIST, intendedEnforcement: E.BLOCK };
25
+
26
+ export const SEED = [
27
+ makeLesson({
28
+ id: 'L01-verify-with-a-capable-channel',
29
+ ...blocking,
30
+ origin: O.USER_STATED,
31
+ severity: 'high',
32
+ statement: 'Before claiming something works, verify through a channel CAPABLE of observing the change — an independent tool, a re-measurement, a read-write connection. Never the exit code of the thing being tested.',
33
+ trigger: T.CLAIM_DONE.key,
34
+ check: 'a measurement was taken AFTER the change by something other than the process that made it',
35
+ evidence: [
36
+ { observed: 'distillation wrote 684 patterns; the success check used a read-only connection that structurally cannot see another process\'s WAL, and reported "produced no new patterns"' },
37
+ { observed: 'the queue flush deleted 60 of 68 distinct lessons and reported success' },
38
+ { observed: 'the capture flush fired on an exact modulo, so the queue reached 491 while both ends reported healthy' },
39
+ { observed: 'a repair promised an undo the console had no branch for, and answered "nothing to undo"' },
40
+ { observed: 'the installer compared versions with !== and told a user who was AHEAD they were out of date' },
41
+ ],
42
+ repeatCount: 25,
43
+ projects: ['Code-PowerPlatePulse', 'Code-ruvnet-brain', 'Code-AppealArmor'],
44
+ }),
45
+
46
+ makeLesson({
47
+ id: 'L02-check-before-you-assert',
48
+ ...blocking,
49
+ origin: O.USER_STATED,
50
+ severity: 'high',
51
+ statement: 'Before stating any fact about the world — a version, an API, what a tool does, how an architecture works — read a live source THIS TURN and name it. Recalling is not checking. The urge to skip the check IS the signal you are about to be wrong.',
52
+ trigger: T.ASSERT_FACT.key,
53
+ check: 'a source was read this turn (tool call, file read, or command output) for the specific claim, and it is cited',
54
+ evidence: [
55
+ { observed: 'owner, 2026-07-22: "I ask you questions about architecture, and you immediately do a casual look and come back and tell me something dead wrong... those assumptions are the big toxic killer"' },
56
+ { observed: 'reading one --help cost 5 seconds and revealed distillation SILENTLY SKIPS corrupt stores — the entire reason the feature appeared broken' },
57
+ { observed: 'this project\'s own hook prints "EFFECTIVE BEATS EFFICIENT. Skipping this step has never once saved time" — and it fired on the author' },
58
+ { observed: 'a model name was asserted as available and turned out to be unsupported on the account in use — caught only by running it' },
59
+ ],
60
+ repeatCount: 28,
61
+ projects: ['Code-PowerPlatePulse', 'Code-ruvnet-brain', 'Code-AppealArmor', 'Code-BWEconstruction'],
62
+ }),
63
+
64
+ makeLesson({
65
+ id: 'L03-research-before-recommending',
66
+ enforcement: E.CHECKLIST,
67
+ origin: O.USER_STATED,
68
+ statement: 'Before recommending an architecture, research it: compare at least three real options with tradeoffs, and check whether the ecosystem already ships it. Pattern-matching from training data is not a recommendation.',
69
+ trigger: T.RECOMMEND_ARCH.key,
70
+ evidence: [
71
+ { observed: 'cross-project lesson promotion was about to be designed from scratch; grounding found the ecosystem already ships it three ways' },
72
+ { observed: 'a proxy health check was three lines from being hand-rolled when an existing doctor command already did all of it' },
73
+ ],
74
+ repeatCount: 6,
75
+ projects: ['Code-ruvnet-brain'],
76
+ }),
77
+
78
+ makeLesson({
79
+ id: 'L04-never-relay-a-number',
80
+ enforcement: E.CHECKLIST,
81
+ origin: O.USER_STATED,
82
+ severity: 'high',
83
+ statement: 'Never repeat a score, benchmark, or subagent result without re-checking the underlying artifact yourself. A number you did not measure is a claim you cannot defend.',
84
+ trigger: T.RELAY_NUMBER.key,
85
+ evidence: [
86
+ { observed: 'a known analyzer hallucinates scores on remote URLs; relaying one would have shipped a fabricated number' },
87
+ { observed: 'a capability detector reported "1 variant promoted" when that one was the baseline — making a run where every improvement was discarded read as partially successful' },
88
+ ],
89
+ repeatCount: 5,
90
+ projects: ['Code-ruvnet-brain', 'Code-PowerPlatePulse'],
91
+ }),
92
+
93
+ makeLesson({
94
+ id: 'L05-version-is-the-update-signal',
95
+ ...blocking,
96
+ origin: O.USER_STATED,
97
+ severity: 'high',
98
+ statement: 'Any behaviour-changing push bumps the version IN THE SAME COMMIT, and the release narrative is updated to match. A fix label on a new subsystem is a lie about what changed.',
99
+ trigger: T.SHIP.key,
100
+ check: 'the diff touches behaviour AND the version is unchanged from origin/main',
101
+ evidence: [
102
+ { observed: 'recorded 14 times in this repository alone — and violated again on 2026-07-22: six behaviour-changing commits at patch level with no bump, caught by the owner rather than the system' },
103
+ { observed: 'promotion across projects could not have helped: the lesson was already here, fourteen times over. Only enforcement closes this.' },
104
+ ],
105
+ repeatCount: 52,
106
+ projects: ['Code-ruvnet-brain', 'Code-AppealArmor', 'Code-PowerPlatePulse', 'Code-Chris-David-Salon'],
107
+ }),
108
+
109
+ makeLesson({
110
+ id: 'L06-use-the-real-tool',
111
+ ...blocking,
112
+ origin: O.USER_STATED,
113
+ severity: 'high',
114
+ statement: 'Before writing code in the RuvNet domain, search for the tool that already implements it. If you still disagree after genuinely looking, say so OUT LOUD, cite the source path, and name the hand-roll as a hand-roll. Never silently.',
115
+ trigger: T.WRITE_CODE.key,
116
+ check: 'the brain was searched for the capability being written, and the result is cited',
117
+ evidence: [
118
+ { observed: 'a fake router was built while the real one sat on npm' },
119
+ { observed: 'a hand-rolled capture hook was built while the real distill pipeline shipped the correct design' },
120
+ { observed: 'the ground-before-write gate fired 3 times on 2026-07-21 and was right every time' },
121
+ { observed: 'it fired twice more on 2026-07-22 while this very file was being written, and was right both times' },
122
+ ],
123
+ repeatCount: 6,
124
+ projects: ['Code-ruvnet-brain'],
125
+ }),
126
+
127
+ makeLesson({
128
+ id: 'L07-blast-radius-not-social-comfort',
129
+ ...blocking,
130
+ origin: O.USER_STATED,
131
+ severity: 'high',
132
+ statement: 'Gate on blast radius, not on how awkward an action feels. Ask: is it reversible, and is it outward-facing? A silent irreversible change is worse than an awkward reversible one.',
133
+ trigger: T.MUTATE_MACHINE.key,
134
+ check: 'the action is classified reversible/irreversible and internal/outward-facing before it runs, and an inverse is recorded first',
135
+ evidence: [
136
+ { observed: 'on 2026-07-22 permission was requested before filing a deletable comment, while six unversioned commits shipped silently in the same session — the risk model was exactly inverted' },
137
+ { observed: 'a --root flag did not scope, so a scratch-scoped run modified a real store outside it' },
138
+ ],
139
+ repeatCount: 4,
140
+ projects: ['Code-ruvnet-brain'],
141
+ }),
142
+
143
+ makeLesson({
144
+ id: 'L08-status-is-a-table',
145
+ enforcement: E.CHECKLIST,
146
+ origin: O.USER_STATED,
147
+ statement: 'Report status as a structured table with an explicit shipped/tested column, never as narrative. Prose lets unfinished work live inside sentences about progress.',
148
+ trigger: T.REPORT_STATUS.key,
149
+ evidence: [
150
+ { observed: 'the owner asked "where are we" four times in one session; each answer was a story, and the incomplete items became legible only once a table was produced' },
151
+ { observed: '"pushed" was allowed to read as "shipped" all evening; the installed plugin was three versions behind the repo the whole time' },
152
+ ],
153
+ repeatCount: 9,
154
+ projects: ['Code-ruvnet-brain', 'Code-PowerPlatePulse'],
155
+ }),
156
+
157
+ // ── MODEL-INFERRED. The model observed these about ITSELF, so they are quarantined: they may
158
+ // never block, regardless of how true they sound. That asymmetry is the trust boundary.
159
+ makeLesson({
160
+ id: 'L09-gradeable-is-not-valuable',
161
+ enforcement: E.CHECKLIST,
162
+ origin: O.MODEL_INFERRED,
163
+ statement: 'When choosing what to work on, name the ungradeable items explicitly and commit to them. The instinct to pick the task with a green test routes systematically away from the user\'s actual value.',
164
+ trigger: T.CHOOSE_WORK.key,
165
+ evidence: [
166
+ { observed: 'of nine requested items on 2026-07-22, the four with test suites were built and the four product surfaces were all skipped' },
167
+ ],
168
+ repeatCount: 3,
169
+ projects: ['Code-ruvnet-brain'],
170
+ }),
171
+
172
+ makeLesson({
173
+ id: 'L10-under-enumeration-is-a-tell',
174
+ enforcement: E.CHECKLIST,
175
+ origin: O.MODEL_INFERRED,
176
+ statement: 'When producing a list of failure modes, options, or requirements, state what was left out and why. A satisfyingly round number is evidence of rounding, not of completeness.',
177
+ trigger: T.REPORT_STATUS.key,
178
+ evidence: [
179
+ { observed: 'an ADR listed exactly five decision points and omitted the owner\'s Rule 0 — "the #1 cause of failure" — because a clean list of five felt more finished than a messy list of seven' },
180
+ ],
181
+ repeatCount: 2,
182
+ projects: ['Code-ruvnet-brain'],
183
+ }),
184
+
185
+ makeLesson({
186
+ id: 'L11-retrieval-without-volition-is-broken',
187
+ enforcement: E.REVIEW,
188
+ origin: O.MODEL_INFERRED,
189
+ statement: 'A surface that CAN detect something useful and stays silent is broken. Judge every feature by whether it volunteers what it knows, not by whether it can answer when asked.',
190
+ trigger: T.CHOOSE_WORK.key,
191
+ evidence: [
192
+ { observed: 'the brain held every fact needed to say "your learning is off" for 21 days and answered other questions instead' },
193
+ { observed: 'for three weeks the brain was queried only defensively — never once asked "what should we be using that we aren\'t?"' },
194
+ ],
195
+ repeatCount: 3,
196
+ projects: ['Code-ruvnet-brain'],
197
+ }),
198
+
199
+ makeLesson({
200
+ id: 'L12-efficiency-seeking-is-the-tell',
201
+ enforcement: E.REVIEW,
202
+ origin: O.USER_STATED,
203
+ statement: 'Treat the impulse to save a step as a defect signal, not a virtue. Skipping a check to save a turn is the specific mechanism that produces wrong answers — effectiveness first, always.',
204
+ trigger: T.CHOOSE_WORK.key,
205
+ evidence: [
206
+ { observed: 'owner, 2026-07-22: "Efficiency means nothing if you\'re wrong, and you focus on it far, far, far too often"' },
207
+ { observed: 'the assumption failure and the efficiency preference are the same behaviour, not two: checking costs a tool call, and turn-count optimisation is what skips it' },
208
+ ],
209
+ repeatCount: 4,
210
+ projects: ['Code-ruvnet-brain', 'Code-PowerPlatePulse'],
211
+ }),
212
+ ];
213
+
214
+ // ── CLI ──────────────────────────────────────────────────────────────────────────────────────────
215
+ const invokedDirectly = process.argv[1] && process.argv[1].endsWith('lesson-seed.mjs');
216
+ if (invokedDirectly) {
217
+ const byTrigger = {};
218
+ for (const l of SEED) (byTrigger[l.trigger] = byTrigger[l.trigger] || []).push(l);
219
+
220
+ console.log(`\n ${SEED.length} lessons — each bound to a trigger that makes it ACT.\n`);
221
+ for (const t of Object.values(T)) {
222
+ const ls = byTrigger[t.key];
223
+ if (!ls) continue;
224
+ const note = t.surface === 'text' ? 'fires on TEXT — least gated, most-failing surface'
225
+ : t.surface === 'plan' ? 'fires while CHOOSING — no hook can observe this'
226
+ : 'fires on a TOOL CALL';
227
+ console.log(` ▸ WHEN ${t.label}\n (${note})`);
228
+ for (const l of ls) {
229
+ const shown = l.intendedEnforcement ? `${l.enforcement}→${l.intendedEnforcement}` : l.enforcement;
230
+ console.log(` ${shown.padEnd(18)} ${l.id}`);
231
+ console.log(` ${''.padEnd(18)} ${l.origin === 'user-stated' ? 'you said it' : 'MODEL-INFERRED (quarantined — can never block)'} · taught ${l.repeatCount}× · weight ${weightOf(l)}`);
232
+ }
233
+ console.log('');
234
+ }
235
+ const un = unenforceable(SEED);
236
+ const pend = pending(SEED);
237
+ console.log(` ${pend.length} awaiting YOUR ratification. Nothing here blocks until you agree to it —`);
238
+ console.log(` the model does not get to ratify its own rules.`);
239
+ if (un.length) console.log(` ${un.length} are declared unenforceable (no hook can observe them); checked at review instead.`);
240
+ console.log('');
241
+
242
+ if (process.argv.includes('--apply')) {
243
+ const res = saveLessons(SEED);
244
+ const back = loadLessons();
245
+ console.log(` ✓ stored ${res.count} candidates at ${res.file.replace(os.homedir(), '~')}`);
246
+ console.log(` ✓ read back ${back.length}, each re-validated against the schema on load`);
247
+ console.log(` ✓ a gate at 'assert-fact' receives: ${lessonsFor('assert-fact', back).map((l) => l.id).join(', ') || 'none'}`);
248
+ console.log(` ✓ a gate at 'ship' receives: ${lessonsFor('ship', back).map((l) => l.id).join(', ') || 'none'}\n`);
249
+ } else {
250
+ console.log(` Preview only — nothing written. Use --apply to store.\n`);
251
+ }
252
+ }