@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.
- package/assets/gep/genes.seed.json +17 -15
- package/index.js +45 -8
- 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/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/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/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/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/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 -32
- 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/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 +92 -5
- package/src/proxy/server/routes.js +12 -2
- package/src/proxy/server/settings.js +37 -11
- package/src/proxy/sync/inbound.js +3 -2
- package/src/proxy/sync/outbound.js +2 -1
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
"id": "gene_gep_repair_from_errors",
|
|
7
7
|
"category": "repair",
|
|
8
8
|
"signals_match": [
|
|
9
|
-
"error",
|
|
10
|
-
"exception",
|
|
11
|
-
"failed",
|
|
12
|
-
"unstable"
|
|
9
|
+
"error|错误|异常|エラー|오류",
|
|
10
|
+
"exception|异常|例外|예외",
|
|
11
|
+
"failed|失败|失敗|실패|fail",
|
|
12
|
+
"unstable|不稳定|不安定|불안정",
|
|
13
|
+
"log_error",
|
|
14
|
+
"test_failure"
|
|
13
15
|
],
|
|
14
16
|
"preconditions": [
|
|
15
17
|
"signals contains error-related indicators"
|
|
@@ -39,11 +41,11 @@
|
|
|
39
41
|
"id": "gene_gep_optimize_prompt_and_assets",
|
|
40
42
|
"category": "optimize",
|
|
41
43
|
"signals_match": [
|
|
42
|
-
"protocol",
|
|
44
|
+
"protocol|协议|プロトコル|프로토콜",
|
|
43
45
|
"gep",
|
|
44
|
-
"prompt",
|
|
45
|
-
"audit",
|
|
46
|
-
"reusable"
|
|
46
|
+
"prompt|提示词|提示|プロンプト|프롬프트",
|
|
47
|
+
"audit|审计|監査|감사",
|
|
48
|
+
"reusable|可复用|再利用|재사용"
|
|
47
49
|
],
|
|
48
50
|
"preconditions": [
|
|
49
51
|
"need stricter, auditable evolution protocol outputs"
|
|
@@ -73,12 +75,12 @@
|
|
|
73
75
|
"id": "gene_gep_innovate_from_opportunity",
|
|
74
76
|
"category": "innovate",
|
|
75
77
|
"signals_match": [
|
|
76
|
-
"user_feature_request",
|
|
77
|
-
"user_improvement_suggestion",
|
|
78
|
-
"perf_bottleneck",
|
|
79
|
-
"capability_gap",
|
|
78
|
+
"user_feature_request|功能请求|機能リクエスト|기능요청",
|
|
79
|
+
"user_improvement_suggestion|改进建议|改善提案|개선제안",
|
|
80
|
+
"perf_bottleneck|性能瓶颈|パフォーマンス|성능병목",
|
|
81
|
+
"capability_gap|能力缺口|機能ギャップ|역량공백",
|
|
80
82
|
"stable_success_plateau",
|
|
81
|
-
"external_opportunity",
|
|
83
|
+
"external_opportunity|外部机会|外部機会|외부기회",
|
|
82
84
|
"bounty_task"
|
|
83
85
|
],
|
|
84
86
|
"preconditions": [
|
|
@@ -114,8 +116,8 @@
|
|
|
114
116
|
"signals_match": [
|
|
115
117
|
"high_tool_usage:exec",
|
|
116
118
|
"repeated_tool_usage:exec",
|
|
117
|
-
"tool_bypass",
|
|
118
|
-
"tool_loop",
|
|
119
|
+
"tool_bypass|工具绕过|ツール迂回|도구우회",
|
|
120
|
+
"tool_loop|工具循环|ツールループ|도구반복",
|
|
119
121
|
"high_tool_usage"
|
|
120
122
|
],
|
|
121
123
|
"preconditions": [
|
package/index.js
CHANGED
|
@@ -187,8 +187,14 @@ function getLastSignals(statePath) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// Singleton Guard - prevent multiple evolver daemon instances
|
|
190
|
+
function getLockFilePath() {
|
|
191
|
+
// Allow tests / sandboxed runs to override the pid-file location so they
|
|
192
|
+
// do not collide with a real daemon's lock at the source-dir default.
|
|
193
|
+
const dir = process.env.EVOLVER_LOCK_DIR || __dirname;
|
|
194
|
+
return path.join(dir, 'evolver.pid');
|
|
195
|
+
}
|
|
190
196
|
function acquireLock() {
|
|
191
|
-
const lockFile =
|
|
197
|
+
const lockFile = getLockFilePath();
|
|
192
198
|
try {
|
|
193
199
|
try {
|
|
194
200
|
fs.writeFileSync(lockFile, String(process.pid), { flag: 'wx' });
|
|
@@ -217,7 +223,7 @@ function acquireLock() {
|
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
function releaseLock() {
|
|
220
|
-
const lockFile =
|
|
226
|
+
const lockFile = getLockFilePath();
|
|
221
227
|
try {
|
|
222
228
|
if (fs.existsSync(lockFile)) {
|
|
223
229
|
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
|
|
@@ -280,22 +286,53 @@ async function main() {
|
|
|
280
286
|
releaseLock();
|
|
281
287
|
process.exit(1);
|
|
282
288
|
});
|
|
283
|
-
|
|
289
|
+
// Sliding window: only exit if many rejections cluster in a short
|
|
290
|
+
// period. A daemon running for weeks can accumulate harmless,
|
|
291
|
+
// unrelated rejections (transient network blips, hub timeouts);
|
|
292
|
+
// the original cumulative counter would eventually kill the
|
|
293
|
+
// process for noise. Cluster = real failure cascade.
|
|
294
|
+
const REJECTION_WINDOW_MS = 5 * 60 * 1000;
|
|
295
|
+
const REJECTION_THRESHOLD = 5;
|
|
296
|
+
let _rejectionTimestamps = [];
|
|
284
297
|
process.on('unhandledRejection', (reason) => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
_rejectionTimestamps.push(now);
|
|
300
|
+
_rejectionTimestamps = _rejectionTimestamps.filter(function (t) {
|
|
301
|
+
return now - t < REJECTION_WINDOW_MS;
|
|
302
|
+
});
|
|
303
|
+
console.error('[FATAL] Unhandled promise rejection (' + _rejectionTimestamps.length + ' in window):', reason && reason.stack ? reason.stack : String(reason));
|
|
304
|
+
if (_rejectionTimestamps.length >= REJECTION_THRESHOLD) {
|
|
305
|
+
console.error('[FATAL] ' + _rejectionTimestamps.length + ' unhandled rejections within ' + (REJECTION_WINDOW_MS / 1000) + 's. Exiting to avoid corrupt state.');
|
|
289
306
|
releaseLock();
|
|
290
307
|
process.exit(1);
|
|
291
308
|
}
|
|
292
309
|
});
|
|
293
310
|
|
|
294
311
|
process.env.EVOLVE_LOOP = 'true';
|
|
312
|
+
// Issue #96: from v1.85.0, --loop defaults EVOLVE_BRIDGE=true so the
|
|
313
|
+
// daemon actually evolves the working tree. The previous default of
|
|
314
|
+
// 'false' caused 33 days of empty cycling on Aurora — every cycle
|
|
315
|
+
// hit rejectPendingRun(reason=loop_bridge_disabled_autoreject_no_rollback)
|
|
316
|
+
// and produced no EvolutionEvent. Failed cycles still recover safely
|
|
317
|
+
// via rollbackTracked (src/gep/gitOps.js#rollbackTracked, mode=stash
|
|
318
|
+
// by default since v1.81.0): the daemon's changes get pushed to a
|
|
319
|
+
// stash entry the user can recover with `git stash pop`.
|
|
320
|
+
// Set EVOLVE_BRIDGE=false explicitly to opt back into observe-only.
|
|
295
321
|
if (!process.env.EVOLVE_BRIDGE) {
|
|
296
|
-
process.env.EVOLVE_BRIDGE = '
|
|
322
|
+
process.env.EVOLVE_BRIDGE = 'true';
|
|
297
323
|
}
|
|
324
|
+
const bridgeEnabled = String(process.env.EVOLVE_BRIDGE).toLowerCase() !== 'false';
|
|
298
325
|
console.log(`Loop mode enabled (internal daemon, bridge=${process.env.EVOLVE_BRIDGE}, verbose=${isVerbose}).`);
|
|
326
|
+
if (bridgeEnabled) {
|
|
327
|
+
console.warn('[Daemon] EVOLVE_BRIDGE=true (default since v1.85.0).');
|
|
328
|
+
console.warn('[Daemon] evolver may modify your working tree.');
|
|
329
|
+
console.warn('[Daemon] Failed cycles auto-stash via "git stash push --include-untracked".');
|
|
330
|
+
console.warn('[Daemon] Recover: git stash list | grep evolver-rollback');
|
|
331
|
+
console.warn('[Daemon] Set EVOLVE_BRIDGE=false to opt out (observe-only mode).');
|
|
332
|
+
} else {
|
|
333
|
+
console.warn('[Daemon] EVOLVE_BRIDGE=false: evolver will NOT modify your working tree (observe-only).');
|
|
334
|
+
console.warn('[Daemon] To enable real evolution: unset EVOLVE_BRIDGE or set it to "true".');
|
|
335
|
+
}
|
|
299
336
|
|
|
300
337
|
// Startup diagnostic: in daemon mode evolver consumes its own stdout
|
|
301
338
|
// instead of handing `sessions_spawn(...)` directives to a host
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.85.0",
|
|
4
4
|
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
"node": ">=22.12"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@evomap/gep-sdk": "^1.
|
|
40
|
-
"dotenv": "^16.4.7"
|
|
39
|
+
"@evomap/gep-sdk": "^1.3.0",
|
|
40
|
+
"dotenv": "^16.4.7",
|
|
41
|
+
"undici": "^7.0.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"javascript-obfuscator": "^5.4.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts } = require('./hookAdapter');
|
|
3
|
+
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
|
|
4
4
|
|
|
5
5
|
const HOOK_SCRIPTS_DIR_NAME = 'hooks';
|
|
6
6
|
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
|
|
@@ -65,6 +65,7 @@ function install({ configRoot, evolverRoot, force }) {
|
|
|
65
65
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
66
66
|
const hooksDir = path.join(claudeDir, HOOK_SCRIPTS_DIR_NAME);
|
|
67
67
|
const claudeMdPath = path.join(configRoot, 'CLAUDE.md');
|
|
68
|
+
assertSafeConfigDir(claudeDir, '.claude', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
68
69
|
|
|
69
70
|
if (!force && fs.existsSync(settingsPath)) {
|
|
70
71
|
try {
|
|
@@ -104,54 +105,66 @@ function uninstall({ configRoot }) {
|
|
|
104
105
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
105
106
|
const hooksDir = path.join(claudeDir, HOOK_SCRIPTS_DIR_NAME);
|
|
106
107
|
const claudeMdPath = path.join(configRoot, 'CLAUDE.md');
|
|
108
|
+
assertSafeConfigDir(claudeDir, '.claude', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
107
109
|
|
|
108
110
|
let changed = false;
|
|
109
111
|
|
|
112
|
+
// Strip evolver entries from settings.json. Even without the marker we
|
|
113
|
+
// still try to filter by command — a missing/dropped marker should not
|
|
114
|
+
// strand obvious evolver-owned entries (#538).
|
|
110
115
|
try {
|
|
111
116
|
if (fs.existsSync(settingsPath)) {
|
|
112
117
|
const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
let touched = false;
|
|
119
|
+
if (data.hooks) {
|
|
120
|
+
for (const event of Object.keys(data.hooks)) {
|
|
121
|
+
if (Array.isArray(data.hooks[event])) {
|
|
122
|
+
const beforeLen = data.hooks[event].length;
|
|
123
|
+
data.hooks[event] = data.hooks[event]
|
|
124
|
+
.map(matcher => {
|
|
125
|
+
if (!matcher || !Array.isArray(matcher.hooks)) return matcher;
|
|
126
|
+
const innerBefore = matcher.hooks.length;
|
|
127
|
+
const filtered = matcher.hooks.filter(h => {
|
|
128
|
+
const cmd = (h && h.command) || '';
|
|
129
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
130
|
+
});
|
|
131
|
+
// A matcher containing both evolver and user hooks shrinks
|
|
132
|
+
// its inner array without changing the outer matcher count.
|
|
133
|
+
// Track the inner-array shrink so `touched` reflects it.
|
|
134
|
+
if (filtered.length !== innerBefore) touched = true;
|
|
135
|
+
return { ...matcher, hooks: filtered };
|
|
136
|
+
})
|
|
137
|
+
.filter(matcher => matcher && Array.isArray(matcher.hooks) && matcher.hooks.length > 0);
|
|
138
|
+
if (data.hooks[event].length !== beforeLen) touched = true;
|
|
139
|
+
if (data.hooks[event].length === 0) delete data.hooks[event];
|
|
129
140
|
}
|
|
130
|
-
if (Object.keys(data.hooks).length === 0) delete data.hooks;
|
|
131
141
|
}
|
|
142
|
+
if (Object.keys(data.hooks).length === 0) delete data.hooks;
|
|
143
|
+
}
|
|
144
|
+
if (data._evolver_managed) {
|
|
132
145
|
delete data._evolver_managed;
|
|
146
|
+
touched = true;
|
|
147
|
+
}
|
|
148
|
+
if (touched) {
|
|
133
149
|
fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
134
150
|
changed = true;
|
|
135
151
|
}
|
|
136
152
|
}
|
|
137
|
-
} catch {
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn(`[claude-code] Failed to clean ${settingsPath}: ${e.message || e}`);
|
|
155
|
+
}
|
|
138
156
|
|
|
139
157
|
const scripts = removeHookScripts(hooksDir);
|
|
140
158
|
if (scripts > 0) changed = true;
|
|
141
|
-
|
|
142
159
|
try {
|
|
143
|
-
if (fs.existsSync(
|
|
144
|
-
|
|
145
|
-
if (content.includes(EVOLVER_MARKER)) {
|
|
146
|
-
const idx = content.indexOf(EVOLVER_MARKER);
|
|
147
|
-
const nextSection = content.indexOf('\n## ', idx + EVOLVER_MARKER.length);
|
|
148
|
-
const endIdx = nextSection !== -1 ? nextSection : content.length;
|
|
149
|
-
content = content.slice(0, idx).trimEnd() + (nextSection !== -1 ? content.slice(endIdx) : '');
|
|
150
|
-
fs.writeFileSync(claudeMdPath, content.trimEnd() + '\n', 'utf8');
|
|
151
|
-
changed = true;
|
|
152
|
-
}
|
|
160
|
+
if (fs.existsSync(hooksDir) && fs.readdirSync(hooksDir).length === 0) {
|
|
161
|
+
fs.rmdirSync(hooksDir);
|
|
153
162
|
}
|
|
154
|
-
} catch { /*
|
|
163
|
+
} catch { /* best-effort */ }
|
|
164
|
+
|
|
165
|
+
if (removeMarkedSection(claudeMdPath, EVOLVER_MARKER)) {
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
155
168
|
|
|
156
169
|
console.log(changed
|
|
157
170
|
? '[claude-code] Uninstalled evolver hooks.'
|
package/src/adapters/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts } = require('./hookAdapter');
|
|
3
|
+
const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir } = require('./hookAdapter');
|
|
4
4
|
|
|
5
5
|
const HOOK_SCRIPTS_DIR_NAME = 'hooks';
|
|
6
6
|
const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
|
|
@@ -57,6 +57,34 @@ function ensureConfigToml(codexDir) {
|
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Reverse of `ensureConfigToml`: drop the `codex_hooks = true` line and, if
|
|
61
|
+
// the surrounding `[features]` block becomes empty as a result, drop that
|
|
62
|
+
// header too. Other unrelated entries under `[features]` are preserved.
|
|
63
|
+
// Returns true when the file changed.
|
|
64
|
+
function cleanConfigToml(codexDir) {
|
|
65
|
+
const tomlPath = path.join(codexDir, 'config.toml');
|
|
66
|
+
let content;
|
|
67
|
+
try { content = fs.readFileSync(tomlPath, 'utf8'); } catch { return false; }
|
|
68
|
+
if (!/codex_hooks\s*=\s*true/i.test(content)) return false;
|
|
69
|
+
|
|
70
|
+
// Drop the `codex_hooks = true` line. The greedy `\s*` after `true`
|
|
71
|
+
// consumes the trailing newline plus any blank lines so the
|
|
72
|
+
// empty-`[features]` check below cannot be fooled by a stray blank
|
|
73
|
+
// line into treating the section as empty while user entries still
|
|
74
|
+
// follow.
|
|
75
|
+
let next = content.replace(/^\s*codex_hooks\s*=\s*true\s*\n?/im, '');
|
|
76
|
+
// Drop a now-empty `[features]` block. Two strict patterns avoid
|
|
77
|
+
// `$` with the /m flag — multiline `$` matches before any `\n`, so
|
|
78
|
+
// a single `(?=\s*$)` lookahead can succeed mid-file and strand
|
|
79
|
+
// user entries below the removed header (PR #94 round-3).
|
|
80
|
+
next = next.replace(/(^|\n)\[features\]\s*\n(?=\s*\[)/, '$1');
|
|
81
|
+
next = next.replace(/(^|\n)\[features\]\s*$/, '$1');
|
|
82
|
+
next = next.replace(/\n{3,}/g, '\n\n').trimEnd();
|
|
83
|
+
if (next.length > 0) next += '\n';
|
|
84
|
+
fs.writeFileSync(tomlPath, next, 'utf8');
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
function buildAgentsMdSection() {
|
|
61
89
|
return `${EVOLVER_MARKER}
|
|
62
90
|
## Evolution Memory (Evolver)
|
|
@@ -75,6 +103,7 @@ function install({ configRoot, evolverRoot, force }) {
|
|
|
75
103
|
const hooksJsonPath = path.join(codexDir, 'hooks.json');
|
|
76
104
|
const hooksDir = path.join(codexDir, HOOK_SCRIPTS_DIR_NAME);
|
|
77
105
|
const agentsMdPath = path.join(configRoot, 'AGENTS.md');
|
|
106
|
+
assertSafeConfigDir(codexDir, '.codex', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
78
107
|
|
|
79
108
|
if (!force && fs.existsSync(hooksJsonPath)) {
|
|
80
109
|
try {
|
|
@@ -119,48 +148,63 @@ function uninstall({ configRoot }) {
|
|
|
119
148
|
const hooksJsonPath = path.join(codexDir, 'hooks.json');
|
|
120
149
|
const hooksDir = path.join(codexDir, HOOK_SCRIPTS_DIR_NAME);
|
|
121
150
|
const agentsMdPath = path.join(configRoot, 'AGENTS.md');
|
|
151
|
+
assertSafeConfigDir(codexDir, '.codex', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
122
152
|
|
|
123
153
|
let changed = false;
|
|
124
154
|
|
|
155
|
+
// Strip evolver entries from hooks.json. Even when the
|
|
156
|
+
// `_evolver_managed` marker is missing (older install, hand-edited
|
|
157
|
+
// file), we still try to filter by command — a missing marker should
|
|
158
|
+
// not strand obvious evolver-owned entries (#538).
|
|
125
159
|
try {
|
|
126
160
|
if (fs.existsSync(hooksJsonPath)) {
|
|
127
161
|
const data = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
162
|
+
let touched = false;
|
|
163
|
+
if (data.hooks) {
|
|
164
|
+
for (const event of Object.keys(data.hooks)) {
|
|
165
|
+
if (Array.isArray(data.hooks[event])) {
|
|
166
|
+
const before = data.hooks[event].length;
|
|
167
|
+
data.hooks[event] = data.hooks[event].filter(h => {
|
|
168
|
+
const cmd = (h && h.command) || '';
|
|
169
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
170
|
+
});
|
|
171
|
+
if (data.hooks[event].length !== before) touched = true;
|
|
172
|
+
if (data.hooks[event].length === 0) delete data.hooks[event];
|
|
138
173
|
}
|
|
139
|
-
if (Object.keys(data.hooks).length === 0) delete data.hooks;
|
|
140
174
|
}
|
|
175
|
+
if (Object.keys(data.hooks).length === 0) delete data.hooks;
|
|
176
|
+
}
|
|
177
|
+
if (data._evolver_managed) {
|
|
141
178
|
delete data._evolver_managed;
|
|
179
|
+
touched = true;
|
|
180
|
+
}
|
|
181
|
+
if (touched) {
|
|
142
182
|
fs.writeFileSync(hooksJsonPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
143
183
|
changed = true;
|
|
144
184
|
}
|
|
145
185
|
}
|
|
146
|
-
} catch {
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.warn(`[codex] Failed to clean ${hooksJsonPath}: ${e.message || e}`);
|
|
188
|
+
}
|
|
147
189
|
|
|
148
190
|
const scripts = removeHookScripts(hooksDir);
|
|
149
191
|
if (scripts > 0) changed = true;
|
|
150
|
-
|
|
192
|
+
// If hooks dir is now empty (only evolver scripts lived there), remove it
|
|
193
|
+
// so a subsequent install starts from a clean slate.
|
|
151
194
|
try {
|
|
152
|
-
if (fs.existsSync(
|
|
153
|
-
|
|
154
|
-
if (content.includes(EVOLVER_MARKER)) {
|
|
155
|
-
const idx = content.indexOf(EVOLVER_MARKER);
|
|
156
|
-
const nextSection = content.indexOf('\n## ', idx + EVOLVER_MARKER.length);
|
|
157
|
-
const endIdx = nextSection !== -1 ? nextSection : content.length;
|
|
158
|
-
content = content.slice(0, idx).trimEnd() + (nextSection !== -1 ? content.slice(endIdx) : '');
|
|
159
|
-
fs.writeFileSync(agentsMdPath, content.trimEnd() + '\n', 'utf8');
|
|
160
|
-
changed = true;
|
|
161
|
-
}
|
|
195
|
+
if (fs.existsSync(hooksDir) && fs.readdirSync(hooksDir).length === 0) {
|
|
196
|
+
fs.rmdirSync(hooksDir);
|
|
162
197
|
}
|
|
163
|
-
} catch { /*
|
|
198
|
+
} catch { /* best-effort */ }
|
|
199
|
+
|
|
200
|
+
if (cleanConfigToml(codexDir)) {
|
|
201
|
+
console.log('[codex] Removed codex_hooks flag from config.toml');
|
|
202
|
+
changed = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (removeMarkedSection(agentsMdPath, EVOLVER_MARKER)) {
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
164
208
|
|
|
165
209
|
console.log(changed
|
|
166
210
|
? '[codex] Uninstalled evolver hooks.'
|
|
@@ -169,4 +213,4 @@ function uninstall({ configRoot }) {
|
|
|
169
213
|
return { ok: true, removed: changed };
|
|
170
214
|
}
|
|
171
215
|
|
|
172
|
-
module.exports = { install, uninstall, buildCodexHooksJson, ensureConfigToml };
|
|
216
|
+
module.exports = { install, uninstall, buildCodexHooksJson, ensureConfigToml, cleanConfigToml };
|
package/src/adapters/cursor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { mergeJsonFile, copyHookScripts, removeEvolverHooks, removeHookScripts } = require('./hookAdapter');
|
|
3
|
+
const { mergeJsonFile, copyHookScripts, removeEvolverHooks, removeHookScripts, assertSafeConfigDir } = require('./hookAdapter');
|
|
4
4
|
|
|
5
5
|
const HOOK_SCRIPTS_DIR_NAME = 'hooks';
|
|
6
6
|
|
|
@@ -39,6 +39,7 @@ function install({ configRoot, evolverRoot, force }) {
|
|
|
39
39
|
const cursorDir = path.join(configRoot, '.cursor');
|
|
40
40
|
const hooksJsonPath = path.join(cursorDir, 'hooks.json');
|
|
41
41
|
const hooksDir = path.join(cursorDir, HOOK_SCRIPTS_DIR_NAME);
|
|
42
|
+
assertSafeConfigDir(cursorDir, '.cursor', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
42
43
|
|
|
43
44
|
if (!force && fs.existsSync(hooksJsonPath)) {
|
|
44
45
|
try {
|
|
@@ -73,6 +74,7 @@ function uninstall({ configRoot, evolverRoot }) {
|
|
|
73
74
|
const cursorDir = path.join(configRoot, '.cursor');
|
|
74
75
|
const hooksJsonPath = path.join(cursorDir, 'hooks.json');
|
|
75
76
|
const hooksDir = path.join(cursorDir, HOOK_SCRIPTS_DIR_NAME);
|
|
77
|
+
assertSafeConfigDir(cursorDir, '.cursor', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
|
|
76
78
|
|
|
77
79
|
const removed = removeEvolverHooks(hooksJsonPath);
|
|
78
80
|
const scripts = removeHookScripts(hooksDir);
|
|
@@ -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
|
};
|