@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,113 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Copyright (c) 2026 Mindrian. BSL 1.1.
4
+ *
5
+ * first-touch-version-stamper.cjs
6
+ * --------------------------------
7
+ * Phase 121.5-05 Sub-plan F (SEED-007 absorption).
8
+ *
9
+ * Pure function module that returns the canonical version stamp for each
10
+ * first-touch surface (banner / splash / onboard / sessionstart /
11
+ * operator-update / larry-extended). The string the user sees -- "MindrianOS
12
+ * v<version>" -- is derived once, here, so the per-surface emission stays in
13
+ * lock-step with .claude-plugin/plugin.json.
14
+ *
15
+ * Source finding (SEED-007, 2026-05-09): a v1.12.0 "Three ways to do this"
16
+ * greeting was rendering on a stale Windows install with NO version-of-record
17
+ * visible. The user could not answer "what version am I running?" by LOOKING
18
+ * at the terminal. This module is the canonical answer to that question.
19
+ *
20
+ * Canon Part 7 (Reuse Before Build): the version-of-record source is
21
+ * .claude-plugin/plugin.json -- the same one scripts/banner already reads via
22
+ * lib/core/platform.cjs::readPluginJsonVersion. This module wraps that source
23
+ * with the per-surface rendering contract; it does NOT introduce a second
24
+ * version-of-record surface.
25
+ *
26
+ * Canon Part 8: local-only; no fetch, no Brain, no telemetry egress.
27
+ *
28
+ * No emoji. No em-dashes. ASCII-clean source.
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ const fs = require('fs');
34
+ const path = require('path');
35
+
36
+ const SURFACES_PATH = path.join(__dirname, '..', '..', 'data', 'first-touch-surfaces.json');
37
+ const PLUGIN_JSON_PATH = path.join(__dirname, '..', '..', '.claude-plugin', 'plugin.json');
38
+
39
+ const KNOWN_SURFACES = new Set([
40
+ 'banner',
41
+ 'splash',
42
+ 'onboard',
43
+ 'sessionstart',
44
+ 'operator-update',
45
+ 'larry-extended',
46
+ ]);
47
+
48
+ function readSurfaces() {
49
+ return JSON.parse(fs.readFileSync(SURFACES_PATH, 'utf8'));
50
+ }
51
+
52
+ function resolveCurrentVersion() {
53
+ try {
54
+ return JSON.parse(fs.readFileSync(PLUGIN_JSON_PATH, 'utf8')).version;
55
+ } catch (_e) {
56
+ return 'unknown';
57
+ }
58
+ }
59
+
60
+ function stampVersion(surfaceId, version) {
61
+ if (!KNOWN_SURFACES.has(surfaceId)) {
62
+ throw new Error('unknown_surface: ' + surfaceId);
63
+ }
64
+ const v = version || resolveCurrentVersion();
65
+ switch (surfaceId) {
66
+ case 'banner':
67
+ return 'MindrianOS v' + v;
68
+ case 'splash':
69
+ return 'MindrianOS v' + v + ' -- conversation as the product surface';
70
+ case 'onboard':
71
+ return 'Welcome to MindrianOS v' + v + '. Let me show you around.';
72
+ case 'sessionstart':
73
+ case 'operator-update':
74
+ case 'larry-extended':
75
+ return 'MindrianOS v' + v;
76
+ default:
77
+ // unreachable: KNOWN_SURFACES gates entry
78
+ throw new Error('unknown_surface: ' + surfaceId);
79
+ }
80
+ }
81
+
82
+ // U+2014 EM DASH detector -- the no-em-dash hard rule scanner uses this
83
+ // (CLAUDE.md / MEMORY feedback_no_emdashes). Written as an escape so this
84
+ // source file stays ASCII-clean.
85
+ const EM_DASH = '\u2014';
86
+
87
+ function hasEmDash(text) {
88
+ return typeof text === 'string' && text.indexOf(EM_DASH) >= 0;
89
+ }
90
+
91
+ module.exports = {
92
+ stampVersion,
93
+ resolveCurrentVersion,
94
+ readSurfaces,
95
+ hasEmDash,
96
+ KNOWN_SURFACES,
97
+ };
98
+
99
+ // Allow CLI invocation: `node lib/core/first-touch-version-stamper.cjs banner`
100
+ if (require.main === module) {
101
+ const surfaceArg = process.argv[2];
102
+ if (!surfaceArg) {
103
+ process.stdout.write(stampVersion('banner') + '\n');
104
+ process.exit(0);
105
+ }
106
+ try {
107
+ process.stdout.write(stampVersion(surfaceArg) + '\n');
108
+ process.exit(0);
109
+ } catch (e) {
110
+ process.stderr.write('first-touch-version-stamper: ' + e.message + '\n');
111
+ process.exit(2);
112
+ }
113
+ }
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 -- Larry thinness-acknowledgment voice + thin-finding detector.
4
+ *
5
+ * Per CONTEXT.md D-05: when Phase 117's composeAutoExploreFinding returns
6
+ * thin output (null OR <= 1 finding), the room is still scaffolded but
7
+ * Larry's voice acknowledges thinness honestly via the verbatim copy below.
8
+ *
9
+ * The verbatim copy is LOCKED per .planning/phases/119-room-as-receipt-invariant/
10
+ * 119-02-PLAN.md objective block (lines 89-93). Do NOT paraphrase.
11
+ *
12
+ * Em-dash HARD RULE: uses `--` (two ASCII hyphens) NEVER `-` (U+2014).
13
+ * Smart-quote BAN: uses ASCII apostrophe `'` (0x27) only -- never `’`.
14
+ *
15
+ * Canon Part 8 invariant: pure-local module; no Brain call, no telemetry
16
+ * egress; the voice line is a static string with zero outbound surface.
17
+ * Canon Part 10 sub-claim 1: Larry IS the product -- the voice line is
18
+ * the conversational anchor that makes "rooms are receipts" feel honest
19
+ * rather than apologetic when the source material is thin (D-05).
20
+ */
21
+
22
+ // The verbatim D-05 voice line. Locked per the plan. Length: 152 chars (ASCII).
23
+ // Composed via two string literals (concat) for readability; the runtime
24
+ // value is one continuous string.
25
+ const THINNESS_VOICE_LINE =
26
+ "I made a room around this -- it's mostly empty until we have more to work with. " +
27
+ "Want to keep going and see what fills in?";
28
+
29
+ /**
30
+ * Decide whether the room should surface the thinness acknowledgment to the user.
31
+ *
32
+ * Per .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 1
33
+ * <behavior> Test 6: D-05 threshold is "2+ findings = substantive; <=1 finding
34
+ * = thin". The detector treats null / undefined / non-object / missing
35
+ * findings array / empty findings array / single-finding all as thin.
36
+ *
37
+ * @param {object|null} autoExploreFinding the JSON shape Phase 117 writes
38
+ * to .mindrian/auto-explore-<material_id>.json
39
+ * (shape: { material_id, findings: [...], composed_at_ms })
40
+ * @returns {boolean} true if Larry should render the thinness voice line
41
+ */
42
+ function shouldAcknowledgeThinness(autoExploreFinding) {
43
+ if (autoExploreFinding === null || autoExploreFinding === undefined) return true;
44
+ if (typeof autoExploreFinding !== 'object') return true;
45
+ const findings = Array.isArray(autoExploreFinding.findings) ? autoExploreFinding.findings : [];
46
+ return findings.length <= 1;
47
+ }
48
+
49
+ /**
50
+ * Return the verbatim D-05 thinness voice line. Single source of truth -- callers
51
+ * MUST use this function (or the THINNESS_VOICE_LINE constant) rather than
52
+ * inlining the string. This avoids cross-file string drift.
53
+ *
54
+ * @returns {string} the locked verbatim voice line
55
+ */
56
+ function voiceLine() {
57
+ return THINNESS_VOICE_LINE;
58
+ }
59
+
60
+ module.exports = {
61
+ THINNESS_VOICE_LINE: THINNESS_VOICE_LINE,
62
+ shouldAcknowledgeThinness: shouldAcknowledgeThinness,
63
+ voiceLine: voiceLine,
64
+ };
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+ /*
3
+ * Phase 119-02 Task 1 -- test suite for larry-thinness-acknowledgment.cjs.
4
+ *
5
+ * Verifies (per CONTEXT.md D-05 verbatim copy lock + the 11 behavior tests in
6
+ * .planning/phases/119-room-as-receipt-invariant/119-02-PLAN.md Task 1):
7
+ * 1. THINNESS_VOICE_LINE returns exact verbatim string.
8
+ * 2. voiceLine() returns the same string as THINNESS_VOICE_LINE.
9
+ * 3. shouldAcknowledgeThinness(null) -> true.
10
+ * 4. shouldAcknowledgeThinness({findings:[]}) -> true.
11
+ * 5. shouldAcknowledgeThinness({findings:[a,b]}) -> false (substantive).
12
+ * 6. shouldAcknowledgeThinness({findings:[a]}) -> true (D-05 threshold 2+).
13
+ * 7-10. Template files exist + valid frontmatter (verified by Task 2 + bash gate).
14
+ * 11. No smart quotes / em-dashes in the verbatim string (ASCII only).
15
+ */
16
+
17
+ const assert = require('node:assert');
18
+ const test = require('node:test');
19
+
20
+ const {
21
+ THINNESS_VOICE_LINE,
22
+ shouldAcknowledgeThinness,
23
+ voiceLine,
24
+ } = require('./larry-thinness-acknowledgment.cjs');
25
+
26
+ const EXPECTED =
27
+ "I made a room around this -- it's mostly empty until we have more to work with. " +
28
+ "Want to keep going and see what fills in?";
29
+
30
+ test('Test 1: THINNESS_VOICE_LINE is the verbatim D-05 string', () => {
31
+ assert.strictEqual(THINNESS_VOICE_LINE, EXPECTED);
32
+ });
33
+
34
+ test('Test 2: voiceLine() returns same string as THINNESS_VOICE_LINE (single source of truth)', () => {
35
+ assert.strictEqual(voiceLine(), THINNESS_VOICE_LINE);
36
+ });
37
+
38
+ test('Test 3: shouldAcknowledgeThinness(null) returns true', () => {
39
+ assert.strictEqual(shouldAcknowledgeThinness(null), true);
40
+ });
41
+
42
+ test('Test 4: shouldAcknowledgeThinness(undefined) returns true', () => {
43
+ assert.strictEqual(shouldAcknowledgeThinness(undefined), true);
44
+ });
45
+
46
+ test('Test 5: shouldAcknowledgeThinness({findings: []}) returns true (empty findings)', () => {
47
+ assert.strictEqual(shouldAcknowledgeThinness({ findings: [] }), true);
48
+ });
49
+
50
+ test('Test 6: shouldAcknowledgeThinness({findings: [a, b]}) returns false (substantive, 2+ findings)', () => {
51
+ const finding = {
52
+ findings: [
53
+ { source_pipeline: 'whitespace', hsi_score: 0.7 },
54
+ { source_pipeline: 'cross-domain', hsi_score: 0.6 },
55
+ ],
56
+ };
57
+ assert.strictEqual(shouldAcknowledgeThinness(finding), false);
58
+ });
59
+
60
+ test('Test 7: shouldAcknowledgeThinness({findings: [a]}) returns true (1-finding edge case, D-05 threshold)', () => {
61
+ const finding = {
62
+ findings: [{ source_pipeline: 'whitespace', hsi_score: 0.3 }],
63
+ };
64
+ assert.strictEqual(shouldAcknowledgeThinness(finding), true);
65
+ });
66
+
67
+ test('Test 8: shouldAcknowledgeThinness on non-object returns true (defensive)', () => {
68
+ assert.strictEqual(shouldAcknowledgeThinness('not an object'), true);
69
+ assert.strictEqual(shouldAcknowledgeThinness(42), true);
70
+ });
71
+
72
+ test('Test 9: shouldAcknowledgeThinness on object without findings array returns true', () => {
73
+ assert.strictEqual(shouldAcknowledgeThinness({}), true);
74
+ assert.strictEqual(shouldAcknowledgeThinness({ findings: null }), true);
75
+ });
76
+
77
+ test('Test 10: THINNESS_VOICE_LINE has no em-dash (em-dash HARD RULE)', () => {
78
+ // Construct via charCode so this test source itself contains no literal
79
+ // em-dash (which would self-trip the shell-harness em-dash invariant).
80
+ const EMDASH = String.fromCharCode(0x2014);
81
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf(EMDASH), -1, 'em-dash U+2014 present');
82
+ });
83
+
84
+ test('Test 11: THINNESS_VOICE_LINE has no smart-quote characters (ASCII only)', () => {
85
+ // Right single quote U+2019, left single quote U+2018, right double U+201D, left double U+201C
86
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('’'), -1, 'right single quote U+2019 present');
87
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('‘'), -1, 'left single quote U+2018 present');
88
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('”'), -1, 'right double quote U+201D present');
89
+ assert.strictEqual(THINNESS_VOICE_LINE.indexOf('“'), -1, 'left double quote U+201C present');
90
+ });
91
+
92
+ test('Test 12: THINNESS_VOICE_LINE contains the locked phrase fragments', () => {
93
+ assert.ok(THINNESS_VOICE_LINE.includes('I made a room around this'), 'opener missing');
94
+ assert.ok(THINNESS_VOICE_LINE.includes("it's mostly empty"), 'middle (ASCII apostrophe) missing');
95
+ assert.ok(THINNESS_VOICE_LINE.includes('Want to keep going'), 'continuation invite missing');
96
+ assert.ok(THINNESS_VOICE_LINE.includes('see what fills in?'), 'closing question missing');
97
+ });
@@ -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
+ });
@@ -19,7 +19,7 @@ const { safeReadFile } = require('./index.cjs');
19
19
  * - maxTools: soft limit on exposed tools (for context budget)
20
20
  *
21
21
  * Server keys map to entries in .mcp.json:
22
- * - brain: brain.mindrian.ai (remote, Streamable HTTP)
22
+ * - brain: mindrian-brain.onrender.com (remote, Streamable HTTP)
23
23
  * - mindrian: MindrianOS local MCP server
24
24
  * - research: web research tools
25
25
  * - pinecone: vector search