@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,336 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 118-06 Plan 06 -- mva-rule-linter unit tests.
4
+ *
5
+ * Tests 1-7: linter library + CLI baseline (Task 1).
6
+ * Tests 8-11: pre-commit hook wire + WARN-5 rule-doc parity audit (Task 2).
7
+ *
8
+ * Test 3 reads LD1 from 118-CONTEXT.md (CRITICAL-1+5 -- not in this file;
9
+ * lives in tests/test-mva-dror-harness.cjs).
10
+ *
11
+ * Pure CJS, node built-ins only. Mirrors the Phase 118-00 in-tree runner
12
+ * pattern from lib/core/mva-classifier.test.cjs.
13
+ */
14
+
15
+ const assert = require('node:assert/strict');
16
+ const fs = require('node:fs');
17
+ const os = require('node:os');
18
+ const path = require('node:path');
19
+ const { spawnSync } = require('node:child_process');
20
+
21
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
22
+ const LINTER_PATH = path.resolve(__dirname, 'mva-rule-linter.cjs');
23
+ const CLI_PATH = path.resolve(REPO_ROOT, 'scripts', 'check-reward-before-investment.cjs');
24
+ const HOOK_PATH = path.resolve(REPO_ROOT, 'scripts', 'hooks', 'pre-commit');
25
+
26
+ let passed = 0;
27
+ let failed = 0;
28
+
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 freshLinter() {
41
+ delete require.cache[LINTER_PATH];
42
+ return require(LINTER_PATH);
43
+ }
44
+
45
+ // Create a temp dir + write fixture files there. Cleanup is best-effort
46
+ // (left in place if a test throws so a human can inspect).
47
+ function mkTempCommandsDir() {
48
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mva-rule-linter-'));
49
+ return dir;
50
+ }
51
+
52
+ function writeFixture(dir, filename, body) {
53
+ fs.writeFileSync(path.join(dir, filename), body, 'utf8');
54
+ }
55
+
56
+ // ---------- T1: compliant command passes ----------
57
+
58
+ run('T1 compliant command: interactive_first_reward present + valid -> compliant', () => {
59
+ const { scanCommands } = freshLinter();
60
+ const dir = mkTempCommandsDir();
61
+ writeFixture(dir, 'good.md', [
62
+ '---',
63
+ 'name: good',
64
+ 'description: A command that follows the rule',
65
+ 'interactive_first_reward: reframe_question',
66
+ '---',
67
+ '',
68
+ '# good',
69
+ ].join('\n'));
70
+ const result = scanCommands(dir);
71
+ assert.equal(result.compliant.length, 1, 'expected 1 compliant, got ' + JSON.stringify(result));
72
+ assert.equal(result.missing.length, 0);
73
+ assert.equal(result.invalid.length, 0);
74
+ assert.equal(result.ok, true);
75
+ assert.equal(result.compliant[0].value, 'reframe_question');
76
+ });
77
+
78
+ // ---------- T2: missing field fails ----------
79
+
80
+ run('T2 missing field: frontmatter present but no interactive_first_reward -> missing', () => {
81
+ const { scanCommands } = freshLinter();
82
+ const dir = mkTempCommandsDir();
83
+ writeFixture(dir, 'forgot.md', [
84
+ '---',
85
+ 'name: forgot',
86
+ 'description: A command that forgot the field',
87
+ '---',
88
+ '',
89
+ '# forgot',
90
+ ].join('\n'));
91
+ const result = scanCommands(dir);
92
+ assert.equal(result.compliant.length, 0);
93
+ assert.equal(result.missing.length, 1);
94
+ assert.equal(result.invalid.length, 0);
95
+ assert.equal(result.ok, false);
96
+ assert.equal(result.missing[0].reason, 'missing_field');
97
+ });
98
+
99
+ // ---------- T3: invalid value fails ----------
100
+
101
+ run('T3 invalid value: typo / unknown reward type -> invalid', () => {
102
+ const { scanCommands } = freshLinter();
103
+ const dir = mkTempCommandsDir();
104
+ writeFixture(dir, 'typo.md', [
105
+ '---',
106
+ 'name: typo',
107
+ 'description: A command with a typo in the reward field',
108
+ 'interactive_first_reward: gibberish',
109
+ '---',
110
+ ].join('\n'));
111
+ const result = scanCommands(dir);
112
+ assert.equal(result.invalid.length, 1);
113
+ assert.equal(result.invalid[0].reason, 'invalid_value');
114
+ assert.equal(result.invalid[0].value, 'gibberish');
115
+ assert.equal(result.ok, false);
116
+ });
117
+
118
+ // ---------- T4: --none (scripting only) accepted verbatim ----------
119
+
120
+ run('T4 --none (scripting only) verbatim accepted per rule doc line 81', () => {
121
+ const { validateFrontmatter } = freshLinter();
122
+ const r = validateFrontmatter({
123
+ name: 'cron-job',
124
+ interactive_first_reward: '--none (scripting only)',
125
+ });
126
+ assert.equal(r.ok, true, 'expected --none (scripting only) to validate; got ' + JSON.stringify(r));
127
+ assert.equal(r.value, '--none (scripting only)');
128
+ });
129
+
130
+ // ---------- T5: every REWARD_TYPES enum value accepted ----------
131
+
132
+ run('T5 every REWARD_TYPES enum value accepted', () => {
133
+ const { validateFrontmatter, REWARD_TYPES } = freshLinter();
134
+ // Convert frozen Set to array for iteration -- check every entry.
135
+ for (const v of REWARD_TYPES) {
136
+ const r = validateFrontmatter({ interactive_first_reward: v });
137
+ assert.equal(r.ok, true, 'expected ' + JSON.stringify(v) + ' to validate; got ' + JSON.stringify(r));
138
+ }
139
+ // And spot-check the 5 named ones (plus the scripting opt-out = 6 total).
140
+ const expected = [
141
+ 'reframe_question',
142
+ 'instant_brief',
143
+ 'schema_preview',
144
+ 'calibration_distribution_preview',
145
+ 'paragraph_preview',
146
+ '--none (scripting only)',
147
+ ];
148
+ for (const v of expected) {
149
+ assert.ok(REWARD_TYPES.has(v), 'REWARD_TYPES missing canonical entry: ' + v);
150
+ }
151
+ });
152
+
153
+ // ---------- T6: no-frontmatter file degrades gracefully ----------
154
+
155
+ run('T6 file without ANY frontmatter: degrades to missing (does NOT crash)', () => {
156
+ const { scanCommands } = freshLinter();
157
+ const dir = mkTempCommandsDir();
158
+ writeFixture(dir, 'legacy.md', '# legacy command\n\nNo frontmatter at all.\n');
159
+ const result = scanCommands(dir);
160
+ assert.equal(result.missing.length, 1);
161
+ assert.equal(result.missing[0].reason, 'no_frontmatter');
162
+ assert.equal(result.invalid.length, 0);
163
+ });
164
+
165
+ // ---------- T7: CLI spawn -- compliant -> exit 0; missing -> exit 1 ----------
166
+
167
+ run('T7 CLI spawn: compliant dir exits 0; missing-field dir exits 1 with offender on stderr', () => {
168
+ // Subtest A: all compliant -> exit 0.
169
+ const dirGood = mkTempCommandsDir();
170
+ writeFixture(dirGood, 'one.md', [
171
+ '---',
172
+ 'name: one',
173
+ 'interactive_first_reward: instant_brief',
174
+ '---',
175
+ ].join('\n'));
176
+ const okResult = spawnSync(process.execPath, [CLI_PATH, dirGood], { encoding: 'utf8' });
177
+ assert.equal(okResult.status, 0, 'expected exit 0 on compliant; got ' + okResult.status + '\nstdout: ' + okResult.stdout + '\nstderr: ' + okResult.stderr);
178
+ assert.ok(/the rule holds/.test(okResult.stdout), 'expected Larry-voice success on stdout; got: ' + okResult.stdout);
179
+
180
+ // Subtest B: missing field -> exit 1, offender named on stderr.
181
+ const dirBad = mkTempCommandsDir();
182
+ writeFixture(dirBad, 'broken.md', [
183
+ '---',
184
+ 'name: broken',
185
+ 'description: A command that forgot the field',
186
+ '---',
187
+ ].join('\n'));
188
+ const badResult = spawnSync(process.execPath, [CLI_PATH, dirBad], { encoding: 'utf8' });
189
+ assert.equal(badResult.status, 1, 'expected exit 1 on missing; got ' + badResult.status);
190
+ assert.ok(/broken\.md/.test(badResult.stderr), 'expected stderr to name broken.md; got: ' + badResult.stderr);
191
+ assert.ok(/missing_field/.test(badResult.stderr), 'expected stderr to mention missing_field reason; got: ' + badResult.stderr);
192
+ });
193
+
194
+ // ---------- T8: hook is wired (CRITICAL-4 invariant) ----------
195
+
196
+ run('T8 hook wired (CRITICAL-4): scripts/hooks/pre-commit references check-reward-before-investment.cjs', () => {
197
+ const hook = fs.readFileSync(HOOK_PATH, 'utf8');
198
+ assert.ok(
199
+ hook.includes('check-reward-before-investment.cjs'),
200
+ 'hooks/pre-commit must reference check-reward-before-investment.cjs (CRITICAL-4); got hook of length ' + hook.length
201
+ );
202
+ });
203
+
204
+ // ---------- T9: scaffold E2E (CRITICAL-4) -- temp git repo, staged offender, hook blocks ----------
205
+
206
+ run('T9 scaffold E2E (CRITICAL-4): hook blocks a staged commands/foo.md that lacks the field', () => {
207
+ // Create an isolated temp git repo with a copy of our hook + scripts + lib.
208
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'mva-precommit-e2e-'));
209
+
210
+ // Initialize bare git repo
211
+ const init = spawnSync('git', ['init', '-q'], { cwd: tmp, encoding: 'utf8' });
212
+ if (init.status !== 0) {
213
+ // Skip if git is unavailable in this environment (exit 77 = POSIX skip).
214
+ process.stderr.write('T9 skipped: git init failed; env=' + init.stderr + '\n');
215
+ return;
216
+ }
217
+ // Local identity so commit (if ever attempted) wouldn't fail on identity --
218
+ // we never actually commit; the hook is run directly via bash.
219
+ spawnSync('git', ['config', 'user.email', 'test@example.invalid'], { cwd: tmp });
220
+ spawnSync('git', ['config', 'user.name', 'Test'], { cwd: tmp });
221
+
222
+ // Recreate the relevant directory structure inside the temp repo so the
223
+ // hook can resolve paths the same way it does in the real repo. We mirror:
224
+ // <tmp>/scripts/hooks/pre-commit
225
+ // <tmp>/scripts/check-reward-before-investment.cjs
226
+ // <tmp>/lib/core/mva-rule-linter.cjs
227
+ // <tmp>/commands/foo.md (the offender)
228
+ fs.mkdirSync(path.join(tmp, 'scripts', 'hooks'), { recursive: true });
229
+ fs.mkdirSync(path.join(tmp, 'lib', 'core'), { recursive: true });
230
+ fs.mkdirSync(path.join(tmp, 'commands'), { recursive: true });
231
+
232
+ fs.copyFileSync(HOOK_PATH, path.join(tmp, 'scripts', 'hooks', 'pre-commit'));
233
+ fs.copyFileSync(CLI_PATH, path.join(tmp, 'scripts', 'check-reward-before-investment.cjs'));
234
+ fs.copyFileSync(LINTER_PATH, path.join(tmp, 'lib', 'core', 'mva-rule-linter.cjs'));
235
+
236
+ // Install the hook into .git/hooks/
237
+ const installedHook = path.join(tmp, '.git', 'hooks', 'pre-commit');
238
+ fs.copyFileSync(HOOK_PATH, installedHook);
239
+ fs.chmodSync(installedHook, 0o755);
240
+
241
+ // The offender: commands/foo.md with NO interactive_first_reward field.
242
+ const offenderPath = path.join(tmp, 'commands', 'foo.md');
243
+ fs.writeFileSync(offenderPath, [
244
+ '---',
245
+ 'name: foo',
246
+ 'description: test command without the rule field',
247
+ '---',
248
+ '',
249
+ '# foo',
250
+ ].join('\n'), 'utf8');
251
+
252
+ // Stage the offender
253
+ spawnSync('git', ['add', 'commands/foo.md'], { cwd: tmp });
254
+
255
+ // Run the hook directly. The Phase 87-01a guard runs BEFORE the linter
256
+ // block; commands/ has no .room-root sentinel so the ROOM.md/MINTO.md
257
+ // invariant skips (per the find_room_root scoping). The Phase 122
258
+ // build-command-registry --check guard would run if scripts/build-command
259
+ // -registry.cjs exists in the temp repo -- we did NOT copy it, so that
260
+ // block is a no-op. That leaves OUR linter block as the gating check.
261
+ //
262
+ // The linter block must:
263
+ // 1. detect that commands/foo.md is staged
264
+ // 2. invoke node scripts/check-reward-before-investment.cjs
265
+ // 3. propagate the non-zero exit
266
+ //
267
+ // We invoke the hook with `bash` so the BASH_SOURCE machinery works.
268
+ const hookResult = spawnSync('bash', [installedHook], { cwd: tmp, encoding: 'utf8' });
269
+
270
+ // The hook should exit non-zero. The stderr should mention foo.md.
271
+ assert.notEqual(
272
+ hookResult.status, 0,
273
+ 'expected non-zero exit from pre-commit hook with missing-field commands/foo.md staged; got ' + hookResult.status +
274
+ '\nstdout: ' + hookResult.stdout + '\nstderr: ' + hookResult.stderr
275
+ );
276
+ // The hook should mention foo.md somewhere in its diagnostic output (either
277
+ // stdout or stderr is fine -- the linter writes the offender to stderr;
278
+ // the hook's wrapper line is on stderr too).
279
+ const combined = (hookResult.stdout || '') + (hookResult.stderr || '');
280
+ assert.ok(
281
+ /foo\.md/.test(combined),
282
+ 'expected hook diagnostic to name foo.md; combined output: ' + combined
283
+ );
284
+ });
285
+
286
+ // ---------- T10: WARN-5 -- new-project.md carries `instant_brief` (NOT reframe_question) ----------
287
+
288
+ run('T10 WARN-5: commands/new-project.md declares interactive_first_reward: instant_brief', () => {
289
+ const { parseFrontmatter } = freshLinter();
290
+ const npPath = path.resolve(REPO_ROOT, 'commands', 'new-project.md');
291
+ const fm = parseFrontmatter(fs.readFileSync(npPath, 'utf8'));
292
+ assert.ok(fm, 'commands/new-project.md must have parseable frontmatter');
293
+ assert.equal(
294
+ fm.interactive_first_reward,
295
+ 'instant_brief',
296
+ 'commands/new-project.md must carry interactive_first_reward: instant_brief (WARN-5; rule doc line 56-58 prescribes Instant Brief, NOT reframe_question); got: ' + JSON.stringify(fm.interactive_first_reward)
297
+ );
298
+ });
299
+
300
+ // ---------- T11: WARN-5 audit -- 4-command rule-doc parity ----------
301
+
302
+ run('T11 WARN-5 audit: 4 named commands match rule-doc remediation column', () => {
303
+ const { parseFrontmatter } = freshLinter();
304
+ // Per docs/reward-before-investment-rule.md (the canonical in-repo copy
305
+ // shipped by Plan 06 Task 2; the prescriptions live at lines 56-70 of the
306
+ // source ~/MindrianRooms/.../reward-before-investment-rule.md):
307
+ // new-project -> instant_brief (line 56-58)
308
+ // file-meeting -> paragraph_preview (line 60-62)
309
+ // grade -> calibration_distribution_preview (line 64-66)
310
+ // onboard -> reframe_question (line 68-70)
311
+ const prescribed = {
312
+ 'new-project.md': 'instant_brief',
313
+ 'file-meeting.md': 'paragraph_preview',
314
+ 'grade.md': 'calibration_distribution_preview',
315
+ 'onboard.md': 'reframe_question',
316
+ };
317
+ for (const [filename, expected] of Object.entries(prescribed)) {
318
+ const fpath = path.resolve(REPO_ROOT, 'commands', filename);
319
+ const fm = parseFrontmatter(fs.readFileSync(fpath, 'utf8'));
320
+ assert.ok(fm, 'commands/' + filename + ' must have parseable frontmatter');
321
+ assert.equal(
322
+ fm.interactive_first_reward,
323
+ expected,
324
+ 'commands/' + filename + ' declares ' + JSON.stringify(fm.interactive_first_reward) +
325
+ ' but rule doc prescribes ' + JSON.stringify(expected) + ' (WARN-5 4-command parity audit)'
326
+ );
327
+ }
328
+ });
329
+
330
+ // ---------- summary ----------
331
+
332
+ process.stdout.write('\nmva-rule-linter tests: ' + passed + '/' + (passed + failed) + ' passed\n');
333
+ if (failed > 0) {
334
+ process.exit(1);
335
+ }
336
+ process.exit(0);
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 118-00 Plan 00 -- mva-state: session-scoped state I/O for the
4
+ * 30-Second MVA pipeline. The wire between Plan 118-00 (this plan, writer)
5
+ * and Plans 118-01..05 (readers + dispatch).
6
+ *
7
+ * State file: ~/.mindrian/mva/<session-id>.json
8
+ * session-id = process.env.CLAUDE_SESSION_ID || 'default'
9
+ *
10
+ * Schema:
11
+ * {
12
+ * sentence_sha256: sha256 hex of the user's prompt sentence (64 chars)
13
+ * -- NEVER the raw sentence (Canon Part 8)
14
+ * classified_at: epoch ms
15
+ * classifier_source: 'heuristic' | 'heuristic_fallback' | 'language_detect' | 'haiku-4-5'
16
+ * classifier_confidence: 'high' | 'medium' | 'low'
17
+ * locale: 'en' | 'he'
18
+ * hebrew_refusal: true -- only present when LD1 short-circuit fired
19
+ * pipeline_status: 'pending' | 'running' | 'complete'
20
+ * started_at: epoch ms (set by markRunning)
21
+ * completed_at: epoch ms (set by markComplete)
22
+ * }
23
+ *
24
+ * Atomicity: every write goes to <session-id>.json.tmp.<pid>.<rand> then
25
+ * fs.renameSync to <session-id>.json. Mirrors the lib/core/rs-egress-telemetry
26
+ * tmp+rename pattern (Phase 89.2 Plan 01). A crashed writer cannot leave a
27
+ * half-JSON readers would choke on.
28
+ *
29
+ * Canon Part 8: file is LOCAL only. Zero network surface. The sentence_sha256
30
+ * is a one-way hash; no path from this file back to the raw prompt.
31
+ *
32
+ * Pure CJS, node built-ins only (fs, path, os).
33
+ */
34
+
35
+ const fs = require('node:fs');
36
+ const path = require('node:path');
37
+ const os = require('node:os');
38
+
39
+ function homeDir() {
40
+ // Match the env-aware home resolution pattern from resolve-brain-key.cjs
41
+ // so hermetic tests overriding process.env.HOME on Linux/POSIX work
42
+ // (os.homedir reads /etc/passwd and ignores process.env.HOME).
43
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
44
+ }
45
+
46
+ function stateDir() {
47
+ return path.join(homeDir(), '.mindrian', 'mva');
48
+ }
49
+
50
+ function sessionId() {
51
+ const s = process.env.CLAUDE_SESSION_ID;
52
+ return (typeof s === 'string' && s.length > 0) ? s : 'default';
53
+ }
54
+
55
+ function stateFile() {
56
+ return path.join(stateDir(), sessionId() + '.json');
57
+ }
58
+
59
+ function ensureDir() {
60
+ const dir = stateDir();
61
+ try {
62
+ fs.mkdirSync(dir, { recursive: true });
63
+ } catch (_e) {
64
+ // best-effort; the writeFileSync will surface a clearer error
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Atomic write: writeFileSync to a tmp path, then renameSync into place.
70
+ * Throws on fs error -- callers (hook scripts) MUST wrap in try/catch and
71
+ * exit 0 to honor the Phase 117 hook-best-effort contract.
72
+ */
73
+ function _atomicWrite(target, body) {
74
+ ensureDir();
75
+ const tmp = target + '.tmp.' + process.pid + '.' + Math.random().toString(36).slice(2, 10);
76
+ fs.writeFileSync(tmp, body, 'utf8');
77
+ fs.renameSync(tmp, target);
78
+ }
79
+
80
+ /**
81
+ * Read the current state file. Returns null on absent / parse-error /
82
+ * read-error (graceful degradation; readers treat null as "no pending").
83
+ */
84
+ function readPending() {
85
+ let raw;
86
+ try {
87
+ raw = fs.readFileSync(stateFile(), 'utf8');
88
+ } catch (_e) {
89
+ return null; // ENOENT or permission -- treat as no state
90
+ }
91
+ try {
92
+ return JSON.parse(raw);
93
+ } catch (_e) {
94
+ return null; // corrupt file -- treat as no state
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Write the venture-classified or hebrew-refusal payload. Initializes
100
+ * pipeline_status='pending' so the dispatcher (Plan 118-01) knows to fire.
101
+ *
102
+ * @param {object} payload Must include sentence_sha256, classified_at,
103
+ * classifier_source, classifier_confidence, locale.
104
+ * May include hebrew_refusal:true (per LD1).
105
+ */
106
+ function writePending(payload) {
107
+ const body = Object.assign({}, payload, { pipeline_status: 'pending' });
108
+ _atomicWrite(stateFile(), JSON.stringify(body));
109
+ }
110
+
111
+ /**
112
+ * Transition pipeline_status -> 'running'. The dispatcher calls this when
113
+ * it begins the 6-agent fan-out so a re-entrant UserPromptSubmit does not
114
+ * re-fire the pipeline for an in-flight session.
115
+ */
116
+ function markRunning() {
117
+ const cur = readPending();
118
+ if (!cur) return; // nothing to mark; silent
119
+ const body = Object.assign({}, cur, {
120
+ pipeline_status: 'running',
121
+ started_at: Date.now(),
122
+ });
123
+ _atomicWrite(stateFile(), JSON.stringify(body));
124
+ }
125
+
126
+ /**
127
+ * Transition pipeline_status -> 'complete'. The orchestrator calls this
128
+ * after the deck deploys (Plan 118-04) or the budget exhausts gracefully.
129
+ */
130
+ function markComplete() {
131
+ const cur = readPending();
132
+ if (!cur) return;
133
+ const body = Object.assign({}, cur, {
134
+ pipeline_status: 'complete',
135
+ completed_at: Date.now(),
136
+ });
137
+ _atomicWrite(stateFile(), JSON.stringify(body));
138
+ }
139
+
140
+ /**
141
+ * @returns {boolean} true if a pipeline is currently running for this session
142
+ * (the hook uses this to gate re-fire on a second UserPromptSubmit).
143
+ */
144
+ function isAlreadyRunning() {
145
+ const cur = readPending();
146
+ return !!(cur && cur.pipeline_status === 'running');
147
+ }
148
+
149
+ module.exports = {
150
+ writePending,
151
+ readPending,
152
+ markRunning,
153
+ markComplete,
154
+ isAlreadyRunning,
155
+ stateDir,
156
+ // exposed for tests + downstream plans that need the canonical path
157
+ stateFile,
158
+ sessionId,
159
+ };
@@ -0,0 +1,58 @@
1
+ /*
2
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
3
+ * Phase 121-01 -- mva-telemetry shim. Delegates to lib/core/telemetry/writer.cjs
4
+ * (the Canon Part 9 chokepoint), with a legacy dual-write to the historical
5
+ * ~/.mindrian/telemetry/v1.13/mva.jsonl path so existing readers keep working.
6
+ * TODO(v1.14.0): delete this shim; callers must import lib/core/telemetry/writer.cjs directly.
7
+ */
8
+ 'use strict';
9
+
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const os = require('node:os');
13
+
14
+ const writer = require('./telemetry/writer.cjs');
15
+ const validator = require('./telemetry/validator.cjs');
16
+ const schema = require('./telemetry/schema.cjs');
17
+
18
+ const EVENT_TYPES = Object.freeze([
19
+ 'mva_pipeline_started', 'mva_agent_returned', 'mva_brief_rendered',
20
+ 'mva_option_selected', 'mva_brief_deployed', 'mva_pipeline_failed',
21
+ ]);
22
+
23
+ const ALLOWED_FIELDS = Object.freeze({
24
+ mva_pipeline_started: schema.ALLOWED_FIELDS.mva_pipeline_started,
25
+ mva_agent_returned: schema.ALLOWED_FIELDS.mva_agent_returned,
26
+ mva_brief_rendered: schema.ALLOWED_FIELDS.mva_brief_rendered,
27
+ mva_option_selected: schema.ALLOWED_FIELDS.mva_option_selected,
28
+ mva_brief_deployed: schema.ALLOWED_FIELDS.mva_brief_deployed,
29
+ mva_pipeline_failed: schema.ALLOWED_FIELDS.mva_pipeline_failed,
30
+ });
31
+
32
+ function _homeDir() {
33
+ return process.env.HOME || process.env.USERPROFILE || os.homedir();
34
+ }
35
+
36
+ function _legacyMvaPath() {
37
+ return path.join(_homeDir(), '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
38
+ }
39
+
40
+ function emit(event, payload) {
41
+ writer.emit(event, payload); // Canon Part 8 gate + unified events-YYYY-WNN.jsonl append.
42
+ try {
43
+ const sid = (typeof process.env.CLAUDE_SESSION_ID === 'string' && process.env.CLAUDE_SESSION_ID.length > 0)
44
+ ? process.env.CLAUDE_SESSION_ID.slice(0, schema.MAX_STRING_LEN) : 'default';
45
+ const record = Object.assign({ event: event, timestamp: new Date().toISOString(), session_id: sid }, payload);
46
+ fs.mkdirSync(path.dirname(_legacyMvaPath()), { recursive: true });
47
+ fs.appendFileSync(_legacyMvaPath(), JSON.stringify(record) + '\n', 'utf8');
48
+ } catch (_) { /* best-effort legacy dual-write */ }
49
+ }
50
+
51
+ module.exports = {
52
+ emit: emit,
53
+ validateEventPayload: validator.validateEventPayload,
54
+ EVENT_TYPES: EVENT_TYPES,
55
+ ALLOWED_FIELDS: ALLOWED_FIELDS,
56
+ telemetryDir: writer.telemetryDir,
57
+ telemetryFile: writer.telemetryFile,
58
+ };