@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
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Integration Registry
|
|
4
|
+
* Integration Registry - detects available integrations from env, MCP config, filesystem.
|
|
5
5
|
*
|
|
6
6
|
* Zero npm dependencies. Pure Node.js built-ins only (Phase 10 pattern).
|
|
7
7
|
*
|
|
8
8
|
* Exports:
|
|
9
|
-
* INTEGRATION_CATALOG
|
|
10
|
-
* detectIntegrations(options)
|
|
11
|
-
* checkIntegration(name)
|
|
12
|
-
* getContextTriggers(userMessage, roomState)
|
|
9
|
+
* INTEGRATION_CATALOG - all known integrations with detection config
|
|
10
|
+
* detectIntegrations(options) - scan all integrations, return structured status
|
|
11
|
+
* checkIntegration(name) - single integration check
|
|
12
|
+
* getContextTriggers(userMessage, roomState) - suggest integrations based on context
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Catalog of all known integrations.
|
|
@@ -61,19 +62,92 @@ const INTEGRATION_CATALOG = {
|
|
|
61
62
|
},
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Read and JSON.parse a config file. Returns the parsed object, or null on
|
|
67
|
+
* any failure (missing file, unreadable, malformed JSON). Private: the one
|
|
68
|
+
* fs.readFileSync + JSON.parse site both parseMcpConfig() and
|
|
69
|
+
* readScopedMcpServers() delegate to, so there is exactly one place that
|
|
70
|
+
* touches disk for either reader (Canon Part 7, no third reader minted).
|
|
71
|
+
*/
|
|
72
|
+
function _readJsonConfig(configPath) {
|
|
73
|
+
try {
|
|
74
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
/**
|
|
65
82
|
* Parse .mcp.json to extract configured MCP server keys.
|
|
66
83
|
* Returns array of server key strings (lowercased).
|
|
84
|
+
* Byte-unchanged behavior from before the _readJsonConfig extraction: any
|
|
85
|
+
* read/parse failure still yields [].
|
|
67
86
|
*/
|
|
68
87
|
function parseMcpConfig(mcpConfigPath) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
const parsed = _readJsonConfig(mcpConfigPath);
|
|
89
|
+
if (!parsed) return [];
|
|
90
|
+
const servers = parsed.mcpServers || parsed.servers || {};
|
|
91
|
+
return Object.keys(servers).map(k => k.toLowerCase());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Read Claude Code's own MCP server config (~/.claude.json by default) and
|
|
96
|
+
* project the mcpServers entries at BOTH scopes Claude Code actually has for
|
|
97
|
+
* this file: top-level mcpServers (scope "user") and
|
|
98
|
+
* projects[projectDir].mcpServers (scope "local"). This intentionally never
|
|
99
|
+
* reads a repo's own .mcp.json ("project" scope in Claude Code's own
|
|
100
|
+
* vocabulary) -- that is a different file entirely and is out of scope for
|
|
101
|
+
* this reader by construction, never by discipline.
|
|
102
|
+
*
|
|
103
|
+
* SECURITY (Canon Part 8, quick task 260911-axz, T-axz-01): the returned
|
|
104
|
+
* shape is a STRUCTURAL secret-leak guard, not a discretionary one. Each
|
|
105
|
+
* entry is projected down to exactly { name, scope, url_host, type }. This
|
|
106
|
+
* function NEVER copies `headers`, NEVER copies the full `url` (only the
|
|
107
|
+
* host, derived via `new URL(entry.url).host`), and NEVER copies `env`, so
|
|
108
|
+
* no caller of this function -- today or in a future edit -- can leak a
|
|
109
|
+
* Bearer token, an API key, or any other header value even by accident:
|
|
110
|
+
* the bytes are never read into the return value in the first place.
|
|
111
|
+
*
|
|
112
|
+
* @param {{configPath?: string, projectDir?: string}} [opts]
|
|
113
|
+
* @returns {Array<{name: string, scope: 'user'|'local', url_host: string|null, type: string|null}>}
|
|
114
|
+
*/
|
|
115
|
+
function readScopedMcpServers(opts) {
|
|
116
|
+
const o = opts || {};
|
|
117
|
+
const configPath = o.configPath || path.join(os.homedir(), '.claude.json');
|
|
118
|
+
const projectDir = o.projectDir || process.cwd();
|
|
119
|
+
|
|
120
|
+
const parsed = _readJsonConfig(configPath);
|
|
121
|
+
if (!parsed) return [];
|
|
122
|
+
|
|
123
|
+
const out = [];
|
|
124
|
+
const pushScope = (serversObj, scope) => {
|
|
125
|
+
if (!serversObj || typeof serversObj !== 'object') return;
|
|
126
|
+
for (const name of Object.keys(serversObj)) {
|
|
127
|
+
const entry = serversObj[name] || {};
|
|
128
|
+
let url_host = null;
|
|
129
|
+
if (typeof entry.url === 'string' && entry.url.length > 0) {
|
|
130
|
+
try {
|
|
131
|
+
url_host = new URL(entry.url).host;
|
|
132
|
+
} catch (_e) {
|
|
133
|
+
url_host = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
out.push({
|
|
137
|
+
name: name,
|
|
138
|
+
scope: scope,
|
|
139
|
+
url_host: url_host,
|
|
140
|
+
type: typeof entry.type === 'string' ? entry.type : null,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
pushScope(parsed.mcpServers, 'user');
|
|
146
|
+
if (parsed.projects && typeof parsed.projects === 'object' && parsed.projects[projectDir]) {
|
|
147
|
+
pushScope(parsed.projects[projectDir].mcpServers, 'local');
|
|
76
148
|
}
|
|
149
|
+
|
|
150
|
+
return out;
|
|
77
151
|
}
|
|
78
152
|
|
|
79
153
|
/**
|
|
@@ -98,8 +172,8 @@ function detectObsidianVault(workDir) {
|
|
|
98
172
|
|
|
99
173
|
/**
|
|
100
174
|
* Detect status of a single integration.
|
|
101
|
-
* @param {string} name
|
|
102
|
-
* @param {object} opts
|
|
175
|
+
* @param {string} name - integration key from INTEGRATION_CATALOG
|
|
176
|
+
* @param {object} opts - { workDir, mcpKeys }
|
|
103
177
|
* @returns {object} status object
|
|
104
178
|
*/
|
|
105
179
|
function detectSingle(name, opts) {
|
|
@@ -147,8 +221,8 @@ function detectSingle(name, opts) {
|
|
|
147
221
|
/**
|
|
148
222
|
* Detect all integrations.
|
|
149
223
|
* @param {object} options
|
|
150
|
-
* @param {string} options.workDir
|
|
151
|
-
* @param {string} options.mcpConfig
|
|
224
|
+
* @param {string} options.workDir - defaults to process.cwd()
|
|
225
|
+
* @param {string} options.mcpConfig - path to .mcp.json (defaults to workDir/.mcp.json)
|
|
152
226
|
* @returns {object} keyed by integration name, each with status object
|
|
153
227
|
*/
|
|
154
228
|
function detectIntegrations(options = {}) {
|
|
@@ -168,8 +242,8 @@ function detectIntegrations(options = {}) {
|
|
|
168
242
|
|
|
169
243
|
/**
|
|
170
244
|
* Check a single integration by name.
|
|
171
|
-
* @param {string} name
|
|
172
|
-
* @param {object} options
|
|
245
|
+
* @param {string} name - integration key
|
|
246
|
+
* @param {object} options - same as detectIntegrations
|
|
173
247
|
* @returns {object} status object for that integration
|
|
174
248
|
*/
|
|
175
249
|
function checkIntegration(name, options = {}) {
|
|
@@ -184,8 +258,8 @@ function checkIntegration(name, options = {}) {
|
|
|
184
258
|
* Analyze user message + room state and return integration suggestions.
|
|
185
259
|
* Returns at most 1 suggestion. Suppressed during active methodology.
|
|
186
260
|
*
|
|
187
|
-
* @param {string} userMessage
|
|
188
|
-
* @param {object} roomState
|
|
261
|
+
* @param {string} userMessage - the user's current message
|
|
262
|
+
* @param {object} roomState - current room state (activeMethodology truthy = suppress)
|
|
189
263
|
* @returns {Array<{integration: string, reason: string, offer_text: string}>}
|
|
190
264
|
*/
|
|
191
265
|
function getContextTriggers(userMessage, roomState = {}) {
|
|
@@ -229,4 +303,6 @@ module.exports = {
|
|
|
229
303
|
detectIntegrations,
|
|
230
304
|
checkIntegration,
|
|
231
305
|
getContextTriggers,
|
|
306
|
+
parseMcpConfig,
|
|
307
|
+
readScopedMcpServers,
|
|
232
308
|
};
|
|
@@ -105,20 +105,35 @@ function isApproved(decision) {
|
|
|
105
105
|
// -> wireAccept, run per dimension. Lazy-required so the module loads without
|
|
106
106
|
// the heavy lens engine; a leg that is unavailable degrades to a disclosed
|
|
107
107
|
// low-quality pass (SEED-059) rather than throwing. Exercised live at phase
|
|
108
|
-
// verification (223-VALIDATION), never in the hermetic fixture.
|
|
108
|
+
// verification (223-VALIDATION), never in the hermetic fixture. SAME research
|
|
109
|
+
// pipe contract as lib/mcp/tool-router.cjs (extractContext takes one object;
|
|
110
|
+
// the driver reads camelCase lensSet); the two callers must change together
|
|
111
|
+
// until a shared adapter exists (Canon Part 7).
|
|
109
112
|
async function defaultResearchFn(dimension, ctx) {
|
|
110
113
|
const context = isPlainObject(ctx) ? ctx : {};
|
|
111
114
|
try {
|
|
112
115
|
// eslint-disable-next-line global-require
|
|
113
|
-
const extractor = require('./research-context-extractor.cjs');
|
|
116
|
+
const extractor = isPlainObject(context._extractor) ? context._extractor : require('./research-context-extractor.cjs');
|
|
114
117
|
// eslint-disable-next-line global-require
|
|
115
|
-
const lensDriver = require('../lens-engine/source-lens-driver.cjs');
|
|
118
|
+
const lensDriver = isPlainObject(context._lensDriver) ? context._lensDriver : require('../lens-engine/source-lens-driver.cjs');
|
|
116
119
|
const wirer = require('./findings-wirer.cjs');
|
|
120
|
+
// The topic is the roster cell handle when present, else the dimension
|
|
121
|
+
// label -- both are GENERIC handles (Part 8 rule at lines 217-219 above),
|
|
122
|
+
// never room content, so nothing user-specific becomes a corpus query.
|
|
123
|
+
const topic = typeof context.handle === 'string' && context.handle.length > 0 ? context.handle : String(dimension);
|
|
117
124
|
const extracted = typeof extractor.extractContext === 'function'
|
|
118
|
-
? await extractor.extractContext(context.roomDir,
|
|
125
|
+
? await extractor.extractContext({ roomDir: context.roomDir, topic: topic, db: context.db })
|
|
119
126
|
: null;
|
|
120
127
|
const lensed = typeof lensDriver.runSourceLens === 'function'
|
|
121
|
-
? await lensDriver.runSourceLens(
|
|
128
|
+
? await lensDriver.runSourceLens({
|
|
129
|
+
roomDir: context.roomDir,
|
|
130
|
+
topic: topic,
|
|
131
|
+
lensSet: extracted ? extracted.lens_set : undefined,
|
|
132
|
+
preflight: extracted ? extracted.preflight : undefined,
|
|
133
|
+
stage: 'explore',
|
|
134
|
+
db: context.db,
|
|
135
|
+
dimension: dimension,
|
|
136
|
+
})
|
|
122
137
|
: null;
|
|
123
138
|
const findings = lensed && Array.isArray(lensed.findings) ? lensed.findings : [];
|
|
124
139
|
const quality = lensed && typeof lensed.quality === 'string' ? lensed.quality : (findings.length > 0 ? 'ok' : 'low');
|
|
@@ -276,7 +291,8 @@ function rosterToCells(roster, axes) {
|
|
|
276
291
|
* gateFn(ctx)->approve, onHalt(info),
|
|
277
292
|
* jtbdFns?{getCurrent, ...write} (the JTBD read/write seam pair),
|
|
278
293
|
* planFn?, researchFn?, computeFn?,
|
|
279
|
-
* writeFn?, classifyFn?, bankRollupFn?, genericDims?, run_id?, dateStr
|
|
294
|
+
* writeFn?, classifyFn?, bankRollupFn?, genericDims?, run_id?, dateStr?,
|
|
295
|
+
* _extractor?, _lensDriver? (test seams for the shipped defaultResearchFn)
|
|
280
296
|
* }
|
|
281
297
|
*
|
|
282
298
|
* result: { ok, dry_run?, halted?, halt_stage?, stages:[{stage,status,disclosure?}],
|
|
@@ -428,6 +444,10 @@ async function runIntelPipeline(opts) {
|
|
|
428
444
|
let pass;
|
|
429
445
|
try {
|
|
430
446
|
const researchCtx = { roomDir: roomDir, db: db, dimension: dim };
|
|
447
|
+
// Test seams for the shipped research pipe (quick-260910-dk1): threaded
|
|
448
|
+
// through only when the caller supplies a plain object, never faked up.
|
|
449
|
+
if (isPlainObject(o._extractor)) researchCtx._extractor = o._extractor;
|
|
450
|
+
if (isPlainObject(o._lensDriver)) researchCtx._lensDriver = o._lensDriver;
|
|
431
451
|
if (cell) {
|
|
432
452
|
researchCtx.entity = cell.entity;
|
|
433
453
|
researchCtx.axis = cell.axis;
|
|
@@ -542,4 +562,5 @@ async function runIntelPipeline(opts) {
|
|
|
542
562
|
return { ok: written ? written.ok !== false : false, halted: false, stages: stages, plan: plan, written: written };
|
|
543
563
|
}
|
|
544
564
|
|
|
545
|
-
|
|
565
|
+
// _internal.defaultResearchFn exposed for tests (private; do NOT consume in production)
|
|
566
|
+
module.exports = { runIntelPipeline, PIPELINE_STAGES, rosterToCells, _internal: { defaultResearchFn } };
|
|
@@ -328,6 +328,16 @@ function _isFreeFormTool(toolName) {
|
|
|
328
328
|
const TAXONOMY_RUNGS = Object.freeze(new Set(['undefined', 'ill-defined', 'well-defined', 'wicked']));
|
|
329
329
|
const KNOWN_LABEL_MAX = 120;
|
|
330
330
|
|
|
331
|
+
// Quick 260910-hni: recommend_chain's problem_type enum, the UNION of the
|
|
332
|
+
// incumbent's canonical rung strings and Theo's own ids (both vocabularies
|
|
333
|
+
// are live during the cutover soak; recommendChain() in brain-client.cjs
|
|
334
|
+
// already selects the right table by origin, this recognizer just has to
|
|
335
|
+
// accept whichever one the wrapper actually sent).
|
|
336
|
+
const RECOMMEND_CHAIN_PROBLEM_TYPES = Object.freeze(new Set([
|
|
337
|
+
'Undefined Problem', 'Ill-Defined Problem', 'Well-Defined Problem',
|
|
338
|
+
'UnDefined', 'IllDefined', 'WellDefined', 'Wicked',
|
|
339
|
+
]));
|
|
340
|
+
|
|
331
341
|
// _isSafeShortLabel(v): a string, 1-120 chars, no CR/LF, and clears _safeAudit.
|
|
332
342
|
// The length-and-single-line bound is label hygiene ("this is a node label,
|
|
333
343
|
// not prose"), NOT the content defense -- step 1's default-deny scan is. It is
|
|
@@ -436,6 +446,23 @@ function _proveKnownToolShape(payload, toolName) {
|
|
|
436
446
|
return { class: 'known_tool_shape', reason: 'taxonomy_ladder rung enum' };
|
|
437
447
|
}
|
|
438
448
|
|
|
449
|
+
if (toolName.indexOf('recommend_chain') !== -1) {
|
|
450
|
+
// BOTH required, ZERO optional: recommendChain() (lib/core/brain-
|
|
451
|
+
// client.cjs) always sends both problem_type and max_steps, and an empty
|
|
452
|
+
// optional list is what makes this fail-closed on any extra key.
|
|
453
|
+
if (!_hasExactKeys(payload, ['problem_type', 'max_steps'], [])) return null;
|
|
454
|
+
if (typeof payload.problem_type !== 'string'
|
|
455
|
+
|| !RECOMMEND_CHAIN_PROBLEM_TYPES.has(payload.problem_type)) return null;
|
|
456
|
+
if (!Number.isInteger(payload.max_steps)
|
|
457
|
+
|| payload.max_steps < 1 || payload.max_steps > 6) return null;
|
|
458
|
+
// _normalizeBrainProblemType passes any well-shaped UNMAPPED token
|
|
459
|
+
// through unchanged, so an off-enum value (say 'Trinity') falls out of
|
|
460
|
+
// this arm to the terminal catch-all as ambiguous, which PROCEEDS with a
|
|
461
|
+
// disclosure rather than blocking: this change strictly NARROWS what is
|
|
462
|
+
// ambiguous and never widens what may carry content.
|
|
463
|
+
return { class: 'known_tool_shape', reason: 'recommend_chain problem_type enum' };
|
|
464
|
+
}
|
|
465
|
+
|
|
439
466
|
return null;
|
|
440
467
|
}
|
|
441
468
|
|
|
@@ -77,15 +77,28 @@
|
|
|
77
77
|
* Section 3.5's explicit APPROVE condition) or validateSites() throws
|
|
78
78
|
* at module load.
|
|
79
79
|
*
|
|
80
|
-
* This module is a DECLARATION
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
80
|
+
* This module is a DECLARATION that requires exactly one pure-data leaf
|
|
81
|
+
* (brain-route-bound.cjs) and nothing else; it still EXECUTES nothing and
|
|
82
|
+
* OPENS NO WIRE (Quick 260911-ddd, DDD-02: the leaf itself requires nothing
|
|
83
|
+
* and opens no wire either, so this property still holds transitively).
|
|
84
|
+
* The scanner that reconciles this declaration against source lives in the
|
|
85
|
+
* test, not here -- that split is what lets this file hold plain quoted
|
|
86
|
+
* path strings as DATA without ever matching its own require-expression
|
|
87
|
+
* scan pattern. The new require names `brain-route-bound.cjs`, which
|
|
88
|
+
* matches neither the census scan's REACH_RE (brain-client|chain-
|
|
89
|
+
* recommender) nor its wire-pattern scan, so this file's own scan-safety
|
|
90
|
+
* property is unaffected.
|
|
85
91
|
*
|
|
86
92
|
* No em-dashes. CJS only.
|
|
87
93
|
*/
|
|
88
94
|
|
|
95
|
+
// Quick 260911-ddd (DDD-02): the single source of the Tier 3 bound. Reading
|
|
96
|
+
// the leaf here (rather than reading brain-router.cjs's own export) keeps
|
|
97
|
+
// the DECLARATION direction correct: this census depends on the constant,
|
|
98
|
+
// never on the implementation, so the declaration cannot silently drift
|
|
99
|
+
// out of sync with a router edit.
|
|
100
|
+
const { BRAIN_ROUTE_TIMEOUT_MS } = require('./brain-route-bound.cjs');
|
|
101
|
+
|
|
89
102
|
const COMPOSITION_SITES = Object.freeze([
|
|
90
103
|
Object.freeze({
|
|
91
104
|
file: 'lib/mcp/brain-router.cjs',
|
|
@@ -94,7 +107,10 @@ const COMPOSITION_SITES = Object.freeze([
|
|
|
94
107
|
via: "brainClient.ask(question) -- a generic problem-type/complexity enum question, never room content",
|
|
95
108
|
reaches_brain: true,
|
|
96
109
|
belt: 'callTool',
|
|
97
|
-
|
|
110
|
+
// Quick 260911-ddd (DDD-02): bound_ms is the DEFAULT from the single
|
|
111
|
+
// source (lib/mcp/brain-route-bound.cjs); MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS
|
|
112
|
+
// can move the APPLIED bound at runtime without editing this file.
|
|
113
|
+
bound_ms: BRAIN_ROUTE_TIMEOUT_MS,
|
|
98
114
|
frequency: 'one call per act* invocation, Tier 3 only (Tier 1 cache and Tier 2 local heuristic run first and can short-circuit before this call is made)',
|
|
99
115
|
reason: "D-01 ratifies this as the orchestration tool's live Brain-grounded recommendation leg, shipped before this phase existed. Desktop and Cowork have no MCP hook surface, so this is their only Brain-grounded enrichment path for act*.",
|
|
100
116
|
ratified_by: 'D-01',
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
5
|
+
*
|
|
6
|
+
* Quick 260911-ddd (DDD-02) -- the single source of truth for
|
|
7
|
+
* brain-router.cjs's Tier 3 race bound. A pure-data leaf: requires nothing,
|
|
8
|
+
* opens no wire. lib/mcp/brain-router.cjs reads BRAIN_ROUTE_TIMEOUT_MS /
|
|
9
|
+
* resolveBrainRouteTimeoutMs() to build its Promise.race; brain-composition-
|
|
10
|
+
* census.cjs's bound_ms entry reads BRAIN_ROUTE_TIMEOUT_MS so the census
|
|
11
|
+
* DECLARATION depends on a constant instead of on the router's own
|
|
12
|
+
* implementation -- that direction (census -> leaf, never census -> router)
|
|
13
|
+
* is what keeps the census honest: a router edit can never silently move the
|
|
14
|
+
* bound the census reports without also moving this leaf.
|
|
15
|
+
*
|
|
16
|
+
* WHY 6000, MEASURED GROUND (not intuition):
|
|
17
|
+
*
|
|
18
|
+
* The raced call is brainClient.ask(), which since quick 260910-hni makes
|
|
19
|
+
* THREE sequential Theo calls (brain_ask, then recommend_chain, then
|
|
20
|
+
* brain_query). A Render instance waking from spin-down was measured at
|
|
21
|
+
* 2.034 s on the FIRST call alone, and Theo redeploys on every push to its
|
|
22
|
+
* main, opening a fresh cold window each time. So the old 2000 ms bound
|
|
23
|
+
* could not cover even a WARM three-call composition on a slow link, let
|
|
24
|
+
* alone a cold one -- every cold window silently degraded /mos:act to the
|
|
25
|
+
* local heuristic with no disclosure of why.
|
|
26
|
+
*
|
|
27
|
+
* 6000 is the measured 2.034 s cold wake plus two warm follow-on calls
|
|
28
|
+
* (roughly 1 s total) with roughly 2x headroom on top of that sum.
|
|
29
|
+
*
|
|
30
|
+
* THE ACCEPTED COST: when Theo is genuinely down, the race now rejects at
|
|
31
|
+
* 6 s instead of 2 s. This is a real cost, not hand-waved away -- but
|
|
32
|
+
* localRec (lib/mcp/brain-router.cjs's Tier 2 local heuristic) is already
|
|
33
|
+
* computed BEFORE the race starts, so the Tier 2 answer is instant once the
|
|
34
|
+
* race loses and /mos:act still resolves either way. The trade is a rarer
|
|
35
|
+
* 6 s worst case against a silently degraded recommendation on every cold
|
|
36
|
+
* window -- accepted (see the threat register, T-ddd-04).
|
|
37
|
+
*
|
|
38
|
+
* No em-dashes. CJS only.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The default Tier 3 race bound in milliseconds. See the file header for
|
|
43
|
+
* the measured justification.
|
|
44
|
+
* @type {number}
|
|
45
|
+
*/
|
|
46
|
+
const BRAIN_ROUTE_TIMEOUT_MS = 6000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* resolveBrainRouteTimeoutMs(env) -- honors MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS
|
|
50
|
+
* only when it parses to a finite integer strictly greater than 0;
|
|
51
|
+
* otherwise returns BRAIN_ROUTE_TIMEOUT_MS unchanged. A malformed operator
|
|
52
|
+
* env can never zero out or invert the bound.
|
|
53
|
+
*
|
|
54
|
+
* @param {object} [env] defaults to process.env
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
function resolveBrainRouteTimeoutMs(env) {
|
|
58
|
+
const source = env || process.env;
|
|
59
|
+
const raw = source && source.MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS;
|
|
60
|
+
if (typeof raw !== 'string' || raw.trim().length === 0) return BRAIN_ROUTE_TIMEOUT_MS;
|
|
61
|
+
const parsed = Number(raw);
|
|
62
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed <= 0) return BRAIN_ROUTE_TIMEOUT_MS;
|
|
63
|
+
return parsed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { BRAIN_ROUTE_TIMEOUT_MS, resolveBrainRouteTimeoutMs };
|
package/lib/mcp/brain-router.cjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 3-tier fallback for framework recommendations:
|
|
7
7
|
* Tier 1: In-memory cache (instant, 10-min TTL)
|
|
8
8
|
* Tier 2: Local heuristic from problem-types.md (~100ms)
|
|
9
|
-
* Tier 3: Brain API via brain-client.cjs (
|
|
9
|
+
* Tier 3: Brain API via brain-client.cjs (bound: MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS, default 6000ms -- lib/mcp/brain-route-bound.cjs)
|
|
10
10
|
*
|
|
11
11
|
* RECOMMENDS only -- never executes frameworks.
|
|
12
12
|
* Called by orchestration router for act*, suggest-next commands.
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const { safeReadFile } = require('../core/index.cjs');
|
|
17
|
+
// Quick 260911-ddd (DDD-02): the single source of the Tier 3 race bound.
|
|
18
|
+
// See brain-route-bound.cjs's header for the measured 6000 ms justification
|
|
19
|
+
// and brain-composition-census.cjs, whose bound_ms entry reads this SAME
|
|
20
|
+
// leaf. brain-composition-census provenance: this file's Tier 3 race is
|
|
21
|
+
// enumerated in lib/mcp/brain-composition-census.cjs.
|
|
22
|
+
const { BRAIN_ROUTE_TIMEOUT_MS, resolveBrainRouteTimeoutMs } = require('./brain-route-bound.cjs');
|
|
17
23
|
|
|
18
24
|
// ---------------------------------------------------------------------------
|
|
19
25
|
// Cache (Tier 1)
|
|
@@ -287,11 +293,12 @@ function localRoute(roomDir, stateContent, intent) {
|
|
|
287
293
|
// Phase 254 (COMP-01): this call is enumerated in
|
|
288
294
|
// lib/mcp/brain-composition-census.cjs as the 'orchestration (act,
|
|
289
295
|
// act-chain, act-dry-run, act-swarm)' reaching site -- reaches_brain: true,
|
|
290
|
-
// belt: 'callTool', bound_ms:
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
296
|
+
// belt: 'callTool', bound_ms: BRAIN_ROUTE_TIMEOUT_MS (Quick 260911-ddd,
|
|
297
|
+
// DDD-02; the Promise.race wrap in recommend() below, default 6000ms,
|
|
298
|
+
// overridable via MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS). D-01 (254-CONTEXT.md)
|
|
299
|
+
// ratified this as SHIPPED, released behaviour, not a new decision. A new
|
|
300
|
+
// composed Brain call anywhere under lib/mcp/ requires an entry there or
|
|
301
|
+
// the build fails (tests/test-254-composition-census.cjs).
|
|
295
302
|
async function brainRoute(roomDir, stateContent, intent) {
|
|
296
303
|
// Phase 339 Plan 05 (D-03b), 2026-09-03: reset at entry so a value read
|
|
297
304
|
// by a caller can only ever reflect THIS invocation's own outcome.
|
|
@@ -349,10 +356,31 @@ async function brainRoute(roomDir, stateContent, intent) {
|
|
|
349
356
|
? (brainResult.directive.guided.framework || null)
|
|
350
357
|
: null;
|
|
351
358
|
|
|
352
|
-
// Build the chain: anchor first (if present + not already in options), then
|
|
359
|
+
// Build the chain: anchor first (if present + not already in options), then
|
|
360
|
+
// options[].commands (Quick 260910-hni), then options[].framework.
|
|
361
|
+
//
|
|
362
|
+
// Quick 260910-hni: KNOWN_METHODOLOGIES holds BARE command slugs (e.g.
|
|
363
|
+
// `find-bottlenecks`), so a Theo framework name like "Design Thinking"
|
|
364
|
+
// normalizes to `designthinking` and matches nothing below, while a slug
|
|
365
|
+
// like `find-bottlenecks` matches itself exactly -- pushing the command
|
|
366
|
+
// slugs ahead of the framework name lets this chain route even when the
|
|
367
|
+
// framework name alone would not.
|
|
368
|
+
//
|
|
369
|
+
// The Phase 339 D-03b BRAIN_ROUTE_NOTE_NO_NEXT_GATE disclosure (:344-346)
|
|
370
|
+
// now stops firing on the Theo path because the composed envelope carries
|
|
371
|
+
// `next_gate` again (Task 1, quick/260910-hni): that is the INTENDED
|
|
372
|
+
// outcome of this quick task, not a regression. The disclosure stays in
|
|
373
|
+
// place for any origin that still returns no `next_gate`.
|
|
353
374
|
const rawChain = [];
|
|
354
375
|
if (anchorFramework && typeof anchorFramework === 'string') rawChain.push(anchorFramework);
|
|
355
376
|
for (const opt of options) {
|
|
377
|
+
if (opt && Array.isArray(opt.commands)) {
|
|
378
|
+
for (const slug of opt.commands) {
|
|
379
|
+
if (typeof slug === 'string' && slug.length > 0 && !rawChain.includes(slug)) {
|
|
380
|
+
rawChain.push(slug);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
356
384
|
if (opt && typeof opt.framework === 'string' && opt.framework.length > 0) {
|
|
357
385
|
if (!rawChain.includes(opt.framework)) rawChain.push(opt.framework);
|
|
358
386
|
}
|
|
@@ -401,7 +429,8 @@ async function brainRoute(roomDir, stateContent, intent) {
|
|
|
401
429
|
|
|
402
430
|
/**
|
|
403
431
|
* Get a framework recommendation for a room.
|
|
404
|
-
* 3-tier fallback: cache -> local heuristic -> Brain API (
|
|
432
|
+
* 3-tier fallback: cache -> local heuristic -> Brain API (bound:
|
|
433
|
+
* MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS, default 6000ms).
|
|
405
434
|
*
|
|
406
435
|
* @param {string} roomDir - Path to room directory
|
|
407
436
|
* @param {{ intent?: string, mode?: string }} [options]
|
|
@@ -436,11 +465,15 @@ async function recommend(roomDir, options = {}) {
|
|
|
436
465
|
// Tier 2: Local heuristic (always computed as fallback)
|
|
437
466
|
const localRec = localRoute(roomDir, stateContent, intent);
|
|
438
467
|
|
|
439
|
-
// Tier 3: Brain API (
|
|
468
|
+
// Tier 3: Brain API (bound: MINDRIAN_BRAIN_ROUTE_TIMEOUT_MS, default
|
|
469
|
+
// 6000ms; non-blocking -- localRec above is already computed, so the
|
|
470
|
+
// Tier 2 answer is instant once this race loses). Quick 260911-ddd
|
|
471
|
+
// (DDD-02): evaluated at CALL TIME so an env override is honored per
|
|
472
|
+
// call and is testable, rather than a frozen 2000 literal.
|
|
440
473
|
try {
|
|
441
474
|
const brainRec = await Promise.race([
|
|
442
475
|
brainRoute(roomDir, stateContent, intent),
|
|
443
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')),
|
|
476
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), resolveBrainRouteTimeoutMs()))
|
|
444
477
|
]);
|
|
445
478
|
if (brainRec) {
|
|
446
479
|
setCache(cacheKey, brainRec);
|
|
@@ -527,4 +560,7 @@ function validateChain(roomDir, chain) {
|
|
|
527
560
|
return { valid: true };
|
|
528
561
|
}
|
|
529
562
|
|
|
530
|
-
|
|
563
|
+
// Quick 260911-ddd (DDD-02): re-exported so the Tier 3 bound pin is
|
|
564
|
+
// readable from either side (this file and brain-composition-census.cjs
|
|
565
|
+
// both read the same lib/mcp/brain-route-bound.cjs leaf).
|
|
566
|
+
module.exports = { recommend, validateChain, BRAIN_ROUTE_TIMEOUT_MS, resolveBrainRouteTimeoutMs };
|
package/lib/mcp/tool-router.cjs
CHANGED
|
@@ -541,6 +541,8 @@ async function runResearchPipeline(roomDir, topic, opts) {
|
|
|
541
541
|
const options = (opts && typeof opts === 'object') ? opts : {};
|
|
542
542
|
|
|
543
543
|
try {
|
|
544
|
+
// Same research pipe contract as lib/core/intel-pipeline.cjs defaultResearchFn
|
|
545
|
+
// (the second caller of extractContext + runSourceLens); change together.
|
|
544
546
|
const extracted = extractContext({ roomDir, topic, db });
|
|
545
547
|
let driverResult;
|
|
546
548
|
try {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.35",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@mindrian_os/cli",
|
|
9
|
-
"version": "2.0.0-beta.
|
|
9
|
+
"version": "2.0.0-beta.35",
|
|
10
10
|
"license": "BSL-1.1",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@ig3/markdown-it-wikilinks": "^1.0.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/cli",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.35",
|
|
4
4
|
"description": "Install MindrianOS into Claude Code with one command -- `npx @mindrian_os/cli`. Ships the MindrianOS plugin (Larry + PWS methodology + Data Room) plus a setup/diagnostics CLI (install/doctor/update).",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"mcp": "node bin/mindrian-mcp-server.cjs",
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
Schema for Room/RoomGroup hierarchy in both KuzuDB (local) and Neo4j Brain (remote).
|
|
4
4
|
Added in Phase 59.2. Additive-only -- filesystem + registry.json remain operational truth.
|
|
5
5
|
|
|
6
|
+
> **RETIRED (2026-09-10, quick task 260910-h32):** the Neo4j/Brain half of this
|
|
7
|
+
> schema is retired. `scripts/sync-rooms-brain` was deleted because it egressed
|
|
8
|
+
> room names, venture names, stages, statuses and paths to the Brain, a Canon
|
|
9
|
+
> Part 8 breach. The KuzuDB/local half remains live via `scripts/sync-rooms-graph`.
|
|
10
|
+
> This document is kept as the record of what the Brain-side schema WAS.
|
|
11
|
+
|
|
6
12
|
## Node Types
|
|
7
13
|
|
|
8
14
|
### Room
|
|
@@ -76,7 +82,7 @@ Connects Room to Framework nodes based on methodology commands run in that room.
|
|
|
76
82
|
|------|----|------------|-------|
|
|
77
83
|
| Room | Framework | first_used (datetime), usage_count (int) | Created from room/.analytics.json |
|
|
78
84
|
|
|
79
|
-
Source: `track-analytics` records command usage per room. `sync-rooms-brain` maps commands to Framework node names.
|
|
85
|
+
Source: `track-analytics` records command usage per room. `sync-rooms-brain` maps commands to Framework node names. (RETIRED 2026-09-10 with the script; kept as history.)
|
|
80
86
|
|
|
81
87
|
### SHARES_THEME (Brain only)
|
|
82
88
|
Cross-room content similarity detected from problem-definition keywords.
|
|
@@ -201,12 +207,12 @@ RETURN r.name, s.name AS stage,
|
|
|
201
207
|
| Script | Target | Trigger | Frequency |
|
|
202
208
|
|--------|--------|---------|-----------|
|
|
203
209
|
| `scripts/sync-rooms-graph` | KuzuDB local | session-start, room-registry create/archive | Every session + on room changes |
|
|
204
|
-
| `scripts/sync-rooms-brain` | Neo4j Brain
|
|
210
|
+
| `scripts/sync-rooms-brain` | Neo4j Brain (RETIRED 2026-09-10) | none -- script deleted, Canon Part 8 | never |
|
|
205
211
|
|
|
206
|
-
|
|
207
|
-
|
|
212
|
+
The one surviving script is idempotent and fire-and-forget. Failure degrades
|
|
213
|
+
gracefully across the two remaining tiers:
|
|
208
214
|
- KuzuDB unavailable -> filesystem only (Tier 0)
|
|
209
|
-
-
|
|
215
|
+
- Everything still works from registry.json even if KuzuDB never runs
|
|
210
216
|
|
|
211
217
|
## Additive-Only Rule (D-15 through D-18)
|
|
212
218
|
|