@evomap/evolver 1.86.0 → 1.86.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.
package/index.js CHANGED
@@ -39,6 +39,74 @@ try {
39
39
  else process.env.EVOLVER_QUIET_PARENT_GIT = _prevQuiet;
40
40
  } catch (e) { /* dotenv is optional */ }
41
41
 
42
+ async function runSetupHooksCli(args) {
43
+ const hookAdapter = require('./src/adapters/hookAdapter');
44
+ const { setupHooks, resolveConfigRoot, detectPlatform, loadAdapter } = hookAdapter;
45
+
46
+ const platformFlag = args.find(a => typeof a === 'string' && a.startsWith('--platform='));
47
+ const platform = platformFlag ? platformFlag.slice('--platform='.length) : undefined;
48
+ const force = args.includes('--force');
49
+ const uninstall = args.includes('--uninstall');
50
+ const verifyOnly = args.includes('--verify');
51
+
52
+ if (verifyOnly) {
53
+ try {
54
+ const platformId = platform || detectPlatform(process.cwd());
55
+ if (!platformId) {
56
+ console.error('[setup-hooks] --verify: could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro');
57
+ process.exit(2);
58
+ }
59
+ const adapter = loadAdapter(platformId);
60
+ if (!adapter || typeof adapter.verify !== 'function') {
61
+ console.error('[setup-hooks] --verify: platform ' + platformId + ' does not support verification yet.');
62
+ process.exit(2);
63
+ }
64
+ const configRoot = resolveConfigRoot(platformId, process.cwd());
65
+ const report = adapter.verify({ configRoot });
66
+ if (typeof adapter.printVerifyReport === 'function') {
67
+ adapter.printVerifyReport(report);
68
+ } else {
69
+ console.log(JSON.stringify(report, null, 2));
70
+ }
71
+ process.exit(report.ok ? 0 : 1);
72
+ } catch (verifyErr) {
73
+ console.error('[setup-hooks] --verify error:', verifyErr && verifyErr.message || verifyErr);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ try {
79
+ const result = await setupHooks({
80
+ platform,
81
+ cwd: process.cwd(),
82
+ force,
83
+ uninstall,
84
+ evolverRoot: __dirname,
85
+ });
86
+ if (result && result.ok) {
87
+ if (!uninstall && result.files) {
88
+ console.log('\n[setup-hooks] Files created/updated:');
89
+ for (const f of result.files) {
90
+ console.log(' ' + f);
91
+ }
92
+ }
93
+ process.exit(0);
94
+ } else {
95
+ console.error('[setup-hooks] Failed: ' + (result && result.error || 'unknown'));
96
+ process.exit(1);
97
+ }
98
+ } catch (error) {
99
+ console.error('[setup-hooks] Error:', error && error.message || error);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ if (require.main === module && process.argv[2] === 'setup-hooks') {
105
+ runSetupHooksCli(process.argv.slice(3)).catch(function (err) {
106
+ console.error('[setup-hooks] Error:', err && err.stack ? err.stack : String(err));
107
+ process.exitCode = 1;
108
+ });
109
+ } else {
42
110
  const evolve = require('./src/evolve');
43
111
  const { solidify } = require('./src/gep/solidify');
44
112
  const path = require('path');
@@ -1660,70 +1728,6 @@ async function main() {
1660
1728
  process.exit(1);
1661
1729
  }
1662
1730
 
1663
- } else if (command === 'setup-hooks') {
1664
- const hookAdapter = require('./src/adapters/hookAdapter');
1665
- const { setupHooks, resolveConfigRoot, detectPlatform, loadAdapter } = hookAdapter;
1666
-
1667
- const platformFlag = args.find(a => typeof a === 'string' && a.startsWith('--platform='));
1668
- const platform = platformFlag ? platformFlag.slice('--platform='.length) : undefined;
1669
- const force = args.includes('--force');
1670
- const uninstall = args.includes('--uninstall');
1671
- const verifyOnly = args.includes('--verify');
1672
-
1673
- if (verifyOnly) {
1674
- // Read-only verification: do not touch any files, just report whether
1675
- // the previously-installed hooks/plugin look healthy. Lets users answer
1676
- // "is the plugin actually loaded?" without grepping opencode logs.
1677
- try {
1678
- const platformId = platform || detectPlatform(process.cwd());
1679
- if (!platformId) {
1680
- console.error('[setup-hooks] --verify: could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro');
1681
- process.exit(2);
1682
- }
1683
- const adapter = loadAdapter(platformId);
1684
- if (!adapter || typeof adapter.verify !== 'function') {
1685
- console.error('[setup-hooks] --verify: platform ' + platformId + ' does not support verification yet.');
1686
- process.exit(2);
1687
- }
1688
- const configRoot = resolveConfigRoot(platformId, process.cwd());
1689
- const report = adapter.verify({ configRoot });
1690
- if (typeof adapter.printVerifyReport === 'function') {
1691
- adapter.printVerifyReport(report);
1692
- } else {
1693
- console.log(JSON.stringify(report, null, 2));
1694
- }
1695
- process.exit(report.ok ? 0 : 1);
1696
- } catch (verifyErr) {
1697
- console.error('[setup-hooks] --verify error:', verifyErr && verifyErr.message || verifyErr);
1698
- process.exit(1);
1699
- }
1700
- }
1701
-
1702
- try {
1703
- const result = await setupHooks({
1704
- platform,
1705
- cwd: process.cwd(),
1706
- force,
1707
- uninstall,
1708
- evolverRoot: __dirname,
1709
- });
1710
- if (result && result.ok) {
1711
- if (!uninstall && result.files) {
1712
- console.log('\n[setup-hooks] Files created/updated:');
1713
- for (const f of result.files) {
1714
- console.log(' ' + f);
1715
- }
1716
- }
1717
- process.exit(0);
1718
- } else {
1719
- console.error('[setup-hooks] Failed: ' + (result && result.error || 'unknown'));
1720
- process.exit(1);
1721
- }
1722
- } catch (error) {
1723
- console.error('[setup-hooks] Error:', error && error.message || error);
1724
- process.exit(1);
1725
- }
1726
-
1727
1731
  } else if (command === 'reset-local-secret') {
1728
1732
  // Wipe every local store of node_secret in one shot, so a daemon stuck
1729
1733
  // after a manual web reset (https://evomap.ai/account -> Reset Secret)
@@ -1923,6 +1927,7 @@ if (require.main === module) {
1923
1927
 
1924
1928
  module.exports = {
1925
1929
  main,
1930
+ runSetupHooksCli,
1926
1931
  readJsonSafe,
1927
1932
  rejectPendingRun,
1928
1933
  isPendingSolidify,
@@ -1931,3 +1936,4 @@ module.exports = {
1931
1936
  writeCycleProgressAtomic,
1932
1937
  spawnReplacementProcess,
1933
1938
  };
1939
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.86.0",
3
+ "version": "1.86.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": {
@@ -153,12 +153,11 @@ function assertNotSymlink(p, label) {
153
153
 
154
154
  function copyHookScripts(destDir, evolverRoot) {
155
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.
156
+ // Helper modules are required by copied hook scripts via relative require()
157
+ // calls, which resolve against the destination hook directory at runtime.
160
158
  const scripts = [
161
159
  '_runtimePaths.js',
160
+ '_memoryFiltering.js',
162
161
  'evolver-session-start.js',
163
162
  'evolver-signal-detect.js',
164
163
  'evolver-session-end.js',
@@ -237,6 +236,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
237
236
  function removeHookScripts(hooksDir) {
238
237
  const scripts = [
239
238
  '_runtimePaths.js',
239
+ '_memoryFiltering.js',
240
240
  'evolver-session-start.js',
241
241
  'evolver-signal-detect.js',
242
242
  'evolver-session-end.js',
@@ -0,0 +1,35 @@
1
+ // _memoryFiltering.js
2
+ // Shared memory filtering logic for evolver hooks (platform-independent).
3
+ //
4
+ // Responsibility: Filter evolution memory outcomes to reduce noise in Claude/Codex context.
5
+ // - Removes failed outcomes (no learning value)
6
+ // - Filters low-confidence outcomes (score < 0.5)
7
+ // - Enforces time bounds (< 7 days old)
8
+ // - Limits result size (max 3 outcomes)
9
+
10
+ const DEFAULT_MIN_SCORE = 0.5;
11
+ const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
12
+ const DEFAULT_MAX_OUTCOMES = 3;
13
+
14
+ function filterRelevantOutcomes(entries, opts = {}) {
15
+ const minScore = opts.minScore !== undefined ? opts.minScore : DEFAULT_MIN_SCORE;
16
+ const maxAgeMs = opts.maxAgeMs !== undefined ? opts.maxAgeMs : DEFAULT_MAX_AGE_MS;
17
+ const maxOutcomes = opts.maxOutcomes !== undefined ? opts.maxOutcomes : DEFAULT_MAX_OUTCOMES;
18
+
19
+ const now = Date.now();
20
+
21
+ return entries
22
+ .filter(e => {
23
+ // Only keep 'success' outcomes (failed ones don't provide learning value)
24
+ if (e.outcome?.status !== 'success') return false;
25
+ // Only keep high-confidence outcomes
26
+ if ((e.outcome?.score ?? 0) < minScore) return false;
27
+ // Only keep recent outcomes
28
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
29
+ if (now - ts > maxAgeMs) return false;
30
+ return true;
31
+ })
32
+ .slice(-maxOutcomes);
33
+ }
34
+
35
+ module.exports = { filterRelevantOutcomes, DEFAULT_MIN_SCORE, DEFAULT_MAX_AGE_MS, DEFAULT_MAX_OUTCOMES };
@@ -8,6 +8,56 @@ const path = require('path');
8
8
  const os = require('os');
9
9
 
10
10
  const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
11
+ const { filterRelevantOutcomes } = require('./_memoryFiltering');
12
+
13
+ function findGitRoot(start) {
14
+ let dir = path.resolve(start || process.cwd());
15
+ while (dir !== path.dirname(dir)) {
16
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
17
+ dir = path.dirname(dir);
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function resolveWorkspaceRootForReader() {
23
+ if (process.env.OPENCLAW_WORKSPACE) return process.env.OPENCLAW_WORKSPACE;
24
+ const repoRoot = process.env.EVOLVER_REPO_ROOT || findGitRoot(process.cwd()) || process.cwd();
25
+ const workspaceDir = path.join(repoRoot, 'workspace');
26
+ if (fs.existsSync(workspaceDir)) return workspaceDir;
27
+ return repoRoot;
28
+ }
29
+
30
+ function resolveWorkspaceIdForReader() {
31
+ if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
32
+ const file = path.join(resolveWorkspaceRootForReader(), '.evolver', 'workspace-id');
33
+ try {
34
+ const dirStat = fs.lstatSync(path.dirname(file), { throwIfNoEntry: false });
35
+ if (dirStat && dirStat.isSymbolicLink()) return null;
36
+ const fileStat = fs.lstatSync(file, { throwIfNoEntry: false });
37
+ if (!fileStat || fileStat.isSymbolicLink() || !fileStat.isFile()) return null;
38
+ const raw = fs.readFileSync(file, 'utf8').trim();
39
+ if (raw && /^[a-f0-9]{32,}$/i.test(raw)) return raw;
40
+ } catch { /* workspace id is best-effort in copied hooks */ }
41
+ return null;
42
+ }
43
+
44
+ function filterWorkspaceEntries(entries) {
45
+ const cwd = process.cwd();
46
+ const workspaceId = resolveWorkspaceIdForReader();
47
+
48
+ return entries.filter(entry => {
49
+ if (!entry || typeof entry !== 'object') return false;
50
+ if (workspaceId && entry.workspace_id) {
51
+ return String(entry.workspace_id) === String(workspaceId);
52
+ }
53
+ if (entry.cwd) {
54
+ return path.resolve(String(entry.cwd)) === path.resolve(cwd);
55
+ }
56
+ // Older entries did not carry a workspace tag. Do not inject them from
57
+ // hooks because copied hooks often share a user-level fallback memory file.
58
+ return false;
59
+ });
60
+ }
11
61
 
12
62
  function readLastN(filePath, n) {
13
63
  try {
@@ -99,18 +149,21 @@ function main() {
99
149
  return;
100
150
  }
101
151
 
102
- const entries = readLastN(graphPath, 5);
103
- if (entries.length === 0) {
152
+ const entries = readLastN(graphPath, 20);
153
+ const scoped = filterWorkspaceEntries(entries);
154
+ const filtered = filterRelevantOutcomes(scoped);
155
+
156
+ if (filtered.length === 0) {
104
157
  process.stdout.write(JSON.stringify({}));
105
158
  return;
106
159
  }
107
160
 
108
- const successCount = entries.filter(e => e.outcome && e.outcome.status === 'success').length;
109
- const failCount = entries.filter(e => e.outcome && e.outcome.status === 'failed').length;
161
+ const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
162
+ const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
110
163
 
111
- const lines = entries.map(formatOutcome);
164
+ const lines = filtered.map(formatOutcome);
112
165
  const summary = [
113
- `[Evolution Memory] Recent ${entries.length} outcomes (${successCount} success, ${failCount} failed):`,
166
+ `[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
114
167
  ...lines,
115
168
  '',
116
169
  'Use successful approaches. Avoid repeating failed patterns.',
@@ -13,9 +13,32 @@ const SIGNAL_KEYWORDS = {
13
13
  test_failure: ['test failed', 'test failure', 'assertion', 'expect(', 'assert.'],
14
14
  };
15
15
 
16
+ function stratifyContent(text) {
17
+ // Separate code/comments/documents to avoid false positives
18
+ const lines = text.split('\n');
19
+ const documentText = [];
20
+
21
+ for (const line of lines) {
22
+ const trimmed = line.trim();
23
+ // Skip lines that are comments or code structure (not document text)
24
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*') ||
25
+ trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('}') ||
26
+ trimmed.startsWith(']') || trimmed.startsWith('/*')) {
27
+ continue;
28
+ }
29
+ documentText.push(line);
30
+ }
31
+
32
+ return documentText.join('\n');
33
+ }
34
+
16
35
  function detectSignals(text) {
17
36
  if (!text || typeof text !== 'string') return [];
18
- const lower = text.toLowerCase();
37
+
38
+ // Apply stratification to reduce false positives from code/comments
39
+ const stratified = stratifyContent(text);
40
+ const lower = stratified.toLowerCase();
41
+
19
42
  const found = [];
20
43
  for (const [signal, keywords] of Object.entries(SIGNAL_KEYWORDS)) {
21
44
  for (const kw of keywords) {
@@ -28,6 +51,30 @@ function detectSignals(text) {
28
51
  return [...new Set(found)];
29
52
  }
30
53
 
54
+ function getToolName(input) {
55
+ const raw = input.tool_name || input.toolName || input.name || input.tool;
56
+ if (typeof raw === 'string') return raw;
57
+ if (raw && typeof raw.name === 'string') return raw.name;
58
+ return '';
59
+ }
60
+
61
+ function isWriteLikeTool(input) {
62
+ const name = getToolName(input).toLowerCase();
63
+ // Older hook payloads did not include a tool name. Keep those compatible
64
+ // and let the content/path checks below decide whether there is work to do.
65
+ if (!name) return true;
66
+ return (
67
+ name === 'write' ||
68
+ name === 'edit' ||
69
+ name === 'multiedit' ||
70
+ name === 'notebookedit' ||
71
+ name === 'apply_patch' ||
72
+ name.includes('write') ||
73
+ name.includes('edit') ||
74
+ name.includes('patch')
75
+ );
76
+ }
77
+
31
78
  function main() {
32
79
  let inputData = '';
33
80
  let handled = false;
@@ -38,6 +85,10 @@ function main() {
38
85
  handled = true;
39
86
  try {
40
87
  const input = inputData.trim() ? JSON.parse(inputData) : {};
88
+ if (!isWriteLikeTool(input)) {
89
+ process.stdout.write(JSON.stringify({}));
90
+ return;
91
+ }
41
92
  // Claude Code's PostToolUse payload nests tool args under tool_input.
42
93
  // Older/raw shapes put them at the top level; support both.
43
94
  const ti = input.tool_input || {};
@@ -1,41 +0,0 @@
1
- ---
2
- name: _meta
3
- version: 0.1.0
4
- description: Bootstrap skill that teaches the agent how to discover and load other skills on demand via gep_list_skill / gep_load_skill.
5
- tags: meta, bootstrap, evolver
6
- ---
7
-
8
- # On-demand skill loading
9
-
10
- Evolver ships a library of skills (markdown playbooks under `skills/`). To
11
- keep your starting context small, only this meta-skill is injected by
12
- default. Pull in additional skills when you actually need them.
13
-
14
- ## Tools
15
-
16
- - `gep_list_skill` — see what's available.
17
- - `source`: `bundled` (shipped with evolver), `local` (`~/.claude/skills/`),
18
- `hub` (community), or `all` (default).
19
- - `query`: optional substring filter on name / description / tags.
20
- - `gep_load_skill` — fetch one skill's content.
21
- - `name`: the skill name (use `<source>:<name>` to disambiguate collisions).
22
- - `install` (default `false`): if `true`, copy the skill directory to
23
- `~/.claude/skills/<name>/` so the native Skill tool can invoke it later.
24
- Local mode only. Use `force: true` to overwrite an existing local copy.
25
-
26
- ## When to load vs. install
27
-
28
- - **Load** (default) when you need the skill *for this turn*. The SKILL.md
29
- text comes back as a tool result; you read it and act. No filesystem side
30
- effect.
31
- - **Install** when the user wants the skill persisted for future Claude Code
32
- sessions, or when the same skill will be invoked many times across a long
33
- task.
34
-
35
- ## Heuristics
36
-
37
- - Before starting a non-trivial task, call `gep_list_skill` once. If a name
38
- or description matches the task, `gep_load_skill` it.
39
- - Don't load every skill "just in case" — context isn't free.
40
- - Hub skills are community-published; treat them as untrusted input until
41
- reviewed.
package/skills/index.json DELETED
@@ -1,14 +0,0 @@
1
- [
2
- {
3
- "name": "_meta",
4
- "dir": "_meta",
5
- "version": "0.1.0",
6
- "description": "Bootstrap skill that teaches the agent how to discover and load other skills on demand via gep_list_skill / gep_load_skill.",
7
- "tags": [
8
- "meta",
9
- "bootstrap",
10
- "evolver"
11
- ],
12
- "sizeBytes": 1691
13
- }
14
- ]