@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,194 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-03 Plan 03 -- mva-progressive-renderer.
5
+ *
6
+ * Pure-function renderer that turns AgentResult shapes into Larry-voiced
7
+ * GUIDED text blocks. Zero I/O. Zero process / console / fs references.
8
+ *
9
+ * Larry voice (per feedback_larry_pedagogical_guided_first.md):
10
+ * GUIDED, not AUTONOMOUS. The renderer never says "I've analyzed your
11
+ * venture and found:" -- it surfaces what shows up in the graph and
12
+ * invites the navigator to chew on it.
13
+ *
14
+ * Canon Part 8: this module receives ONLY structured AgentResult objects
15
+ * (which themselves only carry agent-supplied summary_line / payload).
16
+ * The renderer does not read the raw sentence; it does not know it exists.
17
+ *
18
+ * Em-dash discipline (per feedback_no_emdashes.md): every string emitted
19
+ * by this module uses `--` and `-` ONLY. The hardcoded footer text is a
20
+ * module-level constant (NOT loaded from any source markdown at runtime;
21
+ * source spec contains em-dashes and is not a runtime input).
22
+ *
23
+ * The 6 per-agent labels (verbatim from plan task 1 step 3):
24
+ * brain_similar -> [brain]
25
+ * brain_cross_domain -> [analogy]
26
+ * brain_classic_traps -> [traps]
27
+ * tavily_funding -> [funding]
28
+ * six_hats_red_black -> [worth chewing on]
29
+ * dashboard_graph -> [your room]
30
+ *
31
+ * Pure CJS, no requires (zero deps).
32
+ */
33
+ 'use strict';
34
+
35
+ // ---------- Frozen invariants ----------
36
+
37
+ const AGENT_LABELS = Object.freeze({
38
+ brain_similar: 'brain',
39
+ brain_cross_domain: 'analogy',
40
+ brain_classic_traps: 'traps',
41
+ tavily_funding: 'funding',
42
+ six_hats_red_black: 'worth chewing on',
43
+ dashboard_graph: 'your room',
44
+ });
45
+
46
+ // Verbatim from source spec line 111 + binding decision B7.
47
+ // Note the apostrophe in "didn't" stays as a literal apostrophe; the em-dash
48
+ // in the source spec is rewritten as `--` per feedback_no_emdashes.md.
49
+ const SHARP_QUESTION_FALLBACK = [
50
+ "I didn't find precedents for this in 30 seconds.",
51
+ "That's either a gap in my data or a signal that you're in a genuinely unexplored space.",
52
+ "Which do you think it is?",
53
+ ''
54
+ ].join('\n');
55
+
56
+ // Hardcoded module-level constant for the 3-option footer (CRITICAL-6).
57
+ // MUST use `--`, NEVER `—`. Plan 118-03 Test 8b asserts both invariants.
58
+ const FOOTER_TEXT = [
59
+ '',
60
+ 'What now?',
61
+ ' [1] Just tell me what\'s new (stay in "tell me" mode)',
62
+ ' [2] Build a room around this (invest)',
63
+ ' [3] Challenge me -- Devil\'s Advocate (go deeper cognitively)',
64
+ ''
65
+ ].join('\n');
66
+
67
+ // Hebrew refusal block (per LD1). English first, Hebrew second. No em-dashes.
68
+ const HEBREW_REFUSAL = [
69
+ 'MindrianOS does not yet support Hebrew in v1.13.0. Please try in English.',
70
+ 'MindrianOS לא תומך בעברית ב-v1.13.0; אנא נסה באנגלית.',
71
+ ''
72
+ ].join('\n');
73
+
74
+ // ---------- Pure helpers ----------
75
+
76
+ function _label(agent_id) {
77
+ return AGENT_LABELS[agent_id] || agent_id;
78
+ }
79
+
80
+ function _summaryLine(payload) {
81
+ if (!payload || typeof payload !== 'object') return '';
82
+ if (typeof payload.summary_line === 'string') return payload.summary_line;
83
+ return '';
84
+ }
85
+
86
+ // ---------- Public exports ----------
87
+
88
+ /**
89
+ * renderHeader(sha8) -> 2-line GUIDED opener.
90
+ *
91
+ * "Scanning for precedents... (sentence <sha8>)"
92
+ * "[progress: 0s / 45s, 0 of 6 returned]"
93
+ */
94
+ function renderHeader(sha8) {
95
+ const safeSha8 = (typeof sha8 === 'string' && sha8.length > 0) ? sha8 : 'unknown';
96
+ return [
97
+ `Scanning for precedents... (sentence ${safeSha8})`,
98
+ '[progress: 0s / 45s, 0 of 6 returned]',
99
+ ''
100
+ ].join('\n');
101
+ }
102
+
103
+ /**
104
+ * renderProgressIndicator(elapsedMs, budgetMs, returnedCount) -> single line.
105
+ *
106
+ * " [progress: 12s / 45s, 3 of 6 returned]"
107
+ */
108
+ function renderProgressIndicator(elapsedMs, budgetMs, returnedCount) {
109
+ const elapsed = Math.max(0, Math.round((elapsedMs || 0) / 1000));
110
+ const budget = Math.max(0, Math.round((budgetMs || 0) / 1000));
111
+ const returned = Math.max(0, returnedCount | 0);
112
+ return ` [progress: ${elapsed}s / ${budget}s, ${returned} of 6 returned]\n`;
113
+ }
114
+
115
+ /**
116
+ * renderAgentResult(result) -> 1-3 line Larry-voiced block based on status.
117
+ *
118
+ * Dispatches on result.status:
119
+ * ok -> " [<label>] <summary_line>"
120
+ * empty -> contextualized placeholder (special-case tavily_unavailable)
121
+ * timeout -> " [<label>] (still in progress at 45s)"
122
+ * error -> " [<label>] (skipped)" (raw error NEVER included verbatim)
123
+ */
124
+ function renderAgentResult(result) {
125
+ if (!result || typeof result !== 'object') return '';
126
+ const id = result.agent_id || 'agent';
127
+ const label = _label(id);
128
+ const status = result.status;
129
+
130
+ if (status === 'ok') {
131
+ const line = _summaryLine(result.payload);
132
+ if (line) {
133
+ return ` [${label}] ${line}\n`;
134
+ }
135
+ return ` [${label}] (no summary returned)\n`;
136
+ }
137
+
138
+ if (status === 'empty') {
139
+ const reason = result.payload && typeof result.payload === 'object' ? result.payload.reason : null;
140
+ if (id === 'tavily_funding' && reason === 'tavily_unavailable') {
141
+ return ` [${label}] Live funding scan: not configured (add TAVILY_API_KEY to ~/.mindrian.env)\n`;
142
+ }
143
+ return ` [${label}] (no findings this pass)\n`;
144
+ }
145
+
146
+ if (status === 'timeout') {
147
+ return ` [${label}] (still in progress at 45s)\n`;
148
+ }
149
+
150
+ if (status === 'error') {
151
+ // CRITICAL: do NOT include the raw error string verbatim. Canon Part 8.
152
+ return ` [${label}] (skipped)\n`;
153
+ }
154
+
155
+ return ` [${label}] (unknown status)\n`;
156
+ }
157
+
158
+ /**
159
+ * renderSharpQuestionFallback() -> verbatim sharp question from source spec
160
+ * line 111. Em-dash-free (the source spec's em-dash is rewritten to `--`
161
+ * by hardcoding the text here).
162
+ */
163
+ function renderSharpQuestionFallback() {
164
+ return SHARP_QUESTION_FALLBACK;
165
+ }
166
+
167
+ /**
168
+ * renderHebrewRefusal() -> bilingual refusal block. English first, Hebrew
169
+ * second. No em-dashes. Per LD1 (Locked Decision 1).
170
+ */
171
+ function renderHebrewRefusal() {
172
+ return HEBREW_REFUSAL;
173
+ }
174
+
175
+ /**
176
+ * renderFooter() -> the 3-option footer block (binding decision B4).
177
+ *
178
+ * VERBATIM hardcoded text. MUST use `--`, NEVER `—`. Tested by Test 8b
179
+ * in mva-progressive-renderer.test.cjs.
180
+ */
181
+ function renderFooter() {
182
+ return FOOTER_TEXT;
183
+ }
184
+
185
+ module.exports = {
186
+ renderHeader,
187
+ renderProgressIndicator,
188
+ renderAgentResult,
189
+ renderSharpQuestionFallback,
190
+ renderHebrewRefusal,
191
+ renderFooter,
192
+ // Exported for adversarial/golden-fixture tests in Plan 118-06
193
+ AGENT_LABELS,
194
+ };
@@ -0,0 +1,157 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-03 Plan 03 Task 1 -- mva-progressive-renderer tests.
5
+ *
6
+ * Verifies that the renderer is a pure-function module (no I/O), emits Larry-voiced
7
+ * GUIDED text blocks per AgentResult status, hardcodes the 3-option footer with
8
+ * `--` (not `—`), and complies with Canon Part 8 (zero raw-content access).
9
+ *
10
+ * Pure CJS, node built-ins only. Run via `node --test`.
11
+ */
12
+ 'use strict';
13
+
14
+ const test = require('node:test');
15
+ const assert = require('node:assert');
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const {
20
+ renderAgentResult,
21
+ renderSharpQuestionFallback,
22
+ renderHebrewRefusal,
23
+ renderHeader,
24
+ renderProgressIndicator,
25
+ renderFooter,
26
+ } = require('./mva-progressive-renderer.cjs');
27
+
28
+ test('renderer Test 1 -- ok agent renders Larry-voiced 1-line summary, em-dash-free', () => {
29
+ const out = renderAgentResult({
30
+ agent_id: 'brain_similar',
31
+ status: 'ok',
32
+ duration_ms: 12,
33
+ payload: { summary_line: 'Found 3 ventures in this space: Honeydue (dead), Zeta (alive), Monarch (alive)' }
34
+ });
35
+ assert.ok(typeof out === 'string', 'must return a string');
36
+ assert.ok(out.includes('[brain]'), 'must include brain prefix');
37
+ assert.ok(out.includes('Found 3 ventures'), 'must include summary_line content');
38
+ assert.equal(out.match(/—/), null, 'must have no em-dashes');
39
+ assert.ok(out.endsWith('\n'), 'must end with newline');
40
+ });
41
+
42
+ test('renderer Test 2 -- timeout status renders progress placeholder', () => {
43
+ const out = renderAgentResult({
44
+ agent_id: 'brain_similar',
45
+ status: 'timeout',
46
+ duration_ms: 45000,
47
+ });
48
+ assert.ok(out.includes('[brain]'), 'must include label');
49
+ assert.ok(/still/i.test(out), 'must indicate still-in-progress');
50
+ assert.equal(out.match(/—/), null, 'no em-dashes');
51
+ });
52
+
53
+ test('renderer Test 3 -- error status renders skipped placeholder, sanitized', () => {
54
+ const out = renderAgentResult({
55
+ agent_id: 'brain_classic_traps',
56
+ status: 'error',
57
+ duration_ms: 50,
58
+ error: 'this is a very long error message that should never reach the user via the rendered output and certainly not unsanitized for very long lengths'
59
+ });
60
+ assert.ok(out.includes('[traps]'), 'must include label');
61
+ assert.ok(/skipped/i.test(out), 'must indicate skipped');
62
+ // The raw error should NOT appear verbatim in the rendered output.
63
+ assert.equal(out.includes('this is a very long error message'), false, 'must not include raw error verbatim');
64
+ assert.equal(out.match(/—/), null, 'no em-dashes');
65
+ });
66
+
67
+ test('renderer Test 4 -- empty status with tavily_unavailable shows configuration hint', () => {
68
+ const out = renderAgentResult({
69
+ agent_id: 'tavily_funding',
70
+ status: 'empty',
71
+ duration_ms: 5,
72
+ payload: { reason: 'tavily_unavailable' }
73
+ });
74
+ assert.ok(out.includes('[funding]'), 'must include funding label');
75
+ assert.ok(out.includes('TAVILY_API_KEY') || out.includes('not configured'), 'must hint at configuration');
76
+ assert.equal(out.match(/—/), null, 'no em-dashes');
77
+ });
78
+
79
+ test('renderer Test 5 -- sharp-question fallback is verbatim from source spec, em-dash-free', () => {
80
+ const out = renderSharpQuestionFallback();
81
+ assert.ok(typeof out === 'string', 'must return a string');
82
+ assert.ok(out.includes("I didn't find precedents for this in 30 seconds"), 'must include opener');
83
+ assert.ok(out.includes('gap in my data'), 'must include data gap clause');
84
+ assert.ok(out.includes('genuinely unexplored space'), 'must include unexplored clause');
85
+ assert.ok(out.includes('Which do you think it is?'), 'must include the closing question');
86
+ assert.equal(out.match(/—/), null, 'must have no em-dashes');
87
+ });
88
+
89
+ test('renderer Test 6 -- Hebrew refusal is bilingual, em-dash-free', () => {
90
+ const out = renderHebrewRefusal();
91
+ assert.ok(typeof out === 'string', 'must return a string');
92
+ assert.ok(out.includes('English'), 'must include English instruction');
93
+ assert.ok(out.includes('v1.13.0'), 'must mention version');
94
+ // Hebrew characters must appear
95
+ assert.ok(/[֐-׿]/.test(out), 'must include Hebrew characters');
96
+ assert.equal(out.match(/—/), null, 'no em-dashes');
97
+ });
98
+
99
+ test('renderer Test 7 -- renderHeader produces 2-line opener with sha8 and progress', () => {
100
+ const out = renderHeader('abcd1234');
101
+ assert.ok(typeof out === 'string', 'must return a string');
102
+ assert.ok(out.includes('abcd1234'), 'must include sha8');
103
+ assert.ok(/scanning/i.test(out), 'must include scanning verb');
104
+ const lines = out.split('\n').filter((l) => l.length > 0);
105
+ assert.ok(lines.length >= 2, `must be at least 2 lines, got ${lines.length}`);
106
+ assert.equal(out.match(/—/), null, 'no em-dashes');
107
+ });
108
+
109
+ test('renderer Test 8 -- renderProgressIndicator shows progress with elapsed/budget/returned', () => {
110
+ const out = renderProgressIndicator(12000, 45000, 3);
111
+ assert.ok(typeof out === 'string', 'must return a string');
112
+ assert.ok(out.includes('12') || out.includes('12s'), 'must include elapsed seconds');
113
+ assert.ok(out.includes('45'), 'must include budget seconds');
114
+ assert.ok(out.includes('3'), 'must include returned count');
115
+ assert.equal(out.match(/—/), null, 'no em-dashes');
116
+
117
+ // Edge cases
118
+ const zero = renderProgressIndicator(0, 45000, 0);
119
+ assert.ok(zero.includes('0'), 'edge: 0 elapsed');
120
+ const full = renderProgressIndicator(45000, 45000, 6);
121
+ assert.ok(full.includes('45'), 'edge: full elapsed');
122
+ assert.equal(zero.match(/—/), null);
123
+ assert.equal(full.match(/—/), null);
124
+ });
125
+
126
+ test('renderer Test 8b (CRITICAL-6) -- renderFooter returns hardcoded 3-option footer, em-dash-free', () => {
127
+ const out = renderFooter();
128
+ assert.ok(typeof out === 'string', 'must return a string');
129
+ assert.ok(out.includes('What now?'), 'must include question opener');
130
+ assert.ok(out.includes('[1] Just tell me'), 'must include option 1');
131
+ assert.ok(out.includes('[2] Build a room'), 'must include option 2');
132
+ assert.ok(out.includes('[3] Challenge me'), 'must include option 3');
133
+ // CRITICAL invariant: em-dash-free
134
+ assert.equal(out.match(/—/), null, 'footer must have no em-dashes');
135
+ // CRITICAL invariant: literal `--` substring present
136
+ assert.ok(out.includes("Challenge me -- Devil's Advocate"), "must contain literal 'Challenge me -- Devil's Advocate' with `--`");
137
+ });
138
+
139
+ test('renderer Test 9 -- renderer source is pure (no fs/process/console)', () => {
140
+ const src = fs.readFileSync(path.join(__dirname, 'mva-progressive-renderer.cjs'), 'utf8');
141
+
142
+ // Strip block comments to avoid false-positives in the "pure" assertion.
143
+ // The renderer documents its purity in comments; tokens inside /* ... */ are not
144
+ // executable references and must not trip the source-scan.
145
+ const noBlockComments = src.replace(/\/\*[\s\S]*?\*\//g, '');
146
+ // Also strip line comments so the documentation that explicitly names
147
+ // forbidden tokens (e.g. "// no fs.* / no process.* / no console.*") does
148
+ // not trip the scan. These are intentional self-documentation, not refs.
149
+ const code = noBlockComments.replace(/\/\/[^\n]*/g, '');
150
+
151
+ // Allow `require('fs')` to be absent; verify there is no actual fs.* call.
152
+ assert.equal(/\brequire\s*\(\s*['"]node:fs['"]\s*\)/.test(code), false, 'must not require node:fs');
153
+ assert.equal(/\brequire\s*\(\s*['"]fs['"]\s*\)/.test(code), false, 'must not require fs');
154
+ assert.equal(/\bfs\./.test(code), false, 'must not call fs.*');
155
+ assert.equal(/\bprocess\./.test(code), false, 'must not access process.*');
156
+ assert.equal(/\bconsole\./.test(code), false, 'must not call console.*');
157
+ });
@@ -0,0 +1,213 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ *
4
+ * Phase 118-06 Plan 06 -- mva-rule-linter.
5
+ *
6
+ * The reward-before-investment rule linter (per docs/reward-before-investment
7
+ * -rule.md). Scans commands/*.md frontmatter, validates that every interactive
8
+ * command declares an `interactive_first_reward` field, classifies into three
9
+ * buckets:
10
+ *
11
+ * compliant -> field present with an allowed REWARD_TYPES value
12
+ * missing -> file has no frontmatter, OR no `interactive_first_reward` key
13
+ * invalid -> field present but value not in REWARD_TYPES enum
14
+ *
15
+ * REWARD_TYPES is the v1.13.0 canonical closed vocabulary. Future additions
16
+ * are canon amendments, not command-level inventions (per rule doc + Canon
17
+ * Part 7 reuse-before-build).
18
+ *
19
+ * Per binding decision B5: this library is the enforcement mechanism the
20
+ * universal rule needs. Per-command actual remediations are out-of-scope
21
+ * follow-up phases. This file only validates the DECLARATION.
22
+ *
23
+ * Canon Part 8: zero network, zero Brain calls, zero user-content egress.
24
+ * Reads only plugin-source command files (fs.readdirSync + fs.readFileSync).
25
+ *
26
+ * Pure CJS, node built-ins only.
27
+ */
28
+ 'use strict';
29
+
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+
33
+ // ---------- The closed vocabulary (v1.13.0) ----------
34
+
35
+ // Per docs/reward-before-investment-rule.md + WARN-5 audit (plan-checker
36
+ // iteration 2):
37
+ // reframe_question -- Larry reframes the user's sentence
38
+ // into a beautiful question (onboard).
39
+ // instant_brief -- the 30-second MVA pipeline output
40
+ // (new-project; this phase's deliverable).
41
+ // schema_preview -- structural preview of what would be
42
+ // extracted before the full ask.
43
+ // calibration_distribution_preview-- anonymized score distribution from the
44
+ // calibration set (grade).
45
+ // paragraph_preview -- partial extraction from the first
46
+ // paragraph alone (file-meeting).
47
+ // --none (scripting only) -- explicit opt-out, per rule doc line 81
48
+ // (scripting override).
49
+ const REWARD_TYPES = Object.freeze(new Set([
50
+ 'reframe_question',
51
+ 'instant_brief',
52
+ 'schema_preview',
53
+ 'calibration_distribution_preview',
54
+ 'paragraph_preview',
55
+ '--none (scripting only)',
56
+ ]));
57
+
58
+ // ---------- Frontmatter parser (mirrors scripts/build-command-registry.cjs) ----------
59
+
60
+ // Hand-rolled YAML-ish slice parser. Handles:
61
+ // key: value
62
+ // key: ["a","b"] (JSON.parse)
63
+ // key: + indented `- item` dash list
64
+ // strips surrounding quotes
65
+ // key: null -> null
66
+ // key: true|false -> boolean
67
+ //
68
+ // Returns:
69
+ // - parsed frontmatter object on success
70
+ // - null if no `---` block found (caller flags as "missing")
71
+ function parseFrontmatter(md) {
72
+ if (typeof md !== 'string') return null;
73
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(md);
74
+ if (!m) return null;
75
+ const out = {};
76
+ let key = null;
77
+ for (const rawLine of m[1].split(/\r?\n/)) {
78
+ const line = rawLine.replace(/\s+$/, '');
79
+ // Skip comments and blank lines
80
+ if (line === '' || /^\s*#/.test(line)) {
81
+ continue;
82
+ }
83
+ const kv = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
84
+ if (kv) {
85
+ key = kv[1];
86
+ const v = kv[2].trim();
87
+ if (v === '') {
88
+ out[key] = [];
89
+ } else if (v === 'null' || v === '~') {
90
+ out[key] = null;
91
+ } else if (v === 'true' || v === 'false') {
92
+ out[key] = v === 'true';
93
+ } else if (v.startsWith('[')) {
94
+ try {
95
+ out[key] = JSON.parse(v);
96
+ } catch (_e) {
97
+ out[key] = v.replace(/^["']|["']$/g, '');
98
+ }
99
+ } else {
100
+ // Strip trailing inline comment ("value # comment") -- the linter
101
+ // only cares about the scalar; the comment is metadata.
102
+ const noComment = v.replace(/\s+#.*$/, '');
103
+ out[key] = noComment.replace(/^["']|["']$/g, '');
104
+ }
105
+ } else if (key && /^\s*-\s+/.test(line)) {
106
+ if (!Array.isArray(out[key])) out[key] = [];
107
+ out[key].push(line.replace(/^\s*-\s+/, '').replace(/^["']|["']$/g, ''));
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ // ---------- validateFrontmatter ----------
114
+
115
+ /**
116
+ * validateFrontmatter(fm) -> { ok: boolean, reason?: string, value?: string }
117
+ *
118
+ * - fm = null -> { ok: false, reason: 'no_frontmatter' }
119
+ * - missing interactive_first_reward field -> { ok: false, reason: 'missing_field' }
120
+ * - value not in REWARD_TYPES -> { ok: false, reason: 'invalid_value', value }
121
+ * - else -> { ok: true, value }
122
+ *
123
+ * The `--none (scripting only)` literal is accepted verbatim per rule doc
124
+ * line 81. Whitespace-trim before lookup so trailing-space typos don't
125
+ * falsely fail.
126
+ */
127
+ function validateFrontmatter(fm) {
128
+ if (fm === null || typeof fm !== 'object') {
129
+ return { ok: false, reason: 'no_frontmatter' };
130
+ }
131
+ if (!Object.prototype.hasOwnProperty.call(fm, 'interactive_first_reward')) {
132
+ return { ok: false, reason: 'missing_field' };
133
+ }
134
+ const raw = fm.interactive_first_reward;
135
+ if (typeof raw !== 'string') {
136
+ return { ok: false, reason: 'invalid_value', value: String(raw) };
137
+ }
138
+ const value = raw.trim();
139
+ if (!REWARD_TYPES.has(value)) {
140
+ return { ok: false, reason: 'invalid_value', value };
141
+ }
142
+ return { ok: true, value };
143
+ }
144
+
145
+ // ---------- scanCommands ----------
146
+
147
+ /**
148
+ * scanCommands(commandsDir) -> { ok, compliant[], missing[], invalid[] }
149
+ *
150
+ * Reads every *.md in commandsDir (non-recursive), parses frontmatter, runs
151
+ * validateFrontmatter, classifies into one of three buckets. Each entry is
152
+ * { path, reason?, value? }.
153
+ *
154
+ * ok === true iff missing.length === 0 AND invalid.length === 0.
155
+ *
156
+ * Graceful: a file without a frontmatter block is reported in `missing`
157
+ * (does NOT throw). A file that fs.readFileSync cannot read is reported in
158
+ * `missing` with reason='read_error'. The function never throws.
159
+ */
160
+ function scanCommands(commandsDir) {
161
+ const compliant = [];
162
+ const missing = [];
163
+ const invalid = [];
164
+
165
+ let entries;
166
+ try {
167
+ entries = fs.readdirSync(commandsDir);
168
+ } catch (e) {
169
+ return {
170
+ ok: false,
171
+ compliant,
172
+ missing: [{ path: commandsDir, reason: 'commands_dir_read_error', error: e.message }],
173
+ invalid,
174
+ };
175
+ }
176
+
177
+ const mdFiles = entries.filter((f) => f.endsWith('.md')).sort();
178
+
179
+ for (const f of mdFiles) {
180
+ const full = path.join(commandsDir, f);
181
+ let raw;
182
+ try {
183
+ raw = fs.readFileSync(full, 'utf8');
184
+ } catch (_e) {
185
+ missing.push({ path: full, reason: 'read_error' });
186
+ continue;
187
+ }
188
+ const fm = parseFrontmatter(raw);
189
+ const v = validateFrontmatter(fm);
190
+ if (v.ok) {
191
+ compliant.push({ path: full, value: v.value });
192
+ } else if (v.reason === 'invalid_value') {
193
+ invalid.push({ path: full, reason: v.reason, value: v.value });
194
+ } else {
195
+ // no_frontmatter OR missing_field -- both count as "missing"
196
+ missing.push({ path: full, reason: v.reason });
197
+ }
198
+ }
199
+
200
+ return {
201
+ ok: missing.length === 0 && invalid.length === 0,
202
+ compliant,
203
+ missing,
204
+ invalid,
205
+ };
206
+ }
207
+
208
+ module.exports = {
209
+ scanCommands,
210
+ validateFrontmatter,
211
+ parseFrontmatter, // exposed for tests
212
+ REWARD_TYPES,
213
+ };