@evomap/evolver 1.29.0

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 +479 -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 +68 -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,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 };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { getRepoRoot } = require('./paths');
8
+
9
+ const REVIEW_ENABLED_KEY = 'EVOLVER_LLM_REVIEW';
10
+ const REVIEW_TIMEOUT_MS = 30000;
11
+
12
+ function isLlmReviewEnabled() {
13
+ return String(process.env[REVIEW_ENABLED_KEY] || '').toLowerCase() === 'true';
14
+ }
15
+
16
+ function buildReviewPrompt({ diff, gene, signals, mutation }) {
17
+ const geneId = gene && gene.id ? gene.id : '(unknown)';
18
+ const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
19
+ const rationale = mutation && mutation.rationale ? String(mutation.rationale).slice(0, 500) : '(none)';
20
+ const signalsList = Array.isArray(signals) ? signals.slice(0, 8).join(', ') : '(none)';
21
+ const diffPreview = String(diff || '').slice(0, 6000);
22
+
23
+ return `You are reviewing a code change produced by an autonomous evolution engine.
24
+
25
+ ## Context
26
+ - Gene: ${geneId} (${category})
27
+ - Signals: [${signalsList}]
28
+ - Rationale: ${rationale}
29
+
30
+ ## Diff
31
+ \`\`\`diff
32
+ ${diffPreview}
33
+ \`\`\`
34
+
35
+ ## Review Criteria
36
+ 1. Does this change address the stated signals?
37
+ 2. Are there any obvious regressions or bugs introduced?
38
+ 3. Is the blast radius proportionate to the problem?
39
+ 4. Are there any security or safety concerns?
40
+
41
+ ## Response Format
42
+ Respond with a JSON object:
43
+ {
44
+ "approved": true|false,
45
+ "confidence": 0.0-1.0,
46
+ "concerns": ["..."],
47
+ "summary": "one-line review summary"
48
+ }`;
49
+ }
50
+
51
+ function runLlmReview({ diff, gene, signals, mutation }) {
52
+ if (!isLlmReviewEnabled()) return null;
53
+
54
+ const prompt = buildReviewPrompt({ diff, gene, signals, mutation });
55
+
56
+ try {
57
+ const repoRoot = getRepoRoot();
58
+
59
+ // Write prompt to a temp file to avoid shell quoting issues entirely.
60
+ const tmpFile = path.join(os.tmpdir(), 'evolver_review_prompt_' + process.pid + '.txt');
61
+ fs.writeFileSync(tmpFile, prompt, 'utf8');
62
+
63
+ try {
64
+ // Use execFileSync to bypass shell interpretation (no quoting issues).
65
+ const reviewScript = `
66
+ const fs = require('fs');
67
+ const prompt = fs.readFileSync(process.argv[1], 'utf8');
68
+ console.log(JSON.stringify({ approved: true, confidence: 0.7, concerns: [], summary: 'auto-approved (no external LLM configured)' }));
69
+ `;
70
+ const result = execFileSync(process.execPath, ['-e', reviewScript, tmpFile], {
71
+ cwd: repoRoot,
72
+ encoding: 'utf8',
73
+ timeout: REVIEW_TIMEOUT_MS,
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ windowsHide: true,
76
+ });
77
+
78
+ try {
79
+ return JSON.parse(result.trim());
80
+ } catch (_) {
81
+ return { approved: true, confidence: 0.5, concerns: ['failed to parse review response'], summary: 'review parse error' };
82
+ }
83
+ } finally {
84
+ try { fs.unlinkSync(tmpFile); } catch (_) {}
85
+ }
86
+ } catch (e) {
87
+ console.log('[LLMReview] Execution failed (non-fatal): ' + (e && e.message ? e.message : e));
88
+ return { approved: true, confidence: 0.5, concerns: ['review execution failed'], summary: 'review timeout or error' };
89
+ }
90
+ }
91
+
92
+ module.exports = { isLlmReviewEnabled, runLlmReview, buildReviewPrompt };