@mindrian_os/cli 2.0.0-beta.33 → 2.0.0-beta.37

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.
@@ -2,14 +2,27 @@
2
2
 
3
3
  /**
4
4
  * Phase 127-02 BRAIN-MCP-127-08 (CONTEXT D4) -- Class M Brain smoke.
5
- * 6-layer composable probe replacing ~60% of doctor Brain-adjacent checks.
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 sense:
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 with
39
- * reason="skipped-prior-layer-failed" so the report points at the FIRST
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 = 1000 is a FLOOR, not a target: Theo's canon is
83
- // actively growing (1,253 nodes observed 2026-09-03, up from 712 the
84
- // previous day), so a tight number would be stale within days.
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 = 1000;
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,105 @@ 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
+ // (d) install_id_present (Quick 260911-iko, T-iko-02): whether the opaque
206
+ // per-install id (lib/core/install-id.cjs) can be read from the state
207
+ // dir. This row carries the BOOLEAN ONLY and NEVER the id value -- it
208
+ // reads through peekInstallId, which never mints, so running doctor
209
+ // can never create the id it is reporting on. A missing id is
210
+ // information, never a failure: it never changes the verdict below,
211
+ // exactly as the origin and health halves already do not.
212
+ //
213
+ // Verdict: ok is false if and only if at least one mindrian-brain shadow
214
+ // entry was found. The origin and health halves are information, never the
215
+ // verdict, exactly as L6 treats canon/override as information separate from
216
+ // its own count-floor verdict.
217
+ async function _layer0(opts) {
218
+ const scopedServersFn = opts.mockScopedServers
219
+ || require('../integration-registry.cjs').readScopedMcpServers;
220
+ const scoped = scopedServersFn({ configPath: opts.claudeConfigPath, projectDir: opts.projectDir }) || [];
221
+ const shadows = scoped
222
+ .filter(function (e) { return e && e.name === 'mindrian-brain'; })
223
+ .map(function (e) { return { name: e.name, scope: e.scope, url_host: e.url_host }; });
224
+
225
+ const brainUrlFn = opts.mockBrainUrl || (() => require('../brain-client.cjs').getBrainUrl());
226
+ const resolved_origin = brainUrlFn();
227
+ const is_theo = THEO_ORIGINS.indexOf(resolved_origin) !== -1;
228
+ const override = !!(process.env.MINDRIAN_BRAIN_URL && process.env.MINDRIAN_BRAIN_URL.length > 0);
229
+
230
+ const payload = { resolved_origin: resolved_origin, is_theo: is_theo, override: override, shadows: shadows };
231
+
232
+ // Quick 260911-iko (D-03, T-iko-02): install_id_present, a BOOLEAN ONLY,
233
+ // never the value. Uses peekInstallId, NOT getInstallId, so a diagnostic
234
+ // run never mints the thing it is reporting on. A missing install id is
235
+ // information, never a failure -- this never changes the verdict below,
236
+ // exactly as the origin and health halves already are.
237
+ try {
238
+ const installIdFn = opts.mockInstallId
239
+ || (() => require('../install-id.cjs').peekInstallId());
240
+ payload.install_id_present = typeof installIdFn() === 'string';
241
+ } catch (_e) {
242
+ payload.install_id_present = false;
243
+ }
244
+
245
+ const theoHealthFn = opts.mockTheoHealth
246
+ || (async () => require('../brain-client.cjs').callTool('theo_health', {}));
247
+ try {
248
+ const health = await theoHealthFn();
249
+ if (health && typeof health === 'object') {
250
+ const theo_health = { mode: health.mode };
251
+ if (health.build_stamp && health.build_stamp.sha) theo_health.build_sha = health.build_stamp.sha;
252
+ payload.theo_health = theo_health;
253
+ }
254
+ } catch (_e) {
255
+ // Degrade to no theo_health silently -- never changes the verdict.
256
+ }
257
+
258
+ const scopeNote = 'this check reads Claude Code\'s ~/.claude.json only; Claude Desktop and Cowork '
259
+ + 'keep the mindrian-brain connector by design (docs/339-NOTE-theo-desktop-connector-key.md), '
260
+ + 'so a Desktop config is never read and never flagged';
261
+
262
+ if (shadows.length === 0) {
263
+ let reason = 'no shadowing mindrian-brain entry found; resolved origin=' + resolved_origin
264
+ + ' is_theo=' + is_theo + (override ? ' (MINDRIAN_BRAIN_URL override active)' : '') + '. ' + scopeNote + '.';
265
+ return { ok: true, reason: reason, payload: payload };
266
+ }
267
+
268
+ const shadowLines = shadows.map(function (s) {
269
+ return 'shadowing Claude Code MCP entry `mindrian-brain` at ' + s.scope + ' scope (host=' + s.url_host
270
+ + ') shadows the plugin\'s own stdio shim; the plugin provides this server, so remove the duplicate: '
271
+ + '`claude mcp remove mindrian-brain -s ' + s.scope + '`';
272
+ });
273
+ const reason = shadowLines.join(' | ') + '. resolved origin=' + resolved_origin + ' is_theo=' + is_theo
274
+ + '. ' + scopeNote + '.';
275
+ return { ok: false, reason: reason, payload: payload };
276
+ }
277
+
150
278
  // L1 -- plugin-root-resolver. Reuses lib/core/active-plugin-root.cjs.
151
279
  async function _layer1(opts) {
152
280
  const fn = opts.mockResolveRoot
@@ -413,17 +541,22 @@ async function _layer6(opts) {
413
541
  }
414
542
 
415
543
  /**
416
- * Run the 6-layer Brain smoke probe with fail-fast cascade.
544
+ * Run the 7-layer Brain smoke probe with fail-fast cascade.
417
545
  *
418
546
  * @param {{
419
- * mockResolveRoot?: function,
420
- * mockResolveKey?: function,
421
- * mockSchema?: function,
422
- * mockSpawn?: function,
423
- * shimPath?: string,
424
- * mockBrainUrl?: function,
425
- * mockStats?: function,
426
- * mockQuery?: function,
547
+ * mockResolveRoot?: function,
548
+ * mockResolveKey?: function,
549
+ * mockSchema?: function,
550
+ * mockSpawn?: function,
551
+ * shimPath?: string,
552
+ * mockBrainUrl?: function,
553
+ * mockStats?: function,
554
+ * mockQuery?: function,
555
+ * mockScopedServers?: function,
556
+ * mockTheoHealth?: function,
557
+ * mockInstallId?: function,
558
+ * claudeConfigPath?: string,
559
+ * projectDir?: string,
427
560
  * }} [opts]
428
561
  * @returns {Promise<{ok:boolean, layers:Array<{id,name,ok,reason,ms,payload?}>, overall_ms:number}>}
429
562
  */
@@ -432,7 +565,7 @@ async function checkBrainSmoke(opts) {
432
565
  const t0 = _now();
433
566
  const out = { ok: true, layers: [], overall_ms: 0 };
434
567
  let prevOk = true;
435
- const layerFns = [_layer1, _layer2, _layer3, _layer4, _layer5, _layer6];
568
+ const layerFns = [_layer0, _layer1, _layer2, _layer3, _layer4, _layer5, _layer6];
436
569
  for (let i = 0; i < LAYERS.length; i++) {
437
570
  const meta = LAYERS[i];
438
571
  if (!prevOk) {
@@ -444,7 +577,18 @@ async function checkBrainSmoke(opts) {
444
577
  const row = { id: meta.id, name: meta.name, ok: r.ok, reason: r.reason, ms: r.ms };
445
578
  if (r.payload !== undefined) row.payload = r.payload;
446
579
  out.layers.push(row);
447
- if (!r.ok) { prevOk = false; out.ok = false; }
580
+ if (!r.ok) {
581
+ out.ok = false;
582
+ // L0 (i === 0) is the one layer whose failure never blinds the
583
+ // cascade: the in-process probe through brain-client.cjs (L1-L6) is
584
+ // still valid and still worth running even when a shadow entry is
585
+ // present, so the report must carry BOTH the shadow finding and the
586
+ // six layers' own verdicts. A short circuit here would trade one
587
+ // blind spot (L1-L6 cannot see a Claude Code config shadow) for
588
+ // another (a shadow finding would hide whether the in-process probe
589
+ // itself is healthy).
590
+ if (i > 0) prevOk = false;
591
+ }
448
592
  }
449
593
  out.overall_ms = _now() - t0;
450
594
  if (out.overall_ms > OVERALL_BUDGET_MS) {
@@ -458,10 +602,10 @@ async function checkBrainSmoke(opts) {
458
602
  }
459
603
 
460
604
  /**
461
- * Class M is diagnostic-only. There is no auto-remediation path: the 6
605
+ * Class M is diagnostic-only. There is no auto-remediation path: the 7
462
606
  * failure surfaces require user action (install / set key / restart /
463
- * repoint the endpoint at canon). This function exists for symmetry with
464
- * classes that DO support --fix.
607
+ * repoint the endpoint at canon / remove a shadowing connector). This
608
+ * function exists for symmetry with classes that DO support --fix.
465
609
  *
466
610
  * @param {object} _result the checkBrainSmoke result (unused; signature parity)
467
611
  * @returns {{fixed: false, reason: string}}
@@ -476,4 +620,16 @@ function fixBrainSmoke(_result) {
476
620
  // Phase 257 (LOCUS-01, D-03), Task 2 Arm 5: exported so
477
621
  // tests/test-257-refusal-egress-kind.cjs can assert the refusal-vocabulary /
478
622
  // doctor-recognizer coupling structurally, without re-declaring this list.
479
- module.exports = { checkBrainSmoke, LAYERS, fixBrainSmoke, STDIO_TIMEOUT_MS, STRUCTURED_REFUSAL_STATUSES, CANON_BRAIN_URL };
623
+ // THEO_NODE_FLOOR / CANON_NODE_FLOOR additively exported (quick task
624
+ // 260911-axz) so the floor is assertable without re-typing the number in a
625
+ // test.
626
+ module.exports = {
627
+ checkBrainSmoke,
628
+ LAYERS,
629
+ fixBrainSmoke,
630
+ STDIO_TIMEOUT_MS,
631
+ STRUCTURED_REFUSAL_STATUSES,
632
+ CANON_BRAIN_URL,
633
+ THEO_NODE_FLOOR,
634
+ CANON_NODE_FLOOR,
635
+ };