@evomap/evolver 1.31.0 → 1.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -282,7 +282,7 @@ async function main() {
282
282
 
283
283
  if (res && res.ok && !dryRun) {
284
284
  try {
285
- const { shouldDistill, prepareDistillation } = require('./src/gep/skillDistiller');
285
+ const { shouldDistill, prepareDistillation, autoDistill } = require('./src/gep/skillDistiller');
286
286
  const { readStateForSolidify } = require('./src/gep/solidify');
287
287
  const solidifyState = readStateForSolidify();
288
288
  const count = solidifyState.solidify_count || 0;
@@ -290,16 +290,21 @@ async function main() {
290
290
  const autoTrigger = count > 0 && count % autoDistillInterval === 0;
291
291
 
292
292
  if (autoTrigger || shouldDistill()) {
293
- const dr = prepareDistillation();
294
- if (dr && dr.ok && dr.promptPath) {
295
- const trigger = autoTrigger ? `auto (every ${autoDistillInterval} solidifies, count=${count})` : 'threshold';
296
- console.log('\n[DISTILL_REQUEST]');
297
- console.log(`Distillation triggered: ${trigger}`);
298
- console.log('Read the prompt file, process it with your LLM,');
299
- console.log('save the LLM response to a file, then run:');
300
- console.log(' node index.js distill --response-file=<path_to_llm_response>');
301
- console.log('Prompt file: ' + dr.promptPath);
302
- console.log('[/DISTILL_REQUEST]');
293
+ const auto = autoDistill();
294
+ if (auto && auto.ok && auto.gene) {
295
+ console.log('[Distiller] Auto-distilled gene: ' + auto.gene.id);
296
+ } else {
297
+ const dr = prepareDistillation();
298
+ if (dr && dr.ok && dr.promptPath) {
299
+ const trigger = autoTrigger ? `auto (every ${autoDistillInterval} solidifies, count=${count})` : 'threshold';
300
+ console.log('\n[DISTILL_REQUEST]');
301
+ console.log(`Distillation triggered: ${trigger}`);
302
+ console.log('Read the prompt file, process it with your LLM,');
303
+ console.log('save the LLM response to a file, then run:');
304
+ console.log(' node index.js distill --response-file=<path_to_llm_response>');
305
+ console.log('Prompt file: ' + dr.promptPath);
306
+ console.log('[/DISTILL_REQUEST]');
307
+ }
303
308
  }
304
309
  }
305
310
  } catch (e) {
@@ -454,7 +459,9 @@ async function main() {
454
459
  const s = readJsonSafe(sp);
455
460
  if (s && s.last_run) {
456
461
  s.last_solidify = { run_id: s.last_run.run_id, rejected: true, timestamp: new Date().toISOString() };
457
- fs.writeFileSync(sp, JSON.stringify(s, null, 2));
462
+ const tmpReject = `${sp}.tmp`;
463
+ fs.writeFileSync(tmpReject, JSON.stringify(s, null, 2) + '\n', 'utf8');
464
+ fs.renameSync(tmpReject, sp);
458
465
  }
459
466
  }
460
467
  console.log('[Review] Changes rolled back.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.31.0",
3
+ "version": "1.32.1",
4
4
  "description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/src/evolve.js CHANGED
@@ -39,6 +39,8 @@ const { getEvolutionDir } = require('./gep/paths');
39
39
  const { shouldReflect, buildReflectionContext, recordReflection } = require('./gep/reflection');
40
40
  const { loadNarrativeSummary } = require('./gep/narrativeMemory');
41
41
  const { maybeReportIssue } = require('./gep/issueReporter');
42
+ const { resolveStrategy } = require('./gep/strategy');
43
+ const { expandSignals } = require('./gep/learningSignals');
42
44
 
43
45
  const REPO_ROOT = getRepoRoot();
44
46
 
@@ -352,6 +354,73 @@ function readRecentLog(filePath, size = 10000) {
352
354
  }
353
355
  }
354
356
 
357
+ function computeAdaptiveStrategyPolicy(opts) {
358
+ const recentEvents = Array.isArray(opts && opts.recentEvents) ? opts.recentEvents : [];
359
+ const selectedGene = opts && opts.selectedGene ? opts.selectedGene : null;
360
+ const signals = Array.isArray(opts && opts.signals) ? opts.signals : [];
361
+ const baseStrategy = resolveStrategy({ signals: signals });
362
+
363
+ const tail = recentEvents.slice(-8);
364
+ let repairStreak = 0;
365
+ for (let i = tail.length - 1; i >= 0; i--) {
366
+ if (tail[i] && tail[i].intent === 'repair') repairStreak++;
367
+ else break;
368
+ }
369
+ let failureStreak = 0;
370
+ for (let i = tail.length - 1; i >= 0; i--) {
371
+ if (tail[i] && tail[i].outcome && tail[i].outcome.status === 'failed') failureStreak++;
372
+ else break;
373
+ }
374
+
375
+ const antiPatterns = selectedGene && Array.isArray(selectedGene.anti_patterns) ? selectedGene.anti_patterns.slice(-5) : [];
376
+ const learningHistory = selectedGene && Array.isArray(selectedGene.learning_history) ? selectedGene.learning_history.slice(-6) : [];
377
+ const signalTags = new Set(expandSignals(signals, ''));
378
+ const overlappingAntiPatterns = antiPatterns.filter(function (ap) {
379
+ return ap && Array.isArray(ap.learning_signals) && ap.learning_signals.some(function (tag) {
380
+ return signalTags.has(String(tag));
381
+ });
382
+ });
383
+ const hardFailures = overlappingAntiPatterns.filter(function (ap) { return ap && ap.mode === 'hard'; }).length;
384
+ const softFailures = overlappingAntiPatterns.filter(function (ap) { return ap && ap.mode !== 'hard'; }).length;
385
+ const recentSuccesses = learningHistory.filter(function (x) { return x && x.outcome === 'success'; }).length;
386
+
387
+ const stagnation = signals.includes('stable_success_plateau') ||
388
+ signals.includes('evolution_saturation') ||
389
+ signals.includes('empty_cycle_loop_detected') ||
390
+ failureStreak >= 3 ||
391
+ repairStreak >= 3;
392
+
393
+ const forceInnovate = stagnation && !signals.includes('log_error');
394
+ const highRiskGene = hardFailures >= 1 || (softFailures >= 2 && recentSuccesses === 0);
395
+ const cautiousExecution = highRiskGene || failureStreak >= 2;
396
+
397
+ let blastRadiusMaxFiles = selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
398
+ ? Number(selectedGene.constraints.max_files)
399
+ : 12;
400
+ if (cautiousExecution) blastRadiusMaxFiles = Math.max(2, Math.min(blastRadiusMaxFiles, 6));
401
+ else if (forceInnovate) blastRadiusMaxFiles = Math.max(3, Math.min(blastRadiusMaxFiles, 10));
402
+
403
+ const directives = [];
404
+ directives.push('Base strategy: ' + baseStrategy.label + ' (' + baseStrategy.description + ')');
405
+ if (forceInnovate) directives.push('Force strategy shift: prefer innovate over repeating repair/optimize.');
406
+ if (highRiskGene) directives.push('Selected gene is high risk for current signals; keep blast radius narrow and prefer smallest viable change.');
407
+ if (failureStreak >= 2) directives.push('Recent failure streak detected; avoid repeating recent failed approach.');
408
+ directives.push('Target max files for this cycle: ' + blastRadiusMaxFiles + '.');
409
+
410
+ return {
411
+ name: baseStrategy.name,
412
+ label: baseStrategy.label,
413
+ description: baseStrategy.description,
414
+ forceInnovate: forceInnovate,
415
+ cautiousExecution: cautiousExecution,
416
+ highRiskGene: highRiskGene,
417
+ repairStreak: repairStreak,
418
+ failureStreak: failureStreak,
419
+ blastRadiusMaxFiles: blastRadiusMaxFiles,
420
+ directives: directives,
421
+ };
422
+ }
423
+
355
424
  function checkSystemHealth() {
356
425
  const report = [];
357
426
  try {
@@ -1279,6 +1348,7 @@ async function run() {
1279
1348
  const newCandidates = extractCapabilityCandidates({
1280
1349
  recentSessionTranscript: recentMasterLog,
1281
1350
  signals,
1351
+ recentFailedCapsules: readRecentFailedCapsules(50),
1282
1352
  });
1283
1353
  for (const c of newCandidates) {
1284
1354
  try {
@@ -1433,6 +1503,11 @@ async function run() {
1433
1503
  ? capsuleCandidates.map(c => (c && c.id ? String(c.id) : null)).filter(Boolean)
1434
1504
  : [];
1435
1505
  const selectedCapsuleId = capsulesUsed.length ? capsulesUsed[0] : null;
1506
+ const strategyPolicy = computeAdaptiveStrategyPolicy({
1507
+ recentEvents,
1508
+ selectedGene,
1509
+ signals,
1510
+ });
1436
1511
 
1437
1512
  // Personality selection (natural selection + small mutation when triggered).
1438
1513
  // This state is persisted in MEMORY_DIR and is treated as an evolution control surface (not role-play).
@@ -1463,9 +1538,9 @@ async function run() {
1463
1538
  tailAvgScore >= 0.7;
1464
1539
  const forceInnovation =
1465
1540
  String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase() === 'true';
1466
- const mutationInnovateMode = !!IS_RANDOM_DRIFT || !!innovationPressure || !!forceInnovation;
1541
+ const mutationInnovateMode = !!IS_RANDOM_DRIFT || !!innovationPressure || !!forceInnovation || !!strategyPolicy.forceInnovate;
1467
1542
  const mutationSignals = innovationPressure ? [...(Array.isArray(signals) ? signals : []), 'stable_success_plateau'] : signals;
1468
- const mutationSignalsEffective = forceInnovation
1543
+ const mutationSignalsEffective = (forceInnovation || strategyPolicy.forceInnovate)
1469
1544
  ? [...(Array.isArray(mutationSignals) ? mutationSignals : []), 'force_innovation']
1470
1545
  : mutationSignals;
1471
1546
 
@@ -1565,10 +1640,13 @@ async function run() {
1565
1640
  console.warn('[SolidifyState] Failed to read git HEAD:', e && e.message || e);
1566
1641
  }
1567
1642
 
1568
- const maxFiles =
1569
- selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
1570
- ? Number(selectedGene.constraints.max_files)
1571
- : 12;
1643
+ const maxFiles = strategyPolicy && Number.isFinite(Number(strategyPolicy.blastRadiusMaxFiles))
1644
+ ? Number(strategyPolicy.blastRadiusMaxFiles)
1645
+ : (
1646
+ selectedGene && selectedGene.constraints && Number.isFinite(Number(selectedGene.constraints.max_files))
1647
+ ? Number(selectedGene.constraints.max_files)
1648
+ : 12
1649
+ );
1572
1650
  const blastRadiusEstimate = {
1573
1651
  files: Number.isFinite(maxFiles) && maxFiles > 0 ? maxFiles : 0,
1574
1652
  lines: Number.isFinite(maxFiles) && maxFiles > 0 ? Math.round(maxFiles * 80) : 0,
@@ -1602,6 +1680,7 @@ async function run() {
1602
1680
  baseline_untracked: baselineUntracked,
1603
1681
  baseline_git_head: baselineHead,
1604
1682
  blast_radius_estimate: blastRadiusEstimate,
1683
+ strategy_policy: strategyPolicy,
1605
1684
  active_task_id: activeTask ? (activeTask.id || activeTask.task_id || null) : null,
1606
1685
  active_task_title: activeTask ? (activeTask.title || null) : null,
1607
1686
  worker_assignment_id: activeTask ? (activeTask._worker_assignment_id || null) : null,
@@ -1739,6 +1818,7 @@ ${mutationDirective}
1739
1818
  capabilityCandidatesPreview,
1740
1819
  externalCandidatesPreview,
1741
1820
  hubMatchedBlock,
1821
+ strategyPolicy,
1742
1822
  failedCapsules: recentFailedCapsules,
1743
1823
  hubLessons,
1744
1824
  });
@@ -1827,5 +1907,5 @@ ${mutationDirective}
1827
1907
  }
1828
1908
  }
1829
1909
 
1830
- module.exports = { run };
1910
+ module.exports = { run, computeAdaptiveStrategyPolicy };
1831
1911
 
@@ -1,3 +1,5 @@
1
+ const { expandSignals } = require('./learningSignals');
2
+
1
3
  function stableHash(input) {
2
4
  // Deterministic lightweight hash (not cryptographic).
3
5
  const s = String(input || '');
@@ -60,13 +62,15 @@ function buildFiveQuestionsShape({ title, signals, evidence }) {
60
62
  };
61
63
  }
62
64
 
63
- function extractCapabilityCandidates({ recentSessionTranscript, signals }) {
65
+ function extractCapabilityCandidates({ recentSessionTranscript, signals, recentFailedCapsules }) {
64
66
  const candidates = [];
67
+ const signalList = Array.isArray(signals) ? signals : [];
68
+ const expandedTags = expandSignals(signalList, recentSessionTranscript);
65
69
  const toolCalls = extractToolCalls(recentSessionTranscript);
66
70
  const freq = countFreq(toolCalls);
67
71
 
68
72
  for (const [tool, count] of freq.entries()) {
69
- if (count < 2) continue;
73
+ if (count < 3) continue;
70
74
  const title = `Repeated tool usage: ${tool}`;
71
75
  const evidence = `Observed ${count} occurrences of tool call marker for ${tool}.`;
72
76
  const shape = buildFiveQuestionsShape({ title, signals, evidence });
@@ -76,13 +80,13 @@ function extractCapabilityCandidates({ recentSessionTranscript, signals }) {
76
80
  title,
77
81
  source: 'transcript',
78
82
  created_at: new Date().toISOString(),
79
- signals: Array.isArray(signals) ? signals : [],
83
+ signals: signalList,
84
+ tags: expandedTags,
80
85
  shape,
81
86
  });
82
87
  }
83
88
 
84
89
  // Signals-as-candidates: capture recurring pain points as reusable capability shapes.
85
- const signalList = Array.isArray(signals) ? signals : [];
86
90
  const signalCandidates = [
87
91
  // Defensive signals
88
92
  { signal: 'log_error', title: 'Repair recurring runtime errors' },
@@ -109,10 +113,67 @@ function extractCapabilityCandidates({ recentSessionTranscript, signals }) {
109
113
  source: 'signals',
110
114
  created_at: new Date().toISOString(),
111
115
  signals: signalList,
116
+ tags: expandedTags,
112
117
  shape,
113
118
  });
114
119
  }
115
120
 
121
+ var failedCapsules = Array.isArray(recentFailedCapsules) ? recentFailedCapsules : [];
122
+ var groups = {};
123
+ var problemPriority = [
124
+ 'problem:performance',
125
+ 'problem:protocol',
126
+ 'problem:reliability',
127
+ 'problem:stagnation',
128
+ 'problem:capability',
129
+ ];
130
+ for (var i = 0; i < failedCapsules.length; i++) {
131
+ var fc = failedCapsules[i];
132
+ if (!fc || fc.outcome && fc.outcome.status === 'success') continue;
133
+ var reason = String(fc.failure_reason || '').trim();
134
+ var failureTags = expandSignals((fc.trigger || []).concat(signalList), reason).filter(function (t) {
135
+ return t.indexOf('problem:') === 0 || t.indexOf('risk:') === 0 || t.indexOf('area:') === 0 || t.indexOf('action:') === 0;
136
+ });
137
+ if (failureTags.length === 0) continue;
138
+ var dominantProblem = null;
139
+ for (var p = 0; p < problemPriority.length; p++) {
140
+ if (failureTags.indexOf(problemPriority[p]) !== -1) {
141
+ dominantProblem = problemPriority[p];
142
+ break;
143
+ }
144
+ }
145
+ var groupingTags = dominantProblem
146
+ ? [dominantProblem]
147
+ : failureTags.filter(function (tag) { return tag.indexOf('area:') === 0 || tag.indexOf('risk:') === 0; }).slice(0, 1);
148
+ var key = groupingTags.join('|');
149
+ if (!groups[key]) groups[key] = { count: 0, tags: failureTags, reasons: [], gene: fc.gene || null };
150
+ groups[key].count += 1;
151
+ if (reason) groups[key].reasons.push(reason);
152
+ }
153
+
154
+ Object.keys(groups).forEach(function (key) {
155
+ var group = groups[key];
156
+ if (!group || group.count < 2) return;
157
+ var title = 'Learn from recurring failed evolution paths';
158
+ if (group.tags.indexOf('problem:performance') !== -1) title = 'Resolve recurring performance regressions';
159
+ else if (group.tags.indexOf('problem:protocol') !== -1) title = 'Prevent recurring protocol and validation regressions';
160
+ else if (group.tags.indexOf('problem:reliability') !== -1) title = 'Repair recurring reliability failures';
161
+ else if (group.tags.indexOf('problem:stagnation') !== -1) title = 'Break repeated stagnation loops with a new strategy';
162
+ else if (group.tags.indexOf('area:orchestration') !== -1) title = 'Stabilize task and orchestration behavior';
163
+ var evidence = 'Observed ' + group.count + ' recent failed evolutions with similar learning tags. ' +
164
+ (group.reasons[0] ? 'Latest reason: ' + clip(group.reasons[0], 180) : '');
165
+ candidates.push({
166
+ type: 'CapabilityCandidate',
167
+ id: 'cand_' + stableHash('failed:' + key),
168
+ title: title,
169
+ source: 'failed_capsules',
170
+ created_at: new Date().toISOString(),
171
+ signals: signalList,
172
+ tags: group.tags,
173
+ shape: buildFiveQuestionsShape({ title: title, signals: signalList, evidence: evidence }),
174
+ });
175
+ });
176
+
116
177
  // Dedup by id
117
178
  const seen = new Set();
118
179
  return candidates.filter(c => {
@@ -142,5 +203,6 @@ function renderCandidatesPreview(candidates, maxChars = 1400) {
142
203
  module.exports = {
143
204
  extractCapabilityCandidates,
144
205
  renderCandidatesPreview,
206
+ expandSignals,
145
207
  };
146
208
 
@@ -0,0 +1,88 @@
1
+ function unique(items) {
2
+ return Array.from(new Set((Array.isArray(items) ? items : []).filter(Boolean).map(function (x) {
3
+ return String(x).trim();
4
+ }).filter(Boolean)));
5
+ }
6
+
7
+ function add(tags, value) {
8
+ if (!value) return;
9
+ tags.push(String(value).trim());
10
+ }
11
+
12
+ function expandSignals(signals, extraText) {
13
+ var raw = Array.isArray(signals) ? signals.map(function (s) { return String(s); }) : [];
14
+ var tags = [];
15
+
16
+ for (var i = 0; i < raw.length; i++) {
17
+ var signal = raw[i];
18
+ add(tags, signal);
19
+ var base = signal.split(':')[0];
20
+ if (base && base !== signal) add(tags, base);
21
+ }
22
+
23
+ var text = (raw.join(' ') + ' ' + String(extraText || '')).toLowerCase();
24
+
25
+ if (/(error|exception|failed|unstable|log_error|runtime|429)/.test(text)) {
26
+ add(tags, 'problem:reliability');
27
+ add(tags, 'action:repair');
28
+ }
29
+ if (/(protocol|prompt|audit|gep|schema|drift)/.test(text)) {
30
+ add(tags, 'problem:protocol');
31
+ add(tags, 'action:optimize');
32
+ add(tags, 'area:prompt');
33
+ }
34
+ if (/(perf|performance|bottleneck|latency|slow|throughput)/.test(text)) {
35
+ add(tags, 'problem:performance');
36
+ add(tags, 'action:optimize');
37
+ }
38
+ if (/(feature|capability_gap|user_feature_request|external_opportunity|stagnation recommendation)/.test(text)) {
39
+ add(tags, 'problem:capability');
40
+ add(tags, 'action:innovate');
41
+ }
42
+ if (/(stagnation|plateau|steady_state|saturation|empty_cycle_loop|loop_detected|recurring)/.test(text)) {
43
+ add(tags, 'problem:stagnation');
44
+ add(tags, 'action:innovate');
45
+ }
46
+ if (/(task|worker|heartbeat|hub|commitment|assignment|orchestration)/.test(text)) {
47
+ add(tags, 'area:orchestration');
48
+ }
49
+ if (/(memory|narrative|reflection)/.test(text)) {
50
+ add(tags, 'area:memory');
51
+ }
52
+ if (/(skill|dashboard)/.test(text)) {
53
+ add(tags, 'area:skills');
54
+ }
55
+ if (/(validation|canary|rollback|constraint|blast radius|destructive)/.test(text)) {
56
+ add(tags, 'risk:validation');
57
+ }
58
+
59
+ return unique(tags);
60
+ }
61
+
62
+ function geneTags(gene) {
63
+ if (!gene || typeof gene !== 'object') return [];
64
+ var inputs = [];
65
+ if (gene.category) inputs.push('action:' + String(gene.category).toLowerCase());
66
+ if (Array.isArray(gene.signals_match)) inputs = inputs.concat(gene.signals_match);
67
+ if (typeof gene.id === 'string') inputs.push(gene.id);
68
+ if (typeof gene.summary === 'string') inputs.push(gene.summary);
69
+ return expandSignals(inputs, '');
70
+ }
71
+
72
+ function scoreTagOverlap(gene, signals) {
73
+ var signalTags = expandSignals(signals, '');
74
+ var geneTagList = geneTags(gene);
75
+ if (signalTags.length === 0 || geneTagList.length === 0) return 0;
76
+ var signalSet = new Set(signalTags);
77
+ var hits = 0;
78
+ for (var i = 0; i < geneTagList.length; i++) {
79
+ if (signalSet.has(geneTagList[i])) hits++;
80
+ }
81
+ return hits;
82
+ }
83
+
84
+ module.exports = {
85
+ expandSignals: expandSignals,
86
+ geneTags: geneTags,
87
+ scoreTagOverlap: scoreTagOverlap,
88
+ };
package/src/gep/prompt.js CHANGED
@@ -266,6 +266,7 @@ function buildGepPrompt({
266
266
  recentHistory,
267
267
  failedCapsules,
268
268
  hubLessons,
269
+ strategyPolicy,
269
270
  }) {
270
271
  const parentValue = parentEventId ? `"${parentEventId}"` : 'null';
271
272
  const selectedGeneId = selectedGene && selectedGene.id ? selectedGene.id : 'gene_<name>';
@@ -289,6 +290,15 @@ ACTIVE STRATEGY (Generic):
289
290
  3. Apply minimal, safe changes.
290
291
  4. Validate changes strictly.
291
292
  5. Solidify knowledge.
293
+ `.trim();
294
+ }
295
+ let strategyPolicyBlock = '';
296
+ if (strategyPolicy && Array.isArray(strategyPolicy.directives) && strategyPolicy.directives.length > 0) {
297
+ strategyPolicyBlock = `
298
+ ADAPTIVE STRATEGY POLICY:
299
+ ${strategyPolicy.directives.map((s, i) => `${i + 1}. ${s}`).join('\n')}
300
+ ${strategyPolicy.forceInnovate ? 'You MUST prefer INNOVATE unless a critical blocking error is present.' : ''}
301
+ ${strategyPolicy.cautiousExecution ? 'You MUST reduce blast radius and avoid broad refactors in this cycle.' : ''}
292
302
  `.trim();
293
303
  }
294
304
 
@@ -384,6 +394,7 @@ II. Directives & Logic
384
394
 
385
395
  2. Selection: Selected Gene "${selectedGeneId}".
386
396
  ${strategyBlock}
397
+ ${strategyPolicyBlock ? '\n' + strategyPolicyBlock : ''}
387
398
 
388
399
  3. Execution: Apply changes (tool calls). Repair/Optimize: small/reversible. Innovate: new skills in \`skills/<name>/\`.
389
400
  4. Validation: Run gene's validation steps. Fail = ROLLBACK.
@@ -1,3 +1,6 @@
1
+ const { scoreTagOverlap } = require('./learningSignals');
2
+ const { captureEnvFingerprint } = require('./envFingerprint');
3
+
1
4
  function matchPatternToSignals(pattern, signals) {
2
5
  if (!pattern || !signals || signals.length === 0) return false;
3
6
  const p = String(pattern);
@@ -30,12 +33,54 @@ function matchPatternToSignals(pattern, signals) {
30
33
  function scoreGene(gene, signals) {
31
34
  if (!gene || gene.type !== 'Gene') return 0;
32
35
  const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : [];
33
- if (patterns.length === 0) return 0;
36
+ var tagScore = scoreTagOverlap(gene, signals);
37
+ if (patterns.length === 0) return tagScore > 0 ? tagScore * 0.6 : 0;
34
38
  let score = 0;
35
39
  for (const pat of patterns) {
36
40
  if (matchPatternToSignals(pat, signals)) score += 1;
37
41
  }
38
- return score;
42
+ return score + (tagScore * 0.6);
43
+ }
44
+
45
+ function getEpigeneticBoostLocal(gene, envFingerprint) {
46
+ if (!gene || !Array.isArray(gene.epigenetic_marks)) return 0;
47
+ const platform = envFingerprint && envFingerprint.platform ? String(envFingerprint.platform) : '';
48
+ const arch = envFingerprint && envFingerprint.arch ? String(envFingerprint.arch) : '';
49
+ const nodeVersion = envFingerprint && envFingerprint.node_version ? String(envFingerprint.node_version) : '';
50
+ const envContext = [platform, arch, nodeVersion].filter(Boolean).join('/') || 'unknown';
51
+ const mark = gene.epigenetic_marks.find(function (m) { return m && m.context === envContext; });
52
+ return mark ? Number(mark.boost) || 0 : 0;
53
+ }
54
+
55
+ function scoreGeneLearning(gene, signals, envFingerprint) {
56
+ if (!gene || gene.type !== 'Gene') return 0;
57
+ var boost = 0;
58
+
59
+ var history = Array.isArray(gene.learning_history) ? gene.learning_history.slice(-8) : [];
60
+ for (var i = 0; i < history.length; i++) {
61
+ var entry = history[i];
62
+ if (!entry) continue;
63
+ if (entry.outcome === 'success') boost += 0.12;
64
+ else if (entry.mode === 'hard') boost -= 0.22;
65
+ else if (entry.mode === 'soft') boost -= 0.08;
66
+ }
67
+
68
+ boost += getEpigeneticBoostLocal(gene, envFingerprint);
69
+
70
+ if (Array.isArray(gene.anti_patterns) && gene.anti_patterns.length > 0) {
71
+ var overlapPenalty = 0;
72
+ var signalTags = new Set(require('./learningSignals').expandSignals(signals, ''));
73
+ var recentAntiPatterns = gene.anti_patterns.slice(-6);
74
+ for (var j = 0; j < recentAntiPatterns.length; j++) {
75
+ var anti = recentAntiPatterns[j];
76
+ if (!anti || !Array.isArray(anti.learning_signals)) continue;
77
+ var overlap = anti.learning_signals.some(function (tag) { return signalTags.has(String(tag)); });
78
+ if (overlap) overlapPenalty += anti.mode === 'hard' ? 0.4 : 0.18;
79
+ }
80
+ boost -= overlapPenalty;
81
+ }
82
+
83
+ return Math.max(-1.5, Math.min(1.5, boost));
39
84
  }
40
85
 
41
86
  // Population-size-dependent drift intensity.
@@ -94,9 +139,11 @@ function selectGene(genes, signals, opts) {
94
139
  var DISTILLED_PREFIX = 'gene_distilled_';
95
140
  var DISTILLED_SCORE_FACTOR = 0.8;
96
141
 
142
+ const envFingerprint = captureEnvFingerprint();
97
143
  const scored = genesList
98
144
  .map(g => {
99
145
  var s = scoreGene(g, signals);
146
+ s += scoreGeneLearning(g, signals, envFingerprint);
100
147
  if (s > 0 && g.id && String(g.id).startsWith(DISTILLED_PREFIX)) s *= DISTILLED_SCORE_FACTOR;
101
148
  return { gene: g, score: s };
102
149
  })
@@ -4,6 +4,7 @@ var fs = require('fs');
4
4
  var path = require('path');
5
5
  var crypto = require('crypto');
6
6
  var paths = require('./paths');
7
+ var learningSignals = require('./learningSignals');
7
8
 
8
9
  var DISTILLER_MIN_CAPSULES = parseInt(process.env.DISTILLER_MIN_CAPSULES || '10', 10) || 10;
9
10
  var DISTILLER_INTERVAL_HOURS = parseInt(process.env.DISTILLER_INTERVAL_HOURS || '24', 10) || 24;
@@ -573,6 +574,126 @@ function prepareDistillation() {
573
574
  return { ok: true, promptPath: promptPath, requestPath: reqPath, dataHash: data.dataHash };
574
575
  }
575
576
 
577
+ function inferCategoryFromSignals(signals) {
578
+ var list = Array.isArray(signals) ? signals.map(function (s) { return String(s).toLowerCase(); }) : [];
579
+ if (list.some(function (s) { return s.indexOf('error') !== -1 || s.indexOf('fail') !== -1 || s.indexOf('reliability') !== -1; })) {
580
+ return 'repair';
581
+ }
582
+ if (list.some(function (s) { return s.indexOf('feature') !== -1 || s.indexOf('capability') !== -1 || s.indexOf('stagnation') !== -1; })) {
583
+ return 'innovate';
584
+ }
585
+ return 'optimize';
586
+ }
587
+
588
+ function chooseDistillationSource(data, analysis) {
589
+ var grouped = data && data.grouped ? data.grouped : {};
590
+ var best = null;
591
+ Object.keys(grouped).forEach(function (geneId) {
592
+ var g = grouped[geneId];
593
+ if (!g || g.total_count <= 0) return;
594
+ var score = (g.total_count * 2) + (g.avg_score || 0);
595
+ if (!best || score > best.score) {
596
+ best = { gene_id: geneId, group: g, score: score };
597
+ }
598
+ });
599
+ return best;
600
+ }
601
+
602
+ function synthesizeGeneFromPatterns(data, analysis, existingGenes) {
603
+ var source = chooseDistillationSource(data, analysis);
604
+ if (!source || !source.group) return null;
605
+
606
+ var group = source.group;
607
+ var existing = Array.isArray(existingGenes) ? existingGenes : [];
608
+ var sourceGene = existing.find(function (g) { return g && g.id === source.gene_id; }) || null;
609
+
610
+ var triggerFreq = {};
611
+ (group.triggers || []).forEach(function (arr) {
612
+ (Array.isArray(arr) ? arr : []).forEach(function (s) {
613
+ var k = String(s).toLowerCase();
614
+ triggerFreq[k] = (triggerFreq[k] || 0) + 1;
615
+ });
616
+ });
617
+ var signalsMatch = Object.keys(triggerFreq)
618
+ .sort(function (a, b) { return triggerFreq[b] - triggerFreq[a]; })
619
+ .slice(0, 6);
620
+ var summaryText = (group.summaries || []).slice(0, 5).join(' ');
621
+ var derivedTags = learningSignals.expandSignals(signalsMatch, summaryText)
622
+ .filter(function (tag) { return tag.indexOf('problem:') === 0 || tag.indexOf('area:') === 0; })
623
+ .slice(0, 4);
624
+ signalsMatch = Array.from(new Set(signalsMatch.concat(derivedTags)));
625
+ if (signalsMatch.length === 0 && sourceGene && Array.isArray(sourceGene.signals_match)) {
626
+ signalsMatch = sourceGene.signals_match.slice(0, 6);
627
+ }
628
+
629
+ var category = sourceGene && sourceGene.category ? sourceGene.category : inferCategoryFromSignals(signalsMatch);
630
+ var idSeed = {
631
+ type: 'Gene',
632
+ id: DISTILLED_ID_PREFIX + source.gene_id.replace(/^gene_/, '').replace(/^gene_distilled_/, ''),
633
+ category: category,
634
+ signals_match: signalsMatch,
635
+ strategy: sourceGene && Array.isArray(sourceGene.strategy) && sourceGene.strategy.length > 0
636
+ ? sourceGene.strategy.slice(0, 4)
637
+ : [
638
+ 'Identify the dominant repeated trigger pattern.',
639
+ 'Apply the smallest targeted change for that pattern.',
640
+ 'Run the narrowest validation that proves the regression is gone.',
641
+ 'Rollback immediately if validation fails.',
642
+ ],
643
+ };
644
+
645
+ var summaryBase = (group.summaries && group.summaries[0]) ? String(group.summaries[0]) : '';
646
+ if (!summaryBase) {
647
+ summaryBase = 'Reusable strategy for repeated successful pattern: ' + signalsMatch.slice(0, 3).join(', ');
648
+ }
649
+
650
+ var gene = {
651
+ type: 'Gene',
652
+ id: deriveDescriptiveId(idSeed),
653
+ summary: summaryBase.slice(0, 200),
654
+ category: category,
655
+ signals_match: signalsMatch,
656
+ preconditions: sourceGene && Array.isArray(sourceGene.preconditions) && sourceGene.preconditions.length > 0
657
+ ? sourceGene.preconditions.slice(0, 4)
658
+ : ['repeated success pattern observed in recent capsules'],
659
+ strategy: idSeed.strategy,
660
+ constraints: {
661
+ max_files: sourceGene && sourceGene.constraints && Number(sourceGene.constraints.max_files) > 0
662
+ ? Math.min(DISTILLED_MAX_FILES, Number(sourceGene.constraints.max_files))
663
+ : DISTILLED_MAX_FILES,
664
+ forbidden_paths: sourceGene && sourceGene.constraints && Array.isArray(sourceGene.constraints.forbidden_paths)
665
+ ? sourceGene.constraints.forbidden_paths.slice(0, 6)
666
+ : ['.git', 'node_modules'],
667
+ },
668
+ validation: sourceGene && Array.isArray(sourceGene.validation) && sourceGene.validation.length > 0
669
+ ? sourceGene.validation.slice(0, 4)
670
+ : ['node --test'],
671
+ };
672
+
673
+ return gene;
674
+ }
675
+
676
+ function finalizeDistilledGene(gene, requestLike, status) {
677
+ var state = readDistillerState();
678
+ state.last_distillation_at = new Date().toISOString();
679
+ state.last_data_hash = requestLike.data_hash;
680
+ state.last_gene_id = gene.id;
681
+ state.distillation_count = (state.distillation_count || 0) + 1;
682
+ writeDistillerState(state);
683
+
684
+ appendJsonl(distillerLogPath(), {
685
+ timestamp: new Date().toISOString(),
686
+ data_hash: requestLike.data_hash,
687
+ input_capsule_count: requestLike.input_capsule_count,
688
+ analysis_summary: requestLike.analysis_summary,
689
+ synthesized_gene_id: gene.id,
690
+ validation_passed: true,
691
+ validation_errors: [],
692
+ status: status || 'success',
693
+ gene: gene,
694
+ });
695
+ }
696
+
576
697
  // ---------------------------------------------------------------------------
577
698
  // Step 5b: completeDistillation -- validate LLM response and save gene
578
699
  // ---------------------------------------------------------------------------
@@ -665,11 +786,67 @@ function completeDistillation(responseText) {
665
786
  return { ok: true, gene: gene };
666
787
  }
667
788
 
789
+ function autoDistill() {
790
+ var data = collectDistillationData();
791
+ if (data.successCapsules.length < DISTILLER_MIN_CAPSULES) {
792
+ return { ok: false, reason: 'insufficient_data' };
793
+ }
794
+
795
+ var state = readDistillerState();
796
+ if (state.last_data_hash === data.dataHash) {
797
+ return { ok: false, reason: 'idempotent_skip' };
798
+ }
799
+
800
+ var analysis = analyzePatterns(data);
801
+ var assetsDir = paths.getGepAssetsDir();
802
+ var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
803
+ var existingGenes = existingGenesJson.genes || [];
804
+ var rawGene = synthesizeGeneFromPatterns(data, analysis, existingGenes);
805
+ if (!rawGene) return { ok: false, reason: 'no_candidate_gene' };
806
+
807
+ var validation = validateSynthesizedGene(rawGene, existingGenes);
808
+ if (!validation.valid) {
809
+ appendJsonl(distillerLogPath(), {
810
+ timestamp: new Date().toISOString(),
811
+ data_hash: data.dataHash,
812
+ status: 'auto_validation_failed',
813
+ synthesized_gene_id: validation.gene ? validation.gene.id : null,
814
+ validation_errors: validation.errors,
815
+ });
816
+ return { ok: false, reason: 'validation_failed', errors: validation.errors };
817
+ }
818
+
819
+ var gene = validation.gene;
820
+ gene._distilled_meta = {
821
+ distilled_at: new Date().toISOString(),
822
+ source_capsule_count: data.successCapsules.length,
823
+ data_hash: data.dataHash,
824
+ auto_distilled: true,
825
+ };
826
+
827
+ var assetStore = require('./assetStore');
828
+ assetStore.upsertGene(gene);
829
+ finalizeDistilledGene(gene, {
830
+ data_hash: data.dataHash,
831
+ input_capsule_count: data.successCapsules.length,
832
+ analysis_summary: {
833
+ high_frequency_count: analysis.high_frequency.length,
834
+ drift_count: analysis.strategy_drift.length,
835
+ gap_count: analysis.coverage_gaps.length,
836
+ success_rate: Math.round(analysis.success_rate * 100) / 100,
837
+ },
838
+ }, 'auto_success');
839
+
840
+ return { ok: true, gene: gene, auto: true };
841
+ }
842
+
668
843
  module.exports = {
669
844
  collectDistillationData: collectDistillationData,
670
845
  analyzePatterns: analyzePatterns,
846
+ synthesizeGeneFromPatterns: synthesizeGeneFromPatterns,
671
847
  prepareDistillation: prepareDistillation,
672
848
  completeDistillation: completeDistillation,
849
+ autoDistill: autoDistill,
673
850
  validateSynthesizedGene: validateSynthesizedGene,
674
851
  sanitizeSignalsMatch: sanitizeSignalsMatch,
675
852
  shouldDistill: shouldDistill,
@@ -671,6 +671,108 @@ function buildFailureReason(constraintCheck, validation, protocolViolations, can
671
671
  return reasons.join('; ').slice(0, 2000) || 'unknown';
672
672
  }
673
673
 
674
+ function buildSoftFailureLearningSignals(opts) {
675
+ const { expandSignals } = require('./learningSignals');
676
+ var signals = opts && Array.isArray(opts.signals) ? opts.signals : [];
677
+ var failureReason = opts && opts.failureReason ? String(opts.failureReason) : '';
678
+ var violations = opts && Array.isArray(opts.violations) ? opts.violations : [];
679
+ var validationResults = opts && Array.isArray(opts.validationResults) ? opts.validationResults : [];
680
+ var validationText = validationResults
681
+ .filter(function (r) { return r && r.ok === false; })
682
+ .map(function (r) { return [r.cmd, r.stderr, r.stdout].filter(Boolean).join(' '); })
683
+ .join(' ');
684
+ return expandSignals(signals.concat(violations), failureReason + ' ' + validationText)
685
+ .filter(function (tag) {
686
+ return tag.indexOf('problem:') === 0 || tag.indexOf('risk:') === 0 || tag.indexOf('area:') === 0 || tag.indexOf('action:') === 0;
687
+ });
688
+ }
689
+
690
+ function classifyFailureMode(opts) {
691
+ var constraintViolations = opts && Array.isArray(opts.constraintViolations) ? opts.constraintViolations : [];
692
+ var protocolViolations = opts && Array.isArray(opts.protocolViolations) ? opts.protocolViolations : [];
693
+ var validation = opts && opts.validation ? opts.validation : null;
694
+ var canary = opts && opts.canary ? opts.canary : null;
695
+
696
+ if (constraintViolations.some(function (v) {
697
+ var s = String(v || '');
698
+ return /HARD CAP BREACH|CRITICAL_FILE_|critical_path_modified|forbidden_path touched|ethics:/i.test(s);
699
+ })) {
700
+ return { mode: 'hard', reasonClass: 'constraint_destructive', retryable: false };
701
+ }
702
+
703
+ if (protocolViolations.length > 0) {
704
+ return { mode: 'hard', reasonClass: 'protocol', retryable: false };
705
+ }
706
+
707
+ if (canary && !canary.ok && !canary.skipped) {
708
+ return { mode: 'hard', reasonClass: 'canary', retryable: false };
709
+ }
710
+
711
+ if (constraintViolations.length > 0) {
712
+ return { mode: 'hard', reasonClass: 'constraint', retryable: false };
713
+ }
714
+
715
+ if (validation && validation.ok === false) {
716
+ return { mode: 'soft', reasonClass: 'validation', retryable: true };
717
+ }
718
+
719
+ return { mode: 'soft', reasonClass: 'unknown', retryable: true };
720
+ }
721
+
722
+ function adaptGeneFromLearning(opts) {
723
+ var gene = opts && opts.gene && opts.gene.type === 'Gene' ? opts.gene : null;
724
+ if (!gene) return gene;
725
+
726
+ var outcomeStatus = String(opts && opts.outcomeStatus || '').toLowerCase();
727
+ var learningSignals = Array.isArray(opts && opts.learningSignals) ? opts.learningSignals : [];
728
+ var failureMode = opts && opts.failureMode && typeof opts.failureMode === 'object'
729
+ ? opts.failureMode
730
+ : { mode: 'soft', reasonClass: 'unknown', retryable: true };
731
+
732
+ if (!Array.isArray(gene.learning_history)) gene.learning_history = [];
733
+ if (!Array.isArray(gene.signals_match)) gene.signals_match = [];
734
+
735
+ var seenSignal = new Set(gene.signals_match.map(function (s) { return String(s); }));
736
+ if (outcomeStatus === 'success') {
737
+ for (var i = 0; i < learningSignals.length; i++) {
738
+ var sig = String(learningSignals[i] || '');
739
+ if (!sig || seenSignal.has(sig)) continue;
740
+ if (sig.indexOf('problem:') === 0 || sig.indexOf('area:') === 0) {
741
+ gene.signals_match.push(sig);
742
+ seenSignal.add(sig);
743
+ }
744
+ }
745
+ }
746
+
747
+ gene.learning_history.push({
748
+ at: nowIso(),
749
+ outcome: outcomeStatus || 'unknown',
750
+ mode: failureMode.mode || 'soft',
751
+ reason_class: failureMode.reasonClass || 'unknown',
752
+ retryable: !!failureMode.retryable,
753
+ learning_signals: learningSignals.slice(0, 12),
754
+ });
755
+ if (gene.learning_history.length > 20) {
756
+ gene.learning_history = gene.learning_history.slice(gene.learning_history.length - 20);
757
+ }
758
+
759
+ if (outcomeStatus === 'failed') {
760
+ if (!Array.isArray(gene.anti_patterns)) gene.anti_patterns = [];
761
+ var anti = {
762
+ at: nowIso(),
763
+ mode: failureMode.mode || 'soft',
764
+ reason_class: failureMode.reasonClass || 'unknown',
765
+ learning_signals: learningSignals.slice(0, 8),
766
+ };
767
+ gene.anti_patterns.push(anti);
768
+ if (gene.anti_patterns.length > 12) {
769
+ gene.anti_patterns = gene.anti_patterns.slice(gene.anti_patterns.length - 12);
770
+ }
771
+ }
772
+
773
+ return gene;
774
+ }
775
+
674
776
  function rollbackTracked(repoRoot) {
675
777
  const mode = String(process.env.EVOLVER_ROLLBACK_MODE || 'hard').toLowerCase();
676
778
 
@@ -1157,6 +1259,23 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1157
1259
  const ts = nowIso();
1158
1260
  const outcomeStatus = success ? 'success' : 'failed';
1159
1261
  const score = clamp01(success ? 0.85 : 0.2);
1262
+ const failureReason = !success ? buildFailureReason(constraintCheck, validation, protocolViolations, canary) : '';
1263
+ const failureMode = !success
1264
+ ? classifyFailureMode({
1265
+ constraintViolations: constraintCheck.violations,
1266
+ protocolViolations: protocolViolations,
1267
+ validation: validation,
1268
+ canary: canary,
1269
+ })
1270
+ : { mode: 'none', reasonClass: null, retryable: false };
1271
+ const softFailureLearningSignals = !success
1272
+ ? buildSoftFailureLearningSignals({
1273
+ signals,
1274
+ failureReason,
1275
+ violations: constraintCheck.violations,
1276
+ validationResults: validation.results,
1277
+ })
1278
+ : [];
1160
1279
 
1161
1280
  const selectedCapsuleId =
1162
1281
  lastRun && typeof lastRun.selected_capsule_id === 'string' && lastRun.selected_capsule_id.trim()
@@ -1224,6 +1343,12 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1224
1343
  protocol_ok: protocolViolations.length === 0,
1225
1344
  protocol_violations: protocolViolations,
1226
1345
  memory_graph: memoryGraphPath(),
1346
+ soft_failure: success ? null : {
1347
+ learning_signals: softFailureLearningSignals,
1348
+ retryable: !!failureMode.retryable,
1349
+ class: failureMode.reasonClass,
1350
+ mode: failureMode.mode,
1351
+ },
1227
1352
  },
1228
1353
  };
1229
1354
  // Build desensitized execution trace for cross-agent experience sharing
@@ -1303,7 +1428,8 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1303
1428
  ? 'Failed: ' + geneUsed.id + ' on signals [' + (signals.slice(0, 3).join(', ') || 'none') + ']'
1304
1429
  : 'Failed evolution on signals [' + (signals.slice(0, 3).join(', ') || 'none') + ']',
1305
1430
  diff_snapshot: diffSnapshot,
1306
- failure_reason: buildFailureReason(constraintCheck, validation, protocolViolations, canary),
1431
+ failure_reason: failureReason,
1432
+ learning_signals: softFailureLearningSignals,
1307
1433
  constraint_violations: constraintCheck.violations || [],
1308
1434
  env_fingerprint: envFp,
1309
1435
  blast_radius: { files: blast.files, lines: blast.lines },
@@ -1331,6 +1457,12 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1331
1457
  // Apply epigenetic marks to the gene based on outcome and environment
1332
1458
  if (!dryRun && geneUsed && geneUsed.type === 'Gene') {
1333
1459
  try {
1460
+ adaptGeneFromLearning({
1461
+ gene: geneUsed,
1462
+ outcomeStatus: outcomeStatus,
1463
+ learningSignals: success ? signals : softFailureLearningSignals,
1464
+ failureMode: failureMode,
1465
+ });
1334
1466
  applyEpigeneticMarks(geneUsed, envFp, outcomeStatus);
1335
1467
  upsertGene(geneUsed);
1336
1468
  } catch (e) {
@@ -1562,7 +1694,7 @@ function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true }
1562
1694
  // which we already do above. The Hub-side solicitLesson() handles the rest.
1563
1695
  // For failures without a published event (no auto-publish), we still log locally.
1564
1696
  if (!dryRun && !success && event && event.outcome) {
1565
- var failureContent = buildFailureReason(constraintCheck, validation, protocolViolations, canary);
1697
+ var failureContent = failureReason;
1566
1698
  event.failure_reason = failureContent;
1567
1699
  event.summary = geneUsed
1568
1700
  ? 'Failed: ' + geneUsed.id + ' on signals [' + (signals.slice(0, 3).join(', ') || 'none') + '] - ' + failureContent.slice(0, 200)
@@ -1708,6 +1840,9 @@ module.exports = {
1708
1840
  classifyBlastSeverity,
1709
1841
  analyzeBlastRadiusBreakdown,
1710
1842
  compareBlastEstimate,
1843
+ classifyFailureMode,
1844
+ adaptGeneFromLearning,
1845
+ buildSoftFailureLearningSignals,
1711
1846
  runCanaryCheck,
1712
1847
  applyEpigeneticMarks,
1713
1848
  getEpigeneticBoost,