@evomap/evolver 1.29.8 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.29.8",
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": {
@@ -223,32 +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)',
238
282
  '',
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',
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',
246
291
  '',
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"',
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
+ '---',
252
298
  '',
253
299
  'SUCCESSFUL CAPSULES (grouped by pattern):',
254
300
  JSON.stringify(samples, null, 2),
@@ -260,7 +306,7 @@ function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
260
306
  JSON.stringify(analysis, null, 2),
261
307
  '',
262
308
  'Output a single Gene JSON object with these fields:',
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": [...] }',
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" }',
264
310
  ].join('\n');
265
311
  }
266
312
 
@@ -298,6 +344,34 @@ function deriveDescriptiveId(gene) {
298
344
  return DISTILLED_ID_PREFIX + unique.slice(0, 5).join('-');
299
345
  }
300
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
+
301
375
  // ---------------------------------------------------------------------------
302
376
  // Step 4: validateSynthesizedGene
303
377
  // ---------------------------------------------------------------------------
@@ -311,16 +385,34 @@ function validateSynthesizedGene(gene, existingGenes) {
311
385
  if (!Array.isArray(gene.signals_match) || gene.signals_match.length === 0) errors.push('missing or empty signals_match');
312
386
  if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) errors.push('missing or empty strategy');
313
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 ---
314
402
  if (gene.id && !String(gene.id).startsWith(DISTILLED_ID_PREFIX)) {
315
403
  gene.id = DISTILLED_ID_PREFIX + String(gene.id).replace(/^gene_/, '');
316
404
  }
317
405
 
318
406
  if (gene.id) {
319
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, '');
320
410
  var needsRename = /^\d+$/.test(suffix) || /^\d{10,}/.test(suffix)
321
- || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-_]?\d*/i.test(suffix);
411
+ || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-_]?\d*$/i.test(suffix);
322
412
  if (needsRename) {
323
413
  gene.id = deriveDescriptiveId(gene);
414
+ } else {
415
+ gene.id = DISTILLED_ID_PREFIX + suffix;
324
416
  }
325
417
  var cleanSuffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
326
418
  if (cleanSuffix.replace(/[-_]/g, '').length < 6) {
@@ -328,6 +420,7 @@ function validateSynthesizedGene(gene, existingGenes) {
328
420
  }
329
421
  }
330
422
 
423
+ // --- Summary fallback (summary was already sanitized above, this handles missing/short) ---
331
424
  if (!gene.summary || typeof gene.summary !== 'string' || gene.summary.length < 10) {
332
425
  if (Array.isArray(gene.strategy) && gene.strategy.length > 0) {
333
426
  gene.summary = String(gene.strategy[0]).slice(0, 200);
@@ -336,6 +429,12 @@ function validateSynthesizedGene(gene, existingGenes) {
336
429
  }
337
430
  }
338
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 ---
339
438
  if (!gene.constraints || typeof gene.constraints !== 'object') gene.constraints = {};
340
439
  if (!Array.isArray(gene.constraints.forbidden_paths) || gene.constraints.forbidden_paths.length === 0) {
341
440
  gene.constraints.forbidden_paths = ['.git', 'node_modules'];
@@ -347,6 +446,7 @@ function validateSynthesizedGene(gene, existingGenes) {
347
446
  gene.constraints.max_files = DISTILLED_MAX_FILES;
348
447
  }
349
448
 
449
+ // --- Validation command sanitization ---
350
450
  var ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
351
451
  if (Array.isArray(gene.validation)) {
352
452
  gene.validation = gene.validation.filter(function (cmd) {
@@ -359,11 +459,16 @@ function validateSynthesizedGene(gene, existingGenes) {
359
459
  });
360
460
  }
361
461
 
462
+ // --- Schema version ---
463
+ if (!gene.schema_version) gene.schema_version = '1.6.0';
464
+
465
+ // --- Duplicate ID check ---
362
466
  var existingIds = new Set((existingGenes || []).map(function (g) { return g.id; }));
363
467
  if (gene.id && existingIds.has(gene.id)) {
364
468
  gene.id = gene.id + '_' + Date.now().toString(36);
365
469
  }
366
470
 
471
+ // --- Signal overlap check ---
367
472
  if (gene.signals_match && existingGenes && existingGenes.length > 0) {
368
473
  var newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); }));
369
474
  for (var i = 0; i < existingGenes.length; i++) {
@@ -566,6 +671,7 @@ module.exports = {
566
671
  prepareDistillation: prepareDistillation,
567
672
  completeDistillation: completeDistillation,
568
673
  validateSynthesizedGene: validateSynthesizedGene,
674
+ sanitizeSignalsMatch: sanitizeSignalsMatch,
569
675
  shouldDistill: shouldDistill,
570
676
  buildDistillationPrompt: buildDistillationPrompt,
571
677
  extractJsonFromLlmResponse: extractJsonFromLlmResponse,
@@ -3,54 +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).
7
- *
8
- * @param {object} gene - Gene asset
9
- * @returns {string} SKILL.md content
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.).
10
8
  */
11
9
  function sanitizeSkillName(rawName) {
12
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
13
  if (/^\d{8,}/.test(name) || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-]?\d*$/i.test(name)) {
14
14
  return null;
15
15
  }
16
- name = name.replace(/-?\d{10,}$/g, '').replace(/-+$/, '');
17
16
  if (name.replace(/[-]/g, '').length < 6) return null;
18
17
  return name;
19
18
  }
20
19
 
21
- function geneToSkillMd(gene) {
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);
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);
36
41
  });
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';
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
+ });
41
48
  }
42
- var desc = (gene.summary || 'AI agent skill distilled from evolution experience.').replace(/[\r\n]+/g, ' ').trim();
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.
56
+ *
57
+ * @param {object} gene - Gene asset
58
+ * @returns {string} SKILL.md content
59
+ */
60
+ function geneToSkillMd(gene) {
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.';
43
66
 
44
67
  var lines = [
45
68
  '---',
46
- 'name: ' + name,
69
+ 'name: ' + displayName,
47
70
  'description: ' + desc,
48
71
  '---',
49
72
  '',
50
- '# ' + name,
73
+ '# ' + displayName,
74
+ '',
75
+ desc,
51
76
  '',
52
77
  ];
53
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 --
54
90
  if (gene.signals_match && gene.signals_match.length > 0) {
55
91
  lines.push('## Trigger Signals');
56
92
  lines.push('');
@@ -60,6 +96,7 @@ function geneToSkillMd(gene) {
60
96
  lines.push('');
61
97
  }
62
98
 
99
+ // -- Preconditions --
63
100
  if (gene.preconditions && gene.preconditions.length > 0) {
64
101
  lines.push('## Preconditions');
65
102
  lines.push('');
@@ -69,27 +106,36 @@ function geneToSkillMd(gene) {
69
106
  lines.push('');
70
107
  }
71
108
 
109
+ // -- Strategy --
72
110
  if (gene.strategy && gene.strategy.length > 0) {
73
111
  lines.push('## Strategy');
74
112
  lines.push('');
75
113
  gene.strategy.forEach(function (step, i) {
76
- 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
+ }
77
121
  });
78
122
  lines.push('');
79
123
  }
80
124
 
125
+ // -- Constraints --
81
126
  if (gene.constraints) {
82
127
  lines.push('## Constraints');
83
128
  lines.push('');
84
129
  if (gene.constraints.max_files) {
85
- lines.push('- Max files: ' + gene.constraints.max_files);
130
+ lines.push('- Max files per invocation: ' + gene.constraints.max_files);
86
131
  }
87
132
  if (gene.constraints.forbidden_paths && gene.constraints.forbidden_paths.length > 0) {
88
- lines.push('- Forbidden paths: ' + gene.constraints.forbidden_paths.join(', '));
133
+ lines.push('- Forbidden paths: ' + gene.constraints.forbidden_paths.map(function (p) { return '`' + p + '`'; }).join(', '));
89
134
  }
90
135
  lines.push('');
91
136
  }
92
137
 
138
+ // -- Validation --
93
139
  if (gene.validation && gene.validation.length > 0) {
94
140
  lines.push('## Validation');
95
141
  lines.push('');
@@ -101,6 +147,16 @@ function geneToSkillMd(gene) {
101
147
  });
102
148
  }
103
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
+
104
160
  lines.push('---');
105
161
  lines.push('');
106
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.*');
@@ -109,6 +165,31 @@ function geneToSkillMd(gene) {
109
165
  return lines.join('\n');
110
166
  }
111
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
+
112
193
  /**
113
194
  * Publish a Gene as a Skill to the Hub skill store.
114
195
  *
@@ -121,18 +202,37 @@ function publishSkillToHub(gene, opts) {
121
202
  var hubUrl = getHubUrl();
122
203
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
123
204
 
124
- 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);
125
216
  var nodeId = getNodeId();
126
217
  var fmName = content.match(/^name:\s*(.+)$/m);
127
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, '');
128
221
  var skillId = 'skill_' + derivedName;
129
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
+ });
229
+
130
230
  var body = {
131
231
  sender_id: nodeId,
132
232
  skill_id: skillId,
133
233
  content: content,
134
- category: opts.category || gene.category || null,
135
- tags: opts.tags || gene.signals_match || [],
234
+ category: opts.category || geneCopy.category || null,
235
+ tags: tags,
136
236
  };
137
237
 
138
238
  var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/skill/store/publish';
@@ -165,12 +265,18 @@ function updateSkillOnHub(nodeId, skillId, content, opts, gene) {
165
265
  var hubUrl = getHubUrl();
166
266
  if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
167
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
+
168
274
  var body = {
169
275
  sender_id: nodeId,
170
276
  skill_id: skillId,
171
277
  content: content,
172
278
  category: opts.category || gene.category || null,
173
- tags: opts.tags || gene.signals_match || [],
279
+ tags: tags,
174
280
  changelog: 'Iterative evolution update',
175
281
  };
176
282
 
@@ -196,4 +302,6 @@ module.exports = {
196
302
  geneToSkillMd: geneToSkillMd,
197
303
  publishSkillToHub: publishSkillToHub,
198
304
  updateSkillOnHub: updateSkillOnHub,
305
+ sanitizeSkillName: sanitizeSkillName,
306
+ toTitleCase: toTitleCase,
199
307
  };