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.
- package/README.md +3 -3
- package/bin/install.mjs +112 -7
- package/config/model-router/catalog.template.json +17 -2
- package/console/app.js +32 -14
- package/console/architecture.html +1 -1
- package/data/model-catalog.json +54 -0
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/hooks.json +34 -1
- package/plugin/scripts/ground-ruvnet.sh +2 -1
- package/plugin/scripts/hook-shim.mjs +13 -6
- package/plugin/scripts/lesson-provenance.mjs +21 -0
- package/plugin/scripts/lesson-store.mjs +33 -3
- package/plugin/scripts/route-dispatch.sh +23 -49
- package/plugin/scripts/session-snapshot-contract.mjs +100 -0
- package/plugin/scripts/session-snapshot-hook.mjs +34 -0
- package/plugin/scripts/session-start-core.mjs +25 -7
- package/plugin/scripts/swarm-slot-recycler.mjs +89 -0
- package/plugin/skills/release-proof/SKILL.md +4 -4
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +19 -4
- package/scripts/capability-registry.mjs +3 -3
- package/scripts/lesson-lifecycle.mjs +2 -1
- package/scripts/lesson-ratify.mjs +6 -3
- package/scripts/lesson-seed.mjs +21 -16
- package/scripts/model-router-catalog.mjs +16 -0
- package/scripts/model-router-engine.mjs +4 -1
- package/scripts/onboarding-console.mjs +52 -14
- package/scripts/provider-availability.mjs +12 -0
- package/scripts/release-authority.mjs +14 -2
- package/scripts/release-transaction-provider.mjs +236 -0
- package/scripts/release-transaction.mjs +214 -0
- package/scripts/release.mjs +67 -279
- package/scripts/session-snapshot-contract.mjs +1 -0
- 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.
|
|
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
|
|
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;
|
package/scripts/lesson-seed.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
|
238
|
-
console.log(` the
|
|
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
|
|
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
|
|
425
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
}
|
|
1711
|
-
|
|
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
|
|
1725
|
-
preferredSeat: preferredSeat(
|
|
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
|
|
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
|
|
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
|
|