@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,331 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
6
+ *
7
+ * Phase 121-00 -- unified writer acceptance tests.
8
+ *
9
+ * Verifies lib/core/telemetry/writer.cjs is THE single chokepoint (Canon
10
+ * Part 9 navigation.cjs precedent) for trajectory-telemetry emits. Every
11
+ * downstream capture point in 121-02 and 121-03 routes through emit() here.
12
+ *
13
+ * Test map (10 cases, one-to-one with the PLAN <behavior> block for Task 2):
14
+ * 1. Writer exports surface: emit, telemetryDir, telemetryFile,
15
+ * isoWeekFilename, EVENT_TYPES, ALLOWED_FIELDS.
16
+ * 2. telemetryDir() resolves under $HOME/.mindrian/telemetry/v1.13.
17
+ * 3. isoWeekFilename() zero-pads week and gates 2026-01-01 -> W01,
18
+ * 2026-01-05 -> W02, 2026-05-19 -> W21.
19
+ * 4. telemetryFile(date) composes telemetryDir() + isoWeekFilename(date).
20
+ * 5. emit() writes JSONL with schema_version: 1 (Number), valid
21
+ * ISO-8601 timestamp, session_id from env or 'default', all payload
22
+ * keys present with correct values.
23
+ * 6. emit() throws Error.code='TELEMETRY_VALIDATION' on unknown event.
24
+ * 7. emit() throws on Canon Part 8 forbidden pattern (Cypher in
25
+ * allowed field).
26
+ * 8. emit() creates the v1.13 dir if missing (recursive mkdir); does
27
+ * NOT throw if fs.appendFileSync fails (best-effort -- pipeline
28
+ * must not crash on telemetry disk failure).
29
+ * 9. Two emit() calls 5ms apart land in the SAME ISO-week file.
30
+ * 10. Every line is JSON.parseable (no BOM, single newline terminator).
31
+ *
32
+ * Hermetic: each test creates a tmpdir, points HOME at it, writes, asserts,
33
+ * and tears down via fs.rmSync in finally. Mirrors
34
+ * lib/memory/query-efficiency-telemetry.test.cjs.
35
+ *
36
+ * Registered in lib/memory/run-feynman-tests.cjs.
37
+ */
38
+
39
+ const assert = require('node:assert/strict');
40
+ const fs = require('node:fs');
41
+ const path = require('node:path');
42
+ const os = require('node:os');
43
+
44
+ const REPO = path.resolve(__dirname, '..', '..', '..');
45
+ const WRITER_PATH = path.join(REPO, 'lib/core/telemetry/writer.cjs');
46
+
47
+ try { delete require.cache[require.resolve(WRITER_PATH)]; } catch (_) {}
48
+ const writer = require(WRITER_PATH);
49
+
50
+ // ---------- Fixture scaffolding ----------
51
+
52
+ function mkTmpDir(prefix) {
53
+ const base = path.join(os.tmpdir(), 'mos-121-00-' + prefix + '-' + process.pid + '-' + Date.now().toString(36));
54
+ fs.mkdirSync(base, { recursive: true });
55
+ return base;
56
+ }
57
+
58
+ function rmRf(p) {
59
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {}
60
+ }
61
+
62
+ function withHome(tmp, fn) {
63
+ const originalHome = process.env.HOME;
64
+ process.env.HOME = tmp;
65
+ try { return fn(); } finally { process.env.HOME = originalHome; }
66
+ }
67
+
68
+ function readJsonl(filePath) {
69
+ const text = fs.readFileSync(filePath, 'utf8');
70
+ // No BOM allowed, single newline per line.
71
+ assert.ok(text.charCodeAt(0) !== 0xFEFF, 'JSONL must not start with BOM');
72
+ const lines = text.split('\n').filter(l => l.length > 0);
73
+ return lines.map(l => JSON.parse(l));
74
+ }
75
+
76
+ // ---------- Test 1: writer export surface ----------
77
+
78
+ (function test1Exports() {
79
+ assert.equal(typeof writer.emit, 'function', 'writer.emit must be a function');
80
+ assert.equal(typeof writer.telemetryDir, 'function', 'writer.telemetryDir must be a function');
81
+ assert.equal(typeof writer.telemetryFile, 'function', 'writer.telemetryFile must be a function');
82
+ assert.equal(typeof writer.isoWeekFilename, 'function', 'writer.isoWeekFilename must be a function');
83
+ assert.ok(Array.isArray(writer.EVENT_TYPES), 'writer must re-export EVENT_TYPES');
84
+ assert.equal(typeof writer.ALLOWED_FIELDS, 'object', 'writer must re-export ALLOWED_FIELDS');
85
+ console.log('PASS test 1: writer exports emit/telemetryDir/telemetryFile/isoWeekFilename/EVENT_TYPES/ALLOWED_FIELDS');
86
+ })();
87
+
88
+ // ---------- Test 2: telemetryDir resolves under HOME ----------
89
+
90
+ (function test2TelemetryDir() {
91
+ const tmp = mkTmpDir('dir');
92
+ try {
93
+ withHome(tmp, () => {
94
+ const dir = writer.telemetryDir();
95
+ const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13');
96
+ assert.equal(dir, expected,
97
+ 'telemetryDir() must resolve to <HOME>/.mindrian/telemetry/v1.13; got ' + dir);
98
+ });
99
+ console.log('PASS test 2: telemetryDir() resolves under HOME');
100
+ } finally {
101
+ rmRf(tmp);
102
+ }
103
+ })();
104
+
105
+ // ---------- Test 3: isoWeekFilename zero-pads + handles year boundaries ----------
106
+
107
+ (function test3IsoWeek() {
108
+ // 2026-05-19 is a Tuesday in ISO week 21 of 2026.
109
+ const f1 = writer.isoWeekFilename(new Date('2026-05-19T12:00:00Z'));
110
+ assert.equal(f1, 'events-2026-W21.jsonl',
111
+ 'isoWeekFilename(2026-05-19) must be events-2026-W21.jsonl; got ' + f1);
112
+
113
+ // 2026-01-05 is a Monday in ISO week 02 of 2026 (W01 is the week of
114
+ // 2025-12-29..2026-01-04 because the first Thursday of 2026 is 2026-01-01).
115
+ const f2 = writer.isoWeekFilename(new Date('2026-01-05T12:00:00Z'));
116
+ assert.equal(f2, 'events-2026-W02.jsonl',
117
+ 'isoWeekFilename(2026-01-05) must be events-2026-W02.jsonl (zero-padded); got ' + f2);
118
+
119
+ // 2026-01-01 is the Thursday that anchors ISO week 01 of 2026.
120
+ const f3 = writer.isoWeekFilename(new Date('2026-01-01T12:00:00Z'));
121
+ assert.equal(f3, 'events-2026-W01.jsonl',
122
+ 'isoWeekFilename(2026-01-01) must be events-2026-W01.jsonl; got ' + f3);
123
+
124
+ console.log('PASS test 3: isoWeekFilename zero-pads week (W21, W02, W01)');
125
+ })();
126
+
127
+ // ---------- Test 4: telemetryFile composes ----------
128
+
129
+ (function test4TelemetryFile() {
130
+ const tmp = mkTmpDir('file');
131
+ try {
132
+ withHome(tmp, () => {
133
+ const f = writer.telemetryFile(new Date('2026-05-19T00:00:00Z'));
134
+ const expected = path.join(tmp, '.mindrian', 'telemetry', 'v1.13', 'events-2026-W21.jsonl');
135
+ assert.equal(f, expected,
136
+ 'telemetryFile() must compose dir + isoWeekFilename; got ' + f);
137
+ });
138
+ console.log('PASS test 4: telemetryFile() composes dir + filename');
139
+ } finally {
140
+ rmRf(tmp);
141
+ }
142
+ })();
143
+
144
+ // ---------- Test 5: emit() writes JSONL with schema_version + iso timestamp + session_id + payload ----------
145
+
146
+ (function test5EmitWritesJsonl() {
147
+ const tmp = mkTmpDir('emit');
148
+ const originalSession = process.env.CLAUDE_SESSION_ID;
149
+ process.env.CLAUDE_SESSION_ID = 'test-session-abc';
150
+ try {
151
+ withHome(tmp, () => {
152
+ writer.emit('selector_pick', {
153
+ sub_shape: 'F.1',
154
+ mode: 'A',
155
+ ranker_confidence: 0.73,
156
+ recommended_rendered: true,
157
+ options_count: 4,
158
+ room_slug_sha256: 'a'.repeat(64),
159
+ verb_chosen: 'Run Methodology',
160
+ });
161
+
162
+ const filePath = writer.telemetryFile();
163
+ assert.ok(fs.existsSync(filePath), 'emit() must create the JSONL file at ' + filePath);
164
+
165
+ const rows = readJsonl(filePath);
166
+ assert.equal(rows.length, 1, 'emit() must write exactly 1 line; got ' + rows.length);
167
+
168
+ const r = rows[0];
169
+ assert.equal(r.event, 'selector_pick', 'event field must be selector_pick');
170
+ assert.equal(r.schema_version, 1, 'schema_version must be Number 1; got ' + r.schema_version + ' (' + typeof r.schema_version + ')');
171
+ assert.equal(typeof r.schema_version, 'number', 'schema_version must be typeof number');
172
+ assert.match(r.timestamp, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, 'timestamp must be ISO-8601');
173
+ assert.equal(typeof r.session_id, 'string', 'session_id must be a string');
174
+ assert.ok(r.session_id.length > 0, 'session_id must be non-empty');
175
+ assert.equal(r.session_id, 'test-session-abc', 'session_id must reflect CLAUDE_SESSION_ID env');
176
+
177
+ // All 7 payload keys present with correct values.
178
+ assert.equal(r.sub_shape, 'F.1');
179
+ assert.equal(r.mode, 'A');
180
+ assert.equal(r.ranker_confidence, 0.73);
181
+ assert.equal(r.recommended_rendered, true);
182
+ assert.equal(r.options_count, 4);
183
+ assert.equal(r.room_slug_sha256, 'a'.repeat(64));
184
+ assert.equal(r.verb_chosen, 'Run Methodology');
185
+ });
186
+ console.log('PASS test 5: emit() writes JSONL with schema_version=1, ISO timestamp, session_id, all 7 payload keys');
187
+ } finally {
188
+ if (originalSession === undefined) { delete process.env.CLAUDE_SESSION_ID; }
189
+ else { process.env.CLAUDE_SESSION_ID = originalSession; }
190
+ rmRf(tmp);
191
+ }
192
+ })();
193
+
194
+ // ---------- Test 6: emit() throws on unknown event ----------
195
+
196
+ (function test6UnknownEventThrows() {
197
+ const tmp = mkTmpDir('unknown');
198
+ try {
199
+ withHome(tmp, () => {
200
+ assert.throws(() => writer.emit('definitely_not_a_real_event', {}),
201
+ (err) => err.code === 'TELEMETRY_VALIDATION',
202
+ 'emit() on unknown event must throw with code TELEMETRY_VALIDATION');
203
+ });
204
+ console.log('PASS test 6: emit() throws TELEMETRY_VALIDATION on unknown event');
205
+ } finally {
206
+ rmRf(tmp);
207
+ }
208
+ })();
209
+
210
+ // ---------- Test 7: emit() throws on Canon Part 8 forbidden pattern ----------
211
+
212
+ (function test7ForbiddenPatternThrows() {
213
+ const tmp = mkTmpDir('forbidden');
214
+ try {
215
+ withHome(tmp, () => {
216
+ assert.throws(() => writer.emit('selector_pick', {
217
+ sub_shape: 'F.1',
218
+ verb_chosen: 'MATCH (n:Framework) RETURN n', // Cypher injection
219
+ }), (err) => err.code === 'TELEMETRY_VALIDATION',
220
+ 'emit() must throw on Cypher fragment in allowed field');
221
+ });
222
+ console.log('PASS test 7: emit() throws on Canon Part 8 forbidden pattern (Cypher)');
223
+ } finally {
224
+ rmRf(tmp);
225
+ }
226
+ })();
227
+
228
+ // ---------- Test 8: emit() creates dir + swallows fs errors silently ----------
229
+
230
+ (function test8DirCreationAndFsErrorSilent() {
231
+ // Step A: emit into a HOME that does NOT yet contain .mindrian/telemetry/v1.13.
232
+ const tmp = mkTmpDir('create');
233
+ try {
234
+ withHome(tmp, () => {
235
+ // Verify the dir does not exist yet.
236
+ const dir = writer.telemetryDir();
237
+ assert.equal(fs.existsSync(dir), false, 'dir must NOT exist pre-emit');
238
+ writer.emit('selector_pick', {
239
+ sub_shape: 'F.1',
240
+ mode: 'A',
241
+ options_count: 1,
242
+ });
243
+ assert.equal(fs.existsSync(dir), true, 'emit() must create the v1.13 dir recursively');
244
+ const filePath = writer.telemetryFile();
245
+ assert.equal(fs.existsSync(filePath), true, 'emit() must create the JSONL file');
246
+ });
247
+
248
+ // Step B: emit when fs.appendFileSync is monkeypatched to throw. Pipeline
249
+ // must not crash. Done in a fresh subprocess so we can monkeypatch without
250
+ // breaking the rest of the suite.
251
+ const tmp2 = mkTmpDir('fserr');
252
+ withHome(tmp2, () => {
253
+ const realAppend = fs.appendFileSync;
254
+ fs.appendFileSync = function () { throw new Error('disk full simulated'); };
255
+ try {
256
+ writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
257
+ // If we reach here, the swallow worked.
258
+ } finally {
259
+ fs.appendFileSync = realAppend;
260
+ }
261
+ });
262
+ rmRf(tmp2);
263
+
264
+ console.log('PASS test 8: emit() creates dir recursively + swallows fs errors silently');
265
+ } finally {
266
+ rmRf(tmp);
267
+ }
268
+ })();
269
+
270
+ // ---------- Test 9: two emits 5ms apart land in the same ISO-week file ----------
271
+
272
+ (function test9SameFileAcrossShortInterval() {
273
+ const tmp = mkTmpDir('sameweek');
274
+ try {
275
+ withHome(tmp, () => {
276
+ writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
277
+ const sleep5 = Date.now() + 5;
278
+ while (Date.now() < sleep5) {} // busy-wait
279
+ writer.emit('selector_pick', { sub_shape: 'F.2', mode: 'B' });
280
+
281
+ const filePath = writer.telemetryFile();
282
+ const rows = readJsonl(filePath);
283
+ assert.equal(rows.length, 2,
284
+ 'two emits 5ms apart must land in same file -> 2 lines; got ' + rows.length);
285
+ assert.equal(rows[0].sub_shape, 'F.1');
286
+ assert.equal(rows[1].sub_shape, 'F.2');
287
+ });
288
+ console.log('PASS test 9: two emits 5ms apart in same ISO-week file');
289
+ } finally {
290
+ rmRf(tmp);
291
+ }
292
+ })();
293
+
294
+ // ---------- Test 10: lines are JSON.parseable, no BOM, no trailing whitespace ----------
295
+
296
+ (function test10ParseableLines() {
297
+ const tmp = mkTmpDir('parse');
298
+ try {
299
+ withHome(tmp, () => {
300
+ writer.emit('selector_pick', { sub_shape: 'F.1', mode: 'A' });
301
+ writer.emit('command_invocation', { command: '/mos:status', outcome: 'success', duration_ms: 42 });
302
+ writer.emit('nav_bypass', { op: 'read', reason: 'legacy_path' });
303
+
304
+ const filePath = writer.telemetryFile();
305
+ const text = fs.readFileSync(filePath, 'utf8');
306
+
307
+ // BOM check.
308
+ assert.ok(text.charCodeAt(0) !== 0xFEFF, 'no BOM at start');
309
+
310
+ // Each line must JSON.parse.
311
+ const lines = text.split('\n');
312
+ // Last element after split('\n') is '' if file ends with \n (which it should).
313
+ assert.equal(lines[lines.length - 1], '', 'file must end with newline (last split chunk is empty string)');
314
+
315
+ const dataLines = lines.slice(0, -1);
316
+ assert.equal(dataLines.length, 3, 'must have 3 data lines; got ' + dataLines.length);
317
+ for (const line of dataLines) {
318
+ assert.equal(line[line.length - 1] !== ' ' && line[line.length - 1] !== '\r', true,
319
+ 'no trailing whitespace on line: ' + JSON.stringify(line));
320
+ const obj = JSON.parse(line); // throws if malformed
321
+ assert.equal(typeof obj.event, 'string');
322
+ assert.equal(obj.schema_version, 1);
323
+ }
324
+ });
325
+ console.log('PASS test 10: every line is JSON.parseable, no BOM, single newline terminator');
326
+ } finally {
327
+ rmRf(tmp);
328
+ }
329
+ })();
330
+
331
+ console.log('\nwriter.test.cjs: 10/10 tests passed');
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
5
+ *
6
+ * Phase 121.5-07 Task 1 -- Terminal capability probe.
7
+ *
8
+ * Decision D-05 (LOCKED):
9
+ * probeCapability(opts) returns one of:
10
+ * 'truecolor' -- process.stdout.isTTY === true AND ($COLORTERM matches
11
+ * truecolor/24bit OR equals literal "truecolor")
12
+ * '256color' -- isTTY === true AND ($COLORTERM contains "256color" OR
13
+ * $TERM contains "256color")
14
+ * 'ascii' -- otherwise (non-TTY, no COLORTERM, Desktop chat surface)
15
+ *
16
+ * Desktop hard-override: env.CLAUDE_DESKTOP=1 or env.MINDRIAN_DESKTOP=1 forces
17
+ * 'ascii' regardless of TTY state. This is the Desktop chat surface, which
18
+ * strips ANSI escapes.
19
+ *
20
+ * Canon references:
21
+ * Part 3 UI Ruling System -- bulletproof terminal coherence depends on a
22
+ * single capability probe shared across surfaces.
23
+ * Part 7 Reuse Before Build -- this is the REUSABLE substrate for
24
+ * /mos:status, /mos:doctor, /mos:splash, /mos:help renderer.
25
+ * Part 8 Graph Boundary -- reads env-vars + isTTY only; zero network,
26
+ * zero Brain, zero telemetry egress.
27
+ */
28
+
29
+ /**
30
+ * Probe the current terminal capability.
31
+ *
32
+ * @param {object} [opts]
33
+ * @param {object} [opts.env] -- env override (defaults to process.env)
34
+ * @param {boolean} [opts.isTTY] -- TTY override (defaults to process.stdout.isTTY)
35
+ * @returns {'truecolor'|'256color'|'ascii'}
36
+ */
37
+ function probeCapability(opts) {
38
+ const o = opts || {};
39
+ const env = o.env || process.env;
40
+ const isTTY =
41
+ o.isTTY !== undefined
42
+ ? o.isTTY
43
+ : Boolean(process.stdout && process.stdout.isTTY);
44
+
45
+ // Desktop simulator hard-override.
46
+ if (env.CLAUDE_DESKTOP === '1' || env.MINDRIAN_DESKTOP === '1') {
47
+ return 'ascii';
48
+ }
49
+
50
+ // Test/CI hard-override for harnesses that pipe stdout (isTTY false).
51
+ // Used by the Phase 121.5-09 coherence smoke test harness to verify the
52
+ // truecolor branch fires on the CLI surface. Production code paths never
53
+ // set this; isTTY-based detection remains the default contract.
54
+ if (env.MINDRIAN_FORCE_TRUECOLOR === '1') return 'truecolor';
55
+ if (env.MINDRIAN_FORCE_256COLOR === '1') return '256color';
56
+
57
+ if (!isTTY) return 'ascii';
58
+
59
+ const colorterm = String(env.COLORTERM || '').toLowerCase();
60
+ const term = String(env.TERM || '').toLowerCase();
61
+
62
+ if (colorterm === 'truecolor' || /truecolor/.test(colorterm) || /24bit/.test(colorterm)) {
63
+ return 'truecolor';
64
+ }
65
+ if (colorterm.includes('256color') || term.includes('256color')) {
66
+ return '256color';
67
+ }
68
+
69
+ return 'ascii';
70
+ }
71
+
72
+ /**
73
+ * @param {'truecolor'|'256color'|'ascii'} capability
74
+ * @returns {boolean}
75
+ */
76
+ function supportsColor(capability) {
77
+ return capability === 'truecolor' || capability === '256color';
78
+ }
79
+
80
+ /**
81
+ * @param {'truecolor'|'256color'|'ascii'} capability
82
+ * @returns {boolean}
83
+ */
84
+ function supportsTruecolor(capability) {
85
+ return capability === 'truecolor';
86
+ }
87
+
88
+ module.exports = { probeCapability, supportsColor, supportsTruecolor };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Phase 127-02 BRAIN-MCP-127-09 -- Tier-0 graceful messaging chokepoint.
5
+ *
6
+ * Single source-of-truth for the DIRECTOR_NOT_AVAILABLE sentinel shape used
7
+ * across the plugin:
8
+ * - bin/mindrian-brain-mcp-client.cjs (the stdio shim from Phase 127-00)
9
+ * - Larry's prose surface (one-line hint via larryTier0Hint)
10
+ * - Future statusline + /mos:status surfaces (CONTEXT acceptance gate #4)
11
+ *
12
+ * Before this chokepoint, the shim shipped its own inline copy of the
13
+ * sentinel shape. Future surfaces (statusline, /mos:status, persona output)
14
+ * would each duplicate the same shape, drifting on the upgrade_hint URL or
15
+ * the fallback_advice phrasing. This module locks the wire shape and the
16
+ * Larry-prose phrasing so every consumer reads the same canonical bytes.
17
+ *
18
+ * Wire shape locked here (BRAIN-MCP-127-09 invariant):
19
+ * {
20
+ * status: "DIRECTOR_NOT_AVAILABLE",
21
+ * reason: "MINDRIAN_BRAIN_KEY not set",
22
+ * command_context: <toolName string | "unknown" for non-string input>,
23
+ * upgrade_hint: "Request a Brain key at https://mindrianos.vercel.app/brain-access",
24
+ * fallback_advice: "Larry can still talk with you and reflect on your room context. Methodology orchestration requires Brain."
25
+ * }
26
+ *
27
+ * Canon Part 7 (reuse): isAvailable() is a one-line delegation to
28
+ * brain-client.cjs's existing isAvailable(); no parallel key-resolver code
29
+ * path lives here. The shim's local tier0Response becomes a one-line
30
+ * passthrough after the Phase 127-02 refactor; no duplicate shape exists.
31
+ *
32
+ * Canon Part 8 (graph boundary): zero network surface in this file.
33
+ * isAvailable() delegates to brain-client.cjs (the existing chokepoint that
34
+ * reads ONLY the LOCAL key via resolve-brain-key.cjs). No fetch, no http,
35
+ * no Brain endpoint domain strings.
36
+ *
37
+ * HARD RULE: no em-dashes anywhere in this file (hyphens only).
38
+ */
39
+
40
+ const brainClient = require('./brain-client.cjs');
41
+
42
+ // Locked wire string. Renaming this constant breaks every downstream consumer
43
+ // (the shim, Larry's prose surface, the doctor's Class-M smoke L5 check).
44
+ // Treat as a phase-amendment boundary.
45
+ const DIRECTOR_NOT_AVAILABLE = 'DIRECTOR_NOT_AVAILABLE';
46
+
47
+ // Locked sentinel strings. Tests assert the keys; the values are
48
+ // human-facing and may evolve, but only via explicit phase amendment.
49
+ const REASON_NO_KEY = 'MINDRIAN_BRAIN_KEY not set';
50
+ const UPGRADE_HINT = 'Request a Brain key at https://mindrianos.vercel.app/brain-access';
51
+ const FALLBACK_ADVICE = 'Larry can still talk with you and reflect on your room context. Methodology orchestration requires Brain.';
52
+
53
+ /**
54
+ * Construct the Tier-0 sentinel response. Returned by every Brain-tool entry
55
+ * point when no key is resolvable. The shape is byte-locked.
56
+ *
57
+ * Defensive: non-string / empty / non-truthy commandContext arguments coerce
58
+ * to "unknown" so the wire shape is invariant under bad-caller inputs (the
59
+ * shim's tool-handler closure passes the literal tool name; future callers
60
+ * may pass null in error paths).
61
+ *
62
+ * @param {string} commandContext the tool name (e.g. "brain_ask"); falls back
63
+ * to "unknown" for non-string / empty inputs.
64
+ * @returns {{status: string, reason: string, command_context: string,
65
+ * upgrade_hint: string, fallback_advice: string}}
66
+ */
67
+ function tier0Response(commandContext) {
68
+ const ctx = (typeof commandContext === 'string' && commandContext.length > 0)
69
+ ? commandContext
70
+ : 'unknown';
71
+ return {
72
+ status: DIRECTOR_NOT_AVAILABLE,
73
+ reason: REASON_NO_KEY,
74
+ command_context: ctx,
75
+ upgrade_hint: UPGRADE_HINT,
76
+ fallback_advice: FALLBACK_ADVICE,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Is the Brain reachable from this process right now? Delegates to
82
+ * brain-client.cjs's existing chokepoint (which reads only the LOCAL key via
83
+ * resolve-brain-key.cjs). One-line passthrough -- never duplicate the key
84
+ * resolution logic.
85
+ *
86
+ * @returns {boolean}
87
+ */
88
+ function isAvailable() {
89
+ return brainClient.isAvailable();
90
+ }
91
+
92
+ /**
93
+ * One-line Larry-prose hint for the Tier-0 path. Used by Larry's surface
94
+ * (and future statusline / /mos:status) when isAvailable() returns false to
95
+ * tell the user how to unlock Brain. Locked under 120 chars so it fits in
96
+ * statusline + chat-prefix surfaces without truncation.
97
+ *
98
+ * @returns {string}
99
+ */
100
+ function larryTier0Hint() {
101
+ return 'Methodology orchestration needs a Brain key. Drop one in ~/.mindrian.env or set MINDRIAN_BRAIN_KEY.';
102
+ }
103
+
104
+ module.exports = {
105
+ DIRECTOR_NOT_AVAILABLE,
106
+ tier0Response,
107
+ isAvailable,
108
+ larryTier0Hint,
109
+ };