ruvnet-brain 4.0.6 → 4.0.7

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 (35) hide show
  1. package/README.md +3 -3
  2. package/bin/install.mjs +112 -7
  3. package/config/model-router/catalog.template.json +17 -2
  4. package/console/app.js +32 -14
  5. package/console/architecture.html +1 -1
  6. package/data/model-catalog.json +54 -0
  7. package/package.json +2 -1
  8. package/plugin/.claude-plugin/plugin.json +1 -1
  9. package/plugin/.codex-plugin/plugin.json +1 -1
  10. package/plugin/hooks/hooks.json +34 -1
  11. package/plugin/scripts/ground-ruvnet.sh +2 -1
  12. package/plugin/scripts/hook-shim.mjs +13 -6
  13. package/plugin/scripts/lesson-provenance.mjs +21 -0
  14. package/plugin/scripts/lesson-store.mjs +33 -3
  15. package/plugin/scripts/route-dispatch.sh +23 -49
  16. package/plugin/scripts/session-snapshot-contract.mjs +100 -0
  17. package/plugin/scripts/session-snapshot-hook.mjs +34 -0
  18. package/plugin/scripts/session-start-core.mjs +25 -7
  19. package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
  20. package/plugin/skills/release-proof/SKILL.md +4 -4
  21. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
  22. package/scripts/capability-registry.mjs +3 -3
  23. package/scripts/lesson-lifecycle.mjs +2 -1
  24. package/scripts/lesson-ratify.mjs +6 -3
  25. package/scripts/lesson-seed.mjs +21 -16
  26. package/scripts/model-router-catalog.mjs +16 -0
  27. package/scripts/model-router-engine.mjs +4 -1
  28. package/scripts/onboarding-console.mjs +52 -14
  29. package/scripts/provider-availability.mjs +12 -0
  30. package/scripts/release-authority.mjs +14 -2
  31. package/scripts/release-transaction-provider.mjs +236 -0
  32. package/scripts/release-transaction.mjs +214 -0
  33. package/scripts/release.mjs +67 -279
  34. package/scripts/session-snapshot-contract.mjs +1 -0
  35. package/scripts/staged-host-verifier.mjs +106 -0
@@ -43,7 +43,7 @@
43
43
  import fs from 'node:fs';
44
44
  import path from 'node:path';
45
45
  import crypto from 'node:crypto';
46
- import { ORIGIN, STATUS, ENFORCEMENT, loadLessons } from './lesson-store.mjs';
46
+ import { ORIGIN, SOURCE_CLASS, STATUS, ENFORCEMENT, loadLessons } from './lesson-store.mjs';
47
47
 
48
48
  // ── The bars, in one place, as numbers a human can argue with ────────────────────────────────────
49
49
  export const RETIREMENT = Object.freeze({
@@ -385,6 +385,7 @@ export function explainGeneralization(lesson, otherProjects = [], { minProjects
385
385
  // `{...lesson, ...proposal}`, so the result cannot block even if the source lesson was ratified
386
386
  // and blocking — the trust boundary is enforced by the data, not by the caller remembering.
387
387
  origin: ORIGIN.MODEL_INFERRED,
388
+ sourceClass: SOURCE_CLASS.MODEL_INFERRED,
388
389
  status: STATUS.CANDIDATE,
389
390
  enforcement: ENFORCEMENT.CHECKLIST,
390
391
  intendedEnforcement: null,
@@ -26,7 +26,7 @@
26
26
  * correctly, stops using it.
27
27
  */
28
28
  import os from 'node:os';
29
- import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, TRIGGERS } from './lesson-store.mjs';
29
+ import { loadLessons, saveLessons, ratify, demote, weightOf, pending, ENFORCEMENT, STATUS, ORIGIN, SOURCE_CLASS, TRIGGERS } from './lesson-store.mjs';
30
30
 
31
31
  const argv = process.argv.slice(2);
32
32
  const arg = (f) => { const i = argv.indexOf(f); return i >= 0 && argv[i + 1] ? argv[i + 1] : null; };
@@ -34,7 +34,7 @@ const has = (f) => argv.includes(f);
34
34
 
35
35
  const lessons = loadLessons();
36
36
  if (!lessons.length) {
37
- console.log('\n No lessons stored yet. Seed them with: node scripts/lesson-seed.mjs --apply\n');
37
+ console.log('\n No personal lessons stored yet. They appear after an explicit user correction is captured.\n');
38
38
  process.exit(0);
39
39
  }
40
40
 
@@ -86,7 +86,10 @@ if (has('--ratify')) {
86
86
  // Bulk convenience, deliberately scoped: it can only touch lessons the USER stated. Model-inferred
87
87
  // lessons are never swept up by a bulk action — that would be exactly the hole the boundary closes.
88
88
  let next = lessons;
89
- const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED && l.status === STATUS.CANDIDATE && !l.demoted);
89
+ const targets = lessons.filter((l) => l.origin === ORIGIN.USER_STATED
90
+ && l.sourceClass === SOURCE_CLASS.CURRENT_USER
91
+ && l.status === STATUS.CANDIDATE
92
+ && !l.demoted);
90
93
  for (const l of targets) next = ratify(l.id, next);
91
94
  saveLessons(next);
92
95
  const nowBlocking = next.filter((l) => l.enforcement === ENFORCEMENT.BLOCK).length;
@@ -17,17 +17,22 @@
17
17
  import os from 'node:os';
18
18
  import {
19
19
  makeLesson, saveLessons, loadLessons, lessonsFor, unenforceable, pending, weightOf,
20
- TRIGGERS as T, ENFORCEMENT as E, ORIGIN as O,
20
+ TRIGGERS as T, ENFORCEMENT as E, ORIGIN as O, SOURCE_CLASS,
21
21
  } from './lesson-store.mjs';
22
22
 
23
23
  // Shipped at CHECKLIST; `intendedEnforcement` records what it becomes once a human ratifies it.
24
24
  const blocking = { enforcement: E.CHECKLIST, intendedEnforcement: E.BLOCK };
25
+ const ownerImport = {
26
+ origin: O.IMPORTED,
27
+ sourceClass: SOURCE_CLASS.IMPORTED_OWNER,
28
+ demoted: true,
29
+ };
25
30
 
26
31
  export const SEED = [
27
32
  makeLesson({
28
33
  id: 'L01-verify-with-a-capable-channel',
29
34
  ...blocking,
30
- origin: O.USER_STATED,
35
+ ...ownerImport,
31
36
  severity: 'high',
32
37
  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
38
  trigger: T.CLAIM_DONE.key,
@@ -46,7 +51,7 @@ export const SEED = [
46
51
  makeLesson({
47
52
  id: 'L02-check-before-you-assert',
48
53
  ...blocking,
49
- origin: O.USER_STATED,
54
+ ...ownerImport,
50
55
  severity: 'high',
51
56
  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
57
  trigger: T.ASSERT_FACT.key,
@@ -64,7 +69,7 @@ export const SEED = [
64
69
  makeLesson({
65
70
  id: 'L03-research-before-recommending',
66
71
  enforcement: E.CHECKLIST,
67
- origin: O.USER_STATED,
72
+ ...ownerImport,
68
73
  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
74
  trigger: T.RECOMMEND_ARCH.key,
70
75
  evidence: [
@@ -78,7 +83,7 @@ export const SEED = [
78
83
  makeLesson({
79
84
  id: 'L04-never-relay-a-number',
80
85
  enforcement: E.CHECKLIST,
81
- origin: O.USER_STATED,
86
+ ...ownerImport,
82
87
  severity: 'high',
83
88
  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
89
  trigger: T.RELAY_NUMBER.key,
@@ -93,7 +98,7 @@ export const SEED = [
93
98
  makeLesson({
94
99
  id: 'L05-version-is-the-update-signal',
95
100
  ...blocking,
96
- origin: O.USER_STATED,
101
+ ...ownerImport,
97
102
  severity: 'high',
98
103
  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
104
  trigger: T.SHIP.key,
@@ -109,7 +114,7 @@ export const SEED = [
109
114
  makeLesson({
110
115
  id: 'L06-use-the-real-tool',
111
116
  ...blocking,
112
- origin: O.USER_STATED,
117
+ ...ownerImport,
113
118
  severity: 'high',
114
119
  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
120
  trigger: T.WRITE_CODE.key,
@@ -127,7 +132,7 @@ export const SEED = [
127
132
  makeLesson({
128
133
  id: 'L07-blast-radius-not-social-comfort',
129
134
  ...blocking,
130
- origin: O.USER_STATED,
135
+ ...ownerImport,
131
136
  severity: 'high',
132
137
  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
138
  trigger: T.MUTATE_MACHINE.key,
@@ -143,7 +148,7 @@ export const SEED = [
143
148
  makeLesson({
144
149
  id: 'L08-status-is-a-table',
145
150
  enforcement: E.CHECKLIST,
146
- origin: O.USER_STATED,
151
+ ...ownerImport,
147
152
  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
153
  trigger: T.REPORT_STATUS.key,
149
154
  evidence: [
@@ -159,7 +164,7 @@ export const SEED = [
159
164
  makeLesson({
160
165
  id: 'L09-gradeable-is-not-valuable',
161
166
  enforcement: E.CHECKLIST,
162
- origin: O.MODEL_INFERRED,
167
+ ...ownerImport,
163
168
  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
169
  trigger: T.CHOOSE_WORK.key,
165
170
  evidence: [
@@ -172,7 +177,7 @@ export const SEED = [
172
177
  makeLesson({
173
178
  id: 'L10-under-enumeration-is-a-tell',
174
179
  enforcement: E.CHECKLIST,
175
- origin: O.MODEL_INFERRED,
180
+ ...ownerImport,
176
181
  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
182
  trigger: T.REPORT_STATUS.key,
178
183
  evidence: [
@@ -185,7 +190,7 @@ export const SEED = [
185
190
  makeLesson({
186
191
  id: 'L11-retrieval-without-volition-is-broken',
187
192
  enforcement: E.REVIEW,
188
- origin: O.MODEL_INFERRED,
193
+ ...ownerImport,
189
194
  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
195
  trigger: T.CHOOSE_WORK.key,
191
196
  evidence: [
@@ -199,7 +204,7 @@ export const SEED = [
199
204
  makeLesson({
200
205
  id: 'L12-efficiency-seeking-is-the-tell',
201
206
  enforcement: E.REVIEW,
202
- origin: O.USER_STATED,
207
+ ...ownerImport,
203
208
  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
209
  trigger: T.CHOOSE_WORK.key,
205
210
  evidence: [
@@ -228,14 +233,14 @@ if (invokedDirectly) {
228
233
  for (const l of ls) {
229
234
  const shown = l.intendedEnforcement ? `${l.enforcement}→${l.intendedEnforcement}` : l.enforcement;
230
235
  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)}`);
236
+ console.log(` ${''.padEnd(18)} IMPORTED MAINTAINER HISTORY (quarantined — never personal policy) · observed ${l.repeatCount}× · weight ${weightOf(l)}`);
232
237
  }
233
238
  console.log('');
234
239
  }
235
240
  const un = unenforceable(SEED);
236
241
  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.`);
242
+ console.log(` ${pend.length} awaiting ratification. Bundled maintainer history is quarantined and cannot`);
243
+ console.log(` become the installing user's personal policy.`);
239
244
  if (un.length) console.log(` ${un.length} are declared unenforceable (no hook can observe them); checked at review instead.`);
240
245
  console.log('');
241
246
 
@@ -0,0 +1,16 @@
1
+ // Managed facts are additive for existing users. Their existing row is the user overlay: it wins
2
+ // byte-for-byte, including disablement, tier/priority changes, and local candidates. Newly shipped
3
+ // metered rows are not auto-added; that would silently expand spend authority.
4
+ export function mergeManagedCatalog(existing, managed) {
5
+ const current = Array.isArray(existing?.candidates) ? existing.candidates : [];
6
+ const seen = new Set(current.map((candidate) => candidate?.id).filter(Boolean));
7
+ const additions = (managed?.candidates || []).filter((candidate) => candidate?.id
8
+ && !seen.has(candidate.id)
9
+ && ((candidate.subscription || []).length > 0 || candidate.provider === 'local'));
10
+ const next = {
11
+ ...existing,
12
+ managedVersion: managed?.managedVersion || managed?.updated || null,
13
+ candidates: [...current, ...additions],
14
+ };
15
+ return JSON.stringify(next) === JSON.stringify(existing) ? existing : next;
16
+ }
@@ -113,10 +113,13 @@ export function loadCatalog() {
113
113
  } catch {
114
114
  /* fall through to a minimal built-in so the engine still answers */
115
115
  }
116
- // Built-in fallback (verified OpenRouter prices from route-cheap; Anthropic frontier from same).
116
+ // Built-in fallback. Claude launchability was verified against Claude Code 2.1.220 on 2026-08-02;
117
+ // prices remain null where the subscription host, rather than a metered API, is authoritative.
117
118
  return [
118
119
  { id: 'deepseek/deepseek-chat', provider: 'openrouter', harness: ['claude-code', 'codex'], tier: 'cheap', costPerMTok: { in: 0.2, out: 0.8 }, verified: '2026-07-07' },
119
120
  { id: 'claude-opus-4-8', provider: 'anthropic', harness: ['claude-code'], tier: 'frontier', costPerMTok: { in: 5.0, out: 25.0 }, verified: '2026-07-07' },
121
+ { id: 'claude-opus-5', provider: 'anthropic', harness: ['claude-code'], subscription: ['claude-code'], tier: 'frontier', costPerMTok: null, verified: '2026-08-02 Claude Code 2.1.220 launch' },
122
+ { id: 'claude-fable-5', provider: 'anthropic', harness: ['claude-code'], subscription: ['claude-code'], tier: 'frontier', costPerMTok: null, verified: '2026-08-02 Claude Code 2.1.220 launch' },
120
123
  { id: 'gpt-5.5', provider: 'openai', harness: ['codex'], tier: 'frontier', costPerMTok: null, verified: null },
121
124
  ];
122
125
  }
@@ -41,6 +41,8 @@ import { loadCatalog as engineCatalog, catalogSource as engineCatalogSource, loa
41
41
  import { effectivePrices, loadLabelledRows, MIN_LABELS, OUTCOMES } from './metaharness-router.mjs';
42
42
  import { utilization } from './router-utilization.mjs';
43
43
  import { loadCatalog, detectProvider, frontierFor } from './model-catalog.mjs';
44
+ import { providerAvailability } from './provider-availability.mjs';
45
+ import { inspectSessionSnapshots } from './session-snapshot-contract.mjs';
44
46
  import { learnings } from './learnings.mjs';
45
47
  import { gatesSurvey } from './gates.mjs';
46
48
  // The write-safety primitives, borrowed rather than re-implemented. See saveConfig for why.
@@ -59,7 +61,7 @@ import {
59
61
  } from '../kb/brain-profile.mjs';
60
62
  // Lessons: read model + the two user verbs. Every mutation goes through lesson-store's own
61
63
  // updateLessons/ratify/demote/restore — this file adds a SURFACE, never a second writer.
62
- import { loadLessons, updateLessons, ratify, demote, restore, pending, weightOf, TRIGGERS, ENFORCEMENT, ORIGIN, STATUS } from './lesson-store.mjs';
64
+ import { loadLessons, updateLessons, ratify, demote, restore, pending, weightOf, TRIGGERS, ENFORCEMENT, ORIGIN, SOURCE_CLASS, STATUS } from './lesson-store.mjs';
63
65
  import {
64
66
  openRouterCredentialStatus,
65
67
  saveOpenRouterCredential,
@@ -421,8 +423,19 @@ function probeMemory(projectDir) {
421
423
  const db = path.join(projectDir, '.swarm/memory.db');
422
424
  const probes = {};
423
425
  // compaction survival + session surfacing are filesystem facts, always checkable
424
- const snap = fs.existsSync(path.join(projectDir, 'agentdb-sessions.jsonl')) || fs.existsSync(path.join(projectDir, '.swarm/agentdb-sessions.jsonl'));
425
- probes.compactionSurvival = snap ? { status: 'ok', detail: 'a PreCompact snapshot file is present' } : { status: 'warn', detail: 'no PreCompact snapshot found for this project yet' };
426
+ const snapshot = inspectSessionSnapshots(projectDir);
427
+ const snapshotDetail = snapshot.kind === 'canonical'
428
+ ? 'a fresh versioned PreCompact snapshot is present in .swarm/agentdb-sessions.jsonl'
429
+ : snapshot.kind === 'legacy'
430
+ ? 'a fresh supported Ruflo session snapshot is present (legacy path; migration recommended)'
431
+ : snapshot.kind === 'malformed'
432
+ ? 'snapshot files were found but none matched the supported schema'
433
+ : snapshot.kind !== 'absent'
434
+ ? 'the newest supported snapshot is stale'
435
+ : 'no supported PreCompact snapshot found for this project yet';
436
+ probes.compactionSurvival = snapshot.fresh
437
+ ? { status: 'ok', detail: snapshotDetail, artifact: snapshot.kind }
438
+ : { status: 'warn', detail: snapshotDetail, artifact: snapshot.kind };
426
439
  probes.sessionSurfacing = sessionHookExists() ? { status: 'ok', detail: 'the global SessionStart hook surfaces project state at launch' } : { status: 'warn', detail: 'no SessionStart recall hook found' };
427
440
  // recall quality: honestly NOT probed at render (a true probe needs an embedding query; left for an explicit deep test)
428
441
  probes.recallQuality = { status: 'notTested', detail: 'not checked this session — a real recall probe needs an embedding round-trip, which render deliberately avoids' };
@@ -989,7 +1002,15 @@ function gatherLessons() {
989
1002
  const lessons = all.map((l) => {
990
1003
  const trig = TRIGGER_BY_KEY.get(l.trigger);
991
1004
  const meaning = ENFORCEMENT_MEANING[l.enforcement] || { label: l.enforcement, detail: '' };
992
- const userStated = l.origin === ORIGIN.USER_STATED;
1005
+ const userStated = l.origin === ORIGIN.USER_STATED && l.sourceClass === SOURCE_CLASS.CURRENT_USER;
1006
+ const quarantined = l.sourceClass === SOURCE_CLASS.IMPORTED_OWNER || l.sourceClass === SOURCE_CLASS.DEMONSTRATION;
1007
+ const origin = l.sourceClass === SOURCE_CLASS.CURRENT_USER
1008
+ ? 'you taught me this'
1009
+ : l.sourceClass === SOURCE_CLASS.IMPORTED_OWNER
1010
+ ? 'imported maintainer history — not yours'
1011
+ : l.sourceClass === SOURCE_CLASS.DEMONSTRATION
1012
+ ? 'demonstration data — not personal policy'
1013
+ : 'I inferred this from what happened';
993
1014
  return {
994
1015
  id: l.id,
995
1016
  statement: l.statement,
@@ -1001,8 +1022,10 @@ function gatherLessons() {
1001
1022
  enforcementLabel: meaning.label,
1002
1023
  enforcementDetail: meaning.detail,
1003
1024
  // Provenance drives trust, so it is stated plainly and never flattened into a badge colour.
1004
- origin: userStated ? 'you taught me this' : 'I inferred this from what happened',
1025
+ origin,
1026
+ sourceClass: l.sourceClass,
1005
1027
  userStated,
1028
+ quarantined,
1006
1029
  taughtCount: l.repeatCount || 0,
1007
1030
  projects: Array.isArray(l.projects) ? l.projects : [],
1008
1031
  evidence: l.evidence || null,
@@ -1011,10 +1034,11 @@ function gatherLessons() {
1011
1034
  ratified: l.status === STATUS.RATIFIED || l.status === STATUS.ACTIVE,
1012
1035
  demoted: !!l.demoted,
1013
1036
  // The one thing the user is being ASKED, as opposed to merely shown.
1014
- awaitingYou: l.status === STATUS.CANDIDATE && !l.demoted,
1037
+ awaitingYou: l.status === STATUS.CANDIDATE && !l.demoted && !quarantined,
1015
1038
  // Honest ceiling: ratifying a model-inferred lesson can NOT raise it to block
1016
1039
  // (lesson-store.mjs:380). Say so before they click, not after.
1017
1040
  canReachBlock: userStated,
1041
+ canRatify: !quarantined,
1018
1042
  intendedEnforcement: l.intendedEnforcement || null,
1019
1043
  };
1020
1044
  });
@@ -1038,6 +1062,7 @@ function gatherLessons() {
1038
1062
  active: lessons.filter((l) => l.ratified && !l.demoted).length,
1039
1063
  awaitingYou: lessons.filter((l) => l.awaitingYou).length,
1040
1064
  off: lessons.filter((l) => l.demoted).length,
1065
+ quarantined: lessons.filter((l) => l.quarantined).length,
1041
1066
  blocking: lessons.filter((l) => l.enforcement === 'block' && l.ratified && !l.demoted).length,
1042
1067
  },
1043
1068
  // TASK 2: this endpoint bypasses serveCached entirely and had NO timestamp of any kind. It is
@@ -1068,6 +1093,9 @@ function setLesson(body) {
1068
1093
  }
1069
1094
  const before = loadLessons().find((l) => l.id === id);
1070
1095
  if (!before) return { ok: false, log: `nothing changed — no lesson with id ${id}` };
1096
+ if (action === 'ratify' && (before.sourceClass === SOURCE_CLASS.IMPORTED_OWNER || before.sourceClass === SOURCE_CLASS.DEMONSTRATION)) {
1097
+ return { ok: false, log: 'nothing changed — imported or demonstration history cannot become personal policy' };
1098
+ }
1071
1099
 
1072
1100
  try {
1073
1101
  updateLessons((fresh) => spec.fn(id, fresh));
@@ -1695,7 +1723,10 @@ function gatherRouterEngine() {
1695
1723
  // user's Settings choice is now the single source of truth, via the SAME detectProvider() the
1696
1724
  // savings.utilization frontier calc already uses (config → env → catalog default) — so this and
1697
1725
  // the frontier calc can never disagree either.
1698
- let house, providerKeys = {};
1726
+ const subscriptions = detectSubscriptions();
1727
+ let house;
1728
+ let providerKeys = providerAvailability(null, subscriptions);
1729
+ let providerCatalog = { status: 'degraded', detail: 'provider catalog was not loaded; native boolean detections are shown' };
1699
1730
  try {
1700
1731
  const hcat = loadCatalog();
1701
1732
  house = detectProvider(hcat, { provider: cfg.provider });
@@ -1704,11 +1735,12 @@ function gatherRouterEngine() {
1704
1735
  // key found". Read each provider's real detect_env vars — minus the CLAUDECODE / CLAUDE_CODE_ENTRYPOINT
1705
1736
  // run-context markers (which are not credentials), exactly as detectProvider() itself filters them —
1706
1737
  // so the UI's "key found / not found" is now true instead of decorative.
1707
- const IGNORE_ENV = new Set(['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT']);
1708
- for (const [name, p] of Object.entries(hcat.providers || {})) {
1709
- providerKeys[name] = (p.detect_env || []).some((k) => !IGNORE_ENV.has(k) && !!process.env[k]);
1710
- }
1711
- } catch { house = { provider: cfg.provider && cfg.provider !== 'auto' ? cfg.provider : 'anthropic', source: 'default' }; }
1738
+ providerKeys = providerAvailability(hcat, subscriptions);
1739
+ providerCatalog = { status: 'ok', detail: 'verified provider catalog loaded' };
1740
+ } catch (error) {
1741
+ house = { provider: cfg.provider && cfg.provider !== 'auto' ? cfg.provider : 'anthropic', source: 'default' };
1742
+ providerCatalog = { status: 'degraded', detail: `provider catalog unavailable: ${String(error?.message || error)}` };
1743
+ }
1712
1744
  return {
1713
1745
  engine: {
1714
1746
  package: '@metaharness/router', installed,
@@ -1717,12 +1749,13 @@ function gatherRouterEngine() {
1717
1749
  outcomesLog: OUTCOMES.replace(os.homedir(), '~'),
1718
1750
  },
1719
1751
  keys: { openrouter: openrouterKey, ...providerKeys },
1752
+ providerCatalog,
1720
1753
  // Paid seats, found at USER level. `keys` above is env-var API keys only, which is exactly why a
1721
1754
  // user with ChatGPT Max and Claude Max read as "auto" — neither plan puts a key in the
1722
1755
  // environment. These two fields are what let the UI say "you already have this" instead of
1723
1756
  // asking someone to paste a credential they are already paying not to need.
1724
- subscriptions: detectSubscriptions(),
1725
- preferredSeat: preferredSeat(detectSubscriptions()),
1757
+ subscriptions,
1758
+ preferredSeat: preferredSeat(subscriptions),
1726
1759
  profile: { present: !!profile, path: PROFILE_PATH.replace(os.homedir(), '~') },
1727
1760
  catalogSource: engineCatalogSource(), // 'catalog' | 'built-in-fallback' — so the UI never calls the stub a real catalog
1728
1761
  house,
@@ -1833,6 +1866,10 @@ async function fetchReleaseDigest() {
1833
1866
  // in the heading of the console"). Plugin-cache dir first — the truth on installed machines — then
1834
1867
  // the repo's plugin.json for dev checkouts. null hides the chip rather than guessing.
1835
1868
  function brainVersionOnDisk() {
1869
+ const runtimeIdentity = readJSON(path.join(REPO, 'runtime-identity.json'));
1870
+ if (typeof runtimeIdentity?.runtimeVersion === 'string' && runtimeIdentity.runtimeVersion) {
1871
+ return runtimeIdentity.runtimeVersion.replace(/^v/, '');
1872
+ }
1836
1873
  try { const v = readInstallChannel().version; if (v) return String(v).replace(/^v/, ''); } catch { /* fall through */ }
1837
1874
  try { return getVersion(); } catch { return null; }
1838
1875
  }
@@ -2948,6 +2985,7 @@ export {
2948
2985
  saveBrainPower,
2949
2986
  gatherBrainProfile,
2950
2987
  saveBrainProfile,
2988
+ gatherRouterEngine,
2951
2989
  autoEligibleIds,
2952
2990
  };
2953
2991
  // Exported for the cross-project cache-isolation test (console-cache-scope.test.mjs). serveCached's
@@ -0,0 +1,12 @@
1
+ const IGNORED_ENV = new Set(['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT']);
2
+
3
+ // Boolean-only by construction: no credential value can cross the Console API boundary.
4
+ export function providerAvailability(catalog, subscriptions = {}, env = process.env) {
5
+ if (catalog?.providers && typeof catalog.providers === 'object') {
6
+ return Object.fromEntries(Object.entries(catalog.providers).map(([name, provider]) => [
7
+ name,
8
+ (provider?.detect_env || []).some((key) => !IGNORED_ENV.has(key) && Boolean(env[key])),
9
+ ]));
10
+ }
11
+ return Object.fromEntries(Object.entries(subscriptions).map(([name, value]) => [name, Boolean(value?.apiKey)]));
12
+ }
@@ -7,7 +7,10 @@ import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
 
9
9
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
10
- const CANONICAL_PUBLISHER = 'scripts/release.mjs';
10
+ const CANONICAL_PUBLISHERS = new Set([
11
+ 'scripts/release.mjs',
12
+ 'scripts/release-transaction-provider.mjs',
13
+ ]);
11
14
  const SOURCE_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.sh']);
12
15
 
13
16
  function executableSource(source) {
@@ -27,6 +30,14 @@ const ACTIONS = [
27
30
  ],
28
31
  shellPatterns: [/^\s*gh\s+release\s+create\b/m],
29
32
  },
33
+ {
34
+ action: 'github-release-update',
35
+ jsPatterns: [
36
+ /['"]PATCH['"][\s\S]{0,120}releases\//,
37
+ /\[\s*['"]release['"]\s*,\s*['"]edit['"]/,
38
+ ],
39
+ shellPatterns: [/^\s*gh\s+(?:release\s+edit|api\s+.*releases\/.*PATCH)\b/m],
40
+ },
30
41
  {
31
42
  action: 'npm-publish',
32
43
  jsPatterns: [
@@ -47,7 +58,7 @@ const ACTIONS = [
47
58
 
48
59
  export function detectPublisherActions(file, source) {
49
60
  const relative = file.split(path.sep).join('/');
50
- if (relative === CANONICAL_PUBLISHER) return [];
61
+ if (CANONICAL_PUBLISHERS.has(relative)) return [];
51
62
  const executable = executableSource(source);
52
63
  const kind = path.extname(relative) === '.sh' ? 'shellPatterns' : 'jsPatterns';
53
64
  return ACTIONS
@@ -67,6 +78,7 @@ function sourceFiles(root) {
67
78
  };
68
79
  visit(path.join(root, 'scripts'));
69
80
  visit(path.join(root, 'deploy'));
81
+ visit(path.join(root, 'bin'));
70
82
  return files;
71
83
  }
72
84