@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,172 @@
1
+ 'use strict';
2
+
3
+ /*
4
+ * Phase 127-01 Task 1 -- pure helpers for the Phase 127 migration script
5
+ * (scripts/migrate-brain-mcp-from-http-to-stdio.cjs).
6
+ *
7
+ * Addresses BRAIN-MCP-127-06 (SG-2 snapshot path helper) +
8
+ * BRAIN-MCP-127-07 (SG-4 idempotency log + sha256 fingerprint + raw-identifier
9
+ * scrub).
10
+ *
11
+ * Pure module. NO network surface. NO process spawning. NO claude CLI
12
+ * invocations (those live in the orchestration script, Task 2). Canon Part 8
13
+ * delegation property: this file contains zero direct network calls.
14
+ *
15
+ * Source-name-prefixed sha256 fingerprint pattern matches TELEMETRY-121-02:
16
+ * fingerprint = sha256(SOURCE_NAME_PREFIX + JSON.stringify(entry-tuple)).slice(0, 16)
17
+ *
18
+ * The 16-hex-char truncation matches lib/core/brain-client.cjs::_hashKey (64
19
+ * bits of key space, effectively zero collision at this volume).
20
+ *
21
+ * SG-4 raw-identifier guard: appendMigrationLog rejects any record string
22
+ * matching Bearer-token / UUID / long-base64-or-hex (>= 24 chars, alphabet-
23
+ * heterogeneous) regexes. Sha256 fingerprints (exactly 16 or 64 hex chars)
24
+ * are whitelisted by-length-and-alphabet so legitimate migration metadata
25
+ * passes through.
26
+ *
27
+ * HARD RULE: zero em-dashes in this file (hyphens only).
28
+ */
29
+
30
+ const crypto = require('node:crypto');
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+
34
+ const SOURCE_NAME_PREFIX = 'mindrian-brain:';
35
+
36
+ /**
37
+ * Compute a stable 16-hex-char sha256 fingerprint over a Brain MCP entry.
38
+ * Mirrors brain-client.cjs::_hashKey shape verbatim (sha256 truncated).
39
+ * Source-name prefix prevents cross-source collisions per TELEMETRY-121-02.
40
+ *
41
+ * @param {{command?: string, args?: string[], env?: object, type?: string}} entry
42
+ * @returns {string}
43
+ */
44
+ function fingerprintEntry(entry) {
45
+ const e = entry || {};
46
+ const input = SOURCE_NAME_PREFIX + JSON.stringify({
47
+ command: e.command,
48
+ args: e.args,
49
+ env: e.env,
50
+ type: e.type,
51
+ });
52
+ return crypto.createHash('sha256').update(input).digest('hex').slice(0, 16);
53
+ }
54
+
55
+ /**
56
+ * Compute the pre-migration snapshot path. Replace colons with dashes for
57
+ * Windows / CIFS filesystem safety per Tavily A127.3 cross-platform note.
58
+ *
59
+ * @param {string} homeDir
60
+ * @param {string} isoTimestamp
61
+ * @returns {string}
62
+ */
63
+ function snapshotPath(homeDir, isoTimestamp) {
64
+ const safe = String(isoTimestamp).replace(/:/g, '-');
65
+ return path.join(homeDir, '.mindrian', 'pre-migration-snapshots', safe + '.json');
66
+ }
67
+
68
+ /**
69
+ * Read the migration log (jsonl). Graceful when missing. Parse errors on a
70
+ * single line skip that line silently (project precedent: migrate-telemetry-v1
71
+ * uses the same shape).
72
+ *
73
+ * @param {string} homeDir
74
+ * @returns {Array<object>}
75
+ */
76
+ function readMigrationsLog(homeDir) {
77
+ const logPath = path.join(homeDir, '.mindrian', 'migrations.jsonl');
78
+ if (!fs.existsSync(logPath)) return [];
79
+ let text;
80
+ try { text = fs.readFileSync(logPath, 'utf8'); } catch (_) { return []; }
81
+ const rows = [];
82
+ for (const line of text.split('\n')) {
83
+ if (!line) continue;
84
+ try { rows.push(JSON.parse(line)); } catch (_) { continue; }
85
+ }
86
+ return rows;
87
+ }
88
+
89
+ /**
90
+ * SG-4 raw-identifier guard. Recursively walk a record, scan strings for
91
+ * Bearer / UUID / long-base64-or-hex (>= 24 chars) shapes. Whitelist exact
92
+ * 16-char (truncated) and 64-char (full) sha256 by length-and-alphabet so
93
+ * legitimate fingerprint fields pass through. Throw on any match.
94
+ *
95
+ * @param {object} record
96
+ */
97
+ function _scanForRawIdentifiers(record) {
98
+ const bearerRe = /Bearer\s+[A-Za-z0-9._\-]{16,}/i;
99
+ const uuidRe = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
100
+ const longRe = /[A-Za-z0-9+/=_\-]{24,}/;
101
+ const sha256Full = /^[a-f0-9]{64}$/i;
102
+
103
+ function walk(node) {
104
+ if (node == null) return;
105
+ if (typeof node === 'string') {
106
+ if (bearerRe.test(node)) throw new Error('raw user identifier detected in migration log record (Bearer)');
107
+ if (uuidRe.test(node)) throw new Error('raw user identifier detected in migration log record (UUID)');
108
+ if (longRe.test(node)) {
109
+ if (sha256Full.test(node)) return;
110
+ throw new Error('raw user identifier detected in migration log record (long alphanumeric run)');
111
+ }
112
+ return;
113
+ }
114
+ if (Array.isArray(node)) { for (const v of node) walk(v); return; }
115
+ if (typeof node === 'object') {
116
+ for (const [k, v] of Object.entries(node)) {
117
+ if (k === 'fingerprint' && typeof v === 'string' && /^[a-f0-9]{16}$/.test(v)) continue;
118
+ walk(v);
119
+ }
120
+ }
121
+ }
122
+
123
+ walk(record);
124
+ }
125
+
126
+ /**
127
+ * Append a record to the migration log. Enforces SG-4 raw-identifier scrub
128
+ * BEFORE the write. Creates parent dir with mode 0700 (POSIX). Chmods the
129
+ * log file to mode 0600 after first write per SEC-02 hygiene.
130
+ *
131
+ * @param {string} homeDir
132
+ * @param {object} record
133
+ */
134
+ function appendMigrationLog(homeDir, record) {
135
+ _scanForRawIdentifiers(record);
136
+ const dir = path.join(homeDir, '.mindrian');
137
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
138
+ const logPath = path.join(dir, 'migrations.jsonl');
139
+ const existed = fs.existsSync(logPath);
140
+ fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
141
+ if (process.platform !== 'win32') {
142
+ try { fs.chmodSync(logPath, 0o600); } catch (_) { /* best-effort */ }
143
+ }
144
+ return { written: true, log_path: logPath, first_write: !existed };
145
+ }
146
+
147
+ /**
148
+ * Return true if a prior 'removed' record exists for (sourceName, fingerprint).
149
+ *
150
+ * @param {string} homeDir
151
+ * @param {string} sourceName
152
+ * @param {string} fingerprint
153
+ * @returns {boolean}
154
+ */
155
+ function isAlreadyMigrated(homeDir, sourceName, fingerprint) {
156
+ const rows = readMigrationsLog(homeDir);
157
+ for (const r of rows) {
158
+ if (r && r.source === sourceName && r.fingerprint === fingerprint && r.action === 'removed') {
159
+ return true;
160
+ }
161
+ }
162
+ return false;
163
+ }
164
+
165
+ module.exports = {
166
+ fingerprintEntry,
167
+ snapshotPath,
168
+ readMigrationsLog,
169
+ appendMigrationLog,
170
+ isAlreadyMigrated,
171
+ _scanForRawIdentifiers,
172
+ };
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Phase 127-01 Task 1 -- RED tests for lib/core/migration-snapshot.cjs
6
+ *
7
+ * 9 tests covering BRAIN-MCP-127-06 (SG-2 snapshot path helper) +
8
+ * BRAIN-MCP-127-07 (SG-4 idempotency log + sha256 fingerprint + raw-identifier
9
+ * scrub).
10
+ *
11
+ * Pure-module suite: zero network surface, zero process spawning, zero
12
+ * external state writes outside hermetic os.tmpdir() fixtures (mkdtempSync +
13
+ * rmSync recursive cleanup per Phase 87 pattern).
14
+ *
15
+ * Tests:
16
+ * 1. fingerprintEntry returns 16 hex chars (sha256 truncated)
17
+ * 2. fingerprintEntry distinguishes inputs by args[0]
18
+ * 3. fingerprintEntry uses source-name "mindrian-brain:" prefix (TELEMETRY-121-02 pattern)
19
+ * 4. snapshotPath replaces colons with dashes (Windows filesystem safety)
20
+ * 5. readMigrationsLog returns [] when log file does not exist
21
+ * 6. appendMigrationLog creates log with mode 0600 (POSIX) + subsequent read returns the record
22
+ * 7. isAlreadyMigrated returns true after matching record appended, false for different fingerprint
23
+ * 8. appendMigrationLog rejects a record containing Bearer-shaped string (SG-4 raw-identifier guard)
24
+ * 9. appendMigrationLog rejects a record containing UUID-shaped value (SG-4 raw-identifier guard)
25
+ */
26
+
27
+ const assert = require('node:assert/strict');
28
+ const fs = require('node:fs');
29
+ const os = require('node:os');
30
+ const path = require('node:path');
31
+ const crypto = require('node:crypto');
32
+
33
+ const snap = require('./migration-snapshot.cjs');
34
+
35
+ function mkHome() {
36
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'mig-snap-'));
37
+ }
38
+
39
+ function rmHome(h) {
40
+ try { fs.rmSync(h, { recursive: true, force: true }); } catch (_) {}
41
+ }
42
+
43
+ let pass = 0;
44
+ let fail = 0;
45
+ function t(name, fn) {
46
+ try {
47
+ fn();
48
+ pass++;
49
+ console.log('PASS: ' + name);
50
+ } catch (e) {
51
+ fail++;
52
+ console.error('FAIL: ' + name);
53
+ console.error(' ' + (e && e.message ? e.message : String(e)));
54
+ if (e && e.stack) console.error(e.stack.split('\n').slice(1, 4).join('\n'));
55
+ }
56
+ }
57
+
58
+ // ---- Test 1: 16-hex-char fingerprint ----
59
+ t('T1 fingerprintEntry returns 16 hex chars', () => {
60
+ const fp = snap.fingerprintEntry({ command: 'node', args: ['x'], env: { K: 'v' }, type: 'http' });
61
+ assert.match(fp, /^[a-f0-9]{16}$/);
62
+ });
63
+
64
+ // ---- Test 2: distinguishes by args[0] + deterministic ----
65
+ t('T2 fingerprintEntry distinguishes by args[0] and is deterministic', () => {
66
+ const a = { command: 'node', args: ['x'], env: {}, type: 'http' };
67
+ const b = { command: 'node', args: ['y'], env: {}, type: 'http' };
68
+ const fpA1 = snap.fingerprintEntry(a);
69
+ const fpA2 = snap.fingerprintEntry(a);
70
+ const fpB = snap.fingerprintEntry(b);
71
+ assert.equal(fpA1, fpA2, 'identical inputs must produce identical outputs');
72
+ assert.notEqual(fpA1, fpB, 'different args[0] must produce different fingerprints');
73
+ });
74
+
75
+ // ---- Test 3: source-name prefix ----
76
+ t('T3 fingerprintEntry uses source-name "mindrian-brain:" prefix per TELEMETRY-121-02', () => {
77
+ const e = { command: 'node', args: ['x'], env: { K: 'v' }, type: 'http' };
78
+ const expectedInput = 'mindrian-brain:' + JSON.stringify({ command: e.command, args: e.args, env: e.env, type: e.type });
79
+ const expectedFp = crypto.createHash('sha256').update(expectedInput).digest('hex').slice(0, 16);
80
+ assert.equal(snap.fingerprintEntry(e), expectedFp);
81
+ });
82
+
83
+ // ---- Test 4: snapshotPath colon replacement ----
84
+ t('T4 snapshotPath replaces colons with dashes for Windows filesystem safety', () => {
85
+ const p = snap.snapshotPath('/tmp/home', '2026-05-19T20:30:00Z');
86
+ assert.equal(p, path.join('/tmp/home', '.mindrian', 'pre-migration-snapshots', '2026-05-19T20-30-00Z.json'));
87
+ });
88
+
89
+ // ---- Test 5: readMigrationsLog graceful on missing ----
90
+ t('T5 readMigrationsLog returns [] when log file does not exist', () => {
91
+ const h = mkHome();
92
+ try {
93
+ const r = snap.readMigrationsLog(h);
94
+ assert.ok(Array.isArray(r));
95
+ assert.equal(r.length, 0);
96
+ } finally {
97
+ rmHome(h);
98
+ }
99
+ });
100
+
101
+ // ---- Test 6: appendMigrationLog creates log with mode 0600 + readback ----
102
+ t('T6 appendMigrationLog creates log with mode 0600 and round-trips via readMigrationsLog', () => {
103
+ const h = mkHome();
104
+ try {
105
+ const rec = { ts: '2026-05-19T00:00:00Z', source: 'mindrian-brain', fingerprint: 'abc1234567890def', action: 'removed' };
106
+ snap.appendMigrationLog(h, rec);
107
+ const logPath = path.join(h, '.mindrian', 'migrations.jsonl');
108
+ assert.ok(fs.existsSync(logPath), 'log file must exist after append');
109
+ if (process.platform !== 'win32') {
110
+ const mode = fs.statSync(logPath).mode & 0o777;
111
+ assert.equal(mode, 0o600, 'log mode must be 0600 on POSIX, got 0' + mode.toString(8));
112
+ }
113
+ const rows = snap.readMigrationsLog(h);
114
+ assert.ok(rows.length >= 1);
115
+ const found = rows.find((r) => r.fingerprint === 'abc1234567890def');
116
+ assert.ok(found, 'appended record must be readable');
117
+ assert.equal(found.action, 'removed');
118
+ } finally {
119
+ rmHome(h);
120
+ }
121
+ });
122
+
123
+ // ---- Test 7: isAlreadyMigrated ----
124
+ t('T7 isAlreadyMigrated returns true after matching record appended, false for different fingerprint', () => {
125
+ const h = mkHome();
126
+ try {
127
+ const fp = 'abc1234567890def';
128
+ snap.appendMigrationLog(h, { ts: '2026-05-19T00:00:00Z', source: 'mindrian-brain', fingerprint: fp, action: 'removed' });
129
+ assert.equal(snap.isAlreadyMigrated(h, 'mindrian-brain', fp), true);
130
+ assert.equal(snap.isAlreadyMigrated(h, 'mindrian-brain', '0000000000000000'), false);
131
+ } finally {
132
+ rmHome(h);
133
+ }
134
+ });
135
+
136
+ // ---- Test 8: SG-4 raw-identifier guard rejects Bearer-shaped string ----
137
+ t('T8 appendMigrationLog rejects record containing Bearer-shaped string (SG-4 invariant)', () => {
138
+ const h = mkHome();
139
+ try {
140
+ const badRec = {
141
+ ts: '2026-05-19T00:00:00Z',
142
+ source: 'mindrian-brain',
143
+ fingerprint: 'abc1234567890def',
144
+ action: 'removed',
145
+ bearer: 'Bearer abcdefghijklmnopqrstuv',
146
+ };
147
+ assert.throws(() => snap.appendMigrationLog(h, badRec), /raw user identifier/);
148
+ } finally {
149
+ rmHome(h);
150
+ }
151
+ });
152
+
153
+ // ---- Test 9: SG-4 raw-identifier guard rejects UUID-shaped value ----
154
+ t('T9 appendMigrationLog rejects record containing UUID-shaped value (SG-4 invariant)', () => {
155
+ const h = mkHome();
156
+ try {
157
+ const badRec = {
158
+ ts: '2026-05-19T00:00:00Z',
159
+ source: 'mindrian-brain',
160
+ fingerprint: 'abc1234567890def',
161
+ action: 'removed',
162
+ key: 'c859eb6d-1234-5678-9abc-def012345678',
163
+ };
164
+ assert.throws(() => snap.appendMigrationLog(h, badRec), /raw user identifier/);
165
+ } finally {
166
+ rmHome(h);
167
+ }
168
+ });
169
+
170
+ console.log('');
171
+ console.log('==== RESULTS ====');
172
+ console.log('PASS: ' + pass);
173
+ console.log('FAIL: ' + fail);
174
+ process.exit(fail === 0 ? 0 : 1);
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /*
5
+ * Phase 127-00 (Task 2 RED) -- mindrian-brain stdio shim static + spawn tests.
6
+ *
7
+ * Covers the static / shape / spawn tests for bin/mindrian-brain-mcp-client.cjs:
8
+ * Test 1: shim file exists and is executable
9
+ * Test 2: shim source registers exactly 6 tools
10
+ * Test 3: shim child process boots and writes startup line to stderr (<3s)
11
+ * Test 7: shim source contains zero fetch/http/brain.mindrian/https? matches
12
+ * Test 8: shim source contains zero sendPacket calls (no Phase 110 bypass)
13
+ * Test 9: shim starts with #!/usr/bin/env node shebang
14
+ *
15
+ * Tests 4/5/6 (live JSON-RPC handshake + Tier-0 protocol) land in
16
+ * tests/test-127-00-shim-handshake.sh (Task 3).
17
+ *
18
+ * Canon parts: 7 (reuse of brain-client; thin transport wrapper),
19
+ * 8 (delegation property: zero network surface in the shim itself).
20
+ *
21
+ * HARD RULE: no em-dashes.
22
+ */
23
+
24
+ const assert = require('node:assert/strict');
25
+ const fs = require('node:fs');
26
+ const path = require('node:path');
27
+ const cp = require('node:child_process');
28
+
29
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
30
+ const SHIM_PATH = path.join(REPO_ROOT, 'bin', 'mindrian-brain-mcp-client.cjs');
31
+
32
+ let passed = 0;
33
+ let failed = 0;
34
+
35
+ function ok(name) {
36
+ passed += 1;
37
+ process.stdout.write(' ok ' + name + '\n');
38
+ }
39
+
40
+ function fail(name, err) {
41
+ failed += 1;
42
+ process.stdout.write(' FAIL ' + name + '\n');
43
+ if (err) process.stdout.write(' ' + (err.message || String(err)) + '\n');
44
+ }
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Test 1: shim file exists and is executable.
48
+ // ---------------------------------------------------------------------------
49
+ (function test1_executable() {
50
+ const label = 'shim file exists and is executable (mode & 0o111)';
51
+ try {
52
+ assert.ok(fs.existsSync(SHIM_PATH), 'shim file must exist at ' + SHIM_PATH);
53
+ const st = fs.statSync(SHIM_PATH);
54
+ assert.ok((st.mode & 0o111) !== 0,
55
+ 'shim file must be executable; mode = 0' + (st.mode & 0o777).toString(8));
56
+ ok(label);
57
+ } catch (err) { fail(label, err); }
58
+ })();
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Test 2: exactly 6 tools registered (brain_ask/query/schema/search/stats/write).
62
+ // ---------------------------------------------------------------------------
63
+ (function test2_sixTools() {
64
+ const label = 'shim source registers exactly 6 tools (brain_ask|query|schema|search|stats|write)';
65
+ try {
66
+ const src = fs.readFileSync(SHIM_PATH, 'utf8');
67
+ const re = /server\.tool\(\s*['"]brain_(ask|query|schema|search|stats|write)['"]/g;
68
+ const matches = src.match(re) || [];
69
+ assert.equal(matches.length, 6,
70
+ 'expected exactly 6 server.tool() registrations; got ' + matches.length);
71
+ // Each of the 6 distinct names must appear exactly once. Use a per-name
72
+ // re-scan because the match string spans newlines under the canonical
73
+ // multi-line server.tool() formatting.
74
+ const expected = ['brain_ask', 'brain_query', 'brain_schema', 'brain_search', 'brain_stats', 'brain_write'];
75
+ for (const n of expected) {
76
+ const perName = new RegExp('server\\.tool\\(\\s*[\'"]' + n + '[\'"]', 'g');
77
+ const m = src.match(perName) || [];
78
+ assert.equal(m.length, 1,
79
+ 'tool name ' + n + ' must appear exactly once; got ' + m.length);
80
+ }
81
+ ok(label);
82
+ } catch (err) { fail(label, err); }
83
+ })();
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Test 3: child process boot writes startup line to stderr within 3s.
87
+ // ---------------------------------------------------------------------------
88
+ (function test3_bootStartupLine() {
89
+ const label = 'shim spawn writes "[mindrian-brain] MCP server v... started (stdio)" to stderr within 3s';
90
+ // Async test wrapped in IIFE; promise chain with timeout.
91
+ const done = new Promise((resolve) => {
92
+ const env = Object.assign({}, process.env);
93
+ delete env.MINDRIAN_BRAIN_KEY;
94
+ const proc = cp.spawn('node', [SHIM_PATH], { env: env, stdio: ['pipe', 'pipe', 'pipe'] });
95
+ let stderr = '';
96
+ let resolved = false;
97
+ proc.stderr.on('data', (chunk) => {
98
+ stderr += chunk.toString('utf8');
99
+ if (/\[mindrian-brain\] MCP server v[^ ]+ started \(stdio\)/.test(stderr)) {
100
+ if (!resolved) {
101
+ resolved = true;
102
+ proc.kill('SIGTERM');
103
+ resolve({ ok: true, stderr: stderr });
104
+ }
105
+ }
106
+ });
107
+ proc.on('error', (err) => {
108
+ if (!resolved) { resolved = true; resolve({ ok: false, error: err }); }
109
+ });
110
+ setTimeout(() => {
111
+ if (!resolved) {
112
+ resolved = true;
113
+ proc.kill('SIGKILL');
114
+ resolve({ ok: false, error: new Error('timeout 3s; stderr=' + stderr) });
115
+ }
116
+ }, 3000);
117
+ });
118
+ // Block on the promise via a synchronous loop is wrong; use an
119
+ // async test runner. Use top-level await via an immediately-invoked
120
+ // async function with a process exit deferral.
121
+ done.then((r) => {
122
+ try {
123
+ assert.ok(r.ok, r.error ? r.error.message : 'startup line not seen');
124
+ ok(label);
125
+ } catch (err) { fail(label, err); }
126
+ finalize();
127
+ });
128
+ })();
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Test 7: zero Brain network surface in shim source (Canon Part 8 delegation).
132
+ //
133
+ // Token set aligned with the orchestrator's load-bearing success criterion:
134
+ // grep -rE "fetch\(|http\.|brain\.mindrian|onrender" bin/mindrian-brain-mcp-client.cjs
135
+ // returns 0. Note: the plan's <behavior> Test 7 narrative also lists
136
+ // `https?://`, but the plan's <action> mandates the Tier-0 sentinel embed
137
+ // the upgrade-hint URL https://mindrianos.vercel.app/brain-access verbatim --
138
+ // a self-contradicting test specification (Rule 2 deviation). The intent of
139
+ // Canon Part 8 is to catch Brain-network egress in the shim, not user-facing
140
+ // upgrade-hint URLs in a sentinel string. We therefore enforce the
141
+ // orchestrator's load-bearing token set, which is the canon-faithful read.
142
+ // ---------------------------------------------------------------------------
143
+ (function test7_delegationProperty() {
144
+ const label = 'shim source contains zero Brain network surface (fetch / http. / brain.mindrian / onrender)';
145
+ try {
146
+ const src = fs.readFileSync(SHIM_PATH, 'utf8');
147
+ const forbidden = [
148
+ { re: /fetch\(/g, name: 'fetch(' },
149
+ { re: /\bhttp\./g, name: 'http.' },
150
+ { re: /brain\.mindrian/g, name: 'brain.mindrian' },
151
+ { re: /\bonrender\b/g, name: 'onrender' },
152
+ ];
153
+ // Active-code scan: strip block comments and line comments. The doc
154
+ // header explicitly names the forbidden tokens as PROHIBITED; the scan
155
+ // is against ACTIVE code, not documentation.
156
+ let code = src.replace(/\/\*[\s\S]*?\*\//g, '');
157
+ code = code.replace(/^[ \t]*\/\/.*$/gm, '');
158
+ for (const f of forbidden) {
159
+ const m = code.match(f.re) || [];
160
+ assert.equal(m.length, 0,
161
+ 'forbidden Brain network surface "' + f.name + '" found ' + m.length + ' time(s) in active code');
162
+ }
163
+ ok(label);
164
+ } catch (err) { fail(label, err); }
165
+ })();
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Test 8: zero sendPacket calls (no Phase 110 typed-packet bypass).
169
+ // ---------------------------------------------------------------------------
170
+ (function test8_noSendPacketBypass() {
171
+ const label = 'shim source contains zero sendPacket( / buildBrainPacket calls (Phase 110 contract)';
172
+ try {
173
+ const src = fs.readFileSync(SHIM_PATH, 'utf8');
174
+ let code = src.replace(/\/\*[\s\S]*?\*\//g, '');
175
+ code = code.replace(/^[ \t]*\/\/.*$/gm, '');
176
+ const sp = code.match(/\bsendPacket\(/g) || [];
177
+ const bp = code.match(/\bbuildBrainPacket\b/g) || [];
178
+ const pt = code.match(/\{\s*packet_type\s*:/g) || [];
179
+ assert.equal(sp.length, 0, 'sendPacket( bypass detected ' + sp.length + ' time(s)');
180
+ assert.equal(bp.length, 0, 'buildBrainPacket bypass detected ' + bp.length + ' time(s)');
181
+ assert.equal(pt.length, 0, 'inline { packet_type: } construction detected ' + pt.length + ' time(s)');
182
+ ok(label);
183
+ } catch (err) { fail(label, err); }
184
+ })();
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Test 9: shebang line.
188
+ // ---------------------------------------------------------------------------
189
+ (function test9_shebang() {
190
+ const label = 'shim file starts with #!/usr/bin/env node shebang';
191
+ try {
192
+ const src = fs.readFileSync(SHIM_PATH, 'utf8');
193
+ const firstLine = src.split('\n')[0];
194
+ assert.equal(firstLine, '#!/usr/bin/env node',
195
+ 'first line must be "#!/usr/bin/env node"; got "' + firstLine + '"');
196
+ ok(label);
197
+ } catch (err) { fail(label, err); }
198
+ })();
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // Async finalize: wait for Test 3's spawn promise before printing summary.
202
+ // ---------------------------------------------------------------------------
203
+ let finalized = false;
204
+ function finalize() {
205
+ if (finalized) return;
206
+ finalized = true;
207
+ process.stdout.write('\n');
208
+ process.stdout.write('PASSED: ' + passed + '\n');
209
+ process.stdout.write('FAILED: ' + failed + '\n');
210
+ process.exit(failed === 0 ? 0 : 1);
211
+ }
212
+
213
+ // Safety net: in case Test 3's promise never resolves, finalize at 5s.
214
+ setTimeout(finalize, 5000).unref();
@@ -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.