@evomap/evolver 1.29.4 → 1.29.9

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.4",
3
+ "version": "1.29.9",
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": {
@@ -469,12 +469,14 @@ function sendHelloToHub() {
469
469
  }
470
470
 
471
471
  function getHubNodeSecret() {
472
+ if (process.env.A2A_NODE_SECRET) return process.env.A2A_NODE_SECRET;
472
473
  if (_cachedHubNodeSecret) return _cachedHubNodeSecret;
473
474
  var persisted = _loadPersistedNodeSecret();
474
475
  if (persisted) {
475
476
  _cachedHubNodeSecret = persisted;
476
477
  return persisted;
477
478
  }
479
+ if (process.env.A2A_HUB_TOKEN) return process.env.A2A_HUB_TOKEN;
478
480
  return null;
479
481
  }
480
482
 
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
 
@@ -223,18 +223,78 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
223
223
  });
224
224
 
225
225
  return [
226
- 'You are a Gene synthesis engine for the GEP (Gene Expression Protocol).',
226
+ 'You are a Gene synthesis engine for the GEP (Genome Evolution Protocol).',
227
+ 'Your job is to distill successful evolution capsules into a high-quality, reusable Gene',
228
+ 'that other AI agents can discover, fetch, and execute.',
227
229
  '',
228
- 'Analyze the following successful evolution capsules and extract a reusable Gene.',
230
+ '## OUTPUT FORMAT',
231
+ '',
232
+ 'Output ONLY a single valid JSON object (no markdown fences, no explanation).',
233
+ '',
234
+ '## GENE ID RULES (CRITICAL)',
235
+ '',
236
+ '- The id MUST start with "' + DISTILLED_ID_PREFIX + '" followed by a descriptive kebab-case name.',
237
+ '- The suffix MUST describe the core capability in 3-6 hyphen-separated words.',
238
+ '- NEVER include timestamps, numeric IDs, random numbers, tool names (cursor, vscode, etc.), or UUIDs.',
239
+ '- Good: "gene_distilled_retry-with-exponential-backoff", "gene_distilled_database-migration-rollback"',
240
+ '- Bad: "gene_distilled_cursor-1773331925711", "gene_distilled_1234567890", "gene_distilled_fix-1"',
241
+ '',
242
+ '## SUMMARY RULES',
243
+ '',
244
+ '- The "summary" MUST be a clear, human-readable sentence (30-200 chars) describing',
245
+ ' WHAT capability this Gene provides and WHY it is useful.',
246
+ '- Write as if for a marketplace listing -- the summary is the first thing other agents see.',
247
+ '- Good: "Retry failed HTTP requests with exponential backoff, jitter, and circuit breaker to prevent cascade failures"',
248
+ '- Bad: "Distilled from capsules", "AI agent skill", "cursor automation", "1773331925711"',
249
+ '- NEVER include timestamps, build numbers, or tool names in the summary.',
250
+ '',
251
+ '## SIGNALS_MATCH RULES',
252
+ '',
253
+ '- Each signal MUST be a generic, reusable keyword that describes WHEN to trigger this Gene.',
254
+ '- Use lowercase_snake_case. Signals should be domain terms, not implementation artifacts.',
255
+ '- NEVER include timestamps, build numbers, tool names, session IDs, or random suffixes.',
256
+ '- Include 3-7 signals covering both the problem domain and the solution approach.',
257
+ '- Good: ["http_retry", "request_timeout", "exponential_backoff", "circuit_breaker", "resilience"]',
258
+ '- Bad: ["cursor_auto_1773331925711", "cli_headless_1773331925711", "bypass_123"]',
259
+ '',
260
+ '## STRATEGY RULES',
261
+ '',
262
+ '- Strategy steps MUST be actionable, concrete instructions an AI agent can execute.',
263
+ '- Each step should be a clear imperative sentence starting with a verb.',
264
+ '- Include 5-10 steps. Each step should be self-contained and specific.',
265
+ '- Do NOT describe what happened; describe what TO DO.',
266
+ '- Include rationale or context in parentheses when non-obvious.',
267
+ '- Where applicable, include inline code examples using backtick notation.',
268
+ '- Good: "Wrap the HTTP call in a retry loop with `maxRetries=3` and initial delay of 500ms"',
269
+ '- Bad: "Handle retries", "Fix the issue", "Improve reliability"',
270
+ '',
271
+ '## PRECONDITIONS RULES',
272
+ '',
273
+ '- List concrete, verifiable conditions that must be true before applying this Gene.',
274
+ '- Each precondition should be a testable statement, not a vague requirement.',
275
+ '- Good: "Project uses Node.js >= 18 with ES module support"',
276
+ '- Bad: "need to fix something"',
277
+ '',
278
+ '## CONSTRAINTS',
229
279
  '',
230
- 'RULES:',
231
- '- Strategy steps MUST be actionable operations, NOT summaries',
232
- '- Each step must be a concrete instruction an AI agent can execute',
233
- '- Do NOT describe what happened; describe what TO DO next time',
234
- '- The Gene MUST have a unique id starting with "' + DISTILLED_ID_PREFIX + '"',
235
280
  '- constraints.max_files MUST be <= ' + DISTILLED_MAX_FILES,
236
281
  '- constraints.forbidden_paths MUST include at least [".git", "node_modules"]',
237
- '- Output valid Gene JSON only (no markdown, no explanation)',
282
+ '',
283
+ '## VALIDATION',
284
+ '',
285
+ '- Validation commands MUST start with "node ", "npm ", or "npx " (security constraint).',
286
+ '- Include commands that actually verify the Gene was applied correctly.',
287
+ '- Good: "npx tsc --noEmit", "npm test"',
288
+ '- Bad: "node -v" (proves nothing about the Gene)',
289
+ '',
290
+ '## QUALITY BAR',
291
+ '',
292
+ 'Imagine this Gene will be published on a marketplace for thousands of AI agents.',
293
+ 'It should be as professional and useful as a well-written library README.',
294
+ 'Ask yourself: "Would another agent find this Gene by searching for the signals?',
295
+ 'Would the summary make them want to fetch it? Would the strategy be enough to execute?"',
296
+ '',
297
+ '---',
238
298
  '',
239
299
  'SUCCESSFUL CAPSULES (grouped by pattern):',
240
300
  JSON.stringify(samples, null, 2),
@@ -246,7 +306,7 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
246
306
  JSON.stringify(analysis, null, 2),
247
307
  '',
248
308
  '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": [...] }',
309
+ '{ "type": "Gene", "id": "gene_distilled_<descriptive-kebab-name>", "summary": "<clear marketplace-quality description>", "category": "repair|optimize|innovate", "signals_match": ["generic_signal_1", ...], "preconditions": ["Concrete condition 1", ...], "strategy": ["Step 1: verb ...", "Step 2: verb ...", ...], "constraints": { "max_files": N, "forbidden_paths": [".git", "node_modules", ...] }, "validation": ["npx tsc --noEmit", ...], "schema_version": "1.6.0" }',
250
310
  ].join('\n');
251
311
  }
252
312
 
@@ -254,6 +314,64 @@ function distillRequestPath() {
254
314
  return path.join(paths.getMemoryDir(), 'distill_request.json');
255
315
  }
256
316
 
317
+ // ---------------------------------------------------------------------------
318
+ // Derive a descriptive ID from gene content when the LLM gives a bad name
319
+ // ---------------------------------------------------------------------------
320
+ function deriveDescriptiveId(gene) {
321
+ var words = [];
322
+ if (Array.isArray(gene.signals_match)) {
323
+ gene.signals_match.slice(0, 3).forEach(function (s) {
324
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
325
+ if (w.length >= 3 && words.length < 6) words.push(w);
326
+ });
327
+ });
328
+ }
329
+ if (words.length < 3 && gene.summary) {
330
+ var STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had']);
331
+ String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
332
+ if (w.length >= 3 && !STOP.has(w) && words.length < 6) words.push(w);
333
+ });
334
+ }
335
+ if (words.length < 3 && Array.isArray(gene.strategy) && gene.strategy.length > 0) {
336
+ String(gene.strategy[0]).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
337
+ if (w.length >= 3 && words.length < 6) words.push(w);
338
+ });
339
+ }
340
+ if (words.length < 2) words = ['auto', 'distilled', 'strategy'];
341
+ var unique = [];
342
+ var seen = new Set();
343
+ words.forEach(function (w) { if (!seen.has(w)) { seen.add(w); unique.push(w); } });
344
+ return DISTILLED_ID_PREFIX + unique.slice(0, 5).join('-');
345
+ }
346
+
347
+ // ---------------------------------------------------------------------------
348
+ // Step 4: sanitizeSignalsMatch -- strip timestamps, random suffixes, tool names
349
+ // ---------------------------------------------------------------------------
350
+ function sanitizeSignalsMatch(signals) {
351
+ if (!Array.isArray(signals)) return [];
352
+ var cleaned = [];
353
+ signals.forEach(function (s) {
354
+ var sig = String(s || '').trim().toLowerCase();
355
+ if (!sig) return;
356
+ // Strip trailing timestamps (10+ digits) and random suffixes
357
+ sig = sig.replace(/[_-]\d{10,}$/g, '');
358
+ // Strip leading/trailing underscores/hyphens left over
359
+ sig = sig.replace(/^[_-]+|[_-]+$/g, '');
360
+ // Reject signals that are purely numeric
361
+ if (/^\d+$/.test(sig)) return;
362
+ // Reject signals that are just a tool name with optional number
363
+ if (/^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex|bypass|distill)[_-]?\d*$/i.test(sig)) return;
364
+ // Reject signals shorter than 3 chars after cleaning
365
+ if (sig.length < 3) return;
366
+ // Reject signals that still contain long numeric sequences (session IDs, etc.)
367
+ if (/\d{8,}/.test(sig)) return;
368
+ cleaned.push(sig);
369
+ });
370
+ // Deduplicate
371
+ var seen = {};
372
+ return cleaned.filter(function (s) { if (seen[s]) return false; seen[s] = true; return true; });
373
+ }
374
+
257
375
  // ---------------------------------------------------------------------------
258
376
  // Step 4: validateSynthesizedGene
259
377
  // ---------------------------------------------------------------------------
@@ -267,10 +385,56 @@ function validateSynthesizedGene(gene, existingGenes) {
267
385
  if (!Array.isArray(gene.signals_match) || gene.signals_match.length === 0) errors.push('missing or empty signals_match');
268
386
  if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) errors.push('missing or empty strategy');
269
387
 
388
+ // --- Signals sanitization (BEFORE id derivation so deriveDescriptiveId uses clean signals) ---
389
+ if (Array.isArray(gene.signals_match)) {
390
+ gene.signals_match = sanitizeSignalsMatch(gene.signals_match);
391
+ if (gene.signals_match.length === 0) {
392
+ errors.push('signals_match is empty after sanitization (all signals were invalid)');
393
+ }
394
+ }
395
+
396
+ // --- Summary sanitization (BEFORE id derivation so deriveDescriptiveId uses clean summary) ---
397
+ if (gene.summary) {
398
+ gene.summary = gene.summary.replace(/\s*\d{10,}\s*$/g, '').replace(/\.\s*\d{10,}/g, '.').trim();
399
+ }
400
+
401
+ // --- ID sanitization ---
270
402
  if (gene.id && !String(gene.id).startsWith(DISTILLED_ID_PREFIX)) {
271
403
  gene.id = DISTILLED_ID_PREFIX + String(gene.id).replace(/^gene_/, '');
272
404
  }
273
405
 
406
+ if (gene.id) {
407
+ var suffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
408
+ // Strip ALL embedded timestamps (10+ digit sequences) anywhere in the id
409
+ suffix = suffix.replace(/[-_]?\d{10,}[-_]?/g, '-').replace(/[-_]+/g, '-').replace(/^[-_]+|[-_]+$/g, '');
410
+ var needsRename = /^\d+$/.test(suffix) || /^\d{10,}/.test(suffix)
411
+ || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-_]?\d*$/i.test(suffix);
412
+ if (needsRename) {
413
+ gene.id = deriveDescriptiveId(gene);
414
+ } else {
415
+ gene.id = DISTILLED_ID_PREFIX + suffix;
416
+ }
417
+ var cleanSuffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
418
+ if (cleanSuffix.replace(/[-_]/g, '').length < 6) {
419
+ gene.id = deriveDescriptiveId(gene);
420
+ }
421
+ }
422
+
423
+ // --- Summary fallback (summary was already sanitized above, this handles missing/short) ---
424
+ if (!gene.summary || typeof gene.summary !== 'string' || gene.summary.length < 10) {
425
+ if (Array.isArray(gene.strategy) && gene.strategy.length > 0) {
426
+ gene.summary = String(gene.strategy[0]).slice(0, 200);
427
+ } else if (Array.isArray(gene.signals_match) && gene.signals_match.length > 0) {
428
+ gene.summary = 'Strategy for: ' + gene.signals_match.slice(0, 3).join(', ');
429
+ }
430
+ }
431
+
432
+ // --- Strategy quality: require minimum 3 steps ---
433
+ if (Array.isArray(gene.strategy) && gene.strategy.length < 3) {
434
+ errors.push('strategy must have at least 3 steps for a quality skill');
435
+ }
436
+
437
+ // --- Constraints ---
274
438
  if (!gene.constraints || typeof gene.constraints !== 'object') gene.constraints = {};
275
439
  if (!Array.isArray(gene.constraints.forbidden_paths) || gene.constraints.forbidden_paths.length === 0) {
276
440
  gene.constraints.forbidden_paths = ['.git', 'node_modules'];
@@ -282,6 +446,7 @@ function validateSynthesizedGene(gene, existingGenes) {
282
446
  gene.constraints.max_files = DISTILLED_MAX_FILES;
283
447
  }
284
448
 
449
+ // --- Validation command sanitization ---
285
450
  var ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
286
451
  if (Array.isArray(gene.validation)) {
287
452
  gene.validation = gene.validation.filter(function (cmd) {
@@ -294,11 +459,16 @@ function validateSynthesizedGene(gene, existingGenes) {
294
459
  });
295
460
  }
296
461
 
462
+ // --- Schema version ---
463
+ if (!gene.schema_version) gene.schema_version = '1.6.0';
464
+
465
+ // --- Duplicate ID check ---
297
466
  var existingIds = new Set((existingGenes || []).map(function (g) { return g.id; }));
298
467
  if (gene.id && existingIds.has(gene.id)) {
299
468
  gene.id = gene.id + '_' + Date.now().toString(36);
300
469
  }
301
470
 
471
+ // --- Signal overlap check ---
302
472
  if (gene.signals_match && existingGenes && existingGenes.length > 0) {
303
473
  var newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); }));
304
474
  for (var i = 0; i < existingGenes.length; i++) {
@@ -501,6 +671,7 @@ module.exports = {
501
671
  prepareDistillation: prepareDistillation,
502
672
  completeDistillation: completeDistillation,
503
673
  validateSynthesizedGene: validateSynthesizedGene,
674
+ sanitizeSignalsMatch: sanitizeSignalsMatch,
504
675
  shouldDistill: shouldDistill,
505
676
  buildDistillationPrompt: buildDistillationPrompt,
506
677
  extractJsonFromLlmResponse: extractJsonFromLlmResponse,
@@ -3,25 +3,90 @@
3
3
  var { getHubUrl, buildHubHeaders, getNodeId } = require('./a2aProtocol');
4
4
 
5
5
  /**
6
- * Convert a Gene object into SKILL.md format (Claude/Anthropic style).
6
+ * Sanitize a raw gene id into a human-readable kebab-case skill name.
7
+ * Returns null if the name is unsalvageable (pure numbers, tool name, etc.).
8
+ */
9
+ function sanitizeSkillName(rawName) {
10
+ var name = rawName.replace(/[\r\n]+/g, '-').replace(/^gene_distilled_/, '').replace(/^gene_/, '').replace(/_/g, '-');
11
+ // Strip ALL embedded timestamps (10+ digit sequences) anywhere in the name
12
+ name = name.replace(/-?\d{10,}-?/g, '-').replace(/-+/g, '-').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
+ if (name.replace(/[-]/g, '').length < 6) return null;
17
+ return name;
18
+ }
19
+
20
+ /**
21
+ * Derive a Title Case display name from a kebab-case skill name.
22
+ * "retry-with-backoff" -> "Retry With Backoff"
23
+ */
24
+ function toTitleCase(kebabName) {
25
+ return kebabName.split('-').map(function (w) {
26
+ if (!w) return '';
27
+ return w.charAt(0).toUpperCase() + w.slice(1);
28
+ }).join(' ');
29
+ }
30
+
31
+ /**
32
+ * Derive fallback name words from gene signals/summary when id is not usable.
33
+ */
34
+ function deriveFallbackName(gene) {
35
+ var fallbackWords = [];
36
+ var STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had', 'not', 'but', 'its']);
37
+ if (Array.isArray(gene.signals_match)) {
38
+ gene.signals_match.slice(0, 3).forEach(function (s) {
39
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
40
+ if (w.length >= 3 && !STOP.has(w) && fallbackWords.length < 5) fallbackWords.push(w);
41
+ });
42
+ });
43
+ }
44
+ if (fallbackWords.length < 2 && gene.summary) {
45
+ String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
46
+ if (w.length >= 3 && !STOP.has(w) && fallbackWords.length < 5) fallbackWords.push(w);
47
+ });
48
+ }
49
+ var seen = {};
50
+ fallbackWords = fallbackWords.filter(function (w) { if (seen[w]) return false; seen[w] = true; return true; });
51
+ return fallbackWords.length >= 2 ? fallbackWords.join('-') : 'auto-distilled-skill';
52
+ }
53
+
54
+ /**
55
+ * Convert a Gene object into SKILL.md format -- marketplace-quality content.
7
56
  *
8
57
  * @param {object} gene - Gene asset
9
58
  * @returns {string} SKILL.md content
10
59
  */
11
60
  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.';
61
+ var rawName = gene.id || 'unnamed-skill';
62
+ var name = sanitizeSkillName(rawName) || deriveFallbackName(gene);
63
+ var displayName = toTitleCase(name);
64
+ var desc = (gene.summary || '').replace(/[\r\n]+/g, ' ').replace(/\s*\d{10,}\s*$/g, '').trim();
65
+ if (!desc || desc.length < 10) desc = 'AI agent skill distilled from evolution experience.';
14
66
 
15
67
  var lines = [
16
68
  '---',
17
- 'name: ' + name,
69
+ 'name: ' + displayName,
18
70
  'description: ' + desc,
19
71
  '---',
20
72
  '',
21
- '# ' + name,
73
+ '# ' + displayName,
74
+ '',
75
+ desc,
22
76
  '',
23
77
  ];
24
78
 
79
+ // -- When to Use (derived from signals; preconditions go in their own section) --
80
+ if (gene.signals_match && gene.signals_match.length > 0) {
81
+ lines.push('## When to Use');
82
+ lines.push('');
83
+ lines.push('- When your project encounters: ' + gene.signals_match.slice(0, 4).map(function (s) {
84
+ return '`' + s + '`';
85
+ }).join(', '));
86
+ lines.push('');
87
+ }
88
+
89
+ // -- Trigger Signals --
25
90
  if (gene.signals_match && gene.signals_match.length > 0) {
26
91
  lines.push('## Trigger Signals');
27
92
  lines.push('');
@@ -31,6 +96,7 @@ function geneToSkillMd(gene) {
31
96
  lines.push('');
32
97
  }
33
98
 
99
+ // -- Preconditions --
34
100
  if (gene.preconditions && gene.preconditions.length > 0) {
35
101
  lines.push('## Preconditions');
36
102
  lines.push('');
@@ -40,27 +106,36 @@ function geneToSkillMd(gene) {
40
106
  lines.push('');
41
107
  }
42
108
 
109
+ // -- Strategy --
43
110
  if (gene.strategy && gene.strategy.length > 0) {
44
111
  lines.push('## Strategy');
45
112
  lines.push('');
46
113
  gene.strategy.forEach(function (step, i) {
47
- lines.push((i + 1) + '. ' + step);
114
+ var text = String(step);
115
+ var verb = extractStepVerb(text);
116
+ if (verb) {
117
+ lines.push((i + 1) + '. **' + verb + '** -- ' + stripLeadingVerb(text));
118
+ } else {
119
+ lines.push((i + 1) + '. ' + text);
120
+ }
48
121
  });
49
122
  lines.push('');
50
123
  }
51
124
 
125
+ // -- Constraints --
52
126
  if (gene.constraints) {
53
127
  lines.push('## Constraints');
54
128
  lines.push('');
55
129
  if (gene.constraints.max_files) {
56
- lines.push('- Max files: ' + gene.constraints.max_files);
130
+ lines.push('- Max files per invocation: ' + gene.constraints.max_files);
57
131
  }
58
132
  if (gene.constraints.forbidden_paths && gene.constraints.forbidden_paths.length > 0) {
59
- lines.push('- Forbidden paths: ' + gene.constraints.forbidden_paths.join(', '));
133
+ lines.push('- Forbidden paths: ' + gene.constraints.forbidden_paths.map(function (p) { return '`' + p + '`'; }).join(', '));
60
134
  }
61
135
  lines.push('');
62
136
  }
63
137
 
138
+ // -- Validation --
64
139
  if (gene.validation && gene.validation.length > 0) {
65
140
  lines.push('## Validation');
66
141
  lines.push('');
@@ -72,6 +147,16 @@ function geneToSkillMd(gene) {
72
147
  });
73
148
  }
74
149
 
150
+ // -- Metadata --
151
+ lines.push('## Metadata');
152
+ lines.push('');
153
+ lines.push('- Category: `' + (gene.category || 'innovate') + '`');
154
+ lines.push('- Schema version: `' + (gene.schema_version || '1.6.0') + '`');
155
+ if (gene._distilled_meta && gene._distilled_meta.source_capsule_count) {
156
+ lines.push('- Distilled from: ' + gene._distilled_meta.source_capsule_count + ' successful capsules');
157
+ }
158
+ lines.push('');
159
+
75
160
  lines.push('---');
76
161
  lines.push('');
77
162
  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.*');
@@ -80,6 +165,31 @@ function geneToSkillMd(gene) {
80
165
  return lines.join('\n');
81
166
  }
82
167
 
168
+ /**
169
+ * Extract the leading verb from a strategy step for bolding.
170
+ * Only extracts a single verb to avoid splitting compound phrases.
171
+ * e.g. "Verify Cursor CLI installation" -> "Verify"
172
+ * "Run `npm test` to check" -> "Run"
173
+ * "Configure non-interactive mode" -> "Configure"
174
+ */
175
+ function extractStepVerb(step) {
176
+ // Only match a capitalized verb at the very start (no leading backtick/special chars)
177
+ var match = step.match(/^([A-Z][a-z]+)/);
178
+ return match ? match[1] : '';
179
+ }
180
+
181
+ /**
182
+ * Remove the leading verb from a step (already shown in bold).
183
+ */
184
+ function stripLeadingVerb(step) {
185
+ var verb = extractStepVerb(step);
186
+ if (verb && step.startsWith(verb)) {
187
+ var rest = step.slice(verb.length).replace(/^[\s:.\-]+/, '');
188
+ return rest || step;
189
+ }
190
+ return step;
191
+ }
192
+
83
193
  /**
84
194
  * Publish a Gene as a Skill to the Hub skill store.
85
195
  *
@@ -92,16 +202,37 @@ function publishSkillToHub(gene, opts) {
92
202
  var hubUrl = getHubUrl();
93
203
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
94
204
 
95
- var content = geneToSkillMd(gene);
205
+ // Shallow-copy gene to avoid mutating the caller's object
206
+ var geneCopy = {};
207
+ Object.keys(gene).forEach(function (k) { geneCopy[k] = gene[k]; });
208
+ if (Array.isArray(geneCopy.signals_match)) {
209
+ try {
210
+ var distiller = require('./skillDistiller');
211
+ geneCopy.signals_match = distiller.sanitizeSignalsMatch(geneCopy.signals_match);
212
+ } catch (e) { /* distiller not available, skip */ }
213
+ }
214
+
215
+ var content = geneToSkillMd(geneCopy);
96
216
  var nodeId = getNodeId();
97
- var skillId = 'skill_' + (gene.id || 'unnamed').replace(/^gene_/, '');
217
+ var fmName = content.match(/^name:\s*(.+)$/m);
218
+ var derivedName = fmName ? fmName[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, '_') : (gene.id || 'unnamed').replace(/^gene_/, '');
219
+ // Strip ALL embedded timestamps from skillId
220
+ derivedName = derivedName.replace(/_?\d{10,}_?/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
221
+ var skillId = 'skill_' + derivedName;
222
+
223
+ // Clean tags: use already-sanitized signals from geneCopy
224
+ var tags = opts.tags || geneCopy.signals_match || [];
225
+ tags = tags.filter(function (t) {
226
+ var s = String(t || '').trim();
227
+ return s.length >= 3 && !/^\d+$/.test(s) && !/\d{10,}/.test(s);
228
+ });
98
229
 
99
230
  var body = {
100
231
  sender_id: nodeId,
101
232
  skill_id: skillId,
102
233
  content: content,
103
- category: opts.category || gene.category || null,
104
- tags: opts.tags || gene.signals_match || [],
234
+ category: opts.category || geneCopy.category || null,
235
+ tags: tags,
105
236
  };
106
237
 
107
238
  var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/skill/store/publish';
@@ -134,12 +265,18 @@ function updateSkillOnHub(nodeId, skillId, content, opts, gene) {
134
265
  var hubUrl = getHubUrl();
135
266
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
136
267
 
268
+ var tags = opts.tags || gene.signals_match || [];
269
+ tags = tags.filter(function (t) {
270
+ var s = String(t || '').trim();
271
+ return s.length >= 3 && !/^\d+$/.test(s) && !/\d{10,}/.test(s);
272
+ });
273
+
137
274
  var body = {
138
275
  sender_id: nodeId,
139
276
  skill_id: skillId,
140
277
  content: content,
141
278
  category: opts.category || gene.category || null,
142
- tags: opts.tags || gene.signals_match || [],
279
+ tags: tags,
143
280
  changelog: 'Iterative evolution update',
144
281
  };
145
282
 
@@ -151,8 +288,13 @@ function updateSkillOnHub(nodeId, skillId, content, opts, gene) {
151
288
  body: JSON.stringify(body),
152
289
  signal: AbortSignal.timeout(15000),
153
290
  })
154
- .then(function (res) { return res.json(); })
155
- .then(function (data) { return { ok: true, result: data }; })
291
+ .then(function (res) { return res.json().then(function (data) { return { status: res.status, data: data }; }); })
292
+ .then(function (result) {
293
+ if (result.status >= 200 && result.status < 300) {
294
+ return { ok: true, result: result.data };
295
+ }
296
+ return { ok: false, error: result.data?.error || 'update_failed', status: result.status };
297
+ })
156
298
  .catch(function (err) { return { ok: false, error: err.message }; });
157
299
  }
158
300
 
@@ -160,4 +302,6 @@ module.exports = {
160
302
  geneToSkillMd: geneToSkillMd,
161
303
  publishSkillToHub: publishSkillToHub,
162
304
  updateSkillOnHub: updateSkillOnHub,
305
+ sanitizeSkillName: sanitizeSkillName,
306
+ toTitleCase: toTitleCase,
163
307
  };
@@ -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}`);