@mindrian_os/cli 2.0.0-beta.31 → 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 +86 -0
- package/bin/mindrian-brain-mcp-client.cjs +15 -1
- package/commands/doctor.md +2 -2
- package/data/brain-census.generated.json +2827 -1523
- package/lib/core/brain-client.cjs +320 -20
- package/lib/core/brain-prewarm.cjs +164 -0
- package/lib/core/directive-envelope.cjs +13 -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/core/intel-pipeline.cjs +28 -7
- package/lib/core/part8-egress-guard.cjs +27 -0
- package/lib/mcp/brain-composition-census.cjs +22 -6
- package/lib/mcp/brain-route-bound.cjs +66 -0
- package/lib/mcp/brain-router.cjs +47 -11
- package/lib/mcp/tool-router.cjs +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/references/brain/room-hierarchy-schema.md +11 -5
- package/scripts/build-brain-census.cjs +321 -4
- package/scripts/collect-cold-install-evidence.cjs +411 -0
- package/scripts/doctor.cjs +22 -7
- package/scripts/room-registry +4 -4
- package/scripts/session-start +25 -2
- package/scripts/whitespace-to-brain.cjs +2 -1
- package/skills/doctor/SKILL.md +2 -2
- package/skills/larry-personality/SKILL.md +4 -3
- package/scripts/sync-rooms-brain +0 -438
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* scripts/collect-cold-install-evidence.cjs -- Phase 341 Plan 06 (D-13 step 2).
|
|
6
|
+
*
|
|
7
|
+
* WHAT: emits one JSON envelope proving a cold install of the npm-source
|
|
8
|
+
* plugin artifact landed on this machine and that both bundled MCP servers
|
|
9
|
+
* (mindrian-os, mindrian-brain) load. One command, run identically on
|
|
10
|
+
* Windows, macOS and Linux, so a human operator on a machine this repo
|
|
11
|
+
* cannot reach can paste the result back into a tracked proof document.
|
|
12
|
+
*
|
|
13
|
+
* WHY: there is no Windows box and no macOS box in this dev environment.
|
|
14
|
+
* 341-RESEARCH names that as the phase's one hard external dependency with
|
|
15
|
+
* no fallback (see 341-RESEARCH.md "Loader Ground Truth" F-2/F-3 and
|
|
16
|
+
* assumption A1). The proof this collector produces is evidence, not a
|
|
17
|
+
* claim: the fields below are what the D-13 gate actually reads.
|
|
18
|
+
*
|
|
19
|
+
* Canon Part 8 (Graph Boundary): local filesystem reads plus local spawns of
|
|
20
|
+
* this repo's own scripts (node scripts/doctor.cjs). The ONE network touch
|
|
21
|
+
* in the whole envelope is inherited from `doctor --acceptance`'s full-tier
|
|
22
|
+
* points (the live `npm view` version-of-record check and the brain-smoke
|
|
23
|
+
* wire probe) and from `doctor --eureka-smoke`'s optional model-cache read;
|
|
24
|
+
* this is stated here, not hidden. Nothing in this file opens a socket
|
|
25
|
+
* itself.
|
|
26
|
+
*
|
|
27
|
+
* MCP-server probe note (measured on this Linux dev box, both files read
|
|
28
|
+
* before writing this probe per the plan's own instruction): neither
|
|
29
|
+
* bin/mindrian-mcp-server.cjs nor bin/mindrian-brain-mcp-client.cjs exposes
|
|
30
|
+
* a --version flag or any other CLI probe flag -- both call an unconditional
|
|
31
|
+
* top-level main() that connects a StdioServerTransport. Measured behavior
|
|
32
|
+
* differs from the plan's literal "require() + treat a clean exit as
|
|
33
|
+
* loads:true" fallback: mindrian-brain-mcp-client.cjs happens to exit 0 on
|
|
34
|
+
* stdin EOF, but mindrian-mcp-server.cjs does NOT -- it stays connected to
|
|
35
|
+
* stdio by design (a healthy MCP stdio server never exits on its own) and
|
|
36
|
+
* only stops when killed. A literal "clean exit only" probe would therefore
|
|
37
|
+
* mis-report a HEALTHY mindrian-os server as not-loading. The narrowest
|
|
38
|
+
* non-destructive probe each file actually supports is its own startup log
|
|
39
|
+
* line to stderr (`... MCP server v<version> started ...`), written at the
|
|
40
|
+
* point server.connect() succeeds. This collector spawns the real entry
|
|
41
|
+
* file (not require(), since require() previously start it in-process too),
|
|
42
|
+
* feeds it a closed stdin (input:''), captures stderr up to 2000 characters
|
|
43
|
+
* bounded by an explicit timeout, and treats either a clean exit(0) OR the
|
|
44
|
+
* presence of the entry's own "started" marker in stderr as loads:true. A
|
|
45
|
+
* timeout-kill after a confirmed "started" marker is expected lifecycle for
|
|
46
|
+
* a server that intentionally never exits on inspection, not a failure --
|
|
47
|
+
* documented as a note either way for transparency.
|
|
48
|
+
*
|
|
49
|
+
* Version note: plugin_version is read via lib/core/repo-version.cjs (the
|
|
50
|
+
* ONE sanctioned way to read this repo's own version, never a tree search --
|
|
51
|
+
* see that module's header) when CLAUDE_PLUGIN_ROOT is unset (a dev
|
|
52
|
+
* checkout), and directly from <CLAUDE_PLUGIN_ROOT>/package.json when it is
|
|
53
|
+
* set (an installed copy), per the plan's own instruction.
|
|
54
|
+
*
|
|
55
|
+
* Every probe below is wrapped so a failure produces a recorded `null` (or
|
|
56
|
+
* `false` where the field is boolean-shaped) plus a `notes` entry, never a
|
|
57
|
+
* throw -- an evidence collector that crashes on an unhealthy install is
|
|
58
|
+
* useless precisely when it is needed most.
|
|
59
|
+
*
|
|
60
|
+
* House rule: hyphens only, no em-dashes, no emoji. CJS, process.argv
|
|
61
|
+
* routing.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
const fs = require('node:fs');
|
|
65
|
+
const path = require('node:path');
|
|
66
|
+
const os = require('node:os');
|
|
67
|
+
const { spawnSync } = require('node:child_process');
|
|
68
|
+
|
|
69
|
+
const SCHEMA = 'mos-cold-install-evidence/1';
|
|
70
|
+
|
|
71
|
+
function usage() {
|
|
72
|
+
return 'usage: node scripts/collect-cold-install-evidence.cjs [--json]';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -- small helpers -----------------------------------------------------
|
|
76
|
+
|
|
77
|
+
function safe(notes, label, fn, fallback) {
|
|
78
|
+
try {
|
|
79
|
+
return fn();
|
|
80
|
+
} catch (e) {
|
|
81
|
+
notes.push(label + ': ' + (e && e.message ? e.message : String(e)));
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function existsSafe(p) {
|
|
87
|
+
try {
|
|
88
|
+
return !!p && fs.existsSync(p);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function readJsonSafe(p) {
|
|
95
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// countInstalledPackages(nodeModulesDir): counts installed packages two
|
|
99
|
+
// levels deep for a scoped entry (@scope/name is ONE package, not two), one
|
|
100
|
+
// level deep otherwise. Skips dotfiles (.bin, .package-lock.json).
|
|
101
|
+
function countInstalledPackages(nodeModulesDir) {
|
|
102
|
+
let count = 0;
|
|
103
|
+
const top = fs.readdirSync(nodeModulesDir, { withFileTypes: true });
|
|
104
|
+
for (const entry of top) {
|
|
105
|
+
if (!entry.isDirectory()) continue;
|
|
106
|
+
if (entry.name.startsWith('.')) continue;
|
|
107
|
+
if (entry.name.startsWith('@')) {
|
|
108
|
+
const scopeDir = path.join(nodeModulesDir, entry.name);
|
|
109
|
+
let scoped;
|
|
110
|
+
try {
|
|
111
|
+
scoped = fs.readdirSync(scopeDir, { withFileTypes: true });
|
|
112
|
+
} catch {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const s of scoped) {
|
|
116
|
+
if (s.isDirectory()) count += 1;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
count += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// longestRelativePath(rootDir): walks rootDir recursively (bounded, symlinks
|
|
126
|
+
// NOT followed to avoid a cycle) and returns { length, path } of the longest
|
|
127
|
+
// path relative to rootDir. Returns null on an unreadable root.
|
|
128
|
+
function longestRelativePath(rootDir) {
|
|
129
|
+
let best = { length: 0, path: '' };
|
|
130
|
+
const stack = [rootDir];
|
|
131
|
+
while (stack.length) {
|
|
132
|
+
const dir = stack.pop();
|
|
133
|
+
let entries;
|
|
134
|
+
try {
|
|
135
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
const abs = path.join(dir, entry.name);
|
|
141
|
+
const rel = path.relative(rootDir, abs);
|
|
142
|
+
if (rel.length > best.length) best = { length: rel.length, path: rel };
|
|
143
|
+
if (entry.isSymbolicLink()) continue;
|
|
144
|
+
if (entry.isDirectory()) stack.push(abs);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return best;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// probeMcpEntry(entryPath, notes, id): spawns the entry file directly with a
|
|
151
|
+
// closed stdin, capped stderr and a bounded timeout. loads:true when either
|
|
152
|
+
// the process exits cleanly OR its own startup marker appears in stderr
|
|
153
|
+
// before the timeout kills it (see the header comment for why).
|
|
154
|
+
function probeMcpEntry(entryPath, id, notes) {
|
|
155
|
+
if (!existsSafe(entryPath)) {
|
|
156
|
+
return { loads: false };
|
|
157
|
+
}
|
|
158
|
+
const scratchCwd = safe(notes, 'mcp-probe-' + id + '-scratch-dir', function () {
|
|
159
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'mos-mcp-probe-'));
|
|
160
|
+
}, os.tmpdir());
|
|
161
|
+
const result = spawnSync(process.execPath, [entryPath], {
|
|
162
|
+
cwd: scratchCwd,
|
|
163
|
+
encoding: 'utf8',
|
|
164
|
+
timeout: 5000,
|
|
165
|
+
input: '',
|
|
166
|
+
});
|
|
167
|
+
const stderrTail = (result.stderr || '').slice(0, 2000);
|
|
168
|
+
const startedMarker = /started/i.test(stderrTail);
|
|
169
|
+
const cleanExit = result.status === 0 && !result.signal;
|
|
170
|
+
if (result.signal) {
|
|
171
|
+
notes.push('mcp-probe-' + id + ': killed by signal ' + result.signal + ' after timeout' +
|
|
172
|
+
(startedMarker ? ' (startup marker seen first -- expected lifecycle for a server that never exits on inspection)' : ' (no startup marker seen -- probe inconclusive)'));
|
|
173
|
+
}
|
|
174
|
+
if (result.error) {
|
|
175
|
+
notes.push('mcp-probe-' + id + ': spawn error: ' + result.error.message);
|
|
176
|
+
}
|
|
177
|
+
return { loads: !!(cleanExit || startedMarker) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// spawnDoctorJson(pluginRoot, args, notes, label): spawns
|
|
181
|
+
// `node <pluginRoot>/scripts/doctor.cjs <args> --json`, and extracts the
|
|
182
|
+
// trailing JSON block from stdout (doctor.cjs's --acceptance path prints
|
|
183
|
+
// human-readable lines before the final JSON.stringify block; --eureka-smoke
|
|
184
|
+
// prints JSON only). Returns the parsed object, or null with a notes entry.
|
|
185
|
+
function spawnDoctorJson(pluginRoot, args, notes, label, timeoutMs) {
|
|
186
|
+
const doctorPath = path.join(pluginRoot, 'scripts', 'doctor.cjs');
|
|
187
|
+
if (!existsSafe(doctorPath)) {
|
|
188
|
+
notes.push(label + ': doctor.cjs not found at ' + doctorPath);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const result = spawnSync(process.execPath, [doctorPath].concat(args).concat(['--json']), {
|
|
192
|
+
cwd: pluginRoot,
|
|
193
|
+
encoding: 'utf8',
|
|
194
|
+
timeout: timeoutMs,
|
|
195
|
+
input: '',
|
|
196
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
197
|
+
});
|
|
198
|
+
if (result.error) {
|
|
199
|
+
notes.push(label + ': spawn error: ' + result.error.message);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (result.signal) {
|
|
203
|
+
notes.push(label + ': killed by signal ' + result.signal + ' (timeout ' + timeoutMs + 'ms exceeded)');
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
const stdout = result.stdout || '';
|
|
207
|
+
const lines = stdout.split('\n');
|
|
208
|
+
const startIdx = lines.findIndex(function (l) { return l.trim() === '{'; });
|
|
209
|
+
if (startIdx === -1) {
|
|
210
|
+
notes.push(label + ': no JSON block located in stdout (exit code ' + result.status + ')');
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const jsonText = lines.slice(startIdx).join('\n');
|
|
214
|
+
try {
|
|
215
|
+
return JSON.parse(jsonText);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
notes.push(label + ': JSON.parse failed: ' + e.message);
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// -- the collector -------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function collectEvidence(opts) {
|
|
225
|
+
opts = opts || {};
|
|
226
|
+
const notes = [];
|
|
227
|
+
const home = os.homedir();
|
|
228
|
+
|
|
229
|
+
const pluginRootEnv = process.env.CLAUDE_PLUGIN_ROOT || null;
|
|
230
|
+
// baseRoot: the root this collector treats as "the plugin" for every
|
|
231
|
+
// repo-relative read below (doctor.cjs, .mcp.json). Falls back to this
|
|
232
|
+
// script's own repo when CLAUDE_PLUGIN_ROOT is unset (a dev checkout).
|
|
233
|
+
const repoRoot = path.resolve(__dirname, '..');
|
|
234
|
+
const baseRoot = pluginRootEnv && existsSafe(pluginRootEnv) ? pluginRootEnv : repoRoot;
|
|
235
|
+
if (pluginRootEnv && !existsSafe(pluginRootEnv)) {
|
|
236
|
+
notes.push('CLAUDE_PLUGIN_ROOT is set to a non-existent path (' + pluginRootEnv + '); falling back to this script\'s own repo root for local reads');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// -- claude_code_version --
|
|
240
|
+
const claudeCodeVersion = safe(notes, 'claude_code_version', function () {
|
|
241
|
+
const r = spawnSync('claude', ['--version'], { encoding: 'utf8', timeout: 5000, input: '' });
|
|
242
|
+
if (r.error) throw r.error;
|
|
243
|
+
if (r.status !== 0) throw new Error('exit ' + r.status);
|
|
244
|
+
const m = /(\d+\.\d+\.\d+)/.exec((r.stdout || '') + (r.stderr || ''));
|
|
245
|
+
if (!m) throw new Error('could not parse a version from claude --version output');
|
|
246
|
+
return m[1];
|
|
247
|
+
}, null);
|
|
248
|
+
|
|
249
|
+
// -- plugin_version (per the plan: repo-version.cjs from a checkout, the
|
|
250
|
+
// installed package.json from an install; never a tree search) --
|
|
251
|
+
let pluginVersion = null;
|
|
252
|
+
if (pluginRootEnv) {
|
|
253
|
+
pluginVersion = safe(notes, 'plugin_version', function () {
|
|
254
|
+
const pkg = readJsonSafe(path.join(pluginRootEnv, 'package.json'));
|
|
255
|
+
if (!pkg.version) throw new Error('package.json at CLAUDE_PLUGIN_ROOT has no version field');
|
|
256
|
+
return pkg.version;
|
|
257
|
+
}, null);
|
|
258
|
+
} else {
|
|
259
|
+
pluginVersion = safe(notes, 'plugin_version', function () {
|
|
260
|
+
const { readRepoVersion } = require(path.join(repoRoot, 'lib', 'core', 'repo-version.cjs'));
|
|
261
|
+
return readRepoVersion().version;
|
|
262
|
+
}, null);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// -- marketplace_source: the plugins[].source object for "mos" from the
|
|
266
|
+
// cached marketplace catalog Claude Code itself reads from. --
|
|
267
|
+
const marketplaceCatalogPath = path.join(home, '.claude', 'plugins', 'marketplaces', 'mindrian-marketplace', '.claude-plugin', 'marketplace.json');
|
|
268
|
+
const marketplaceSource = safe(notes, 'marketplace_source', function () {
|
|
269
|
+
const catalog = readJsonSafe(marketplaceCatalogPath);
|
|
270
|
+
const entry = Array.isArray(catalog.plugins) ? catalog.plugins.find(function (p) { return p.name === 'mos'; }) : null;
|
|
271
|
+
if (!entry) throw new Error('no "mos" entry in ' + marketplaceCatalogPath);
|
|
272
|
+
if (!entry.source) throw new Error('"mos" entry has no source field');
|
|
273
|
+
return entry.source;
|
|
274
|
+
}, null);
|
|
275
|
+
|
|
276
|
+
// -- npm_cache_present --
|
|
277
|
+
const npmCachePath = path.join(home, '.claude', 'plugins', 'npm-cache');
|
|
278
|
+
const npmCachePresent = existsSafe(npmCachePath);
|
|
279
|
+
|
|
280
|
+
// -- cache_dir / node_modules_present / completion_record_present /
|
|
281
|
+
// installed_package_count / longest_relative_path --
|
|
282
|
+
let cacheDir = null;
|
|
283
|
+
let nodeModulesPresent = false;
|
|
284
|
+
let completionRecordPresent = false;
|
|
285
|
+
let installedPackageCount = 0;
|
|
286
|
+
let longestPath = { length: 0, path: '' };
|
|
287
|
+
if (pluginVersion) {
|
|
288
|
+
cacheDir = path.join(home, '.claude', 'plugins', 'cache', 'mindrian-marketplace', 'mos', pluginVersion);
|
|
289
|
+
if (existsSafe(cacheDir)) {
|
|
290
|
+
const nodeModulesDir = path.join(cacheDir, 'node_modules');
|
|
291
|
+
nodeModulesPresent = existsSafe(nodeModulesDir);
|
|
292
|
+
completionRecordPresent = existsSafe(path.join(nodeModulesDir, '.package-lock.json'));
|
|
293
|
+
if (nodeModulesPresent) {
|
|
294
|
+
installedPackageCount = safe(notes, 'installed_package_count', function () {
|
|
295
|
+
return countInstalledPackages(nodeModulesDir);
|
|
296
|
+
}, 0);
|
|
297
|
+
}
|
|
298
|
+
longestPath = safe(notes, 'longest_relative_path', function () {
|
|
299
|
+
return longestRelativePath(cacheDir);
|
|
300
|
+
}, { length: 0, path: '' });
|
|
301
|
+
} else {
|
|
302
|
+
notes.push('cache_dir does not exist yet: ' + cacheDir);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
notes.push('cache_dir could not be computed: plugin_version is unknown');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// -- mcp_servers: probe both .mcp.json entries --
|
|
309
|
+
const mcpJsonPath = path.join(baseRoot, '.mcp.json');
|
|
310
|
+
let mcpServers = [];
|
|
311
|
+
const mcpConfig = safe(notes, 'mcp_servers', function () { return readJsonSafe(mcpJsonPath); }, null);
|
|
312
|
+
if (mcpConfig && mcpConfig.mcpServers) {
|
|
313
|
+
mcpServers = Object.keys(mcpConfig.mcpServers).map(function (id) {
|
|
314
|
+
const def = mcpConfig.mcpServers[id];
|
|
315
|
+
const rawArg = Array.isArray(def.args) && def.args.length ? def.args[0] : '';
|
|
316
|
+
const entryPath = rawArg.replace('${CLAUDE_PLUGIN_ROOT}', baseRoot);
|
|
317
|
+
const entryExists = existsSafe(entryPath);
|
|
318
|
+
const probe = probeMcpEntry(entryPath, id, notes);
|
|
319
|
+
return { id: id, entry_path: entryPath, entry_exists: entryExists, loads: entryExists && probe.loads };
|
|
320
|
+
});
|
|
321
|
+
} else {
|
|
322
|
+
notes.push('mcp_servers: could not read ' + mcpJsonPath + ' (or it has no mcpServers block)');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// -- doctor_acceptance (full tier: the one network touch this envelope
|
|
326
|
+
// documents, per the header comment) --
|
|
327
|
+
const doctorAcceptanceRaw = spawnDoctorJson(baseRoot, ['--acceptance'], notes, 'doctor_acceptance', 180000);
|
|
328
|
+
const doctorAcceptance = doctorAcceptanceRaw
|
|
329
|
+
? { ok: Array.isArray(doctorAcceptanceRaw.failed_points) && doctorAcceptanceRaw.failed_points.length === 0, failed_point_ids: doctorAcceptanceRaw.failed_points || [] }
|
|
330
|
+
: { ok: false, failed_point_ids: [] };
|
|
331
|
+
|
|
332
|
+
// -- eureka_smoke --
|
|
333
|
+
const eurekaSmokeRaw = spawnDoctorJson(baseRoot, ['--eureka-smoke'], notes, 'eureka_smoke', 60000);
|
|
334
|
+
const eurekaSmoke = eurekaSmokeRaw
|
|
335
|
+
? {
|
|
336
|
+
ok: !!eurekaSmokeRaw.ok,
|
|
337
|
+
layers: Array.isArray(eurekaSmokeRaw.layers)
|
|
338
|
+
? eurekaSmokeRaw.layers.map(function (l) { return { id: l.id, ok: !!l.ok, advisory: !!l.advisory }; })
|
|
339
|
+
: [],
|
|
340
|
+
}
|
|
341
|
+
: { ok: false, layers: [] };
|
|
342
|
+
|
|
343
|
+
const envelope = {
|
|
344
|
+
schema: SCHEMA,
|
|
345
|
+
collected_at: new Date().toISOString(),
|
|
346
|
+
platform: process.platform,
|
|
347
|
+
arch: process.arch,
|
|
348
|
+
node: process.version,
|
|
349
|
+
os_release: os.release(),
|
|
350
|
+
claude_code_version: claudeCodeVersion,
|
|
351
|
+
plugin_root: pluginRootEnv,
|
|
352
|
+
plugin_version: pluginVersion,
|
|
353
|
+
marketplace_source: marketplaceSource,
|
|
354
|
+
npm_cache_present: npmCachePresent,
|
|
355
|
+
cache_dir: cacheDir,
|
|
356
|
+
node_modules_present: nodeModulesPresent,
|
|
357
|
+
completion_record_present: completionRecordPresent,
|
|
358
|
+
installed_package_count: installedPackageCount,
|
|
359
|
+
mcp_servers: mcpServers,
|
|
360
|
+
doctor_acceptance: doctorAcceptance,
|
|
361
|
+
eureka_smoke: eurekaSmoke,
|
|
362
|
+
longest_relative_path: longestPath,
|
|
363
|
+
first_install_seconds: opts.firstInstallSeconds != null ? opts.firstInstallSeconds : null,
|
|
364
|
+
notes: opts.firstInstallSeconds == null
|
|
365
|
+
? notes.concat(['first_install_seconds is null by default -- the human operator records the wall-clock seconds the "claude plugin update mos@mindrian-marketplace" command took, per 341-RESEARCH Open Question 1'])
|
|
366
|
+
: notes,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
return envelope;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// -- CLI -------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
function main() {
|
|
375
|
+
const args = process.argv.slice(2);
|
|
376
|
+
let json = false;
|
|
377
|
+
for (const arg of args) {
|
|
378
|
+
if (arg === '--json') { json = true; continue; }
|
|
379
|
+
process.stderr.write(usage() + '\n');
|
|
380
|
+
process.exit(2);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const envelope = collectEvidence({});
|
|
385
|
+
|
|
386
|
+
if (json) {
|
|
387
|
+
process.stdout.write(JSON.stringify(envelope) + '\n');
|
|
388
|
+
process.exit(0);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log('Cold-install evidence (' + envelope.schema + ')');
|
|
393
|
+
console.log(' platform: ' + envelope.platform + ' / ' + envelope.arch + ' / node ' + envelope.node);
|
|
394
|
+
console.log(' plugin_version: ' + envelope.plugin_version);
|
|
395
|
+
console.log(' marketplace_source: ' + JSON.stringify(envelope.marketplace_source));
|
|
396
|
+
console.log(' node_modules_present: ' + envelope.node_modules_present + ' completion_record_present: ' + envelope.completion_record_present);
|
|
397
|
+
console.log(' installed_package_count: ' + envelope.installed_package_count);
|
|
398
|
+
console.log(' mcp_servers: ' + envelope.mcp_servers.map(function (s) { return s.id + '=' + (s.loads ? 'loads' : 'no-load'); }).join(', '));
|
|
399
|
+
console.log(' doctor_acceptance.ok: ' + envelope.doctor_acceptance.ok);
|
|
400
|
+
console.log(' eureka_smoke.ok: ' + envelope.eureka_smoke.ok);
|
|
401
|
+
console.log(' longest_relative_path: ' + envelope.longest_relative_path.length + ' chars');
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log(JSON.stringify(envelope, null, 2));
|
|
404
|
+
process.exit(0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = { collectEvidence };
|
|
408
|
+
|
|
409
|
+
if (require.main === module) {
|
|
410
|
+
main();
|
|
411
|
+
}
|
package/scripts/doctor.cjs
CHANGED
|
@@ -371,9 +371,10 @@ Class flags (combine freely; --all activates them all):
|
|
|
371
371
|
transcripts at ~/.claude/projects/.../*.jsonl for /mos:<deprecated>
|
|
372
372
|
patterns; surfaces a per-command "use /mos:<new> instead" hint.
|
|
373
373
|
Phase 121.5-08 Sub-plan J. LOCAL-only, zero network.)
|
|
374
|
-
--brain-smoke class M (Brain end-to-end smoke:
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
--brain-smoke class M (Brain end-to-end smoke: 7-layer probe -- origin and
|
|
375
|
+
shadow connector (quick 260911-axz), plugin root, key resolver,
|
|
376
|
+
HTTPS schema, MCP stdio handshake, e2e brain_schema, store identity
|
|
377
|
+
(stale-replica detection, quick 260819-c9b).
|
|
377
378
|
Diagnostic-only; reports the exact failing layer. Phase 127-02.)
|
|
378
379
|
--eureka-smoke class S (Eureka local-embedding-stack smoke: 4-layer probe --
|
|
379
380
|
deps present, vec backend, model cache, graceful degrade.
|
|
@@ -4198,15 +4199,17 @@ async function classMBrainSmoke(flags) {
|
|
|
4198
4199
|
if (flags.json) {
|
|
4199
4200
|
console.log(JSON.stringify(Object.assign({ class: 'M' }, result), null, 2));
|
|
4200
4201
|
} else {
|
|
4201
|
-
console.log('Class M -- Brain end-to-end smoke (
|
|
4202
|
+
console.log('Class M -- Brain end-to-end smoke (7-layer probe)');
|
|
4202
4203
|
console.log(' Overall: ' + (result.ok ? 'PASS' : 'FAIL') + ' (' + result.overall_ms + 'ms)');
|
|
4203
4204
|
for (const layer of result.layers) {
|
|
4204
4205
|
const marker = layer.ok ? 'PASS' : 'FAIL';
|
|
4205
4206
|
console.log(' [' + marker + '] ' + layer.name + ' -- ' + layer.reason + ' (' + layer.ms + 'ms)');
|
|
4206
4207
|
// Handoff section 7 item g: report which endpoint the wire resolved
|
|
4207
|
-
// to and whether it is canon.
|
|
4208
|
-
//
|
|
4209
|
-
|
|
4208
|
+
// to and whether it is canon. Branches on layer.id so the pre-existing
|
|
4209
|
+
// store_identity shape stays byte-identical and the new origin_shadow
|
|
4210
|
+
// shape (quick task 260911-axz) gets its own rendering instead of
|
|
4211
|
+
// printing "endpoint=undefined node_count=undefined canon=undefined".
|
|
4212
|
+
if (layer.payload && layer.id === 'store_identity') {
|
|
4210
4213
|
const p = layer.payload;
|
|
4211
4214
|
console.log(' endpoint=' + p.endpoint + ' node_count=' + p.node_count
|
|
4212
4215
|
+ ' canon=' + p.canon + (p.override ? ' override=true' : ''));
|
|
@@ -4217,6 +4220,18 @@ async function classMBrainSmoke(flags) {
|
|
|
4217
4220
|
if (p.stamp.refreshed_at != null) stampParts.push('refreshed_at=' + p.stamp.refreshed_at);
|
|
4218
4221
|
console.log(' GraphRagMeta stamp: ' + stampParts.join(' '));
|
|
4219
4222
|
}
|
|
4223
|
+
} else if (layer.payload && layer.id === 'origin_shadow') {
|
|
4224
|
+
const p = layer.payload;
|
|
4225
|
+
console.log(' origin=' + p.resolved_origin + ' theo=' + p.is_theo
|
|
4226
|
+
+ (p.override ? ' override=true' : ''));
|
|
4227
|
+
if (p.theo_health) {
|
|
4228
|
+
console.log(' theo_health mode=' + p.theo_health.mode
|
|
4229
|
+
+ (p.theo_health.build_sha != null ? ' sha=' + p.theo_health.build_sha : ''));
|
|
4230
|
+
}
|
|
4231
|
+
for (const s of (p.shadows || [])) {
|
|
4232
|
+
console.log(' shadow: scope=' + s.scope + ' host=' + s.url_host
|
|
4233
|
+
+ ' fix=`claude mcp remove mindrian-brain -s ' + s.scope + '`');
|
|
4234
|
+
}
|
|
4220
4235
|
}
|
|
4221
4236
|
}
|
|
4222
4237
|
}
|
package/scripts/room-registry
CHANGED
|
@@ -332,9 +332,9 @@ print(json.dumps(reg['rooms'][name], indent=2))
|
|
|
332
332
|
*) REAL_ROOMDIR="${ROOMS_HOME}/${RPATH}" ;;
|
|
333
333
|
esac
|
|
334
334
|
_write_current_room "${REAL_ROOMDIR}" "${NAME}"
|
|
335
|
-
# Fire-and-forget graph sync (D-16: never blocks)
|
|
335
|
+
# Fire-and-forget local SQLite graph sync (D-16: never blocks). The Brain half
|
|
336
|
+
# was removed 2026-09-10 as a Canon Part 8 breach (quick task 260910-h32).
|
|
336
337
|
node "${SCRIPT_DIR}/sync-rooms-graph" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
337
|
-
node "${SCRIPT_DIR}/sync-rooms-brain" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
338
338
|
;;
|
|
339
339
|
|
|
340
340
|
read)
|
|
@@ -597,9 +597,9 @@ with open(tmp, 'w') as f:
|
|
|
597
597
|
os.replace(tmp, reg_file)
|
|
598
598
|
print('archived')
|
|
599
599
|
" "$REGISTRY_FILE" "$NAME"
|
|
600
|
-
# Fire-and-forget graph sync (D-16: never blocks)
|
|
600
|
+
# Fire-and-forget local SQLite graph sync (D-16: never blocks). The Brain half
|
|
601
|
+
# was removed 2026-09-10 as a Canon Part 8 breach (quick task 260910-h32).
|
|
601
602
|
node "${SCRIPT_DIR}/sync-rooms-graph" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
602
|
-
node "${SCRIPT_DIR}/sync-rooms-brain" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
603
603
|
;;
|
|
604
604
|
|
|
605
605
|
get-active)
|
package/scripts/session-start
CHANGED
|
@@ -369,10 +369,11 @@ Per Canon Part 10 sub-claim 1, conversation IS the surface; commands are interna
|
|
|
369
369
|
context=""
|
|
370
370
|
|
|
371
371
|
if [ -d "$ROOM_DIR" ]; then
|
|
372
|
-
# Fire-and-forget room hierarchy graph sync (Phase 59.2, D-16: never
|
|
372
|
+
# Fire-and-forget local SQLite room hierarchy graph sync (Phase 59.2, D-16: never
|
|
373
|
+
# blocks). The Brain half was removed 2026-09-10 as a Canon Part 8 breach (quick
|
|
374
|
+
# task 260910-h32).
|
|
373
375
|
ROOMS_HOME="${MINDRIAN_ROOMS_HOME:-$HOME/MindrianRooms}"
|
|
374
376
|
node "${SCRIPT_DIR}/sync-rooms-graph" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
375
|
-
node "${SCRIPT_DIR}/sync-rooms-brain" "$ROOMS_HOME" >/dev/null 2>&1 &
|
|
376
377
|
|
|
377
378
|
# =============================================================================
|
|
378
379
|
# CONTEXT INTELLIGENCE: Archetype detection + tiered loading (CTX-01, CTX-02, CTX-05)
|
|
@@ -1909,6 +1910,28 @@ fi
|
|
|
1909
1910
|
unset _BRAIN_KEY_JSON _BRAIN_AVAILABLE _BRAIN_REASON
|
|
1910
1911
|
# --- END Brain status ---
|
|
1911
1912
|
|
|
1913
|
+
# ---------------------------------------------------------------------------
|
|
1914
|
+
# Quick 260911-ddd (DDD-02): CLI-only pre-warm extra, never the only path.
|
|
1915
|
+
#
|
|
1916
|
+
# The MCP shim (bin/mindrian-brain-mcp-client.cjs) already fires this same
|
|
1917
|
+
# probe at startup on all three surfaces (CLI, Desktop, Cowork) because
|
|
1918
|
+
# .mcp.json registers it with alwaysLoad: true -- that is the one
|
|
1919
|
+
# surface-neutral pre-warm point. This spawn is a CLI-only belt-and-braces
|
|
1920
|
+
# extra: session-start fires earlier in the CLI turn than the shim
|
|
1921
|
+
# necessarily connects, so firing it here too shortens the CLI cold window
|
|
1922
|
+
# further without duplicating any state (both calls write the same marker
|
|
1923
|
+
# idempotently).
|
|
1924
|
+
#
|
|
1925
|
+
# This is NOT the egress quick 260910-h32 removed: that was a brain_write of
|
|
1926
|
+
# room data. This is a content-free theo_health READ with no arguments and
|
|
1927
|
+
# no room bytes -- it reintroduces no Brain write. Detached, guarded idiom
|
|
1928
|
+
# (matches the existing `( node ... >/dev/null 2>&1 || true ) &` pattern
|
|
1929
|
+
# used elsewhere in this file, e.g. the feynman-minto-guardian spawn below).
|
|
1930
|
+
# Deliberately NOT inside the `if [ -d "$ROOM_DIR" ]` guard: pre-warm has
|
|
1931
|
+
# nothing to do with a room.
|
|
1932
|
+
# ---------------------------------------------------------------------------
|
|
1933
|
+
( node "${PLUGIN_ROOT}/lib/core/brain-prewarm.cjs" >/dev/null 2>&1 || true ) &
|
|
1934
|
+
|
|
1912
1935
|
# ---------------------------------------------------------------------------
|
|
1913
1936
|
# 260517-dcw dogfood-bridge drain (Canon Part 6 Product-as-Venture)
|
|
1914
1937
|
# ---------------------------------------------------------------------------
|
|
@@ -37,7 +37,8 @@ const path = require('path');
|
|
|
37
37
|
const crypto = require('crypto');
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// Brain client (lazy-loaded, fire-and-forget pattern
|
|
40
|
+
// Brain client (lazy-loaded, fire-and-forget pattern, same style as the
|
|
41
|
+
// retired room-hierarchy sync writer removed 2026-09-10, quick task 260910-h32)
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
|
|
43
44
|
const PLUGIN_ROOT = path.resolve(__dirname, '..');
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -76,7 +76,7 @@ Every check has a stable class letter (or a registry id). The class flag that ac
|
|
|
76
76
|
- **class J -- deployment-surfaces** (shares `--install-state`; `--fix` supported) -- reconciles every owned surface in `data/deployment-surfaces.json` against disk. Reads topology / active-root / active-version from class I's same-invocation result (self-derives via `shared.cjs` when absent). `--fix` re-stamps `ok:false` session-start-owned surfaces and prunes the marketplace cache.
|
|
77
77
|
- **class K -- stale-first-touch-copy** (`--stale-first-touch`; check-only) -- greeting surfaces declared by `data/first-touch-surfaces.json` (banner, splash, onboard, sessionstart, operator-update, larry-extended) scanned for stale version literals and U+2014 em-dash violations. SEED-007 absorption. Also activated by `--all`.
|
|
78
78
|
- **class L -- deprecated-usage** (`--deprecated-usage`; check-only) -- scans the last 7 days of `~/.claude/projects/.../*.jsonl` session transcripts for `/mos:<deprecated>` patterns and surfaces a per-command "use `/mos:<new>` instead" hint. Pure LOCAL scan; zero network, zero Brain. Also activated by `--all`.
|
|
79
|
-
- **class M -- brain-smoke** (`--brain-smoke`; check-only) --
|
|
79
|
+
- **class M -- brain-smoke** (`--brain-smoke`; check-only) -- 7-layer Brain end-to-end probe: layer 0 origin and shadow connector (quick task 260911-axz -- resolved Brain origin, Theo status, `MINDRIAN_BRAIN_URL` override, Theo's live mode/build sha, and any Claude Code `~/.claude.json` entry shadowing the plugin's own `mindrian-brain` shim), then plugin root resolver, key resolver, HTTPS schema, MCP stdio handshake, e2e brain_schema via the bundled shim, and store identity (stale-replica detection). Diagnostic-only; reports the exact failing layer. This is an async runner and stays special-cased (carve-out, below). Activated by `--all`.
|
|
80
80
|
- **class N -- plugin-enabled-state** (no flag; bare run + `--all`; check-only) -- the silent-disable watchdog (DRIFT-12). Reads `~/.claude/settings.json` enabledPlugins + installed_plugins.json; installed && enabled===false is CRITICAL with a re-enable hint. It runs on a bare run and under `--all` precisely because a DISABLED plugin cannot fire its own SessionStart hooks to report itself. LOCAL read only; never writes settings.json.
|
|
81
81
|
- **class P / Q / R -- drift** (`--drift`; opt-in, NOT in `--all`) -- class P is prose-vs-code drift (skill-vs-code + first-touch, report-only even under `--fix`), class Q is gsd-record drift (shells out to `gsd-tools validate health`, parses W007 ROADMAP gaps + I001 missing SUMMARYs; `--fix` writes the DRIFT.md baseline and stubs missing SUMMARYs), class R is runtime-reachability drift (FAILS NON-ZERO when a capability is WIRED in the connector registry but UNREACHABLE by `decide()` at runtime). LOCAL-only, zero network.
|
|
82
82
|
- **class S -- eureka-smoke** (`--eureka-smoke`; check-only) -- 4-layer Eureka local-embedding-stack probe (deps present, vec backend, model cache, graceful degrade). Non-cascading; never downloads a model unless `MINDRIAN_EUREKA_SMOKE_ALLOW_DOWNLOAD=1`; rolls into `--acceptance`. Another async carve-out (below). `--fix eureka` installs the local embedding stack into `~/.mindrian/eureka-deps/` (one-time, about 380 MB), the same action as `/mos:eureka enable`.
|
|
@@ -102,7 +102,7 @@ Look at the user's invocation:
|
|
|
102
102
|
- `/mos:doctor --install-state` -> class I (install-state + topology + 6-way version-of-record) + class J (deployment-surface reconciliation)
|
|
103
103
|
- `/mos:doctor --stale-first-touch` -> class K
|
|
104
104
|
- `/mos:doctor --deprecated-usage` -> class L
|
|
105
|
-
- `/mos:doctor --brain-smoke` -> class M (
|
|
105
|
+
- `/mos:doctor --brain-smoke` -> class M (7-layer Brain probe)
|
|
106
106
|
- `/mos:doctor --eureka-smoke` -> class S (4-layer Eureka probe; opt-in)
|
|
107
107
|
- `/mos:doctor --drift` -> class P + class Q + class R (opt-in; NOT in `--all`)
|
|
108
108
|
- `/mos:doctor --report-registration-bug` -> READ-ONLY escalation reporter (below). NOT a class flag, NOT part of `--all`, NOT a `--fix`.
|
|
@@ -395,9 +395,10 @@ into Larry's voice for the thin-coverage case. Decision #8 already settled the d
|
|
|
395
395
|
subsection is the missing wiring, not a new rule.
|
|
396
396
|
|
|
397
397
|
The trigger is a SIGNAL, never a fixed roster of command names. Any brain_* result carrying one
|
|
398
|
-
of these shapes fires the clause:
|
|
399
|
-
|
|
400
|
-
|
|
398
|
+
of these shapes fires the clause: the `brain_ask` tool's own description now says its
|
|
399
|
+
DirectiveEnvelope's `directive` and `next_gate` are composed from the graph-grounded rows behind
|
|
400
|
+
the answer, and that an empty `grounding.rows` means thin footing, not a clean answer -- an empty
|
|
401
|
+
`grounding.rows` is exactly that signal; a `normalize_framework_name` result with zero
|
|
401
402
|
canonical matches or with multiple ambiguous ones (the healthy floor is exactly one match); a low
|
|
402
403
|
`orchestration_readiness` `readiness_score` (the floor is 3). Any low-confidence signal counts,
|
|
403
404
|
including ones from tools not named here -- this is signal-driven, not a hardcoded list.
|