@mindrian_os/cli 2.0.0-beta.33 → 2.0.0-beta.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +56 -0
- package/bin/mindrian-brain-mcp-client.cjs +14 -0
- package/commands/doctor.md +2 -2
- package/data/brain-census.generated.json +2827 -1523
- package/lib/core/brain-client.cjs +47 -3
- package/lib/core/brain-prewarm.cjs +164 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +160 -26
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +441 -82
- package/lib/core/integration-registry.cjs +96 -20
- package/lib/mcp/brain-composition-census.cjs +22 -6
- package/lib/mcp/brain-route-bound.cjs +66 -0
- package/lib/mcp/brain-router.cjs +25 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/build-brain-census.cjs +321 -4
- package/scripts/doctor.cjs +22 -7
- package/scripts/session-start +22 -0
- package/skills/doctor/SKILL.md +2 -2
|
@@ -1250,29 +1250,68 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
1252
|
// CONFIDENCE: top step reads exactly 0.9; a missing/non-finite degree -> 0.5.
|
|
1253
|
+
// theo_rank and the command-bearing-first sort below never touch this
|
|
1254
|
+
// computation -- the sort reorders `options`, it never recomputes a
|
|
1255
|
+
// confidence value (Quick 260911-ddd, DDD-01).
|
|
1253
1256
|
const top = steps.reduce((max, s) => {
|
|
1254
1257
|
const d = s && Number.isFinite(s.degree) ? s.degree : 0;
|
|
1255
1258
|
return d > max ? d : max;
|
|
1256
1259
|
}, 0);
|
|
1257
|
-
const options = steps.map((s) => {
|
|
1260
|
+
const options = steps.map((s, idx) => {
|
|
1258
1261
|
const degree = s && s.degree;
|
|
1259
1262
|
let confidence = 0.5;
|
|
1260
1263
|
if (top > 0 && Number.isFinite(degree)) {
|
|
1261
1264
|
confidence = Math.max(0.5, Math.min(0.9, Math.round((0.5 + 0.4 * (degree / top)) * 100) / 100));
|
|
1262
1265
|
}
|
|
1266
|
+
// Quick 260911-ddd (DDD-01): theo_rank carries Theo's own rank so it
|
|
1267
|
+
// is never lost by the reorder below. Theo's `step` field when it is
|
|
1268
|
+
// a finite number, else the 1-based index of this step WITHIN Theo's
|
|
1269
|
+
// own chain array, computed here before any sort so a later reorder
|
|
1270
|
+
// can never leak into this number.
|
|
1271
|
+
const theoRank = (s && Number.isFinite(s.step)) ? s.step : idx + 1;
|
|
1263
1272
|
return {
|
|
1264
1273
|
framework: s && s.framework,
|
|
1265
1274
|
confidence: confidence,
|
|
1266
1275
|
commands: commandsByFramework.get(s && s.framework) || [],
|
|
1276
|
+
theo_rank: theoRank,
|
|
1267
1277
|
};
|
|
1268
1278
|
});
|
|
1269
1279
|
|
|
1280
|
+
// Quick 260911-ddd (DDD-01): stable partition, command-bearing options
|
|
1281
|
+
// first, Theo's relative order preserved inside each group. Theo's
|
|
1282
|
+
// ranking is NEVER altered at the source -- this reorders the plugin's
|
|
1283
|
+
// OWN options array only. A single forward pass into two queues, then
|
|
1284
|
+
// concatenated, is stable BY CONSTRUCTION; it does not rest on
|
|
1285
|
+
// Array.prototype.sort's engine-stability semantics. When every option
|
|
1286
|
+
// or no option carries a command, one queue is empty and the
|
|
1287
|
+
// concatenation is the identity, so Theo's order survives unchanged
|
|
1288
|
+
// for free.
|
|
1289
|
+
//
|
|
1290
|
+
// DOWNSTREAM CONSEQUENCE, stated here so it is not mistaken for a
|
|
1291
|
+
// regression later: lib/mcp/brain-router.cjs:398 derives topConf from
|
|
1292
|
+
// options[0].confidence. On a chain whose top-ranked framework has no
|
|
1293
|
+
// command, the routed confidence now reads the first command-bearing
|
|
1294
|
+
// option's confidence (0.83 on the live IllDefined shape, previously
|
|
1295
|
+
// 0.9) -- that is the intended meaning of the change (confidence
|
|
1296
|
+
// describes the option actually surfaced to the user), not a bug.
|
|
1297
|
+
// brain-router.cjs is deliberately NOT touched to compensate for this.
|
|
1298
|
+
const commandBearing = [];
|
|
1299
|
+
const commandLess = [];
|
|
1300
|
+
for (const opt of options) {
|
|
1301
|
+
if (Array.isArray(opt.commands) && opt.commands.length > 0) {
|
|
1302
|
+
commandBearing.push(opt);
|
|
1303
|
+
} else {
|
|
1304
|
+
commandLess.push(opt);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const sortedOptions = commandBearing.concat(commandLess);
|
|
1308
|
+
|
|
1270
1309
|
const out = Object.assign({}, payload);
|
|
1271
1310
|
delete out.query_terms;
|
|
1272
1311
|
out.directive = {
|
|
1273
|
-
guided: { questions: [], framework: (
|
|
1312
|
+
guided: { questions: [], framework: (sortedOptions[0] && sortedOptions[0].framework) || null, stage: rung },
|
|
1274
1313
|
};
|
|
1275
|
-
out.next_gate = { sub_shape: 'F.1', options:
|
|
1314
|
+
out.next_gate = { sub_shape: 'F.1', options: sortedOptions };
|
|
1276
1315
|
out.grounding = {
|
|
1277
1316
|
source: 'theo',
|
|
1278
1317
|
answer_mode: payload.answer_mode,
|
|
@@ -1282,6 +1321,10 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1282
1321
|
chain_coverage: (chainRes && chainRes.coverage && typeof chainRes.coverage === 'object') ? chainRes.coverage : null,
|
|
1283
1322
|
chain_status: chainStatus,
|
|
1284
1323
|
confidence_source: 'theo_degree_normalized',
|
|
1324
|
+
// Quick 260911-ddd (DDD-01): names the applied ordering so a
|
|
1325
|
+
// consumer never has to branch on the key's presence -- present here
|
|
1326
|
+
// AND on the trailing catch path below.
|
|
1327
|
+
option_order: 'command_bearing_first',
|
|
1285
1328
|
};
|
|
1286
1329
|
return out;
|
|
1287
1330
|
} catch (_e) {
|
|
@@ -1299,6 +1342,7 @@ async function _composeTheoAsk(payload, question, deps) {
|
|
|
1299
1342
|
chain_coverage: null,
|
|
1300
1343
|
chain_status: 'unreachable',
|
|
1301
1344
|
confidence_source: 'theo_degree_normalized',
|
|
1345
|
+
option_order: 'command_bearing_first',
|
|
1302
1346
|
};
|
|
1303
1347
|
return out;
|
|
1304
1348
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Quick 260911-ddd (DDD-02) -- a content-free Brain pre-warm fired at MCP
|
|
8
|
+
* shim startup so the Brain is already warm by the time the first question
|
|
9
|
+
* arrives, instead of leaving every cold Render wake to silently degrade
|
|
10
|
+
* /mos:act's Tier 3 race to the local heuristic (see
|
|
11
|
+
* lib/mcp/brain-route-bound.cjs for the measured cold-wake numbers this
|
|
12
|
+
* exists to cover).
|
|
13
|
+
*
|
|
14
|
+
* WHY THE TIMEOUT DOES NOT BOUND THE WAKE ITSELF: the request reaching
|
|
15
|
+
* Render is what wakes the instance; aborting our own wait does not cancel
|
|
16
|
+
* that in-flight wake on Render's side. The timeout here only bounds how
|
|
17
|
+
* long THIS PROCESS holds the marker write open waiting for a verdict --
|
|
18
|
+
* which is why 15000 ms is generous without costing anyone anything: even
|
|
19
|
+
* if we give up waiting, the wake we triggered keeps running on Render and
|
|
20
|
+
* the NEXT real call benefits from it.
|
|
21
|
+
*
|
|
22
|
+
* CANON PART 8 (D-02): the probe sends theo_health with NO ARGUMENTS
|
|
23
|
+
* (`callTool('theo_health', {})`, the exact shape class-m-brain-smoke.cjs:225
|
|
24
|
+
* already ships) and the marker persists ONLY {at, ok, origin_host} --
|
|
25
|
+
* never a response body, never a header value, never a question. Every fs
|
|
26
|
+
* and network failure is swallowed; this function NEVER throws and NEVER
|
|
27
|
+
* rejects.
|
|
28
|
+
*
|
|
29
|
+
* MCP STDIO SAFETY: this file writes NOTHING to stdout, ever, because it
|
|
30
|
+
* can run inside an MCP stdio process where a stray stdout byte corrupts
|
|
31
|
+
* the JSON-RPC transport. A single stderr line only when MINDRIAN_DEBUG is
|
|
32
|
+
* set.
|
|
33
|
+
*
|
|
34
|
+
* The deps seam ({ probe, originHost, now, homeDir, timeoutMs }, all
|
|
35
|
+
* optional) mirrors the same injectable-probe discipline
|
|
36
|
+
* lib/core/doctor/class-m-brain-smoke.cjs already uses for its own
|
|
37
|
+
* theoHealthFn seam, so a test drives every arm through deps and never
|
|
38
|
+
* touches a real Brain.
|
|
39
|
+
*
|
|
40
|
+
* No em-dashes. CJS only.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
const path = require('path');
|
|
45
|
+
const os = require('os');
|
|
46
|
+
|
|
47
|
+
const DEFAULT_PREWARM_TIMEOUT_MS = 15000;
|
|
48
|
+
|
|
49
|
+
const debugLog = (msg) => {
|
|
50
|
+
if (!process.env.MINDRIAN_DEBUG) return;
|
|
51
|
+
try {
|
|
52
|
+
process.stderr.write('[brain-prewarm] ' + msg + '\n');
|
|
53
|
+
} catch (_e) {
|
|
54
|
+
// swallow -- this function must never throw
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Derive the timeout (ms) for the probe race. Reads
|
|
60
|
+
* MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS when it parses to a finite positive
|
|
61
|
+
* integer; otherwise DEFAULT_PREWARM_TIMEOUT_MS.
|
|
62
|
+
* @returns {number}
|
|
63
|
+
*/
|
|
64
|
+
function _resolveTimeoutMs() {
|
|
65
|
+
const raw = process.env.MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS;
|
|
66
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) return DEFAULT_PREWARM_TIMEOUT_MS;
|
|
67
|
+
const parsed = Number(raw);
|
|
68
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return DEFAULT_PREWARM_TIMEOUT_MS;
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Reduce a Brain origin URL to its host only (never the full URL, never a
|
|
74
|
+
* path, never a query string -- the marker holds origin_host, not the URL).
|
|
75
|
+
* @param {string} url
|
|
76
|
+
* @returns {string|null}
|
|
77
|
+
*/
|
|
78
|
+
function _hostOnly(url) {
|
|
79
|
+
if (typeof url !== 'string' || url.length === 0) return null;
|
|
80
|
+
try {
|
|
81
|
+
return new URL(url).host || null;
|
|
82
|
+
} catch (_e) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* markerPath(homeDir) -- the on-disk location of the pre-warm marker.
|
|
89
|
+
* @param {string} [homeDir] defaults to MINDRIAN_HOME or ~/.mindrian
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
function markerPath(homeDir) {
|
|
93
|
+
const home = homeDir || process.env.MINDRIAN_HOME || path.join(os.homedir(), '.mindrian');
|
|
94
|
+
return path.join(home, 'brain-prewarm.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* prewarm(deps) -- fires one content-free theo_health probe and persists a
|
|
99
|
+
* minimal marker. NEVER throws, NEVER rejects, NEVER writes to stdout.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} [deps]
|
|
102
|
+
* @param {() => Promise<any>} [deps.probe] defaults to
|
|
103
|
+
* `() => require('./brain-client.cjs').callTool('theo_health', {})`,
|
|
104
|
+
* required LAZILY so importing this module costs nothing.
|
|
105
|
+
* @param {string} [deps.originHost] defaults to the host of
|
|
106
|
+
* `require('./brain-client.cjs').getBrainUrl()`.
|
|
107
|
+
* @param {() => Date} [deps.now] defaults to `() => new Date()`.
|
|
108
|
+
* @param {string} [deps.homeDir] defaults to MINDRIAN_HOME or ~/.mindrian.
|
|
109
|
+
* @param {number} [deps.timeoutMs] defaults to
|
|
110
|
+
* MINDRIAN_BRAIN_PREWARM_TIMEOUT_MS or 15000.
|
|
111
|
+
* @returns {Promise<{ at: string, ok: boolean, origin_host: string|null }>}
|
|
112
|
+
*/
|
|
113
|
+
async function prewarm(deps) {
|
|
114
|
+
deps = deps || {};
|
|
115
|
+
const probe = deps.probe || (() => require('./brain-client.cjs').callTool('theo_health', {}));
|
|
116
|
+
const originHost = (typeof deps.originHost === 'string')
|
|
117
|
+
? deps.originHost
|
|
118
|
+
: _hostOnly((() => {
|
|
119
|
+
try {
|
|
120
|
+
return require('./brain-client.cjs').getBrainUrl();
|
|
121
|
+
} catch (_e) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
})());
|
|
125
|
+
const now = deps.now || (() => new Date());
|
|
126
|
+
const homeDir = deps.homeDir;
|
|
127
|
+
const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : _resolveTimeoutMs();
|
|
128
|
+
|
|
129
|
+
let ok = false;
|
|
130
|
+
try {
|
|
131
|
+
const TIMEOUT_SENTINEL = Symbol('brain-prewarm-timeout');
|
|
132
|
+
const result = await Promise.race([
|
|
133
|
+
Promise.resolve()
|
|
134
|
+
.then(() => probe())
|
|
135
|
+
.catch(() => null),
|
|
136
|
+
new Promise((resolve) => setTimeout(() => resolve(TIMEOUT_SENTINEL), timeoutMs)),
|
|
137
|
+
]);
|
|
138
|
+
ok = !!(result && result !== TIMEOUT_SENTINEL && typeof result === 'object');
|
|
139
|
+
} catch (_e) {
|
|
140
|
+
ok = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Canon Part 8 (D-02): EXACTLY three keys, nothing else. Never any part
|
|
144
|
+
// of the probe response body, never a key, never a question.
|
|
145
|
+
const marker = { at: now().toISOString(), ok: ok, origin_host: originHost || null };
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const filePath = markerPath(homeDir);
|
|
149
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
150
|
+
fs.writeFileSync(filePath, JSON.stringify(marker), 'utf8');
|
|
151
|
+
debugLog('marker written: ' + JSON.stringify(marker));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
debugLog('marker write failed: ' + (e && e.message ? e.message : String(e)));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return marker;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Directly spawnable: `node lib/core/brain-prewarm.cjs`.
|
|
160
|
+
if (require.main === module) {
|
|
161
|
+
prewarm().catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = { prewarm, markerPath };
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Phase 127-02 BRAIN-MCP-127-08 (CONTEXT D4) -- Class M Brain smoke.
|
|
5
|
-
*
|
|
5
|
+
* 7-layer composable probe replacing ~60% of doctor Brain-adjacent checks.
|
|
6
|
+
* Grew from 6 to 7 layers in quick task 260911-axz (AXZ-01): L0 prepended.
|
|
6
7
|
*
|
|
7
8
|
* "Class M" rationale: CONTEXT D4 text reads "K" but letter K is already
|
|
8
9
|
* taken in scripts/doctor.cjs by --stale-first-touch (SEED-007). A-L are
|
|
9
10
|
* assigned. M is the next free letter. The CAPABILITY-MAP.md doc patch
|
|
10
11
|
* lands in plan 127-03.
|
|
11
12
|
*
|
|
12
|
-
* Detects 12 Phase 126 taxonomy rows plus the store-identity
|
|
13
|
+
* Detects 12 Phase 126 taxonomy rows plus the store-identity and
|
|
14
|
+
* origin-and-shadow senses:
|
|
15
|
+
* L0 origin_shadow quick task 260911-axz (AXZ-01): answers "which
|
|
16
|
+
* origin am I on, is it alive, and is anything
|
|
17
|
+
* shadowing my shim" in one row. Closes the exact
|
|
18
|
+
* blind spot that let a beta.33 install run Larry at
|
|
19
|
+
* Tier 0 for a whole session: a user-scope or
|
|
20
|
+
* local-scope `mindrian-brain` entry in
|
|
21
|
+
* ~/.claude.json can shadow the plugin's own
|
|
22
|
+
* `.mcp.json` stdio shim of the same name, and L1-L6
|
|
23
|
+
* below probe IN PROCESS through brain-client.cjs, so
|
|
24
|
+
* they cannot see it -- the probe was never wrong,
|
|
25
|
+
* it was blind to the one thing that mattered.
|
|
13
26
|
* L1 plugin_root #5 install-cache stale, #9 install-state drift
|
|
14
27
|
* L2 key_resolver #1 missing key, #2 perms-too-open, #8 env unreadable,
|
|
15
28
|
* #13 Bearer format mismatch
|
|
@@ -28,16 +41,26 @@
|
|
|
28
41
|
* (active-plugin-root, resolve-brain-key, brain-client.schema). L4/L5
|
|
29
42
|
* stdio orchestration and L6 both reuse the brain-client stats and query
|
|
30
43
|
* chokepoints; L6 mints no new server-side tool for the GraphRagMeta
|
|
31
|
-
* stamp, it reads that through the existing bounded brain_query path.
|
|
44
|
+
* stamp, it reads that through the existing bounded brain_query path. L0
|
|
45
|
+
* reuses lib/core/integration-registry.cjs's readScopedMcpServers (the
|
|
46
|
+
* ONE ~/.claude.json scoped mcpServers reader) rather than minting a
|
|
47
|
+
* third config reader.
|
|
32
48
|
* Canon Part 8 (graph boundary): probe queries the methodology schema
|
|
33
49
|
* handle only; zero user-content egress; every Brain payload routes
|
|
34
50
|
* through brain-client.cjs (the delegation chokepoint). L6 reads store
|
|
35
51
|
* metadata only (endpoint, node count, GraphRagMeta stamp fields) -- zero
|
|
36
|
-
* user content.
|
|
52
|
+
* user content. L0's shadow scan is a STRUCTURAL secret-leak guard: the
|
|
53
|
+
* integration-registry.cjs projection it consumes never carries headers,
|
|
54
|
+
* the full url, or env, so no Authorization value or other header value
|
|
55
|
+
* can reach this layer's output even by accident.
|
|
37
56
|
*
|
|
38
|
-
* fail-fast cascade: if layer N fails, layers N+1..6 are SKIPPED
|
|
39
|
-
* reason="skipped-prior-layer-failed" so the report points at the
|
|
40
|
-
* failure, not the cascade noise.
|
|
57
|
+
* fail-fast cascade: if layer N fails (N >= 1), layers N+1..6 are SKIPPED
|
|
58
|
+
* with reason="skipped-prior-layer-failed" so the report points at the
|
|
59
|
+
* FIRST failure, not the cascade noise. L0 (index 0) is the one
|
|
60
|
+
* exception: its own ok=false never triggers this skip (see the `i > 0`
|
|
61
|
+
* gate in checkBrainSmoke below) -- a shadow finding is information
|
|
62
|
+
* layered ON TOP of the in-process probe below it, never a substitute for
|
|
63
|
+
* running that probe.
|
|
41
64
|
*
|
|
42
65
|
* HARD RULE: no em-dashes anywhere in this file.
|
|
43
66
|
*/
|
|
@@ -48,6 +71,7 @@ const { spawn } = require('node:child_process');
|
|
|
48
71
|
|
|
49
72
|
// Layer registry. Wire-locked: the shell harness asserts id strings + order.
|
|
50
73
|
const LAYERS = Object.freeze([
|
|
74
|
+
Object.freeze({ id: 'origin_shadow', name: 'L0 origin and shadow connector' }),
|
|
51
75
|
Object.freeze({ id: 'plugin_root', name: 'L1 plugin-root-resolver' }),
|
|
52
76
|
Object.freeze({ id: 'key_resolver', name: 'L2 brain-key-resolver' }),
|
|
53
77
|
Object.freeze({ id: 'https_schema', name: 'L3 HTTPS schema probe' }),
|
|
@@ -79,9 +103,14 @@ const OVERALL_BUDGET_MS = 30000;
|
|
|
79
103
|
// client.cjs, never re-declared here) so a MINDRIAN_BRAIN_URL rollback to
|
|
80
104
|
// the incumbent restores the incumbent's 29000 floor in the same motion
|
|
81
105
|
// that moves the URL and the alias vocabulary back -- one lever, not two.
|
|
82
|
-
// THEO_NODE_FLOOR
|
|
83
|
-
//
|
|
84
|
-
//
|
|
106
|
+
// THEO_NODE_FLOOR moved from 1000 to 27000 in quick task 260911-axz
|
|
107
|
+
// (AXZ-01). The old prose argued a tight number would be stale within
|
|
108
|
+
// days because Theo's canon was growing from 712 to 1,253 nodes; that
|
|
109
|
+
// argument expired. Measured live on 2026-09-11, Theo holds 27,951 nodes,
|
|
110
|
+
// so 27,000 is a 3.4 percent margin below the measured floor -- close to
|
|
111
|
+
// the incumbent's own posture (29,000 against roughly 29,200, about 1
|
|
112
|
+
// percent). A floor of 1000 against a 27,951-node live store would pass a
|
|
113
|
+
// 96 percent content loss; that is not a floor, it is a rubber stamp.
|
|
85
114
|
// STALE_REPLICA_NODE_COUNT: the frozen, roughly-July signature of the
|
|
86
115
|
// retired replica store (the pre-migration onrender host, now decommissioned).
|
|
87
116
|
// Seeing exactly this count means the wire is pointed at a copy, not
|
|
@@ -95,7 +124,7 @@ const OVERALL_BUDGET_MS = 30000;
|
|
|
95
124
|
const { THEO_ORIGINS } = require('../brain-client.cjs');
|
|
96
125
|
const CANON_BRAIN_URL = 'https://theo-mcp.onrender.com';
|
|
97
126
|
const CANON_NODE_FLOOR = 29000;
|
|
98
|
-
const THEO_NODE_FLOOR =
|
|
127
|
+
const THEO_NODE_FLOOR = 27000;
|
|
99
128
|
const STALE_REPLICA_NODE_COUNT = 28325;
|
|
100
129
|
|
|
101
130
|
// GraphRagMeta stamp read: one bounded LIMIT 1 read projecting only the
|
|
@@ -147,6 +176,84 @@ async function _runLayer(_name, fn) {
|
|
|
147
176
|
}
|
|
148
177
|
}
|
|
149
178
|
|
|
179
|
+
// L0 -- origin and shadow connector (quick task 260911-axz, AXZ-01).
|
|
180
|
+
//
|
|
181
|
+
// Three things in one row, always attempted, never throwing to the caller:
|
|
182
|
+
//
|
|
183
|
+
// (a) Shadow scan. Reads Claude Code's OWN config (~/.claude.json by
|
|
184
|
+
// default) through the ONE scoped reader lib/core/integration-
|
|
185
|
+
// registry.cjs exports (readScopedMcpServers), never a second reader.
|
|
186
|
+
// Filters for name === 'mindrian-brain' at either scope Claude Code
|
|
187
|
+
// recognizes for this file (user = top-level mcpServers, local =
|
|
188
|
+
// projects[dir].mcpServers). A repo's own .mcp.json ("project" scope in
|
|
189
|
+
// Claude Code's vocabulary, where the plugin ships its stdio shim) is
|
|
190
|
+
// structurally never read by this reader, so it is never flagged.
|
|
191
|
+
//
|
|
192
|
+
// (b) Resolved-origin row, reported whether or not a shadow was found: the
|
|
193
|
+
// endpoint getBrainUrl() resolves to, whether it is a Theo origin, and
|
|
194
|
+
// whether MINDRIAN_BRAIN_URL overrode it. This is information, not a
|
|
195
|
+
// verdict; a non-Theo origin under an explicit override is not itself
|
|
196
|
+
// a failure (mirrors L6's own canon/override distinction).
|
|
197
|
+
//
|
|
198
|
+
// (c) theo_health, best effort, mirroring L6's GraphRagMeta stamp
|
|
199
|
+
// discipline: wrapped in try/catch, degrading silently to no health
|
|
200
|
+
// data on any rejection. It NEVER changes the verdict below. Only
|
|
201
|
+
// { mode, build_sha } are ever projected onto the payload --
|
|
202
|
+
// `instanceUri`, `quarantineCode`, and `serverAgent` are never copied
|
|
203
|
+
// (T-axz-02).
|
|
204
|
+
//
|
|
205
|
+
// Verdict: ok is false if and only if at least one mindrian-brain shadow
|
|
206
|
+
// entry was found. The origin and health halves are information, never the
|
|
207
|
+
// verdict, exactly as L6 treats canon/override as information separate from
|
|
208
|
+
// its own count-floor verdict.
|
|
209
|
+
async function _layer0(opts) {
|
|
210
|
+
const scopedServersFn = opts.mockScopedServers
|
|
211
|
+
|| require('../integration-registry.cjs').readScopedMcpServers;
|
|
212
|
+
const scoped = scopedServersFn({ configPath: opts.claudeConfigPath, projectDir: opts.projectDir }) || [];
|
|
213
|
+
const shadows = scoped
|
|
214
|
+
.filter(function (e) { return e && e.name === 'mindrian-brain'; })
|
|
215
|
+
.map(function (e) { return { name: e.name, scope: e.scope, url_host: e.url_host }; });
|
|
216
|
+
|
|
217
|
+
const brainUrlFn = opts.mockBrainUrl || (() => require('../brain-client.cjs').getBrainUrl());
|
|
218
|
+
const resolved_origin = brainUrlFn();
|
|
219
|
+
const is_theo = THEO_ORIGINS.indexOf(resolved_origin) !== -1;
|
|
220
|
+
const override = !!(process.env.MINDRIAN_BRAIN_URL && process.env.MINDRIAN_BRAIN_URL.length > 0);
|
|
221
|
+
|
|
222
|
+
const payload = { resolved_origin: resolved_origin, is_theo: is_theo, override: override, shadows: shadows };
|
|
223
|
+
|
|
224
|
+
const theoHealthFn = opts.mockTheoHealth
|
|
225
|
+
|| (async () => require('../brain-client.cjs').callTool('theo_health', {}));
|
|
226
|
+
try {
|
|
227
|
+
const health = await theoHealthFn();
|
|
228
|
+
if (health && typeof health === 'object') {
|
|
229
|
+
const theo_health = { mode: health.mode };
|
|
230
|
+
if (health.build_stamp && health.build_stamp.sha) theo_health.build_sha = health.build_stamp.sha;
|
|
231
|
+
payload.theo_health = theo_health;
|
|
232
|
+
}
|
|
233
|
+
} catch (_e) {
|
|
234
|
+
// Degrade to no theo_health silently -- never changes the verdict.
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const scopeNote = 'this check reads Claude Code\'s ~/.claude.json only; Claude Desktop and Cowork '
|
|
238
|
+
+ 'keep the mindrian-brain connector by design (docs/339-NOTE-theo-desktop-connector-key.md), '
|
|
239
|
+
+ 'so a Desktop config is never read and never flagged';
|
|
240
|
+
|
|
241
|
+
if (shadows.length === 0) {
|
|
242
|
+
let reason = 'no shadowing mindrian-brain entry found; resolved origin=' + resolved_origin
|
|
243
|
+
+ ' is_theo=' + is_theo + (override ? ' (MINDRIAN_BRAIN_URL override active)' : '') + '. ' + scopeNote + '.';
|
|
244
|
+
return { ok: true, reason: reason, payload: payload };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const shadowLines = shadows.map(function (s) {
|
|
248
|
+
return 'shadowing Claude Code MCP entry `mindrian-brain` at ' + s.scope + ' scope (host=' + s.url_host
|
|
249
|
+
+ ') shadows the plugin\'s own stdio shim; the plugin provides this server, so remove the duplicate: '
|
|
250
|
+
+ '`claude mcp remove mindrian-brain -s ' + s.scope + '`';
|
|
251
|
+
});
|
|
252
|
+
const reason = shadowLines.join(' | ') + '. resolved origin=' + resolved_origin + ' is_theo=' + is_theo
|
|
253
|
+
+ '. ' + scopeNote + '.';
|
|
254
|
+
return { ok: false, reason: reason, payload: payload };
|
|
255
|
+
}
|
|
256
|
+
|
|
150
257
|
// L1 -- plugin-root-resolver. Reuses lib/core/active-plugin-root.cjs.
|
|
151
258
|
async function _layer1(opts) {
|
|
152
259
|
const fn = opts.mockResolveRoot
|
|
@@ -413,17 +520,21 @@ async function _layer6(opts) {
|
|
|
413
520
|
}
|
|
414
521
|
|
|
415
522
|
/**
|
|
416
|
-
* Run the
|
|
523
|
+
* Run the 7-layer Brain smoke probe with fail-fast cascade.
|
|
417
524
|
*
|
|
418
525
|
* @param {{
|
|
419
|
-
* mockResolveRoot?:
|
|
420
|
-
* mockResolveKey?:
|
|
421
|
-
* mockSchema?:
|
|
422
|
-
* mockSpawn?:
|
|
423
|
-
* shimPath?:
|
|
424
|
-
* mockBrainUrl?:
|
|
425
|
-
* mockStats?:
|
|
426
|
-
* mockQuery?:
|
|
526
|
+
* mockResolveRoot?: function,
|
|
527
|
+
* mockResolveKey?: function,
|
|
528
|
+
* mockSchema?: function,
|
|
529
|
+
* mockSpawn?: function,
|
|
530
|
+
* shimPath?: string,
|
|
531
|
+
* mockBrainUrl?: function,
|
|
532
|
+
* mockStats?: function,
|
|
533
|
+
* mockQuery?: function,
|
|
534
|
+
* mockScopedServers?: function,
|
|
535
|
+
* mockTheoHealth?: function,
|
|
536
|
+
* claudeConfigPath?: string,
|
|
537
|
+
* projectDir?: string,
|
|
427
538
|
* }} [opts]
|
|
428
539
|
* @returns {Promise<{ok:boolean, layers:Array<{id,name,ok,reason,ms,payload?}>, overall_ms:number}>}
|
|
429
540
|
*/
|
|
@@ -432,7 +543,7 @@ async function checkBrainSmoke(opts) {
|
|
|
432
543
|
const t0 = _now();
|
|
433
544
|
const out = { ok: true, layers: [], overall_ms: 0 };
|
|
434
545
|
let prevOk = true;
|
|
435
|
-
const layerFns = [_layer1, _layer2, _layer3, _layer4, _layer5, _layer6];
|
|
546
|
+
const layerFns = [_layer0, _layer1, _layer2, _layer3, _layer4, _layer5, _layer6];
|
|
436
547
|
for (let i = 0; i < LAYERS.length; i++) {
|
|
437
548
|
const meta = LAYERS[i];
|
|
438
549
|
if (!prevOk) {
|
|
@@ -444,7 +555,18 @@ async function checkBrainSmoke(opts) {
|
|
|
444
555
|
const row = { id: meta.id, name: meta.name, ok: r.ok, reason: r.reason, ms: r.ms };
|
|
445
556
|
if (r.payload !== undefined) row.payload = r.payload;
|
|
446
557
|
out.layers.push(row);
|
|
447
|
-
if (!r.ok) {
|
|
558
|
+
if (!r.ok) {
|
|
559
|
+
out.ok = false;
|
|
560
|
+
// L0 (i === 0) is the one layer whose failure never blinds the
|
|
561
|
+
// cascade: the in-process probe through brain-client.cjs (L1-L6) is
|
|
562
|
+
// still valid and still worth running even when a shadow entry is
|
|
563
|
+
// present, so the report must carry BOTH the shadow finding and the
|
|
564
|
+
// six layers' own verdicts. A short circuit here would trade one
|
|
565
|
+
// blind spot (L1-L6 cannot see a Claude Code config shadow) for
|
|
566
|
+
// another (a shadow finding would hide whether the in-process probe
|
|
567
|
+
// itself is healthy).
|
|
568
|
+
if (i > 0) prevOk = false;
|
|
569
|
+
}
|
|
448
570
|
}
|
|
449
571
|
out.overall_ms = _now() - t0;
|
|
450
572
|
if (out.overall_ms > OVERALL_BUDGET_MS) {
|
|
@@ -458,10 +580,10 @@ async function checkBrainSmoke(opts) {
|
|
|
458
580
|
}
|
|
459
581
|
|
|
460
582
|
/**
|
|
461
|
-
* Class M is diagnostic-only. There is no auto-remediation path: the
|
|
583
|
+
* Class M is diagnostic-only. There is no auto-remediation path: the 7
|
|
462
584
|
* failure surfaces require user action (install / set key / restart /
|
|
463
|
-
* repoint the endpoint at canon
|
|
464
|
-
* classes that DO support --fix.
|
|
585
|
+
* repoint the endpoint at canon / remove a shadowing connector). This
|
|
586
|
+
* function exists for symmetry with classes that DO support --fix.
|
|
465
587
|
*
|
|
466
588
|
* @param {object} _result the checkBrainSmoke result (unused; signature parity)
|
|
467
589
|
* @returns {{fixed: false, reason: string}}
|
|
@@ -476,4 +598,16 @@ function fixBrainSmoke(_result) {
|
|
|
476
598
|
// Phase 257 (LOCUS-01, D-03), Task 2 Arm 5: exported so
|
|
477
599
|
// tests/test-257-refusal-egress-kind.cjs can assert the refusal-vocabulary /
|
|
478
600
|
// doctor-recognizer coupling structurally, without re-declaring this list.
|
|
479
|
-
|
|
601
|
+
// THEO_NODE_FLOOR / CANON_NODE_FLOOR additively exported (quick task
|
|
602
|
+
// 260911-axz) so the floor is assertable without re-typing the number in a
|
|
603
|
+
// test.
|
|
604
|
+
module.exports = {
|
|
605
|
+
checkBrainSmoke,
|
|
606
|
+
LAYERS,
|
|
607
|
+
fixBrainSmoke,
|
|
608
|
+
STDIO_TIMEOUT_MS,
|
|
609
|
+
STRUCTURED_REFUSAL_STATUSES,
|
|
610
|
+
CANON_BRAIN_URL,
|
|
611
|
+
THEO_NODE_FLOOR,
|
|
612
|
+
CANON_NODE_FLOOR,
|
|
613
|
+
};
|