@mindrian_os/install 1.13.0-beta.17 → 1.13.0-beta.21

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.
Files changed (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.mcp.json +6 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +51 -56
  5. package/bin/mindrian-brain-mcp-client.cjs +152 -0
  6. package/commands/act.md +1 -0
  7. package/commands/admin.md +1 -0
  8. package/commands/analyze-needs.md +2 -0
  9. package/commands/analyze-systems.md +2 -0
  10. package/commands/analyze-timing.md +2 -0
  11. package/commands/auto-explore.md +2 -0
  12. package/commands/beautiful-question.md +2 -0
  13. package/commands/brain-derive.md +2 -0
  14. package/commands/build-knowledge.md +2 -0
  15. package/commands/build-thesis.md +2 -0
  16. package/commands/causal.md +2 -0
  17. package/commands/challenge-assumptions.md +2 -0
  18. package/commands/compare-ventures.md +2 -0
  19. package/commands/dashboard.md +2 -1
  20. package/commands/deep-grade.md +2 -0
  21. package/commands/diagnose.md +21 -1
  22. package/commands/diagnostics.md +14 -3
  23. package/commands/doctor.md +6 -2
  24. package/commands/dogfood-flush.md +92 -0
  25. package/commands/dominant-designs.md +2 -0
  26. package/commands/explain-decision.md +2 -0
  27. package/commands/explore-domains.md +2 -0
  28. package/commands/explore-futures.md +2 -0
  29. package/commands/explore-trends.md +2 -0
  30. package/commands/export.md +1 -0
  31. package/commands/feynman-timeline-refresh.md +2 -0
  32. package/commands/file-meeting.md +2 -0
  33. package/commands/find-analogies.md +1 -0
  34. package/commands/find-bottlenecks.md +2 -0
  35. package/commands/find-connections.md +2 -0
  36. package/commands/funding.md +1 -0
  37. package/commands/grade.md +2 -0
  38. package/commands/graph.md +1 -0
  39. package/commands/hat-briefing.md +1 -0
  40. package/commands/heal.md +22 -170
  41. package/commands/help.md +54 -334
  42. package/commands/hmi-status.md +23 -144
  43. package/commands/jtbd.md +1 -0
  44. package/commands/leadership.md +2 -0
  45. package/commands/lean-canvas.md +2 -0
  46. package/commands/macro-trends.md +2 -0
  47. package/commands/map-unknowns.md +2 -0
  48. package/commands/memory.md +1 -0
  49. package/commands/models.md +1 -0
  50. package/commands/mos-reason.md +2 -0
  51. package/commands/mos.md +139 -0
  52. package/commands/mullins.md +2 -0
  53. package/commands/mva-brief.md +2 -0
  54. package/commands/mva-option.md +2 -0
  55. package/commands/new-project.md +2 -0
  56. package/commands/onboard.md +20 -7
  57. package/commands/operator.md +1 -0
  58. package/commands/opportunities.md +1 -0
  59. package/commands/organize.md +22 -469
  60. package/commands/persona.md +1 -0
  61. package/commands/pipeline.md +2 -0
  62. package/commands/present.md +1 -0
  63. package/commands/publish.md +2 -0
  64. package/commands/query.md +24 -102
  65. package/commands/radar.md +2 -0
  66. package/commands/reanalyze.md +1 -0
  67. package/commands/research.md +2 -0
  68. package/commands/room.md +2 -0
  69. package/commands/rooms.md +1 -0
  70. package/commands/root-cause.md +2 -0
  71. package/commands/rs-experts.md +1 -0
  72. package/commands/rs-explain.md +1 -0
  73. package/commands/rs-fetch.md +1 -0
  74. package/commands/rs-thesis.md +1 -0
  75. package/commands/scenario-plan.md +2 -0
  76. package/commands/scheduled-tasks.md +1 -0
  77. package/commands/score-innovation.md +2 -0
  78. package/commands/scout.md +1 -0
  79. package/commands/setup.md +2 -0
  80. package/commands/snapshot.md +2 -0
  81. package/commands/speakers.md +1 -0
  82. package/commands/splash.md +5 -2
  83. package/commands/status.md +1 -0
  84. package/commands/structure-argument.md +2 -0
  85. package/commands/suggest-next.md +2 -0
  86. package/commands/systems-thinking.md +2 -0
  87. package/commands/think-hats.md +2 -0
  88. package/commands/update.md +2 -0
  89. package/commands/user-needs.md +2 -0
  90. package/commands/validate.md +2 -0
  91. package/commands/value-proposition.md +2 -0
  92. package/commands/vault.md +2 -0
  93. package/commands/visualize.md +24 -29
  94. package/commands/whitespace.md +2 -1
  95. package/commands/wiki.md +1 -0
  96. package/hooks/hooks.json +22 -88
  97. package/lib/agents/auto-explore-agent.cjs +82 -0
  98. package/lib/core/breakthrough/canary.cjs +134 -0
  99. package/lib/core/breakthrough/canary.test.cjs +136 -0
  100. package/lib/core/breakthrough/detectors.cjs +359 -0
  101. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  102. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  103. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  104. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  105. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  106. package/lib/core/breakthrough/review-queue.cjs +154 -0
  107. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  108. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  109. package/lib/core/breakthrough/scanner.cjs +426 -0
  110. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  111. package/lib/core/breakthrough/schema.cjs +164 -0
  112. package/lib/core/breakthrough/schema.test.cjs +256 -0
  113. package/lib/core/breakthrough/scoring.cjs +293 -0
  114. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  115. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  116. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  117. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  118. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  119. package/lib/core/directive-envelope.cjs +175 -0
  120. package/lib/core/directive-envelope.test.cjs +225 -0
  121. package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
  122. package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
  123. package/lib/core/first-touch-version-stamper.cjs +113 -0
  124. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  125. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  126. package/lib/core/llm-name-suggester.cjs +194 -0
  127. package/lib/core/llm-name-suggester.test.cjs +132 -0
  128. package/lib/core/mcp-profiles.cjs +1 -1
  129. package/lib/core/migration-snapshot.cjs +172 -0
  130. package/lib/core/migration-snapshot.test.cjs +174 -0
  131. package/lib/core/mindrian-brain-shim.test.cjs +214 -0
  132. package/lib/core/mva-orchestrator.cjs +41 -0
  133. package/lib/core/mva-telemetry.cjs +31 -143
  134. package/lib/core/navigation/edges.cjs +35 -0
  135. package/lib/core/navigation/memory-events.cjs +126 -0
  136. package/lib/core/room-auto-create.cjs +318 -0
  137. package/lib/core/room-auto-create.test.cjs +198 -0
  138. package/lib/core/room-discard-cascade.cjs +225 -0
  139. package/lib/core/room-discard-cascade.test.cjs +135 -0
  140. package/lib/core/room-name-validator.cjs +132 -0
  141. package/lib/core/room-name-validator.test.cjs +156 -0
  142. package/lib/core/room-naming-selector.cjs +357 -0
  143. package/lib/core/room-naming-selector.test.cjs +277 -0
  144. package/lib/core/room-receipt-emit.cjs +63 -0
  145. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  146. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  147. package/lib/core/rs-nl-to-query.cjs +1 -1
  148. package/lib/core/stale-copy-scanner.cjs +190 -0
  149. package/lib/core/state-aware-router.cjs +78 -0
  150. package/lib/core/telemetry/schema.cjs +168 -0
  151. package/lib/core/telemetry/schema.test.cjs +124 -0
  152. package/lib/core/telemetry/validator.cjs +200 -0
  153. package/lib/core/telemetry/validator.test.cjs +188 -0
  154. package/lib/core/telemetry/writer.cjs +141 -0
  155. package/lib/core/telemetry/writer.test.cjs +331 -0
  156. package/lib/core/terminal-capability.cjs +88 -0
  157. package/lib/core/tier0-messaging.cjs +109 -0
  158. package/lib/core/tier0-messaging.test.cjs +218 -0
  159. package/lib/core/venture-shape-nudge.cjs +163 -0
  160. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  161. package/lib/core/visual-ops.cjs +70 -2
  162. package/lib/hmi/selector-dispatcher.cjs +90 -1
  163. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  164. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  165. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  166. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
  167. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  168. package/lib/memory/first-touch-version.test.cjs +198 -0
  169. package/lib/memory/help-coverage.test.cjs +108 -0
  170. package/lib/memory/help-renderer.test.cjs +145 -0
  171. package/lib/memory/mos-status-renderer.test.cjs +2 -2
  172. package/lib/memory/navigation-engine-core.test.cjs +1 -1
  173. package/lib/memory/palette-consistency.test.cjs +127 -0
  174. package/lib/memory/pending-tension-store.cjs +80 -0
  175. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  176. package/lib/memory/run-feynman-tests.cjs +223 -0
  177. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  178. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  179. package/lib/memory/soft-alias.test.cjs +144 -0
  180. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  181. package/lib/memory/state-aware-router.test.cjs +90 -0
  182. package/lib/memory/statusline-two-row.test.cjs +338 -0
  183. package/lib/memory/terminal-capability.test.cjs +155 -0
  184. package/lib/render/ROOM.md +74 -22
  185. package/lib/sessionstart/budget-compressor.cjs +130 -0
  186. package/lib/sessionstart/contributor-interface.cjs +134 -0
  187. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  188. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  189. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  190. package/lib/statusline/two-row-renderer.cjs +186 -0
  191. package/lib/statusline/version-resolver.cjs +81 -0
  192. package/package.json +1 -1
  193. package/references/visual/ROOM.md +55 -0
  194. package/references/visual/palette.json +54 -0
  195. package/skills/larry-personality/SKILL.md +34 -0
  196. package/skills/ui-system/SKILL.md +109 -1
  197. package/skills/ui-system/rules/dual-palette.md +156 -0
  198. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  199. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -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 };
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Phase 127-02 Task 2 (TDD RED -> GREEN) -- Class M Brain smoke probe tests.
6
+ *
7
+ * Covers BRAIN-MCP-127-08 acceptance (CONTEXT Deliverable 4):
8
+ * Test 1: LAYERS constant -- exactly 5 entries with stable ids
9
+ * Test 2: checkBrainSmoke() returns { ok, layers: [5 x {id,name,ok,reason,ms}], overall_ms }
10
+ * Test 3: L1 broken topology -> L2-L5 SKIPPED with reason "skipped-prior-layer-failed"
11
+ * Test 4: L1 OK + L2 no-key -> L3-L5 SKIPPED
12
+ * Test 5: L1 + L2 OK + L3 unreachable -> L4-L5 SKIPPED
13
+ * Test 6: L1 + L2 + L3 OK + L4 timeout -> L5 SKIPPED
14
+ * Test 7: All 5 layers pass when all mocks succeed; overall_ms < 30000
15
+ * Test 8: Each layer.ms >= 0 (sanity); ms fields are numbers
16
+ * Test 9: fixBrainSmoke is a no-op (diagnostic-only invariant)
17
+ * Test 10: opts injection seams (mockResolveRoot/mockResolveKey/mockSchema/mockSpawn)
18
+ *
19
+ * Hermetic via the opts injection seams -- NO real network IO, NO real spawn.
20
+ * The shell harness in tests/test-127-02-doctor-class-m.sh exercises real spawn
21
+ * against the actual shim binary.
22
+ *
23
+ * Canon parts:
24
+ * - Part 7 (reuse): LAYERS L1/L2/L3 import the existing resolver chokepoints
25
+ * (active-plugin-root, resolve-brain-key, brain-client.schema)
26
+ * - Part 8 (graph boundary): the smoke probe queries brain_schema only (a
27
+ * generic methodology handle); zero user content
28
+ * egress in the smoke surface.
29
+ *
30
+ * HARD RULE: no em-dashes.
31
+ */
32
+
33
+ const assert = require('node:assert/strict');
34
+ const fs = require('node:fs');
35
+ const path = require('node:path');
36
+
37
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
38
+ const SMOKE_PATH = path.join(REPO_ROOT, 'lib', 'core', 'doctor', 'class-m-brain-smoke.cjs');
39
+
40
+ let passed = 0;
41
+ let failed = 0;
42
+
43
+ function ok(name) {
44
+ passed += 1;
45
+ process.stdout.write(' ok ' + name + '\n');
46
+ }
47
+
48
+ function fail(name, err) {
49
+ failed += 1;
50
+ process.stdout.write(' FAIL ' + name + '\n');
51
+ if (err) process.stdout.write(' ' + (err.message || String(err)) + '\n');
52
+ }
53
+
54
+ // Helper: clean require cache so each test gets a fresh module instance with
55
+ // fresh closure state for the opts seams.
56
+ function freshLoad() {
57
+ delete require.cache[SMOKE_PATH];
58
+ return require(SMOKE_PATH);
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Test 1: LAYERS constant shape.
63
+ // ---------------------------------------------------------------------------
64
+ (async function test1_layers_constant() {
65
+ const label = 'LAYERS constant: 5 frozen entries with stable ids';
66
+ try {
67
+ const mod = freshLoad();
68
+ assert.ok(Array.isArray(mod.LAYERS), 'LAYERS must be an array');
69
+ assert.equal(mod.LAYERS.length, 5, 'LAYERS must have exactly 5 entries');
70
+ const ids = mod.LAYERS.map(l => l.id);
71
+ assert.deepEqual(ids, [
72
+ 'plugin_root',
73
+ 'key_resolver',
74
+ 'https_schema',
75
+ 'stdio_handshake',
76
+ 'e2e_brain_schema',
77
+ ], 'LAYERS ids must match the canonical 5-layer probe order');
78
+ for (const l of mod.LAYERS) {
79
+ assert.equal(typeof l.id, 'string', 'each layer has string id');
80
+ assert.equal(typeof l.name, 'string', 'each layer has string name');
81
+ }
82
+ ok(label);
83
+ } catch (e) { fail(label, e); }
84
+ })();
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Test 2: checkBrainSmoke() return shape.
88
+ // ---------------------------------------------------------------------------
89
+ (async function test2_return_shape() {
90
+ const label = 'checkBrainSmoke() returns { ok, layers:[5], overall_ms }';
91
+ try {
92
+ const mod = freshLoad();
93
+ const result = await mod.checkBrainSmoke({
94
+ // All-pass mock chain for shape assertion
95
+ mockResolveRoot: () => ({ root: '/tmp/fake-root', source: 'env', topology: 'dev-clone' }),
96
+ mockResolveKey: () => ({ key: 'k', source: 'env', available: true, reason: null }),
97
+ mockSchema: async () => ({ labels: ['Framework'], rel_types: [] }),
98
+ mockSpawn: async (_shimPath, opts) => ({ ok: true, reason: 'mocked ' + opts.intent }),
99
+ });
100
+ assert.equal(typeof result.ok, 'boolean', 'ok must be boolean');
101
+ assert.ok(Array.isArray(result.layers), 'layers must be an array');
102
+ assert.equal(result.layers.length, 5, 'layers must have 5 entries');
103
+ assert.equal(typeof result.overall_ms, 'number', 'overall_ms must be number');
104
+ assert.ok(result.overall_ms >= 0, 'overall_ms must be >= 0');
105
+ for (const l of result.layers) {
106
+ assert.equal(typeof l.id, 'string', 'layer.id is string');
107
+ assert.equal(typeof l.name, 'string', 'layer.name is string');
108
+ assert.equal(typeof l.ok, 'boolean', 'layer.ok is boolean');
109
+ assert.equal(typeof l.reason, 'string', 'layer.reason is string');
110
+ assert.equal(typeof l.ms, 'number', 'layer.ms is number');
111
+ }
112
+ ok(label);
113
+ } catch (e) { fail(label, e); }
114
+ })();
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Test 3: L1 broken -> L2..L5 skipped.
118
+ // ---------------------------------------------------------------------------
119
+ (async function test3_l1_broken_cascade() {
120
+ const label = 'L1 broken -> L2-L5 cascade to skipped-prior-layer-failed';
121
+ try {
122
+ const mod = freshLoad();
123
+ const result = await mod.checkBrainSmoke({
124
+ mockResolveRoot: () => ({ root: null, source: 'not-found', topology: 'not-found' }),
125
+ });
126
+ assert.equal(result.ok, false, 'overall must be false');
127
+ assert.equal(result.layers[0].id, 'plugin_root', 'L1 id');
128
+ assert.equal(result.layers[0].ok, false, 'L1 must be false');
129
+ assert.match(result.layers[0].reason, /plugin root not resolved/i, 'L1 reason matches');
130
+ for (let i = 1; i < 5; i++) {
131
+ assert.equal(result.layers[i].ok, false, 'L' + (i + 1) + ' must be false (skipped)');
132
+ assert.equal(result.layers[i].reason, 'skipped-prior-layer-failed',
133
+ 'L' + (i + 1) + ' reason must be skipped-prior-layer-failed');
134
+ }
135
+ ok(label);
136
+ } catch (e) { fail(label, e); }
137
+ })();
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Test 4: L1 OK + L2 no key -> L3-L5 skipped.
141
+ // ---------------------------------------------------------------------------
142
+ (async function test4_l2_no_key_cascade() {
143
+ const label = 'L1 OK + L2 no key -> L3-L5 cascade to skipped';
144
+ try {
145
+ const mod = freshLoad();
146
+ const result = await mod.checkBrainSmoke({
147
+ mockResolveRoot: () => ({ root: '/tmp/fake', source: 'env', topology: 'dev-clone' }),
148
+ mockResolveKey: () => ({ key: null, source: 'not-found', available: false, reason: 'MINDRIAN_BRAIN_KEY not set (env) ...' }),
149
+ });
150
+ assert.equal(result.ok, false, 'overall must be false');
151
+ assert.equal(result.layers[0].ok, true, 'L1 must be true');
152
+ assert.equal(result.layers[1].ok, false, 'L2 must be false');
153
+ assert.match(result.layers[1].reason, /MINDRIAN_BRAIN_KEY not set/, 'L2 reason carries the resolver reason');
154
+ for (let i = 2; i < 5; i++) {
155
+ assert.equal(result.layers[i].ok, false, 'L' + (i + 1) + ' must be false (skipped)');
156
+ assert.equal(result.layers[i].reason, 'skipped-prior-layer-failed',
157
+ 'L' + (i + 1) + ' reason must be skipped-prior-layer-failed');
158
+ }
159
+ ok(label);
160
+ } catch (e) { fail(label, e); }
161
+ })();
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Test 5: L1+L2 OK + L3 unreachable -> L4-L5 skipped.
165
+ // ---------------------------------------------------------------------------
166
+ (async function test5_l3_unreachable_cascade() {
167
+ const label = 'L1+L2 OK + L3 returns null -> L4-L5 cascade to skipped';
168
+ try {
169
+ const mod = freshLoad();
170
+ const result = await mod.checkBrainSmoke({
171
+ mockResolveRoot: () => ({ root: '/tmp/fake', source: 'env', topology: 'dev-clone' }),
172
+ mockResolveKey: () => ({ key: 'k', source: 'env', available: true, reason: null }),
173
+ mockSchema: async () => null,
174
+ });
175
+ assert.equal(result.ok, false, 'overall must be false');
176
+ assert.equal(result.layers[0].ok, true, 'L1 ok');
177
+ assert.equal(result.layers[1].ok, true, 'L2 ok');
178
+ assert.equal(result.layers[2].ok, false, 'L3 must be false');
179
+ assert.match(result.layers[2].reason, /HTTPS|schema|unreachable/i, 'L3 reason matches');
180
+ for (let i = 3; i < 5; i++) {
181
+ assert.equal(result.layers[i].ok, false, 'L' + (i + 1) + ' must be false (skipped)');
182
+ assert.equal(result.layers[i].reason, 'skipped-prior-layer-failed',
183
+ 'L' + (i + 1) + ' reason must be skipped-prior-layer-failed');
184
+ }
185
+ ok(label);
186
+ } catch (e) { fail(label, e); }
187
+ })();
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Test 6: L1+L2+L3 OK + L4 handshake timeout -> L5 skipped.
191
+ // ---------------------------------------------------------------------------
192
+ (async function test6_l4_handshake_timeout() {
193
+ const label = 'L1+L2+L3 OK + L4 timeout -> L5 cascade to skipped';
194
+ try {
195
+ const mod = freshLoad();
196
+ const result = await mod.checkBrainSmoke({
197
+ mockResolveRoot: () => ({ root: '/tmp/fake', source: 'env', topology: 'dev-clone' }),
198
+ mockResolveKey: () => ({ key: 'k', source: 'env', available: true, reason: null }),
199
+ mockSchema: async () => ({ labels: ['Framework'] }),
200
+ mockSpawn: async (_shimPath, opts) => {
201
+ if (opts.intent === 'handshake') return { ok: false, reason: 'handshake timed out after 10000ms' };
202
+ return { ok: true, reason: 'should-not-be-called' };
203
+ },
204
+ });
205
+ assert.equal(result.ok, false, 'overall must be false');
206
+ assert.equal(result.layers[3].ok, false, 'L4 must be false');
207
+ assert.match(result.layers[3].reason, /handshake|timeout/i, 'L4 reason matches');
208
+ assert.equal(result.layers[4].ok, false, 'L5 must be false (skipped)');
209
+ assert.equal(result.layers[4].reason, 'skipped-prior-layer-failed', 'L5 reason skipped');
210
+ ok(label);
211
+ } catch (e) { fail(label, e); }
212
+ })();
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // Test 7: All 5 layers pass; overall_ms < 30000.
216
+ // ---------------------------------------------------------------------------
217
+ (async function test7_all_pass() {
218
+ const label = 'All 5 layers PASS via mocks; overall_ms < 30000 (30s budget)';
219
+ try {
220
+ const mod = freshLoad();
221
+ const result = await mod.checkBrainSmoke({
222
+ mockResolveRoot: () => ({ root: '/tmp/fake', source: 'env', topology: 'dev-clone' }),
223
+ mockResolveKey: () => ({ key: 'k', source: 'env', available: true, reason: null }),
224
+ mockSchema: async () => ({ labels: ['Framework'] }),
225
+ mockSpawn: async (_shimPath, _opts) => ({ ok: true, reason: 'mocked-success' }),
226
+ });
227
+ assert.equal(result.ok, true, 'overall must be true');
228
+ for (const l of result.layers) {
229
+ assert.equal(l.ok, true, 'layer ' + l.id + ' must be true');
230
+ }
231
+ assert.ok(result.overall_ms < 30000, 'overall_ms must be under 30s budget (got ' + result.overall_ms + ')');
232
+ ok(label);
233
+ } catch (e) { fail(label, e); }
234
+ })();
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Test 8: ms-field sanity.
238
+ // ---------------------------------------------------------------------------
239
+ (async function test8_ms_sanity() {
240
+ const label = 'Each layer.ms is non-negative number';
241
+ try {
242
+ const mod = freshLoad();
243
+ const result = await mod.checkBrainSmoke({
244
+ mockResolveRoot: () => ({ root: '/tmp/fake', source: 'env', topology: 'dev-clone' }),
245
+ mockResolveKey: () => ({ key: 'k', source: 'env', available: true, reason: null }),
246
+ mockSchema: async () => ({ labels: ['Framework'] }),
247
+ mockSpawn: async (_shimPath, _opts) => ({ ok: true, reason: 'ok' }),
248
+ });
249
+ for (const l of result.layers) {
250
+ assert.equal(typeof l.ms, 'number', 'layer.ms is number');
251
+ assert.ok(l.ms >= 0, 'layer.ms must be >= 0 (got ' + l.ms + ')');
252
+ }
253
+ ok(label);
254
+ } catch (e) { fail(label, e); }
255
+ })();
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Test 9: fixBrainSmoke is a no-op (diagnostic-only).
259
+ // ---------------------------------------------------------------------------
260
+ (async function test9_fix_is_noop() {
261
+ const label = 'fixBrainSmoke(result) is a no-op (diagnostic-only invariant)';
262
+ try {
263
+ const mod = freshLoad();
264
+ const fakeResult = { ok: false, layers: [], overall_ms: 0 };
265
+ const r = mod.fixBrainSmoke(fakeResult);
266
+ assert.equal(r.fixed, false, 'fixed must be false');
267
+ assert.match(r.reason, /diagnostic-only/, 'reason must indicate diagnostic-only nature');
268
+ ok(label);
269
+ } catch (e) { fail(label, e); }
270
+ })();
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // Test 10: opts injection seams + Class M letter consistency.
274
+ // ---------------------------------------------------------------------------
275
+ (async function test10_opts_seams_and_class_m() {
276
+ const label = 'opts injection seams enable hermetic testing + Class M referenced in source';
277
+ try {
278
+ const mod = freshLoad();
279
+ // Verify all four seams individually substitute their layer.
280
+ let mockSchemaCalled = false;
281
+ let mockSpawnCalled = false;
282
+ const result = await mod.checkBrainSmoke({
283
+ mockResolveRoot: () => ({ root: '/tmp/seam', source: 'opts-mock', topology: 'dev-clone' }),
284
+ mockResolveKey: () => ({ key: 'seam-key', source: 'opts-mock', available: true, reason: null }),
285
+ mockSchema: async () => { mockSchemaCalled = true; return { labels: [], rel_types: [] }; },
286
+ mockSpawn: async (_shimPath, _opts) => { mockSpawnCalled = true; return { ok: true, reason: 'seam-mock' }; },
287
+ });
288
+ assert.equal(result.ok, true, 'all seams provided -> overall true');
289
+ assert.equal(mockSchemaCalled, true, 'mockSchema seam invoked for L3');
290
+ assert.equal(mockSpawnCalled, true, 'mockSpawn seam invoked for L4 and L5');
291
+ assert.equal(result.layers[0].reason, 'resolved (source=opts-mock, topology=dev-clone)', 'L1 reason carries source');
292
+ // Source-level class letter check (CRITICAL: plan uses Class M, not K).
293
+ const src = fs.readFileSync(SMOKE_PATH, 'utf8');
294
+ assert.match(src, /Class[- ]?M/, 'source must reference Class M (not Class K -- K is taken by --stale-first-touch)');
295
+ assert.equal(src.indexOf('Class K'), -1, 'source MUST NOT reference Class K (collides with existing --stale-first-touch)');
296
+ ok(label);
297
+ } catch (e) { fail(label, e); }
298
+ })();
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Summary -- IIFEs above are async; collect results after a microtask drain.
302
+ // ---------------------------------------------------------------------------
303
+ setImmediate(function summarize() {
304
+ // Allow any straggling async test to settle. Two passes of setImmediate is
305
+ // enough for the trivial test bodies above.
306
+ setImmediate(function () {
307
+ process.stdout.write('\nPASSED: ' + passed + '\nFAILED: ' + failed + '\n');
308
+ process.exit(failed === 0 ? 0 : 1);
309
+ });
310
+ });