@mindrian_os/cli 2.0.0-beta.31 → 2.0.0-beta.33
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 +30 -0
- package/bin/mindrian-brain-mcp-client.cjs +1 -1
- package/lib/core/brain-client.cjs +276 -20
- package/lib/core/directive-envelope.cjs +13 -0
- package/lib/core/intel-pipeline.cjs +28 -7
- package/lib/core/part8-egress-guard.cjs +27 -0
- package/lib/mcp/brain-router.cjs +22 -1
- 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/collect-cold-install-evidence.cjs +411 -0
- package/scripts/room-registry +4 -4
- package/scripts/session-start +3 -2
- package/scripts/whitespace-to-brain.cjs +2 -1
- package/skills/larry-personality/SKILL.md +4 -3
- package/scripts/sync-rooms-brain +0 -438
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mos",
|
|
3
3
|
"description": "MindrianOS -- Your AI innovation co-founder. Larry thinks with you through PWS methodology, builds your Data Room as you explore, and chains frameworks intelligently. Install and go.",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.33",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Jonathan Sagir",
|
|
7
7
|
"url": "https://mindrian-os.com"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## [2.0.0-beta.33] - 2026-09-10
|
|
2
|
+
|
|
3
|
+
### Fixed - Theo answers Larry end to end (quick task 260910-hni)
|
|
4
|
+
|
|
5
|
+
- `brain_ask` through the plugin no longer returns an empty DirectiveEnvelope on every Theo answer.
|
|
6
|
+
`lib/core/brain-client.cjs` `ask()` now composes `directive.guided.framework`, `next_gate.options`
|
|
7
|
+
(ranked framework chain with the `/mos:` commands Theo links to each framework) and a new additive
|
|
8
|
+
`grounding` field (Theo's ranked book rows: chapterId, section, score, snippet) from Theo's
|
|
9
|
+
`structured_rows` + `recommend_chain`. The question's own words are never echoed back (Canon Part 8).
|
|
10
|
+
Incumbent-shaped responses pass through byte-unchanged. Live e2e through the MCP shim: framework
|
|
11
|
+
Design Thinking, 4 chain options, 8 grounding rows.
|
|
12
|
+
- `askOp()` accepts Theo's op-mode shape (`rows` + `coverage`); it had been degrading every curated op to
|
|
13
|
+
zero rows because Theo never sends a `count` key.
|
|
14
|
+
- `lib/mcp/brain-router.cjs` routes `/mos:act` chains from the commands Theo attaches to each framework,
|
|
15
|
+
so Tier 3 is graph-grounded again instead of silently falling to the local heuristic.
|
|
16
|
+
- `lib/core/part8-egress-guard.cjs` recognizes the `recommend_chain` payload shape (rung enum +
|
|
17
|
+
step budget) as a known safe shape. `tests/run-all-339.sh` now counts a `.cjs` leg that prints SKIP
|
|
18
|
+
as skipped, not passed.
|
|
19
|
+
- Larry's thin-grounding clause (`skills/larry-personality/SKILL.md`) keys on an empty `grounding.rows`
|
|
20
|
+
instead of the retired "empty signals set" wording.
|
|
21
|
+
|
|
22
|
+
### Removed - Canon Part 8 (quick task 260910-h32)
|
|
23
|
+
|
|
24
|
+
- `scripts/sync-rooms-brain` and its three detached spawns (`scripts/session-start`, twice in
|
|
25
|
+
`scripts/room-registry`). It string-interpolated every room's name, venture, stage, status and path
|
|
26
|
+
into `brain_write` Cypher and sent it to the Brain origin on every SessionStart (observed: 1,498
|
|
27
|
+
POSTs, 55 rooms, Authorization header on each). Room state already lives in local SQLite via
|
|
28
|
+
`sync-rooms-graph`. A default-deny census test (`tests/test-quick-260910-h32-no-brain-write-from-scripts.cjs`,
|
|
29
|
+
registered in `run-all-257.sh`) keeps it from coming back.
|
|
30
|
+
|
|
1
31
|
## [2.0.0-beta.31] - 2026-09-10
|
|
2
32
|
|
|
3
33
|
### Changed - install and update overhaul, Phase 341 waves 1-5 (SEED teardown 2026-09-09)
|
|
@@ -163,7 +163,7 @@ const server = new McpServer({ name: 'mindrian-brain', version: version });
|
|
|
163
163
|
server.registerTool(
|
|
164
164
|
'brain_ask',
|
|
165
165
|
{
|
|
166
|
-
description: 'Ask the remote teaching graph a natural-language methodology question. Routing happens server-side. Returns a DirectiveEnvelope
|
|
166
|
+
description: 'Ask the remote teaching graph a natural-language methodology question. Routing happens server-side. Returns a DirectiveEnvelope whose directive and next_gate are composed from the graph-grounded rows behind the answer plus a recommended framework chain; grounding.rows names the rows the answer stands on, and an empty grounding.rows means thin footing, not a clean answer. Reach for this first for an open methodology question; use brain_search when you already know the topic and want matching nodes directly.',
|
|
167
167
|
inputSchema: z.strictObject({ question: z.string().describe('A methodology question (generic framework handles only -- never user artifacts or personal data per Canon Part 8).') }),
|
|
168
168
|
},
|
|
169
169
|
async ({ question }) => {
|
|
@@ -1110,6 +1110,200 @@ async function schema() {
|
|
|
1110
1110
|
return result;
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* _inferRungFromQuestion(question) -> one of the four Theo rung ids.
|
|
1115
|
+
*
|
|
1116
|
+
* Theo's `classify_problem_type` tool takes NO parameters (the caller
|
|
1117
|
+
* classifies), and there is no free-text rung classifier anywhere in this
|
|
1118
|
+
* repo (`brain-derivation.cjs::classifyProblemType` scores MINTO triples,
|
|
1119
|
+
* not text). This helper fills that local gap. It is PURE and LOCAL: it
|
|
1120
|
+
* never sends the question anywhere, which is the whole reason it exists
|
|
1121
|
+
* (Canon Part 8 -- the question is user data and stays local).
|
|
1122
|
+
*
|
|
1123
|
+
* Precedence is FIXED and load-bearing (do not reorder): Wicked is the
|
|
1124
|
+
* orthogonal stakeholder-conflict axis and wins outright; UnDefined's
|
|
1125
|
+
* markers are the least ambiguous of the remaining three; WellDefined's
|
|
1126
|
+
* markers are next; IllDefined's markers are the broadest and are also the
|
|
1127
|
+
* default, so they run last and cost nothing when they lose.
|
|
1128
|
+
*
|
|
1129
|
+
* Multi-word markers match as a substring; single-word markers match on a
|
|
1130
|
+
* word boundary so `which` does not fire inside `whichever`. Non-string or
|
|
1131
|
+
* empty input returns `'IllDefined'` and never throws.
|
|
1132
|
+
*
|
|
1133
|
+
* @param {*} question
|
|
1134
|
+
* @returns {'Wicked'|'UnDefined'|'WellDefined'|'IllDefined'}
|
|
1135
|
+
*/
|
|
1136
|
+
const _RUNG_MARKERS = [
|
|
1137
|
+
{ rung: 'Wicked', markers: ['stakeholder', 'disagree', 'values', 'political'] },
|
|
1138
|
+
{ rung: 'UnDefined', markers: ['future of', 'no boundary', 'unbounded'] },
|
|
1139
|
+
{ rung: 'WellDefined', markers: ['measure', 'spec', 'test', 'kpi', 'how do we'] },
|
|
1140
|
+
{ rung: 'IllDefined', markers: ['next big thing', 'which', 'should we', 'opportunity'] },
|
|
1141
|
+
];
|
|
1142
|
+
|
|
1143
|
+
function _matchesRungMarker(text, marker) {
|
|
1144
|
+
if (marker.indexOf(' ') !== -1) return text.indexOf(marker) !== -1;
|
|
1145
|
+
return new RegExp('\\b' + marker + '\\b').test(text);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function _inferRungFromQuestion(question) {
|
|
1149
|
+
if (typeof question !== 'string' || !question.trim()) return 'IllDefined';
|
|
1150
|
+
const lc = question.trim().toLowerCase();
|
|
1151
|
+
for (let i = 0; i < _RUNG_MARKERS.length; i++) {
|
|
1152
|
+
const entry = _RUNG_MARKERS[i];
|
|
1153
|
+
for (let j = 0; j < entry.markers.length; j++) {
|
|
1154
|
+
if (_matchesRungMarker(lc, entry.markers[j])) return entry.rung;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return 'IllDefined';
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* _composeTheoAsk(payload, question, deps) -> composed envelope payload.
|
|
1162
|
+
*
|
|
1163
|
+
* Composes `directive`, `next_gate` and `grounding` onto a Theo
|
|
1164
|
+
* `answer_mode: 'structured_rows'` payload so downstream consumers
|
|
1165
|
+
* (`wrapDirective`, `brainRoute`, `rs-chain-feeder.cjs`) get a real
|
|
1166
|
+
* framework and chain instead of an empty scaffold. `deps` is the
|
|
1167
|
+
* injectable wire seam (`{ recommendChain, query }`) the offline test suite
|
|
1168
|
+
* drives; it defaults to this module's own wrappers so production behavior
|
|
1169
|
+
* needs no injection.
|
|
1170
|
+
*
|
|
1171
|
+
* Runs entirely inside one outer try/catch and NEVER throws: on any failure
|
|
1172
|
+
* it still returns the same object shape with `options: []` and
|
|
1173
|
+
* `chain_status: 'unreachable'`, so a rows response with a failed chain
|
|
1174
|
+
* still returns `grounding`.
|
|
1175
|
+
*
|
|
1176
|
+
* Canon Part 8: the only two extra wire calls this makes carry a closed
|
|
1177
|
+
* rung enum (`recommendChain`) and framework names Theo itself just
|
|
1178
|
+
* returned (`query`, label-anchored on `(f:Framework)`, never
|
|
1179
|
+
* relationship-first -- Theo's read allow-list refuses
|
|
1180
|
+
* `DirectedRelationshipTypeScan`). `query_terms` (the question's own words)
|
|
1181
|
+
* is the ONLY key removed from the payload, because it is the question
|
|
1182
|
+
* echoed back; the row projection is by the four NAMED keys
|
|
1183
|
+
* (chapterId/section/score/snippet), never a blind row copy, for the same
|
|
1184
|
+
* reason plus shape stability.
|
|
1185
|
+
*
|
|
1186
|
+
* @param {object} payload - the raw Theo structured_rows payload
|
|
1187
|
+
* @param {string} question - the original question (local only, never sent)
|
|
1188
|
+
* @param {{recommendChain?: Function, query?: Function}} [deps]
|
|
1189
|
+
* @returns {Promise<object>}
|
|
1190
|
+
*/
|
|
1191
|
+
function _projectGroundingRows(rows) {
|
|
1192
|
+
return Array.isArray(rows)
|
|
1193
|
+
? rows
|
|
1194
|
+
.filter((r) => r && typeof r === 'object')
|
|
1195
|
+
.map((r) => ({ chapterId: r.chapterId, section: r.section, score: r.score, snippet: r.snippet }))
|
|
1196
|
+
: [];
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async function _composeTheoAsk(payload, question, deps) {
|
|
1200
|
+
deps = deps || {};
|
|
1201
|
+
const recommend = deps.recommendChain || recommendChain;
|
|
1202
|
+
const runQuery = deps.query || query;
|
|
1203
|
+
|
|
1204
|
+
try {
|
|
1205
|
+
const rung = _inferRungFromQuestion(question);
|
|
1206
|
+
|
|
1207
|
+
let chainRes = null;
|
|
1208
|
+
let chainStatus;
|
|
1209
|
+
try {
|
|
1210
|
+
chainRes = await recommend(rung, 4);
|
|
1211
|
+
if (chainRes == null || chainRes.error || !Array.isArray(chainRes.chain)) {
|
|
1212
|
+
chainStatus = 'unreachable';
|
|
1213
|
+
} else if (chainRes.chain.length === 0) {
|
|
1214
|
+
chainStatus = 'empty';
|
|
1215
|
+
} else {
|
|
1216
|
+
chainStatus = 'ok';
|
|
1217
|
+
}
|
|
1218
|
+
} catch (_e) {
|
|
1219
|
+
chainStatus = 'unreachable';
|
|
1220
|
+
}
|
|
1221
|
+
const steps = chainStatus === 'ok' ? chainRes.chain : [];
|
|
1222
|
+
|
|
1223
|
+
// COMMANDS: exactly one runQuery call, only when there are steps, in its
|
|
1224
|
+
// own try/catch (on any failure every option gets commands: []).
|
|
1225
|
+
// Label-anchored and never relationship-first: Theo's read allow-list
|
|
1226
|
+
// refuses a template that starts from [:USES_FRAMEWORK].
|
|
1227
|
+
const commandsByFramework = new Map();
|
|
1228
|
+
if (steps.length > 0) {
|
|
1229
|
+
try {
|
|
1230
|
+
const names = steps
|
|
1231
|
+
.map((s) => s && s.framework)
|
|
1232
|
+
.filter((n) => typeof n === 'string');
|
|
1233
|
+
const result = await runQuery(
|
|
1234
|
+
'MATCH (f:Framework) WHERE f.name IN $names OPTIONAL MATCH (c:MindrianCommand)-[:USES_FRAMEWORK]->(f) RETURN f.name AS framework, collect(DISTINCT c.name) AS commands',
|
|
1235
|
+
{ names: names }
|
|
1236
|
+
);
|
|
1237
|
+
const records = (result && Array.isArray(result.records)) ? result.records : [];
|
|
1238
|
+
for (const rec of records) {
|
|
1239
|
+
if (!rec || typeof rec.framework !== 'string') continue;
|
|
1240
|
+
const slugs = Array.isArray(rec.commands)
|
|
1241
|
+
? rec.commands
|
|
1242
|
+
.filter((c) => typeof c === 'string' && c.length > 0)
|
|
1243
|
+
.map((c) => c.replace(/^\/mos:/, ''))
|
|
1244
|
+
: [];
|
|
1245
|
+
commandsByFramework.set(rec.framework, slugs);
|
|
1246
|
+
}
|
|
1247
|
+
} catch (_e) {
|
|
1248
|
+
// any failure here -> every option gets commands: [] below (Map stays empty)
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// CONFIDENCE: top step reads exactly 0.9; a missing/non-finite degree -> 0.5.
|
|
1253
|
+
const top = steps.reduce((max, s) => {
|
|
1254
|
+
const d = s && Number.isFinite(s.degree) ? s.degree : 0;
|
|
1255
|
+
return d > max ? d : max;
|
|
1256
|
+
}, 0);
|
|
1257
|
+
const options = steps.map((s) => {
|
|
1258
|
+
const degree = s && s.degree;
|
|
1259
|
+
let confidence = 0.5;
|
|
1260
|
+
if (top > 0 && Number.isFinite(degree)) {
|
|
1261
|
+
confidence = Math.max(0.5, Math.min(0.9, Math.round((0.5 + 0.4 * (degree / top)) * 100) / 100));
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
framework: s && s.framework,
|
|
1265
|
+
confidence: confidence,
|
|
1266
|
+
commands: commandsByFramework.get(s && s.framework) || [],
|
|
1267
|
+
};
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
const out = Object.assign({}, payload);
|
|
1271
|
+
delete out.query_terms;
|
|
1272
|
+
out.directive = {
|
|
1273
|
+
guided: { questions: [], framework: (steps[0] && steps[0].framework) || null, stage: rung },
|
|
1274
|
+
};
|
|
1275
|
+
out.next_gate = { sub_shape: 'F.1', options: options };
|
|
1276
|
+
out.grounding = {
|
|
1277
|
+
source: 'theo',
|
|
1278
|
+
answer_mode: payload.answer_mode,
|
|
1279
|
+
rows: _projectGroundingRows(payload.rows),
|
|
1280
|
+
problem_type: rung,
|
|
1281
|
+
problem_type_source: 'heuristic',
|
|
1282
|
+
chain_coverage: (chainRes && chainRes.coverage && typeof chainRes.coverage === 'object') ? chainRes.coverage : null,
|
|
1283
|
+
chain_status: chainStatus,
|
|
1284
|
+
confidence_source: 'theo_degree_normalized',
|
|
1285
|
+
};
|
|
1286
|
+
return out;
|
|
1287
|
+
} catch (_e) {
|
|
1288
|
+
const rung = _inferRungFromQuestion(question);
|
|
1289
|
+
const out = Object.assign({}, payload);
|
|
1290
|
+
delete out.query_terms;
|
|
1291
|
+
out.directive = { guided: { questions: [], framework: null, stage: rung } };
|
|
1292
|
+
out.next_gate = { sub_shape: 'F.1', options: [] };
|
|
1293
|
+
out.grounding = {
|
|
1294
|
+
source: 'theo',
|
|
1295
|
+
answer_mode: payload.answer_mode,
|
|
1296
|
+
rows: _projectGroundingRows(payload.rows),
|
|
1297
|
+
problem_type: rung,
|
|
1298
|
+
problem_type_source: 'heuristic',
|
|
1299
|
+
chain_coverage: null,
|
|
1300
|
+
chain_status: 'unreachable',
|
|
1301
|
+
confidence_source: 'theo_degree_normalized',
|
|
1302
|
+
};
|
|
1303
|
+
return out;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1113
1307
|
/**
|
|
1114
1308
|
* Natural-language methodology question against the Brain (wraps brain_ask).
|
|
1115
1309
|
*
|
|
@@ -1119,9 +1313,14 @@ async function schema() {
|
|
|
1119
1313
|
* question. Canon Part 8: the question string carries only generic methodology
|
|
1120
1314
|
* language -- never user artifacts, meeting text, or personal identifiers.
|
|
1121
1315
|
*
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
*
|
|
1316
|
+
* Two response shapes on success:
|
|
1317
|
+
* - INCUMBENT: carries `directive` already -- returned BYTE-UNCHANGED, the
|
|
1318
|
+
* same object reference, no copy.
|
|
1319
|
+
* - THEO structured_rows: `{ answer_mode: 'structured_rows', rows: [...] }`
|
|
1320
|
+
* with no `directive` -- composed through `_composeTheoAsk` into a full
|
|
1321
|
+
* envelope payload (`directive`, `next_gate`, `grounding` added).
|
|
1322
|
+
* A `{ text: 'Error: ...' } / { error: ... }` sentinel passes through
|
|
1323
|
+
* UNCHANGED on a server-side error; null when the Brain is unreachable or no
|
|
1125
1324
|
* API key is configured (graceful degradation -- mirrors query()).
|
|
1126
1325
|
*
|
|
1127
1326
|
* @param {string} question
|
|
@@ -1129,7 +1328,15 @@ async function schema() {
|
|
|
1129
1328
|
*/
|
|
1130
1329
|
async function ask(question) {
|
|
1131
1330
|
if (typeof question !== 'string' || !question.trim()) return null;
|
|
1132
|
-
|
|
1331
|
+
const raw = await callTool('brain_ask', { question: question });
|
|
1332
|
+
if (raw == null) return raw; // transport-null contract, byte-locked
|
|
1333
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) return raw;
|
|
1334
|
+
if (raw.error) return raw; // egress_blocked / tier_denied / rate_limited / invalid_key
|
|
1335
|
+
if (raw.directive) return raw; // incumbent proof -- same object reference
|
|
1336
|
+
if (raw.answer_mode === 'structured_rows' && Array.isArray(raw.rows)) {
|
|
1337
|
+
return _composeTheoAsk(raw, question);
|
|
1338
|
+
}
|
|
1339
|
+
return raw;
|
|
1133
1340
|
}
|
|
1134
1341
|
|
|
1135
1342
|
/**
|
|
@@ -1162,24 +1369,64 @@ async function ask(question) {
|
|
|
1162
1369
|
* @param {object} [params] - generic-handles-only params object
|
|
1163
1370
|
* @returns {Promise<{op: string, source?: string, count: number, rows: Array, degraded?: boolean}>}
|
|
1164
1371
|
*/
|
|
1372
|
+
/**
|
|
1373
|
+
* _normalizeAskOpResult(result, operation) -- the askOp() shape recognizer,
|
|
1374
|
+
* extracted so tests can drive it directly with zero network. Copies the
|
|
1375
|
+
* dual-shape recognizer STRUCTURE from
|
|
1376
|
+
* lib/core/enrichment-queue.cjs::captureReadinessMiss: one entry point, arms
|
|
1377
|
+
* keyed on payload SHAPE never on key presence alone.
|
|
1378
|
+
*
|
|
1379
|
+
* Theo op answers carry `coverage`, never `count`, so a `count`-keyed
|
|
1380
|
+
* recognizer alone reads a live three-row answer as a degraded zero. Arms,
|
|
1381
|
+
* in order:
|
|
1382
|
+
* 1. INCUMBENT: `count` is a number and `rows` is an array -- the exact
|
|
1383
|
+
* object askOp built before this task. MUST run first so an incumbent
|
|
1384
|
+
* payload can never take arm 2.
|
|
1385
|
+
* 2. THEO: `rows` is an array (no numeric `count` required) -- reports
|
|
1386
|
+
* `coverage.matched` as `count` when it is a finite number, else
|
|
1387
|
+
* `rows.length`.
|
|
1388
|
+
* 3. Everything else (null, an `{error:...}` sentinel, a `{text:...}`
|
|
1389
|
+
* passthrough, a non-array `rows`) -- the existing degraded sentinel.
|
|
1390
|
+
* Degrade ONLY when `rows` is not an array.
|
|
1391
|
+
*
|
|
1392
|
+
* @param {*} result
|
|
1393
|
+
* @param {string} operation
|
|
1394
|
+
* @returns {{op: string, source?: string, count: number, rows: Array, coverage?: object, degraded?: boolean}}
|
|
1395
|
+
*/
|
|
1396
|
+
function _normalizeAskOpResult(result, operation) {
|
|
1397
|
+
if (result && typeof result === 'object'
|
|
1398
|
+
&& typeof result.count === 'number' && Array.isArray(result.rows)) {
|
|
1399
|
+
return {
|
|
1400
|
+
op: result.op || operation,
|
|
1401
|
+
source: result.source,
|
|
1402
|
+
count: result.count,
|
|
1403
|
+
rows: result.rows,
|
|
1404
|
+
...(result.degraded ? { degraded: true } : {}),
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (result && typeof result === 'object' && Array.isArray(result.rows)) {
|
|
1408
|
+
const coverage = result.coverage;
|
|
1409
|
+
const matched = (coverage && typeof coverage === 'object' && Number.isFinite(coverage.matched))
|
|
1410
|
+
? coverage.matched
|
|
1411
|
+
: result.rows.length;
|
|
1412
|
+
const out = {
|
|
1413
|
+
op: result.op || operation,
|
|
1414
|
+
source: 'theo',
|
|
1415
|
+
count: matched,
|
|
1416
|
+
rows: result.rows,
|
|
1417
|
+
};
|
|
1418
|
+
if (coverage && typeof coverage === 'object') out.coverage = coverage;
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
return { op: operation, count: 0, rows: [], degraded: true };
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1165
1424
|
async function askOp(operation, params = {}) {
|
|
1166
1425
|
try {
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
&& typeof result.count === 'number' && Array.isArray(result.rows)) {
|
|
1172
|
-
return {
|
|
1173
|
-
op: result.op || operation,
|
|
1174
|
-
source: result.source,
|
|
1175
|
-
count: result.count,
|
|
1176
|
-
rows: result.rows,
|
|
1177
|
-
...(result.degraded ? { degraded: true } : {}),
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
// Unreachable Brain (null), an error/text passthrough, or any unexpected
|
|
1181
|
-
// shape -> graceful degraded sentinel.
|
|
1182
|
-
return { op: operation, count: 0, rows: [], degraded: true };
|
|
1426
|
+
return _normalizeAskOpResult(
|
|
1427
|
+
await callTool('brain_ask', { op: operation, params: params || {} }),
|
|
1428
|
+
operation
|
|
1429
|
+
);
|
|
1183
1430
|
} catch (_err) {
|
|
1184
1431
|
return { op: operation, count: 0, rows: [], degraded: true };
|
|
1185
1432
|
}
|
|
@@ -2300,6 +2547,10 @@ module.exports = {
|
|
|
2300
2547
|
smartSearch,
|
|
2301
2548
|
ask,
|
|
2302
2549
|
askOp,
|
|
2550
|
+
// Quick 260910-hni: the pure local rung heuristic ask()/_composeTheoAsk
|
|
2551
|
+
// uses to classify a question before the one recommend_chain wire call.
|
|
2552
|
+
// A NAMED export per the locked design, not a _test-only member.
|
|
2553
|
+
_inferRungFromQuestion,
|
|
2303
2554
|
schema,
|
|
2304
2555
|
stats,
|
|
2305
2556
|
getBrainUrl,
|
|
@@ -2361,5 +2612,10 @@ module.exports = {
|
|
|
2361
2612
|
// so the exact D-01/D-02 schedule is unit-tested with zero sleeps.
|
|
2362
2613
|
_parseRetryAfterMs,
|
|
2363
2614
|
_rateLimitWaitMs,
|
|
2615
|
+
// Quick 260910-hni test surface: the composer's injectable deps seam
|
|
2616
|
+
// and the widened askOp recognizer, both offline-drivable with zero
|
|
2617
|
+
// network (see tests/test-339-theo-ask-compose.cjs).
|
|
2618
|
+
_composeTheoAsk,
|
|
2619
|
+
_normalizeAskOpResult,
|
|
2364
2620
|
},
|
|
2365
2621
|
};
|
|
@@ -159,6 +159,15 @@ function _copyIfPlainObject(value) {
|
|
|
159
159
|
* `lib/mcp/no-instructions.test.cjs` exists to prevent). When neither field
|
|
160
160
|
* is present, the returned object keeps the exact same seven keys in the
|
|
161
161
|
* exact same insertion order it had before this change.
|
|
162
|
+
*
|
|
163
|
+
* Quick 260910-hni adds `grounding` as a THIRD named additive field, same
|
|
164
|
+
* shape guard and same reason: an untyped top-level copy would let
|
|
165
|
+
* arbitrary Brain-returned keys reach the model, exactly the leakage class
|
|
166
|
+
* `lib/mcp/no-instructions.test.cjs` exists to prevent. `_copyIfPlainObject`
|
|
167
|
+
* is a SHALLOW copy, so `grounding.rows` stays the same array reference,
|
|
168
|
+
* exactly as `refusal.next_moves` already behaves. Absence of `grounding`
|
|
169
|
+
* leaves the seven keys and their insertion order untouched
|
|
170
|
+
* (`tests/test-257-envelope-passthrough.cjs` Arm 3 pins this).
|
|
162
171
|
* @param {object|null} brainResponse
|
|
163
172
|
* @param {object} signals
|
|
164
173
|
* @returns {object} DirectiveEnvelope per CAPABILITY-MAP spec.
|
|
@@ -206,6 +215,10 @@ function wrapDirective(brainResponse, signals) {
|
|
|
206
215
|
if (refusal !== null) {
|
|
207
216
|
envelope.refusal = refusal;
|
|
208
217
|
}
|
|
218
|
+
const grounding = _copyIfPlainObject(brainResponse.grounding);
|
|
219
|
+
if (grounding !== null) {
|
|
220
|
+
envelope.grounding = grounding;
|
|
221
|
+
}
|
|
209
222
|
|
|
210
223
|
return envelope;
|
|
211
224
|
}
|
|
@@ -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
|
|
package/lib/mcp/brain-router.cjs
CHANGED
|
@@ -349,10 +349,31 @@ async function brainRoute(roomDir, stateContent, intent) {
|
|
|
349
349
|
? (brainResult.directive.guided.framework || null)
|
|
350
350
|
: null;
|
|
351
351
|
|
|
352
|
-
// Build the chain: anchor first (if present + not already in options), then
|
|
352
|
+
// Build the chain: anchor first (if present + not already in options), then
|
|
353
|
+
// options[].commands (Quick 260910-hni), then options[].framework.
|
|
354
|
+
//
|
|
355
|
+
// Quick 260910-hni: KNOWN_METHODOLOGIES holds BARE command slugs (e.g.
|
|
356
|
+
// `find-bottlenecks`), so a Theo framework name like "Design Thinking"
|
|
357
|
+
// normalizes to `designthinking` and matches nothing below, while a slug
|
|
358
|
+
// like `find-bottlenecks` matches itself exactly -- pushing the command
|
|
359
|
+
// slugs ahead of the framework name lets this chain route even when the
|
|
360
|
+
// framework name alone would not.
|
|
361
|
+
//
|
|
362
|
+
// The Phase 339 D-03b BRAIN_ROUTE_NOTE_NO_NEXT_GATE disclosure (:344-346)
|
|
363
|
+
// now stops firing on the Theo path because the composed envelope carries
|
|
364
|
+
// `next_gate` again (Task 1, quick/260910-hni): that is the INTENDED
|
|
365
|
+
// outcome of this quick task, not a regression. The disclosure stays in
|
|
366
|
+
// place for any origin that still returns no `next_gate`.
|
|
353
367
|
const rawChain = [];
|
|
354
368
|
if (anchorFramework && typeof anchorFramework === 'string') rawChain.push(anchorFramework);
|
|
355
369
|
for (const opt of options) {
|
|
370
|
+
if (opt && Array.isArray(opt.commands)) {
|
|
371
|
+
for (const slug of opt.commands) {
|
|
372
|
+
if (typeof slug === 'string' && slug.length > 0 && !rawChain.includes(slug)) {
|
|
373
|
+
rawChain.push(slug);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
356
377
|
if (opt && typeof opt.framework === 'string' && opt.framework.length > 0) {
|
|
357
378
|
if (!rawChain.includes(opt.framework)) rawChain.push(opt.framework);
|
|
358
379
|
}
|
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.33",
|
|
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.33",
|
|
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.33",
|
|
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
|
|