@mindrian_os/install 1.13.0-beta.19 → 1.13.0-beta.22
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/.mcp.json +6 -1
- package/CHANGELOG.md +13 -0
- package/README.md +51 -56
- package/bin/mindrian-brain-mcp-client.cjs +152 -0
- package/commands/doctor.md +3 -2
- package/lib/core/directive-envelope.cjs +175 -0
- package/lib/core/directive-envelope.test.cjs +225 -0
- package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
- package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
- package/lib/core/mcp-profiles.cjs +1 -1
- package/lib/core/migration-snapshot.cjs +172 -0
- package/lib/core/migration-snapshot.test.cjs +174 -0
- package/lib/core/mindrian-brain-shim.test.cjs +214 -0
- package/lib/core/rs-nl-to-query.cjs +1 -1
- package/lib/core/telemetry/validator.cjs +5 -2
- package/lib/core/telemetry/validator.test.cjs +5 -5
- package/lib/core/tier0-messaging.cjs +109 -0
- package/lib/core/tier0-messaging.test.cjs +218 -0
- package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
- package/lib/memory/mos-status-renderer.test.cjs +2 -2
- package/lib/memory/navigation-engine-core.test.cjs +1 -1
- package/lib/memory/run-feynman-tests.cjs +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copyright (c) 2026 Mindrian. BSL 1.1.
|
|
6
|
+
*
|
|
7
|
+
* Phase 127-00 (Task 1 RED) -- DirectiveEnvelope wrapper tests.
|
|
8
|
+
*
|
|
9
|
+
* 9 behavior tests covering:
|
|
10
|
+
* - Tier-0 sentinel (null brainResponse)
|
|
11
|
+
* - Default GUIDED mode for ordinary responses
|
|
12
|
+
* - Explicit AUTONOMOUS via "just tell me" / "bottom line"
|
|
13
|
+
* - HYBRID for mature-room commit-phase signals
|
|
14
|
+
* - Cold-start FORCES GUIDED (never autonomous)
|
|
15
|
+
* - user_override map present + the 3 documented keys
|
|
16
|
+
* - next_gate sub_shape regex + options array
|
|
17
|
+
* - selectMode default invariant
|
|
18
|
+
* - DEFAULT_MODE constant exported and locked
|
|
19
|
+
*
|
|
20
|
+
* Canon parts: 2 (Larry pedagogy), 3 (Decision Gate F-shape next_gate),
|
|
21
|
+
* 7 (Reuse Before Build: pure data shaping, zero network surface).
|
|
22
|
+
*
|
|
23
|
+
* Constraints (Phase 87 invariant):
|
|
24
|
+
* - Node built-ins only (node:assert/strict).
|
|
25
|
+
* - CJS only.
|
|
26
|
+
* - Zero new runtime dependencies.
|
|
27
|
+
*
|
|
28
|
+
* HARD RULE: no em-dashes (hyphens only).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const assert = require('node:assert/strict');
|
|
32
|
+
|
|
33
|
+
let passed = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
|
|
36
|
+
function ok(name) {
|
|
37
|
+
passed += 1;
|
|
38
|
+
process.stdout.write(' ok ' + name + '\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fail(name, err) {
|
|
42
|
+
failed += 1;
|
|
43
|
+
process.stdout.write(' FAIL ' + name + '\n');
|
|
44
|
+
if (err) process.stdout.write(' ' + (err.message || String(err)) + '\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const mod = require('./directive-envelope.cjs');
|
|
48
|
+
const { wrapDirective, selectMode, DEFAULT_MODE } = mod;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Test 1: wrapDirective(null, {}) returns Tier-0 sentinel envelope.
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
(function test1_tier0Sentinel() {
|
|
54
|
+
const label = 'wrapDirective(null, {}) returns Tier-0 sentinel envelope';
|
|
55
|
+
try {
|
|
56
|
+
const env = wrapDirective(null, {});
|
|
57
|
+
assert.equal(env.packet_version, '1.0', 'packet_version must be "1.0"');
|
|
58
|
+
assert.equal(env.packet_type, 'DirectiveEnvelope', 'packet_type must be "DirectiveEnvelope"');
|
|
59
|
+
assert.equal(env.mode, 'GUIDED', 'Tier-0 mode must be GUIDED');
|
|
60
|
+
assert.equal(env.mode_rationale, 'brain_unreachable',
|
|
61
|
+
'Tier-0 mode_rationale must be "brain_unreachable"; got ' + env.mode_rationale);
|
|
62
|
+
assert.ok(env.directive && typeof env.directive === 'object', 'directive must be an object');
|
|
63
|
+
assert.ok(env.directive.guided && typeof env.directive.guided === 'object',
|
|
64
|
+
'Tier-0 directive.guided must be present');
|
|
65
|
+
assert.ok(Array.isArray(env.directive.guided.questions),
|
|
66
|
+
'Tier-0 directive.guided.questions must be an array');
|
|
67
|
+
ok(label);
|
|
68
|
+
} catch (err) { fail(label, err); }
|
|
69
|
+
})();
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Test 2: ordinary brainResponse with empty signals defaults to GUIDED.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
(function test2_defaultGuided() {
|
|
75
|
+
const label = 'wrapDirective({text:"..."}, {}) defaults to mode=GUIDED';
|
|
76
|
+
try {
|
|
77
|
+
const env = wrapDirective({ text: 'raw brain response' }, {});
|
|
78
|
+
assert.equal(env.mode, 'GUIDED', 'default mode must be GUIDED');
|
|
79
|
+
assert.ok(typeof env.mode_rationale === 'string' && env.mode_rationale.length > 0,
|
|
80
|
+
'mode_rationale must be a non-empty string');
|
|
81
|
+
assert.ok(env.mode_rationale.includes('guided') || env.mode_rationale.includes('pedagogical')
|
|
82
|
+
|| env.mode_rationale.includes('default'),
|
|
83
|
+
'mode_rationale for default GUIDED should reference guided/pedagogical/default; got ' + env.mode_rationale);
|
|
84
|
+
ok(label);
|
|
85
|
+
} catch (err) { fail(label, err); }
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Test 3: explicit AUTONOMOUS via "just tell me" signal.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
(function test3_explicitAutonomous() {
|
|
92
|
+
const label = 'wrapDirective(resp, {user_said_just_tell_me:true}) returns AUTONOMOUS';
|
|
93
|
+
try {
|
|
94
|
+
const env = wrapDirective({ text: 'r' }, { user_said_just_tell_me: true });
|
|
95
|
+
assert.equal(env.mode, 'AUTONOMOUS', 'mode must be AUTONOMOUS');
|
|
96
|
+
assert.equal(env.mode_rationale, 'explicit_user_invitation',
|
|
97
|
+
'mode_rationale must be "explicit_user_invitation"; got ' + env.mode_rationale);
|
|
98
|
+
ok(label);
|
|
99
|
+
} catch (err) { fail(label, err); }
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Test 4: HYBRID for mature-room commit-phase.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
(function test4_hybridMatureCommit() {
|
|
106
|
+
const label = 'wrapDirective(resp, {session_count:9, room_mature:true, in_commit_phase:true}) returns HYBRID';
|
|
107
|
+
try {
|
|
108
|
+
const env = wrapDirective({ text: 'r' }, {
|
|
109
|
+
session_count: 9,
|
|
110
|
+
room_mature: true,
|
|
111
|
+
in_commit_phase: true,
|
|
112
|
+
});
|
|
113
|
+
assert.equal(env.mode, 'HYBRID', 'mode must be HYBRID; got ' + env.mode);
|
|
114
|
+
assert.equal(env.mode_rationale, 'mature_room_commit_gate',
|
|
115
|
+
'mode_rationale must be "mature_room_commit_gate"; got ' + env.mode_rationale);
|
|
116
|
+
ok(label);
|
|
117
|
+
} catch (err) { fail(label, err); }
|
|
118
|
+
})();
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Test 5: cold start FORCES GUIDED (never autonomous on first material).
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
(function test5_coldStartForcesGuided() {
|
|
124
|
+
const label = 'wrapDirective(resp, {is_first_material:true}) returns GUIDED (never autonomous on cold start)';
|
|
125
|
+
try {
|
|
126
|
+
const env = wrapDirective({ text: 'r' }, { is_first_material: true });
|
|
127
|
+
assert.equal(env.mode, 'GUIDED', 'cold-start mode must be GUIDED; got ' + env.mode);
|
|
128
|
+
assert.ok(env.mode_rationale.includes('cold_start'),
|
|
129
|
+
'cold-start mode_rationale must include "cold_start"; got ' + env.mode_rationale);
|
|
130
|
+
ok(label);
|
|
131
|
+
} catch (err) { fail(label, err); }
|
|
132
|
+
})();
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Test 6: every envelope contains user_override with the 3 documented keys.
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
(function test6_userOverrideMap() {
|
|
138
|
+
const label = 'every envelope contains user_override map with the 3 documented overrides';
|
|
139
|
+
try {
|
|
140
|
+
const envs = [
|
|
141
|
+
wrapDirective(null, {}),
|
|
142
|
+
wrapDirective({ text: 'r' }, {}),
|
|
143
|
+
wrapDirective({ text: 'r' }, { user_said_just_tell_me: true }),
|
|
144
|
+
wrapDirective({ text: 'r' }, { is_first_material: true }),
|
|
145
|
+
];
|
|
146
|
+
for (const env of envs) {
|
|
147
|
+
assert.ok(env.user_override && typeof env.user_override === 'object',
|
|
148
|
+
'user_override must be an object');
|
|
149
|
+
assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'just tell me'),
|
|
150
|
+
'user_override must have "just tell me" key');
|
|
151
|
+
assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'let me think'),
|
|
152
|
+
'user_override must have "let me think" key');
|
|
153
|
+
assert.ok(Object.prototype.hasOwnProperty.call(env.user_override, 'stop'),
|
|
154
|
+
'user_override must have "stop" key');
|
|
155
|
+
}
|
|
156
|
+
ok(label);
|
|
157
|
+
} catch (err) { fail(label, err); }
|
|
158
|
+
})();
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Test 7: every envelope contains next_gate.sub_shape F.[1-5] + options array.
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
(function test7_nextGateShape() {
|
|
164
|
+
const label = 'every envelope contains next_gate.sub_shape matching /^F\\.[1-5]$/ and options array';
|
|
165
|
+
try {
|
|
166
|
+
const re = /^F\.[1-5]$/;
|
|
167
|
+
const envs = [
|
|
168
|
+
wrapDirective(null, {}),
|
|
169
|
+
wrapDirective({ text: 'r' }, {}),
|
|
170
|
+
wrapDirective({ text: 'r' }, { user_said_just_tell_me: true }),
|
|
171
|
+
wrapDirective({ text: 'r' }, { session_count: 9, room_mature: true, in_commit_phase: true }),
|
|
172
|
+
];
|
|
173
|
+
for (const env of envs) {
|
|
174
|
+
assert.ok(env.next_gate && typeof env.next_gate === 'object', 'next_gate must be an object');
|
|
175
|
+
assert.ok(re.test(env.next_gate.sub_shape),
|
|
176
|
+
'next_gate.sub_shape must match F.[1-5]; got ' + env.next_gate.sub_shape);
|
|
177
|
+
assert.ok(Array.isArray(env.next_gate.options),
|
|
178
|
+
'next_gate.options must be an array');
|
|
179
|
+
}
|
|
180
|
+
ok(label);
|
|
181
|
+
} catch (err) { fail(label, err); }
|
|
182
|
+
})();
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// Test 8: selectMode returns GUIDED by default with no signals.
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
(function test8_selectModeDefault() {
|
|
188
|
+
const label = 'selectMode({}) returns mode=GUIDED by default (DEFAULT_MODE invariant)';
|
|
189
|
+
try {
|
|
190
|
+
const r = selectMode({});
|
|
191
|
+
assert.ok(r && typeof r === 'object', 'selectMode must return an object');
|
|
192
|
+
assert.equal(r.mode, 'GUIDED', 'default selectMode mode must be GUIDED; got ' + r.mode);
|
|
193
|
+
assert.ok(typeof r.rationale === 'string' && r.rationale.length > 0,
|
|
194
|
+
'rationale must be a non-empty string');
|
|
195
|
+
// Non-object input must be treated as empty signals (closed vocabulary).
|
|
196
|
+
const r2 = selectMode(null);
|
|
197
|
+
assert.equal(r2.mode, 'GUIDED', 'selectMode(null) must default to GUIDED');
|
|
198
|
+
const r3 = selectMode(undefined);
|
|
199
|
+
assert.equal(r3.mode, 'GUIDED', 'selectMode(undefined) must default to GUIDED');
|
|
200
|
+
const r4 = selectMode('string');
|
|
201
|
+
assert.equal(r4.mode, 'GUIDED', 'selectMode(non-object) must default to GUIDED');
|
|
202
|
+
ok(label);
|
|
203
|
+
} catch (err) { fail(label, err); }
|
|
204
|
+
})();
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Test 9: DEFAULT_MODE constant exported and locked to 'GUIDED'.
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
(function test9_defaultModeConstant() {
|
|
210
|
+
const label = 'DEFAULT_MODE constant exported and === "GUIDED" (Larry pedagogical canon lock)';
|
|
211
|
+
try {
|
|
212
|
+
assert.equal(DEFAULT_MODE, 'GUIDED',
|
|
213
|
+
'DEFAULT_MODE must be exported and equal "GUIDED"; got ' + DEFAULT_MODE);
|
|
214
|
+
assert.equal(typeof DEFAULT_MODE, 'string', 'DEFAULT_MODE must be a string');
|
|
215
|
+
ok(label);
|
|
216
|
+
} catch (err) { fail(label, err); }
|
|
217
|
+
})();
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Summary.
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
process.stdout.write('\n');
|
|
223
|
+
process.stdout.write('PASSED: ' + passed + '\n');
|
|
224
|
+
process.stdout.write('FAILED: ' + failed + '\n');
|
|
225
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 127-02 BRAIN-MCP-127-08 (CONTEXT D4) -- Class M Brain smoke.
|
|
5
|
+
* 5-layer composable probe replacing ~60% of doctor Brain-adjacent checks.
|
|
6
|
+
*
|
|
7
|
+
* "Class M" rationale: CONTEXT D4 text reads "K" but letter K is already
|
|
8
|
+
* taken in scripts/doctor.cjs by --stale-first-touch (SEED-007). A-L are
|
|
9
|
+
* assigned. M is the next free letter. The CAPABILITY-MAP.md doc patch
|
|
10
|
+
* lands in plan 127-03.
|
|
11
|
+
*
|
|
12
|
+
* Detects 12 Phase 126 taxonomy rows:
|
|
13
|
+
* L1 plugin_root #5 install-cache stale, #9 install-state drift
|
|
14
|
+
* L2 key_resolver #1 missing key, #2 perms-too-open, #8 env unreadable,
|
|
15
|
+
* #13 Bearer format mismatch
|
|
16
|
+
* L3 https_schema #4 cold-start timeout, #14 HTTPS 401, #19 cache
|
|
17
|
+
* stale, #21 schema shape
|
|
18
|
+
* L4 stdio_handshake #15 stdio handshake never returns
|
|
19
|
+
* L5 e2e_brain_schema #3 user-scope HTTP coexists with stdio (SHIM should
|
|
20
|
+
* answer, not the legacy HTTP transport)
|
|
21
|
+
*
|
|
22
|
+
* Canon Part 7 (reuse): L1/L2/L3 import the existing resolver chokepoints
|
|
23
|
+
* (active-plugin-root, resolve-brain-key, brain-client.schema). Only the
|
|
24
|
+
* L4/L5 stdio orchestration is net-new.
|
|
25
|
+
* Canon Part 8 (graph boundary): probe queries the methodology schema
|
|
26
|
+
* handle only; zero user-content egress; every Brain payload routes
|
|
27
|
+
* through brain-client.cjs (the delegation chokepoint).
|
|
28
|
+
*
|
|
29
|
+
* fail-fast cascade: if layer N fails, layers N+1..5 are SKIPPED with
|
|
30
|
+
* reason="skipped-prior-layer-failed" so the report points at the FIRST
|
|
31
|
+
* failure, not the cascade noise.
|
|
32
|
+
*
|
|
33
|
+
* HARD RULE: no em-dashes anywhere in this file.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const { spawn } = require('node:child_process');
|
|
39
|
+
|
|
40
|
+
// Layer registry. Wire-locked: the shell harness asserts id strings + order.
|
|
41
|
+
const LAYERS = Object.freeze([
|
|
42
|
+
Object.freeze({ id: 'plugin_root', name: 'L1 plugin-root-resolver' }),
|
|
43
|
+
Object.freeze({ id: 'key_resolver', name: 'L2 brain-key-resolver' }),
|
|
44
|
+
Object.freeze({ id: 'https_schema', name: 'L3 HTTPS schema probe' }),
|
|
45
|
+
Object.freeze({ id: 'stdio_handshake', name: 'L4 MCP stdio handshake' }),
|
|
46
|
+
Object.freeze({ id: 'e2e_brain_schema', name: 'L5 e2e brain_schema via shim' }),
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const STDIO_TIMEOUT_MS = Number(process.env.MINDRIAN_BRAIN_SMOKE_TIMEOUT_MS) || 10000;
|
|
50
|
+
const OVERALL_BUDGET_MS = 30000;
|
|
51
|
+
|
|
52
|
+
function _now() { return Date.now(); }
|
|
53
|
+
|
|
54
|
+
async function _runLayer(_name, fn) {
|
|
55
|
+
const t0 = _now();
|
|
56
|
+
try {
|
|
57
|
+
const r = await fn();
|
|
58
|
+
return { ok: !!r.ok, reason: r.reason || (r.ok ? 'pass' : 'fail'), ms: _now() - t0 };
|
|
59
|
+
} catch (e) {
|
|
60
|
+
return { ok: false, reason: 'exception: ' + (e && e.message ? e.message : String(e)), ms: _now() - t0 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// L1 -- plugin-root-resolver. Reuses lib/core/active-plugin-root.cjs.
|
|
65
|
+
async function _layer1(opts) {
|
|
66
|
+
const fn = opts.mockResolveRoot
|
|
67
|
+
|| require('../active-plugin-root.cjs').resolveActivePluginRoot;
|
|
68
|
+
const r = fn();
|
|
69
|
+
if (!r || !r.root) {
|
|
70
|
+
const src = (r && r.source) ? r.source : 'unknown';
|
|
71
|
+
return { ok: false, reason: 'plugin root not resolved (source=' + src + ')' };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, reason: 'resolved (source=' + r.source + ', topology=' + (r.topology || 'unknown') + ')' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// L2 -- key-resolver. Reuses lib/core/resolve-brain-key.cjs.
|
|
77
|
+
async function _layer2(opts) {
|
|
78
|
+
const fn = opts.mockResolveKey
|
|
79
|
+
|| require('../resolve-brain-key.cjs').resolveBrainKey;
|
|
80
|
+
const r = fn();
|
|
81
|
+
if (!r || !r.available) {
|
|
82
|
+
return { ok: false, reason: (r && r.reason) ? r.reason : 'key not available (no reason)' };
|
|
83
|
+
}
|
|
84
|
+
return { ok: true, reason: 'key resolved (source=' + r.source + ')' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// L3 -- schema probe. Reuses lib/core/brain-client.cjs::schema(). Catches the
|
|
88
|
+
// #19 cache-stale + #21 schema-shape rows at this layer.
|
|
89
|
+
async function _layer3(opts) {
|
|
90
|
+
const schemaFn = opts.mockSchema
|
|
91
|
+
|| (async () => require('../brain-client.cjs').schema());
|
|
92
|
+
const r = await schemaFn();
|
|
93
|
+
if (r == null) {
|
|
94
|
+
return { ok: false, reason: 'schema probe returned null (Brain unreachable or 401)' };
|
|
95
|
+
}
|
|
96
|
+
return { ok: true, reason: 'schema probe returned non-null payload' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _resolveShimPath(opts) {
|
|
100
|
+
return opts.shimPath
|
|
101
|
+
|| path.resolve(__dirname, '..', '..', '..', 'bin', 'mindrian-brain-mcp-client.cjs');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// L4 -- MCP stdio handshake. Spawns the bundled shim and asserts initialize
|
|
105
|
+
// resolves with serverInfo.name === 'mindrian-brain' within STDIO_TIMEOUT_MS.
|
|
106
|
+
async function _layer4(opts) {
|
|
107
|
+
const shimPath = _resolveShimPath(opts);
|
|
108
|
+
if (!opts.mockSpawn && !fs.existsSync(shimPath)) {
|
|
109
|
+
return { ok: false, reason: 'shim binary not found at ' + shimPath };
|
|
110
|
+
}
|
|
111
|
+
const orchestrator = opts.mockSpawn || _spawnAndHandshake;
|
|
112
|
+
return orchestrator(shimPath, Object.assign({}, opts, { intent: 'handshake' }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// L5 -- end-to-end brain_schema via the shim's tools/call. After initialize
|
|
116
|
+
// succeeds, send a tools/call brain_schema and accept either a real schema
|
|
117
|
+
// payload OR a DIRECTOR_NOT_AVAILABLE Tier-0 sentinel (the second case is
|
|
118
|
+
// expected when no key is available; the L5 contract is "the shim ANSWERED
|
|
119
|
+
// over the stdio path", not "the Brain returned a schema").
|
|
120
|
+
async function _layer5(opts) {
|
|
121
|
+
const shimPath = _resolveShimPath(opts);
|
|
122
|
+
if (!opts.mockSpawn && !fs.existsSync(shimPath)) {
|
|
123
|
+
return { ok: false, reason: 'shim binary not found at ' + shimPath };
|
|
124
|
+
}
|
|
125
|
+
const orchestrator = opts.mockSpawn || _spawnAndHandshake;
|
|
126
|
+
return orchestrator(shimPath, Object.assign({}, opts, { intent: 'e2e_brain_schema' }));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Real stdio orchestrator. opts.intent in { 'handshake', 'e2e_brain_schema' }.
|
|
130
|
+
// Wraps spawn + JSON-RPC initialize + (optional) tools/call + timeout +
|
|
131
|
+
// SIGTERM-on-resolve. Resolves with { ok, reason } -- never throws -- so the
|
|
132
|
+
// caller sees a structured FAIL row instead of an unhandled rejection.
|
|
133
|
+
function _spawnAndHandshake(shimPath, opts) {
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const proc = spawn(process.execPath, [shimPath], {
|
|
136
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
137
|
+
env: process.env,
|
|
138
|
+
});
|
|
139
|
+
const timer = setTimeout(function () {
|
|
140
|
+
try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ }
|
|
141
|
+
resolve({ ok: false, reason: opts.intent + ' timed out after ' + STDIO_TIMEOUT_MS + 'ms' });
|
|
142
|
+
}, STDIO_TIMEOUT_MS);
|
|
143
|
+
|
|
144
|
+
let buf = '';
|
|
145
|
+
proc.stdout.on('data', function (chunk) {
|
|
146
|
+
buf += chunk.toString('utf8');
|
|
147
|
+
const lines = buf.split('\n');
|
|
148
|
+
buf = lines.pop();
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
if (!line.trim()) continue;
|
|
151
|
+
let msg;
|
|
152
|
+
try { msg = JSON.parse(line); }
|
|
153
|
+
catch (_) { continue; /* non-JSON line: ignore (stderr boot noise) */ }
|
|
154
|
+
|
|
155
|
+
if (msg.id === 1 && msg.result && msg.result.serverInfo) {
|
|
156
|
+
const serverName = msg.result.serverInfo.name;
|
|
157
|
+
if (opts.intent === 'handshake') {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ }
|
|
160
|
+
if (serverName === 'mindrian-brain') {
|
|
161
|
+
return resolve({
|
|
162
|
+
ok: true,
|
|
163
|
+
reason: 'handshake succeeded, server=mindrian-brain v' + msg.result.serverInfo.version,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return resolve({ ok: false, reason: 'unexpected serverInfo.name=' + serverName });
|
|
167
|
+
}
|
|
168
|
+
// L5: initialize succeeded, send tools/call brain_schema.
|
|
169
|
+
try {
|
|
170
|
+
proc.stdin.write(JSON.stringify({
|
|
171
|
+
jsonrpc: '2.0', id: 2, method: 'tools/call',
|
|
172
|
+
params: { name: 'brain_schema', arguments: {} },
|
|
173
|
+
}) + '\n');
|
|
174
|
+
} catch (e) {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ }
|
|
177
|
+
return resolve({ ok: false, reason: 'tools/call write error: ' + e.message });
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (msg.id === 2 && msg.result && msg.result.content) {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ }
|
|
185
|
+
const c0 = msg.result.content[0];
|
|
186
|
+
const text = c0 && c0.text;
|
|
187
|
+
let parsed;
|
|
188
|
+
try { parsed = JSON.parse(text); } catch (_) { parsed = { text: text }; }
|
|
189
|
+
if (parsed && parsed.status === 'DIRECTOR_NOT_AVAILABLE') {
|
|
190
|
+
return resolve({
|
|
191
|
+
ok: true,
|
|
192
|
+
reason: 'e2e brain_schema returned Tier-0 sentinel (expected when no key)',
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return resolve({ ok: true, reason: 'e2e brain_schema returned a payload' });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
proc.stderr.on('data', function () { /* swallow shim startup line + stderr noise */ });
|
|
201
|
+
proc.on('error', function (err) {
|
|
202
|
+
clearTimeout(timer);
|
|
203
|
+
resolve({ ok: false, reason: 'spawn error: ' + err.message });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
proc.stdin.write(JSON.stringify({
|
|
208
|
+
jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
209
|
+
params: {
|
|
210
|
+
protocolVersion: '2024-11-05',
|
|
211
|
+
capabilities: {},
|
|
212
|
+
clientInfo: { name: 'class-m-smoke', version: '1.0' },
|
|
213
|
+
},
|
|
214
|
+
}) + '\n');
|
|
215
|
+
} catch (e) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ }
|
|
218
|
+
resolve({ ok: false, reason: 'initialize write error: ' + e.message });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Run the 5-layer Brain smoke probe with fail-fast cascade.
|
|
225
|
+
*
|
|
226
|
+
* @param {{
|
|
227
|
+
* mockResolveRoot?: function,
|
|
228
|
+
* mockResolveKey?: function,
|
|
229
|
+
* mockSchema?: function,
|
|
230
|
+
* mockSpawn?: function,
|
|
231
|
+
* shimPath?: string,
|
|
232
|
+
* }} [opts]
|
|
233
|
+
* @returns {Promise<{ok:boolean, layers:Array<{id,name,ok,reason,ms}>, overall_ms:number}>}
|
|
234
|
+
*/
|
|
235
|
+
async function checkBrainSmoke(opts) {
|
|
236
|
+
const o = opts || {};
|
|
237
|
+
const t0 = _now();
|
|
238
|
+
const out = { ok: true, layers: [], overall_ms: 0 };
|
|
239
|
+
let prevOk = true;
|
|
240
|
+
const layerFns = [_layer1, _layer2, _layer3, _layer4, _layer5];
|
|
241
|
+
for (let i = 0; i < LAYERS.length; i++) {
|
|
242
|
+
const meta = LAYERS[i];
|
|
243
|
+
if (!prevOk) {
|
|
244
|
+
out.layers.push({ id: meta.id, name: meta.name, ok: false, reason: 'skipped-prior-layer-failed', ms: 0 });
|
|
245
|
+
out.ok = false;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const r = await _runLayer(meta.name, function () { return layerFns[i](o); });
|
|
249
|
+
out.layers.push({ id: meta.id, name: meta.name, ok: r.ok, reason: r.reason, ms: r.ms });
|
|
250
|
+
if (!r.ok) { prevOk = false; out.ok = false; }
|
|
251
|
+
}
|
|
252
|
+
out.overall_ms = _now() - t0;
|
|
253
|
+
if (out.overall_ms > OVERALL_BUDGET_MS) {
|
|
254
|
+
out.ok = false;
|
|
255
|
+
out.layers.push({
|
|
256
|
+
id: 'budget', name: 'overall-budget', ok: false,
|
|
257
|
+
reason: 'overall_ms ' + out.overall_ms + ' > ' + OVERALL_BUDGET_MS, ms: 0,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Class M is diagnostic-only. There is no auto-remediation path: the 5
|
|
265
|
+
* failure surfaces require user action (install / set key / restart). This
|
|
266
|
+
* function exists for symmetry with classes that DO support --fix.
|
|
267
|
+
*
|
|
268
|
+
* @param {object} _result the checkBrainSmoke result (unused; signature parity)
|
|
269
|
+
* @returns {{fixed: false, reason: string}}
|
|
270
|
+
*/
|
|
271
|
+
function fixBrainSmoke(_result) {
|
|
272
|
+
return {
|
|
273
|
+
fixed: false,
|
|
274
|
+
reason: 'class-m is diagnostic-only; remediation requires user action: install / set key / restart',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { checkBrainSmoke, LAYERS, fixBrainSmoke, STDIO_TIMEOUT_MS };
|