@mindrian_os/install 1.13.0-beta.16 → 1.13.0-beta.19

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 (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +36 -0
  3. package/commands/act.md +1 -0
  4. package/commands/admin.md +1 -0
  5. package/commands/analyze-needs.md +2 -0
  6. package/commands/analyze-systems.md +2 -0
  7. package/commands/analyze-timing.md +2 -0
  8. package/commands/auto-explore.md +2 -0
  9. package/commands/beautiful-question.md +2 -0
  10. package/commands/brain-derive.md +2 -0
  11. package/commands/build-knowledge.md +2 -0
  12. package/commands/build-thesis.md +2 -0
  13. package/commands/causal.md +2 -0
  14. package/commands/challenge-assumptions.md +2 -0
  15. package/commands/compare-ventures.md +2 -0
  16. package/commands/dashboard.md +2 -1
  17. package/commands/deep-grade.md +2 -0
  18. package/commands/diagnose.md +21 -1
  19. package/commands/diagnostics.md +14 -3
  20. package/commands/doctor.md +4 -1
  21. package/commands/dogfood-flush.md +92 -0
  22. package/commands/dominant-designs.md +2 -0
  23. package/commands/explain-decision.md +2 -0
  24. package/commands/explore-domains.md +2 -0
  25. package/commands/explore-futures.md +2 -0
  26. package/commands/explore-trends.md +2 -0
  27. package/commands/export.md +1 -0
  28. package/commands/feynman-timeline-refresh.md +2 -0
  29. package/commands/file-meeting.md +4 -0
  30. package/commands/find-analogies.md +1 -0
  31. package/commands/find-bottlenecks.md +2 -0
  32. package/commands/find-connections.md +2 -0
  33. package/commands/funding.md +1 -0
  34. package/commands/grade.md +4 -0
  35. package/commands/graph.md +1 -0
  36. package/commands/hat-briefing.md +1 -0
  37. package/commands/heal.md +22 -170
  38. package/commands/help.md +54 -334
  39. package/commands/hmi-status.md +23 -144
  40. package/commands/jtbd.md +1 -0
  41. package/commands/leadership.md +2 -0
  42. package/commands/lean-canvas.md +2 -0
  43. package/commands/macro-trends.md +2 -0
  44. package/commands/map-unknowns.md +2 -0
  45. package/commands/memory.md +1 -0
  46. package/commands/models.md +1 -0
  47. package/commands/mos-reason.md +2 -0
  48. package/commands/mos.md +139 -0
  49. package/commands/mullins.md +2 -0
  50. package/commands/mva-brief.md +58 -0
  51. package/commands/mva-option.md +91 -0
  52. package/commands/new-project.md +4 -0
  53. package/commands/onboard.md +22 -7
  54. package/commands/operator.md +1 -0
  55. package/commands/opportunities.md +1 -0
  56. package/commands/organize.md +22 -469
  57. package/commands/persona.md +1 -0
  58. package/commands/pipeline.md +2 -0
  59. package/commands/present.md +1 -0
  60. package/commands/publish.md +2 -0
  61. package/commands/query.md +24 -102
  62. package/commands/radar.md +2 -0
  63. package/commands/reanalyze.md +1 -0
  64. package/commands/research.md +2 -0
  65. package/commands/room.md +2 -0
  66. package/commands/rooms.md +1 -0
  67. package/commands/root-cause.md +2 -0
  68. package/commands/rs-experts.md +1 -0
  69. package/commands/rs-explain.md +1 -0
  70. package/commands/rs-fetch.md +1 -0
  71. package/commands/rs-thesis.md +1 -0
  72. package/commands/scenario-plan.md +2 -0
  73. package/commands/scheduled-tasks.md +1 -0
  74. package/commands/score-innovation.md +2 -0
  75. package/commands/scout.md +1 -0
  76. package/commands/setup.md +2 -0
  77. package/commands/snapshot.md +2 -0
  78. package/commands/speakers.md +1 -0
  79. package/commands/splash.md +5 -2
  80. package/commands/status.md +1 -0
  81. package/commands/structure-argument.md +2 -0
  82. package/commands/suggest-next.md +2 -0
  83. package/commands/systems-thinking.md +2 -0
  84. package/commands/think-hats.md +2 -0
  85. package/commands/update.md +2 -0
  86. package/commands/user-needs.md +2 -0
  87. package/commands/validate.md +2 -0
  88. package/commands/value-proposition.md +2 -0
  89. package/commands/vault.md +2 -0
  90. package/commands/visualize.md +24 -29
  91. package/commands/whitespace.md +2 -1
  92. package/commands/wiki.md +1 -0
  93. package/hooks/hooks.json +31 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/agents/mva/brain-classic-traps.cjs +77 -0
  96. package/lib/agents/mva/brain-cross-domain.cjs +79 -0
  97. package/lib/agents/mva/brain-similar-ventures.cjs +93 -0
  98. package/lib/agents/mva/dashboard-graph-neighborhood.cjs +72 -0
  99. package/lib/agents/mva/index.cjs +42 -0
  100. package/lib/agents/mva/six-hats-red-black.cjs +137 -0
  101. package/lib/agents/mva/tavily-funding-scan.cjs +147 -0
  102. package/lib/agents/mva/test-all-six-agents.cjs +467 -0
  103. package/lib/conversation/operator.cjs +64 -0
  104. package/lib/conversation/operator.test.cjs +160 -0
  105. package/lib/core/breakthrough/canary.cjs +134 -0
  106. package/lib/core/breakthrough/canary.test.cjs +136 -0
  107. package/lib/core/breakthrough/detectors.cjs +359 -0
  108. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  109. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  110. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  111. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  112. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  113. package/lib/core/breakthrough/review-queue.cjs +154 -0
  114. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  115. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  116. package/lib/core/breakthrough/scanner.cjs +426 -0
  117. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  118. package/lib/core/breakthrough/schema.cjs +164 -0
  119. package/lib/core/breakthrough/schema.test.cjs +256 -0
  120. package/lib/core/breakthrough/scoring.cjs +293 -0
  121. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  122. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  123. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  124. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  125. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  126. package/lib/core/first-touch-version-stamper.cjs +113 -0
  127. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  128. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  129. package/lib/core/llm-name-suggester.cjs +194 -0
  130. package/lib/core/llm-name-suggester.test.cjs +132 -0
  131. package/lib/core/mva-agent-contract.cjs +170 -0
  132. package/lib/core/mva-agent-contract.test.cjs +169 -0
  133. package/lib/core/mva-budget.cjs +75 -0
  134. package/lib/core/mva-budget.test.cjs +68 -0
  135. package/lib/core/mva-classifier.cjs +370 -0
  136. package/lib/core/mva-classifier.test.cjs +248 -0
  137. package/lib/core/mva-deck-builder.cjs +452 -0
  138. package/lib/core/mva-deck-builder.test.cjs +287 -0
  139. package/lib/core/mva-detect.smoke.test.cjs +197 -0
  140. package/lib/core/mva-dispatcher.cjs +110 -0
  141. package/lib/core/mva-dispatcher.test.cjs +216 -0
  142. package/lib/core/mva-option-router.cjs +292 -0
  143. package/lib/core/mva-option-router.test.cjs +483 -0
  144. package/lib/core/mva-orchestrator.cjs +365 -0
  145. package/lib/core/mva-orchestrator.test.cjs +908 -0
  146. package/lib/core/mva-progressive-renderer.cjs +194 -0
  147. package/lib/core/mva-progressive-renderer.test.cjs +157 -0
  148. package/lib/core/mva-rule-linter.cjs +213 -0
  149. package/lib/core/mva-rule-linter.test.cjs +336 -0
  150. package/lib/core/mva-state.cjs +159 -0
  151. package/lib/core/mva-telemetry.cjs +58 -0
  152. package/lib/core/mva-telemetry.test.cjs +196 -0
  153. package/lib/core/mva-vercel-deploy.cjs +168 -0
  154. package/lib/core/mva-vercel-deploy.test.cjs +239 -0
  155. package/lib/core/navigation/dashboard-helpers.cjs +145 -0
  156. package/lib/core/navigation/edges.cjs +35 -0
  157. package/lib/core/navigation/memory-events.cjs +126 -0
  158. package/lib/core/navigation.cjs +11 -0
  159. package/lib/core/resolve-vercel-key.cjs +107 -0
  160. package/lib/core/resolve-vercel-key.test.cjs +137 -0
  161. package/lib/core/room-auto-create.cjs +318 -0
  162. package/lib/core/room-auto-create.test.cjs +198 -0
  163. package/lib/core/room-discard-cascade.cjs +225 -0
  164. package/lib/core/room-discard-cascade.test.cjs +135 -0
  165. package/lib/core/room-name-validator.cjs +132 -0
  166. package/lib/core/room-name-validator.test.cjs +156 -0
  167. package/lib/core/room-naming-selector.cjs +357 -0
  168. package/lib/core/room-naming-selector.test.cjs +277 -0
  169. package/lib/core/room-receipt-emit.cjs +63 -0
  170. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  171. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  172. package/lib/core/stale-copy-scanner.cjs +190 -0
  173. package/lib/core/state-aware-router.cjs +78 -0
  174. package/lib/core/telemetry/schema.cjs +168 -0
  175. package/lib/core/telemetry/schema.test.cjs +124 -0
  176. package/lib/core/telemetry/validator.cjs +197 -0
  177. package/lib/core/telemetry/validator.test.cjs +188 -0
  178. package/lib/core/telemetry/writer.cjs +141 -0
  179. package/lib/core/telemetry/writer.test.cjs +331 -0
  180. package/lib/core/terminal-capability.cjs +88 -0
  181. package/lib/core/venture-shape-nudge.cjs +163 -0
  182. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  183. package/lib/core/visual-ops.cjs +70 -2
  184. package/lib/hmi/selector-dispatcher.cjs +90 -1
  185. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  186. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  187. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  188. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  189. package/lib/memory/first-touch-version.test.cjs +198 -0
  190. package/lib/memory/help-coverage.test.cjs +108 -0
  191. package/lib/memory/help-renderer.test.cjs +145 -0
  192. package/lib/memory/palette-consistency.test.cjs +127 -0
  193. package/lib/memory/pending-tension-store.cjs +80 -0
  194. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  195. package/lib/memory/run-feynman-tests.cjs +240 -0
  196. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  197. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  198. package/lib/memory/soft-alias.test.cjs +144 -0
  199. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  200. package/lib/memory/state-aware-router.test.cjs +90 -0
  201. package/lib/memory/statusline-two-row.test.cjs +338 -0
  202. package/lib/memory/terminal-capability.test.cjs +155 -0
  203. package/lib/render/ROOM.md +74 -22
  204. package/lib/sessionstart/budget-compressor.cjs +130 -0
  205. package/lib/sessionstart/contributor-interface.cjs +134 -0
  206. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  207. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  208. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  209. package/lib/statusline/two-row-renderer.cjs +186 -0
  210. package/lib/statusline/version-resolver.cjs +81 -0
  211. package/package.json +1 -1
  212. package/references/visual/ROOM.md +55 -0
  213. package/references/visual/palette.json +54 -0
  214. package/skills/larry-personality/SKILL.md +34 -0
  215. package/skills/mva-pipeline/SKILL.md +129 -0
  216. package/skills/ui-system/SKILL.md +109 -1
  217. package/skills/ui-system/rules/dual-palette.md +156 -0
  218. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  219. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,287 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-04 Plan 04 Task 2 -- mva-deck-builder tests.
5
+ *
6
+ * The deck builder is a pure function that takes an OrchestratorOutcome
7
+ * (from Plan 118-03) and returns a full HTML document. De Stijl theme,
8
+ * INLINE styles only (no <style> blocks), Mondrian palette, em-dash-free.
9
+ *
10
+ * Canon Part 8 invariants:
11
+ * - The deck contains the RENDERED summary_lines + structured deck_data
12
+ * (already sanitized by Plan 118-02 agents).
13
+ * - The raw sentence is NEVER serialized into the HTML.
14
+ * - The sentence sha256 hash IS rendered (as a hash marker, not a leak).
15
+ *
16
+ * Em-dash discipline (per feedback_no_emdashes.md): every emitted string
17
+ * uses `--` and `-` ONLY. The 3-option footer uses `--` not `—`.
18
+ *
19
+ * No real-name discipline (per feedback_no_real_names_in_repo.md): the
20
+ * template MUST NOT mention testers/advisors/partners.
21
+ *
22
+ * Per OQ13 lean: deck includes "Generated by MindrianOS" footer with link to
23
+ * the install minisite.
24
+ *
25
+ * Per OQ15 lean: deck shows static 3-option text + copy-to-clipboard button
26
+ * for the brief sha8.
27
+ *
28
+ * NIT-3 palette parity: DECK_PALETTE must match the De Stijl source-of-truth
29
+ * (lib/wiki/wiki-layout.cjs CSS variables; lib/core/visual-ops.cjs DS_HEX).
30
+ */
31
+ 'use strict';
32
+
33
+ const test = require('node:test');
34
+ const assert = require('node:assert/strict');
35
+ const fs = require('node:fs');
36
+ const path = require('node:path');
37
+
38
+ const { buildDeck, buildSlide, DECK_PALETTE } = require('./mva-deck-builder.cjs');
39
+
40
+ // ---------- Synthetic OrchestratorOutcome factories ----------
41
+
42
+ function _agentOk(agent_id, summary_line, extra) {
43
+ return {
44
+ agent_id,
45
+ status: 'ok',
46
+ duration_ms: 100,
47
+ payload: Object.assign({ summary_line }, extra || {}),
48
+ };
49
+ }
50
+
51
+ function _agentEmpty(agent_id, reason) {
52
+ return {
53
+ agent_id,
54
+ status: 'empty',
55
+ duration_ms: 10,
56
+ payload: { reason: reason || 'no_findings' },
57
+ };
58
+ }
59
+
60
+ function _agentError(agent_id) {
61
+ return {
62
+ agent_id,
63
+ status: 'error',
64
+ duration_ms: 10,
65
+ error: 'mock_error',
66
+ payload: null,
67
+ };
68
+ }
69
+
70
+ function _sixOkOutcome() {
71
+ const sha = '0'.repeat(64);
72
+ return {
73
+ results: [
74
+ _agentOk('brain_similar', 'Found 3 ventures in this space: A, B, C',
75
+ { deck_data: { ventures: [{ name: 'A', stage: 'seed' }, { name: 'B', stage: 'pre-seed' }] } }),
76
+ _agentOk('brain_cross_domain', 'Cross-domain analogy: ConstraintTheory -- bottleneck pattern',
77
+ { deck_data: { framework: 'ConstraintTheory', signature: 'bottleneck pattern' } }),
78
+ _agentOk('brain_classic_traps', 'Classic trap: PrematureScale -- raised before PMF',
79
+ { deck_data: { trap: 'PrematureScale', signature: 'raised before PMF' } }),
80
+ _agentOk('tavily_funding', 'Live funding match: NSF SBIR Phase I -- deadline 2026-06-15',
81
+ { deck_data: { title: 'NSF SBIR Phase I', deadline: '2026-06-15' } }),
82
+ _agentOk('six_hats_red_black', "One question you haven't asked yourself: What if no one cares?",
83
+ { deck_data: { question: 'What if no one cares?' } }),
84
+ _agentOk('dashboard_graph', 'Your room already has 5 related decision nodes. Quick preview:',
85
+ { deck_data: { node_count: 5, nodes: ['Idea-2026-03', 'Pivot-2026-04'] } }),
86
+ ],
87
+ rendered: '(stub terminal output)',
88
+ footer_data: { ok: 6, failed: 0, sha256: sha },
89
+ };
90
+ }
91
+
92
+ // ---------- Tests ----------
93
+
94
+ test('Test 1: 6 agents ok -> 6 slides + header + footer; valid HTML', () => {
95
+ const outcome = _sixOkOutcome();
96
+ const html = buildDeck(outcome);
97
+ assert.ok(html.startsWith('<!DOCTYPE html>'), 'must start with DOCTYPE');
98
+ assert.ok(html.endsWith('</html>\n') || html.endsWith('</html>'),
99
+ 'must end with </html>');
100
+ // 6 slide articles
101
+ const slideCount = (html.match(/<article\b/g) || []).length;
102
+ assert.equal(slideCount, 6, 'expected 6 slides, got ' + slideCount);
103
+ assert.ok(html.length > 2000, 'deck should be substantive (>2000 chars), got ' + html.length);
104
+ });
105
+
106
+ test('Test 2: em-dash sweep -- zero — characters in output', () => {
107
+ const outcome = _sixOkOutcome();
108
+ const html = buildDeck(outcome);
109
+ const matches = html.match(/—/g);
110
+ assert.equal(matches, null, 'no em-dashes (—) allowed; found ' + (matches && matches.length));
111
+ // Verify `--` IS present in 3-option footer
112
+ assert.ok(html.includes("Challenge me -- Devil"), 'footer must contain "Challenge me -- Devil"');
113
+ });
114
+
115
+ test('Test 3: no <style> blocks anywhere in output', () => {
116
+ const outcome = _sixOkOutcome();
117
+ const html = buildDeck(outcome);
118
+ assert.equal(/<style[^>]*>/.test(html), false,
119
+ 'no <style> blocks allowed; inline styles only');
120
+ });
121
+
122
+ test('Test 4: inline styles present (>= 10 style="" attributes)', () => {
123
+ const outcome = _sixOkOutcome();
124
+ const html = buildDeck(outcome);
125
+ const inlineMatches = html.match(/style="[^"]+"/g) || [];
126
+ assert.ok(inlineMatches.length >= 10,
127
+ 'expected >= 10 inline style attributes, got ' + inlineMatches.length);
128
+ });
129
+
130
+ test('Test 5: Mondrian/De Stijl palette present (>= 3 of canonical hexes)', () => {
131
+ const outcome = _sixOkOutcome();
132
+ const html = buildDeck(outcome);
133
+ const palette = Object.values(DECK_PALETTE);
134
+ let hits = 0;
135
+ for (const hex of palette) {
136
+ if (html.toUpperCase().includes(hex.toUpperCase())) hits += 1;
137
+ }
138
+ assert.ok(hits >= 3, 'expected >= 3 palette hexes in HTML, got ' + hits);
139
+ });
140
+
141
+ test('Test 6: 3 empty agents render placeholder slides', () => {
142
+ const sha = 'a'.repeat(64);
143
+ const outcome = {
144
+ results: [
145
+ _agentOk('brain_similar', 'A B C'),
146
+ _agentEmpty('brain_cross_domain'),
147
+ _agentEmpty('brain_classic_traps'),
148
+ _agentEmpty('tavily_funding', 'tavily_unavailable'),
149
+ _agentOk('six_hats_red_black', "What if no one cares?"),
150
+ _agentOk('dashboard_graph', 'Your room has 2 nodes.'),
151
+ ],
152
+ rendered: '',
153
+ footer_data: { ok: 3, failed: 3, sha256: sha },
154
+ };
155
+ const html = buildDeck(outcome);
156
+ const slideCount = (html.match(/<article\b/g) || []).length;
157
+ assert.equal(slideCount, 6, '6 slides total (3 ok + 3 empty placeholders)');
158
+ // Placeholder phrase appears (matches renderer empty-state strings)
159
+ assert.ok(html.includes('no findings this pass') || html.includes('Live funding scan'),
160
+ 'empty agents should render placeholder text');
161
+ });
162
+
163
+ test('Test 7: all-fail -> single sharp-question slide', () => {
164
+ const sha = 'b'.repeat(64);
165
+ const outcome = {
166
+ results: [
167
+ _agentError('brain_similar'),
168
+ _agentError('brain_cross_domain'),
169
+ _agentError('brain_classic_traps'),
170
+ _agentError('tavily_funding'),
171
+ _agentError('six_hats_red_black'),
172
+ _agentError('dashboard_graph'),
173
+ ],
174
+ rendered: '',
175
+ footer_data: { ok: 0, failed: 6, sha256: sha },
176
+ };
177
+ const html = buildDeck(outcome);
178
+ // Exactly one slide article on all-fail
179
+ const slideCount = (html.match(/<article\b/g) || []).length;
180
+ assert.equal(slideCount, 1, 'all-fail should render exactly 1 slide');
181
+ // Verbatim sharp-question source-spec text (em-dash-free)
182
+ assert.ok(html.includes("didn't find precedents for this in 30 seconds"),
183
+ 'sharp-question text must appear');
184
+ assert.ok(html.includes('genuinely unexplored space'),
185
+ 'sharp-question second line must appear');
186
+ });
187
+
188
+ test('Test 8: raw-sentence absent -- only sha8 marker appears, no "sentence"/"prompt"/"user_input" fields', () => {
189
+ const outcome = _sixOkOutcome();
190
+ const html = buildDeck(outcome);
191
+ // sha8 (first 8 chars of sha256) IS rendered as identifier
192
+ assert.ok(html.includes(outcome.footer_data.sha256.slice(0, 8)),
193
+ 'sha8 should appear as the brief identifier');
194
+ // But raw "sentence" / "prompt" / "user_input" field labels do NOT appear
195
+ // (We check for the field-label patterns, not arbitrary occurrences of "sentence")
196
+ assert.equal(/raw_sentence|MVA_SENTENCE|user_input|user_prompt/i.test(html), false,
197
+ 'no raw sentence field labels should appear in HTML');
198
+ });
199
+
200
+ test('Test 9: footer includes "Generated by MindrianOS" attribution + install-site link', () => {
201
+ const outcome = _sixOkOutcome();
202
+ const html = buildDeck(outcome);
203
+ assert.ok(html.includes('Generated by MindrianOS'),
204
+ 'footer must include "Generated by MindrianOS" attribution');
205
+ assert.ok(html.includes('mindrianos-install-site.vercel.app'),
206
+ 'footer must link to install minisite');
207
+ assert.ok(html.includes(outcome.footer_data.sha256.slice(0, 8)),
208
+ 'footer must include sha8 marker');
209
+ });
210
+
211
+ test('Test 10: 3-option footer text + copy-to-clipboard button for /mos:new-project --from-brief <sha8>', () => {
212
+ const outcome = _sixOkOutcome();
213
+ const html = buildDeck(outcome);
214
+ // 3-option verbatim text (em-dash-free)
215
+ assert.ok(html.includes('What now?'), 'must include "What now?" prompt');
216
+ assert.ok(html.includes("Just tell me what's new") || html.includes('Just tell me what&#39;s new'),
217
+ 'option 1 text');
218
+ assert.ok(html.includes('Build a room around this'), 'option 2 text');
219
+ assert.ok(html.includes("Devil's Advocate") || html.includes('Devil&#39;s Advocate'),
220
+ "option 3 text contains Devil's Advocate");
221
+ // Copy-to-clipboard hint
222
+ const sha8 = outcome.footer_data.sha256.slice(0, 8);
223
+ assert.ok(html.includes('/mos:new-project --from-brief ' + sha8),
224
+ 'copy hint must include /mos:new-project --from-brief <sha8>');
225
+ });
226
+
227
+ test('Test 11: data/mva-deck-template.html exists + has placeholder tokens', () => {
228
+ const tmplPath = path.join(__dirname, '..', '..', 'data', 'mva-deck-template.html');
229
+ assert.ok(fs.existsSync(tmplPath), 'template must exist at ' + tmplPath);
230
+ const tmpl = fs.readFileSync(tmplPath, 'utf8');
231
+ assert.ok(tmpl.includes('{{HEADER}}'), 'template must have {{HEADER}}');
232
+ assert.ok(tmpl.includes('{{SLIDES}}'), 'template must have {{SLIDES}}');
233
+ assert.ok(tmpl.includes('{{FOOTER}}'), 'template must have {{FOOTER}}');
234
+ });
235
+
236
+ test('Test 12: NIT-3 palette parity with lib/wiki/wiki-layout.cjs', () => {
237
+ const wikiPath = path.join(__dirname, '..', '..', 'lib', 'wiki', 'wiki-layout.cjs');
238
+ if (!fs.existsSync(wikiPath)) {
239
+ // Documented skip per plan-checker iter 2 NIT-3
240
+ console.log(' (skipped: lib/wiki/wiki-layout.cjs absent; palette is freshly defined for the MVA deck)');
241
+ return;
242
+ }
243
+ const wikiSource = fs.readFileSync(wikiPath, 'utf8');
244
+ const divergent = [];
245
+ for (const [name, hex] of Object.entries(DECK_PALETTE)) {
246
+ const hexUpper = hex.toUpperCase();
247
+ const wikiHasSameHex = wikiSource.toUpperCase().includes(hexUpper);
248
+ if (!wikiHasSameHex) {
249
+ const wikiMentionsColor = new RegExp('\\b' + name + '\\b', 'i').test(wikiSource);
250
+ if (wikiMentionsColor) {
251
+ const otherHexes = (wikiSource.match(/#[0-9A-Fa-f]{6}/g) || []).map(h => h.toUpperCase());
252
+ // Only flag if wiki has hex literals AND wiki names this color
253
+ if (otherHexes.length > 0) {
254
+ divergent.push(
255
+ name + ': deck=' + hex + ', wiki references "' + name + '" but uses different hex(es): ' +
256
+ otherHexes.slice(0, 3).join(', ')
257
+ );
258
+ }
259
+ }
260
+ }
261
+ }
262
+ assert.equal(divergent.length, 0,
263
+ 'Palette divergence detected vs lib/wiki/wiki-layout.cjs:\n ' + divergent.join('\n ') +
264
+ '\nReconcile DECK_PALETTE with wiki-layout.cjs or document the divergence in the SUMMARY.');
265
+ });
266
+
267
+ test('Test 13: Canon Part 8 source grep -- builder source has no raw-sentence references', () => {
268
+ const code = fs.readFileSync(path.join(__dirname, 'mva-deck-builder.cjs'), 'utf8');
269
+ // Strip comments
270
+ const stripped = code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
271
+ assert.equal(stripped.match(/MVA_SENTENCE/), null, 'no MVA_SENTENCE in builder source');
272
+ assert.equal(stripped.match(/process\.env\.CLAUDE_USER_PROMPT/), null,
273
+ 'no process.env.CLAUDE_USER_PROMPT in builder source');
274
+ // Template-level forbidden tokens + real-name discipline
275
+ const tmplPath = path.join(__dirname, '..', '..', 'data', 'mva-deck-template.html');
276
+ const template = fs.readFileSync(tmplPath, 'utf8');
277
+ assert.equal(template.match(/MVA_SENTENCE/), null, 'no MVA_SENTENCE in template');
278
+ // feedback_no_real_names_in_repo.md: no biographical tester/advisor names in deck template
279
+ assert.equal(template.match(/Lawrence|Gary|Natan|Aronhime|Reuven|Schler/), null,
280
+ 'no real names in template (per feedback_no_real_names_in_repo.md)');
281
+ });
282
+
283
+ test('Test 14: buildSlide is exported and produces an <article>', () => {
284
+ const slideHtml = buildSlide(_agentOk('brain_similar', 'Found 3 ventures: A, B, C'));
285
+ assert.ok(slideHtml.includes('<article'), 'slide must be wrapped in <article>');
286
+ assert.ok(slideHtml.includes('A, B, C'), 'slide must contain the summary line');
287
+ });
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 118-00 Plan 00 -- mva-detect.cjs smoke test (Task 2 verification).
4
+ *
5
+ * Spawns `node scripts/mva-detect.cjs` as a child with stdin JSON payload
6
+ * and asserts:
7
+ * - exit code 0
8
+ * - completes in < 1500ms (the hook budget)
9
+ * - state file ~/.mindrian/mva/<session-id>.json written for venture prompt
10
+ * - state file contains hebrew_refusal:true for Hebrew prompt
11
+ * - state file UNWRITTEN for non-venture prompt
12
+ * - telemetry jsonl gains one event per run
13
+ *
14
+ * Tests use a tmpdir HOME so the real ~/.mindrian is never touched.
15
+ *
16
+ * Pure CJS, node built-ins only.
17
+ */
18
+
19
+ const assert = require('node:assert/strict');
20
+ const fs = require('node:fs');
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+ const { spawnSync } = require('node:child_process');
24
+
25
+ const HOOK = path.resolve(__dirname, '..', '..', 'scripts', 'mva-detect.cjs');
26
+
27
+ let passed = 0;
28
+ let failed = 0;
29
+ function run(name, fn) {
30
+ try {
31
+ fn();
32
+ process.stdout.write('ok ' + name + '\n');
33
+ passed += 1;
34
+ } catch (err) {
35
+ process.stderr.write('FAIL ' + name + '\n' + (err && err.stack ? err.stack : String(err)) + '\n');
36
+ failed += 1;
37
+ }
38
+ }
39
+
40
+ function withTmpHome(fn) {
41
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mva-smoke-'));
42
+ const prevHome = process.env.HOME;
43
+ process.env.HOME = tmpHome;
44
+ try {
45
+ return fn(tmpHome);
46
+ } finally {
47
+ process.env.HOME = prevHome;
48
+ try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch (_e) {}
49
+ }
50
+ }
51
+
52
+ function spawnHook(stdinStr, env) {
53
+ const t0 = Date.now();
54
+ const res = spawnSync(process.execPath, [HOOK], {
55
+ input: stdinStr,
56
+ encoding: 'utf8',
57
+ env: Object.assign({}, process.env, env || {}),
58
+ timeout: 3000,
59
+ });
60
+ return { res, elapsed: Date.now() - t0 };
61
+ }
62
+
63
+ // ---------- S1 venture sentence -> state file written; exit 0; under budget ----------
64
+
65
+ run('S1 venture sentence -> state file written + telemetry + sub-1500ms', () => {
66
+ withTmpHome((tmpHome) => {
67
+ const sessionId = 'smoke-session-S1';
68
+ const { res, elapsed } = spawnHook(
69
+ JSON.stringify({ prompt: 'I have an idea for a couples finance app' }),
70
+ { HOME: tmpHome, CLAUDE_SESSION_ID: sessionId, ANTHROPIC_API_KEY: '' }
71
+ );
72
+ assert.equal(res.status, 0, 'hook must exit 0; stderr=' + (res.stderr || ''));
73
+ assert.ok(elapsed < 1500, 'hook must complete under 1500ms; got ' + elapsed + 'ms');
74
+ const stateFile = path.join(tmpHome, '.mindrian', 'mva', sessionId + '.json');
75
+ assert.ok(fs.existsSync(stateFile), 'state file must exist at ' + stateFile);
76
+ const body = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
77
+ assert.ok(body.sentence_sha256 && body.sentence_sha256.length === 64,
78
+ 'state must carry sentence_sha256 (64 hex chars); got ' + body.sentence_sha256);
79
+ assert.equal(body.locale, 'en');
80
+ assert.equal(body.pipeline_status, 'pending');
81
+ assert.ok(body.classifier_source === 'heuristic' || body.classifier_source === 'heuristic_fallback',
82
+ 'classifier_source must be heuristic/heuristic_fallback; got ' + body.classifier_source);
83
+ // Raw sentence must NOT appear in state file
84
+ const rawState = fs.readFileSync(stateFile, 'utf8');
85
+ assert.equal(rawState.indexOf('couples finance app'), -1,
86
+ 'state file must NEVER contain raw prompt text');
87
+ // Telemetry
88
+ const tFile = path.join(tmpHome, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
89
+ assert.ok(fs.existsSync(tFile), 'telemetry file must exist at ' + tFile);
90
+ const lines = fs.readFileSync(tFile, 'utf8').trim().split('\n').filter(Boolean);
91
+ assert.equal(lines.length, 1, 'expected exactly 1 telemetry line; got ' + lines.length);
92
+ const ev = JSON.parse(lines[0]);
93
+ assert.equal(ev.event, 'mva_classified');
94
+ assert.equal(ev.venture, true);
95
+ assert.equal(ev.sha256_of_sentence, body.sentence_sha256);
96
+ assert.equal(typeof ev.classified_in_ms, 'number');
97
+ // Telemetry must NOT contain raw prompt
98
+ assert.equal(lines[0].indexOf('couples finance app'), -1,
99
+ 'telemetry must NEVER contain raw prompt text');
100
+ });
101
+ });
102
+
103
+ // ---------- S2 non-venture sentence -> NO state file; exit 0 ----------
104
+
105
+ run('S2 non-venture sentence -> no state file; exit 0', () => {
106
+ withTmpHome((tmpHome) => {
107
+ const sessionId = 'smoke-session-S2';
108
+ const { res } = spawnHook(
109
+ JSON.stringify({ prompt: 'fix the failing test in foo.test.js' }),
110
+ { HOME: tmpHome, CLAUDE_SESSION_ID: sessionId, ANTHROPIC_API_KEY: '' }
111
+ );
112
+ assert.equal(res.status, 0);
113
+ const stateFile = path.join(tmpHome, '.mindrian', 'mva', sessionId + '.json');
114
+ assert.equal(fs.existsSync(stateFile), false,
115
+ 'state file must NOT exist for non-venture prompt; found ' + stateFile);
116
+ // Telemetry MUST still be appended (we record classification outcomes for analysis)
117
+ const tFile = path.join(tmpHome, '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
118
+ assert.ok(fs.existsSync(tFile));
119
+ const ev = JSON.parse(fs.readFileSync(tFile, 'utf8').trim().split('\n')[0]);
120
+ assert.equal(ev.venture, false);
121
+ });
122
+ });
123
+
124
+ // ---------- S3 Hebrew sentence -> hebrew_refusal state; exit 0 ----------
125
+
126
+ run('S3 Hebrew sentence -> hebrew_refusal:true in state; exit 0 (LD1)', () => {
127
+ withTmpHome((tmpHome) => {
128
+ const sessionId = 'smoke-session-S3';
129
+ const { res } = spawnHook(
130
+ JSON.stringify({ prompt: 'יש לי רעיון לאפליקציה לזוגות מתחתנים' }),
131
+ { HOME: tmpHome, CLAUDE_SESSION_ID: sessionId, ANTHROPIC_API_KEY: '' }
132
+ );
133
+ assert.equal(res.status, 0);
134
+ const stateFile = path.join(tmpHome, '.mindrian', 'mva', sessionId + '.json');
135
+ assert.ok(fs.existsSync(stateFile), 'Hebrew refusal must still write a state file');
136
+ const body = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
137
+ assert.equal(body.hebrew_refusal, true);
138
+ assert.equal(body.locale, 'he');
139
+ assert.equal(body.classifier_source, 'language_detect');
140
+ // Raw Hebrew sentence must NOT appear in state file
141
+ const rawState = fs.readFileSync(stateFile, 'utf8');
142
+ assert.equal(rawState.indexOf('יש לי רעיון'), -1,
143
+ 'state file must NEVER contain raw Hebrew prompt text');
144
+ });
145
+ });
146
+
147
+ // ---------- S4 no stdin payload -> exit 0 silently ----------
148
+
149
+ run('S4 empty stdin -> exit 0 silently', () => {
150
+ withTmpHome((tmpHome) => {
151
+ const sessionId = 'smoke-session-S4';
152
+ const { res } = spawnHook('', { HOME: tmpHome, CLAUDE_SESSION_ID: sessionId });
153
+ assert.equal(res.status, 0);
154
+ const stateFile = path.join(tmpHome, '.mindrian', 'mva', sessionId + '.json');
155
+ assert.equal(fs.existsSync(stateFile), false);
156
+ });
157
+ });
158
+
159
+ // ---------- S5 malformed JSON -> exit 0 silently ----------
160
+
161
+ run('S5 malformed stdin JSON -> exit 0 silently; no state mutation', () => {
162
+ withTmpHome((tmpHome) => {
163
+ const sessionId = 'smoke-session-S5';
164
+ const { res } = spawnHook('not-json-at-all', { HOME: tmpHome, CLAUDE_SESSION_ID: sessionId });
165
+ assert.equal(res.status, 0);
166
+ const stateFile = path.join(tmpHome, '.mindrian', 'mva', sessionId + '.json');
167
+ assert.equal(fs.existsSync(stateFile), false);
168
+ });
169
+ });
170
+
171
+ // ---------- S6 Canon Part 8 source-grep sweep ----------
172
+
173
+ run('S6 Canon Part 8 sweep: no Brain MCP / brain-client refs in hook + classifier + state', () => {
174
+ const files = [
175
+ path.resolve(__dirname, '..', '..', 'scripts', 'mva-detect.cjs'),
176
+ path.resolve(__dirname, 'mva-classifier.cjs'),
177
+ path.resolve(__dirname, 'mva-state.cjs'),
178
+ ];
179
+ const forbidden = [
180
+ /require\(['"][^'"]*brain-client['"]\)/,
181
+ /mcp__brain_/,
182
+ /brain_query/,
183
+ /brain_search/,
184
+ ];
185
+ for (const f of files) {
186
+ const src = fs.readFileSync(f, 'utf8');
187
+ for (const re of forbidden) {
188
+ assert.ok(!re.test(src),
189
+ 'Canon Part 8 forbidden token ' + re + ' found in ' + path.basename(f));
190
+ }
191
+ }
192
+ });
193
+
194
+ // ---------- Summary ----------
195
+
196
+ process.stderr.write('\n' + passed + ' passed, ' + failed + ' failed\n');
197
+ process.exit(failed === 0 ? 0 : 1);
@@ -0,0 +1,110 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-01 Plan 01 Task 3 -- mva-dispatcher.
5
+ *
6
+ * The parallel fan-out primitive used by Plan 118-03 (progressive streaming)
7
+ * and Plan 118-04 (deck generation). Plan 118-02 supplies the 6 specific
8
+ * agents (brain_similar, brain_cross_domain, brain_classic_traps,
9
+ * tavily_funding, six_hats_red_black, dashboard_graph); this module knows
10
+ * NOTHING about which agents are which -- it accepts any { id, fn } tuple
11
+ * conforming to the Agent contract in lib/core/mva-agent-contract.cjs.
12
+ *
13
+ * Per binding decision B2 (HARD):
14
+ * - 45-second global wall-clock budget
15
+ * - 35-second per-agent budget
16
+ * - Per-agent abort signal feeds from global deadline:
17
+ * perAgentTimeout = min(35000, remainingGlobalMs)
18
+ *
19
+ * Per binding decision B7:
20
+ * - Per-agent failure (throw, timeout, empty) does NOT abort sibling agents
21
+ * - All-fail does NOT throw; it returns N error/timeout results so the
22
+ * caller (Plan 118-03) can render the sharp-question fallback
23
+ *
24
+ * Streaming-as-settled pattern: results yield in arrival order. Fast agents
25
+ * yield before slow agents finish. Implemented via a settlement queue with
26
+ * a deferred-resolve "ready" promise.
27
+ *
28
+ * Canon Part 8 hard invariants (zero user-content egress):
29
+ * - The dispatcher passes ONLY sentence_sha256 to agents (NOT the raw sentence)
30
+ * - process.env.MVA_SENTENCE is NEVER read or written by this module
31
+ * - No stdout / stderr writes (telemetry side-channel rule)
32
+ * - No Brain MCP client require (agents own their own I/O)
33
+ *
34
+ * Pure CJS, node built-ins only, zero new runtime dependencies.
35
+ */
36
+ 'use strict';
37
+
38
+ const { runAgent } = require('./mva-agent-contract.cjs');
39
+ const { createBudget } = require('./mva-budget.cjs');
40
+
41
+ /**
42
+ * dispatch -- parallel fan-out over agents, yielding each AgentResult as it
43
+ * settles (streaming-as-settled). Compatible with `for await (...)` consumers.
44
+ *
45
+ * @param {Array<{ id: string, fn: Function }>} agents - 6 agents from Plan 118-02
46
+ * @param {string} sentence_sha256 - the ONLY sentence-derived identifier
47
+ * @param {{ globalBudgetMs?: number, perAgentCapMs?: number }} [opts]
48
+ * @returns {AsyncGenerator<{ agent_id: string, status: string, duration_ms: number, payload?: any, error?: string }>}
49
+ */
50
+ async function* dispatch(agents, sentence_sha256, opts) {
51
+ const options = opts || {};
52
+ const budget = createBudget(options.globalBudgetMs);
53
+ const perAgentCap = (typeof options.perAgentCapMs === 'number') ? options.perAgentCapMs : undefined;
54
+
55
+ // Settlement queue. Each entry is a promise that resolves with an AgentResult.
56
+ // We use a deferred "ready" notification so the async generator can yield
57
+ // results in arrival order without busy-waiting.
58
+ const queue = [];
59
+ let pendingCount = agents.length;
60
+ let notify;
61
+ let ready = new Promise((r) => { notify = r; });
62
+
63
+ // Kick off every agent in parallel. Each agent runs under its own per-agent
64
+ // budget = min(perAgentCap, budget.remainingMs()) computed at start.
65
+ for (const agent of agents) {
66
+ const timeoutMs = budget.perAgentMs(perAgentCap);
67
+ const context = { sentence_sha256: sentence_sha256 };
68
+
69
+ // Fire-and-forget; result lands in the queue. runAgent never throws.
70
+ runAgent(agent, context, { timeoutMs }).then((result) => {
71
+ queue.push(result);
72
+ const fire = notify;
73
+ ready = new Promise((r) => { notify = r; });
74
+ fire();
75
+ });
76
+ }
77
+
78
+ while (pendingCount > 0) {
79
+ if (queue.length === 0) {
80
+ await ready;
81
+ }
82
+ while (queue.length > 0) {
83
+ pendingCount -= 1;
84
+ yield queue.shift();
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * dispatchToArray -- convenience wrapper that collects the AsyncIterable into
91
+ * a Promise<Array>. Plan 118-04 (deck generator) uses this; Plan 118-03
92
+ * (progressive streaming) uses the streaming dispatch directly.
93
+ *
94
+ * @param {Array<{ id: string, fn: Function }>} agents
95
+ * @param {string} sentence_sha256
96
+ * @param {{ globalBudgetMs?: number, perAgentCapMs?: number }} [opts]
97
+ * @returns {Promise<Array<{ agent_id: string, status: string, duration_ms: number, payload?: any, error?: string }>>}
98
+ */
99
+ async function dispatchToArray(agents, sentence_sha256, opts) {
100
+ const results = [];
101
+ for await (const r of dispatch(agents, sentence_sha256, opts)) {
102
+ results.push(r);
103
+ }
104
+ return results;
105
+ }
106
+
107
+ module.exports = {
108
+ dispatch,
109
+ dispatchToArray
110
+ };