@evomap/evolver 1.85.0 → 1.85.1

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 (45) hide show
  1. package/README.md +22 -1
  2. package/package.json +1 -1
  3. package/scripts/build_binaries.js +69 -12
  4. package/src/adapters/scripts/evolver-session-end.js +110 -15
  5. package/src/evolve/guards.js +1 -1
  6. package/src/evolve/pipeline/collect.js +1 -1
  7. package/src/evolve/pipeline/dispatch.js +1 -1
  8. package/src/evolve/pipeline/enrich.js +1 -1
  9. package/src/evolve/pipeline/hub.js +1 -1
  10. package/src/evolve/pipeline/select.js +1 -1
  11. package/src/evolve/pipeline/signals.js +1 -1
  12. package/src/evolve/utils.js +1 -1
  13. package/src/evolve.js +1 -1
  14. package/src/gep/a2aProtocol.js +1 -1
  15. package/src/gep/assetStore.js +17 -0
  16. package/src/gep/candidateEval.js +1 -1
  17. package/src/gep/candidates.js +1 -1
  18. package/src/gep/contentHash.js +1 -1
  19. package/src/gep/crypto.js +1 -1
  20. package/src/gep/curriculum.js +1 -1
  21. package/src/gep/deviceId.js +1 -1
  22. package/src/gep/envFingerprint.js +1 -1
  23. package/src/gep/epigenetics.js +1 -1
  24. package/src/gep/explore.js +1 -1
  25. package/src/gep/hash.js +1 -1
  26. package/src/gep/hubFetch.js +1 -1
  27. package/src/gep/hubReview.js +1 -1
  28. package/src/gep/hubSearch.js +1 -1
  29. package/src/gep/hubVerify.js +1 -1
  30. package/src/gep/learningSignals.js +1 -1
  31. package/src/gep/memoryGraph.js +1 -1
  32. package/src/gep/memoryGraphAdapter.js +1 -1
  33. package/src/gep/mutation.js +1 -1
  34. package/src/gep/narrativeMemory.js +1 -1
  35. package/src/gep/openPRRegistry.js +1 -1
  36. package/src/gep/paths.js +79 -0
  37. package/src/gep/personality.js +1 -1
  38. package/src/gep/policyCheck.js +1 -1
  39. package/src/gep/prompt.js +1 -1
  40. package/src/gep/recallVerifier.js +1 -1
  41. package/src/gep/reflection.js +1 -1
  42. package/src/gep/selector.js +1 -1
  43. package/src/gep/skillDistiller.js +1 -1
  44. package/src/gep/solidify.js +1 -1
  45. package/src/gep/strategy.js +1 -1
package/README.md CHANGED
@@ -136,11 +136,32 @@ Evolver integrates with major agent runtimes through `setup-hooks`. Run it once
136
136
  |---|---|---|
137
137
  | [Cursor](https://cursor.com) | `evolver setup-hooks --platform=cursor` | `~/.cursor/hooks.json` + scripts in `~/.cursor/hooks/`. Restart Cursor or open a new session. Fires on `sessionStart`, `afterFileEdit`, `stop`. |
138
138
  | [Claude Code](https://www.anthropic.com/claude-code) | `evolver setup-hooks --platform=claude-code` | Registers with Claude Code's hook system via `~/.claude/`. Restart the Claude Code CLI. |
139
- | [Codex](https://github.com/openai/codex) | `evolver setup-hooks --platform=codex` | `~/.codex/hooks.json` + scripts in `~/.codex/hooks/`, enables `codex_hooks` feature in `config.toml`. Restart the Codex CLI. |
139
+ | [Codex](https://github.com/openai/codex) | `evolver setup-hooks --platform=codex` | `~/.codex/hooks.json` + scripts in `~/.codex/hooks/`, enables `codex_hooks` feature in `config.toml`. Restart the Codex CLI. See [Codex caveats](#codex-caveats) below. |
140
140
  | [Kiro](https://kiro.dev) | `evolver setup-hooks --platform=kiro` | Three `*.kiro.hook` files + scripts in `~/.kiro/hooks/`. Auto-discovered, no restart needed. |
141
141
  | [opencode](https://opencode.ai) | `evolver setup-hooks --platform=opencode` | Plugin at `~/.opencode/plugins/evolver.js` + scripts in `~/.opencode/hooks/`. Restart opencode. |
142
142
  | [OpenClaw](https://openclaw.com) | No setup needed | OpenClaw natively interprets the `sessions_spawn(...)` stdout directives Evolver emits. Just run `evolver` from inside an OpenClaw session. |
143
143
 
144
+ #### Codex caveats
145
+
146
+ The Codex CLI exposes `SessionStart` / `Stop` / `PostToolUse` hooks (which is
147
+ how `setup-hooks --platform=codex` wires Evolver in), but it does **not**
148
+ emit a session transcript file the way Cursor / Claude Code / opencode do.
149
+ That means `evolver --review` cannot read raw session logs on Codex.
150
+
151
+ Evolver compensates by reading, in order:
152
+
153
+ 1. `MEMORY.md` / `USER.md` in the workspace root (if you maintain them);
154
+ 2. the `<!-- evolver-evolution-memory -->` section that
155
+ `setup-hooks --platform=codex` injects into your project's
156
+ `AGENTS.md`;
157
+ 3. the tail of the local `memory_graph.jsonl` (the per-cycle outcome log
158
+ that Evolver writes itself).
159
+
160
+ If none of those have content yet, you'll see `memory_missing` /
161
+ `user_missing` / `session_logs_missing` show up as advisory signals
162
+ during the first few cycles. They will go quiet on their own as
163
+ `memory_graph.jsonl` accumulates outcomes — no manual setup required.
164
+
144
165
  ## Run from Source (Contributors Only)
145
166
 
146
167
  Skip this section entirely if you installed via `npm install -g @evomap/evolver` above. This path exists so contributors can hack on the engine.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.85.0",
3
+ "version": "1.85.1",
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": {
@@ -208,13 +208,15 @@ if (!OPTS.skipObfuscate) {
208
208
  if (!OPTS.dryRun) {
209
209
  const O = require(require.resolve('javascript-obfuscator', { paths: [REPO_ROOT] }));
210
210
  const src = fs.readFileSync(BUNDLED_JS, 'utf8');
211
- // Deterministic obfuscation: same release version + same source = same
212
- // output. This makes binary diffs across re-runs meaningful and lets
213
- // SHA256SUMS be reproduced by anyone with the source tree.
214
- const seed = parseInt(crypto.createHash('sha256').update(`evolver:${releaseVersion}`).digest('hex').slice(0, 8), 16);
215
- const t0 = Date.now();
216
- const result = O.obfuscate(src, {
217
- seed,
211
+ // Seed obfuscation from release version: gives same-version reruns a
212
+ // narrow PRNG path, but the obfuscator has internal non-determinism
213
+ // beyond the seed (Set iteration / stringArray rotation timing) so two
214
+ // runs with the same seed can still differ slightly. Empirically ~5%
215
+ // of those runs emit invalid syntax (e.g. mangling `new.target` to
216
+ // `#target`, which then crashes `bun compile`). Validate after each
217
+ // attempt and retry — see RETRY note in pipeline rationale below.
218
+ const baseSeed = parseInt(crypto.createHash('sha256').update(`evolver:${releaseVersion}`).digest('hex').slice(0, 8), 16);
219
+ const obfOpts = {
218
220
  compact: true,
219
221
  controlFlowFlattening: true,
220
222
  controlFlowFlatteningThreshold: 0.75,
@@ -237,12 +239,49 @@ if (!OPTS.skipObfuscate) {
237
239
  numbersToExpressions: true,
238
240
  unicodeEscapeSequence: true,
239
241
  target: 'node',
240
- });
241
- fs.writeFileSync(OBF_JS, result.getObfuscatedCode());
242
- const obfSize = fs.statSync(OBF_JS).size;
243
- console.log(` obfuscation: ${((Date.now() - t0) / 1000).toFixed(1)}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB`);
242
+ };
243
+
244
+ const MAX_OBF_ATTEMPTS_RAW = process.env.OBF_MAX_ATTEMPTS;
245
+ const MAX_OBF_ATTEMPTS = MAX_OBF_ATTEMPTS_RAW === undefined
246
+ ? 4
247
+ : parseInt(MAX_OBF_ATTEMPTS_RAW, 10);
248
+ if (!Number.isInteger(MAX_OBF_ATTEMPTS) || MAX_OBF_ATTEMPTS < 1) {
249
+ console.error(` ERROR: OBF_MAX_ATTEMPTS must be a positive integer; got ${JSON.stringify(MAX_OBF_ATTEMPTS_RAW)}.`);
250
+ process.exit(1);
251
+ }
252
+ let attempt = 0;
253
+ let usedSeed = baseSeed;
254
+ let lastValidationErr = null;
255
+ let succeeded = false;
256
+ while (attempt < MAX_OBF_ATTEMPTS) {
257
+ attempt++;
258
+ // Perturb seed on retries to dodge a stuck PRNG path. Attempt 1 keeps
259
+ // the canonical seed for best-effort reproducibility; later attempts
260
+ // shift by attempt index so the next deploy gets a fresh trajectory.
261
+ usedSeed = baseSeed + (attempt - 1);
262
+ const t0 = Date.now();
263
+ const result = O.obfuscate(src, { ...obfOpts, seed: usedSeed });
264
+ fs.writeFileSync(OBF_JS, result.getObfuscatedCode());
265
+ const obfSize = fs.statSync(OBF_JS).size;
266
+ const obfSecs = ((Date.now() - t0) / 1000).toFixed(1);
267
+
268
+ const check = spawnSync('node', ['--check', OBF_JS], { encoding: 'utf8' });
269
+ if (check.status === 0) {
270
+ console.log(` obfuscation: ${obfSecs}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB (attempt ${attempt}/${MAX_OBF_ATTEMPTS}, seed=0x${usedSeed.toString(16)})`);
271
+ succeeded = true;
272
+ break;
273
+ }
274
+ lastValidationErr = (check.stderr || check.stdout || '').split('\n').slice(0, 3).join(' | ');
275
+ console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output failed node --check (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
276
+ }
277
+ if (!succeeded) {
278
+ console.error(` ERROR: javascript-obfuscator produced syntactically invalid output in ${MAX_OBF_ATTEMPTS} attempts.`);
279
+ console.error(` last error: ${lastValidationErr || '(none — loop did not run)'}`);
280
+ console.error(` raise OBF_MAX_ATTEMPTS env var to retry more times, or temporarily run with --skip-obfuscate.`);
281
+ process.exit(2);
282
+ }
244
283
  } else {
245
- console.log(' [dry-run] would obfuscate stage/bundled.js -> stage/bundled.obf.js');
284
+ console.log(' [dry-run] would obfuscate stage/bundled.js -> stage/bundled.obf.js (with retry-on-syntax-error)');
246
285
  }
247
286
 
248
287
  payloadJs = OBF_JS;
@@ -386,3 +425,21 @@ console.log(' next: gh release upload v<ver> dist-binaries/* --repo EvoMap/evol
386
425
  // in GitHub Actions on `runs-on: macos-latest, ubuntu-latest` should
387
426
  // set up the matrix so each runner smoke-tests its own native target.
388
427
  //
428
+ // Stage 2 retry-on-syntax-error (added 2026-05-22, v1.85.0 deploy
429
+ // post-mortem):
430
+ //
431
+ // The v1.85.0 release deploy hit `bun compile` failing with
432
+ // `Expected "in" but found ","` at offset ~1.5MB into bundled.obf.js.
433
+ // The failing region contained `(#target,this)` — javascript-obfuscator
434
+ // had mangled `new.target` into `#target` (a private class field syntax
435
+ // that's only legal inside a class body). A from-scratch rebuild on the
436
+ // same source + seed produced a different output (15.18 MB vs 15.14 MB)
437
+ // that compiled cleanly, confirming the obfuscator has internal
438
+ // non-determinism beyond the user-supplied seed.
439
+ //
440
+ // Mitigation: after each obfuscation attempt, run `node --check` on the
441
+ // output; if syntax is invalid, perturb the seed by +attempt and retry
442
+ // up to OBF_MAX_ATTEMPTS times (default 4). Cost of validation is
443
+ // ~1 second on 15 MB; cost of catching the failure here vs after a
444
+ // doomed bun compile pass is roughly 50s saved per failure.
445
+ //
@@ -2,16 +2,40 @@
2
2
  // evolver-session-end.js
3
3
  // Records evolution outcome at session end.
4
4
  // Collects git diff stats, extracts signals, records via Hub API or local memory.
5
- // Input: stdin JSON. Output: stdout JSON with followup_message.
5
+ // Input: stdin JSON. Output: stdout JSON with `systemMessage` (Claude Code Stop
6
+ // hook notification) — or empty `{}` on Cursor where systemMessage is mishandled.
6
7
 
7
8
  const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
8
11
  const { spawnSync } = require('child_process');
9
12
  // 10 MB — prevents RangeError on large child process output (e.g. git log/diff
10
13
  // on large repos). See GHSA reports / issue #451.
11
14
  const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
12
15
 
16
+ const path = require('path');
13
17
  const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
14
18
 
19
+ // Workspace-id must use the same resolution as the reader in
20
+ // src/evolve/pipeline/collect.js (which goes through src/gep/paths.js#
21
+ // getWorkspaceRoot()). Otherwise writer and reader could land on
22
+ // different `.evolver/workspace-id` files when EVOLVER_REPO_ROOT or
23
+ // OPENCLAW_WORKSPACE is set, or when a `<repoRoot>/workspace`
24
+ // subdirectory exists — in which case the IDs would never match and
25
+ // every memory-graph entry would silently get dropped (Bugbot PR #109
26
+ // round-1 MEDIUM). Lazy-load the canonical resolver from the resolved
27
+ // evolver root; fall back to env-only when paths.js is unreachable.
28
+ function resolveWorkspaceIdForWriter() {
29
+ if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
30
+ const evolverRoot = findEvolverRoot();
31
+ if (!evolverRoot) return null;
32
+ try {
33
+ const paths = require(path.join(evolverRoot, 'src', 'gep', 'paths.js'));
34
+ if (typeof paths.getWorkspaceId === 'function') return paths.getWorkspaceId();
35
+ } catch { /* paths.js unreachable — return null */ }
36
+ return null;
37
+ }
38
+
15
39
  function runGit(args, cwd) {
16
40
  // Argv-array form, no shell. Avoids POSIX `2>/dev/null` redirects that
17
41
  // break on Windows cmd.exe (#537). Failures (e.g. no HEAD~1 in a fresh
@@ -52,6 +76,31 @@ function getGitDiffStats() {
52
76
  };
53
77
  }
54
78
 
79
+ // Detect whether the hook is running inside Cursor.
80
+ //
81
+ // Why: Claude Code's Stop hook spec says `systemMessage` is a notification
82
+ // shown to the user and is NOT fed back into Claude's context. Cursor's
83
+ // Claude Code-compatible runtime currently splices it into the next
84
+ // inference round as if it were a user prompt, so Claude "responds" to the
85
+ // evolution receipt — visible to users as an unexplained extra reasoning
86
+ // turn after every task. Until Cursor fixes this, suppress systemMessage
87
+ // on Cursor while keeping the local-memory append intact.
88
+ //
89
+ // Detection (any of):
90
+ // - TERM_PROGRAM=cursor
91
+ // - CURSOR_TRACE_ID / CURSOR_SESSION_ID set
92
+ // - EVOLVER_HOOK_HOST=cursor (manual override)
93
+ // Escape hatch: EVOLVER_HOOK_VERBOSE=1 forces the message on regardless.
94
+ function isCursorHost() {
95
+ const verbose = String(process.env.EVOLVER_HOOK_VERBOSE || '').toLowerCase();
96
+ if (verbose === '1' || verbose === 'true') return false;
97
+ if (String(process.env.EVOLVER_HOOK_HOST || '').toLowerCase() === 'cursor') return true;
98
+ if (String(process.env.TERM_PROGRAM || '').toLowerCase() === 'cursor') return true;
99
+ if (process.env.CURSOR_TRACE_ID) return true;
100
+ if (process.env.CURSOR_SESSION_ID) return true;
101
+ return false;
102
+ }
103
+
55
104
  function detectSignals(text) {
56
105
  if (!text) return [];
57
106
  const lower = text.toLowerCase();
@@ -113,6 +162,21 @@ function recordToLocal(graphPath, outcome) {
113
162
  score: outcome.score,
114
163
  note: outcome.summary,
115
164
  },
165
+ // Tag the originating workspace so the review-time reader in
166
+ // collect.js can scope user-level fallback entries to the current
167
+ // cwd. Without this, two unrelated projects sharing the user-level
168
+ // fallback file (~/.evolver/memory/evolution/memory_graph.jsonl,
169
+ // used by npm-global installs) would cross-pollinate each other's
170
+ // review context — a prompt-injection / disclosure surface flagged
171
+ // by Bugbot on PR #105 round-2.
172
+ //
173
+ // workspace_id is the forge-resistant tag (PR #108 round-3): the
174
+ // reader compares it against the secret in the workspace's own
175
+ // .evolver/workspace-id file. cwd is retained as a backward-compat
176
+ // tag so older entries written before this hardening still pass
177
+ // the cwd check.
178
+ cwd: process.cwd(),
179
+ workspace_id: resolveWorkspaceIdForWriter(),
116
180
  source: 'hook:session-end',
117
181
  };
118
182
  fs.appendFileSync(graphPath, JSON.stringify(entry) + '\n', 'utf8');
@@ -125,16 +189,23 @@ function recordToLocal(graphPath, outcome) {
125
189
  function main() {
126
190
  let inputData = '';
127
191
  let handled = false;
192
+ let watchdog = null;
193
+ const finish = (payload) => {
194
+ if (handled) return;
195
+ handled = true;
196
+ if (watchdog) clearTimeout(watchdog);
197
+ process.stdout.write(JSON.stringify(payload || {}));
198
+ process.exit(0);
199
+ };
128
200
  process.stdin.setEncoding('utf8');
129
201
  process.stdin.on('data', chunk => { inputData += chunk; });
130
202
  process.stdin.on('end', () => {
131
203
  if (handled) return;
132
- handled = true;
133
204
  try {
134
205
  const diffInfo = getGitDiffStats();
135
206
 
136
207
  if (!diffInfo.hasChanges) {
137
- process.stdout.write(JSON.stringify({}));
208
+ finish({});
138
209
  return;
139
210
  }
140
211
 
@@ -162,22 +233,46 @@ function main() {
162
233
  const target = hubOk ? 'Hub' : localOk ? 'local memory' : 'nowhere (no Hub or local path)';
163
234
  const msg = `[Evolution] Session outcome recorded to ${target}: ${outcome.summary}`;
164
235
 
165
- process.stdout.write(JSON.stringify({
166
- followup_message: msg,
167
- stopMessage: msg,
168
- additionalContext: msg,
169
- }));
236
+ // Stop hook output schema (per Claude Code docs):
237
+ // - decision: "approve" | "block"
238
+ // - reason: string (shown when decision is set)
239
+ // - systemMessage: string (notification displayed to user)
240
+ // - continue: boolean
241
+ // - stopReason: string
242
+ //
243
+ // Earlier versions emitted `followup_message`, `stopMessage`, and
244
+ // `additionalContext` together. `followup_message` is the field that
245
+ // re-injects the receipt into Claude's next inference round, which
246
+ // caused the agent to "respond" to its own evolution log line —
247
+ // visible to users as an unexplained extra reasoning turn after
248
+ // every task. The evolver is supposed to be observational, so we
249
+ // now use `systemMessage` only — that surfaces the receipt to the
250
+ // user without forcing another inference round.
251
+ //
252
+ // Cursor compatibility: Cursor's Claude Code-compatible runtime
253
+ // currently treats `systemMessage` as a user prompt for the next
254
+ // inference round. When we detect Cursor, omit systemMessage too.
255
+ // The receipt is always appended to ~/.evolver/logs/evolution.log
256
+ // so it is never silently lost; users can opt back in to the inline
257
+ // notification with EVOLVER_HOOK_VERBOSE=1.
258
+ try {
259
+ const logDir = process.env.EVOLVER_HOOK_LOG_DIR
260
+ || path.join(os.homedir(), '.evolver', 'logs');
261
+ fs.mkdirSync(logDir, { recursive: true });
262
+ fs.appendFileSync(
263
+ path.join(logDir, 'evolution.log'),
264
+ `${new Date().toISOString()} ${msg}\n`,
265
+ 'utf8'
266
+ );
267
+ } catch { /* best-effort, never break the hook on log write */ }
268
+
269
+ finish(isCursorHost() ? {} : { systemMessage: msg });
170
270
  } catch (e) {
171
- process.stdout.write(JSON.stringify({}));
271
+ finish({});
172
272
  }
173
273
  });
174
274
 
175
- setTimeout(() => {
176
- if (handled) return;
177
- handled = true;
178
- process.stdout.write(JSON.stringify({}));
179
- process.exit(0);
180
- }, 7000);
275
+ watchdog = setTimeout(() => finish({}), 7000);
181
276
  }
182
277
 
183
278
  main();