@evomap/evolver 1.28.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.
Files changed (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +290 -0
  3. package/README.zh-CN.md +236 -0
  4. package/SKILL.md +132 -0
  5. package/assets/gep/capsules.json +79 -0
  6. package/assets/gep/events.jsonl +7 -0
  7. package/assets/gep/genes.json +108 -0
  8. package/index.js +530 -0
  9. package/package.json +38 -0
  10. package/src/canary.js +13 -0
  11. package/src/evolve.js +1704 -0
  12. package/src/gep/a2a.js +173 -0
  13. package/src/gep/a2aProtocol.js +736 -0
  14. package/src/gep/analyzer.js +35 -0
  15. package/src/gep/assetCallLog.js +130 -0
  16. package/src/gep/assetStore.js +297 -0
  17. package/src/gep/assets.js +36 -0
  18. package/src/gep/bridge.js +71 -0
  19. package/src/gep/candidates.js +142 -0
  20. package/src/gep/contentHash.js +65 -0
  21. package/src/gep/deviceId.js +209 -0
  22. package/src/gep/envFingerprint.js +83 -0
  23. package/src/gep/hubReview.js +206 -0
  24. package/src/gep/hubSearch.js +237 -0
  25. package/src/gep/issueReporter.js +262 -0
  26. package/src/gep/llmReview.js +92 -0
  27. package/src/gep/memoryGraph.js +771 -0
  28. package/src/gep/memoryGraphAdapter.js +203 -0
  29. package/src/gep/mutation.js +186 -0
  30. package/src/gep/narrativeMemory.js +108 -0
  31. package/src/gep/paths.js +113 -0
  32. package/src/gep/personality.js +355 -0
  33. package/src/gep/prompt.js +566 -0
  34. package/src/gep/questionGenerator.js +212 -0
  35. package/src/gep/reflection.js +127 -0
  36. package/src/gep/sanitize.js +67 -0
  37. package/src/gep/selector.js +250 -0
  38. package/src/gep/signals.js +417 -0
  39. package/src/gep/skillDistiller.js +499 -0
  40. package/src/gep/solidify.js +1681 -0
  41. package/src/gep/strategy.js +126 -0
  42. package/src/gep/taskReceiver.js +528 -0
  43. package/src/gep/validationReport.js +55 -0
  44. package/src/ops/cleanup.js +80 -0
  45. package/src/ops/commentary.js +60 -0
  46. package/src/ops/health_check.js +106 -0
  47. package/src/ops/index.js +11 -0
  48. package/src/ops/innovation.js +67 -0
  49. package/src/ops/lifecycle.js +168 -0
  50. package/src/ops/self_repair.js +72 -0
  51. package/src/ops/skills_monitor.js +143 -0
  52. package/src/ops/trigger.js +33 -0
@@ -0,0 +1,417 @@
1
+ // Opportunity signal names (shared with mutation.js and personality.js).
2
+ var OPPORTUNITY_SIGNALS = [
3
+ 'user_feature_request',
4
+ 'user_improvement_suggestion',
5
+ 'perf_bottleneck',
6
+ 'capability_gap',
7
+ 'stable_success_plateau',
8
+ 'external_opportunity',
9
+ 'recurring_error',
10
+ 'unsupported_input_type',
11
+ 'evolution_stagnation_detected',
12
+ 'repair_loop_detected',
13
+ 'force_innovation_after_repair_loop',
14
+ ];
15
+
16
+ function hasOpportunitySignal(signals) {
17
+ var list = Array.isArray(signals) ? signals : [];
18
+ for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
19
+ var name = OPPORTUNITY_SIGNALS[i];
20
+ if (list.includes(name)) return true;
21
+ if (list.some(function (s) { return String(s).startsWith(name + ':'); })) return true;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ // Build a de-duplication set from recent evolution events.
27
+ // Returns an object: { suppressedSignals: Set<string>, recentIntents: string[], consecutiveRepairCount: number }
28
+ function analyzeRecentHistory(recentEvents) {
29
+ if (!Array.isArray(recentEvents) || recentEvents.length === 0) {
30
+ return { suppressedSignals: new Set(), recentIntents: [], consecutiveRepairCount: 0 };
31
+ }
32
+ // Take only the last 10 events
33
+ var recent = recentEvents.slice(-10);
34
+
35
+ // Count consecutive same-intent runs at the tail
36
+ var consecutiveRepairCount = 0;
37
+ for (var i = recent.length - 1; i >= 0; i--) {
38
+ if (recent[i].intent === 'repair') {
39
+ consecutiveRepairCount++;
40
+ } else {
41
+ break;
42
+ }
43
+ }
44
+
45
+ // Count signal frequency in last 8 events: signal -> count
46
+ var signalFreq = {};
47
+ var geneFreq = {};
48
+ var tail = recent.slice(-8);
49
+ for (var j = 0; j < tail.length; j++) {
50
+ var evt = tail[j];
51
+ var sigs = Array.isArray(evt.signals) ? evt.signals : [];
52
+ for (var k = 0; k < sigs.length; k++) {
53
+ var s = String(sigs[k]);
54
+ // Normalize: strip details suffix so frequency keys match dedup filter keys
55
+ var key = s.startsWith('errsig:') ? 'errsig'
56
+ : s.startsWith('recurring_errsig') ? 'recurring_errsig'
57
+ : s.startsWith('user_feature_request:') ? 'user_feature_request'
58
+ : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
59
+ : s;
60
+ signalFreq[key] = (signalFreq[key] || 0) + 1;
61
+ }
62
+ var genes = Array.isArray(evt.genes_used) ? evt.genes_used : [];
63
+ for (var g = 0; g < genes.length; g++) {
64
+ geneFreq[String(genes[g])] = (geneFreq[String(genes[g])] || 0) + 1;
65
+ }
66
+ }
67
+
68
+ // Suppress signals that appeared in 3+ of the last 8 events (they are being over-processed)
69
+ var suppressedSignals = new Set();
70
+ var entries = Object.entries(signalFreq);
71
+ for (var ei = 0; ei < entries.length; ei++) {
72
+ if (entries[ei][1] >= 3) {
73
+ suppressedSignals.add(entries[ei][0]);
74
+ }
75
+ }
76
+
77
+ var recentIntents = recent.map(function(e) { return e.intent || 'unknown'; });
78
+
79
+ // Count empty cycles (blast_radius.files === 0) in last 8 events.
80
+ // High ratio indicates the evolver is spinning without producing real changes.
81
+ var emptyCycleCount = 0;
82
+ for (var ec = 0; ec < tail.length; ec++) {
83
+ var br = tail[ec].blast_radius;
84
+ var em = tail[ec].meta && tail[ec].meta.empty_cycle;
85
+ if (em || (br && br.files === 0 && br.lines === 0)) {
86
+ emptyCycleCount++;
87
+ }
88
+ }
89
+
90
+ // Count consecutive empty cycles at the tail (not just total in last 8).
91
+ // This detects saturation: the evolver has exhausted innovation space and keeps producing
92
+ // zero-change cycles. Used to trigger graceful degradation to steady-state mode.
93
+ var consecutiveEmptyCycles = 0;
94
+ for (var se = recent.length - 1; se >= 0; se--) {
95
+ var seBr = recent[se].blast_radius;
96
+ var seEm = recent[se].meta && recent[se].meta.empty_cycle;
97
+ if (seEm || (seBr && seBr.files === 0 && seBr.lines === 0)) {
98
+ consecutiveEmptyCycles++;
99
+ } else {
100
+ break;
101
+ }
102
+ }
103
+
104
+ // Count consecutive failures at the tail of recent events.
105
+ // This tells the evolver "you have been failing N times in a row -- slow down."
106
+ var consecutiveFailureCount = 0;
107
+ for (var cf = recent.length - 1; cf >= 0; cf--) {
108
+ var outcome = recent[cf].outcome;
109
+ if (outcome && outcome.status === 'failed') {
110
+ consecutiveFailureCount++;
111
+ } else {
112
+ break;
113
+ }
114
+ }
115
+
116
+ // Count total failures in last 8 events (failure ratio).
117
+ var recentFailureCount = 0;
118
+ for (var rf = 0; rf < tail.length; rf++) {
119
+ var rfOut = tail[rf].outcome;
120
+ if (rfOut && rfOut.status === 'failed') recentFailureCount++;
121
+ }
122
+
123
+ return {
124
+ suppressedSignals: suppressedSignals,
125
+ recentIntents: recentIntents,
126
+ consecutiveRepairCount: consecutiveRepairCount,
127
+ emptyCycleCount: emptyCycleCount,
128
+ consecutiveEmptyCycles: consecutiveEmptyCycles,
129
+ consecutiveFailureCount: consecutiveFailureCount,
130
+ recentFailureCount: recentFailureCount,
131
+ recentFailureRatio: tail.length > 0 ? recentFailureCount / tail.length : 0,
132
+ signalFreq: signalFreq,
133
+ geneFreq: geneFreq,
134
+ };
135
+ }
136
+
137
+ function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, userSnippet, recentEvents }) {
138
+ var signals = [];
139
+ var corpus = [
140
+ String(recentSessionTranscript || ''),
141
+ String(todayLog || ''),
142
+ String(memorySnippet || ''),
143
+ String(userSnippet || ''),
144
+ ].join('\n');
145
+ var lower = corpus.toLowerCase();
146
+
147
+ // Analyze recent evolution history for de-duplication
148
+ var history = analyzeRecentHistory(recentEvents || []);
149
+
150
+ // --- Defensive signals (errors, missing resources) ---
151
+
152
+ // Refined error detection regex to avoid false positives on "fail"/"failed" in normal text.
153
+ // We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns.
154
+ var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/.test(lower);
155
+ if (errorHit) signals.push('log_error');
156
+
157
+ // Error signature (more reproducible than a coarse "log_error" tag).
158
+ try {
159
+ var lines = corpus
160
+ .split('\n')
161
+ .map(function (l) { return String(l || '').trim(); })
162
+ .filter(Boolean);
163
+
164
+ var errLine =
165
+ lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/i.test(l); }) ||
166
+ null;
167
+
168
+ if (errLine) {
169
+ var clipped = errLine.replace(/\s+/g, ' ').slice(0, 260);
170
+ signals.push('errsig:' + clipped);
171
+ }
172
+ } catch (e) {}
173
+
174
+ if (lower.includes('memory.md missing')) signals.push('memory_missing');
175
+ if (lower.includes('user.md missing')) signals.push('user_missing');
176
+ if (lower.includes('key missing')) signals.push('integration_key_missing');
177
+ if (lower.includes('no session logs found') || lower.includes('no jsonl files')) signals.push('session_logs_missing');
178
+ // if (lower.includes('pgrep') || lower.includes('ps aux')) signals.push('windows_shell_incompatible');
179
+ if (lower.includes('path.resolve(__dirname, \'../../../')) signals.push('path_outside_workspace');
180
+
181
+ // Protocol-specific drift signals
182
+ if (lower.includes('prompt') && !lower.includes('evolutionevent')) signals.push('protocol_drift');
183
+
184
+ // --- Recurring error detection (robustness signals) ---
185
+ // Count repeated identical errors -- these indicate systemic issues that need automated fixes
186
+ try {
187
+ var errorCounts = {};
188
+ var errPatterns = corpus.match(/(?:LLM error|"error"|"status":\s*"error")[^}]{0,200}/gi) || [];
189
+ for (var ep = 0; ep < errPatterns.length; ep++) {
190
+ // Normalize to a short key
191
+ var key = errPatterns[ep].replace(/\s+/g, ' ').slice(0, 100);
192
+ errorCounts[key] = (errorCounts[key] || 0) + 1;
193
+ }
194
+ var recurringErrors = Object.entries(errorCounts).filter(function (e) { return e[1] >= 3; });
195
+ if (recurringErrors.length > 0) {
196
+ signals.push('recurring_error');
197
+ // Include the top recurring error signature for the agent to diagnose
198
+ var topErr = recurringErrors.sort(function (a, b) { return b[1] - a[1]; })[0];
199
+ signals.push('recurring_errsig(' + topErr[1] + 'x):' + topErr[0].slice(0, 150));
200
+ }
201
+ } catch (e) {}
202
+
203
+ // --- Unsupported input type (e.g. GIF, video formats the LLM can't handle) ---
204
+ if (/unsupported mime|unsupported.*type|invalid.*mime/i.test(lower)) {
205
+ signals.push('unsupported_input_type');
206
+ }
207
+
208
+ // --- Opportunity signals (innovation / feature requests) ---
209
+ // Support 4 languages: EN, ZH-CN, ZH-TW, JA. Attach snippet for selector/prompt use.
210
+
211
+ var featureRequestSnippet = '';
212
+ var featEn = corpus.match(/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,120}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i);
213
+ if (featEn) featureRequestSnippet = featEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
214
+ if (!featureRequestSnippet && /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) {
215
+ var featWant = corpus.match(/.{0,80}\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b.{0,80}/i);
216
+ featureRequestSnippet = featWant ? featWant[0].replace(/\s+/g, ' ').trim().slice(0, 200) : 'feature request';
217
+ }
218
+ if (!featureRequestSnippet && /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus)) {
219
+ var featZh = corpus.match(/.{0,100}(加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能).{0,100}/);
220
+ if (featZh) featureRequestSnippet = featZh[0].replace(/\s+/g, ' ').trim().slice(0, 200);
221
+ if (!featureRequestSnippet && /我想/.test(corpus)) {
222
+ var featWantZh = corpus.match(/我想\s*[,,\.。、\s]*([\s\S]{0,400})/);
223
+ featureRequestSnippet = featWantZh ? (featWantZh[1].replace(/\s+/g, ' ').trim().slice(0, 200) || '功能需求') : '功能需求';
224
+ }
225
+ if (!featureRequestSnippet) featureRequestSnippet = '功能需求';
226
+ }
227
+ if (!featureRequestSnippet && /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus)) {
228
+ var featTw = corpus.match(/.{0,100}(加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加).{0,100}/);
229
+ featureRequestSnippet = featTw ? featTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '功能需求';
230
+ }
231
+ if (!featureRequestSnippet && /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
232
+ var featJa = corpus.match(/.{0,100}(追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい).{0,100}/);
233
+ featureRequestSnippet = featJa ? featJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '機能要望';
234
+ }
235
+ if (featureRequestSnippet || /\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus) ||
236
+ /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower) ||
237
+ /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus) ||
238
+ /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus) ||
239
+ /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
240
+ signals.push('user_feature_request:' + (featureRequestSnippet || ''));
241
+ }
242
+
243
+ // user_improvement_suggestion: 4 languages + snippet
244
+ var improvementSnippet = '';
245
+ if (!errorHit) {
246
+ var impEn = corpus.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/i);
247
+ if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
248
+ if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) {
249
+ var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/);
250
+ improvementSnippet = impZh ? impZh[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改进建议';
251
+ }
252
+ if (!improvementSnippet && /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus)) {
253
+ var impTw = corpus.match(/.{0,100}(改進一下|優化一下|簡化|重構|整理一下|弄得更好).{0,100}/);
254
+ improvementSnippet = impTw ? impTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改進建議';
255
+ }
256
+ if (!improvementSnippet && /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus)) {
257
+ var impJa = corpus.match(/.{0,100}(改善|最適化|簡素化|リファクタ|良くして|改良).{0,100}/);
258
+ improvementSnippet = impJa ? impJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改善要望';
259
+ }
260
+ var hasImprovement = improvementSnippet ||
261
+ /\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower) ||
262
+ /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus) ||
263
+ /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus) ||
264
+ /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus);
265
+ if (hasImprovement) {
266
+ signals.push('user_improvement_suggestion:' + (improvementSnippet || ''));
267
+ }
268
+ }
269
+
270
+ // perf_bottleneck: performance issues detected
271
+ if (/\b(slow|timeout|timed?\s*out|latency|bottleneck|took too long|performance issue|high cpu|high memory|oom|out of memory)\b/i.test(lower)) {
272
+ signals.push('perf_bottleneck');
273
+ }
274
+
275
+ // capability_gap: something is explicitly unsupported or missing
276
+ if (/\b(not supported|cannot|doesn'?t support|no way to|missing feature|unsupported|not available|not implemented|no support for)\b/i.test(lower)) {
277
+ // Only fire if it is not just a missing file/config signal
278
+ if (!signals.includes('memory_missing') && !signals.includes('user_missing') && !signals.includes('session_logs_missing')) {
279
+ signals.push('capability_gap');
280
+ }
281
+ }
282
+
283
+ // --- Tool Usage Analytics ---
284
+ var toolUsage = {};
285
+ var toolMatches = corpus.match(/\[TOOL:\s*([\w-]+)\]/g) || [];
286
+
287
+ // Extract exec commands to identify benign loops (like watchdog checks)
288
+ var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
289
+ var benignExecCount = execCommands.length;
290
+
291
+ for (var i = 0; i < toolMatches.length; i++) {
292
+ var toolName = toolMatches[i].match(/\[TOOL:\s*([\w-]+)\]/)[1];
293
+ toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
294
+ }
295
+
296
+ // Adjust exec count by subtracting benign commands
297
+ if (toolUsage['exec']) {
298
+ toolUsage['exec'] = Math.max(0, toolUsage['exec'] - benignExecCount);
299
+ }
300
+
301
+ Object.keys(toolUsage).forEach(function(tool) {
302
+ if (toolUsage[tool] >= 10) { // Bumped threshold from 5 to 10
303
+ signals.push('high_tool_usage:' + tool);
304
+ }
305
+ // Detect repeated exec usage (often a sign of manual loops or inefficient automation)
306
+ if (tool === 'exec' && toolUsage[tool] >= 5) { // Bumped threshold from 3 to 5
307
+ signals.push('repeated_tool_usage:exec');
308
+ }
309
+ });
310
+
311
+ // --- Signal prioritization ---
312
+ // Remove cosmetic signals when actionable signals exist
313
+ var actionable = signals.filter(function (s) {
314
+ return s !== 'user_missing' && s !== 'memory_missing' && s !== 'session_logs_missing' && s !== 'windows_shell_incompatible';
315
+ });
316
+ // If we have actionable signals, drop the cosmetic ones
317
+ if (actionable.length > 0) {
318
+ signals = actionable;
319
+ }
320
+
321
+ // --- De-duplication: suppress signals that have been over-processed ---
322
+ if (history.suppressedSignals.size > 0) {
323
+ var beforeDedup = signals.length;
324
+ signals = signals.filter(function (s) {
325
+ // Normalize signal key for comparison
326
+ var key = s.startsWith('errsig:') ? 'errsig'
327
+ : s.startsWith('recurring_errsig') ? 'recurring_errsig'
328
+ : s.startsWith('user_feature_request:') ? 'user_feature_request'
329
+ : s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
330
+ : s;
331
+ return !history.suppressedSignals.has(key);
332
+ });
333
+ if (beforeDedup > 0 && signals.length === 0) {
334
+ // All signals were suppressed = system is stable but stuck in a loop
335
+ // Force innovation
336
+ signals.push('evolution_stagnation_detected');
337
+ signals.push('stable_success_plateau');
338
+ }
339
+ }
340
+
341
+ // --- Force innovation after 3+ consecutive repairs ---
342
+ if (history.consecutiveRepairCount >= 3) {
343
+ // Remove repair-only signals (log_error, errsig) and inject innovation signals
344
+ signals = signals.filter(function (s) {
345
+ return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
346
+ });
347
+ if (signals.length === 0) {
348
+ signals.push('repair_loop_detected');
349
+ signals.push('stable_success_plateau');
350
+ }
351
+ // Append a directive signal that the prompt can pick up
352
+ signals.push('force_innovation_after_repair_loop');
353
+ }
354
+
355
+ // --- Force innovation after too many empty cycles (zero blast radius) ---
356
+ // If >= 50% of last 8 cycles produced no code changes, the evolver is spinning idle.
357
+ // Strip repair signals and force innovate to break the empty loop.
358
+ if (history.emptyCycleCount >= 4) {
359
+ signals = signals.filter(function (s) {
360
+ return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
361
+ });
362
+ if (!signals.includes('empty_cycle_loop_detected')) signals.push('empty_cycle_loop_detected');
363
+ if (!signals.includes('stable_success_plateau')) signals.push('stable_success_plateau');
364
+ }
365
+
366
+ // --- Saturation detection (graceful degradation) ---
367
+ // When consecutive empty cycles pile up at the tail, the evolver has exhausted its
368
+ // innovation space. Instead of spinning idle forever, signal that the system should
369
+ // switch to steady-state maintenance mode with reduced evolution frequency.
370
+ // This directly addresses the Echo-MingXuan failure: Cycle #55 hit "no committable
371
+ // code changes" and load spiked to 1.30 because there was no degradation strategy.
372
+ if (history.consecutiveEmptyCycles >= 5) {
373
+ if (!signals.includes('force_steady_state')) signals.push('force_steady_state');
374
+ if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
375
+ } else if (history.consecutiveEmptyCycles >= 3) {
376
+ if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
377
+ }
378
+
379
+ // --- Failure streak awareness ---
380
+ // When the evolver has failed many consecutive cycles, inject a signal
381
+ // telling the LLM to be more conservative and avoid repeating the same approach.
382
+ if (history.consecutiveFailureCount >= 3) {
383
+ signals.push('consecutive_failure_streak_' + history.consecutiveFailureCount);
384
+ // After 5+ consecutive failures, force a strategy change (don't keep trying the same thing)
385
+ if (history.consecutiveFailureCount >= 5) {
386
+ signals.push('failure_loop_detected');
387
+ // Strip the dominant gene's signals to force a different gene selection
388
+ var topGene = null;
389
+ var topGeneCount = 0;
390
+ var gfEntries = Object.entries(history.geneFreq);
391
+ for (var gfi = 0; gfi < gfEntries.length; gfi++) {
392
+ if (gfEntries[gfi][1] > topGeneCount) {
393
+ topGeneCount = gfEntries[gfi][1];
394
+ topGene = gfEntries[gfi][0];
395
+ }
396
+ }
397
+ if (topGene) {
398
+ signals.push('ban_gene:' + topGene);
399
+ }
400
+ }
401
+ }
402
+
403
+ // High failure ratio in recent history (>= 75% failed in last 8 cycles)
404
+ if (history.recentFailureRatio >= 0.75) {
405
+ signals.push('high_failure_ratio');
406
+ signals.push('force_innovation_after_repair_loop');
407
+ }
408
+
409
+ // If no signals at all, add a default innovation signal
410
+ if (signals.length === 0) {
411
+ signals.push('stable_success_plateau');
412
+ }
413
+
414
+ return Array.from(new Set(signals));
415
+ }
416
+
417
+ module.exports = { extractSignals, hasOpportunitySignal, analyzeRecentHistory, OPPORTUNITY_SIGNALS };