@mindrian_os/install 1.13.0-beta.17 → 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 (182) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +26 -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 +2 -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 +2 -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 +2 -0
  51. package/commands/mva-option.md +2 -0
  52. package/commands/new-project.md +2 -0
  53. package/commands/onboard.md +20 -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 +22 -88
  94. package/lib/agents/auto-explore-agent.cjs +82 -0
  95. package/lib/core/breakthrough/canary.cjs +134 -0
  96. package/lib/core/breakthrough/canary.test.cjs +136 -0
  97. package/lib/core/breakthrough/detectors.cjs +359 -0
  98. package/lib/core/breakthrough/detectors.test.cjs +333 -0
  99. package/lib/core/breakthrough/ethics-fence.cjs +127 -0
  100. package/lib/core/breakthrough/ethics-fence.test.cjs +178 -0
  101. package/lib/core/breakthrough/resurfacing.cjs +150 -0
  102. package/lib/core/breakthrough/resurfacing.test.cjs +233 -0
  103. package/lib/core/breakthrough/review-queue.cjs +154 -0
  104. package/lib/core/breakthrough/review-queue.test.cjs +160 -0
  105. package/lib/core/breakthrough/scanner-d17-d18.test.cjs +229 -0
  106. package/lib/core/breakthrough/scanner.cjs +426 -0
  107. package/lib/core/breakthrough/scanner.test.cjs +267 -0
  108. package/lib/core/breakthrough/schema.cjs +164 -0
  109. package/lib/core/breakthrough/schema.test.cjs +256 -0
  110. package/lib/core/breakthrough/scoring.cjs +293 -0
  111. package/lib/core/breakthrough/scoring.test.cjs +423 -0
  112. package/lib/core/breakthrough/verb-dispatch.cjs +221 -0
  113. package/lib/core/breakthrough/verb-dispatch.test.cjs +185 -0
  114. package/lib/core/breakthrough/voice-scaffold.cjs +247 -0
  115. package/lib/core/breakthrough/voice-scaffold.test.cjs +251 -0
  116. package/lib/core/first-touch-version-stamper.cjs +113 -0
  117. package/lib/core/larry-thinness-acknowledgment.cjs +64 -0
  118. package/lib/core/larry-thinness-acknowledgment.test.cjs +97 -0
  119. package/lib/core/llm-name-suggester.cjs +194 -0
  120. package/lib/core/llm-name-suggester.test.cjs +132 -0
  121. package/lib/core/mva-orchestrator.cjs +41 -0
  122. package/lib/core/mva-telemetry.cjs +31 -143
  123. package/lib/core/navigation/edges.cjs +35 -0
  124. package/lib/core/navigation/memory-events.cjs +126 -0
  125. package/lib/core/room-auto-create.cjs +318 -0
  126. package/lib/core/room-auto-create.test.cjs +198 -0
  127. package/lib/core/room-discard-cascade.cjs +225 -0
  128. package/lib/core/room-discard-cascade.test.cjs +135 -0
  129. package/lib/core/room-name-validator.cjs +132 -0
  130. package/lib/core/room-name-validator.test.cjs +156 -0
  131. package/lib/core/room-naming-selector.cjs +357 -0
  132. package/lib/core/room-naming-selector.test.cjs +277 -0
  133. package/lib/core/room-receipt-emit.cjs +63 -0
  134. package/lib/core/room-skeleton-scaffold.cjs +315 -0
  135. package/lib/core/room-skeleton-scaffold.test.cjs +291 -0
  136. package/lib/core/stale-copy-scanner.cjs +190 -0
  137. package/lib/core/state-aware-router.cjs +78 -0
  138. package/lib/core/telemetry/schema.cjs +168 -0
  139. package/lib/core/telemetry/schema.test.cjs +124 -0
  140. package/lib/core/telemetry/validator.cjs +197 -0
  141. package/lib/core/telemetry/validator.test.cjs +188 -0
  142. package/lib/core/telemetry/writer.cjs +141 -0
  143. package/lib/core/telemetry/writer.test.cjs +331 -0
  144. package/lib/core/terminal-capability.cjs +88 -0
  145. package/lib/core/venture-shape-nudge.cjs +163 -0
  146. package/lib/core/venture-shape-nudge.test.cjs +161 -0
  147. package/lib/core/visual-ops.cjs +70 -2
  148. package/lib/hmi/selector-dispatcher.cjs +90 -1
  149. package/lib/hmi/shape-f7-breakthrough-renderer.cjs +222 -0
  150. package/lib/hmi/shape-f7-breakthrough-renderer.test.cjs +233 -0
  151. package/lib/memory/body-shape-coverage.test.cjs +268 -0
  152. package/lib/memory/doctor-deprecation-surface.test.cjs +185 -0
  153. package/lib/memory/first-touch-version.test.cjs +198 -0
  154. package/lib/memory/help-coverage.test.cjs +108 -0
  155. package/lib/memory/help-renderer.test.cjs +145 -0
  156. package/lib/memory/palette-consistency.test.cjs +127 -0
  157. package/lib/memory/pending-tension-store.cjs +80 -0
  158. package/lib/memory/render-v2-disposition.test.cjs +199 -0
  159. package/lib/memory/run-feynman-tests.cjs +213 -0
  160. package/lib/memory/sessionstart-coordinator.test.cjs +446 -0
  161. package/lib/memory/skill-vs-code-drift.test.cjs +257 -0
  162. package/lib/memory/soft-alias.test.cjs +144 -0
  163. package/lib/memory/stale-copy-scanner.test.cjs +291 -0
  164. package/lib/memory/state-aware-router.test.cjs +90 -0
  165. package/lib/memory/statusline-two-row.test.cjs +338 -0
  166. package/lib/memory/terminal-capability.test.cjs +155 -0
  167. package/lib/render/ROOM.md +74 -22
  168. package/lib/sessionstart/budget-compressor.cjs +130 -0
  169. package/lib/sessionstart/contributor-interface.cjs +134 -0
  170. package/lib/sessionstart/contributor-isolator.cjs +128 -0
  171. package/lib/sessionstart/precedence-ladder.cjs +47 -0
  172. package/lib/statusline/governing-thought-truncator.cjs +45 -0
  173. package/lib/statusline/two-row-renderer.cjs +186 -0
  174. package/lib/statusline/version-resolver.cjs +81 -0
  175. package/package.json +1 -1
  176. package/references/visual/ROOM.md +55 -0
  177. package/references/visual/palette.json +54 -0
  178. package/skills/larry-personality/SKILL.md +34 -0
  179. package/skills/ui-system/SKILL.md +109 -1
  180. package/skills/ui-system/rules/dual-palette.md +156 -0
  181. package/skills/ui-system/rules/glyph-disambiguation.md +171 -0
  182. package/skills/ui-system/rules/shape-f-zero-and-six.md +169 -0
@@ -0,0 +1,194 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-01 -- LLM-suggested room name resolver.
4
+ *
5
+ * One-shot Haiku 4.5 call seeded with LOCAL Phase 117 auto-explore finding +
6
+ * Phase 118 MVA brief sentence. Returns a venture-shaped slug suggestion (e.g.
7
+ * 'acme-robotics', 'quantum-imaging', 'biotech-translation').
8
+ *
9
+ * Canon Part 8 invariant: this module MUST NOT invoke any Brain endpoint.
10
+ * The Brain repository holds GENERIC methodology -- framework chaining rules,
11
+ * phase progressions -- never user-specific content. A LLM call seeded with
12
+ * the user's auto-explore output + MVA brief sentence is purely LOCAL: the
13
+ * model sees the user's content directly via the local Anthropic API path,
14
+ * and the model's response is consumed locally.
15
+ *
16
+ * Canon Part 8 NOTE (REVISION 2026-05-16 Warning 5 fix): this module's fetch
17
+ * carries user content (the auto_explore_finding summary + the mva_brief_sentence)
18
+ * to api.anthropic.com. Per the standard plugin LLM usage pattern (precedent:
19
+ * lib/core/mva-classifier.cjs, lib/agents/mva/*.cjs, lib/chat/fabric-chat.cjs),
20
+ * this is acceptable: the Anthropic API is the LOCAL LLM transport for the
21
+ * plugin. The Canon Part 8 boundary covers ONLY the Mindrian-owned Brain MCP
22
+ * host (the Mindrian-owned methodology repository that must never receive user
23
+ * data) -- NOT api.anthropic.com (the Anthropic LLM transport). The two are
24
+ * distinct: Brain is a Mindrian-owned methodology repository that must never
25
+ * receive user data; api.anthropic.com is a stateless LLM transport.
26
+ *
27
+ * Tripwire: scaffold harness Gate 3 + Test 9 grep this module for any Brain-host
28
+ * substring AND any brain-client require AND any fetch to a brain.* URL; all
29
+ * three must return 0. This module body therefore avoids the literal Brain-host
30
+ * hostname string entirely (the scaffold harness uses literal-grep on the
31
+ * forbidden substring).
32
+ *
33
+ * Cost: ~$0.0005 per first-MVA completion (Haiku 4.5 input ~800 tokens,
34
+ * output ~10 tokens). See CONTEXT.md Architectural Decisions item 1.
35
+ *
36
+ * Em-dash discipline: uses `--` never the U+2014 character per memory
37
+ * feedback_no_emdashes.md.
38
+ *
39
+ * Graceful degradation: on LLM error, returns {ok: false, suggested_name:
40
+ * 'untitled', ...} so the F.1 selector still renders correctly with the
41
+ * fallback label `[name this room: untitled]`.
42
+ */
43
+
44
+ const FALLBACK_SUGGESTION = 'untitled';
45
+
46
+ // Phase 119-01 REVISION 2026-05-16 (Blocker 2 Option A): HAIKU_MODEL_ID is the
47
+ // project-wide source-of-truth constant. The plan's REVISION text says to
48
+ // import HAIKU_MODEL from lib/core/mva-classifier.cjs::HAIKU_MODEL, BUT
49
+ // inspection of that module's module.exports (verified at lib/core/mva-classifier.cjs
50
+ // line 359-370) shows HAIKU_MODEL is a module-internal const NOT exported. Per
51
+ // Rule 1 deviation, the constant is inlined here with provenance pointing to
52
+ // the source-of-truth declaration at lib/core/mva-classifier.cjs:53. If a
53
+ // future phase exports HAIKU_MODEL, replace the inline literal with a require.
54
+ const HAIKU_MODEL_ID = 'claude-haiku-4-5';
55
+
56
+ /**
57
+ * suggestRoomName({auto_explore_finding, mva_brief_sentence, opts})
58
+ * @returns {Promise<{ok, suggested_name, model_used, latency_ms, error_short?}>}
59
+ */
60
+ async function suggestRoomName(args) {
61
+ const opts = (args && typeof args === 'object') ? args : {};
62
+ const autoExploreFinding = opts.auto_explore_finding || null;
63
+ const mvaBriefSentence = (typeof opts.mva_brief_sentence === 'string') ? opts.mva_brief_sentence : '';
64
+ const llmClient = opts.llmClient || null;
65
+
66
+ const t0 = Date.now();
67
+ try {
68
+ const client = llmClient || _resolveProductionLlmClient();
69
+ const prompt = _buildLocalPrompt(autoExploreFinding, mvaBriefSentence);
70
+ const response = await client.complete({
71
+ model: HAIKU_MODEL_ID,
72
+ messages: [{ role: 'user', content: prompt }],
73
+ max_tokens: 20,
74
+ });
75
+ const raw = (response && typeof response.content === 'string') ? response.content : '';
76
+ const normalized = _normalizeSlug(raw);
77
+ const latency_ms = Date.now() - t0;
78
+ if (!normalized || normalized.length === 0) {
79
+ return { ok: false, suggested_name: FALLBACK_SUGGESTION, model_used: HAIKU_MODEL_ID, latency_ms, error_short: 'empty_response' };
80
+ }
81
+ return { ok: true, suggested_name: normalized, model_used: HAIKU_MODEL_ID, latency_ms };
82
+ } catch (err) {
83
+ const latency_ms = Date.now() - t0;
84
+ const error_short = String(err && err.message || err).slice(0, 60);
85
+ return { ok: false, suggested_name: FALLBACK_SUGGESTION, model_used: HAIKU_MODEL_ID, latency_ms, error_short };
86
+ }
87
+ }
88
+
89
+ function _buildLocalPrompt(autoExploreFinding, mvaBriefSentence) {
90
+ // Build a short prompt from LOCAL signals ONLY. The findings array carries
91
+ // the top whitespace + reverse-salient + cross-domain hits from Phase 117;
92
+ // the MVA brief sentence is the user's first conversational turn (already
93
+ // local). NEVER include any Brain-derived suggestion in this prompt.
94
+ const findingsSummary = _summarizeFindings(autoExploreFinding);
95
+ return [
96
+ 'You are naming a venture. Suggest a 2-3 word kebab-case slug that captures',
97
+ 'the core domain. Return ONLY the slug -- no prose, no quotes, no markdown.',
98
+ '',
99
+ 'Brief: ' + (mvaBriefSentence || '(no brief)'),
100
+ 'Findings: ' + findingsSummary,
101
+ '',
102
+ 'Slug:',
103
+ ].join('\n');
104
+ }
105
+
106
+ function _summarizeFindings(autoExploreFinding) {
107
+ if (!autoExploreFinding || typeof autoExploreFinding !== 'object') return '(none)';
108
+ const findings = Array.isArray(autoExploreFinding.findings) ? autoExploreFinding.findings : [];
109
+ if (findings.length === 0) return '(empty)';
110
+ return findings.slice(0, 3).map(function (f) {
111
+ const sp = (typeof f.source_pipeline === 'string') ? f.source_pipeline : 'unknown';
112
+ const hsi = (typeof f.hsi_score === 'number') ? f.hsi_score.toFixed(2) : '?';
113
+ return sp + ':' + hsi;
114
+ }).join(', ');
115
+ }
116
+
117
+ function _normalizeSlug(raw) {
118
+ if (typeof raw !== 'string') return '';
119
+ let slug = raw.trim().toLowerCase();
120
+ // Collapse whitespace to single hyphen; drop everything that's not [a-z0-9-].
121
+ slug = slug.replace(/\s+/g, '-');
122
+ slug = slug.replace(/[^a-z0-9-]/g, '');
123
+ slug = slug.replace(/-{2,}/g, '-');
124
+ slug = slug.replace(/^-+|-+$/g, '');
125
+ return slug;
126
+ }
127
+
128
+ // Phase 119-01 REVISION 2026-05-16 (Blocker 2 Option A): mva-agent-contract.cjs
129
+ // exports {runAgent, validateAgentResult, AGENT_RESULT_SHAPE} ONLY -- no
130
+ // createLlmClient factory exists. The project-wide LLM-call idiom is direct
131
+ // fetch to https://api.anthropic.com/v1/messages with x-api-key header
132
+ // (precedent: lib/core/mva-classifier.cjs::_callHaiku same Haiku 4.5 model,
133
+ // same anthropic-version header pattern, same AbortController timeout).
134
+ //
135
+ // This module mirrors that precedent verbatim. No @anthropic-ai/sdk dependency
136
+ // added. Canon Part 8 invariant preserved: api.anthropic.com is the LOCAL
137
+ // Anthropic API endpoint; the Mindrian-owned Brain MCP host is NEVER contacted.
138
+ const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
139
+ const ANTHROPIC_VERSION = '2023-06-01';
140
+ const LLM_TIMEOUT_MS = 5000;
141
+
142
+ function _resolveProductionLlmClient() {
143
+ const apiKey = process.env.ANTHROPIC_API_KEY;
144
+ if (!apiKey || typeof apiKey !== 'string' || apiKey.length === 0) {
145
+ throw new Error('anthropic_api_key_missing');
146
+ }
147
+ if (typeof fetch !== 'function') {
148
+ throw new Error('global_fetch_unavailable');
149
+ }
150
+ return {
151
+ complete: async function (req) {
152
+ const model = req.model || HAIKU_MODEL_ID;
153
+ const messages = Array.isArray(req.messages) ? req.messages : [];
154
+ const max_tokens = (typeof req.max_tokens === 'number') ? req.max_tokens : 20;
155
+ const ctrl = (typeof AbortController === 'function') ? new AbortController() : null;
156
+ const timer = ctrl ? setTimeout(function () { try { ctrl.abort(); } catch (_e) {} }, LLM_TIMEOUT_MS) : null;
157
+ try {
158
+ const res = await fetch(ANTHROPIC_URL, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'content-type': 'application/json',
162
+ 'x-api-key': apiKey,
163
+ 'anthropic-version': ANTHROPIC_VERSION,
164
+ },
165
+ body: JSON.stringify({ model: model, max_tokens: max_tokens, messages: messages }),
166
+ signal: ctrl ? ctrl.signal : undefined,
167
+ });
168
+ if (!res || !res.ok) {
169
+ throw new Error('anthropic_http_' + (res ? res.status : 'no_response'));
170
+ }
171
+ const j = await res.json();
172
+ let text = '';
173
+ if (j && Array.isArray(j.content)) {
174
+ for (const blk of j.content) {
175
+ if (blk && blk.type === 'text' && typeof blk.text === 'string') {
176
+ text += blk.text;
177
+ }
178
+ }
179
+ }
180
+ return { content: text };
181
+ } finally {
182
+ if (timer) clearTimeout(timer);
183
+ }
184
+ },
185
+ };
186
+ }
187
+
188
+ module.exports = {
189
+ suggestRoomName: suggestRoomName,
190
+ FALLBACK_SUGGESTION: FALLBACK_SUGGESTION,
191
+ HAIKU_MODEL_ID: HAIKU_MODEL_ID,
192
+ _normalizeSlug: _normalizeSlug,
193
+ _buildLocalPrompt: _buildLocalPrompt,
194
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+ // Phase 119-01 Task 1 tests for lib/core/llm-name-suggester.cjs.
3
+ // Validates HAIKU_MODEL_ID + FALLBACK_SUGGESTION + suggestRoomName happy path +
4
+ // graceful degradation + Canon Part 8 LOCAL invariant (zero brain.mindrian
5
+ // substrings in module source) + EVENT_TYPES floor + room_discard_partial_failure
6
+ // membership.
7
+
8
+ const test = require('node:test');
9
+ const assert = require('node:assert');
10
+ const fs = require('node:fs');
11
+ const path = require('node:path');
12
+ const os = require('node:os');
13
+ const crypto = require('node:crypto');
14
+
15
+ const suggester = require('./llm-name-suggester.cjs');
16
+ const memoryEvents = require('./navigation/memory-events.cjs');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // EVENT_TYPES tests (floor + named membership)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ test('Test 1: EVENT_TYPES Set size grows by at least 1 above the Plan 119-00 baseline (floor >= 42)', function () {
23
+ assert.ok(memoryEvents.EVENT_TYPES.size >= 42,
24
+ 'EVENT_TYPES.size must be >= 42 (38 baseline + 3 from Plan 119-00 + 1 from this plan); got ' + memoryEvents.EVENT_TYPES.size);
25
+ });
26
+
27
+ test('Test 2: room_discard_partial_failure is in EVENT_TYPES; regression check on Plan 119-00 strings preserved', function () {
28
+ assert.ok(memoryEvents.EVENT_TYPES.has('room_discard_partial_failure'), 'room_discard_partial_failure missing');
29
+ assert.ok(memoryEvents.EVENT_TYPES.has('room_auto_created'), 'Plan 119-00 string room_auto_created regressed');
30
+ assert.ok(memoryEvents.EVENT_TYPES.has('room_naming_decided'), 'Plan 119-00 string room_naming_decided regressed');
31
+ assert.ok(memoryEvents.EVENT_TYPES.has('room_discarded'), 'Plan 119-00 string room_discarded regressed');
32
+ });
33
+
34
+ test('Test 3: EVENT_TYPES object is Object.frozen (own-properties invariant)', function () {
35
+ assert.ok(Object.isFrozen(memoryEvents.EVENT_TYPES),
36
+ 'EVENT_TYPES Set must be Object.frozen (own-properties; the internal Set slot is documentation-only frozen per ECMAScript spec)');
37
+ });
38
+
39
+ test('Test 4: logEvent acceptance round-trip for room_discard_partial_failure', function () {
40
+ // Use a real tmp room.db via room-db.cjs::openRoomDb (the canonical opener).
41
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'lln-suggester-test-'));
42
+ try {
43
+ const { openRoomDb, closeRoomDb } = require('./room-db.cjs');
44
+ const db = openRoomDb(tmpRoot);
45
+ try {
46
+ const result = memoryEvents.logEvent(db, 'room_discard_partial_failure', {
47
+ previous_slug: 'untitled-2026-05-16-1845',
48
+ partial_state: { fs_removed: true, registry_purged: false },
49
+ error_short: 'EBUSY',
50
+ source_path: 'system:room-discard-cascade',
51
+ created_by: 'system',
52
+ });
53
+ assert.strictEqual(result.ok, true, 'logEvent must accept room_discard_partial_failure as a valid event_type');
54
+ assert.match(result.eventId, /^memory_event:room_discard_partial_failure:\d+:[0-9a-f]{8}$/);
55
+ } finally {
56
+ closeRoomDb(db);
57
+ }
58
+ } finally {
59
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch (_e) {}
60
+ }
61
+ });
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // suggestRoomName tests
65
+ // ---------------------------------------------------------------------------
66
+
67
+ test('Test 5: HAIKU_MODEL_ID constant equals claude-haiku-4-5 (mirrors lib/core/mva-classifier.cjs:53)', function () {
68
+ assert.strictEqual(suggester.HAIKU_MODEL_ID, 'claude-haiku-4-5');
69
+ });
70
+
71
+ test('Test 6: FALLBACK_SUGGESTION constant equals untitled (verbatim)', function () {
72
+ assert.strictEqual(suggester.FALLBACK_SUGGESTION, 'untitled');
73
+ });
74
+
75
+ test('Test 7: suggestRoomName happy path returns normalized slug', async function () {
76
+ const stub = { complete: async function () { return { content: 'acme-robotics' }; } };
77
+ const r = await suggester.suggestRoomName({
78
+ auto_explore_finding: { findings: [{ source_pipeline: 'whitespace', hsi_score: 0.83 }] },
79
+ mva_brief_sentence: 'Robotic precision for industrial automation.',
80
+ llmClient: stub,
81
+ });
82
+ assert.strictEqual(r.ok, true);
83
+ assert.strictEqual(r.suggested_name, 'acme-robotics');
84
+ assert.strictEqual(r.model_used, 'claude-haiku-4-5');
85
+ assert.ok(typeof r.latency_ms === 'number' && r.latency_ms >= 0);
86
+ });
87
+
88
+ test('Test 8: suggestRoomName graceful degradation on LLM error -> FALLBACK_SUGGESTION', async function () {
89
+ const stub = { complete: async function () { throw new Error('timeout'); } };
90
+ const r = await suggester.suggestRoomName({
91
+ auto_explore_finding: null,
92
+ mva_brief_sentence: '',
93
+ llmClient: stub,
94
+ });
95
+ assert.strictEqual(r.ok, false);
96
+ assert.strictEqual(r.suggested_name, 'untitled');
97
+ assert.strictEqual(r.model_used, 'claude-haiku-4-5');
98
+ assert.ok(typeof r.latency_ms === 'number' && r.latency_ms >= 0);
99
+ assert.ok(typeof r.error_short === 'string' && r.error_short.length > 0);
100
+ });
101
+
102
+ test('Test 9: Canon Part 8 LOCAL invariant -- grep tripwire on llm-name-suggester source', function () {
103
+ const src = fs.readFileSync(require.resolve('./llm-name-suggester.cjs'), 'utf8');
104
+ // The forbidden Brain coupling regex (from the plan scaffold harness Gate 3):
105
+ // brain.mindrian | require.+brain-client | fetch.+brain
106
+ assert.ok(!/brain\.mindrian/.test(src), 'source contains brain.mindrian substring (Canon Part 8 breach)');
107
+ assert.ok(!/require\([^)]*brain-client[^)]*\)/.test(src), 'source requires a brain-client module (Canon Part 8 breach)');
108
+ // The fetch.+brain regex would match commentary about "brain" near a fetch
109
+ // discussion; we narrow to actual fetch() invocations to brain.* hosts:
110
+ assert.ok(!/fetch\([^)]*['\"][^'\"]*brain[^'\"]*['\"]/.test(src), 'source fetches a brain.* URL (Canon Part 8 breach)');
111
+ });
112
+
113
+ test('Test 10: Canon Part 8 LOCAL invariant -- payload audit (constructed prompt contains no Brain handle)', function () {
114
+ const prompt = suggester._buildLocalPrompt({
115
+ findings: [{ source_pipeline: 'whitespace', hsi_score: 0.5 }],
116
+ }, 'AI-assisted protein folding for drug discovery.');
117
+ const serialized = JSON.stringify(prompt);
118
+ assert.ok(serialized.indexOf('brain.mindrian') === -1, 'prompt must not contain brain.mindrian');
119
+ assert.ok(serialized.indexOf('brain-client') === -1, 'prompt must not contain brain-client handle');
120
+ });
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Em-dash invariant on the test file itself + module source
124
+ // ---------------------------------------------------------------------------
125
+
126
+ test('Test 11: zero em-dashes in llm-name-suggester source + this test file', function () {
127
+ const EMDASH = String.fromCharCode(0x2014);
128
+ const src = fs.readFileSync(require.resolve('./llm-name-suggester.cjs'), 'utf8');
129
+ const testSrc = fs.readFileSync(__filename, 'utf8');
130
+ assert.ok(src.indexOf(EMDASH) === -1, 'em-dash present in llm-name-suggester.cjs (HARD RULE)');
131
+ assert.ok(testSrc.indexOf(EMDASH) === -1, 'em-dash present in llm-name-suggester.test.cjs (HARD RULE)');
132
+ });
@@ -303,6 +303,47 @@ async function runPipeline(opts) {
303
303
  });
304
304
  } catch (_e) { /* best-effort */ }
305
305
 
306
+ // phase-119-01-naming-selector-hook: spawn the post-MVA retroactive-naming F.1
307
+ // selector as a detached child. The selector runs entirely LOCAL (Canon Part 8);
308
+ // it surfaces an F.1 selector with the four locked labels from CONTEXT.md D-06
309
+ // (name this room: <LLM-suggested> / type your own name / keep as untitled /
310
+ // discard room), validates user choice through the Phase 88.2 dispatcher, and
311
+ // emits room_naming_decided + room_discarded memory_events via the navigation.cjs
312
+ // chokepoint.
313
+ //
314
+ // Phase 119-01 failure NEVER regresses Phase 118: the spawn is detached + unref'd;
315
+ // any failure (missing shim, require failure, etc.) is swallowed; runPipeline
316
+ // returns immediately. The state.json write below is unaffected.
317
+ try {
318
+ const cp = require('node:child_process');
319
+ const hookPath = require('node:path');
320
+ const hookFs = require('node:fs');
321
+ const hookOs = require('node:os');
322
+ const shimPath = hookPath.join(__dirname, '..', '..', 'scripts', 'room-naming-selector.cjs');
323
+ // Resolve the current room dir via the rooms registry to honor Plan 119-00's
324
+ // reassignment of the active slug.
325
+ const roomsHome = process.env.MINDRIAN_ROOMS_HOME ||
326
+ hookPath.join(process.env.HOME || hookOs.homedir(), 'MindrianRooms');
327
+ const registryPath = hookPath.join(roomsHome, '.rooms', 'registry.json');
328
+ let activeRoomDir = null;
329
+ if (hookFs.existsSync(registryPath)) {
330
+ const reg = JSON.parse(hookFs.readFileSync(registryPath, 'utf8'));
331
+ if (reg && typeof reg.active === 'string' && reg.active.length > 0) {
332
+ activeRoomDir = hookPath.join(roomsHome, reg.active);
333
+ }
334
+ }
335
+ if (activeRoomDir && hookFs.existsSync(shimPath)) {
336
+ const child = cp.spawn('node', [shimPath, '--room-dir', activeRoomDir, '--sentence-sha256', sha256], {
337
+ detached: true,
338
+ stdio: 'ignore',
339
+ });
340
+ if (typeof child.unref === 'function') child.unref();
341
+ }
342
+ } catch (_e) {
343
+ // Phase 119-01 failure NEVER regresses Phase 118. Swallow silently; the user's
344
+ // MVA brief still rendered + deployed + state.json still writes below.
345
+ }
346
+
306
347
  // CRITICAL-3 wire: atomic state.json manifest after mva_brief_rendered.
307
348
  // Only on the rendered path (not on Hebrew short-circuit which returned earlier).
308
349
  // Plan 118-04 carries the deck_url into the manifest atomically.
@@ -1,32 +1,9 @@
1
1
  /*
2
2
  * Copyright (c) 2026 Mindrian. BSL 1.1.
3
- *
4
- * Phase 118-03 Plan 03 -- mva-telemetry.
5
- *
6
- * JSONL writer for the 6 Phase 121 MVA event types. Atomic append to
7
- * ~/.mindrian/telemetry/v1.13/mva.jsonl. Schema-enforced (rejects events
8
- * with raw user content; per-event ALLOWED_FIELDS is the source of truth
9
- * for Plan 118-06's Dror harness grep test).
10
- *
11
- * Per Plan 118-03 OQ8 (resolved): the 6 event types are
12
- * mva_pipeline_started, mva_agent_returned, mva_brief_rendered,
13
- * mva_option_selected, mva_brief_deployed, mva_pipeline_failed.
14
- *
15
- * CRITICAL invariant (WARN-2 from iteration 2): the mva_brief_rendered
16
- * event uses `total_duration_ms` (NOT `duration_ms`). Plan 118-06's
17
- * Dror harness greps ALLOWED_FIELDS.mva_brief_rendered to assert this.
18
- *
19
- * Canon Part 8 (LOCAL telemetry):
20
- * - Sentence-related identifier is sentence_sha256 ONLY (one-way hash).
21
- * - Every string field is capped (sentence_sha256=64 exact, error_short<=60,
22
- * other strings<=64) to prevent raw user content from sneaking through.
23
- * - Fields not in ALLOWED_FIELDS for the event type are rejected.
24
- *
25
- * Atomic append: fs.appendFileSync writes a single short line. POSIX
26
- * append semantics guarantee atomicity for writes within PIPE_BUF (4096
27
- * bytes on Linux). Our lines are < 512 bytes, well below the limit.
28
- *
29
- * Pure CJS, node built-ins only.
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.
30
7
  */
31
8
  'use strict';
32
9
 
@@ -34,137 +11,48 @@ const fs = require('node:fs');
34
11
  const path = require('node:path');
35
12
  const os = require('node:os');
36
13
 
37
- // ---------- Frozen invariants ----------
14
+ const writer = require('./telemetry/writer.cjs');
15
+ const validator = require('./telemetry/validator.cjs');
16
+ const schema = require('./telemetry/schema.cjs');
38
17
 
39
18
  const EVENT_TYPES = Object.freeze([
40
- 'mva_pipeline_started',
41
- 'mva_agent_returned',
42
- 'mva_brief_rendered',
43
- 'mva_option_selected',
44
- 'mva_brief_deployed',
45
- 'mva_pipeline_failed'
19
+ 'mva_pipeline_started', 'mva_agent_returned', 'mva_brief_rendered',
20
+ 'mva_option_selected', 'mva_brief_deployed', 'mva_pipeline_failed',
46
21
  ]);
47
22
 
48
- // Per-event scalar field schema. Source-of-truth for Plan 118-06 Dror harness.
49
- // CRITICAL: mva_brief_rendered uses 'total_duration_ms' (NOT 'duration_ms').
50
23
  const ALLOWED_FIELDS = Object.freeze({
51
- mva_pipeline_started: Object.freeze(['sentence_sha256']),
52
- mva_agent_returned: Object.freeze(['sentence_sha256', 'agent_id', 'duration_ms', 'status', 'error_short']),
53
- mva_brief_rendered: Object.freeze(['sentence_sha256', 'total_duration_ms', 'agent_count_ok', 'agent_count_failed']),
54
- mva_option_selected: Object.freeze(['sentence_sha256', 'option_id', 'time_to_click_ms']),
55
- mva_brief_deployed: Object.freeze(['sentence_sha256', 'vercel_subdomain_hash', 'deploy_duration_ms', 'status', 'error_short']),
56
- mva_pipeline_failed: Object.freeze(['sentence_sha256', 'total_duration_ms', 'error_short'])
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,
57
30
  });
58
31
 
59
- // String-length caps. error_short is special-cased to <= 60.
60
- // sentence_sha256 must be exactly 64 hex chars.
61
- const MAX_STRING_LEN = 64;
62
- const MAX_ERROR_SHORT_LEN = 60;
63
- const SHA256_LEN = 64;
64
-
65
- // ---------- Path resolvers (env-aware for hermetic testing) ----------
66
-
67
- function homeDir() {
32
+ function _homeDir() {
68
33
  return process.env.HOME || process.env.USERPROFILE || os.homedir();
69
34
  }
70
35
 
71
- function telemetryDir() {
72
- return path.join(homeDir(), '.mindrian', 'telemetry', 'v1.13');
73
- }
74
-
75
- function telemetryFile() {
76
- return path.join(telemetryDir(), 'mva.jsonl');
77
- }
78
-
79
- // ---------- Validation ----------
80
-
81
- /**
82
- * validateEventPayload(event, payload) -> { ok: boolean, error?: string }
83
- *
84
- * Returns ok:false if:
85
- * - event is not in EVENT_TYPES
86
- * - any key in payload is not in ALLOWED_FIELDS[event]
87
- * - any string value violates the per-field length cap
88
- *
89
- * Numeric fields are not length-checked. The session_id field is added
90
- * by emit() (not the caller), so it is not validated against ALLOWED_FIELDS.
91
- */
92
- function validateEventPayload(event, payload) {
93
- if (!EVENT_TYPES.includes(event)) {
94
- return { ok: false, error: 'unknown_event' };
95
- }
96
- if (!payload || typeof payload !== 'object') {
97
- return { ok: false, error: 'payload_not_object' };
98
- }
99
- const allowed = ALLOWED_FIELDS[event];
100
- for (const key of Object.keys(payload)) {
101
- if (!allowed.includes(key)) {
102
- return { ok: false, error: 'unknown_field:' + key };
103
- }
104
- const v = payload[key];
105
- if (typeof v === 'string') {
106
- if (key === 'sentence_sha256') {
107
- if (v.length !== SHA256_LEN) {
108
- return { ok: false, error: 'sha256_length_invalid' };
109
- }
110
- } else if (key === 'error_short') {
111
- if (v.length > MAX_ERROR_SHORT_LEN) {
112
- return { ok: false, error: 'error_short_too_long' };
113
- }
114
- } else {
115
- if (v.length > MAX_STRING_LEN) {
116
- return { ok: false, error: 'string_too_long:' + key };
117
- }
118
- }
119
- }
120
- }
121
- return { ok: true };
36
+ function _legacyMvaPath() {
37
+ return path.join(_homeDir(), '.mindrian', 'telemetry', 'v1.13', 'mva.jsonl');
122
38
  }
123
39
 
124
- // ---------- Public emit() ----------
125
-
126
- /**
127
- * emit(event, payload) -> appends one JSONL line to ~/.mindrian/telemetry/v1.13/mva.jsonl
128
- *
129
- * Validates first. On invalid payload throws a ValidationError (so callers
130
- * cannot silently leak). On disk error returns silently (best-effort: the
131
- * pipeline must not crash because telemetry is unavailable).
132
- */
133
40
  function emit(event, payload) {
134
- const v = validateEventPayload(event, payload);
135
- if (!v.ok) {
136
- const e = new Error('telemetry validation failed: ' + v.error);
137
- e.code = 'TELEMETRY_VALIDATION';
138
- throw e;
139
- }
140
-
141
- const record = Object.assign(
142
- {
143
- event: event,
144
- timestamp: new Date().toISOString(),
145
- session_id: (typeof process.env.CLAUDE_SESSION_ID === 'string' && process.env.CLAUDE_SESSION_ID.length > 0)
146
- ? process.env.CLAUDE_SESSION_ID.slice(0, MAX_STRING_LEN)
147
- : 'default'
148
- },
149
- payload
150
- );
151
-
41
+ writer.emit(event, payload); // Canon Part 8 gate + unified events-YYYY-WNN.jsonl append.
152
42
  try {
153
- fs.mkdirSync(telemetryDir(), { recursive: true });
154
- fs.appendFileSync(telemetryFile(), JSON.stringify(record) + '\n', 'utf8');
155
- } catch (_e) {
156
- // Best-effort. The MVA pipeline must not fail because telemetry disk
157
- // is unavailable. Plan 118-06's Dror harness checks for the file's
158
- // existence post-run as the success signal.
159
- }
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 */ }
160
49
  }
161
50
 
162
51
  module.exports = {
163
- emit,
164
- validateEventPayload,
165
- EVENT_TYPES,
166
- ALLOWED_FIELDS,
167
- // Exposed for tests + downstream plans that need the canonical path
168
- telemetryDir,
169
- telemetryFile,
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,
170
58
  };
@@ -33,6 +33,41 @@ const ALLOWED_EDGE_TYPES = Object.freeze(new Set([
33
33
  // Phase 125 D7 -- F-selector decision edges (LOCKED LOCAL per Canon Part 8).
34
34
  'DEFERRED',
35
35
  'REJECTED',
36
+ // Phase 120-00 Wave 1 extension (Breakthrough Scan / Category G; D-18 HARD FLOOR enforcement
37
+ // + D-20 Cypher-provable principle). DERIVED_FROM is the structural enforcement: a
38
+ // Breakthrough node CANNOT surface without at least one DERIVED_FROM edge to an
39
+ // Artifact node. Mirrors the Phase 125-00 DEFERRED + REJECTED additive idiom.
40
+ //
41
+ // Canon Part 4: every choice is graph data. The Breakthrough node + its DERIVED_FROM
42
+ // edges are the graph-native artifact of pattern detection.
43
+ //
44
+ // Canon Part 8: writeEdge takes (db, params) over a LOCAL room.db handle; DERIVED_FROM
45
+ // never crosses to Brain. Cross-room aggregation forbidden (Phase 8 cross-room fence).
46
+ //
47
+ // D-20 enforcement: lib/core/breakthrough/schema.cjs::writeBreakthrough wraps the
48
+ // Breakthrough node insert + N DERIVED_FROM edge inserts in a single SQLite transaction.
49
+ // If any step fails, the transaction rolls back -- partial Breakthrough state CANNOT
50
+ // land. The Cypher invariant `MATCH (b:Breakthrough)-[:DERIVED_FROM]->(a:Artifact)
51
+ // RETURN count(a)` is guaranteed >= 1 by construction.
52
+ 'DERIVED_FROM',
53
+ // Phase 120-02 Wave 2 extension (Breakthrough Scan / Category G; D-09 file-as-decision
54
+ // bridge). FILED_AS_DECISION is the typed edge that promotes a Breakthrough node into
55
+ // the Phase 88 decision-log machinery when the user picks the [File as decision]
56
+ // verb on F.7. Mirrors the Phase 120-00 DERIVED_FROM additive idiom verbatim.
57
+ //
58
+ // Canon Part 4: every choice is graph data. The Breakthrough -> Decision edge is the
59
+ // graph-native bridge that lets future related breakthroughs reference the filed
60
+ // decision via ENABLES edges (per CONTEXT.md D-15 "may be referenced as ENABLES in
61
+ // future related breakthroughs").
62
+ //
63
+ // Canon Part 8: writeEdge takes (db, params) over a LOCAL room.db handle;
64
+ // FILED_AS_DECISION never crosses to Brain. Cross-room aggregation forbidden.
65
+ //
66
+ // Emitted by: lib/core/breakthrough/verb-dispatch.cjs::handleFileAsDecision.
67
+ // The destination Decision node id is 'decision:' + breakthroughId by convention;
68
+ // Phase 88 decision-log machinery (or a future Phase 121 housekeeping pass) is
69
+ // responsible for materializing the Decision node body when one does not yet exist.
70
+ 'FILED_AS_DECISION',
36
71
  ]));
37
72
 
38
73
  function isPlainObject(v) {