ruvnet-brain 4.3.21 → 4.3.26

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 (143) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +275 -60
  3. package/console/app.js +189 -9
  4. package/console/index.html +70 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/style.css +26 -0
  9. package/console/tips.html +1 -0
  10. package/kb/corpus-release-identity.mjs +239 -0
  11. package/kb/update-storage-transaction.mjs +20 -3
  12. package/package.json +9 -2
  13. package/plugin/.claude-plugin/plugin.json +2 -2
  14. package/plugin/.codex-plugin/plugin.json +1 -1
  15. package/plugin/commands/checkpoint.md +61 -0
  16. package/plugin/hooks/codex-hooks.json +64 -1
  17. package/plugin/hooks/hook-contracts.json +299 -6
  18. package/plugin/hooks/hooks.json +81 -1
  19. package/plugin/mcp/server.mjs +23 -0
  20. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  21. package/plugin/scripts/advocacy-route.mjs +460 -0
  22. package/plugin/scripts/continuation-gate.mjs +25 -2
  23. package/plugin/scripts/continuation-objective.mjs +7 -1
  24. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  25. package/plugin/scripts/coverage-integrity.mjs +7 -0
  26. package/plugin/scripts/gates.mjs +113 -10
  27. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  28. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  29. package/plugin/scripts/hook-shim.mjs +14 -0
  30. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  31. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  32. package/plugin/scripts/project-progression-contract.mjs +16 -0
  33. package/plugin/scripts/project-progression-hook.mjs +3 -0
  34. package/plugin/scripts/project-progression-producer.mjs +252 -0
  35. package/plugin/scripts/project-progression-reader.mjs +271 -0
  36. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  37. package/plugin/scripts/project-progression-sources.mjs +220 -0
  38. package/plugin/scripts/project-progression-store.mjs +106 -13
  39. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  40. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  41. package/plugin/scripts/session-start-budget.mjs +59 -0
  42. package/plugin/scripts/session-start-core.mjs +234 -457
  43. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  44. package/plugin/scripts/session-start-health.mjs +64 -0
  45. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  46. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  47. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  48. package/plugin/scripts/session-start-signals.mjs +73 -0
  49. package/plugin/scripts/session-start-trace.mjs +86 -0
  50. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  51. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  52. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  53. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  54. package/scripts/adr-072-completion.mjs +1 -1
  55. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  56. package/scripts/approved-runtime.mjs +197 -0
  57. package/scripts/brain-novice-50.mjs +16 -1
  58. package/scripts/brain-score.mjs +23 -5
  59. package/scripts/build-bundle.mjs +971 -530
  60. package/scripts/build-concepts.mjs +36 -116
  61. package/scripts/console-engine.test.mjs +8 -7
  62. package/scripts/console-runtime-identity.mjs +4 -0
  63. package/scripts/corpus-aggregates.mjs +94 -77
  64. package/scripts/corpus-candidate.mjs +475 -222
  65. package/scripts/corpus-next-seed.mjs +225 -0
  66. package/scripts/corpus-promotion.mjs +58 -0
  67. package/scripts/corpus-reconcile.mjs +411 -105
  68. package/scripts/doc-currency.mjs +16 -1
  69. package/scripts/dual-host-deliberation.mjs +25 -2
  70. package/scripts/dual-host-suggest.mjs +17 -1
  71. package/scripts/falsify.mjs +13 -3
  72. package/scripts/gist-receipts.mjs +482 -87
  73. package/scripts/github-health-watch.mjs +12 -2
  74. package/scripts/handoff-asset.mjs +34 -0
  75. package/scripts/hook-retirement-check.mjs +8 -1
  76. package/scripts/host-registry.mjs +1 -1
  77. package/scripts/ingest-gists.mjs +74 -101
  78. package/scripts/job-heartbeat.sh +77 -14
  79. package/scripts/learning-replay-execution.mjs +10 -4
  80. package/scripts/nightly-gists.sh +27 -13
  81. package/scripts/nightly-two-run-proof.mjs +1 -1
  82. package/scripts/nightly-watchdog.mjs +61 -4
  83. package/scripts/onboarding-console.mjs +364 -28
  84. package/scripts/oracle/produce-questions.mjs +293 -0
  85. package/scripts/oracle/producer-hosts.mjs +235 -0
  86. package/scripts/oracle/repo-recall.mjs +448 -0
  87. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  88. package/scripts/oracle/source-tree.mjs +165 -0
  89. package/scripts/oracle/source-units.mjs +391 -0
  90. package/scripts/oracle/spike-run.mjs +98 -0
  91. package/scripts/oracle/unit-inventory.mjs +141 -0
  92. package/scripts/oracle/unit-sampling.mjs +128 -0
  93. package/scripts/oracle/validate-labels.mjs +250 -0
  94. package/scripts/private-overlay.mjs +248 -0
  95. package/scripts/product-integrity-contract.mjs +1 -1
  96. package/scripts/proxy/claude-proxied.sh +6 -0
  97. package/scripts/proxy/proxy-revert.sh +5 -0
  98. package/scripts/proxy/proxy-up.sh +6 -0
  99. package/scripts/proxy/proxy-verify.mjs +4 -0
  100. package/scripts/public-inputs.mjs +409 -0
  101. package/scripts/public-verification-inputs.mjs +112 -26
  102. package/scripts/public-verification-lane.mjs +1 -1
  103. package/scripts/published-surface-probe.mjs +34 -4
  104. package/scripts/qe/card-lane-gate.mjs +16 -1
  105. package/scripts/qe/session-start-gate.mjs +16 -1
  106. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  107. package/scripts/record-lesson.mjs +4 -1
  108. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  109. package/scripts/release-abort-stale.mjs +5 -1
  110. package/scripts/release-authority.mjs +104 -12
  111. package/scripts/release-channel-kind.mjs +86 -0
  112. package/scripts/release-convergence-watchdog.mjs +7 -2
  113. package/scripts/release-projection.mjs +177 -72
  114. package/scripts/release-transaction-provider.mjs +47 -10
  115. package/scripts/release-transaction.mjs +40 -11
  116. package/scripts/release.mjs +252 -17
  117. package/scripts/retrieval-canary.mjs +87 -0
  118. package/scripts/rvf-index-audit.mjs +573 -13
  119. package/scripts/rvf-wire.mjs +269 -0
  120. package/scripts/seal-gist-receipt.mjs +65 -0
  121. package/scripts/selfcheck.mjs +42 -21
  122. package/scripts/source-coverage.mjs +253 -24
  123. package/scripts/status-honesty.mjs +25 -0
  124. package/scripts/sync-census.mjs +0 -0
  125. package/scripts/sync-version.mjs +2 -0
  126. package/scripts/trismart.mjs +42 -0
  127. package/scripts/updater-manifest.mjs +162 -0
  128. package/scripts/verify-channels.mjs +17 -5
  129. package/scripts/wired-check.mjs +48 -10
  130. package/tri-smart-skill/QUICKSTART.md +37 -0
  131. package/tri-smart-skill/README.md +92 -0
  132. package/tri-smart-skill/install.cmd +14 -0
  133. package/tri-smart-skill/install.command +13 -0
  134. package/tri-smart-skill/install.mjs +51 -0
  135. package/tri-smart-skill/install.sh +9 -0
  136. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  137. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  138. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  139. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  140. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  141. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  142. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  143. package/scripts/corpus-seed-publish.mjs +0 -110
@@ -3,346 +3,43 @@
3
3
  // latency budget before the shell body reached its first instruction—motivated moving every host
4
4
  // onto this dependency-free core. It runs from both the frozen plugin and a Stable Spine generation;
5
5
  // session-start.sh remains only a compatibility launcher for callers that still invoke that path.
6
+ //
7
+ // 2026-09-11 REWRITE (latency + scoping + banner-truth pass, two-reviewer-corrected):
8
+ // - Stage bodies moved into small sibling modules (session-start-{fsutil,health,signals,
9
+ // issue-alert,repo-identity,update-plane,hook-description,trace,budget}.mjs) so this file stays
10
+ // under 500 lines. `announceVersion` and `commandExists` were dead code (defined, never called,
11
+ // never tested anywhere) and were deleted rather than relocated.
12
+ // - Every stage now runs under session-start-trace.mjs's stage()/stageAsync(), budgeted by
13
+ // session-start-budget.mjs's STAGE_BUDGETS_MS (the derived-sum contract enforced by
14
+ // tests/unit/session-start-budget.test.mjs). A stage that would blow the shared deadline is
15
+ // SKIPPED with an always-written stderr note instead of risking the whole hook getting killed by
16
+ // its own hooks.json watchdog with zero output (the exact failure this pass started from).
17
+ // - The maintainer-only open-issue detail moved OUT of this hook entirely; SessionStart may print
18
+ // at most a one-line pointer (session-start-issue-alert.mjs), gated by BOTH the per-user
19
+ // entitlement file AND the current project's git remote being the entitled repo (fixed constant,
20
+ // session-start-repo-identity.mjs) — closing a real leak where the alert surfaced in unrelated
21
+ // projects.
22
+ // - LOCAL installation-integrity conditions (split plugin/bundle generation, retrieval down) are
23
+ // NOT maintainer-only: they are delivered to every user, worded for the user, through the same
24
+ // always-shown alarm mechanism as the existing HEALTH ALARM.
6
25
  import fs from 'node:fs';
7
26
  import os from 'node:os';
8
27
  import path from 'node:path';
9
- import { spawnSync } from 'node:child_process';
10
28
  import { fileURLToPath } from 'node:url';
11
29
  import { restoreProgressionForSession } from './project-progression-session-start.mjs';
30
+ import {
31
+ read, json, exists, mkdir, write, runNode,
32
+ } from './session-start-fsutil.mjs';
33
+ import { maintainerIssueEntitlement, surfaceIssuePointer } from './session-start-issue-alert.mjs';
34
+ import { surfaceSignals } from './session-start-signals.mjs';
35
+ import { brainState, health, mcpReadiness } from './session-start-health.mjs';
36
+ import { stableSpine, heartbeat } from './session-start-update-plane.mjs';
37
+ import { describeLifecycleHooks, readHookContracts } from './session-start-hook-description.mjs';
38
+ import { createStageTracer } from './session-start-trace.mjs';
12
39
 
13
- const HERE = path.dirname(fileURLToPath(import.meta.url));
14
- const read = (file, fallback = '') => {
15
- try { return fs.readFileSync(file, 'utf8'); } catch { return fallback; }
16
- };
17
- const json = (file, fallback = null) => {
18
- try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; }
19
- };
20
- const exists = (file) => {
21
- try { return fs.existsSync(file); } catch { return false; }
22
- };
23
- const mkdir = (dir) => {
24
- try { fs.mkdirSync(dir, { recursive: true }); return true; } catch { return false; }
25
- };
26
- const write = (file, value) => {
27
- try { mkdir(path.dirname(file)); fs.writeFileSync(file, String(value)); return true; } catch { return false; }
28
- };
29
- const mtimeMs = (file) => {
30
- try { return fs.statSync(file).mtimeMs; } catch { return 0; }
31
- };
32
- const firstVersion = (text) => String(text).split(/\r?\n/).find((line) =>
33
- /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(line.trim()))?.trim() || '';
34
- const commandExists = (name, env, platform) => {
35
- const extensions = platform === 'win32'
36
- ? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';')
37
- : [''];
38
- for (const dir of String(env.PATH || '').split(path.delimiter).filter(Boolean)) {
39
- for (const extension of extensions) {
40
- if (exists(path.join(dir, `${name}${extension}`))) return true;
41
- if (platform === 'win32' && exists(path.join(dir, `${name}${extension.toLowerCase()}`))) return true;
42
- }
43
- }
44
- return false;
45
- };
46
-
47
- const runNode = (file, args = [], options = {}) => {
48
- if (!exists(file)) return null;
49
- try {
50
- return spawnSync(process.execPath, [file, ...args], {
51
- cwd: options.cwd,
52
- env: options.env,
53
- encoding: 'utf8',
54
- stdio: options.stdio || 'ignore',
55
- timeout: options.timeout,
56
- windowsHide: true,
57
- });
58
- } catch { return null; }
59
- };
60
-
61
- const dispatchDetached = (hookDir, ttl, log, command, args = [], env = process.env) =>
62
- runNode(path.join(hookDir, 'detach.mjs'), [
63
- String(ttl), log, command, ...args,
64
- ], { env, stdio: 'ignore', timeout: 2000 })?.status === 0;
65
-
66
- export const maintainerIssueEntitlement = (env, home, repo, platform = process.platform) => {
67
- const file = env.RUVNET_BRAIN_MAINTAINER_ISSUES_FILE
68
- || path.join(home, '.config', 'ruvnet-brain', 'maintainer-issues.json');
69
- // Windows ACL ownership is not available through this dependency-free hot path. Fail closed
70
- // instead of weakening an owner-only promise into "any local user who can write the file".
71
- if (platform === 'win32') return false;
72
- let stat;
73
- try { stat = fs.lstatSync(file); } catch { return false; }
74
- if (!stat.isFile() || stat.isSymbolicLink()) return false;
75
- // This is maintainer-only operational data. Refuse group/world-readable opt-ins on POSIX so a
76
- // shared machine cannot turn a private maintainer signal into a terminal banner for other users.
77
- if ((stat.mode & 0o077) !== 0) return false;
78
- if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) return false;
79
- const entitlement = json(file);
80
- return entitlement?.enabled === true
81
- && Array.isArray(entitlement.repos)
82
- && entitlement.repos.includes(repo);
83
- };
84
-
85
- const surfaceIssues = (stateDir, emit, now, env, home, platform) => {
86
- const status = json(path.join(stateDir, 'open-issues.json'));
87
- const observedAt = Date.parse(status?.at || '');
88
- if (!Number.isFinite(observedAt) || observedAt > now + 5 * 60_000 || now - observedAt > 6 * 3600_000) return;
89
- if (!maintainerIssueEntitlement(env, home, status.repo, platform)) return;
90
- const open = Array.isArray(status.issues) ? status.issues : [];
91
- if (!open.length) return;
92
- const breaches = open.filter((issue) => issue.breach).sort((a, b) => b.ageHours - a.ageHours);
93
- const selected = breaches.length ? breaches : [...open].sort((a, b) => b.ageHours - a.ageHours);
94
- const top = selected.slice(0, 4).map((issue) =>
95
- breaches.length
96
- ? `#${issue.number} (${issue.ageHours}h) ${String(issue.title || '').slice(0, 64)}`
97
- : `#${issue.number} (${issue.ageHours}h)`).join(' · ');
98
- const more = selected.length > 4 ? ` · +${selected.length - 4} more` : '';
99
- if (breaches.length) {
100
- emit('[RuvNet Brain — OPEN ISSUES need attention (surface this to the maintainer, once, near the top)]');
101
- emit(`${breaches.length} open issue(s) past SLA on ${status.repo}: ${top}${more}; open issues are visible to the maintainer`);
102
- emit('These are real user-filed bugs sitting past the response SLA. Mention them plainly so they do not stack unseen; offer to fix them (gh issue list --state open for detail).');
103
- return;
104
- }
105
- emit("[RuvNet Brain — open issues on the maintainer's repo (mention once, calmly, near the top)]");
106
- emit(`${open.length} open issue(s) on ${status.repo}, none past SLA: ${top}${more}; open issues are visible to the maintainer`);
107
- emit('Within SLA, but the maintainer should know they exist. One line is enough; offer to look.');
108
- };
109
-
110
- const surfaceSignals = ({ env, cwd, stateDir, hookDir, emit, now }) => {
111
- const signalDir = env.RUVNET_SIGNAL_DIR || path.join(stateDir, 'external-signals');
112
- const pending = path.join(signalDir, 'pending.jsonl');
113
- const statusFile = path.join(signalDir, 'ci-status.json');
114
- const surfacedFile = path.join(signalDir, 'surfaced.json');
115
- if (!exists(pending)) return;
116
-
117
- const poller = path.join(env.CLAUDE_PROJECT_DIR || cwd, 'scripts', 'signal-watch.mjs');
118
- if (exists(poller) && now - mtimeMs(statusFile) > 600_000) {
119
- runNode(poller, [], { cwd, env, stdio: 'ignore', timeout: 3500 });
120
- }
121
- const status = json(statusFile);
122
- if (!status || typeof status !== 'object') return;
123
- const onDisk = json(surfacedFile, {});
124
- const surfaced = {
125
- debts: onDisk?.debts && typeof onDisk.debts === 'object' ? onDisk.debts : {},
126
- redRepo: onDisk?.redRepo && typeof onDisk.redRepo === 'object' ? onDisk.redRepo : {},
127
- };
128
- let changed = false;
129
- const entries = Object.entries(status).sort((a, b) =>
130
- new Date(a[1]?.checkedAt || 0) - new Date(b[1]?.checkedAt || 0));
131
- for (const [key, debt] of entries) {
132
- const shortSha = String(debt?.ref || '').slice(0, 7);
133
- if (debt?.state === 'resolved' && debt.conclusion !== 'success') {
134
- if (surfaced.debts[key] === 'red') continue;
135
- emit(`[RuvNet Brain — EXTERNAL SIGNAL: CI is RED for ${debt.repo}@${shortSha} — surface this to the user now, near the top, with ZERO prompting]`);
136
- emit(`Workflow ${debt.workflowName || 'ci'} concluded ${debt.conclusion} on ${debt.repo}@${shortSha}; inspect with gh run list --repo ${debt.repo} --commit ${debt.ref}.`);
137
- surfaced.debts[key] = 'red';
138
- surfaced.redRepo[debt.repo] = key;
139
- changed = true;
140
- continue;
141
- }
142
- if (debt?.state === 'resolved' && debt.conclusion === 'success') {
143
- if (surfaced.redRepo[debt.repo]) {
144
- emit(`[RuvNet Brain — external signal: CI is GREEN again for ${debt.repo}@${shortSha} — one line, then move on]`);
145
- delete surfaced.redRepo[debt.repo];
146
- changed = true;
147
- }
148
- if (surfaced.debts[key] !== 'green') { surfaced.debts[key] = 'green'; changed = true; }
149
- continue;
150
- }
151
- if (debt?.state === 'unverifiable' && surfaced.debts[key] !== 'unverifiable') {
152
- emit(`[RuvNet Brain — external signal: CI status could not be checked for ${debt.repo}@${shortSha}: ${debt.reason || 'unknown reason'}]`);
153
- surfaced.debts[key] = 'unverifiable';
154
- changed = true;
155
- }
156
- }
157
- if (changed) write(surfacedFile, JSON.stringify(surfaced, null, 2));
158
- };
159
-
160
- const brainState = (env, home) => {
161
- const stateDir = env.RUVNET_BRAIN_STATE_DIR || path.join(home, '.config', 'ruvnet-brain');
162
- const file = path.join(stateDir, 'brain-off');
163
- let off = env.RUVNET_BRAIN_OFF === '1';
164
- if (!off) {
165
- try { fs.statSync(file); off = true; }
166
- catch (error) { off = !(error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')); }
167
- }
168
- let since = '';
169
- if (off) {
170
- const record = json(file);
171
- if (typeof record?.since === 'string') since = record.since.slice(0, 10);
172
- if (!since && mtimeMs(file)) since = new Date(mtimeMs(file)).toISOString().slice(0, 10);
173
- }
174
- return { off, since, stateDir, file };
175
- };
176
-
177
- const health = (home, off) => {
178
- const kb = path.join(home, '.cache', 'ruvnet-brain', 'kb');
179
- let rvf = false;
180
- try { rvf = fs.readdirSync(kb).some((name) => name.endsWith('.rvf') && exists(path.join(kb, name))); }
181
- catch { /* absent */ }
182
- const absentByChoice = off && (!exists(kb) || !rvf);
183
- if (absentByChoice) return { problem: '', absentByChoice };
184
- if (!exists(kb)) return { problem: `the brain cache directory is MISSING (${kb}) — reinstall: npx github:stuinfla/ruvnet-brain`, absentByChoice };
185
- if (!rvf) return { problem: `NO vector stores (.rvf) found in ${kb} — the brain is empty; reinstall: npx github:stuinfla/ruvnet-brain --force`, absentByChoice };
186
- if (!exists(path.join(kb, 'node_modules', '@xenova', 'transformers', 'package.json'))) {
187
- return { problem: `reader dependencies are MISSING (node_modules gone) — every search WILL fail. Fix: cd ${kb} && npm i`, absentByChoice };
188
- }
189
- const last = json(path.join(home, '.cache', 'ruvnet-brain', 'health.json'));
190
- if (last?.status === 'down') {
191
- const detail = `"error": ${JSON.stringify(String(last.error || 'unknown error'))}`.slice(0, 180);
192
- return { problem: `the last real search FAILED across all repos (${detail}). Fix: cd ${kb} && npm i, then run one search to clear the alarm`, absentByChoice };
193
- }
194
- return { problem: '', absentByChoice };
195
- };
196
-
197
- const mcpReadiness = (env, home) => {
198
- const brainHome = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
199
- const receipt = json(path.join(brainHome, 'mcp-readiness.json'));
200
- if (receipt?.state === 'ready' && Number.isInteger(receipt.pid) && Number.isInteger(receipt.workerPid)) {
201
- try {
202
- process.kill(receipt.pid, 0);
203
- process.kill(receipt.workerPid, 0);
204
- return { state: 'ready', receipt };
205
- } catch { /* a stale receipt is registration evidence, not live evidence */ }
206
- }
207
- if (receipt?.state === 'degraded') return { state: 'degraded', receipt };
208
- return { state: 'registered', receipt };
209
- };
210
-
211
- const announceVersion = ({ running, off, stateDir, consoleInvoke, emit }) => {
212
- if (off || !running) return;
213
- const announced = path.join(stateDir, '.last-announced-version');
214
- const milestoneFile = path.join(stateDir, '.last-major-milestone');
215
- const [base] = running.split('-');
216
- const parts = base.split('.').map(Number);
217
- let milestone = '';
218
- if (parts.every(Number.isFinite)) {
219
- const [major = 0, minor = 0, patch = 0] = parts;
220
- if (major >= 4) milestone = '4.x';
221
- else if (major * 1_000_000 + minor * 1000 + patch >= 3_009_071) milestone = '4.0-line';
222
- }
223
- let milestoneShown = false;
224
- if (milestone && read(milestoneFile).trim() !== milestone) {
225
- milestoneShown = true;
226
- emit(`[RuvNet Brain — MAJOR-LINE welcome (${milestone}), show ONCE near the top of your first response]`);
227
- emit("They upgraded INTO the big release and will NOT have seen the web explainer. Warmly, a few sentences, adapt — don't recite:");
228
- emit(milestone === '4.x'
229
- ? ` 1. They're on RuvNet-Brain ${running}, a major version — the release where the brain got honest, legible, fast and self-measuring.`
230
- : ` 1. The 4.0-LINE enhancements have landed (they're on v${running}). Honestly: the number stays 3.9.x until the work is field-verified (ADR-042), so 'the 4.0-line upgrades are here', NOT 'you're on 4.0'.`);
231
- emit(` 2. Two or three things they can DO now: the Console on ${consoleInvoke} (their whole stack, one live page); every number measured from their machine, never projected; it learns across their projects.`);
232
- emit(` 3. OFFER to open the Console now — on yes, invoke ${consoleInvoke}. Point at the native what's-new workflow for the full highlights.`);
233
- emit("Never claim 'proven better' or 'fully proactive' — the self-measurement is new and still filling. Once only.");
234
- write(milestoneFile, `${milestone}\n`);
235
- }
236
- if (read(announced).trim() === running) return;
237
- const whatsNew = running.startsWith('3.2.') || running.startsWith('3.1.')
238
- ? `opens with the Console — ${consoleInvoke} shows your whole RuvNet stack on one live page: what's installed, what your AI has actually learned from YOUR projects, and one-click reversible fixes. Every number is measured from your machine, never projected.`
239
- : running.startsWith('3.0.') ? 'ships a visual configurator — /ruvnet-brain:configure mirrors your machine\'s RuvNet setup in plain English and turns things on safely with one click. Read-only until you say so; nothing leaves your machine.'
240
- : running.startsWith('2.4.') ? 'routes every task to the cheapest model that can do the job, aware of YOUR subscriptions specifically — it detects what it can prove, asks what it can\'t, and learns from every override.'
241
- : running.startsWith('2.3.') ? 'can no longer break silently: a failed search now rings three independent alarms (phone push, red banner at session start, nightly canary), each tested by deliberately breaking the brain. Failed searches say WHY, instead of pretending nothing matched.'
242
- : /^2\.2\.[012]/.test(running) ? 'ships a safety watchdog that alerts you the instant a background tool starts running up API costs or a scheduled job starts failing silently. (Agentic QE testing still bills your Anthropic key — cost-optimized, and opt-in.)'
243
- : '';
244
- if (whatsNew && !milestoneShown) {
245
- emit(`[RuvNet Brain — new in v${running}]`);
246
- emit(`Near the top of your first response, share ONE upbeat line, in your words, on what this update gives them (say it once): RuvNet Brain v${running} ${whatsNew}`);
247
- }
248
- write(announced, `${running}\n`);
249
- };
250
-
251
- const compareVersions = (a, b) => {
252
- const left = String(a).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
253
- const right = String(b).split(/[.-]/).map((part) => (/^\d+$/.test(part) ? Number(part) : part));
254
- for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
255
- const x = left[i] ?? 0;
256
- const y = right[i] ?? 0;
257
- if (x === y) continue;
258
- if (typeof x === 'number' && typeof y === 'number') return x - y;
259
- return String(x) < String(y) ? -1 : 1;
260
- }
261
- return 0;
262
- };
263
-
264
- const stableSpine = ({ env, hookDir, stateDir, home, pluginVersion, emit, now }) => {
265
- const activeFile = path.join(stateDir, 'active.json');
266
- const stamp = path.join(stateDir, '.last-update-check');
267
- let seedDispatched = false;
268
- if (!exists(activeFile)) {
269
- const seedStamp = path.join(stateDir, '.seed-attempted');
270
- const last = Number(read(seedStamp).trim()) || 0;
271
- const canSeed = exists(path.join(env.CLAUDE_PLUGIN_ROOT || '', 'scripts'))
272
- || exists(path.join(home, '.claude', 'plugins', 'cache', 'ruvnet-brain'));
273
- if (canSeed && now / 1000 - last > 300) {
274
- write(seedStamp, `${Math.floor(now / 1000)}\n`);
275
- seedDispatched = dispatchDetached(hookDir, 120, path.join(stateDir, '.seed.log'),
276
- 'node', [
277
- path.join(hookDir, 'first-session-worker.mjs'),
278
- path.join(hookDir, 'update-apply.mjs'),
279
- path.join(hookDir, 'host-update.mjs'),
280
- path.join(stateDir, '.last-version-check.log'),
281
- ], env);
282
- if (seedDispatched) write(stamp, `${Math.floor(now / 1000)}\n`);
283
- }
284
- } else {
285
- const active = json(activeFile);
286
- const shellBoundary = active?.shellChangedAtVersion;
287
- const shellChangedBeforeHost = shellBoundary && pluginVersion
288
- ? compareVersions(pluginVersion, shellBoundary) < 0
289
- : false;
290
- if ((active?.shellChanged || shellChangedBeforeHost) && pluginVersion && active.version && active.version !== pluginVersion) {
291
- emit(`[RuvNet Brain — v${active.version} changed boot-level declarations (the rare case); this session booted v${pluginVersion}'s]`);
292
- const host = env.RUVNET_HOOK_HOST || 'claude';
293
- const convergence = json(path.join(stateDir, 'host-convergence.json'));
294
- const ready = convergence?.desiredVersion === active.version
295
- && convergence?.hosts?.[host]?.state === 'ready'
296
- && convergence.hosts[host].version === active.version;
297
- if (!ready) {
298
- emit(`Tell the user ONE line: "🧠 RuvNet Brain v${active.version} runtime is live, but this host's exact boot snapshot is not yet verified — do not restart for this update yet; automatic host repair will retry."`);
299
- } else if (host === 'codex') {
300
- emit(`Tell the user ONE line: "🧠 RuvNet Brain v${active.version} is already installed and verified for Codex; restart Codex to load its boot-level declarations, then run /hooks and trust only ruvnet-brain@ruvnet-brain if Codex shows the changed definitions as pending. Runtime behavior already updated live."`);
301
- } else {
302
- emit(`Tell the user ONE line: "🧠 RuvNet Brain v${active.version} is already installed and verified for Claude Code; one restart picks up its boot-level declarations (\`claude --continue\` keeps this conversation). Runtime behavior already updated live."`);
303
- }
304
- }
305
- }
306
- return { seedDispatched, stamp };
307
- };
308
-
309
- const heartbeat = ({ env, hookDir, stateDir, home, running, seedDispatched, stamp, emit, now }) => {
310
- const last = Number(read(stamp).trim()) || 0;
311
- const epoch = Math.floor(now / 1000);
312
- if (seedDispatched || epoch <= 0 || epoch - last <= 900) return;
313
- write(stamp, `${epoch}\n`);
314
- const pref = read(path.join(stateDir, '.auto-update-pref')).trim();
315
- const kbDir = path.join(home, '.cache', 'ruvnet-brain', 'kb');
316
- if (pref === 'yes' && exists(path.join(kbDir, 'forge-update.mjs'))) {
317
- const kbLog = path.join(stateDir, '.last-kb-check.log');
318
- if (/\bBEHIND\b/.test(read(kbLog))) {
319
- emit('[RuvNet Brain — a newer knowledge bundle is available. It is signed (Ed25519) and the updater verifies that signature before extracting anything. We do NOT auto-apply it: applying replaces executable tool files, which is your call. To update: cd ~/.cache/ruvnet-brain/kb && node forge-update.mjs --apply]');
320
- }
321
- dispatchDetached(hookDir, 60, kbLog, process.execPath,
322
- [path.join(kbDir, 'forge-update.mjs'), '--check'], env);
323
- }
324
- const versionLog = path.join(stateDir, '.last-version-check.log');
325
- const remote = firstVersion(read(versionLog));
326
- dispatchDetached(hookDir, 10, versionLog, process.execPath,
327
- [path.join(hookDir, 'host-update.mjs'), '--check'], env);
328
- if (!running || !remote || remote === running) return;
329
- if (pref === 'yes' && exists(path.join(hookDir, 'host-update.mjs'))) {
330
- dispatchDetached(hookDir, 600, path.join(stateDir, '.last-auto-update.log'),
331
- process.execPath, [path.join(hookDir, 'host-update.mjs')], env);
332
- emit(`[RuvNet Brain — v${remote} is downloading and will AUTO-APPLY via the Stable Spine (ADR-023); this session picks up the new behavior live]`);
333
- emit('Tell the user ONE short line, near the top of your first response:');
334
- emit(` "🧠 RuvNet Brain v${remote} is installing in the background for every detected host. Runtime behavior goes live automatically; if boot-level declarations changed, I'll ask for a restart only after that host's exact new snapshot is verified."`);
335
- emit("Don't repeat this notice later in the same session.");
336
- emit('');
337
- return;
338
- }
339
- emit('[RuvNet Brain — update available, auto-update not enabled]');
340
- emit('Tell the user this PLAINLY, near the top of your first response:');
341
- emit(` "🧠 RuvNet Brain found v${remote} (you're on v${running}). Run: npx ruvnet-brain@latest --update"`);
342
- emit(" (Or say the word and I'll turn on auto-update so this never comes up again.)\"");
343
- emit("Don't repeat this notice later in the same session.");
344
- emit('');
345
- };
40
+ // Re-exported for callers/tests that import the entitlement check directly from this file's own
41
+ // long-standing public surface (tests/unit/session-start-core-parity.test.mjs).
42
+ export { maintainerIssueEntitlement };
346
43
 
347
44
  const meter = ({ env, cwd, stateDir, output }) => {
348
45
  if (env.RUVNET_BRAIN_METER === '0') return;
@@ -380,6 +77,7 @@ export async function runSessionStart({
380
77
  const s = String(line);
381
78
  return s.startsWith('🚨')
382
79
  || s.startsWith('[RuvNet Brain — HEALTH ALARM')
80
+ || s.startsWith('[RuvNet Brain — INSTALL ALARM')
383
81
  || s.startsWith('[RuvNet Brain — NIGHTLY FAILED')
384
82
  || s.startsWith('[RuvNet Brain — OPEN ISSUES')
385
83
  || /\bopen issue\(s\)/i.test(s)
@@ -403,8 +101,33 @@ export async function runSessionStart({
403
101
  || s.startsWith('[RuvNet Brain — PROJECT CONTINUITY RESTORED]')
404
102
  || s.startsWith('[RuvNet Brain — MAINTAINER ONLY:');
405
103
  };
104
+ // An alarm's HEADER line always matches isSafeStatus on its own dedicated prefix (above); its
105
+ // explanatory BODY lines do not, and without this they were silently dropped — a real gap this
106
+ // pass closes, because correction #4 requires local-integrity alarms to actually reach the user,
107
+ // not just their header. A header opens a continuation budget sized to EXACTLY that alarm's own
108
+ // body-line count (never a shared generic number): a generic budget was tried first and found,
109
+ // by direct reproduction, to LEAK — unused credits from a short alarm silently let an unrelated
110
+ // LATER unsafe line (a one-time setup prompt) through on whichever run happened to have fewer
111
+ // lines emitted in between, corrupting a completely unrelated byte-count test. Exact-sizing means
112
+ // a block can only ever cover its own known body, never bleed into whatever comes after it.
113
+ const ALARM_BODY_LINES = [
114
+ { prefix: '🚨', lines: 3 }, // HEALTH ALARM's own 3 body lines (the only bare 🚨 alarm today)
115
+ { prefix: '[RuvNet Brain — HEALTH ALARM', lines: 3 },
116
+ { prefix: '[RuvNet Brain — INSTALL ALARM', lines: 1 },
117
+ { prefix: '[RuvNet Brain — NIGHTLY FAILED', lines: 5 },
118
+ ];
119
+ const alarmBodyLines = (s) => {
120
+ if (s.startsWith('[RuvNet Brain v') && s.includes('RETRIEVAL DOWN')) return 1;
121
+ return ALARM_BODY_LINES.find((a) => s.startsWith(a.prefix))?.lines || 0;
122
+ };
123
+ let alarmContinuationBudget = 0;
406
124
  const emit = (line = '') => {
407
- if (env.RUVNET_VERBOSE_HOOKS === '1' || isSafeStatus(line)) lines.push(String(line));
125
+ const s = String(line);
126
+ const safe = env.RUVNET_VERBOSE_HOOKS === '1' || isSafeStatus(s) || alarmContinuationBudget > 0;
127
+ if (safe) lines.push(s);
128
+ const bodyLines = alarmBodyLines(s);
129
+ if (bodyLines > 0) alarmContinuationBudget = bodyLines;
130
+ else if (alarmContinuationBudget > 0) alarmContinuationBudget -= 1;
408
131
  };
409
132
  const home = env.HOME || env.USERPROFILE || os.homedir();
410
133
  const stateDir = env.RUVNET_BRAIN_HOME || path.join(home, '.cache', 'ruvnet-brain');
@@ -427,6 +150,7 @@ export async function runSessionStart({
427
150
  }
428
151
  };
429
152
 
153
+ const restoreStart = Date.now();
430
154
  try {
431
155
  const continuity = await restoreContinuity({ env, cwd });
432
156
  if (continuity?.context) emit(continuity.context);
@@ -434,150 +158,190 @@ export async function runSessionStart({
434
158
  emit('[RuvNet Brain — PROJECT CONTINUITY UNKNOWN]');
435
159
  emit('The SessionStart restore boundary failed unexpectedly. Do not claim project state was restored; verify the canonical store before relying on remembered state.');
436
160
  }
161
+ // Opt-in, matching the pre-existing `trace()` convention below: several other tests assert
162
+ // SessionStart's stderr is EMPTY in the clean case (hook-battery.test.mjs, hook-hardening.test.mjs
163
+ // — a real contract, not incidental), so this must never write unconditionally. `restore` shares
164
+ // this hook's wall-clock budget with everything below even though its code belongs to the
165
+ // continuity lane — see session-start-budget.mjs — and is reported here under EITHER trace flag.
166
+ if (env.RUVNET_SESSION_TRACE === '1' || env.RUVNET_BRAIN_SESSION_START_TRACE === '1') {
167
+ stderr.write(`SESSION_TRACE stage=restore elapsed_ms=${Date.now() - restoreStart}\n`);
168
+ }
169
+
170
+ const tracer = createStageTracer({
171
+ enabled: env.RUVNET_BRAIN_SESSION_START_TRACE === '1',
172
+ write: (chunk) => stderr.write(chunk),
173
+ });
174
+ let bannerEmitted = false;
437
175
 
438
176
  try {
439
177
  trace('body-start');
440
- const settingsFile = env.RUVNET_SETTINGS_FILE
441
- || path.join(home, '.config', 'ruvnet-brain', 'settings.json');
442
- if (/"newProjectDefaults"\s*:\s*true(?:\s*[,}]|$)/.test(read(settingsFile))) {
443
- runNode(path.join(hookDir, 'runtime-preferences.mjs'), ['--seed-project'], {
444
- cwd, env, stdio: 'ignore', timeout: 2000,
445
- });
446
- }
178
+ tracer.stage('misc', () => {
179
+ const settingsFile = env.RUVNET_SETTINGS_FILE
180
+ || path.join(home, '.config', 'ruvnet-brain', 'settings.json');
181
+ if (/"newProjectDefaults"\s*:\s*true(?:\s*[,}]|$)/.test(read(settingsFile))) {
182
+ runNode(path.join(hookDir, 'runtime-preferences.mjs'), ['--seed-project'], {
183
+ cwd, env, stdio: 'ignore', timeout: 2000,
184
+ });
185
+ }
447
186
 
448
- const nightly = path.join(env.CLAUDE_PROJECT_DIR || cwd, '.ruvnet-brain', 'nightly-failure.json');
449
- if (exists(nightly)) {
450
- emit('[RuvNet Brain — NIGHTLY FAILED, self-heal retry also failed, this is now yours to fix]');
451
- emit('Before anything else this session: the ruvnet-brain nightly publish failed twice last run (immediate');
452
- emit(`attempt + a 3-minute retry) and could not self-heal. Read ${nightly} for the exact tag/error,`);
453
- emit('then logs/nightly.log in that repo for full context. Diagnose and fix it now — do not wait to be');
454
- emit('asked, this is a standing directive. Once genuinely fixed and verified (a real release tag advances),');
455
- emit('delete the marker file.');
456
- }
187
+ const nightly = path.join(env.CLAUDE_PROJECT_DIR || cwd, '.ruvnet-brain', 'nightly-failure.json');
188
+ if (exists(nightly)) {
189
+ emit('[RuvNet Brain — NIGHTLY FAILED, self-heal retry also failed, this is now yours to fix]');
190
+ emit('Before anything else this session: the ruvnet-brain nightly publish failed twice last run (immediate');
191
+ emit(`attempt + a 3-minute retry) and could not self-heal. Read ${nightly} for the exact tag/error,`);
192
+ emit('then logs/nightly.log in that repo for full context. Diagnose and fix it now — do not wait to be');
193
+ emit('asked, this is a standing directive. Once genuinely fixed and verified (a real release tag advances),');
194
+ emit('delete the marker file.');
195
+ }
196
+ });
457
197
 
458
- const state = health(home, brain.off);
459
- if (state.problem) {
460
- emit('🚨 [RuvNet Brain — HEALTH ALARM: THE BRAIN IS DOWN — SURFACE THIS FIRST] 🚨');
461
- emit(`Health check failed: ${state.problem}`);
462
- emit('Open your VERY FIRST line with a loud plain alert naming that reason and that fix, and offer to run');
463
- emit('it. Never answer rUv-stack questions from memory while it is down. Verify after: npx ruvnet-brain --doctor');
464
- }
198
+ const state = tracer.stage('misc', () => {
199
+ const result = health(home, brain.off);
200
+ if (result.problem) {
201
+ emit('🚨 [RuvNet Brain — HEALTH ALARM: THE BRAIN IS DOWN — SURFACE THIS FIRST] 🚨');
202
+ emit(`Health check failed: ${result.problem}`);
203
+ emit('Open your VERY FIRST line with a loud plain alert naming that reason and that fix, and offer to run');
204
+ emit('it. Never answer rUv-stack questions from memory while it is down. Verify after: npx ruvnet-brain --doctor');
205
+ }
206
+ return result;
207
+ }) || { problem: '', absentByChoice: false };
465
208
 
466
- const consoleOffered = path.join(stateDir, '.console-offered');
467
- const firstSession = !brain.off && !exists(consoleOffered);
468
- if (firstSession) emit('[RuvNet Brain — first session initialized]');
469
- if (!brain.off && !exists(consoleOffered)) {
470
- write(consoleOffered, '');
471
- emit('[RuvNet Brain — FIRST LOAD: offer the Console once]');
472
- emit(`Offer ONCE: "Want to see your whole RuvNet stack on one page?" — installed parts, learned project knowledge and reversible fixes, read-only until clicked; later it's ${consoleInvoke}. On yes invoke ${consoleInvoke}; on no, don't re-offer.`);
473
- }
209
+ tracer.stage('misc', () => {
210
+ const consoleOffered = path.join(stateDir, '.console-offered');
211
+ const firstSession = !brain.off && !exists(consoleOffered);
212
+ if (firstSession) emit('[RuvNet Brain — first session initialized]');
213
+ if (!brain.off && !exists(consoleOffered)) {
214
+ write(consoleOffered, '');
215
+ emit('[RuvNet Brain — FIRST LOAD: offer the Console once]');
216
+ emit(`Offer ONCE: "Want to see your whole RuvNet stack on one page?" — installed parts, learned project knowledge and reversible fixes, read-only until clicked; later it's ${consoleInvoke}. On yes invoke ${consoleInvoke}; on no, don't re-offer.`);
217
+ }
218
+ });
474
219
 
475
- surfaceIssues(stateDir, emit, now, env, home, platform);
476
- surfaceSignals({ env, cwd, stateDir, hookDir, emit, now });
220
+ tracer.stage('issue-pointer', () => {
221
+ surfaceIssuePointer({ stateDir, emit, now, env, home, platform, cwd });
222
+ });
223
+ tracer.stage('signal-surface', () => {
224
+ surfaceSignals({ env, cwd, stateDir, hookDir, emit, now });
225
+ });
477
226
 
478
- const routerProfile = path.join(home, '.claude', 'model-router', 'profile.json');
479
- const routerNudge = path.join(stateDir, '.router-profile-nudged');
480
- if (!brain.off && !exists(routerProfile) && !exists(routerNudge)) {
481
- write(routerNudge, '');
482
- emit('[RuvNet Brain — MetaHarness routing is available but not set up for THIS user yet]');
483
- emit('Offer ONCE: route each task to the cheapest capable model, using THEIR subscriptions ($0) before billed models. Set it up?');
484
- emit('On yes ask only (1) Claude sub, Pro or Max? (2) Codex CLI on a ChatGPT plan? then:');
485
- const routerSetup = path.join(home, '.claude', 'model-router', 'bin', 'model-router-setup.mjs');
486
- emit(exists(routerSetup)
487
- ? ` in ${path.dirname(routerSetup)}: node model-router-setup.mjs --detect-only; set profile.json's subscription fields from their answers (basis 'user-attested <date>'); node model-router-status.mjs and RELAY its 'Recommended path' block — SEEING their zero-cost options IS the deliverable of yes.`
488
- : " the router isn't installed here — run: npx github:stuinfla/ruvnet-brain (it sets it up, asks these questions and shows the path)");
489
- emit('On no or silence: drop it, never re-offer.');
490
- }
227
+ tracer.stage('router-nudge', () => {
228
+ const routerProfile = path.join(home, '.claude', 'model-router', 'profile.json');
229
+ const routerNudge = path.join(stateDir, '.router-profile-nudged');
230
+ if (!brain.off && !exists(routerProfile) && !exists(routerNudge)) {
231
+ write(routerNudge, '');
232
+ emit('[RuvNet Brain — MetaHarness routing is available but not set up for THIS user yet]');
233
+ emit('Offer ONCE: route each task to the cheapest capable model, using THEIR subscriptions ($0) before billed models. Set it up?');
234
+ emit('On yes ask only (1) Claude sub, Pro or Max? (2) Codex CLI on a ChatGPT plan? then:');
235
+ const routerSetup = path.join(home, '.claude', 'model-router', 'bin', 'model-router-setup.mjs');
236
+ emit(exists(routerSetup)
237
+ ? ` in ${path.dirname(routerSetup)}: node model-router-setup.mjs --detect-only; set profile.json's subscription fields from their answers (basis 'user-attested <date>'); node model-router-status.mjs and RELAY its 'Recommended path' block — SEEING their zero-cost options IS the deliverable of yes.`
238
+ : " the router isn't installed here — run: npx github:stuinfla/ruvnet-brain (it sets it up, asks these questions and shows the path)");
239
+ emit('On no or silence: drop it, never re-offer.');
240
+ }
241
+ });
491
242
 
492
- // Version announcements belong to the explicit update/what's-new flows, not SessionStart.
493
- // Keeping this hook context-only prevents host sessions from receiving response scripts.
494
- const autoPref = path.join(stateDir, '.auto-update-pref');
495
- if (!exists(autoPref)) {
496
- emit('[RuvNet Brain — one-time setup question]');
497
- emit('Ask ONCE: should the brain auto-update in the background? (recommended; reversible). Then run ONE via Bash:');
498
- emit(` mkdir -p '${stateDir}' && echo yes > '${autoPref}' (or echo no)`);
499
- emit('No answer: ask next session, never twice.');
243
+ tracer.stage('misc', () => {
244
+ const autoPref = path.join(stateDir, '.auto-update-pref');
245
+ if (!exists(autoPref)) {
246
+ emit('[RuvNet Brain — one-time setup question]');
247
+ emit('Ask ONCE: should the brain auto-update in the background? (recommended; reversible). Then run ONE via Bash:');
248
+ emit(` mkdir -p '${stateDir}' && echo yes > '${autoPref}' (or echo no)`);
249
+ emit('No answer: ask next session, never twice.');
250
+ }
251
+ });
252
+ const spine = tracer.stage('stable-spine', () => stableSpine({
253
+ env, hookDir, stateDir, home, pluginVersion: running, emit, now,
254
+ })) || { seedDispatched: false, stamp: path.join(stateDir, '.last-update-check') };
255
+ if (runHeartbeat) {
256
+ tracer.stage('heartbeat', () => heartbeat({
257
+ env, hookDir, stateDir, home, running, ...spine, emit, now,
258
+ }));
500
259
  }
501
- const spine = stableSpine({ env, hookDir, stateDir, home, pluginVersion: running, emit, now });
502
- if (runHeartbeat) heartbeat({ env, hookDir, stateDir, home, running, ...spine, emit, now });
503
260
 
504
- const star = path.join(stateDir, '.star-ask-shown');
505
- if (!brain.off && exists(path.join(stateDir, '.grounded-once')) && !exists(star) && write(star, '')) {
506
- emit('[RuvNet Brain — one-time note. This will NEVER be shown again on this machine.]');
507
- emit('At a natural CLOSING moment (after the work, never as an interruption), relay this line once and move on — never repeated, never embellished, in this or any future session:');
508
- emit(' "Finding this useful? Star github.com/stuinfla/ruvnet-brain or leave feedback — it keeps the nightly updates coming."');
509
- }
261
+ tracer.stage('misc', () => {
262
+ const star = path.join(stateDir, '.star-ask-shown');
263
+ if (!brain.off && exists(path.join(stateDir, '.grounded-once')) && !exists(star) && write(star, '')) {
264
+ emit('[RuvNet Brain — one-time note. This will NEVER be shown again on this machine.]');
265
+ emit('At a natural CLOSING moment (after the work, never as an interruption), relay this line once and move on — never repeated, never embellished, in this or any future session:');
266
+ emit(' "Finding this useful? Star github.com/stuinfla/ruvnet-brain or leave feedback — it keeps the nightly updates coming."');
267
+ }
268
+ });
510
269
 
511
- const bannerVersion = effectiveVersion || 'unknown';
512
- if (pluginRoot.startsWith(path.join(home, '.claude', 'plugins') + path.sep)) {
513
- if (running) write(path.join(stateDir, '.running-version'), `${running}\n`);
514
- } else if (running) {
515
- write(path.join(stateDir, '.dev-version'), `${running}\n`);
516
- }
517
- const source = json(path.join(home, '.cache', 'ruvnet-brain', 'kb', 'SOURCE.json'), {});
518
- const kbVersion = typeof source?.releaseTag === 'string' ? source.releaseTag : '';
519
- const readiness = mcpReadiness(env, home);
270
+ tracer.stage('ascii-drift', () => {
271
+ const asciiDrift = path.join(env.CLAUDE_PROJECT_DIR || cwd, 'scripts', 'ascii-drift.mjs');
272
+ if (!brain.off && exists(asciiDrift)) {
273
+ const result = runNode(asciiDrift, ['--quiet'], { cwd, env, stdio: 'pipe', timeout: 1000 });
274
+ const text = String(result?.stdout || '').replace(/\r\n/g, '\n').replace(/\n$/, '');
275
+ if (text) for (const line of text.split('\n')) emit(line);
276
+ }
277
+ });
520
278
 
521
- const grounding = json(path.join(stateDir, 'install-state.json'));
522
- const asciiDrift = path.join(env.CLAUDE_PROJECT_DIR || cwd, 'scripts', 'ascii-drift.mjs');
523
- if (!brain.off && exists(asciiDrift)) {
524
- const result = runNode(asciiDrift, ['--quiet'], {
525
- cwd, env, stdio: 'pipe', timeout: 1000,
526
- });
527
- const text = String(result?.stdout || '').replace(/\r\n/g, '\n').replace(/\n$/, '');
528
- if (text) for (const line of text.split('\n')) emit(line);
529
- }
530
- if (grounding?.grounding && grounding.grounding !== 'proven') {
531
- const when = grounding.at ? new Date(grounding.at).toISOString().slice(0, 16).replace('T', ' ') : 'an earlier run';
532
- emit('[RuvNet Brain — grounding not yet PROVEN on this machine (mention once, calmly, near the top)]');
533
- emit(`The last check (${when} (${grounding.reason || 'no reason recorded'})) could not verify a real, resolvable citation — often just a first-run model download or an offline machine, not necessarily a broken install. Say so once, plainly: the next real search_ruvnet confirms or clears it automatically, and \`npx ruvnet-brain --doctor\` shows the current verdict any time.`);
534
- }
279
+ tracer.stage('banner', () => {
280
+ const bannerVersion = effectiveVersion || 'unknown';
281
+ if (pluginRoot.startsWith(path.join(home, '.claude', 'plugins') + path.sep)) {
282
+ if (running) write(path.join(stateDir, '.running-version'), `${running}\n`);
283
+ } else if (running) {
284
+ write(path.join(stateDir, '.dev-version'), `${running}\n`);
285
+ }
286
+ const source = json(path.join(home, '.cache', 'ruvnet-brain', 'kb', 'SOURCE.json'), {});
287
+ const kbVersion = typeof source?.releaseTag === 'string' ? source.releaseTag : '';
288
+ const readiness = mcpReadiness(env, home);
289
+
290
+ const grounding = json(path.join(stateDir, 'install-state.json'));
291
+ if (grounding?.grounding && grounding.grounding !== 'proven') {
292
+ const when = grounding.at ? new Date(grounding.at).toISOString().slice(0, 16).replace('T', ' ') : 'an earlier run';
293
+ emit('[RuvNet Brain — grounding not yet PROVEN on this machine (mention once, calmly, near the top)]');
294
+ emit(`The last check (${when} (${grounding.reason || 'no reason recorded'})) could not verify a real, resolvable citation — often just a first-run model download or an offline machine, not necessarily a broken install. Say so once, plainly: the next real search_ruvnet confirms or clears it automatically, and \`npx ruvnet-brain --doctor\` shows the current verdict any time.`);
295
+ }
296
+
297
+ if (brain.off) {
298
+ const absent = state.absentByChoice ? '; no knowledge bundle on this machine — disabled by choice, not broken' : '';
299
+ emit(`[RuvNet Brain — brain OFF by your setting${brain.since ? ` (since ${brain.since})` : ''}${absent}. Do not mention it unless the user asks.]`);
300
+ return;
301
+ }
535
302
 
536
- if (brain.off) {
537
- const absent = state.absentByChoice ? '; no knowledge bundle on this machine — disabled by choice, not broken' : '';
538
- emit(`[RuvNet Brain — brain OFF by your setting${brain.since ? ` (since ${brain.since})` : ''}${absent}. Do not mention it unless the user asks.]`);
539
- } else {
540
303
  if (state.problem) {
541
304
  emit(`[RuvNet Brain v${bannerVersion} — active this session, RETRIEVAL DOWN]`);
542
305
  emit(`The plugin and its hooks are running, but the brain itself is broken (see the alarm above). Do not claim grounding works. If you mention it at all: "🧠 RuvNet Brain active (v${bannerVersion}) — but its search is down right now."`);
306
+ bannerEmitted = true;
307
+ return;
308
+ }
309
+
310
+ // ONE VERSION, CUSTOMER-FACING (issue #77, restated from the customer's side). The plugin
311
+ // version and the knowledge bundle's tag are two internal artefacts of ONE product; only the
312
+ // plugin version is shown. A version SPLIT is a LOCAL INSTALLATION INTEGRITY problem that
313
+ // affects every user's search quality, not maintainer trivia — per the 2026-09-11 reviewer
314
+ // correction it is delivered to everyone, worded for the user, through the same always-shown
315
+ // alarm mechanism as HEALTH ALARM above (never gated by maintainerIssueEntitlement).
316
+ emit(`[RuvNet Brain v${bannerVersion} — active this session${updated ? ` · updated ${updated}` : ''}]`);
317
+ bannerEmitted = true;
318
+ const bundleTag = String(kbVersion).replace(/^v/, '');
319
+ if (bundleTag && bannerVersion !== 'unknown' && bundleTag !== bannerVersion) {
320
+ emit('🚨 [RuvNet Brain — INSTALL ALARM: plugin and knowledge bundle are out of sync] 🚨');
321
+ emit(`Your plugin is v${bannerVersion} but the knowledge bundle on this machine is v${bundleTag} — they are meant to ship together, so search results may not match this plugin's behavior yet. Fix: npx ruvnet-brain@latest --update (or reinstall: npx github:stuinfla/ruvnet-brain --force).`);
322
+ }
323
+
324
+ const hookContracts = readHookContracts(path.join(pluginRoot, 'hooks', 'hook-contracts.json'));
325
+ const lifecycleLine = describeLifecycleHooks(hookContracts);
326
+ if (readiness.state === 'ready') {
327
+ emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is ready and live. ${lifecycleLine}`);
328
+ } else if (readiness.state === 'degraded') {
329
+ const receipt = readiness.receipt || {};
330
+ emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered but degraded (${receipt.phase || 'startup'}: ${receipt.error || 'readiness failed'}). ${lifecycleLine}`);
543
331
  } else {
544
- // ONE VERSION, CUSTOMER-FACING (issue #77, restated from the customer's side).
545
- //
546
- // This printed the plugin version and the knowledge bundle's tag side by side —
547
- // "RuvNet Brain active (v4.0.8, brain v4.0.7)". Those are two internal artefacts of ONE
548
- // product. Showing both makes every user adjudicate whether their install is out of sync,
549
- // a question they cannot answer and should never have been asked. Keeping the two in
550
- // lockstep is this project's job; printing the seam is an admission leaking into the UI.
551
- //
552
- // The divergence is NOT hidden — when the tags differ it goes to the maintainer on the
553
- // same private entitlement that gates open-issue alerts, because a bundle behind its
554
- // plugin is a RELEASE defect to fix, not a banner to annotate.
555
- emit(`[RuvNet Brain v${bannerVersion} — active this session${updated ? ` · updated ${updated}` : ''}]`);
556
- const bundleTag = String(kbVersion).replace(/^v/, '');
557
- if (bundleTag && bannerVersion !== 'unknown' && bundleTag !== bannerVersion
558
- && maintainerIssueEntitlement(env, home, 'stuinfla/ruvnet-brain')) {
559
- emit('[RuvNet Brain — MAINTAINER ONLY: the shipped generation is split. Do NOT surface this to the user.]');
560
- emit(`Plugin is ${bannerVersion}; the knowledge bundle on this machine is ${kbVersion}. Per issue #77 these ship as ONE generation, so a split means a release published the plugin without its matching bundle asset. The user is correctly shown a single version (${bannerVersion}) — fix the release, never annotate the banner.`);
561
- }
562
- if (readiness.state === 'ready') {
563
- emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is ready and live; the grounding hooks are active.');
564
- } else if (readiness.state === 'degraded') {
565
- const receipt = readiness.receipt || {};
566
- emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered but degraded (${receipt.phase || 'startup'}: ${receipt.error || 'readiness failed'}); the grounding hooks remain active.`);
567
- } else {
568
- emit('USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered; live readiness is not yet proven. The grounding hooks are active.');
569
- }
332
+ emit(`USER-LEVEL: one brain ON DISK (~/.cache/ruvnet-brain/kb) shared by every project and window here — nothing to reinstall per project (each window still runs its own worker process, which now exits when idle). search_ruvnet is registered; live readiness is not yet proven. ${lifecycleLine}`);
570
333
  }
571
- }
334
+ });
572
335
  } catch (error) {
573
336
  if (env.RUVNET_SESSION_TRACE === '1') stderr.write(`SESSION_TRACE native-fail-open ${error?.message || error}\n`);
574
337
  }
575
338
 
576
- if (env.RUVNET_VERBOSE_HOOKS !== '1' && !brain.off) {
339
+ // ONE banner line, always — the fallback that keeps SessionStart fail-open even if the rich body
340
+ // above threw before reaching its own banner stage. No longer duplicated (was printed twice, plus
341
+ // a bare "[RuvNet Brain active]" a third time — three lines from one fact) once the rich path
342
+ // already emitted its own banner.
343
+ if (env.RUVNET_VERBOSE_HOOKS !== '1' && !brain.off && !bannerEmitted) {
577
344
  lines.push(`[RuvNet Brain v${running || 'unknown'} — active this session]`);
578
- // Keep the legacy plain status token for host integrations that key off it; it carries no
579
- // instruction and does not introduce a second version or bundle identity.
580
- lines.push('[RuvNet Brain active]');
581
345
  }
582
346
  const output = lines.length ? `${lines.join('\n')}\n` : '';
583
347
  meter({ env, cwd, stateDir, output });
@@ -586,8 +350,21 @@ export async function runSessionStart({
586
350
  return { ok: true, outputBytes: Buffer.byteLength(output, 'utf8'), platform };
587
351
  }
588
352
 
589
- const direct = process.argv[1]
590
- && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
353
+ // Entry-point guard. Compares REALPATHS on both sides: path.resolve() normalizes a path but does
354
+ // NOT follow symlinks, while import.meta.url IS symlink-resolved by Node. Through a symlink (npm bin
355
+ // shims, wrapper scripts, and every os.tmpdir() path on macOS) the two sides disagree, so main()
356
+ // never runs -- and because nothing throws, the process exits 0. A silent exit 0 is indistinguishable
357
+ // from "ran, found nothing". Reproduced live 2026-07-27; pinned by tests/unit/entrypoint-symlink.test.mjs.
358
+ function isDirectInvocation() {
359
+ try {
360
+ if (!process.argv[1]) return false;
361
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+
367
+ const direct = isDirectInvocation();
591
368
  if (direct) {
592
369
  runSessionStart().catch((error) => {
593
370
  if (process.env.RUVNET_SESSION_TRACE === '1') {