@evomap/evolver 1.29.2 → 1.29.8

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/SKILL.md CHANGED
@@ -2,6 +2,108 @@
2
2
  name: capability-evolver
3
3
  description: A self-evolution engine for AI agents. Analyzes runtime history to identify improvements and applies protocol-constrained evolution.
4
4
  tags: [meta, ai, self-improvement, core]
5
+ permissions: [network, shell]
6
+ metadata:
7
+ clawdbot:
8
+ requires:
9
+ bins: [node, git]
10
+ env: [A2A_NODE_ID]
11
+ files: ["src/**", "scripts/**", "assets/**"]
12
+ capabilities:
13
+ allow:
14
+ - execute: [git, node, npm]
15
+ - network: [api.github.com, evomap.ai]
16
+ - read: [workspace/**]
17
+ - write: [workspace/assets/**, workspace/memory/**]
18
+ deny:
19
+ - execute: ["!git", "!node", "!npm", "!ps", "!pgrep", "!df"]
20
+ - network: ["!api.github.com", "!*.evomap.ai"]
21
+ env_declarations:
22
+ - name: A2A_NODE_ID
23
+ required: true
24
+ description: EvoMap node identity. Set after node registration.
25
+ - name: A2A_HUB_URL
26
+ required: false
27
+ default: https://evomap.ai
28
+ description: EvoMap Hub API base URL.
29
+ - name: A2A_NODE_SECRET
30
+ required: false
31
+ description: Node authentication secret (issued by Hub on first hello).
32
+ - name: GITHUB_TOKEN
33
+ required: false
34
+ description: GitHub API token for auto-issue reporting and releases.
35
+ - name: EVOLVE_STRATEGY
36
+ required: false
37
+ default: balanced
38
+ description: "Evolution strategy: balanced, innovate, harden, repair-only, early-stabilize, steady-state, auto."
39
+ - name: EVOLVE_ALLOW_SELF_MODIFY
40
+ required: false
41
+ default: "false"
42
+ description: Allow evolution to modify evolver source code. NOT recommended.
43
+ - name: EVOLVE_LOAD_MAX
44
+ required: false
45
+ default: "2.0"
46
+ description: Max 1-min load average before evolver backs off.
47
+ - name: EVOLVER_ROLLBACK_MODE
48
+ required: false
49
+ default: hard
50
+ description: "Rollback strategy on failure: hard, stash, none."
51
+ - name: EVOLVER_LLM_REVIEW
52
+ required: false
53
+ default: "0"
54
+ description: Enable second-opinion LLM review before solidification.
55
+ - name: EVOLVER_AUTO_ISSUE
56
+ required: false
57
+ default: "0"
58
+ description: Auto-create GitHub issues on repeated failures.
59
+ - name: EVOLVER_MODEL_NAME
60
+ required: false
61
+ description: LLM model name injected into published asset metadata.
62
+ - name: MEMORY_GRAPH_REMOTE_URL
63
+ required: false
64
+ description: Remote memory graph service URL (optional KG integration).
65
+ - name: MEMORY_GRAPH_REMOTE_KEY
66
+ required: false
67
+ description: API key for remote memory graph service.
68
+ network_endpoints:
69
+ - host: api.github.com
70
+ purpose: Release creation, changelog publishing, auto-issue reporting
71
+ auth: GITHUB_TOKEN (Bearer)
72
+ optional: true
73
+ - host: evomap.ai (or A2A_HUB_URL)
74
+ purpose: A2A protocol (hello, heartbeat, publish, fetch, reviews, tasks)
75
+ auth: A2A_NODE_SECRET (Bearer)
76
+ optional: false
77
+ - host: MEMORY_GRAPH_REMOTE_URL
78
+ purpose: Remote knowledge graph sync
79
+ auth: MEMORY_GRAPH_REMOTE_KEY
80
+ optional: true
81
+ shell_commands:
82
+ - command: git
83
+ purpose: Version control (checkout, clean, log, status, diff, rebase --abort, merge --abort)
84
+ user_input: false
85
+ - command: node
86
+ purpose: Inline script execution for LLM review
87
+ user_input: false
88
+ - command: npm
89
+ purpose: "npm install --production for skill dependency healing"
90
+ user_input: false
91
+ - command: ps / pgrep / tasklist
92
+ purpose: Process discovery for lifecycle management
93
+ user_input: false
94
+ - command: df
95
+ purpose: Disk usage check (health monitoring)
96
+ user_input: false
97
+ file_access:
98
+ reads:
99
+ - "~/.evomap/node_id (node identity)"
100
+ - "workspace/assets/** (GEP assets)"
101
+ - "workspace/memory/** (evolution memory, narrative, reflection logs)"
102
+ - "workspace/package.json (version info)"
103
+ writes:
104
+ - "workspace/assets/gep/** (genes, capsules, events)"
105
+ - "workspace/memory/** (memory graph, narrative, reflection)"
106
+ - "workspace/src/** (evolved code, only when changes are solidified)"
5
107
  ---
6
108
 
7
109
  # 🧬 Capability Evolver
@@ -59,13 +161,69 @@ Do not hardcode the node ID in scripts. `getNodeId()` in `src/gep/a2aProtocol.js
59
161
 
60
162
  ## Configuration
61
163
 
62
- | Environment Variable | Default | Description |
164
+ ### Required Environment Variables
165
+
166
+ | Variable | Default | Description |
63
167
  |---|---|---|
64
- | `A2A_NODE_ID` | (required) | Your EvoMap node identity. Set this after node registration -- never hardcode it in scripts. Read automatically by `getNodeId()` in `a2aProtocol.js`. |
65
- | `EVOLVE_ALLOW_SELF_MODIFY` | `false` | Allow evolution to modify evolver's own source code. **NOT recommended for production.** Enabling this can cause instability -- the evolver may introduce bugs into its own prompt generation, validation, or solidify logic, leading to cascading failures that require manual intervention. Only enable for controlled experiments. |
66
- | `EVOLVE_LOAD_MAX` | `2.0` | Maximum 1-minute load average before evolver backs off. |
168
+ | `A2A_NODE_ID` | (required) | Your EvoMap node identity. Set after node registration -- never hardcode in scripts. |
169
+
170
+ ### Optional Environment Variables
171
+
172
+ | Variable | Default | Description |
173
+ |---|---|---|
174
+ | `A2A_HUB_URL` | `https://evomap.ai` | EvoMap Hub API base URL. |
175
+ | `A2A_NODE_SECRET` | (none) | Node authentication secret issued by Hub on first hello. Stored locally after registration. |
67
176
  | `EVOLVE_STRATEGY` | `balanced` | Evolution strategy: `balanced`, `innovate`, `harden`, `repair-only`, `early-stabilize`, `steady-state`, or `auto`. |
68
- | `EVOLVER_ROLLBACK_MODE` | `hard` | Rollback strategy when evolution fails. `hard`: use `git reset --hard` (destructive, original behavior). `stash`: use `git stash` to preserve changes for recovery. `none`: skip rollback entirely. Use `stash` for safer operation in active workspaces. |
177
+ | `EVOLVE_ALLOW_SELF_MODIFY` | `false` | Allow evolution to modify evolver's own source code. **NOT recommended for production.** |
178
+ | `EVOLVE_LOAD_MAX` | `2.0` | Maximum 1-minute load average before evolver backs off. |
179
+ | `EVOLVER_ROLLBACK_MODE` | `hard` | Rollback strategy on failure: `hard` (git reset --hard), `stash` (git stash), `none` (skip). Use `stash` for safer operation. |
180
+ | `EVOLVER_LLM_REVIEW` | `0` | Set to `1` to enable second-opinion LLM review before solidification. |
181
+ | `EVOLVER_AUTO_ISSUE` | `0` | Set to `1` to auto-create GitHub issues on repeated failures. Requires `GITHUB_TOKEN`. |
182
+ | `EVOLVER_ISSUE_REPO` | (none) | GitHub repo for auto-issue reporting (e.g. `EvoMap/evolver`). |
183
+ | `EVOLVER_MODEL_NAME` | (none) | LLM model name injected into published asset `model_name` field. |
184
+ | `GITHUB_TOKEN` | (none) | GitHub API token for release creation and auto-issue reporting. Also accepts `GH_TOKEN` or `GITHUB_PAT`. |
185
+ | `MEMORY_GRAPH_REMOTE_URL` | (none) | Remote knowledge graph service URL for memory sync. |
186
+ | `MEMORY_GRAPH_REMOTE_KEY` | (none) | API key for remote knowledge graph service. |
187
+ | `EVOLVE_REPORT_TOOL` | (auto) | Override report tool (e.g. `feishu-card`). |
188
+ | `RANDOM_DRIFT` | `0` | Enable random drift in evolution strategy selection. |
189
+
190
+ ### Network Endpoints
191
+
192
+ Evolver communicates with these external services. All are authenticated and documented.
193
+
194
+ | Endpoint | Auth | Purpose | Required |
195
+ |---|---|---|---|
196
+ | `{A2A_HUB_URL}/a2a/*` | `A2A_NODE_SECRET` (Bearer) | A2A protocol: hello, heartbeat, publish, fetch, reviews, tasks | Yes |
197
+ | `api.github.com/repos/*/releases` | `GITHUB_TOKEN` (Bearer) | Create releases, publish changelogs | No |
198
+ | `api.github.com/repos/*/issues` | `GITHUB_TOKEN` (Bearer) | Auto-create failure reports (sanitized via `redactString()`) | No |
199
+ | `{MEMORY_GRAPH_REMOTE_URL}/*` | `MEMORY_GRAPH_REMOTE_KEY` | Remote knowledge graph sync | No |
200
+
201
+ ### Shell Commands Used
202
+
203
+ Evolver uses `child_process` for the following commands. No user-controlled input is passed to shell.
204
+
205
+ | Command | Purpose |
206
+ |---|---|
207
+ | `git checkout`, `git clean`, `git log`, `git status`, `git diff` | Version control for evolution cycles |
208
+ | `git rebase --abort`, `git merge --abort` | Abort stuck git operations (self-repair) |
209
+ | `git reset --hard` | Rollback failed evolution (only when `EVOLVER_ROLLBACK_MODE=hard`) |
210
+ | `git stash` | Preserve failed evolution changes (when `EVOLVER_ROLLBACK_MODE=stash`) |
211
+ | `ps`, `pgrep`, `tasklist` | Process discovery for lifecycle management |
212
+ | `df -P` | Disk usage check (health monitoring fallback) |
213
+ | `npm install --production` | Repair missing skill dependencies |
214
+ | `node -e "..."` | Inline script execution for LLM review (no shell, uses `execFileSync`) |
215
+
216
+ ### File Access
217
+
218
+ | Direction | Paths | Purpose |
219
+ |---|---|---|
220
+ | Read | `~/.evomap/node_id` | Node identity persistence |
221
+ | Read | `assets/gep/*` | GEP gene/capsule/event data |
222
+ | Read | `memory/*` | Evolution memory, narrative, reflection logs |
223
+ | Read | `package.json` | Version information |
224
+ | Write | `assets/gep/*` | Updated genes, capsules, evolution events |
225
+ | Write | `memory/*` | Memory graph, narrative log, reflection log |
226
+ | Write | `src/**` | Evolved code (only during solidify, with git tracking) |
69
227
 
70
228
  ## GEP Protocol (Auditable Evolution)
71
229
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.29.2",
3
+ "version": "1.29.8",
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": {
package/src/evolve.js CHANGED
@@ -64,7 +64,9 @@ const TODAY_LOG = path.join(MEMORY_DIR, new Date().toISOString().split('T')[0] +
64
64
  // Ensure memory directory exists so state/cache writes work.
65
65
  try {
66
66
  if (!fs.existsSync(MEMORY_DIR)) fs.mkdirSync(MEMORY_DIR, { recursive: true });
67
- } catch (e) {}
67
+ } catch (e) {
68
+ console.warn('[Evolver] Failed to create MEMORY_DIR (may cause downstream errors):', e && e.message || e);
69
+ }
68
70
 
69
71
  function formatSessionLog(jsonlContent) {
70
72
  const result = [];
@@ -1041,7 +1043,9 @@ async function run() {
1041
1043
  try {
1042
1044
  const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
1043
1045
  taskMemoryEvents = tryReadMemoryGraphEvents(1000);
1044
- } catch {}
1046
+ } catch (e) {
1047
+ console.warn('[TaskReceiver] MemoryGraph read failed (task selection proceeds without history):', e && e.message || e);
1048
+ }
1045
1049
  const best = selectBestTask(hubTasks, taskMemoryEvents);
1046
1050
  if (best) {
1047
1051
  const alreadyClaimed = best.status === 'claimed';
@@ -1085,7 +1089,9 @@ async function run() {
1085
1089
  }
1086
1090
  }
1087
1091
  }
1088
- } catch {}
1092
+ } catch (e) {
1093
+ console.warn('[Commitment] Overdue task check failed (non-fatal):', e && e.message || e);
1094
+ }
1089
1095
 
1090
1096
  // --- Worker Pool: select task from heartbeat available_work (deferred claim) ---
1091
1097
  // Only remember the best task and inject its signals; actual claim+complete
@@ -1099,7 +1105,9 @@ async function run() {
1099
1105
  try {
1100
1106
  const { tryReadMemoryGraphEvents } = require('./gep/memoryGraph');
1101
1107
  taskMemoryEvents = tryReadMemoryGraphEvents(1000);
1102
- } catch {}
1108
+ } catch (e) {
1109
+ console.warn('[WorkerPool] MemoryGraph read failed (task selection proceeds without history):', e && e.message || e);
1110
+ }
1103
1111
  const best = selectBestTask(workerTasks, taskMemoryEvents);
1104
1112
  if (best) {
1105
1113
  activeTask = best;
@@ -1174,7 +1182,9 @@ async function run() {
1174
1182
  for (const c of newCandidates) {
1175
1183
  try {
1176
1184
  appendCandidateJsonl(c);
1177
- } catch (e) {}
1185
+ } catch (e) {
1186
+ console.warn('[Candidates] Failed to persist candidate:', e && e.message || e);
1187
+ }
1178
1188
  }
1179
1189
  const recentCandidates = readRecentCandidates(20);
1180
1190
  const capabilityCandidatesPreview = renderCandidatesPreview(recentCandidates.slice(-8), 1600);
@@ -1237,7 +1247,9 @@ async function run() {
1237
1247
  2
1238
1248
  )}\n\`\`\``;
1239
1249
  }
1240
- } catch (e) {}
1250
+ } catch (e) {
1251
+ console.warn('[ExternalCandidates] Preview build failed (non-fatal):', e && e.message || e);
1252
+ }
1241
1253
 
1242
1254
  // Search-First Evolution: query Hub for reusable solutions before local reasoning.
1243
1255
  let hubHit = null;
@@ -1425,7 +1437,9 @@ async function run() {
1425
1437
  .split('\n')
1426
1438
  .map(l => l.trim())
1427
1439
  .filter(Boolean);
1428
- } catch (e) {}
1440
+ } catch (e) {
1441
+ console.warn('[SolidifyState] Failed to read baseline untracked files:', e && e.message || e);
1442
+ }
1429
1443
 
1430
1444
  try {
1431
1445
  const out = execSync('git rev-parse HEAD', {
@@ -1436,7 +1450,9 @@ async function run() {
1436
1450
  windowsHide: true,
1437
1451
  });
1438
1452
  baselineHead = String(out || '').trim() || null;
1439
- } catch (e) {}
1453
+ } catch (e) {
1454
+ console.warn('[SolidifyState] Failed to read git HEAD:', e && e.message || e);
1455
+ }
1440
1456
 
1441
1457
  const maxFiles =
1442
1458
  selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
@@ -399,6 +399,7 @@ var _heartbeatStartedAt = null;
399
399
  var _heartbeatConsecutiveFailures = 0;
400
400
  var _heartbeatTotalSent = 0;
401
401
  var _heartbeatTotalFailed = 0;
402
+ var _heartbeatFpSent = false;
402
403
  var _latestAvailableWork = [];
403
404
  var _latestOverdueTasks = [];
404
405
  var _pendingCommitmentUpdates = [];
@@ -468,12 +469,14 @@ function sendHelloToHub() {
468
469
  }
469
470
 
470
471
  function getHubNodeSecret() {
472
+ if (process.env.A2A_NODE_SECRET) return process.env.A2A_NODE_SECRET;
471
473
  if (_cachedHubNodeSecret) return _cachedHubNodeSecret;
472
474
  var persisted = _loadPersistedNodeSecret();
473
475
  if (persisted) {
474
476
  _cachedHubNodeSecret = persisted;
475
477
  return persisted;
476
478
  }
479
+ if (process.env.A2A_HUB_TOKEN) return process.env.A2A_HUB_TOKEN;
477
480
  return null;
478
481
  }
479
482
 
@@ -516,6 +519,16 @@ function sendHeartbeat() {
516
519
  meta.commitment_updates = _pendingCommitmentUpdates.splice(0);
517
520
  }
518
521
 
522
+ if (!_heartbeatFpSent) {
523
+ try {
524
+ var fp = captureEnvFingerprint();
525
+ if (fp && fp.evolver_version) {
526
+ meta.env_fingerprint = fp;
527
+ _heartbeatFpSent = true;
528
+ }
529
+ } catch {}
530
+ }
531
+
519
532
  if (Object.keys(meta).length > 0) {
520
533
  bodyObj.meta = meta;
521
534
  }
@@ -177,11 +177,27 @@ function readRecentCandidates(limit = 20) {
177
177
  try {
178
178
  const p = candidatesPath();
179
179
  if (!fs.existsSync(p)) return [];
180
- const raw = fs.readFileSync(p, 'utf8');
181
- const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
182
- return lines.slice(Math.max(0, lines.length - limit)).map(l => {
183
- try { return JSON.parse(l); } catch { return null; }
184
- }).filter(Boolean);
180
+ const stat = fs.statSync(p);
181
+ if (stat.size < 1024 * 1024) {
182
+ const raw = fs.readFileSync(p, 'utf8');
183
+ const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
184
+ return lines.slice(-limit).map(l => {
185
+ try { return JSON.parse(l); } catch { return null; }
186
+ }).filter(Boolean);
187
+ }
188
+ // Large file (>1MB): only read the tail to avoid OOM.
189
+ const fd = fs.openSync(p, 'r');
190
+ try {
191
+ const chunkSize = Math.min(stat.size, limit * 4096);
192
+ const buf = Buffer.alloc(chunkSize);
193
+ fs.readSync(fd, buf, 0, chunkSize, stat.size - chunkSize);
194
+ const lines = buf.toString('utf8').split('\n').map(l => l.trim()).filter(Boolean);
195
+ return lines.slice(-limit).map(l => {
196
+ try { return JSON.parse(l); } catch { return null; }
197
+ }).filter(Boolean);
198
+ } finally {
199
+ fs.closeSync(fd);
200
+ }
185
201
  } catch { return []; }
186
202
  }
187
203
 
@@ -189,11 +205,26 @@ function readRecentExternalCandidates(limit = 50) {
189
205
  try {
190
206
  const p = externalCandidatesPath();
191
207
  if (!fs.existsSync(p)) return [];
192
- const raw = fs.readFileSync(p, 'utf8');
193
- const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
194
- return lines.slice(Math.max(0, lines.length - limit)).map(l => {
195
- try { return JSON.parse(l); } catch { return null; }
196
- }).filter(Boolean);
208
+ const stat = fs.statSync(p);
209
+ if (stat.size < 1024 * 1024) {
210
+ const raw = fs.readFileSync(p, 'utf8');
211
+ const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
212
+ return lines.slice(-limit).map(l => {
213
+ try { return JSON.parse(l); } catch { return null; }
214
+ }).filter(Boolean);
215
+ }
216
+ const fd = fs.openSync(p, 'r');
217
+ try {
218
+ const chunkSize = Math.min(stat.size, limit * 4096);
219
+ const buf = Buffer.alloc(chunkSize);
220
+ fs.readSync(fd, buf, 0, chunkSize, stat.size - chunkSize);
221
+ const lines = buf.toString('utf8').split('\n').map(l => l.trim()).filter(Boolean);
222
+ return lines.slice(-limit).map(l => {
223
+ try { return JSON.parse(l); } catch { return null; }
224
+ }).filter(Boolean);
225
+ } finally {
226
+ fs.closeSync(fd);
227
+ }
197
228
  } catch { return []; }
198
229
  }
199
230
 
package/src/gep/paths.js CHANGED
@@ -6,16 +6,33 @@ function getRepoRoot() {
6
6
  return process.env.EVOLVER_REPO_ROOT;
7
7
  }
8
8
 
9
- let dir = path.resolve(__dirname, '..', '..');
9
+ const ownDir = path.resolve(__dirname, '..', '..');
10
+
11
+ // Safety: check evolver's own directory first to prevent operating on a
12
+ // parent repo that happens to contain .git (which could cause data loss
13
+ // when git reset --hard runs in the wrong scope).
14
+ if (fs.existsSync(path.join(ownDir, '.git'))) {
15
+ return ownDir;
16
+ }
17
+
18
+ let dir = path.dirname(ownDir);
10
19
  while (dir !== '/' && dir !== '.') {
11
- const gitDir = path.join(dir, '.git');
12
- if (fs.existsSync(gitDir)) {
13
- return dir;
20
+ if (fs.existsSync(path.join(dir, '.git'))) {
21
+ if (process.env.EVOLVER_USE_PARENT_GIT === 'true') {
22
+ console.warn('[evolver] Using parent git repository at:', dir);
23
+ return dir;
24
+ }
25
+ console.warn(
26
+ '[evolver] Detected .git in parent directory', dir,
27
+ '-- ignoring. Set EVOLVER_USE_PARENT_GIT=true to override,',
28
+ 'or EVOLVER_REPO_ROOT to specify the target directory explicitly.'
29
+ );
30
+ return ownDir;
14
31
  }
15
32
  dir = path.dirname(dir);
16
33
  }
17
34
 
18
- return path.resolve(__dirname, '..', '..');
35
+ return ownDir;
19
36
  }
20
37
 
21
38
  function getWorkspaceRoot() {
package/src/gep/prompt.js CHANGED
@@ -134,10 +134,14 @@ ENSURE VALID JSON SYNTAX (escape quotes in strings).
134
134
 
135
135
  3. Gene (The Knowledge)
136
136
  - Reuse/update existing ID if possible. Create new only if novel pattern.
137
+ - ID MUST be descriptive: gene_<descriptive_name> (e.g., gene_retry_on_timeout)
138
+ - NEVER use timestamps, random numbers, or tool names (cursor, vscode, etc.) in IDs
139
+ - summary MUST be a clear human-readable sentence describing what the Gene does
137
140
  {
138
141
  "type": "Gene",
139
142
  "schema_version": "1.5.0",
140
- "id": "gene_<name>",
143
+ "id": "gene_<descriptive_name>",
144
+ "summary": "<clear description of what this gene does>",
141
145
  "category": "repair|optimize|innovate",
142
146
  "signals_match": ["<pattern>"],
143
147
  "preconditions": ["<condition>"],
@@ -439,24 +443,33 @@ When creating a new skill in skills/<name>/:
439
443
  |- assets/ (optional: templates, data files)
440
444
  Creating an empty directory or a directory missing index.js = FAILED.
441
445
  Do NOT create unnecessary files (README.md, CHANGELOG.md, INSTALLATION_GUIDE.md, etc.).
442
- 2. SKILL.MD FRONTMATTER: Every SKILL.md MUST start with YAML frontmatter:
446
+ 2. SKILL NAMING (CRITICAL):
447
+ a) <name> MUST be descriptive kebab-case (e.g., "log-rotation", "retry-handler", "cache-manager")
448
+ b) NEVER use timestamps, random numbers, tool names (cursor, vscode), or UUIDs as names
449
+ c) Names like "cursor-1773331925711", "skill-12345", "fix-1" = FAILED
450
+ d) Name must be 2-6 descriptive words separated by hyphens, conveying what the skill does
451
+ e) Good: "http-retry-with-backoff", "log-file-rotation", "config-validator"
452
+ f) Bad: "cursor-auto-1234", "new-skill", "test-skill", "my-skill"
453
+ 3. SKILL.MD FRONTMATTER: Every SKILL.md MUST start with YAML frontmatter:
443
454
  ---
444
455
  name: <skill-name>
445
456
  description: <what it does and when to use it>
446
457
  ---
458
+ The name MUST follow the naming rules above.
447
459
  The description is the triggering mechanism -- include WHAT the skill does and WHEN to use it.
448
- 3. CONCISENESS: SKILL.md body should be under 500 lines. Keep instructions lean.
460
+ Description must be a clear, complete sentence (min 20 chars). Generic descriptions = FAILED.
461
+ 4. CONCISENESS: SKILL.md body should be under 500 lines. Keep instructions lean.
449
462
  Only include information the agent does not already know. Move detailed reference
450
463
  material to references/ files, not into SKILL.md itself.
451
- 4. EXPORT VERIFICATION: Every exported function must be importable.
464
+ 5. EXPORT VERIFICATION: Every exported function must be importable.
452
465
  Run: node -e "const s = require('./skills/<name>'); console.log(Object.keys(s))"
453
466
  If this fails, the skill is broken. Fix before solidify.
454
- 5. NO HARDCODED SECRETS: Never embed API keys, tokens, or secrets in code.
467
+ 6. NO HARDCODED SECRETS: Never embed API keys, tokens, or secrets in code.
455
468
  Use process.env or .env references. Hardcoded App ID, App Secret, Bearer tokens = FAILED.
456
- 6. TEST BEFORE SOLIDIFY: Actually run the skill's core function to verify it works:
469
+ 7. TEST BEFORE SOLIDIFY: Actually run the skill's core function to verify it works:
457
470
  node -e "require('./skills/<name>').main ? require('./skills/<name>').main() : console.log('ok')"
458
471
  Scripts in scripts/ must also be tested by executing them.
459
- 7. ATOMIC CREATION: Create ALL files for a skill in a single cycle.
472
+ 8. ATOMIC CREATION: Create ALL files for a skill in a single cycle.
460
473
  Do not create a directory in one cycle and fill it in the next.
461
474
  Empty directories from failed cycles will be automatically cleaned up on rollback.
462
475
 
@@ -236,6 +236,20 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
236
236
  '- constraints.forbidden_paths MUST include at least [".git", "node_modules"]',
237
237
  '- Output valid Gene JSON only (no markdown, no explanation)',
238
238
  '',
239
+ 'GENE ID NAMING RULES (CRITICAL):',
240
+ '- The id suffix (after "' + DISTILLED_ID_PREFIX + '") MUST be a descriptive kebab-case name',
241
+ ' derived from the strategy content or signals_match (e.g., "retry-on-timeout", "log-rotation-cleanup")',
242
+ '- NEVER use timestamps, random numbers, tool names (cursor, vscode, etc.), or UUIDs in the id',
243
+ '- Good: "gene_distilled_retry-on-timeout", "gene_distilled_cache-invalidation-strategy"',
244
+ '- Bad: "gene_distilled_cursor-1773331925711", "gene_distilled_1234567890", "gene_distilled_fix-1"',
245
+ '- The id suffix must be 3+ words separated by hyphens, describing the core capability',
246
+ '',
247
+ 'SUMMARY RULES:',
248
+ '- The "summary" field MUST be a clear, human-readable description (10-200 chars)',
249
+ '- It should describe WHAT the Gene does, not implementation details',
250
+ '- Good: "Retry failed HTTP requests with exponential backoff and circuit breaker"',
251
+ '- Bad: "Distilled from capsules", "AI agent skill", "cursor automation"',
252
+ '',
239
253
  'SUCCESSFUL CAPSULES (grouped by pattern):',
240
254
  JSON.stringify(samples, null, 2),
241
255
  '',
@@ -246,7 +260,7 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
246
260
  JSON.stringify(analysis, null, 2),
247
261
  '',
248
262
  'Output a single Gene JSON object with these fields:',
249
- '{ "type": "Gene", "id": "gene_distilled_...", "category": "...", "signals_match": [...], "preconditions": [...], "strategy": [...], "constraints": { "max_files": N, "forbidden_paths": [...] }, "validation": [...] }',
263
+ '{ "type": "Gene", "id": "gene_distilled_<descriptive-kebab-name>", "summary": "<clear human-readable description>", "category": "...", "signals_match": [...], "preconditions": [...], "strategy": [...], "constraints": { "max_files": N, "forbidden_paths": [...] }, "validation": [...] }',
250
264
  ].join('\n');
251
265
  }
252
266
 
@@ -254,6 +268,36 @@ function distillRequestPath() {
254
268
  return path.join(paths.getMemoryDir(), 'distill_request.json');
255
269
  }
256
270
 
271
+ // ---------------------------------------------------------------------------
272
+ // Derive a descriptive ID from gene content when the LLM gives a bad name
273
+ // ---------------------------------------------------------------------------
274
+ function deriveDescriptiveId(gene) {
275
+ var words = [];
276
+ if (Array.isArray(gene.signals_match)) {
277
+ gene.signals_match.slice(0, 3).forEach(function (s) {
278
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
279
+ if (w.length >= 3 && words.length < 6) words.push(w);
280
+ });
281
+ });
282
+ }
283
+ if (words.length < 3 && gene.summary) {
284
+ var STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had']);
285
+ String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
286
+ if (w.length >= 3 && !STOP.has(w) && words.length < 6) words.push(w);
287
+ });
288
+ }
289
+ if (words.length < 3 && Array.isArray(gene.strategy) && gene.strategy.length > 0) {
290
+ String(gene.strategy[0]).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
291
+ if (w.length >= 3 && words.length < 6) words.push(w);
292
+ });
293
+ }
294
+ if (words.length < 2) words = ['auto', 'distilled', 'strategy'];
295
+ var unique = [];
296
+ var seen = new Set();
297
+ words.forEach(function (w) { if (!seen.has(w)) { seen.add(w); unique.push(w); } });
298
+ return DISTILLED_ID_PREFIX + unique.slice(0, 5).join('-');
299
+ }
300
+
257
301
  // ---------------------------------------------------------------------------
258
302
  // Step 4: validateSynthesizedGene
259
303
  // ---------------------------------------------------------------------------
@@ -271,6 +315,27 @@ function validateSynthesizedGene(gene, existingGenes) {
271
315
  gene.id = DISTILLED_ID_PREFIX + String(gene.id).replace(/^gene_/, '');
272
316
  }
273
317
 
318
+ if (gene.id) {
319
+ var suffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
320
+ var needsRename = /^\d+$/.test(suffix) || /^\d{10,}/.test(suffix)
321
+ || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-_]?\d*/i.test(suffix);
322
+ if (needsRename) {
323
+ gene.id = deriveDescriptiveId(gene);
324
+ }
325
+ var cleanSuffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
326
+ if (cleanSuffix.replace(/[-_]/g, '').length < 6) {
327
+ gene.id = deriveDescriptiveId(gene);
328
+ }
329
+ }
330
+
331
+ if (!gene.summary || typeof gene.summary !== 'string' || gene.summary.length < 10) {
332
+ if (Array.isArray(gene.strategy) && gene.strategy.length > 0) {
333
+ gene.summary = String(gene.strategy[0]).slice(0, 200);
334
+ } else if (Array.isArray(gene.signals_match) && gene.signals_match.length > 0) {
335
+ gene.summary = 'Strategy for: ' + gene.signals_match.slice(0, 3).join(', ');
336
+ }
337
+ }
338
+
274
339
  if (!gene.constraints || typeof gene.constraints !== 'object') gene.constraints = {};
275
340
  if (!Array.isArray(gene.constraints.forbidden_paths) || gene.constraints.forbidden_paths.length === 0) {
276
341
  gene.constraints.forbidden_paths = ['.git', 'node_modules'];
@@ -8,9 +8,38 @@ var { getHubUrl, buildHubHeaders, getNodeId } = require('./a2aProtocol');
8
8
  * @param {object} gene - Gene asset
9
9
  * @returns {string} SKILL.md content
10
10
  */
11
+ function sanitizeSkillName(rawName) {
12
+ var name = rawName.replace(/[\r\n]+/g, '-').replace(/^gene_distilled_/, '').replace(/^gene_/, '').replace(/_/g, '-');
13
+ if (/^\d{8,}/.test(name) || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-]?\d*$/i.test(name)) {
14
+ return null;
15
+ }
16
+ name = name.replace(/-?\d{10,}$/g, '').replace(/-+$/, '');
17
+ if (name.replace(/[-]/g, '').length < 6) return null;
18
+ return name;
19
+ }
20
+
11
21
  function geneToSkillMd(gene) {
12
- var name = (gene.id || 'unnamed-skill').replace(/^gene_distilled_/, '').replace(/_/g, '-');
13
- var desc = gene.summary || 'AI agent skill distilled from evolution experience.';
22
+ var rawName = gene.id || 'unnamed-skill';
23
+ var name = sanitizeSkillName(rawName);
24
+ if (!name) {
25
+ var fallbackWords = [];
26
+ if (Array.isArray(gene.signals_match)) {
27
+ gene.signals_match.slice(0, 3).forEach(function (s) {
28
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
29
+ if (w.length >= 3 && fallbackWords.length < 5) fallbackWords.push(w);
30
+ });
31
+ });
32
+ }
33
+ if (fallbackWords.length < 2 && gene.summary) {
34
+ String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
35
+ if (w.length >= 3 && fallbackWords.length < 5) fallbackWords.push(w);
36
+ });
37
+ }
38
+ var seen = {};
39
+ fallbackWords = fallbackWords.filter(function (w) { if (seen[w]) return false; seen[w] = true; return true; });
40
+ name = fallbackWords.length >= 2 ? fallbackWords.join('-') : 'auto-distilled-skill';
41
+ }
42
+ var desc = (gene.summary || 'AI agent skill distilled from evolution experience.').replace(/[\r\n]+/g, ' ').trim();
14
43
 
15
44
  var lines = [
16
45
  '---',
@@ -72,6 +101,11 @@ function geneToSkillMd(gene) {
72
101
  });
73
102
  }
74
103
 
104
+ lines.push('---');
105
+ lines.push('');
106
+ lines.push('*This Skill was generated by [Evolver](https://github.com/autogame-17/evolver) and is distributed under the [EvoMap Skill License (ESL-1.0)](https://evomap.ai/terms). Unauthorized redistribution, bulk scraping, or republishing is prohibited. See LICENSE file for full terms.*');
107
+ lines.push('');
108
+
75
109
  return lines.join('\n');
76
110
  }
77
111
 
@@ -89,7 +123,9 @@ function publishSkillToHub(gene, opts) {
89
123
 
90
124
  var content = geneToSkillMd(gene);
91
125
  var nodeId = getNodeId();
92
- var skillId = 'skill_' + (gene.id || 'unnamed').replace(/^gene_/, '');
126
+ var fmName = content.match(/^name:\s*(.+)$/m);
127
+ var derivedName = fmName ? fmName[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '_') : (gene.id || 'unnamed').replace(/^gene_/, '');
128
+ var skillId = 'skill_' + derivedName;
93
129
 
94
130
  var body = {
95
131
  sender_id: nodeId,
@@ -146,8 +182,13 @@ function updateSkillOnHub(nodeId, skillId, content, opts, gene) {
146
182
  body: JSON.stringify(body),
147
183
  signal: AbortSignal.timeout(15000),
148
184
  })
149
- .then(function (res) { return res.json(); })
150
- .then(function (data) { return { ok: true, result: data }; })
185
+ .then(function (res) { return res.json().then(function (data) { return { status: res.status, data: data }; }); })
186
+ .then(function (result) {
187
+ if (result.status >= 200 && result.status < 300) {
188
+ return { ok: true, result: result.data };
189
+ }
190
+ return { ok: false, error: result.data?.error || 'update_failed', status: result.status };
191
+ })
151
192
  .catch(function (err) { return { ok: false, error: err.message }; });
152
193
  }
153
194
 
@@ -136,6 +136,7 @@ function readOpenclawConstraintPolicy() {
136
136
  includeExtensions: Array.isArray(pol.includeExtensions) ? pol.includeExtensions.map(String) : defaults.includeExtensions,
137
137
  };
138
138
  } catch (_) {
139
+ console.warn('[evolver] readOpenclawConstraintPolicy failed:', _ && _.message || _);
139
140
  return defaults;
140
141
  }
141
142
  }
@@ -159,7 +160,9 @@ function matchAnyRegex(rel, regexList) {
159
160
  for (const raw of Array.isArray(regexList) ? regexList : []) {
160
161
  try {
161
162
  if (new RegExp(String(raw), 'i').test(rel)) return true;
162
- } catch (_) {}
163
+ } catch (_) {
164
+ console.warn('[evolver] matchAnyRegex invalid pattern:', raw, _ && _.message || _);
165
+ }
163
166
  }
164
167
  return false;
165
168
  }
@@ -339,7 +342,9 @@ function checkConstraints({ gene, blast, blastRadiusEstimate, repoRoot }) {
339
342
  if (entries.length < 2) {
340
343
  warnings.push('incomplete_skill: skills/' + skillName + '/ has only ' + entries.length + ' file(s). New skills should have at least index.js + SKILL.md.');
341
344
  }
342
- } catch (e) { /* dir might not exist yet */ }
345
+ } catch (e) {
346
+ console.warn('[evolver] checkConstraints skill dir read failed:', skillName, e && e.message || e);
347
+ }
343
348
  });
344
349
  }
345
350
 
@@ -381,7 +386,9 @@ function writeStateForSolidify(state) {
381
386
  const statePath = path.join(getEvolutionDir(), 'evolution_solidify_state.json');
382
387
  try {
383
388
  if (!fs.existsSync(memoryDir)) fs.mkdirSync(memoryDir, { recursive: true });
384
- } catch {}
389
+ } catch (e) {
390
+ console.warn('[evolver] writeStateForSolidify mkdir failed:', memoryDir, e && e.message || e);
391
+ }
385
392
  const tmp = `${statePath}.tmp`;
386
393
  fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
387
394
  fs.renameSync(tmp, statePath);
@@ -559,7 +566,9 @@ function detectDestructiveChanges({ repoRoot, changedFiles, baselineUntracked })
559
566
  if (stat.isFile() && stat.size === 0) {
560
567
  violations.push(`CRITICAL_FILE_EMPTIED: ${norm}`);
561
568
  }
562
- } catch (e) {}
569
+ } catch (e) {
570
+ console.warn('[evolver] detectDestructiveChanges stat failed:', norm, e && e.message || e);
571
+ }
563
572
  }
564
573
  }
565
574
  }
@@ -716,7 +725,9 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
716
725
  fs.unlinkSync(normAbs);
717
726
  deleted.push(safeRel);
718
727
  }
719
- } catch (e) {}
728
+ } catch (e) {
729
+ console.warn('[evolver] rollbackNewUntrackedFiles unlink failed:', safeRel, e && e.message || e);
730
+ }
720
731
  }
721
732
  if (skipped.length > 0) {
722
733
  console.log(`[Rollback] Skipped ${skipped.length} critical protected file(s): ${skipped.slice(0, 5).join(', ')}`);
@@ -749,7 +760,9 @@ function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
749
760
  fs.rmdirSync(dirAbs);
750
761
  removedDirs.push(sortedDirs[si]);
751
762
  }
752
- } catch (e) { /* ignore -- dir may already be gone */ }
763
+ } catch (e) {
764
+ console.warn('[evolver] rollbackNewUntrackedFiles rmdir failed:', sortedDirs[si], e && e.message || e);
765
+ }
753
766
  }
754
767
  if (removedDirs.length > 0) {
755
768
  console.log('[Rollback] Removed ' + removedDirs.length + ' empty director' + (removedDirs.length === 1 ? 'y' : 'ies') + ': ' + removedDirs.slice(0, 5).join(', '));
@@ -1226,7 +1239,9 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1226
1239
  const list = require('./assetStore').loadCapsules();
1227
1240
  prevCapsule = Array.isArray(list) ? list.find(c => c && c.type === 'Capsule' && String(c.id) === selectedCapsuleId) : null;
1228
1241
  }
1229
- } catch (e) {}
1242
+ } catch (e) {
1243
+ console.warn('[evolver] solidify loadCapsules failed:', e && e.message || e);
1244
+ }
1230
1245
  const successReason = buildSuccessReason({ gene: geneUsed, signals, blast, mutation, score });
1231
1246
  const capsuleDiff = captureDiffSnapshot(repoRoot);
1232
1247
  const capsuleContent = buildCapsuleContent({ intent, gene: geneUsed, signals, blast, mutation, score });
@@ -1302,7 +1317,7 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1302
1317
  applyEpigeneticMarks(geneUsed, envFp, outcomeStatus);
1303
1318
  upsertGene(geneUsed);
1304
1319
  } catch (e) {
1305
- // Non-blocking: epigenetic mark failure must not break solidify
1320
+ console.warn('[evolver] applyEpigeneticMarks failed (non-blocking):', e && e.message || e);
1306
1321
  }
1307
1322
  }
1308
1323
 
@@ -1326,7 +1341,9 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1326
1341
  if (personalityState) {
1327
1342
  updatePersonalityStats({ personalityState, outcome: outcomeStatus, score, notes: `event:${event.id}` });
1328
1343
  }
1329
- } catch (e) {}
1344
+ } catch (e) {
1345
+ console.warn('[evolver] updatePersonalityStats failed:', e && e.message || e);
1346
+ }
1330
1347
  }
1331
1348
 
1332
1349
  const runId = lastRun && lastRun.run_id ? String(lastRun.run_id) : stableHash(`${parentEventId || 'root'}|${geneId || 'none'}|${signalKey}`);