@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.
- package/assets/gep/genes.seed.json +17 -15
- package/index.js +52 -16
- package/package.json +4 -3
- package/src/adapters/claudeCode.js +44 -31
- package/src/adapters/codex.js +70 -26
- package/src/adapters/cursor.js +3 -1
- package/src/adapters/hookAdapter.js +142 -2
- package/src/adapters/kiro.js +6 -14
- package/src/adapters/opencode.js +6 -14
- package/src/adapters/scripts/_runtimePaths.js +114 -0
- package/src/adapters/scripts/evolver-session-end.js +37 -61
- package/src/adapters/scripts/evolver-session-start.js +1 -31
- package/src/atp/hubClient.js +3 -1
- package/src/config.js +20 -1
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/forceUpdate.js +5 -21
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +27 -6
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/directoryClient.js +4 -3
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/gitOps.js +0 -5
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -0
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/mailboxTransport.js +8 -5
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +2 -1
- package/src/gep/schemas/gene.js +70 -1
- package/src/gep/schemas/protocol.js +9 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/selfPR.js +62 -34
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/skillPublisher.js +3 -2
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/taskReceiver.js +6 -5
- package/src/gep/validator/index.js +10 -6
- package/src/gep/validator/reporter.js +2 -1
- package/src/gep/validator/stakeBootstrap.js +2 -1
- package/src/ops/health_check.js +1 -11
- package/src/ops/lifecycle.js +1 -3
- package/src/proxy/index.js +69 -0
- package/src/proxy/lifecycle/manager.js +3 -2
- package/src/proxy/router/cache_passthrough.js +26 -0
- package/src/proxy/router/features.js +84 -0
- package/src/proxy/router/messages_route.js +242 -0
- package/src/proxy/router/model_router.js +113 -0
- package/src/proxy/server/http.js +108 -6
- package/src/proxy/server/routes.js +12 -2
- package/src/proxy/server/settings.js +43 -10
- package/src/proxy/sync/inbound.js +3 -2
- package/src/proxy/sync/outbound.js +2 -1
- package/src/webui/observer/interactions.js +22 -16
- package/scripts/check_wrapper_compat.js +0 -113
- package/src/gep/.integrity +0 -0
- package/src/gep/integrityCheck.js +0 -1
- 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 =
|
|
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 {
|
|
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
|
};
|
package/src/adapters/kiro.js
CHANGED
|
@@ -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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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).`
|
package/src/adapters/opencode.js
CHANGED
|
@@ -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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
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
|
|
31
|
+
return { ok: false, out: '' };
|
|
50
32
|
}
|
|
51
33
|
|
|
52
34
|
function getGitDiffStats() {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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/atp/hubClient.js
CHANGED
|
@@ -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
|
-
|
|
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 ---
|