@evomap/evolver 1.84.1 → 1.85.0

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 (73) hide show
  1. package/assets/gep/genes.seed.json +17 -15
  2. package/index.js +45 -8
  3. package/package.json +4 -3
  4. package/src/adapters/claudeCode.js +44 -31
  5. package/src/adapters/codex.js +70 -26
  6. package/src/adapters/cursor.js +3 -1
  7. package/src/adapters/hookAdapter.js +142 -2
  8. package/src/adapters/kiro.js +6 -14
  9. package/src/adapters/opencode.js +6 -14
  10. package/src/adapters/scripts/_runtimePaths.js +114 -0
  11. package/src/adapters/scripts/evolver-session-end.js +37 -61
  12. package/src/adapters/scripts/evolver-session-start.js +1 -31
  13. package/src/config.js +20 -1
  14. package/src/evolve/guards.js +1 -1
  15. package/src/evolve/pipeline/collect.js +1 -1
  16. package/src/evolve/pipeline/dispatch.js +1 -1
  17. package/src/evolve/pipeline/enrich.js +1 -1
  18. package/src/evolve/pipeline/hub.js +1 -1
  19. package/src/evolve/pipeline/select.js +1 -1
  20. package/src/evolve/pipeline/signals.js +1 -1
  21. package/src/evolve/utils.js +1 -1
  22. package/src/evolve.js +1 -1
  23. package/src/gep/a2aProtocol.js +1 -1
  24. package/src/gep/assetStore.js +27 -6
  25. package/src/gep/candidateEval.js +1 -1
  26. package/src/gep/candidates.js +1 -1
  27. package/src/gep/contentHash.js +1 -1
  28. package/src/gep/crypto.js +1 -1
  29. package/src/gep/curriculum.js +1 -1
  30. package/src/gep/deviceId.js +1 -1
  31. package/src/gep/directoryClient.js +4 -3
  32. package/src/gep/envFingerprint.js +1 -1
  33. package/src/gep/epigenetics.js +1 -1
  34. package/src/gep/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hubFetch.js +1 -0
  37. package/src/gep/hubReview.js +1 -1
  38. package/src/gep/hubSearch.js +1 -1
  39. package/src/gep/hubVerify.js +1 -1
  40. package/src/gep/learningSignals.js +1 -1
  41. package/src/gep/memoryGraph.js +1 -1
  42. package/src/gep/memoryGraphAdapter.js +1 -1
  43. package/src/gep/mutation.js +1 -1
  44. package/src/gep/narrativeMemory.js +1 -1
  45. package/src/gep/openPRRegistry.js +1 -1
  46. package/src/gep/personality.js +1 -1
  47. package/src/gep/policyCheck.js +1 -1
  48. package/src/gep/prompt.js +1 -1
  49. package/src/gep/recallVerifier.js +1 -1
  50. package/src/gep/reflection.js +1 -1
  51. package/src/gep/schemas/gene.js +70 -1
  52. package/src/gep/schemas/protocol.js +9 -1
  53. package/src/gep/selector.js +1 -1
  54. package/src/gep/selfPR.js +62 -32
  55. package/src/gep/skillDistiller.js +1 -1
  56. package/src/gep/skillPublisher.js +3 -2
  57. package/src/gep/solidify.js +1 -1
  58. package/src/gep/strategy.js +1 -1
  59. package/src/gep/taskReceiver.js +6 -5
  60. package/src/gep/validator/index.js +10 -6
  61. package/src/gep/validator/reporter.js +2 -1
  62. package/src/gep/validator/stakeBootstrap.js +2 -1
  63. package/src/proxy/index.js +69 -0
  64. package/src/proxy/lifecycle/manager.js +3 -2
  65. package/src/proxy/router/cache_passthrough.js +26 -0
  66. package/src/proxy/router/features.js +84 -0
  67. package/src/proxy/router/messages_route.js +242 -0
  68. package/src/proxy/router/model_router.js +113 -0
  69. package/src/proxy/server/http.js +92 -5
  70. package/src/proxy/server/routes.js +12 -2
  71. package/src/proxy/server/settings.js +37 -11
  72. package/src/proxy/sync/inbound.js +3 -2
  73. package/src/proxy/sync/outbound.js +2 -1
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { copyHookScripts, removeHookScripts } = require('./hookAdapter');
3
+ const { copyHookScripts, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
4
4
 
5
5
  const HOOK_SCRIPTS_DIR_NAME = 'hooks';
6
6
  const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
@@ -110,6 +110,7 @@ function install({ configRoot, evolverRoot, force }) {
110
110
  const hooksDir = path.join(kiroDir, HOOK_SCRIPTS_DIR_NAME);
111
111
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
112
112
  const scriptsBase = '.kiro/hooks';
113
+ assertSafeConfigDir(kiroDir, '.kiro', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
113
114
 
114
115
  const hookPaths = Object.values(HOOK_FILES).map(name => path.join(hooksDir, name));
115
116
 
@@ -153,6 +154,7 @@ function uninstall({ configRoot }) {
153
154
  const kiroDir = path.join(configRoot, '.kiro');
154
155
  const hooksDir = path.join(kiroDir, HOOK_SCRIPTS_DIR_NAME);
155
156
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
157
+ assertSafeConfigDir(kiroDir, '.kiro', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
156
158
 
157
159
  let changed = false;
158
160
  let removedCount = 0;
@@ -173,19 +175,9 @@ function uninstall({ configRoot }) {
173
175
  const scripts = removeHookScripts(hooksDir);
174
176
  if (scripts > 0) changed = true;
175
177
 
176
- try {
177
- if (fs.existsSync(agentsMdPath)) {
178
- let content = fs.readFileSync(agentsMdPath, 'utf8');
179
- if (content.includes(EVOLVER_MARKER)) {
180
- const idx = content.indexOf(EVOLVER_MARKER);
181
- const nextSection = content.indexOf('\n## ', idx + EVOLVER_MARKER.length);
182
- const endIdx = nextSection !== -1 ? nextSection : content.length;
183
- content = content.slice(0, idx).trimEnd() + (nextSection !== -1 ? content.slice(endIdx) : '');
184
- fs.writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf8');
185
- changed = true;
186
- }
187
- }
188
- } catch { /* ignore */ }
178
+ if (removeMarkedSection(agentsMdPath, EVOLVER_MARKER)) {
179
+ changed = true;
180
+ }
189
181
 
190
182
  console.log(changed
191
183
  ? `[kiro] Uninstalled evolver hooks (${removedCount} hook files + ${scripts} scripts removed).`
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { copyHookScripts, removeHookScripts } = require('./hookAdapter');
3
+ const { copyHookScripts, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
4
4
 
5
5
  const HOOK_SCRIPTS_DIR_NAME = 'hooks';
6
6
  const PLUGINS_DIR_NAME = 'plugins';
@@ -124,6 +124,7 @@ function install({ configRoot, evolverRoot, force }) {
124
124
  const pluginsDir = path.join(opencodeDir, PLUGINS_DIR_NAME);
125
125
  const pluginPath = path.join(pluginsDir, PLUGIN_FILE_NAME);
126
126
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
127
+ assertSafeConfigDir(opencodeDir, '.opencode', { subdirs: [HOOK_SCRIPTS_DIR_NAME, PLUGINS_DIR_NAME] });
127
128
 
128
129
  if (!force && isEvolverManagedPluginFile(pluginPath)) {
129
130
  console.log('[opencode] Evolver plugin already installed. Use --force to overwrite.');
@@ -292,6 +293,7 @@ function uninstall({ configRoot }) {
292
293
  const pluginsDir = path.join(opencodeDir, PLUGINS_DIR_NAME);
293
294
  const pluginPath = path.join(pluginsDir, PLUGIN_FILE_NAME);
294
295
  const agentsMdPath = path.join(configRoot, 'AGENTS.md');
296
+ assertSafeConfigDir(opencodeDir, '.opencode', { subdirs: [HOOK_SCRIPTS_DIR_NAME, PLUGINS_DIR_NAME] });
295
297
 
296
298
  let changed = false;
297
299
 
@@ -302,19 +304,9 @@ function uninstall({ configRoot }) {
302
304
  const scripts = removeHookScripts(hooksDir);
303
305
  if (scripts > 0) changed = true;
304
306
 
305
- try {
306
- if (fs.existsSync(agentsMdPath)) {
307
- let content = fs.readFileSync(agentsMdPath, 'utf8');
308
- if (content.includes(EVOLVER_MARKER)) {
309
- const idx = content.indexOf(EVOLVER_MARKER);
310
- const nextSection = content.indexOf('\n## ', idx + EVOLVER_MARKER.length);
311
- const endIdx = nextSection !== -1 ? nextSection : content.length;
312
- content = content.slice(0, idx).trimEnd() + (nextSection !== -1 ? content.slice(endIdx) : '');
313
- fs.writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf8');
314
- changed = true;
315
- }
316
- }
317
- } catch { /* ignore */ }
307
+ if (removeMarkedSection(agentsMdPath, EVOLVER_MARKER)) {
308
+ changed = true;
309
+ }
318
310
 
319
311
  console.log(changed
320
312
  ? '[opencode] Uninstalled evolver plugin and hooks.'
@@ -0,0 +1,114 @@
1
+ // _runtimePaths.js
2
+ // Shared path resolution for evolver hook scripts.
3
+ //
4
+ // Two responsibilities:
5
+ // 1. Locate the evolver package root, supporting:
6
+ // - $EVOLVER_ROOT explicit override
7
+ // - The "scripts colocated with src" layout used during dev (../../..)
8
+ // - The npm-global install layout, where the hook script lives under
9
+ // `<prefix>/lib/node_modules/<host>/.../hooks/` and `..` walks lead
10
+ // somewhere outside the evolver package. We resolve via
11
+ // `require.resolve('@evomap/evolver/package.json')` instead.
12
+ // - The `~/skills/evolver` fallback (some users symlink there).
13
+ //
14
+ // 2. Locate (or pick a writable default for) the evolution memory graph,
15
+ // so that hook scripts in environments without an evolver-managed
16
+ // project directory still record outcomes somewhere instead of
17
+ // reporting "nowhere (no Hub or local path)" (#536).
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const os = require('os');
22
+
23
+ function isEvolverPackageJson(filePath) {
24
+ try {
25
+ const raw = fs.readFileSync(filePath, 'utf8');
26
+ const pkg = JSON.parse(raw);
27
+ return pkg && (pkg.name === '@evomap/evolver' || pkg.name === 'evolver');
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function findEvolverRoot() {
34
+ if (process.env.EVOLVER_ROOT) {
35
+ const explicit = process.env.EVOLVER_ROOT;
36
+ if (fs.existsSync(path.join(explicit, 'package.json')) &&
37
+ isEvolverPackageJson(path.join(explicit, 'package.json'))) {
38
+ return explicit;
39
+ }
40
+ }
41
+
42
+ // Dev/repo layout: this file lives at src/adapters/scripts/_runtimePaths.js,
43
+ // so `../../..` is the package root.
44
+ const repoRoot = path.resolve(__dirname, '..', '..', '..');
45
+ if (fs.existsSync(path.join(repoRoot, 'package.json')) &&
46
+ isEvolverPackageJson(path.join(repoRoot, 'package.json'))) {
47
+ return repoRoot;
48
+ }
49
+
50
+ // npm-global / npm-local install layout. The hook script may have been
51
+ // copied out of the package into `.claude/hooks/` etc., breaking relative
52
+ // walks. Use require.resolve to find the installed package authoritatively.
53
+ //
54
+ // SECURITY: do NOT include `process.cwd()` here. A hostile workspace can
55
+ // place its own `node_modules/@evomap/evolver/package.json`, which would
56
+ // be selected here and control `findMemoryGraph()` -> the memory graph
57
+ // contents become attacker-controlled prompt-injection material in
58
+ // `evolver-session-start.js`'s `additionalContext`. Restrict to trusted,
59
+ // user/system-scoped install roots.
60
+ try {
61
+ const pkgJson = require.resolve('@evomap/evolver/package.json', {
62
+ paths: [
63
+ path.join(os.homedir(), '.npm-global', 'lib', 'node_modules'),
64
+ path.join(os.homedir(), '.local', 'lib', 'node_modules'),
65
+ '/usr/lib/node_modules',
66
+ '/usr/local/lib/node_modules',
67
+ ],
68
+ });
69
+ if (pkgJson && isEvolverPackageJson(pkgJson)) {
70
+ return path.dirname(pkgJson);
71
+ }
72
+ } catch { /* not installed via npm */ }
73
+
74
+ const homeSkills = path.join(os.homedir(), 'skills', 'evolver');
75
+ if (fs.existsSync(path.join(homeSkills, 'package.json')) &&
76
+ isEvolverPackageJson(path.join(homeSkills, 'package.json'))) {
77
+ return homeSkills;
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ // Returns a path to the evolution memory graph, or a fallback location that
84
+ // is guaranteed to be writable. Never returns null — when no evolver root is
85
+ // available, we fall back to `~/.evolver/memory/evolution/memory_graph.jsonl`
86
+ // so npm-global installs without a project-local evolver still capture
87
+ // outcomes (#536). Callers that need a "does the file already exist" check
88
+ // should use `fs.existsSync()` separately.
89
+ function findMemoryGraph(evolverRoot) {
90
+ if (process.env.MEMORY_GRAPH_PATH) {
91
+ return process.env.MEMORY_GRAPH_PATH;
92
+ }
93
+ if (evolverRoot) {
94
+ const lower = path.join(evolverRoot, 'memory', 'evolution', 'memory_graph.jsonl');
95
+ if (fs.existsSync(lower)) return lower;
96
+ const upper = path.join(evolverRoot, 'MEMORY', 'evolution', 'memory_graph.jsonl');
97
+ if (fs.existsSync(upper)) return upper;
98
+ // Neither exists yet — prefer lowercase under the evolver root if the
99
+ // root itself is writable (dev/local install case).
100
+ try {
101
+ fs.accessSync(evolverRoot, fs.constants.W_OK);
102
+ const dir = path.dirname(lower);
103
+ try { fs.mkdirSync(dir, { recursive: true }); } catch { /* fall through */ }
104
+ return lower;
105
+ } catch { /* not writable, fall through to user-level */ }
106
+ }
107
+
108
+ // User-level fallback. Always writable, consistent across platforms.
109
+ const userDir = path.join(os.homedir(), '.evolver', 'memory', 'evolution');
110
+ try { fs.mkdirSync(userDir, { recursive: true }); } catch { /* best-effort */ }
111
+ return path.join(userDir, 'memory_graph.jsonl');
112
+ }
113
+
114
+ module.exports = { findEvolverRoot, findMemoryGraph };
@@ -5,75 +5,51 @@
5
5
  // Input: stdin JSON. Output: stdout JSON with followup_message.
6
6
 
7
7
  const fs = require('fs');
8
- const path = require('path');
9
- const { execSync, spawnSync } = require('child_process');
8
+ const { spawnSync } = require('child_process');
10
9
  // 10 MB — prevents RangeError on large child process output (e.g. git log/diff
11
10
  // on large repos). See GHSA reports / issue #451.
12
11
  const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
13
12
 
14
-
15
- function findEvolverRoot() {
16
- const candidates = [
17
- process.env.EVOLVER_ROOT,
18
- path.resolve(__dirname, '..', '..', '..'),
19
- ];
20
- for (const c of candidates) {
21
- if (c && fs.existsSync(path.join(c, 'package.json'))) {
22
- try {
23
- const pkg = JSON.parse(fs.readFileSync(path.join(c, 'package.json'), 'utf8'));
24
- if (pkg.name === '@evomap/evolver' || pkg.name === 'evolver') return c;
25
- } catch { /* skip */ }
26
- }
27
- }
28
- const homeSkills = path.join(require('os').homedir(), 'skills', 'evolver');
29
- if (fs.existsSync(path.join(homeSkills, 'package.json'))) return homeSkills;
30
- return null;
31
- }
32
-
33
- function findMemoryGraph(evolverRoot) {
34
- if (process.env.MEMORY_GRAPH_PATH && fs.existsSync(process.env.MEMORY_GRAPH_PATH)) {
35
- return process.env.MEMORY_GRAPH_PATH;
36
- }
37
- const candidates = [
38
- evolverRoot && path.join(evolverRoot, 'memory', 'evolution', 'memory_graph.jsonl'),
39
- evolverRoot && path.join(evolverRoot, 'MEMORY', 'evolution', 'memory_graph.jsonl'),
40
- ];
41
- for (const c of candidates) {
42
- if (c && fs.existsSync(c)) return c;
43
- }
44
- if (evolverRoot) {
45
- const defaultPath = path.join(evolverRoot, 'memory', 'evolution', 'memory_graph.jsonl');
46
- fs.mkdirSync(path.dirname(defaultPath), { recursive: true });
47
- return defaultPath;
13
+ const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
14
+
15
+ function runGit(args, cwd) {
16
+ // Argv-array form, no shell. Avoids POSIX `2>/dev/null` redirects that
17
+ // break on Windows cmd.exe (#537). Failures (e.g. no HEAD~1 in a fresh
18
+ // repo) are surfaced as a non-zero status; callers distinguish them
19
+ // from successful empty output via the `ok` flag (PR #94 round-6 LOW).
20
+ const res = spawnSync('git', args, {
21
+ cwd,
22
+ encoding: 'utf8',
23
+ timeout: 5000,
24
+ maxBuffer: MAX_EXEC_BUFFER,
25
+ stdio: ['ignore', 'pipe', 'pipe'],
26
+ shell: false,
27
+ });
28
+ if (res.status === 0 && typeof res.stdout === 'string') {
29
+ return { ok: true, out: res.stdout.trim() };
48
30
  }
49
- return null;
31
+ return { ok: false, out: '' };
50
32
  }
51
33
 
52
34
  function getGitDiffStats() {
53
- try {
54
- const cwd = process.cwd();
55
- const stat = execSync('git diff --stat HEAD~1 2>/dev/null || git diff --stat 2>/dev/null || echo ""', {
56
- cwd,
57
- encoding: 'utf8',
58
- timeout: 5000, maxBuffer: MAX_EXEC_BUFFER
59
- }).trim();
60
- const diffContent = execSync('git diff HEAD~1 --no-color 2>/dev/null || git diff --no-color 2>/dev/null || echo ""', {
61
- cwd,
62
- encoding: 'utf8',
63
- timeout: 5000, maxBuffer: MAX_EXEC_BUFFER
64
- }).trim();
65
- const filesChanged = (stat.match(/\d+ files? changed/) || ['0'])[0];
66
- const insertions = (stat.match(/(\d+) insertions?/) || [null, '0'])[1];
67
- const deletions = (stat.match(/(\d+) deletions?/) || [null, '0'])[1];
68
- return {
69
- stat,
70
- summary: `${filesChanged}, +${insertions}/-${deletions}`,
71
- diffSnippet: diffContent.slice(0, 2000),
72
- hasChanges: stat.length > 0,
73
- };
74
- } catch {
75
- return { stat: '', summary: 'unknown', diffSnippet: '', hasChanges: false };
76
- }
35
+ const cwd = process.cwd();
36
+ // Distinguish "git failed (no HEAD~1, etc.)" from "git succeeded with
37
+ // empty output (e.g. empty merge)". The previous `||` chain treated
38
+ // both as falsy and fell through to the working-tree diff, which can
39
+ // surface unrelated unstaged changes as the session outcome.
40
+ const statHead1 = runGit(['diff', '--stat', 'HEAD~1'], cwd);
41
+ const stat = statHead1.ok ? statHead1.out : runGit(['diff', '--stat'], cwd).out;
42
+ const diffHead1 = runGit(['diff', '--no-color', 'HEAD~1'], cwd);
43
+ const diffContent = diffHead1.ok ? diffHead1.out : runGit(['diff', '--no-color'], cwd).out;
44
+ const filesChanged = (stat.match(/\d+ files? changed/) || ['0'])[0];
45
+ const insertions = (stat.match(/(\d+) insertions?/) || [null, '0'])[1];
46
+ const deletions = (stat.match(/(\d+) deletions?/) || [null, '0'])[1];
47
+ return {
48
+ stat,
49
+ summary: `${filesChanged}, +${insertions}/-${deletions}`,
50
+ diffSnippet: diffContent.slice(0, 2000),
51
+ hasChanges: stat.length > 0,
52
+ };
77
53
  }
78
54
 
79
55
  function detectSignals(text) {
@@ -7,37 +7,7 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
9
 
10
- function findEvolverRoot() {
11
- const candidates = [
12
- process.env.EVOLVER_ROOT,
13
- path.resolve(__dirname, '..', '..', '..'),
14
- ];
15
- for (const c of candidates) {
16
- if (c && fs.existsSync(path.join(c, 'package.json'))) {
17
- try {
18
- const pkg = JSON.parse(fs.readFileSync(path.join(c, 'package.json'), 'utf8'));
19
- if (pkg.name === '@evomap/evolver' || pkg.name === 'evolver') return c;
20
- } catch { /* skip */ }
21
- }
22
- }
23
- const homeSkills = path.join(require('os').homedir(), 'skills', 'evolver');
24
- if (fs.existsSync(path.join(homeSkills, 'package.json'))) return homeSkills;
25
- return null;
26
- }
27
-
28
- function findMemoryGraph(evolverRoot) {
29
- if (process.env.MEMORY_GRAPH_PATH && fs.existsSync(process.env.MEMORY_GRAPH_PATH)) {
30
- return process.env.MEMORY_GRAPH_PATH;
31
- }
32
- const candidates = [
33
- evolverRoot && path.join(evolverRoot, 'memory', 'evolution', 'memory_graph.jsonl'),
34
- evolverRoot && path.join(evolverRoot, 'MEMORY', 'evolution', 'memory_graph.jsonl'),
35
- ];
36
- for (const c of candidates) {
37
- if (c && fs.existsSync(c)) return c;
38
- }
39
- return null;
40
- }
10
+ const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
41
11
 
42
12
  function readLastN(filePath, n) {
43
13
  try {
package/src/config.js CHANGED
@@ -51,10 +51,29 @@ const HUB_SEARCH_TIMEOUT_MS = envInt('EVOLVER_HUB_SEARCH_TIMEOUT_MS', 8000);
51
51
  const PUBLIC_DEFAULT_HUB_URL = 'https://evomap.ai';
52
52
 
53
53
  function resolveHubUrl() {
54
- return process.env.A2A_HUB_URL
54
+ const raw = process.env.A2A_HUB_URL
55
55
  || process.env.EVOMAP_HUB_URL
56
56
  || process.env.EVOLVER_DEFAULT_HUB_URL
57
57
  || PUBLIC_DEFAULT_HUB_URL;
58
+
59
+ if (process.env.EVOMAP_HUB_ALLOW_INSECURE !== '1') {
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(raw);
63
+ } catch {
64
+ throw new Error(
65
+ '[config] Hub URL is not a valid URL: ' + JSON.stringify(raw) + '. ' +
66
+ 'Set EVOMAP_HUB_ALLOW_INSECURE=1 to bypass (local dev / mock hub only).'
67
+ );
68
+ }
69
+ if (parsed.protocol !== 'https:') {
70
+ throw new Error(
71
+ '[config] Hub URL must use https:// — got ' + JSON.stringify(raw) + '. ' +
72
+ 'Set EVOMAP_HUB_ALLOW_INSECURE=1 to bypass (local dev / mock hub only).'
73
+ );
74
+ }
75
+ }
76
+ return raw;
58
77
  }
59
78
 
60
79
  // --- Solidify & Validation ---