@evomap/evolver 1.84.0 → 1.84.2

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 (85) hide show
  1. package/assets/gep/genes.seed.json +17 -15
  2. package/index.js +52 -16
  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/atp/hubClient.js +3 -1
  14. package/src/config.js +20 -1
  15. package/src/evolve/guards.js +1 -1
  16. package/src/evolve/pipeline/collect.js +1 -1
  17. package/src/evolve/pipeline/dispatch.js +1 -1
  18. package/src/evolve/pipeline/enrich.js +1 -1
  19. package/src/evolve/pipeline/hub.js +1 -1
  20. package/src/evolve/pipeline/select.js +1 -1
  21. package/src/evolve/pipeline/signals.js +1 -1
  22. package/src/evolve/utils.js +1 -1
  23. package/src/evolve.js +1 -1
  24. package/src/forceUpdate.js +5 -21
  25. package/src/gep/a2aProtocol.js +1 -1
  26. package/src/gep/assetStore.js +27 -6
  27. package/src/gep/candidateEval.js +1 -1
  28. package/src/gep/candidates.js +1 -1
  29. package/src/gep/contentHash.js +1 -1
  30. package/src/gep/crypto.js +1 -1
  31. package/src/gep/curriculum.js +1 -1
  32. package/src/gep/deviceId.js +1 -1
  33. package/src/gep/directoryClient.js +4 -3
  34. package/src/gep/envFingerprint.js +1 -1
  35. package/src/gep/epigenetics.js +1 -1
  36. package/src/gep/explore.js +1 -1
  37. package/src/gep/gitOps.js +0 -5
  38. package/src/gep/hash.js +1 -1
  39. package/src/gep/hubFetch.js +1 -0
  40. package/src/gep/hubReview.js +1 -1
  41. package/src/gep/hubSearch.js +1 -1
  42. package/src/gep/hubVerify.js +1 -1
  43. package/src/gep/learningSignals.js +1 -1
  44. package/src/gep/mailboxTransport.js +8 -5
  45. package/src/gep/memoryGraph.js +1 -1
  46. package/src/gep/memoryGraphAdapter.js +1 -1
  47. package/src/gep/mutation.js +1 -1
  48. package/src/gep/narrativeMemory.js +1 -1
  49. package/src/gep/openPRRegistry.js +1 -1
  50. package/src/gep/personality.js +1 -1
  51. package/src/gep/policyCheck.js +1 -1
  52. package/src/gep/prompt.js +1 -1
  53. package/src/gep/recallVerifier.js +1 -1
  54. package/src/gep/reflection.js +1 -1
  55. package/src/gep/sanitize.js +2 -1
  56. package/src/gep/schemas/gene.js +70 -1
  57. package/src/gep/schemas/protocol.js +9 -1
  58. package/src/gep/selector.js +1 -1
  59. package/src/gep/selfPR.js +62 -34
  60. package/src/gep/skillDistiller.js +1 -1
  61. package/src/gep/skillPublisher.js +3 -2
  62. package/src/gep/solidify.js +1 -1
  63. package/src/gep/strategy.js +1 -1
  64. package/src/gep/taskReceiver.js +6 -5
  65. package/src/gep/validator/index.js +10 -6
  66. package/src/gep/validator/reporter.js +2 -1
  67. package/src/gep/validator/stakeBootstrap.js +2 -1
  68. package/src/ops/health_check.js +1 -11
  69. package/src/ops/lifecycle.js +1 -3
  70. package/src/proxy/index.js +69 -0
  71. package/src/proxy/lifecycle/manager.js +3 -2
  72. package/src/proxy/router/cache_passthrough.js +26 -0
  73. package/src/proxy/router/features.js +84 -0
  74. package/src/proxy/router/messages_route.js +242 -0
  75. package/src/proxy/router/model_router.js +113 -0
  76. package/src/proxy/server/http.js +108 -6
  77. package/src/proxy/server/routes.js +12 -2
  78. package/src/proxy/server/settings.js +43 -10
  79. package/src/proxy/sync/inbound.js +3 -2
  80. package/src/proxy/sync/outbound.js +2 -1
  81. package/src/webui/observer/interactions.js +22 -16
  82. package/scripts/check_wrapper_compat.js +0 -113
  83. package/src/gep/.integrity +0 -0
  84. package/src/gep/integrityCheck.js +0 -1
  85. package/src/gep/shield.js +0 -1
@@ -51,7 +51,7 @@ function mergeJsonFile(filePath, patch, { markerKey = '_evolver_managed' } = {})
51
51
  if (raw) existing = JSON.parse(raw);
52
52
  }
53
53
  } catch { /* start fresh */ }
54
- const merged = deepMerge(existing, patch);
54
+ const merged = mergeWithHooksUnion(existing, patch);
55
55
  merged[markerKey] = true;
56
56
  const tmp = filePath + '.tmp';
57
57
  fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + '\n', 'utf8');
@@ -59,6 +59,48 @@ function mergeJsonFile(filePath, patch, { markerKey = '_evolver_managed' } = {})
59
59
  return merged;
60
60
  }
61
61
 
62
+ // Like deepMerge, but for `hooks.<event>` arrays specifically: instead of
63
+ // replacing the user's existing entries, keep them and append/refresh evolver-
64
+ // owned entries (matched by command containing `evolver-session/-signal`).
65
+ // This preserves user-installed Stop/SessionStart hooks (#539) while still
66
+ // updating evolver hooks across reinstalls.
67
+ function mergeWithHooksUnion(target, source) {
68
+ const result = deepMerge(target, source);
69
+ if (
70
+ target && target.hooks && typeof target.hooks === 'object' &&
71
+ source && source.hooks && typeof source.hooks === 'object'
72
+ ) {
73
+ for (const event of Object.keys(source.hooks)) {
74
+ const tArr = Array.isArray(target.hooks[event]) ? target.hooks[event] : null;
75
+ const sArr = Array.isArray(source.hooks[event]) ? source.hooks[event] : null;
76
+ if (tArr && sArr) {
77
+ const isEvolverOwned = (entry) => {
78
+ const cmds = collectCommands(entry);
79
+ return cmds.some(c => c.includes('evolver-session') || c.includes('evolver-signal'));
80
+ };
81
+ const userEntries = tArr.filter(e => !isEvolverOwned(e));
82
+ result.hooks[event] = [...userEntries, ...sArr];
83
+ }
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+
89
+ // Pull all `command` strings out of an event entry, supporting both flat
90
+ // shape (Codex: `{type, command}`) and Claude Code matcher shape
91
+ // (`{matcher, hooks: [{type, command}]}`). Returns [] when neither applies.
92
+ function collectCommands(entry) {
93
+ if (!entry || typeof entry !== 'object') return [];
94
+ const out = [];
95
+ if (typeof entry.command === 'string') out.push(entry.command);
96
+ if (Array.isArray(entry.hooks)) {
97
+ for (const h of entry.hooks) {
98
+ if (h && typeof h.command === 'string') out.push(h.command);
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+
62
104
  function deepMerge(target, source) {
63
105
  const result = { ...target };
64
106
  for (const key of Object.keys(source)) {
@@ -74,9 +116,49 @@ function deepMerge(target, source) {
74
116
  return result;
75
117
  }
76
118
 
119
+ // Refuse to write/read through a symbolic link at the adapter's
120
+ // platform config dir (`<root>/.codex`, `<root>/.claude`, …) or any
121
+ // nested adapter-owned subdir (`hooks/`, `plugins/`, …). A
122
+ // repository-controlled symlink at any of these paths would let
123
+ // install/uninstall writes land on attacker-chosen files outside the
124
+ // workspace (PR #94 round-4 surfaced the top-level case; round-5
125
+ // surfaced that a hostile repo can keep `.codex` real and only
126
+ // symlink `.codex/hooks`). Missing dirs are fine — install will
127
+ // create them.
128
+ function assertSafeConfigDir(dir, label, { subdirs = [] } = {}) {
129
+ assertNotSymlink(dir, label || 'config dir');
130
+ for (const sub of subdirs) {
131
+ assertNotSymlink(path.join(dir, sub), `${label || 'config dir'}/${sub}`);
132
+ }
133
+ }
134
+
135
+ function assertNotSymlink(p, label) {
136
+ let st;
137
+ try {
138
+ st = fs.lstatSync(p);
139
+ } catch (e) {
140
+ if (e && e.code === 'ENOENT') return;
141
+ throw e;
142
+ }
143
+ if (st.isSymbolicLink()) {
144
+ throw new Error(
145
+ `[setup-hooks] Refusing to operate: ${label} ${p} is a ` +
146
+ `symbolic link. evolver will not follow symlinks for ` +
147
+ `adapter-owned dirs — a hostile workspace could redirect ` +
148
+ `writes/unlinks outside the project root. Replace it with a ` +
149
+ `real directory and rerun.`
150
+ );
151
+ }
152
+ }
153
+
77
154
  function copyHookScripts(destDir, evolverRoot) {
78
155
  const scriptsDir = path.join(evolverRoot || __dirname, 'scripts');
156
+ // _runtimePaths.js is required by the two session-* scripts via
157
+ // `require('./_runtimePaths')`, which resolves relative to the *destination*
158
+ // (__dirname after copy). It MUST be copied alongside or both hooks crash
159
+ // with MODULE_NOT_FOUND at runtime. Caught in PR #94 review.
79
160
  const scripts = [
161
+ '_runtimePaths.js',
80
162
  'evolver-session-start.js',
81
163
  'evolver-signal-detect.js',
82
164
  'evolver-session-end.js',
@@ -90,6 +172,13 @@ function copyHookScripts(destDir, evolverRoot) {
90
172
  console.warn(`[setup-hooks] Warning: script not found: ${src}`);
91
173
  continue;
92
174
  }
175
+ // PR #94 round-6 HIGH: reject if the destination is a pre-planted
176
+ // symlink. fs.copyFileSync follows symlinks at the destination, so
177
+ // a hostile repo that pre-creates `.codex/hooks/evolver-session-end.js`
178
+ // pointing at e.g. `~/.bashrc` would have its target overwritten with
179
+ // evolver script content. Round-5 closed the directory hole; this
180
+ // closes the per-file hole.
181
+ assertNotSymlink(dest, `hook destination ${name}`);
93
182
  fs.copyFileSync(src, dest);
94
183
  try { fs.chmodSync(dest, 0o755); } catch { /* windows */ }
95
184
  copied.push(dest);
@@ -147,6 +236,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
147
236
 
148
237
  function removeHookScripts(hooksDir) {
149
238
  const scripts = [
239
+ '_runtimePaths.js',
150
240
  'evolver-session-start.js',
151
241
  'evolver-signal-detect.js',
152
242
  'evolver-session-end.js',
@@ -156,11 +246,56 @@ function removeHookScripts(hooksDir) {
156
246
  const p = path.join(hooksDir, name);
157
247
  try {
158
248
  if (fs.existsSync(p)) { fs.unlinkSync(p); removed++; }
159
- } catch { /* ignore */ }
249
+ } catch (e) {
250
+ // Surface unlink failures so users can see why a "successful"
251
+ // uninstall left files behind (Windows file-locking, perms, …).
252
+ console.warn(`[setup-hooks] Failed to remove ${p}: ${e.message || e}`);
253
+ }
160
254
  }
161
255
  return removed;
162
256
  }
163
257
 
258
+ // Remove a marker-bracketed section from a markdown file. Used by adapter
259
+ // uninstall to clean up CLAUDE.md / AGENTS.md without nuking surrounding
260
+ // user content.
261
+ //
262
+ // The previous inline implementations (codex/claude/kiro/opencode) searched
263
+ // for the *next* `\n## ` after the marker, which matched evolver's own
264
+ // `## Evolution Memory` heading and left the entire injected section in
265
+ // place (#538). This helper skips any `## ` heading on the same line as the
266
+ // marker, then looks for the next H2 to know where the user's content
267
+ // resumes.
268
+ function removeMarkedSection(filePath, marker) {
269
+ try {
270
+ if (!fs.existsSync(filePath)) return false;
271
+ const raw = fs.readFileSync(filePath, 'utf8');
272
+ const idx = raw.indexOf(marker);
273
+ if (idx === -1) return false;
274
+
275
+ // Skip past the marker line (and any heading on the same line).
276
+ let scanFrom = idx + marker.length;
277
+ const eol = raw.indexOf('\n', scanFrom);
278
+ if (eol !== -1) scanFrom = eol + 1;
279
+
280
+ // Skip past evolver's own `## ...` heading line if present.
281
+ if (raw.startsWith('## ', scanFrom)) {
282
+ const eol2 = raw.indexOf('\n', scanFrom);
283
+ scanFrom = eol2 !== -1 ? eol2 + 1 : raw.length;
284
+ }
285
+
286
+ const nextSection = raw.indexOf('\n## ', scanFrom);
287
+ const endIdx = nextSection !== -1 ? nextSection : raw.length;
288
+ const before = raw.slice(0, idx).trimEnd();
289
+ const after = nextSection !== -1 ? raw.slice(endIdx) : '';
290
+ const next = (before ? before + (after.startsWith('\n') ? '' : '\n') : '') + after;
291
+ fs.writeFileSync(filePath, next.trimEnd() + '\n', 'utf8');
292
+ return true;
293
+ } catch (e) {
294
+ console.warn(`[setup-hooks] Failed to clean section in ${filePath}: ${e.message || e}`);
295
+ return false;
296
+ }
297
+ }
298
+
164
299
  async function setupHooks({ platform, cwd, force, uninstall, evolverRoot } = {}) {
165
300
  const effectiveCwd = cwd || process.cwd();
166
301
  const effectiveEvolverRoot = evolverRoot || path.resolve(__dirname, '..');
@@ -200,10 +335,15 @@ module.exports = {
200
335
  loadAdapter,
201
336
  mergeJsonFile,
202
337
  deepMerge,
338
+ mergeWithHooksUnion,
339
+ collectCommands,
203
340
  copyHookScripts,
204
341
  appendSectionToFile,
342
+ assertSafeConfigDir,
343
+ assertNotSymlink,
205
344
  removeEvolverHooks,
206
345
  removeHookScripts,
346
+ removeMarkedSection,
207
347
  setupHooks,
208
348
  PLATFORMS,
209
349
  };
@@ -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 {
@@ -16,7 +16,7 @@
16
16
 
17
17
  const http = require('http');
18
18
  const { getHubUrl, buildHubHeaders, getNodeId } = require('../gep/a2aProtocol');
19
- const { getProxyUrl } = require('../proxy/server/settings');
19
+ const { getProxyUrl, getProxyToken } = require('../proxy/server/settings');
20
20
 
21
21
  function _isProxyMode() {
22
22
  if (process.env.EVOMAP_PROXY === '1') return true;
@@ -35,6 +35,8 @@ function _proxyRequest(method, path, body, timeoutMs) {
35
35
  const payload = body ? JSON.stringify(body) : '';
36
36
  const headers = { 'Content-Type': 'application/json' };
37
37
  if (payload) headers['Content-Length'] = Buffer.byteLength(payload);
38
+ const proxyToken = getProxyToken();
39
+ if (proxyToken) headers['Authorization'] = 'Bearer ' + proxyToken;
38
40
 
39
41
  const req = http.request(
40
42
  {
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 ---