@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,206 @@
1
+ // Hub Asset Review: submit usage-verified reviews after solidify.
2
+ //
3
+ // When an evolution cycle reuses a Hub asset (source_type = 'reused' or 'reference'),
4
+ // we submit a review to POST /a2a/assets/:assetId/reviews after solidify completes.
5
+ // Rating is derived from outcome: success -> 4-5, failure -> 1-2.
6
+ // Reviews are non-blocking; errors never affect the solidify result.
7
+ // Duplicate prevention: a local file tracks reviewed assetIds to avoid re-reviewing.
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { getNodeId, getHubNodeSecret } = require('./a2aProtocol');
12
+ const { logAssetCall } = require('./assetCallLog');
13
+
14
+ const REVIEW_HISTORY_FILE = path.join(
15
+ require('./paths').getEvolutionDir(),
16
+ 'hub_review_history.json'
17
+ );
18
+
19
+ const REVIEW_HISTORY_MAX_ENTRIES = 500;
20
+
21
+ function _loadReviewHistory() {
22
+ try {
23
+ if (!fs.existsSync(REVIEW_HISTORY_FILE)) return {};
24
+ const raw = fs.readFileSync(REVIEW_HISTORY_FILE, 'utf8');
25
+ if (!raw.trim()) return {};
26
+ return JSON.parse(raw);
27
+ } catch {
28
+ return {};
29
+ }
30
+ }
31
+
32
+ function _saveReviewHistory(history) {
33
+ try {
34
+ const dir = path.dirname(REVIEW_HISTORY_FILE);
35
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
36
+ const keys = Object.keys(history);
37
+ if (keys.length > REVIEW_HISTORY_MAX_ENTRIES) {
38
+ const sorted = keys
39
+ .map(k => ({ k, t: history[k].at || 0 }))
40
+ .sort((a, b) => a.t - b.t);
41
+ const toRemove = sorted.slice(0, keys.length - REVIEW_HISTORY_MAX_ENTRIES);
42
+ for (const entry of toRemove) delete history[entry.k];
43
+ }
44
+ const tmp = REVIEW_HISTORY_FILE + '.tmp';
45
+ fs.writeFileSync(tmp, JSON.stringify(history, null, 2) + '\n', 'utf8');
46
+ fs.renameSync(tmp, REVIEW_HISTORY_FILE);
47
+ } catch {}
48
+ }
49
+
50
+ function _alreadyReviewed(assetId) {
51
+ const history = _loadReviewHistory();
52
+ return !!history[assetId];
53
+ }
54
+
55
+ function _markReviewed(assetId, rating, success) {
56
+ const history = _loadReviewHistory();
57
+ history[assetId] = { at: Date.now(), rating, success };
58
+ _saveReviewHistory(history);
59
+ }
60
+
61
+ function _deriveRating(outcome, constraintCheck) {
62
+ if (outcome && outcome.status === 'success') {
63
+ const score = Number(outcome.score) || 0;
64
+ return score >= 0.85 ? 5 : 4;
65
+ }
66
+ const hasConstraintViolation =
67
+ constraintCheck &&
68
+ Array.isArray(constraintCheck.violations) &&
69
+ constraintCheck.violations.length > 0;
70
+ return hasConstraintViolation ? 1 : 2;
71
+ }
72
+
73
+ function _buildReviewContent({ outcome, gene, signals, blast, sourceType }) {
74
+ const parts = [];
75
+ const status = outcome && outcome.status ? outcome.status : 'unknown';
76
+ const score = outcome && Number.isFinite(Number(outcome.score))
77
+ ? Number(outcome.score).toFixed(2) : '?';
78
+
79
+ parts.push('Outcome: ' + status + ' (score: ' + score + ')');
80
+ parts.push('Reuse mode: ' + (sourceType || 'unknown'));
81
+
82
+ if (gene && gene.id) {
83
+ parts.push('Gene: ' + gene.id + ' (' + (gene.category || 'unknown') + ')');
84
+ }
85
+
86
+ if (Array.isArray(signals) && signals.length > 0) {
87
+ parts.push('Signals: ' + signals.slice(0, 6).join(', '));
88
+ }
89
+
90
+ if (blast) {
91
+ parts.push('Blast radius: ' + (blast.files || 0) + ' file(s), ' + (blast.lines || 0) + ' line(s)');
92
+ }
93
+
94
+ if (status === 'success') {
95
+ parts.push('The fetched asset was successfully applied and solidified.');
96
+ } else {
97
+ parts.push('The fetched asset did not lead to a successful evolution cycle.');
98
+ }
99
+
100
+ return parts.join('\n').slice(0, 2000);
101
+ }
102
+
103
+ function getHubUrl() {
104
+ return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
105
+ }
106
+
107
+ async function submitHubReview({
108
+ reusedAssetId,
109
+ sourceType,
110
+ outcome,
111
+ gene,
112
+ signals,
113
+ blast,
114
+ constraintCheck,
115
+ runId,
116
+ }) {
117
+ var hubUrl = getHubUrl();
118
+ if (!hubUrl) return { submitted: false, reason: 'no_hub_url' };
119
+
120
+ if (!reusedAssetId || typeof reusedAssetId !== 'string') {
121
+ return { submitted: false, reason: 'no_reused_asset_id' };
122
+ }
123
+
124
+ if (sourceType !== 'reused' && sourceType !== 'reference') {
125
+ return { submitted: false, reason: 'not_hub_sourced' };
126
+ }
127
+
128
+ if (_alreadyReviewed(reusedAssetId)) {
129
+ return { submitted: false, reason: 'already_reviewed' };
130
+ }
131
+
132
+ var rating = _deriveRating(outcome, constraintCheck);
133
+ var content = _buildReviewContent({ outcome, gene, signals, blast, sourceType });
134
+ var senderId = getNodeId();
135
+
136
+ var endpoint = hubUrl + '/a2a/assets/' + encodeURIComponent(reusedAssetId) + '/reviews';
137
+
138
+ var headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
139
+ var secret = getHubNodeSecret();
140
+ if (secret) {
141
+ headers['Authorization'] = 'Bearer ' + secret;
142
+ }
143
+
144
+ var body = JSON.stringify({
145
+ sender_id: senderId,
146
+ rating: rating,
147
+ content: content,
148
+ });
149
+
150
+ try {
151
+ var controller = new AbortController();
152
+ var timer = setTimeout(function () { controller.abort('hub_review_timeout'); }, 10000);
153
+
154
+ var res = await fetch(endpoint, {
155
+ method: 'POST',
156
+ headers: headers,
157
+ body: body,
158
+ signal: controller.signal,
159
+ });
160
+ clearTimeout(timer);
161
+
162
+ if (res.ok) {
163
+ _markReviewed(reusedAssetId, rating, true);
164
+ console.log(
165
+ '[HubReview] Submitted review for ' + reusedAssetId + ': rating=' + rating + ', outcome=' + (outcome && outcome.status)
166
+ );
167
+ logAssetCall({
168
+ run_id: runId || null,
169
+ action: 'hub_review_submitted',
170
+ asset_id: reusedAssetId,
171
+ extra: { rating: rating, outcome_status: outcome && outcome.status },
172
+ });
173
+ return { submitted: true, rating: rating, asset_id: reusedAssetId };
174
+ }
175
+
176
+ var errData = await res.json().catch(function () { return {}; });
177
+ var errCode = errData.error || errData.code || ('http_' + res.status);
178
+
179
+ if (errCode === 'already_reviewed') {
180
+ _markReviewed(reusedAssetId, rating, false);
181
+ }
182
+
183
+ console.log('[HubReview] Hub rejected review for ' + reusedAssetId + ': ' + errCode);
184
+ logAssetCall({
185
+ run_id: runId || null,
186
+ action: 'hub_review_rejected',
187
+ asset_id: reusedAssetId,
188
+ extra: { rating: rating, error: errCode },
189
+ });
190
+ return { submitted: false, reason: errCode, rating: rating };
191
+ } catch (err) {
192
+ var reason = err.name === 'AbortError' ? 'timeout' : 'fetch_error';
193
+ console.log('[HubReview] Failed (non-fatal, ' + reason + '): ' + err.message);
194
+ logAssetCall({
195
+ run_id: runId || null,
196
+ action: 'hub_review_failed',
197
+ asset_id: reusedAssetId,
198
+ extra: { rating: rating, reason: reason, error: err.message },
199
+ });
200
+ return { submitted: false, reason: reason, error: err.message };
201
+ }
202
+ }
203
+
204
+ module.exports = {
205
+ submitHubReview,
206
+ };
@@ -0,0 +1,237 @@
1
+ // Hub Search-First Evolution: query evomap-hub for reusable solutions before local solve.
2
+ //
3
+ // Flow: extractSignals() -> hubSearch(signals) -> if hit: reuse; if miss: normal evolve
4
+ // Two modes: direct (skip local reasoning) | reference (inject into prompt as strong hint)
5
+ //
6
+ // Two-phase search-then-fetch to minimize credit cost:
7
+ // Phase 1: POST /a2a/fetch with signals + search_only=true (free, metadata only)
8
+ // Phase 2: POST /a2a/fetch with asset_ids=[selected] (pays for 1 asset only)
9
+
10
+ const { getNodeId, buildFetch, getHubNodeSecret } = require('./a2aProtocol');
11
+ const { logAssetCall } = require('./assetCallLog');
12
+
13
+ const DEFAULT_MIN_REUSE_SCORE = 0.72;
14
+ const DEFAULT_REUSE_MODE = 'reference'; // 'direct' | 'reference'
15
+ const MAX_STREAK_CAP = 5;
16
+ const TIMEOUT_REASON = 'hub_search_timeout';
17
+
18
+ function getHubUrl() {
19
+ return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
20
+ }
21
+
22
+ function getReuseMode() {
23
+ const m = String(process.env.EVOLVER_REUSE_MODE || DEFAULT_REUSE_MODE).toLowerCase();
24
+ return m === 'direct' ? 'direct' : 'reference';
25
+ }
26
+
27
+ function getMinReuseScore() {
28
+ const n = Number(process.env.EVOLVER_MIN_REUSE_SCORE);
29
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_MIN_REUSE_SCORE;
30
+ }
31
+
32
+ /**
33
+ * Score a hub asset for local reuse quality.
34
+ * rank = confidence * min(max(success_streak, 1), MAX_STREAK_CAP) * (reputation / 100)
35
+ * Streak is capped to prevent unbounded score inflation.
36
+ */
37
+ function scoreHubResult(asset) {
38
+ const confidence = Number(asset.confidence) || 0;
39
+ const streak = Math.min(Math.max(Number(asset.success_streak) || 0, 1), MAX_STREAK_CAP);
40
+ const repRaw = Number(asset.reputation_score);
41
+ const reputation = Number.isFinite(repRaw) ? repRaw : 50;
42
+ return confidence * streak * (reputation / 100);
43
+ }
44
+
45
+ /**
46
+ * Pick the best matching asset above the threshold.
47
+ * Returns { match, score, mode } or null if nothing qualifies.
48
+ */
49
+ function pickBestMatch(results, threshold) {
50
+ if (!Array.isArray(results) || results.length === 0) return null;
51
+
52
+ let best = null;
53
+ let bestScore = 0;
54
+
55
+ for (const asset of results) {
56
+ if (asset.status && asset.status !== 'promoted') continue;
57
+ const s = scoreHubResult(asset);
58
+ if (s > bestScore) {
59
+ bestScore = s;
60
+ best = asset;
61
+ }
62
+ }
63
+
64
+ if (!best || bestScore < threshold) return null;
65
+
66
+ return {
67
+ match: best,
68
+ score: Math.round(bestScore * 1000) / 1000,
69
+ mode: getReuseMode(),
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Search the hub for reusable assets matching the given signals.
75
+ *
76
+ * Two-phase flow to minimize credit cost:
77
+ * Phase 1: search_only=true -> get candidate metadata (free, no credit cost)
78
+ * Phase 2: asset_ids=[best_match] -> fetch full payload for the selected asset only
79
+ *
80
+ * Falls back to single-call fetch (old behavior) if search_only is not supported.
81
+ * Returns { hit: true, match, score, mode } or { hit: false }.
82
+ */
83
+ async function hubSearch(signals, opts) {
84
+ const hubUrl = getHubUrl();
85
+ if (!hubUrl) return { hit: false, reason: 'no_hub_url' };
86
+
87
+ const signalList = Array.isArray(signals)
88
+ ? signals.map(s => typeof s === 'string' ? s.trim() : '').filter(Boolean)
89
+ : [];
90
+ if (signalList.length === 0) return { hit: false, reason: 'no_signals' };
91
+
92
+ const threshold = (opts && Number.isFinite(opts.threshold)) ? opts.threshold : getMinReuseScore();
93
+ const timeout = (opts && Number.isFinite(opts.timeoutMs)) ? opts.timeoutMs : 8000;
94
+
95
+ try {
96
+ // Phase 1: search_only to get candidate metadata (free)
97
+ const searchMsg = buildFetch({ signals: signalList, searchOnly: true });
98
+ const endpoint = hubUrl + '/a2a/fetch';
99
+
100
+ const controller = new AbortController();
101
+ const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeout);
102
+
103
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };
104
+ const secret = getHubNodeSecret();
105
+ if (secret) {
106
+ headers['Authorization'] = 'Bearer ' + secret;
107
+ } else {
108
+ const token = process.env.A2A_HUB_TOKEN;
109
+ if (token) headers['Authorization'] = `Bearer ${token}`;
110
+ }
111
+
112
+ const res = await fetch(endpoint, {
113
+ method: 'POST',
114
+ headers,
115
+ body: JSON.stringify(searchMsg),
116
+ signal: controller.signal,
117
+ });
118
+ clearTimeout(timer);
119
+
120
+ if (!res.ok) {
121
+ logAssetCall({
122
+ run_id: (opts && opts.run_id) || null,
123
+ action: 'hub_search_miss',
124
+ signals: signalList,
125
+ reason: `hub_http_${res.status}`,
126
+ via: 'search_then_fetch',
127
+ });
128
+ return { hit: false, reason: `hub_http_${res.status}` };
129
+ }
130
+
131
+ const data = await res.json();
132
+ const results = (data && data.payload && Array.isArray(data.payload.results))
133
+ ? data.payload.results
134
+ : [];
135
+
136
+ if (results.length === 0) {
137
+ logAssetCall({
138
+ run_id: (opts && opts.run_id) || null,
139
+ action: 'hub_search_miss',
140
+ signals: signalList,
141
+ reason: 'no_results',
142
+ via: 'search_then_fetch',
143
+ });
144
+ return { hit: false, reason: 'no_results' };
145
+ }
146
+
147
+ const pick = pickBestMatch(results, threshold);
148
+ if (!pick) {
149
+ logAssetCall({
150
+ run_id: (opts && opts.run_id) || null,
151
+ action: 'hub_search_miss',
152
+ signals: signalList,
153
+ reason: 'below_threshold',
154
+ extra: { candidates: results.length, threshold },
155
+ via: 'search_then_fetch',
156
+ });
157
+ return { hit: false, reason: 'below_threshold', candidates: results.length };
158
+ }
159
+
160
+ // Phase 2: fetch full payload for the selected asset only (pays for 1 asset)
161
+ const selectedAssetId = pick.match.asset_id;
162
+ if (selectedAssetId) {
163
+ try {
164
+ const fetchMsg = buildFetch({ assetIds: [selectedAssetId] });
165
+ const controller2 = new AbortController();
166
+ const timer2 = setTimeout(() => controller2.abort(TIMEOUT_REASON), timeout);
167
+
168
+ const res2 = await fetch(endpoint, {
169
+ method: 'POST',
170
+ headers,
171
+ body: JSON.stringify(fetchMsg),
172
+ signal: controller2.signal,
173
+ });
174
+ clearTimeout(timer2);
175
+
176
+ if (res2.ok) {
177
+ const data2 = await res2.json();
178
+ const fullResults = (data2 && data2.payload && Array.isArray(data2.payload.results))
179
+ ? data2.payload.results
180
+ : [];
181
+ if (fullResults.length > 0) {
182
+ pick.match = { ...pick.match, ...fullResults[0] };
183
+ }
184
+ }
185
+ } catch (fetchErr) {
186
+ console.log(`[HubSearch] Phase 2 fetch failed (non-fatal): ${fetchErr.message}`);
187
+ }
188
+ }
189
+
190
+ console.log(`[HubSearch] Hit via search+fetch: ${pick.match.asset_id || 'unknown'} (score=${pick.score}, mode=${pick.mode})`);
191
+
192
+ logAssetCall({
193
+ run_id: (opts && opts.run_id) || null,
194
+ action: 'hub_search_hit',
195
+ asset_id: pick.match.asset_id || null,
196
+ asset_type: pick.match.asset_type || pick.match.type || null,
197
+ source_node_id: pick.match.source_node_id || null,
198
+ chain_id: pick.match.chain_id || null,
199
+ score: pick.score,
200
+ mode: pick.mode,
201
+ signals: signalList,
202
+ via: 'search_then_fetch',
203
+ });
204
+
205
+ return {
206
+ hit: true,
207
+ match: pick.match,
208
+ score: pick.score,
209
+ mode: pick.mode,
210
+ asset_id: pick.match.asset_id || null,
211
+ source_node_id: pick.match.source_node_id || null,
212
+ chain_id: pick.match.chain_id || null,
213
+ };
214
+ } catch (err) {
215
+ const isTimeout = err.name === 'AbortError' || (err.cause && err.cause === TIMEOUT_REASON);
216
+ const reason = isTimeout ? 'timeout' : 'fetch_error';
217
+ console.log(`[HubSearch] Failed (non-fatal, ${reason}): ${err.message}`);
218
+ logAssetCall({
219
+ run_id: (opts && opts.run_id) || null,
220
+ action: 'hub_search_miss',
221
+ signals: signalList,
222
+ reason,
223
+ extra: { error: err.message },
224
+ via: 'search_then_fetch',
225
+ });
226
+ return { hit: false, reason, error: err.message };
227
+ }
228
+ }
229
+
230
+ module.exports = {
231
+ hubSearch,
232
+ scoreHubResult,
233
+ pickBestMatch,
234
+ getReuseMode,
235
+ getMinReuseScore,
236
+ getHubUrl,
237
+ };
@@ -0,0 +1,262 @@
1
+ // Automatic GitHub issue reporter for recurring evolver failures.
2
+ // When the evolver hits persistent errors (failure streaks, recurring errors),
3
+ // this module files a GitHub issue with sanitized logs and environment info.
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const crypto = require('crypto');
8
+ const { getEvolutionDir } = require('./paths');
9
+ const { captureEnvFingerprint } = require('./envFingerprint');
10
+ const { redactString } = require('./sanitize');
11
+ const { getNodeId } = require('./a2aProtocol');
12
+
13
+ const STATE_FILE_NAME = 'issue_reporter_state.json';
14
+ const DEFAULT_REPO = 'autogame-17/capability-evolver';
15
+ const DEFAULT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
16
+ const DEFAULT_MIN_STREAK = 5;
17
+ const MAX_LOG_CHARS = 2000;
18
+ const MAX_EVENTS = 5;
19
+
20
+ function getConfig() {
21
+ var enabled = String(process.env.EVOLVER_AUTO_ISSUE || 'true').toLowerCase();
22
+ if (enabled === 'false' || enabled === '0') return null;
23
+ return {
24
+ repo: process.env.EVOLVER_ISSUE_REPO || DEFAULT_REPO,
25
+ cooldownMs: Number(process.env.EVOLVER_ISSUE_COOLDOWN_MS) || DEFAULT_COOLDOWN_MS,
26
+ minStreak: Number(process.env.EVOLVER_ISSUE_MIN_STREAK) || DEFAULT_MIN_STREAK,
27
+ };
28
+ }
29
+
30
+ function getGithubToken() {
31
+ return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_PAT || '';
32
+ }
33
+
34
+ function getStatePath() {
35
+ return path.join(getEvolutionDir(), STATE_FILE_NAME);
36
+ }
37
+
38
+ function readState() {
39
+ try {
40
+ var p = getStatePath();
41
+ if (fs.existsSync(p)) {
42
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
43
+ }
44
+ } catch (_) {}
45
+ return { lastReportedAt: null, recentIssueKeys: [] };
46
+ }
47
+
48
+ function writeState(state) {
49
+ try {
50
+ var dir = getEvolutionDir();
51
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
52
+ fs.writeFileSync(getStatePath(), JSON.stringify(state, null, 2) + '\n');
53
+ } catch (_) {}
54
+ }
55
+
56
+ function truncateNodeId(nodeId) {
57
+ if (!nodeId || typeof nodeId !== 'string') return 'unknown';
58
+ if (nodeId.length <= 10) return nodeId;
59
+ return nodeId.slice(0, 10) + '...';
60
+ }
61
+
62
+ function computeErrorKey(signals) {
63
+ var relevant = signals
64
+ .filter(function (s) {
65
+ return s.startsWith('recurring_errsig') ||
66
+ s.startsWith('ban_gene:') ||
67
+ s === 'recurring_error' ||
68
+ s === 'failure_loop_detected' ||
69
+ s === 'high_failure_ratio';
70
+ })
71
+ .sort()
72
+ .join('|');
73
+ return crypto.createHash('sha256').update(relevant || 'unknown').digest('hex').slice(0, 16);
74
+ }
75
+
76
+ function extractErrorSignature(signals) {
77
+ var errSig = signals.find(function (s) { return s.startsWith('recurring_errsig'); });
78
+ if (errSig) {
79
+ return errSig.replace(/^recurring_errsig\(\d+x\):/, '').trim().slice(0, 200);
80
+ }
81
+ var banned = signals.find(function (s) { return s.startsWith('ban_gene:'); });
82
+ if (banned) return 'Repeated failures with gene: ' + banned.replace('ban_gene:', '');
83
+ return 'Persistent evolution failure';
84
+ }
85
+
86
+ function extractStreakCount(signals) {
87
+ for (var i = 0; i < signals.length; i++) {
88
+ if (signals[i].startsWith('consecutive_failure_streak_')) {
89
+ var n = parseInt(signals[i].replace('consecutive_failure_streak_', ''), 10);
90
+ if (Number.isFinite(n)) return n;
91
+ }
92
+ }
93
+ return 0;
94
+ }
95
+
96
+ function formatRecentEvents(events) {
97
+ if (!Array.isArray(events) || events.length === 0) return '_No recent events available._';
98
+ var failed = events.filter(function (e) { return e && e.outcome && e.outcome.status === 'failed'; });
99
+ var rows = failed.slice(-MAX_EVENTS).map(function (e, idx) {
100
+ var intent = e.intent || '-';
101
+ var gene = (Array.isArray(e.genes_used) && e.genes_used[0]) || '-';
102
+ var outcome = (e.outcome && e.outcome.status) || '-';
103
+ var reason = (e.outcome && e.outcome.reason) || '';
104
+ if (reason.length > 80) reason = reason.slice(0, 80) + '...';
105
+ reason = redactString(reason);
106
+ return '| ' + (idx + 1) + ' | ' + intent + ' | ' + gene + ' | ' + outcome + ' | ' + reason + ' |';
107
+ });
108
+ if (rows.length === 0) return '_No failed events in recent history._';
109
+ return '| # | Intent | Gene | Outcome | Reason |\n|---|--------|------|---------|--------|\n' + rows.join('\n');
110
+ }
111
+
112
+ function buildIssueBody(opts) {
113
+ var fp = opts.envFingerprint || captureEnvFingerprint();
114
+ var signals = opts.signals || [];
115
+ var recentEvents = opts.recentEvents || [];
116
+ var sessionLog = opts.sessionLog || '';
117
+ var streakCount = extractStreakCount(signals);
118
+ var errorSig = extractErrorSignature(signals);
119
+ var nodeId = truncateNodeId(getNodeId());
120
+
121
+ var failureSignals = signals.filter(function (s) {
122
+ return s.startsWith('recurring_') ||
123
+ s.startsWith('consecutive_failure') ||
124
+ s.startsWith('failure_loop') ||
125
+ s.startsWith('high_failure') ||
126
+ s.startsWith('ban_gene:') ||
127
+ s === 'force_innovation_after_repair_loop';
128
+ }).join(', ');
129
+
130
+ var sanitizedLog = redactString(
131
+ typeof sessionLog === 'string' ? sessionLog.slice(-MAX_LOG_CHARS) : ''
132
+ );
133
+
134
+ var eventsTable = formatRecentEvents(recentEvents);
135
+
136
+ var reportId = crypto.createHash('sha256')
137
+ .update(nodeId + '|' + Date.now() + '|' + errorSig)
138
+ .digest('hex').slice(0, 12);
139
+
140
+ var body = [
141
+ '## Environment',
142
+ '- **Evolver Version:** ' + (fp.evolver_version || 'unknown'),
143
+ '- **Node.js:** ' + (fp.node_version || process.version),
144
+ '- **Platform:** ' + (fp.platform || process.platform) + ' ' + (fp.arch || process.arch),
145
+ '- **Container:** ' + (fp.container ? 'yes' : 'no'),
146
+ '',
147
+ '## Failure Summary',
148
+ '- **Consecutive failures:** ' + (streakCount || 'N/A'),
149
+ '- **Failure signals:** ' + (failureSignals || 'none'),
150
+ '',
151
+ '## Error Signature',
152
+ '```',
153
+ redactString(errorSig),
154
+ '```',
155
+ '',
156
+ '## Recent Evolution Events (sanitized)',
157
+ eventsTable,
158
+ '',
159
+ '## Session Log Excerpt (sanitized)',
160
+ '```',
161
+ sanitizedLog || '_No session log available._',
162
+ '```',
163
+ '',
164
+ '---',
165
+ '_This issue was automatically created by evolver v' + (fp.evolver_version || 'unknown') + '._',
166
+ '_Device: ' + nodeId + ' | Report ID: ' + reportId + '_',
167
+ ];
168
+
169
+ return body.join('\n');
170
+ }
171
+
172
+ function shouldReport(signals, config) {
173
+ if (!config) return false;
174
+
175
+ var hasFailureLoop = signals.includes('failure_loop_detected');
176
+ var hasRecurringAndHigh = signals.includes('recurring_error') && signals.includes('high_failure_ratio');
177
+
178
+ if (!hasFailureLoop && !hasRecurringAndHigh) return false;
179
+
180
+ var streakCount = extractStreakCount(signals);
181
+ if (streakCount > 0 && streakCount < config.minStreak) return false;
182
+
183
+ var state = readState();
184
+ var errorKey = computeErrorKey(signals);
185
+
186
+ if (state.lastReportedAt) {
187
+ var elapsed = Date.now() - new Date(state.lastReportedAt).getTime();
188
+ if (elapsed < config.cooldownMs) {
189
+ var recentKeys = Array.isArray(state.recentIssueKeys) ? state.recentIssueKeys : [];
190
+ if (recentKeys.includes(errorKey)) {
191
+ return false;
192
+ }
193
+ }
194
+ }
195
+
196
+ return true;
197
+ }
198
+
199
+ async function createGithubIssue(repo, title, body, token) {
200
+ var url = 'https://api.github.com/repos/' + repo + '/issues';
201
+ var response = await fetch(url, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Authorization': 'Bearer ' + token,
205
+ 'Accept': 'application/vnd.github+json',
206
+ 'Content-Type': 'application/json',
207
+ 'X-GitHub-Api-Version': '2022-11-28',
208
+ },
209
+ body: JSON.stringify({ title: title, body: body }),
210
+ signal: AbortSignal.timeout(15000),
211
+ });
212
+
213
+ if (!response.ok) {
214
+ var errText = '';
215
+ try { errText = await response.text(); } catch (_) {}
216
+ throw new Error('GitHub API ' + response.status + ': ' + errText.slice(0, 200));
217
+ }
218
+
219
+ var data = await response.json();
220
+ return { number: data.number, url: data.html_url };
221
+ }
222
+
223
+ async function maybeReportIssue(opts) {
224
+ var config = getConfig();
225
+ if (!config) return;
226
+
227
+ var signals = opts.signals || [];
228
+
229
+ if (!shouldReport(signals, config)) return;
230
+
231
+ var token = getGithubToken();
232
+ if (!token) {
233
+ console.log('[IssueReporter] No GitHub token available. Skipping auto-report.');
234
+ return;
235
+ }
236
+
237
+ var errorSig = extractErrorSignature(signals);
238
+ var titleSig = errorSig.slice(0, 80);
239
+ var title = '[Auto] Recurring failure: ' + titleSig;
240
+ var body = buildIssueBody(opts);
241
+
242
+ try {
243
+ var result = await createGithubIssue(config.repo, title, body, token);
244
+ console.log('[IssueReporter] Created GitHub issue #' + result.number + ': ' + result.url);
245
+
246
+ var state = readState();
247
+ var errorKey = computeErrorKey(signals);
248
+ var recentKeys = Array.isArray(state.recentIssueKeys) ? state.recentIssueKeys : [];
249
+ recentKeys.push(errorKey);
250
+ if (recentKeys.length > 20) recentKeys = recentKeys.slice(-20);
251
+ writeState({
252
+ lastReportedAt: new Date().toISOString(),
253
+ recentIssueKeys: recentKeys,
254
+ lastIssueUrl: result.url,
255
+ lastIssueNumber: result.number,
256
+ });
257
+ } catch (e) {
258
+ console.log('[IssueReporter] Failed to create issue (non-fatal): ' + (e && e.message ? e.message : String(e)));
259
+ }
260
+ }
261
+
262
+ module.exports = { maybeReportIssue, buildIssueBody, shouldReport };