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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.mcp.json +6 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +51 -56
  5. package/bin/mindrian-brain-mcp-client.cjs +152 -0
  6. package/commands/act.md +1 -0
  7. package/commands/admin.md +1 -0
  8. package/commands/analyze-needs.md +2 -0
  9. package/commands/analyze-systems.md +2 -0
  10. package/commands/analyze-timing.md +2 -0
  11. package/commands/auto-explore.md +2 -0
  12. package/commands/beautiful-question.md +2 -0
  13. package/commands/brain-derive.md +2 -0
  14. package/commands/build-knowledge.md +2 -0
  15. package/commands/build-thesis.md +2 -0
  16. package/commands/causal.md +2 -0
  17. package/commands/challenge-assumptions.md +2 -0
  18. package/commands/compare-ventures.md +2 -0
  19. package/commands/dashboard.md +2 -1
  20. package/commands/deep-grade.md +2 -0
  21. package/commands/diagnose.md +21 -1
  22. package/commands/diagnostics.md +14 -3
  23. package/commands/doctor.md +6 -2
  24. package/commands/dogfood-flush.md +92 -0
  25. package/commands/dominant-designs.md +2 -0
  26. package/commands/explain-decision.md +2 -0
  27. package/commands/explore-domains.md +2 -0
  28. package/commands/explore-futures.md +2 -0
  29. package/commands/explore-trends.md +2 -0
  30. package/commands/export.md +1 -0
  31. package/commands/feynman-timeline-refresh.md +2 -0
  32. package/commands/file-meeting.md +2 -0
  33. package/commands/find-analogies.md +1 -0
  34. package/commands/find-bottlenecks.md +2 -0
  35. package/commands/find-connections.md +2 -0
  36. package/commands/funding.md +1 -0
  37. package/commands/grade.md +2 -0
  38. package/commands/graph.md +1 -0
  39. package/commands/hat-briefing.md +1 -0
  40. package/commands/heal.md +22 -170
  41. package/commands/help.md +54 -334
  42. package/commands/hmi-status.md +23 -144
  43. package/commands/jtbd.md +1 -0
  44. package/commands/leadership.md +2 -0
  45. package/commands/lean-canvas.md +2 -0
  46. package/commands/macro-trends.md +2 -0
  47. package/commands/map-unknowns.md +2 -0
  48. package/commands/memory.md +1 -0
  49. package/commands/models.md +1 -0
  50. package/commands/mos-reason.md +2 -0
  51. package/commands/mos.md +139 -0
  52. package/commands/mullins.md +2 -0
  53. package/commands/mva-brief.md +2 -0
  54. package/commands/mva-option.md +2 -0
  55. package/commands/new-project.md +2 -0
  56. package/commands/onboard.md +20 -7
  57. package/commands/operator.md +1 -0
  58. package/commands/opportunities.md +1 -0
  59. package/commands/organize.md +22 -469
  60. package/commands/persona.md +1 -0
  61. package/commands/pipeline.md +2 -0
  62. package/commands/present.md +1 -0
  63. package/commands/publish.md +2 -0
  64. package/commands/query.md +24 -102
  65. package/commands/radar.md +2 -0
  66. package/commands/reanalyze.md +1 -0
  67. package/commands/research.md +2 -0
  68. package/commands/room.md +2 -0
  69. package/commands/rooms.md +1 -0
  70. package/commands/root-cause.md +2 -0
  71. package/commands/rs-experts.md +1 -0
  72. package/commands/rs-explain.md +1 -0
  73. package/commands/rs-fetch.md +1 -0
  74. package/commands/rs-thesis.md +1 -0
  75. package/commands/scenario-plan.md +2 -0
  76. package/commands/scheduled-tasks.md +1 -0
  77. package/commands/score-innovation.md +2 -0
  78. package/commands/scout.md +1 -0
  79. package/commands/setup.md +2 -0
  80. package/commands/snapshot.md +2 -0
  81. package/commands/speakers.md +1 -0
  82. package/commands/splash.md +5 -2
  83. package/commands/status.md +1 -0
  84. package/commands/structure-argument.md +2 -0
  85. package/commands/suggest-next.md +2 -0
  86. package/commands/systems-thinking.md +2 -0
  87. package/commands/think-hats.md +2 -0
  88. package/commands/update.md +2 -0
  89. package/commands/user-needs.md +2 -0
  90. package/commands/validate.md +2 -0
  91. package/commands/value-proposition.md +2 -0
  92. package/commands/vault.md +2 -0
  93. package/commands/visualize.md +24 -29
  94. package/commands/whitespace.md +2 -1
  95. package/commands/wiki.md +1 -0
  96. package/hooks/hooks.json +22 -88
  97. package/lib/agents/auto-explore-agent.cjs +82 -0
  98. package/lib/core/breakthrough/canary.cjs +134 -0
  99. package/lib/core/breakthrough/canary.test.cjs +136 -0
  100. package/lib/core/breakthrough/detectors.cjs +359 -0
  101. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  102. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  103. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  104. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  105. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  106. package/lib/core/breakthrough/review-queue.cjs +154 -0
  107. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  108. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  109. package/lib/core/breakthrough/scanner.cjs +426 -0
  110. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  111. package/lib/core/breakthrough/schema.cjs +164 -0
  112. package/lib/core/breakthrough/schema.test.cjs +256 -0
  113. package/lib/core/breakthrough/scoring.cjs +293 -0
  114. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  115. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  116. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  117. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  118. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  119. package/lib/core/directive-envelope.cjs +175 -0
  120. package/lib/core/directive-envelope.test.cjs +225 -0
  121. package/lib/core/doctor/class-m-brain-smoke.cjs +278 -0
  122. package/lib/core/doctor/class-m-brain-smoke.test.cjs +310 -0
  123. package/lib/core/first-touch-version-stamper.cjs +113 -0
  124. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  125. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  126. package/lib/core/llm-name-suggester.cjs +194 -0
  127. package/lib/core/llm-name-suggester.test.cjs +132 -0
  128. package/lib/core/mcp-profiles.cjs +1 -1
  129. package/lib/core/migration-snapshot.cjs +172 -0
  130. package/lib/core/migration-snapshot.test.cjs +174 -0
  131. package/lib/core/mindrian-brain-shim.test.cjs +214 -0
  132. package/lib/core/mva-orchestrator.cjs +41 -0
  133. package/lib/core/mva-telemetry.cjs +31 -143
  134. package/lib/core/navigation/edges.cjs +35 -0
  135. package/lib/core/navigation/memory-events.cjs +126 -0
  136. package/lib/core/room-auto-create.cjs +318 -0
  137. package/lib/core/room-auto-create.test.cjs +198 -0
  138. package/lib/core/room-discard-cascade.cjs +225 -0
  139. package/lib/core/room-discard-cascade.test.cjs +135 -0
  140. package/lib/core/room-name-validator.cjs +132 -0
  141. package/lib/core/room-name-validator.test.cjs +156 -0
  142. package/lib/core/room-naming-selector.cjs +357 -0
  143. package/lib/core/room-naming-selector.test.cjs +277 -0
  144. package/lib/core/room-receipt-emit.cjs +63 -0
  145. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  146. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  147. package/lib/core/rs-nl-to-query.cjs +1 -1
  148. package/lib/core/stale-copy-scanner.cjs +190 -0
  149. package/lib/core/state-aware-router.cjs +78 -0
  150. package/lib/core/telemetry/schema.cjs +168 -0
  151. package/lib/core/telemetry/schema.test.cjs +124 -0
  152. package/lib/core/telemetry/validator.cjs +200 -0
  153. package/lib/core/telemetry/validator.test.cjs +188 -0
  154. package/lib/core/telemetry/writer.cjs +141 -0
  155. package/lib/core/telemetry/writer.test.cjs +331 -0
  156. package/lib/core/terminal-capability.cjs +88 -0
  157. package/lib/core/tier0-messaging.cjs +109 -0
  158. package/lib/core/tier0-messaging.test.cjs +218 -0
  159. package/lib/core/venture-shape-nudge.cjs +163 -0
  160. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  161. package/lib/core/visual-ops.cjs +70 -2
  162. package/lib/hmi/selector-dispatcher.cjs +90 -1
  163. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  164. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  165. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  166. package/lib/memory/brain-derivation-graceful-degradation.test.cjs +2 -2
  167. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  168. package/lib/memory/first-touch-version.test.cjs +198 -0
  169. package/lib/memory/help-coverage.test.cjs +108 -0
  170. package/lib/memory/help-renderer.test.cjs +145 -0
  171. package/lib/memory/mos-status-renderer.test.cjs +2 -2
  172. package/lib/memory/navigation-engine-core.test.cjs +1 -1
  173. package/lib/memory/palette-consistency.test.cjs +127 -0
  174. package/lib/memory/pending-tension-store.cjs +80 -0
  175. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  176. package/lib/memory/run-feynman-tests.cjs +223 -0
  177. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  178. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  179. package/lib/memory/soft-alias.test.cjs +144 -0
  180. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  181. package/lib/memory/state-aware-router.test.cjs +90 -0
  182. package/lib/memory/statusline-two-row.test.cjs +338 -0
  183. package/lib/memory/terminal-capability.test.cjs +155 -0
  184. package/lib/render/ROOM.md +74 -22
  185. package/lib/sessionstart/budget-compressor.cjs +130 -0
  186. package/lib/sessionstart/contributor-interface.cjs +134 -0
  187. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  188. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  189. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  190. package/lib/statusline/two-row-renderer.cjs +186 -0
  191. package/lib/statusline/version-resolver.cjs +81 -0
  192. package/package.json +1 -1
  193. package/references/visual/ROOM.md +55 -0
  194. package/references/visual/palette.json +54 -0
  195. package/skills/larry-personality/SKILL.md +34 -0
  196. package/skills/ui-system/SKILL.md +109 -1
  197. package/skills/ui-system/rules/dual-palette.md +156 -0
  198. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  199. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 121.5-01 Task 2 -- Body-shape coverage acceptance tests.
8
+ *
9
+ * Verifies scripts/audit-body-shape-coverage.cjs and output-styles/destijl.md.
10
+ * The audit script is the CI tripwire that scans commands/*.md frontmatter for
11
+ * body_shape: presence and a valid value from the locked vocabulary. The
12
+ * output style is the system-prompt-enforced 4-zone De Stijl format that lifts
13
+ * SKILL.md from skill-instruction-suggested to force-for-plugin-enforced.
14
+ *
15
+ * Canon references:
16
+ * Part 3 UI Ruling System -- this test enforces the body_shape contract
17
+ * that the ruling system depends on.
18
+ * Part 7 Reuse Before Build -- no new commands are created; this is a
19
+ * pure compliance + infrastructure pass.
20
+ * Part 8 Graph Boundary -- audit script + output style add zero network
21
+ * surface, zero Brain calls, zero telemetry egress.
22
+ *
23
+ * Test map (7 cases, one-to-one with PLAN Task 2 <behavior>):
24
+ *
25
+ * 1. output-styles/destijl.md exists and parses as valid YAML frontmatter
26
+ * + markdown body.
27
+ * 2. Frontmatter contains exactly `force-for-plugin: true` AND
28
+ * `keep-coding-instructions: true` (literal string match).
29
+ * 3. Body documents the four zones in fixed order matching SKILL.md
30
+ * Section 1: Header Panel / Content Body / Intelligence Strip /
31
+ * Action Footer.
32
+ * 4. Body documents the 5+2 body shapes (A, B, C, D, E + F family)
33
+ * mapping to the SKILL.md Section 2 vocabulary.
34
+ * 5. scripts/audit-body-shape-coverage.cjs exits 0 against the live
35
+ * commands/ directory (post-Task-1 state). Exits 1 against a
36
+ * synthetic fixture with a command missing body_shape:.
37
+ * 6. The audit script reports per-category counts (A / B / C / D / E /
38
+ * F.x / methodology) in --json mode.
39
+ * 7. Every body_shape: value in commands/*.md is one of the locked
40
+ * vocabulary {A, B, C, D, E, F.0..F.6, methodology}. The audit
41
+ * script flags any other value.
42
+ */
43
+
44
+ const assert = require('node:assert/strict');
45
+ const fs = require('node:fs');
46
+ const os = require('node:os');
47
+ const path = require('node:path');
48
+ const { execFileSync } = require('node:child_process');
49
+
50
+ const REPO = path.resolve(__dirname, '..', '..');
51
+ const DESTIJL_PATH = path.join(REPO, 'output-styles', 'destijl.md');
52
+ const AUDIT_PATH = path.join(REPO, 'scripts', 'audit-body-shape-coverage.cjs');
53
+ const COMMANDS_DIR = path.join(REPO, 'commands');
54
+
55
+ // ---------- Fixture helpers ----------
56
+
57
+ const TMP_ROOTS = [];
58
+ function mkTmp(prefix) {
59
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
60
+ TMP_ROOTS.push(d);
61
+ return d;
62
+ }
63
+ process.on('exit', () => {
64
+ for (const d of TMP_ROOTS) {
65
+ try { fs.rmSync(d, { recursive: true, force: true }); } catch (_) {}
66
+ }
67
+ });
68
+
69
+ function readDestijl() {
70
+ return fs.readFileSync(DESTIJL_PATH, 'utf8');
71
+ }
72
+
73
+ function loadAudit() {
74
+ try { delete require.cache[require.resolve(AUDIT_PATH)]; } catch (_) {}
75
+ return require(AUDIT_PATH);
76
+ }
77
+
78
+ // Run audit script as a subprocess (mirrors how CI will invoke it).
79
+ // Returns { code, stdout, stderr }.
80
+ function runAudit(args, env) {
81
+ const result = { code: 0, stdout: '', stderr: '' };
82
+ try {
83
+ result.stdout = execFileSync('node', [AUDIT_PATH, ...(args || [])], {
84
+ env: Object.assign({}, process.env, env || {}),
85
+ encoding: 'utf8',
86
+ stdio: ['ignore', 'pipe', 'pipe'],
87
+ });
88
+ } catch (e) {
89
+ result.code = (e && typeof e.status === 'number') ? e.status : 1;
90
+ result.stdout = (e && e.stdout) ? e.stdout.toString() : '';
91
+ result.stderr = (e && e.stderr) ? e.stderr.toString() : '';
92
+ }
93
+ return result;
94
+ }
95
+
96
+ // ---------- Tests ----------
97
+
98
+ let passed = 0;
99
+ let failed = 0;
100
+ const failures = [];
101
+
102
+ function test(name, fn) {
103
+ try {
104
+ fn();
105
+ passed++;
106
+ console.log(' ok ' + name);
107
+ } catch (e) {
108
+ failed++;
109
+ failures.push({ name, error: e });
110
+ console.log(' FAIL ' + name);
111
+ console.log(' ' + (e && e.message ? e.message : String(e)));
112
+ }
113
+ }
114
+
115
+ console.log('Phase 121.5-01 Task 2 -- body-shape-coverage acceptance suite');
116
+ console.log('');
117
+
118
+ // Test 1: destijl.md exists and parses
119
+ test('Test 1: output-styles/destijl.md exists with frontmatter + body', () => {
120
+ assert.ok(fs.existsSync(DESTIJL_PATH), 'output-styles/destijl.md missing');
121
+ const text = readDestijl();
122
+ const fm = text.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
123
+ assert.ok(fm, 'destijl.md does not parse as frontmatter + body');
124
+ assert.ok(fm[1].length > 0, 'frontmatter is empty');
125
+ assert.ok(fm[2].length > 100, 'body is too short (< 100 chars)');
126
+ });
127
+
128
+ // Test 2: force-for-plugin + keep-coding-instructions present
129
+ test('Test 2: frontmatter has force-for-plugin: true AND keep-coding-instructions: true', () => {
130
+ const text = readDestijl();
131
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
132
+ assert.ok(fm, 'no frontmatter found');
133
+ assert.ok(/^force-for-plugin:\s*true\s*$/m.test(fm[1]),
134
+ 'force-for-plugin: true not found in frontmatter');
135
+ assert.ok(/^keep-coding-instructions:\s*true\s*$/m.test(fm[1]),
136
+ 'keep-coding-instructions: true not found in frontmatter');
137
+ });
138
+
139
+ // Test 3: 4 zones documented in fixed order
140
+ test('Test 3: body documents 4 zones in SKILL.md Section 1 order', () => {
141
+ const text = readDestijl();
142
+ const idxHeader = text.indexOf('Header Panel');
143
+ const idxBody = text.indexOf('Content Body');
144
+ const idxStrip = text.indexOf('Intelligence Strip');
145
+ const idxFooter = text.indexOf('Action Footer');
146
+ assert.ok(idxHeader > 0, 'Header Panel zone not documented');
147
+ assert.ok(idxBody > idxHeader, 'Content Body must come after Header Panel');
148
+ assert.ok(idxStrip > idxBody, 'Intelligence Strip must come after Content Body');
149
+ assert.ok(idxFooter > idxStrip, 'Action Footer must come after Intelligence Strip');
150
+ });
151
+
152
+ // Test 4: 5+2 body shapes documented
153
+ test('Test 4: body documents shapes A, B, C, D, E + F family + methodology', () => {
154
+ const text = readDestijl();
155
+ // Each shape letter must appear in the shape table or shape documentation.
156
+ // Use regex to find the shape table row pattern: "| <letter> |" or "Shape <letter>".
157
+ for (const shape of ['A', 'B', 'C', 'D', 'E']) {
158
+ const re = new RegExp('(\\|\\s*' + shape + '\\s*\\||Shape\\s+' + shape + '\\b)');
159
+ assert.ok(re.test(text), 'Shape ' + shape + ' not documented in body');
160
+ }
161
+ // F family members
162
+ for (const fSub of ['F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6']) {
163
+ const re = new RegExp('\\b' + fSub.replace('.', '\\.') + '\\b');
164
+ assert.ok(re.test(text), fSub + ' not documented in body');
165
+ }
166
+ assert.ok(/methodology/i.test(text), 'methodology not documented in body');
167
+ });
168
+
169
+ // Test 5a: audit script exits 0 against live commands/ (post-Task-1 state)
170
+ test('Test 5a: audit exits 0 against live commands/ (100% coverage)', () => {
171
+ assert.ok(fs.existsSync(AUDIT_PATH), 'audit script missing');
172
+ const r = runAudit([]);
173
+ if (r.code !== 0) {
174
+ console.log(' stdout: ' + r.stdout);
175
+ console.log(' stderr: ' + r.stderr);
176
+ }
177
+ assert.strictEqual(r.code, 0, 'audit script exited non-zero on live repo');
178
+ });
179
+
180
+ // Test 5b: audit exits 1 on synthetic fixture missing body_shape
181
+ test('Test 5b: audit exits 1 on synthetic command missing body_shape', () => {
182
+ const tmp = mkTmp('body-shape-audit-');
183
+ const cmdsDir = path.join(tmp, 'commands');
184
+ fs.mkdirSync(cmdsDir, { recursive: true });
185
+ // Good file
186
+ fs.writeFileSync(path.join(cmdsDir, 'good.md'),
187
+ '---\nname: good\ndescription: test\nbody_shape: A\n---\n# good\n');
188
+ // Bad file (missing body_shape)
189
+ fs.writeFileSync(path.join(cmdsDir, 'bad.md'),
190
+ '---\nname: bad\ndescription: test\n---\n# bad\n');
191
+
192
+ // Audit accepts a --dir override.
193
+ const r = runAudit(['--dir', cmdsDir]);
194
+ assert.notStrictEqual(r.code, 0,
195
+ 'audit must fail when a command is missing body_shape (got exit 0)');
196
+ });
197
+
198
+ // Test 6: --json mode reports per-category counts
199
+ test('Test 6: --json mode reports histogram per category', () => {
200
+ const r = runAudit(['--json']);
201
+ assert.strictEqual(r.code, 0, 'audit --json exited non-zero');
202
+ let parsed;
203
+ try {
204
+ parsed = JSON.parse(r.stdout);
205
+ } catch (e) {
206
+ assert.fail('--json output not valid JSON: ' + e.message);
207
+ }
208
+ assert.ok(parsed && typeof parsed === 'object', '--json must emit object');
209
+ assert.ok(typeof parsed.total === 'number', 'total missing in --json output');
210
+ assert.ok(parsed.histogram && typeof parsed.histogram === 'object',
211
+ 'histogram missing in --json output');
212
+ // At least 3 distinct shape categories should appear in a healthy repo
213
+ // (proves the sweep is a spread, not a monoculture).
214
+ const keys = Object.keys(parsed.histogram);
215
+ assert.ok(keys.length >= 3,
216
+ 'histogram has < 3 distinct shapes -- monoculture detected: ' + keys.join(','));
217
+ });
218
+
219
+ // Test 7: all body_shape values in live repo are in the locked vocabulary
220
+ test('Test 7: every body_shape value in commands/*.md is in locked vocab', () => {
221
+ const { VALID_SHAPES } = loadAudit();
222
+ assert.ok(VALID_SHAPES && typeof VALID_SHAPES.has === 'function',
223
+ 'audit must export VALID_SHAPES Set');
224
+ // Sanity-check the vocabulary itself
225
+ for (const v of ['A', 'B', 'C', 'D', 'E',
226
+ 'F.0', 'F.1', 'F.2', 'F.3', 'F.4', 'F.5', 'F.6',
227
+ 'methodology']) {
228
+ assert.ok(VALID_SHAPES.has(v), 'VALID_SHAPES missing required value: ' + v);
229
+ }
230
+ // Now scan live commands and validate each
231
+ const files = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md'));
232
+ const offenders = [];
233
+ for (const f of files) {
234
+ const text = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf8');
235
+ const fm = text.match(/^---\n([\s\S]*?)\n---/);
236
+ if (!fm) { offenders.push(f + ' (no frontmatter)'); continue; }
237
+ const m = fm[1].match(/^body_shape:\s*(.+)$/m);
238
+ if (!m) { offenders.push(f + ' (no body_shape)'); continue; }
239
+ // Strip trailing parenthetical (e.g. "B (Semantic Tree)" -> "B")
240
+ let v = m[1].trim().replace(/\s*\(.*\)\s*$/, '');
241
+ // Strip surrounding quotes if present
242
+ v = v.replace(/^['"]|['"]$/g, '');
243
+ if (!VALID_SHAPES.has(v)) {
244
+ offenders.push(f + ' = "' + m[1].trim() + '" (extracted: "' + v + '")');
245
+ }
246
+ }
247
+ if (offenders.length) {
248
+ assert.fail('commands with invalid body_shape values:\n ' + offenders.join('\n '));
249
+ }
250
+ });
251
+
252
+ // ---------- Summary ----------
253
+
254
+ console.log('');
255
+ console.log('========================================');
256
+ console.log('Tests: ' + (passed + failed) + ' total, ' + passed + ' passed, ' + failed + ' failed');
257
+ console.log('========================================');
258
+
259
+ if (failed > 0) {
260
+ console.log('');
261
+ console.log('Failures:');
262
+ for (const f of failures) {
263
+ console.log(' - ' + f.name);
264
+ }
265
+ process.exit(1);
266
+ }
267
+
268
+ process.exit(0);
@@ -620,8 +620,8 @@ async function main() {
620
620
  // category); no BRAIN.md; no tmpfile.
621
621
  await runScenario('Scenario 07: Network partition', async function () {
622
622
  installMockBrainClient({
623
- query: function () { throw new Error('ECONNREFUSED brain.mindrian.ai:443'); },
624
- search: function () { throw new Error('ECONNREFUSED brain.mindrian.ai:443'); },
623
+ query: function () { throw new Error('ECONNREFUSED mindrian-brain.onrender.com:443'); },
624
+ search: function () { throw new Error('ECONNREFUSED mindrian-brain.onrender.com:443'); },
625
625
  });
626
626
  const { sections } = buildRoom({ tag: 's07', count: 1 });
627
627
  const s = sections[0];
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Phase 121.5-08 Plan Task 2 -- doctor class I (deprecated-usage surface)
4
+ * + F.1 selector wiring on onboard Step 6 + diagnose recommendation.
5
+ *
6
+ * Behavior contract (per plan):
7
+ * T1: scripts/doctor.cjs --deprecated-usage detects synthetic usage in temp
8
+ * transcript fixtures (a temp ~/.claude/projects/.../session.jsonl-like
9
+ * fixture mentioning "/mos:heal" -- the class I detector picks it up).
10
+ * T2: Class I returns {class: 'I', status, action, deprecated_uses: []}.
11
+ * T3: --deprecated-usage exits 0 when no deprecated commands used; exits 1
12
+ * when violations found.
13
+ * T4: doctor --all includes class I in the cascade.
14
+ * T5: commands/onboard.md Step 6 body contains the canonical F.1 selector
15
+ * vocabulary (Run Methodology / Defer / Free-Text).
16
+ * T6: commands/diagnose.md recommendation block contains the canonical F.1
17
+ * selector vocabulary.
18
+ * T7: CHANGELOG.md has an Unreleased / next-beta entry listing all 5
19
+ * renames + the /mos:mos creation + the F.1 wiring + the class I add.
20
+ *
21
+ * Hermetic: uses fs.mkdtempSync for synthetic transcript fixture (T1). The
22
+ * doctor invocation spawns scripts/doctor.cjs as a subprocess with the
23
+ * fixture dir bound via MINDRIAN_DOCTOR_TRANSCRIPTS_DIR.
24
+ */
25
+ 'use strict';
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const os = require('os');
30
+ const cp = require('child_process');
31
+
32
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
33
+ const DOCTOR_CJS = path.join(REPO_ROOT, 'scripts', 'doctor.cjs');
34
+
35
+ let pass = 0;
36
+ let fail = 0;
37
+ const failures = [];
38
+
39
+ function assert(cond, name, detail) {
40
+ if (cond) {
41
+ pass++;
42
+ console.log('PASS ' + name);
43
+ } else {
44
+ fail++;
45
+ failures.push(name + (detail ? ' -- ' + detail : ''));
46
+ console.log('FAIL ' + name + (detail ? ' -- ' + detail : ''));
47
+ }
48
+ }
49
+
50
+ // --- T1 + T2 + T3 (positive): synthesize a fixture transcript dir with
51
+ // "/mos:heal" usage; --deprecated-usage detects + exits 1 + returns class I.
52
+ {
53
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-'));
54
+ const sessionDir = path.join(tmpRoot, 'sessions', 'fake-project-hash');
55
+ fs.mkdirSync(sessionDir, { recursive: true });
56
+ const jsonlPath = path.join(sessionDir, 'session-abc.jsonl');
57
+ // Recent mtime (now). Per implementation contract: scan last 7 days.
58
+ const fixture = JSON.stringify({ role: 'user', content: 'let me run /mos:heal here' }) + '\n'
59
+ + JSON.stringify({ role: 'user', content: 'now /mos:query something' }) + '\n';
60
+ fs.writeFileSync(jsonlPath, fixture);
61
+ const now = Date.now();
62
+ fs.utimesSync(jsonlPath, new Date(now), new Date(now));
63
+
64
+ const r = cp.spawnSync('node', [DOCTOR_CJS, '--deprecated-usage', '--json'], {
65
+ encoding: 'utf8',
66
+ env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
67
+ timeout: 30000,
68
+ });
69
+ // Parse JSON envelope
70
+ let envelope = null;
71
+ if (r.stdout) {
72
+ const start = r.stdout.indexOf('{');
73
+ if (start >= 0) {
74
+ try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) { envelope = null; }
75
+ }
76
+ }
77
+ assert(envelope !== null, 'T1: --deprecated-usage emits parseable JSON', (r.stdout || '').slice(0, 300));
78
+ const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
79
+ assert(cls !== undefined && cls !== null, 'T1: report.checks["deprecated-usage"] exists', JSON.stringify(envelope && envelope.checks));
80
+ if (cls) {
81
+ assert(cls.class === 'L', 'T2: class==="L"', cls.class);
82
+ assert(typeof cls.status === 'string', 'T2: status is a string', cls.status);
83
+ assert(typeof cls.action === 'string', 'T2: action is a string', cls.action);
84
+ assert(Array.isArray(cls.deprecated_uses), 'T2: deprecated_uses is array', JSON.stringify(cls.deprecated_uses));
85
+ assert(cls.deprecated_uses.length >= 2, 'T1: detected at least 2 deprecated uses (heal + query)', JSON.stringify(cls.deprecated_uses));
86
+ const found = cls.deprecated_uses.map(function (x) { return x.deprecated; }).sort();
87
+ assert(found.indexOf('/mos:heal') >= 0, 'T1: /mos:heal detected', JSON.stringify(found));
88
+ assert(found.indexOf('/mos:query') >= 0, 'T1: /mos:query detected', JSON.stringify(found));
89
+ assert(cls.status === 'DEPRECATED_USAGE' || cls.status === 'warn', 'T2: status flags violation', cls.status);
90
+ }
91
+ // Cleanup
92
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
93
+ }
94
+
95
+ // --- T3 negative: no transcripts -> --deprecated-usage clean.
96
+ {
97
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-clean-'));
98
+ const r = cp.spawnSync('node', [DOCTOR_CJS, '--deprecated-usage', '--json'], {
99
+ encoding: 'utf8',
100
+ env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
101
+ timeout: 30000,
102
+ });
103
+ let envelope = null;
104
+ if (r.stdout) {
105
+ const start = r.stdout.indexOf('{');
106
+ if (start >= 0) { try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) {} }
107
+ }
108
+ const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
109
+ assert(cls && cls.status === 'OK', 'T3: status === OK with no deprecated usage', cls && cls.status);
110
+ assert(cls && cls.deprecated_uses && cls.deprecated_uses.length === 0, 'T3: deprecated_uses is empty array');
111
+ // Per spec: --deprecated-usage exits 0 when clean.
112
+ assert(r.status === 0, 'T3: exit 0 when clean', String(r.status));
113
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
114
+ }
115
+
116
+ // --- T4: --all activates --deprecated-usage in the cascade.
117
+ {
118
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'doctor-deprec-all-'));
119
+ const r = cp.spawnSync('node', [DOCTOR_CJS, '--all', '--json'], {
120
+ encoding: 'utf8',
121
+ env: Object.assign({}, process.env, { MINDRIAN_DOCTOR_TRANSCRIPTS_DIR: tmpRoot }),
122
+ timeout: 60000,
123
+ });
124
+ let envelope = null;
125
+ if (r.stdout) {
126
+ const start = r.stdout.indexOf('{');
127
+ if (start >= 0) { try { envelope = JSON.parse(r.stdout.slice(start)); } catch (_) {} }
128
+ }
129
+ const cls = envelope && envelope.checks && envelope.checks['deprecated-usage'];
130
+ assert(cls !== undefined && cls !== null, 'T4: --all includes deprecated-usage in cascade', JSON.stringify(envelope && envelope.checks ? Object.keys(envelope.checks) : null));
131
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_) {}
132
+ }
133
+
134
+ // --- T5: commands/onboard.md Step 6 has F.1 selector vocabulary.
135
+ {
136
+ const onboardPath = path.join(REPO_ROOT, 'commands', 'onboard.md');
137
+ const body = fs.readFileSync(onboardPath, 'utf8');
138
+ // Locate Step 6 region for verification (the test asserts the F.1 lives
139
+ // inside or in the vicinity of Step 6, not just anywhere in the file).
140
+ const step6Idx = body.indexOf('## Step 6');
141
+ assert(step6Idx > 0, 'T5: Step 6 section exists', String(step6Idx));
142
+ // The whole step-6 region (from Step 6 to end of file or next ##)
143
+ const rest = body.slice(step6Idx);
144
+ const nextStep = rest.indexOf('\n## ', 5);
145
+ const step6Region = nextStep > 0 ? rest.slice(0, nextStep) : rest;
146
+ assert(/F\.1/.test(step6Region), 'T5: Step 6 mentions F.1', step6Region.slice(0, 400));
147
+ assert(/Run Methodology/.test(step6Region), 'T5: Step 6 has "Run Methodology" verb');
148
+ assert(/Defer/.test(step6Region), 'T5: Step 6 has "Defer" verb');
149
+ assert(/Free-Text/.test(step6Region), 'T5: Step 6 has "Free-Text" verb');
150
+ assert(step6Region.indexOf('—') < 0, 'T5: Step 6 region has no em-dash');
151
+ }
152
+
153
+ // --- T6: commands/diagnose.md recommendation has F.1 selector vocabulary.
154
+ {
155
+ const diagPath = path.join(REPO_ROOT, 'commands', 'diagnose.md');
156
+ const body = fs.readFileSync(diagPath, 'utf8');
157
+ assert(/F\.1/.test(body), 'T6: diagnose.md mentions F.1');
158
+ assert(/Run Methodology/.test(body), 'T6: diagnose.md has "Run Methodology" verb');
159
+ assert(/Defer/.test(body), 'T6: diagnose.md has "Defer" verb');
160
+ assert(/Free-Text/.test(body), 'T6: diagnose.md has "Free-Text" verb');
161
+ assert(body.indexOf('—') < 0, 'T6: diagnose.md has no em-dash');
162
+ }
163
+
164
+ // --- T7: CHANGELOG.md entry for Phase 121.5-08 covers all 5 renames + /mos:mos + F.1.
165
+ {
166
+ const cl = fs.readFileSync(path.join(REPO_ROOT, 'CHANGELOG.md'), 'utf8');
167
+ assert(/Phase 121\.5-08/.test(cl), 'T7: CHANGELOG mentions Phase 121.5-08');
168
+ assert(/heal/i.test(cl), 'T7: CHANGELOG mentions heal rename');
169
+ assert(/query/i.test(cl), 'T7: CHANGELOG mentions query rename');
170
+ assert(/organize/i.test(cl), 'T7: CHANGELOG mentions organize rename');
171
+ assert(/hmi-status/i.test(cl), 'T7: CHANGELOG mentions hmi-status rename');
172
+ assert(/visualize/i.test(cl), 'T7: CHANGELOG mentions visualize rename');
173
+ assert(/\/mos:mos/.test(cl), 'T7: CHANGELOG mentions /mos:mos creation');
174
+ // F.1 selector wiring announcement
175
+ assert(/F\.1/.test(cl) || /F.1 selector/.test(cl), 'T7: CHANGELOG mentions F.1 selector wiring');
176
+ }
177
+
178
+ console.log('');
179
+ console.log('doctor-deprecation-surface.test.cjs: ' + pass + ' passed, ' + fail + ' failed');
180
+ if (fail > 0) {
181
+ console.log('FAILURES:');
182
+ for (const f of failures) console.log(' - ' + f);
183
+ process.exit(1);
184
+ }
185
+ process.exit(0);
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * lib/memory/first-touch-version.test.cjs
6
+ * ---------------------------------------
7
+ * Phase 121.5-05 Sub-plan F Task 1 acceptance tests.
8
+ *
9
+ * 8 tests covering the first-touch-version-stamper module:
10
+ *
11
+ * 1. stampVersion('banner', '1.13.0-beta.15') contains "MindrianOS v1.13.0-beta.15"
12
+ * 2. stampVersion('splash', '1.13.0') starts with "MindrianOS v1.13.0"
13
+ * 3. stampVersion('onboard', '1.14.0') embeds "MindrianOS v1.14.0"
14
+ * 4. stampVersion(<unknown_surface>, version) throws Error('unknown_surface: ...')
15
+ * 5. stampVersion called without version arg auto-resolves from plugin.json
16
+ * 6. data/first-touch-surfaces.json parses; surfaces[] length >= 6
17
+ * 7. hasEmDash(text) returns true on U+2014, false otherwise
18
+ * 8. scripts/banner output contains "MindrianOS v" + live plugin.json version
19
+ *
20
+ * Pure CJS, node built-ins only. No emoji. No em-dashes.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const os = require('os');
27
+ const path = require('path');
28
+ const { execSync } = require('child_process');
29
+
30
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
31
+ const stamperPath = path.join(REPO_ROOT, 'lib', 'core', 'first-touch-version-stamper.cjs');
32
+ const stamper = require(stamperPath);
33
+
34
+ let passed = 0;
35
+ let failed = 0;
36
+ const failures = [];
37
+
38
+ function assert(name, cond, detail) {
39
+ if (cond) {
40
+ passed++;
41
+ console.log(' PASS ' + name);
42
+ } else {
43
+ failed++;
44
+ failures.push({ name, detail });
45
+ console.log(' FAIL ' + name + (detail ? ' :: ' + detail : ''));
46
+ }
47
+ }
48
+
49
+ // --- Test 1: banner stamp contains MindrianOS v<version> ----------------
50
+ {
51
+ const out = stamper.stampVersion('banner', '1.13.0-beta.15');
52
+ assert(
53
+ 'Test 1: stampVersion(banner, "1.13.0-beta.15") contains "MindrianOS v1.13.0-beta.15"',
54
+ out.indexOf('MindrianOS v1.13.0-beta.15') >= 0,
55
+ 'got: ' + out,
56
+ );
57
+ }
58
+
59
+ // --- Test 2: splash stamp starts with MindrianOS v<version> -------------
60
+ {
61
+ const out = stamper.stampVersion('splash', '1.13.0');
62
+ assert(
63
+ 'Test 2: stampVersion(splash, "1.13.0") starts with "MindrianOS v1.13.0"',
64
+ out.indexOf('MindrianOS v1.13.0') === 0,
65
+ 'got: ' + out,
66
+ );
67
+ }
68
+
69
+ // --- Test 3: onboard stamp embeds MindrianOS v<version> -----------------
70
+ {
71
+ const out = stamper.stampVersion('onboard', '1.14.0');
72
+ assert(
73
+ 'Test 3: stampVersion(onboard, "1.14.0") embeds "MindrianOS v1.14.0"',
74
+ out.indexOf('MindrianOS v1.14.0') >= 0,
75
+ 'got: ' + out,
76
+ );
77
+ }
78
+
79
+ // --- Test 4: unknown surface throws -------------------------------------
80
+ {
81
+ let threw = false;
82
+ let msg = '';
83
+ try {
84
+ stamper.stampVersion('not-a-real-surface', '1.0.0');
85
+ } catch (e) {
86
+ threw = true;
87
+ msg = e.message;
88
+ }
89
+ assert(
90
+ 'Test 4: stampVersion(unknown_surface) throws Error(unknown_surface: ...)',
91
+ threw && /^unknown_surface:/.test(msg),
92
+ 'threw=' + threw + ' msg=' + msg,
93
+ );
94
+ }
95
+
96
+ // --- Test 5: no version arg -> auto-resolve from plugin.json ------------
97
+ {
98
+ const pluginJsonPath = path.join(REPO_ROOT, '.claude-plugin', 'plugin.json');
99
+ const liveVersion = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8')).version;
100
+ const out = stamper.stampVersion('banner');
101
+ assert(
102
+ 'Test 5: stampVersion(banner) auto-resolves version from plugin.json (' + liveVersion + ')',
103
+ out.indexOf(liveVersion) >= 0,
104
+ 'got: ' + out + ' live=' + liveVersion,
105
+ );
106
+ }
107
+
108
+ // --- Test 6: surfaces JSON parses + has >= 6 entries --------------------
109
+ {
110
+ const surfaces = stamper.readSurfaces();
111
+ assert(
112
+ 'Test 6: data/first-touch-surfaces.json parses; surfaces[].length >= 6',
113
+ Array.isArray(surfaces.surfaces) && surfaces.surfaces.length >= 6,
114
+ 'length=' + (surfaces.surfaces ? surfaces.surfaces.length : 'undefined'),
115
+ );
116
+
117
+ // Sanity: each surface has the required keys.
118
+ const requiredKeys = ['id', 'file', 'kind', 'version_stamp_required', 'em_dash_check'];
119
+ let allOk = true;
120
+ let missingDetail = '';
121
+ for (const s of surfaces.surfaces) {
122
+ for (const k of requiredKeys) {
123
+ if (!(k in s)) {
124
+ allOk = false;
125
+ missingDetail = 'surface ' + (s.id || '?') + ' missing key ' + k;
126
+ break;
127
+ }
128
+ }
129
+ if (!allOk) break;
130
+ }
131
+ assert(
132
+ 'Test 6b: every surface has id / file / kind / version_stamp_required / em_dash_check',
133
+ allOk,
134
+ missingDetail,
135
+ );
136
+ }
137
+
138
+ // --- Test 7: hasEmDash detects U+2014 -----------------------------------
139
+ {
140
+ // U+2014 EM DASH, written via escape so this source stays ASCII-clean.
141
+ const withDash = 'this has an \u2014 em dash';
142
+ const withoutDash = 'this has a - hyphen';
143
+ assert(
144
+ 'Test 7a: hasEmDash returns true on U+2014',
145
+ stamper.hasEmDash(withDash) === true,
146
+ );
147
+ assert(
148
+ 'Test 7b: hasEmDash returns false on plain hyphen',
149
+ stamper.hasEmDash(withoutDash) === false,
150
+ );
151
+ assert(
152
+ 'Test 7c: hasEmDash returns false on non-string',
153
+ stamper.hasEmDash(null) === false && stamper.hasEmDash(42) === false,
154
+ );
155
+ }
156
+
157
+ // --- Test 8: scripts/banner emits "MindrianOS v" + live version ---------
158
+ {
159
+ const bannerPath = path.join(REPO_ROOT, 'scripts', 'banner');
160
+ let bannerOut = '';
161
+ let bannerErr = null;
162
+ try {
163
+ bannerOut = execSync('bash ' + JSON.stringify(bannerPath), {
164
+ cwd: REPO_ROOT,
165
+ encoding: 'utf8',
166
+ env: Object.assign({}, process.env, { COLUMNS: '120' }),
167
+ });
168
+ } catch (e) {
169
+ bannerErr = e.message;
170
+ }
171
+ const liveVersion = stamper.resolveCurrentVersion();
172
+ assert(
173
+ 'Test 8a: scripts/banner runs without error',
174
+ bannerErr === null,
175
+ bannerErr || '',
176
+ );
177
+ assert(
178
+ 'Test 8b: banner output contains "MindrianOS v"',
179
+ bannerOut.indexOf('MindrianOS v') >= 0,
180
+ 'first 200 chars: ' + bannerOut.slice(0, 200),
181
+ );
182
+ assert(
183
+ 'Test 8c: banner output contains the live plugin.json version (' + liveVersion + ')',
184
+ bannerOut.indexOf(liveVersion) >= 0,
185
+ 'live=' + liveVersion,
186
+ );
187
+ }
188
+
189
+ // --- Summary ------------------------------------------------------------
190
+ console.log('');
191
+ console.log('first-touch-version.test.cjs: ' + passed + ' passed, ' + failed + ' failed');
192
+ if (failed > 0) {
193
+ for (const f of failures) {
194
+ console.log(' - ' + f.name + (f.detail ? ' :: ' + f.detail : ''));
195
+ }
196
+ process.exit(1);
197
+ }
198
+ process.exit(0);