@evomap/gep-mcp-server 1.0.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 (3) hide show
  1. package/package.json +34 -0
  2. package/src/index.js +276 -0
  3. package/src/runtime.js +516 -0
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@evomap/gep-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP Server that exposes GEP (Genome Evolution Protocol) evolution capabilities to any MCP-compatible AI agent",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "gep-mcp-server": "./src/index.js"
9
+ },
10
+ "keywords": [
11
+ "gep",
12
+ "mcp",
13
+ "ai",
14
+ "agent",
15
+ "evolution",
16
+ "self-evolution",
17
+ "evomap"
18
+ ],
19
+ "files": [
20
+ "src"
21
+ ],
22
+ "author": "EvoMap",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/EvoMap/gep-mcp-server"
27
+ },
28
+ "engines": {
29
+ "node": ">=18.0.0"
30
+ },
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.26.0"
33
+ }
34
+ }
package/src/index.js ADDED
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ } from '@modelcontextprotocol/sdk/types.js';
11
+
12
+ import { resolve, dirname } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ import { GepRuntime } from './runtime.js';
18
+
19
+ const ASSETS_DIR = process.env.GEP_ASSETS_DIR || resolve(process.cwd(), 'assets/gep');
20
+ const MEMORY_DIR = process.env.GEP_MEMORY_DIR || resolve(process.cwd(), 'memory/evolution');
21
+ const HUB_URL = process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
22
+
23
+ const runtime = new GepRuntime({ assetsDir: ASSETS_DIR, memoryDir: MEMORY_DIR });
24
+
25
+ const server = new Server(
26
+ { name: 'gep-mcp-server', version: '1.0.0' },
27
+ { capabilities: { tools: {}, resources: {} } }
28
+ );
29
+
30
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
31
+ tools: [
32
+ {
33
+ name: 'gep_evolve',
34
+ description: 'Trigger a GEP evolution cycle. The agent detects signals from the provided context, selects the best gene (evolution strategy), and returns the evolution plan. Use this when you encounter a problem you cannot solve or want to learn a new capability.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ context: {
39
+ type: 'string',
40
+ description: 'The current execution context: error messages, logs, user requests, or any text that describes what needs to evolve',
41
+ },
42
+ intent: {
43
+ type: 'string',
44
+ enum: ['repair', 'optimize', 'innovate'],
45
+ description: 'Optional: force a specific evolution intent. If omitted, the system infers from signals.',
46
+ },
47
+ },
48
+ required: ['context'],
49
+ },
50
+ },
51
+ {
52
+ name: 'gep_recall',
53
+ description: 'Query the evolution memory graph for relevant past experience. Returns historical signal-gene-outcome mappings that match the query. Use this to check if you have dealt with a similar situation before.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ query: {
58
+ type: 'string',
59
+ description: 'Description of what you want to recall from evolution history',
60
+ },
61
+ signals: {
62
+ type: 'array',
63
+ items: { type: 'string' },
64
+ description: 'Optional: specific signal patterns to search for',
65
+ },
66
+ },
67
+ required: ['query'],
68
+ },
69
+ },
70
+ {
71
+ name: 'gep_record_outcome',
72
+ description: 'Record the outcome of an evolution attempt. Call this after applying an evolution plan to provide feedback to the memory graph.',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ geneId: {
77
+ type: 'string',
78
+ description: 'The gene ID that was used',
79
+ },
80
+ signals: {
81
+ type: 'array',
82
+ items: { type: 'string' },
83
+ description: 'The signals that triggered the evolution',
84
+ },
85
+ status: {
86
+ type: 'string',
87
+ enum: ['success', 'failed'],
88
+ description: 'Whether the evolution was successful',
89
+ },
90
+ score: {
91
+ type: 'number',
92
+ description: 'Quality score from 0.0 to 1.0',
93
+ },
94
+ summary: {
95
+ type: 'string',
96
+ description: 'Brief description of what happened',
97
+ },
98
+ },
99
+ required: ['geneId', 'signals', 'status', 'score'],
100
+ },
101
+ },
102
+ {
103
+ name: 'gep_list_genes',
104
+ description: 'List all available evolution genes (strategies). Each gene responds to specific signals and contains actionable steps.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: {
108
+ category: {
109
+ type: 'string',
110
+ enum: ['repair', 'optimize', 'innovate'],
111
+ description: 'Optional: filter by category',
112
+ },
113
+ },
114
+ },
115
+ },
116
+ {
117
+ name: 'gep_install_gene',
118
+ description: 'Install a new gene (evolution strategy) into the local gene pool.',
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ gene: {
123
+ type: 'object',
124
+ description: 'The Gene object to install (must conform to GEP Gene schema)',
125
+ },
126
+ },
127
+ required: ['gene'],
128
+ },
129
+ },
130
+ {
131
+ name: 'gep_export',
132
+ description: 'Export the complete evolution history as a portable .gepx archive. This is your sovereign evolution data.',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ outputPath: {
137
+ type: 'string',
138
+ description: 'File path for the .gepx archive',
139
+ },
140
+ agentName: {
141
+ type: 'string',
142
+ description: 'Name of the agent whose evolution is being exported',
143
+ },
144
+ },
145
+ required: ['outputPath'],
146
+ },
147
+ },
148
+ {
149
+ name: 'gep_status',
150
+ description: 'Get the current evolution status: gene count, capsule count, recent events, memory graph size.',
151
+ inputSchema: { type: 'object', properties: {} },
152
+ },
153
+ {
154
+ name: 'gep_search_community',
155
+ description: 'Search the EvoMap community for evolution strategies and capsules published by other agents. Use natural language to find relevant past experiences across the entire ecosystem.',
156
+ inputSchema: {
157
+ type: 'object',
158
+ properties: {
159
+ query: {
160
+ type: 'string',
161
+ description: 'Natural language search query (e.g. "how to fix retry timeout issues")',
162
+ },
163
+ type: {
164
+ type: 'string',
165
+ enum: ['Gene', 'Capsule'],
166
+ description: 'Optional: filter by asset type',
167
+ },
168
+ outcome: {
169
+ type: 'string',
170
+ enum: ['success', 'failed'],
171
+ description: 'Optional: filter by outcome status',
172
+ },
173
+ limit: {
174
+ type: 'number',
175
+ description: 'Max results (default 10)',
176
+ },
177
+ },
178
+ required: ['query'],
179
+ },
180
+ },
181
+ ],
182
+ }));
183
+
184
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
185
+ const { name, arguments: args } = request.params;
186
+
187
+ try {
188
+ switch (name) {
189
+ case 'gep_evolve':
190
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.evolve(args), null, 2) }] };
191
+ case 'gep_recall':
192
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.recall(args), null, 2) }] };
193
+ case 'gep_record_outcome':
194
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.recordOutcome(args), null, 2) }] };
195
+ case 'gep_list_genes':
196
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.listGenes(args), null, 2) }] };
197
+ case 'gep_install_gene':
198
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.installGene(args), null, 2) }] };
199
+ case 'gep_export':
200
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.exportEvolution(args), null, 2) }] };
201
+ case 'gep_status':
202
+ return { content: [{ type: 'text', text: JSON.stringify(runtime.getStatus(), null, 2) }] };
203
+ case 'gep_search_community': {
204
+ if (!args.query || typeof args.query !== 'string' || args.query.trim().length < 2) {
205
+ return { content: [{ type: 'text', text: JSON.stringify({ error: 'query must be a string with at least 2 characters' }) }], isError: true };
206
+ }
207
+ const params = new URLSearchParams();
208
+ params.set('q', args.query.trim().slice(0, 500));
209
+ if (args.type && ['Gene', 'Capsule'].includes(args.type)) params.set('type', args.type);
210
+ if (args.outcome && ['success', 'failed'].includes(args.outcome)) params.set('outcome', args.outcome);
211
+ params.set('limit', String(args.limit || 10));
212
+ params.set('include_context', 'true');
213
+ const url = `${HUB_URL}/a2a/assets/semantic-search?${params.toString()}`;
214
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
215
+ if (!res.ok) throw new Error(`Hub returned ${res.status}`);
216
+ const data = await res.json();
217
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
218
+ }
219
+ default:
220
+ return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
221
+ }
222
+ } catch (err) {
223
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
224
+ }
225
+ });
226
+
227
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
228
+ resources: [
229
+ {
230
+ uri: 'gep://spec',
231
+ name: 'GEP Protocol Specification',
232
+ description: 'The complete Genome Evolution Protocol specification',
233
+ mimeType: 'text/markdown',
234
+ },
235
+ {
236
+ uri: 'gep://genes',
237
+ name: 'Gene Pool',
238
+ description: 'All currently installed evolution genes',
239
+ mimeType: 'application/json',
240
+ },
241
+ {
242
+ uri: 'gep://capsules',
243
+ name: 'Evolution Capsules',
244
+ description: 'Records of past successful evolutions',
245
+ mimeType: 'application/json',
246
+ },
247
+ ],
248
+ }));
249
+
250
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
251
+ const { uri } = request.params;
252
+ switch (uri) {
253
+ case 'gep://spec': {
254
+ const specPath = resolve(__dirname, '../../gep-protocol/spec/gep-spec-v1.md');
255
+ const content = existsSync(specPath) ? readFileSync(specPath, 'utf8') : 'GEP spec not found at ' + specPath;
256
+ return { contents: [{ uri, mimeType: 'text/markdown', text: content }] };
257
+ }
258
+ case 'gep://genes':
259
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(runtime.store.loadGenes(), null, 2) }] };
260
+ case 'gep://capsules':
261
+ return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(runtime.store.loadCapsules(), null, 2) }] };
262
+ default:
263
+ throw new Error(`Unknown resource: ${uri}`);
264
+ }
265
+ });
266
+
267
+ async function main() {
268
+ const transport = new StdioServerTransport();
269
+ await server.connect(transport);
270
+ console.error('GEP MCP Server running on stdio');
271
+ }
272
+
273
+ main().catch(err => {
274
+ console.error('Fatal:', err);
275
+ process.exit(1);
276
+ });
package/src/runtime.js ADDED
@@ -0,0 +1,516 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { resolve, join } from 'node:path';
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync, rmSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+
6
+ const SCHEMA_VERSION = '1.5.0';
7
+
8
+ export class GepRuntime {
9
+ constructor({ assetsDir, memoryDir }) {
10
+ this.assetsDir = assetsDir;
11
+ this.memoryDir = memoryDir;
12
+ this.store = new SimpleStore(assetsDir);
13
+ this.memoryGraphPath = join(memoryDir, 'memory_graph.jsonl');
14
+ mkdirSync(assetsDir, { recursive: true });
15
+ mkdirSync(memoryDir, { recursive: true });
16
+ this.store.ensureFiles();
17
+ }
18
+
19
+ evolve({ context, intent }) {
20
+ const signals = this._extractSignals(context);
21
+
22
+ if (intent) {
23
+ const intentSignalMap = {
24
+ repair: 'log_error',
25
+ innovate: 'stable_success_plateau',
26
+ optimize: 'user_improvement_suggestion',
27
+ };
28
+ if (intentSignalMap[intent] && !signals.includes(intentSignalMap[intent])) {
29
+ signals.unshift(intentSignalMap[intent]);
30
+ }
31
+ }
32
+
33
+ const genes = this.store.loadGenes();
34
+ const capsules = this.store.loadCapsules();
35
+ const memoryAdvice = this._getMemoryAdvice(signals, genes);
36
+ const { selected, alternatives } = this._selectGene(genes, signals, memoryAdvice);
37
+
38
+ if (!selected) {
39
+ return {
40
+ ok: false,
41
+ signals,
42
+ message: 'No matching gene found for these signals. A new gene may need to be created.',
43
+ suggestion: 'Use gep_install_gene to add a gene that matches these signal patterns.',
44
+ };
45
+ }
46
+
47
+ const category = this._inferCategory(signals, intent);
48
+ const mutation = {
49
+ type: 'Mutation',
50
+ id: `mut_${Date.now()}`,
51
+ category,
52
+ trigger_signals: signals,
53
+ target: `gene:${selected.id}`,
54
+ expected_effect: this._effectFromCategory(category),
55
+ risk_level: category === 'innovate' ? 'medium' : 'low',
56
+ };
57
+
58
+ this._recordToGraph({
59
+ kind: 'attempt',
60
+ signals,
61
+ gene: { id: selected.id, category: selected.category },
62
+ mutation: { id: mutation.id, category: mutation.category, risk_level: mutation.risk_level },
63
+ });
64
+
65
+ const matchingCapsule = this._selectCapsule(capsules, signals);
66
+
67
+ return {
68
+ ok: true,
69
+ signals,
70
+ selected_gene: {
71
+ id: selected.id,
72
+ category: selected.category,
73
+ strategy: selected.strategy,
74
+ constraints: selected.constraints,
75
+ validation: selected.validation,
76
+ },
77
+ mutation,
78
+ alternatives: alternatives.map(g => ({ id: g.id, category: g.category })),
79
+ matching_capsule: matchingCapsule ? {
80
+ id: matchingCapsule.id,
81
+ gene: matchingCapsule.gene,
82
+ summary: matchingCapsule.summary,
83
+ confidence: matchingCapsule.confidence,
84
+ } : null,
85
+ memory_advice: {
86
+ preferred: memoryAdvice.preferredGeneId,
87
+ banned_count: memoryAdvice.bannedGeneIds.size,
88
+ },
89
+ instructions: [
90
+ 'Follow the gene strategy steps in order.',
91
+ `Constraint: modify at most ${selected.constraints?.max_files || 12} files.`,
92
+ `Forbidden paths: ${(selected.constraints?.forbidden_paths || []).join(', ')}`,
93
+ 'After applying changes, run validation commands to verify correctness.',
94
+ 'Then call gep_record_outcome with the result.',
95
+ ],
96
+ };
97
+ }
98
+
99
+ recall({ query, signals }) {
100
+ const events = this._readGraphEvents(500);
101
+ const querySignals = signals || this._extractSignals(query);
102
+ const queryKey = this._computeSignalKey(querySignals);
103
+
104
+ const outcomes = events.filter(e => e.kind === 'outcome');
105
+ const relevant = [];
106
+
107
+ for (const ev of outcomes) {
108
+ const evKey = ev.signal?.key || '';
109
+ const evSignals = ev.signal?.signals || [];
110
+ const sim = this._jaccard(querySignals, evSignals);
111
+ if (sim >= 0.2 || evKey === queryKey) {
112
+ relevant.push({
113
+ signal_key: evKey,
114
+ gene_id: ev.gene?.id,
115
+ gene_category: ev.gene?.category,
116
+ outcome: ev.outcome,
117
+ similarity: Math.round(sim * 100) / 100,
118
+ timestamp: ev.ts,
119
+ });
120
+ }
121
+ }
122
+
123
+ relevant.sort((a, b) => b.similarity - a.similarity);
124
+
125
+ return {
126
+ query,
127
+ signals_extracted: querySignals,
128
+ matches: relevant.slice(0, 10),
129
+ total_memory_events: events.length,
130
+ };
131
+ }
132
+
133
+ recordOutcome({ geneId, signals, status, score, summary }) {
134
+ const signalKey = this._computeSignalKey(signals);
135
+ const ev = {
136
+ type: 'MemoryGraphEvent',
137
+ kind: 'outcome',
138
+ id: `mge_${Date.now()}_${this._stableHash(`${signalKey}|${geneId}|outcome`)}`,
139
+ ts: new Date().toISOString(),
140
+ signal: { key: signalKey, signals },
141
+ gene: { id: geneId },
142
+ outcome: { status, score: clamp01(score), note: summary || null },
143
+ };
144
+ this._appendToGraph(ev);
145
+
146
+ if (status === 'success' && score >= 0.5) {
147
+ const capsule = {
148
+ type: 'Capsule',
149
+ schema_version: SCHEMA_VERSION,
150
+ id: `capsule_${Date.now()}`,
151
+ trigger: signals,
152
+ gene: geneId,
153
+ summary: summary || `Evolution with ${geneId}: ${status}`,
154
+ confidence: clamp01(score),
155
+ blast_radius: { files: 0, lines: 0 },
156
+ outcome: { status, score: clamp01(score) },
157
+ success_streak: 1,
158
+ };
159
+ capsule.asset_id = this._computeAssetId(capsule);
160
+ this.store.upsertCapsule(capsule);
161
+ }
162
+
163
+ return { ok: true, recorded: ev.id };
164
+ }
165
+
166
+ listGenes({ category } = {}) {
167
+ let genes = this.store.loadGenes();
168
+ if (category) genes = genes.filter(g => g.category === category);
169
+ return {
170
+ total: genes.length,
171
+ genes: genes.map(g => ({
172
+ id: g.id,
173
+ category: g.category,
174
+ signals_match: g.signals_match,
175
+ strategy_steps: g.strategy?.length || 0,
176
+ constraints: g.constraints,
177
+ })),
178
+ };
179
+ }
180
+
181
+ installGene({ gene }) {
182
+ if (!gene || gene.type !== 'Gene' || !gene.id) {
183
+ return { ok: false, error: 'Invalid gene: must have type="Gene" and a non-empty id' };
184
+ }
185
+ if (!gene.schema_version) gene.schema_version = SCHEMA_VERSION;
186
+ if (!gene.asset_id) gene.asset_id = this._computeAssetId(gene);
187
+ this.store.upsertGene(gene);
188
+ return { ok: true, installed: gene.id };
189
+ }
190
+
191
+ exportEvolution({ outputPath, agentName }) {
192
+ const tmpDir = `${outputPath}.tmp`;
193
+ mkdirSync(join(tmpDir, 'genes'), { recursive: true });
194
+ mkdirSync(join(tmpDir, 'capsules'), { recursive: true });
195
+ mkdirSync(join(tmpDir, 'events'), { recursive: true });
196
+ mkdirSync(join(tmpDir, 'memory'), { recursive: true });
197
+
198
+ const copies = [
199
+ [join(this.assetsDir, 'genes.json'), join(tmpDir, 'genes', 'genes.json')],
200
+ [join(this.assetsDir, 'capsules.json'), join(tmpDir, 'capsules', 'capsules.json')],
201
+ [join(this.assetsDir, 'events.jsonl'), join(tmpDir, 'events', 'events.jsonl')],
202
+ [this.memoryGraphPath, join(tmpDir, 'memory', 'memory_graph.jsonl')],
203
+ ];
204
+
205
+ for (const [src, dest] of copies) {
206
+ if (existsSync(src)) writeFileSync(dest, readFileSync(src));
207
+ }
208
+
209
+ const manifest = {
210
+ gep_version: '1.0.0',
211
+ schema_version: SCHEMA_VERSION,
212
+ created_at: new Date().toISOString(),
213
+ agent_name: agentName || 'unknown',
214
+ statistics: this.getStatus().statistics,
215
+ source: { platform: 'gep-mcp-server', version: '1.0.0' },
216
+ };
217
+ writeFileSync(join(tmpDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
218
+
219
+ execFileSync('tar', ['-czf', outputPath, '-C', tmpDir, '.'], { timeout: 30000 });
220
+ rmSync(tmpDir, { recursive: true, force: true });
221
+
222
+ return { ok: true, outputPath, manifest };
223
+ }
224
+
225
+ getStatus() {
226
+ const genes = this.store.loadGenes();
227
+ const capsules = this.store.loadCapsules();
228
+ const events = this.store.readAllEvents();
229
+ const graphEvents = this._readGraphEvents(100);
230
+
231
+ const recentEvents = events.slice(-5).map(e => ({
232
+ id: e.id,
233
+ intent: e.intent,
234
+ outcome: e.outcome?.status,
235
+ score: e.outcome?.score,
236
+ }));
237
+
238
+ const successCount = events.filter(e => e.outcome?.status === 'success').length;
239
+
240
+ return {
241
+ schema_version: SCHEMA_VERSION,
242
+ statistics: {
243
+ total_genes: genes.length,
244
+ total_capsules: capsules.length,
245
+ total_events: events.length,
246
+ memory_graph_entries: graphEvents.length,
247
+ success_rate: events.length > 0 ? Math.round((successCount / events.length) * 100) / 100 : 0,
248
+ },
249
+ recent_events: recentEvents,
250
+ gene_categories: {
251
+ repair: genes.filter(g => g.category === 'repair').length,
252
+ optimize: genes.filter(g => g.category === 'optimize').length,
253
+ innovate: genes.filter(g => g.category === 'innovate').length,
254
+ },
255
+ };
256
+ }
257
+
258
+ _extractSignals(context) {
259
+ const signals = [];
260
+ const text = String(context || '');
261
+ const lower = text.toLowerCase();
262
+
263
+ if (/\[error\]|error:|exception:|"status":\s*"error"/.test(lower)) signals.push('log_error');
264
+ if (/\b(add|implement|create|build)\b[^.]{3,60}\b(feature|function|module|capability)\b/i.test(text)) {
265
+ signals.push('user_feature_request');
266
+ }
267
+ if (/\b(i want|i need|we need|please add)\b/i.test(lower)) signals.push('user_feature_request');
268
+ if (/\b(improve|enhance|upgrade|refactor|optimize)\b/i.test(lower) && !signals.includes('log_error')) {
269
+ signals.push('user_improvement_suggestion');
270
+ }
271
+ if (/\b(slow|timeout|latency|bottleneck|performance)\b/i.test(lower)) signals.push('perf_bottleneck');
272
+ if (/\b(not supported|cannot|unsupported|missing feature)\b/i.test(lower)) signals.push('capability_gap');
273
+ if (signals.length === 0) signals.push('stable_success_plateau');
274
+ return [...new Set(signals)];
275
+ }
276
+
277
+ _selectGene(genes, signals, advice) {
278
+ const bannedIds = advice?.bannedGeneIds || new Set();
279
+ const scored = genes
280
+ .map(g => {
281
+ const pats = Array.isArray(g.signals_match) ? g.signals_match : [];
282
+ let score = 0;
283
+ for (const p of pats) {
284
+ const needle = String(p).toLowerCase();
285
+ if (signals.some(s => s.toLowerCase().includes(needle))) score++;
286
+ }
287
+ return { gene: g, score };
288
+ })
289
+ .filter(x => x.score > 0 && !bannedIds.has(x.gene.id))
290
+ .sort((a, b) => b.score - a.score);
291
+
292
+ if (advice?.preferredGeneId) {
293
+ const preferred = scored.find(x => x.gene.id === advice.preferredGeneId);
294
+ if (preferred) {
295
+ const rest = scored.filter(x => x.gene.id !== advice.preferredGeneId);
296
+ return { selected: preferred.gene, alternatives: rest.slice(0, 4).map(x => x.gene) };
297
+ }
298
+ }
299
+
300
+ return {
301
+ selected: scored.length > 0 ? scored[0].gene : null,
302
+ alternatives: scored.slice(1, 5).map(x => x.gene),
303
+ };
304
+ }
305
+
306
+ _selectCapsule(capsules, signals) {
307
+ const scored = (capsules || [])
308
+ .map(c => {
309
+ const triggers = Array.isArray(c.trigger) ? c.trigger : [];
310
+ const score = triggers.reduce((acc, t) => {
311
+ return signals.some(s => s.toLowerCase().includes(String(t).toLowerCase())) ? acc + 1 : acc;
312
+ }, 0);
313
+ return { capsule: c, score };
314
+ })
315
+ .filter(x => x.score > 0)
316
+ .sort((a, b) => b.score - a.score);
317
+ return scored.length > 0 ? scored[0].capsule : null;
318
+ }
319
+
320
+ _getMemoryAdvice(signals, genes) {
321
+ const events = this._readGraphEvents(1000);
322
+ const edges = new Map();
323
+ for (const ev of events) {
324
+ if (ev.kind !== 'outcome') continue;
325
+ const k = `${ev.signal?.key || ''}::${ev.gene?.id || ''}`;
326
+ const cur = edges.get(k) || { success: 0, fail: 0 };
327
+ if (ev.outcome?.status === 'success') cur.success++;
328
+ else if (ev.outcome?.status === 'failed') cur.fail++;
329
+ edges.set(k, cur);
330
+ }
331
+
332
+ const curKey = this._computeSignalKey(signals);
333
+ const bannedGeneIds = new Set();
334
+ let bestGeneId = null;
335
+ let bestScore = -1;
336
+
337
+ for (const g of genes) {
338
+ if (!g?.id) continue;
339
+ const k = `${curKey}::${g.id}`;
340
+ const edge = edges.get(k);
341
+ if (!edge) continue;
342
+ const total = edge.success + edge.fail;
343
+ const p = (edge.success + 1) / (total + 2);
344
+ if (total >= 2 && p < 0.35) bannedGeneIds.add(g.id);
345
+ if (p > bestScore) { bestScore = p; bestGeneId = g.id; }
346
+ }
347
+
348
+ return { preferredGeneId: bestGeneId, bannedGeneIds };
349
+ }
350
+
351
+ _inferCategory(signals, forceIntent) {
352
+ if (forceIntent && ['repair', 'optimize', 'innovate'].includes(forceIntent)) return forceIntent;
353
+ if (signals.some(s => s === 'log_error' || s.startsWith('errsig:'))) return 'repair';
354
+ const oppSignals = ['user_feature_request', 'capability_gap', 'stable_success_plateau', 'force_innovation_after_repair_loop'];
355
+ if (signals.some(s => oppSignals.includes(s))) return 'innovate';
356
+ return 'optimize';
357
+ }
358
+
359
+ _effectFromCategory(cat) {
360
+ if (cat === 'repair') return 'reduce runtime errors, increase stability';
361
+ if (cat === 'innovate') return 'explore new strategy combinations';
362
+ return 'improve success rate and efficiency';
363
+ }
364
+
365
+ _readGraphEvents(limit = 1000) {
366
+ try {
367
+ if (!existsSync(this.memoryGraphPath)) return [];
368
+ const raw = readFileSync(this.memoryGraphPath, 'utf8');
369
+ const lines = raw.split('\n').filter(l => l.trim());
370
+ return lines.slice(Math.max(0, lines.length - limit)).map(l => {
371
+ try { return JSON.parse(l); } catch { return null; }
372
+ }).filter(Boolean);
373
+ } catch { return []; }
374
+ }
375
+
376
+ _appendToGraph(event) {
377
+ mkdirSync(this.memoryDir, { recursive: true });
378
+ appendFileSync(this.memoryGraphPath, JSON.stringify(event) + '\n', 'utf8');
379
+ }
380
+
381
+ _recordToGraph({ kind, signals, gene, mutation }) {
382
+ const ev = {
383
+ type: 'MemoryGraphEvent',
384
+ kind,
385
+ id: `mge_${Date.now()}_${this._stableHash(JSON.stringify({ kind, signals, gene }))}`,
386
+ ts: new Date().toISOString(),
387
+ signal: { key: this._computeSignalKey(signals), signals },
388
+ gene: gene || null,
389
+ mutation: mutation || null,
390
+ };
391
+ this._appendToGraph(ev);
392
+ }
393
+
394
+ _computeSignalKey(signals) {
395
+ return [...new Set((signals || []).map(String).filter(Boolean))].sort().join('|') || '(none)';
396
+ }
397
+
398
+ _stableHash(input) {
399
+ const s = String(input || '');
400
+ let h = 2166136261;
401
+ for (let i = 0; i < s.length; i++) {
402
+ h ^= s.charCodeAt(i);
403
+ h = Math.imul(h, 16777619);
404
+ }
405
+ return (h >>> 0).toString(16).padStart(8, '0');
406
+ }
407
+
408
+ _jaccard(a, b) {
409
+ const setA = new Set(a.map(String));
410
+ const setB = new Set(b.map(String));
411
+ if (setA.size === 0 && setB.size === 0) return 1;
412
+ if (setA.size === 0 || setB.size === 0) return 0;
413
+ let inter = 0;
414
+ for (const x of setA) if (setB.has(x)) inter++;
415
+ return inter / (setA.size + setB.size - inter);
416
+ }
417
+
418
+ _computeAssetId(obj) {
419
+ const clean = {};
420
+ for (const k of Object.keys(obj)) {
421
+ if (k === 'asset_id') continue;
422
+ clean[k] = obj[k];
423
+ }
424
+ const canonical = this._canonicalize(clean);
425
+ return 'sha256:' + createHash('sha256').update(canonical, 'utf8').digest('hex');
426
+ }
427
+
428
+ _canonicalize(obj) {
429
+ if (obj === null || obj === undefined) return 'null';
430
+ if (typeof obj === 'boolean') return obj ? 'true' : 'false';
431
+ if (typeof obj === 'number') return Number.isFinite(obj) ? String(obj) : 'null';
432
+ if (typeof obj === 'string') return JSON.stringify(obj);
433
+ if (Array.isArray(obj)) return '[' + obj.map(x => this._canonicalize(x)).join(',') + ']';
434
+ if (typeof obj === 'object') {
435
+ const keys = Object.keys(obj).sort();
436
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + this._canonicalize(obj[k])).join(',') + '}';
437
+ }
438
+ return 'null';
439
+ }
440
+ }
441
+
442
+ class SimpleStore {
443
+ constructor(dir) {
444
+ this.dir = dir;
445
+ mkdirSync(dir, { recursive: true });
446
+ }
447
+
448
+ loadGenes() {
449
+ const data = readJsonSafe(join(this.dir, 'genes.json'), { genes: [] });
450
+ return Array.isArray(data.genes) ? data.genes : [];
451
+ }
452
+
453
+ loadCapsules() {
454
+ const data = readJsonSafe(join(this.dir, 'capsules.json'), { capsules: [] });
455
+ return Array.isArray(data.capsules) ? data.capsules : [];
456
+ }
457
+
458
+ readAllEvents() {
459
+ return readJsonl(join(this.dir, 'events.jsonl'));
460
+ }
461
+
462
+ upsertGene(gene) {
463
+ const data = readJsonSafe(join(this.dir, 'genes.json'), { version: 1, genes: [] });
464
+ const genes = Array.isArray(data.genes) ? data.genes : [];
465
+ const idx = genes.findIndex(g => g?.id === gene.id);
466
+ if (idx >= 0) genes[idx] = gene; else genes.push(gene);
467
+ writeJsonAtomic(join(this.dir, 'genes.json'), { version: data.version || 1, genes });
468
+ }
469
+
470
+ upsertCapsule(capsule) {
471
+ const data = readJsonSafe(join(this.dir, 'capsules.json'), { version: 1, capsules: [] });
472
+ const capsules = Array.isArray(data.capsules) ? data.capsules : [];
473
+ const idx = capsules.findIndex(c => c?.id === capsule.id);
474
+ if (idx >= 0) capsules[idx] = capsule; else capsules.push(capsule);
475
+ writeJsonAtomic(join(this.dir, 'capsules.json'), { version: data.version || 1, capsules });
476
+ }
477
+
478
+ ensureFiles() {
479
+ const files = [
480
+ [join(this.dir, 'genes.json'), JSON.stringify({ version: 1, genes: [] }, null, 2)],
481
+ [join(this.dir, 'capsules.json'), JSON.stringify({ version: 1, capsules: [] }, null, 2)],
482
+ [join(this.dir, 'events.jsonl'), ''],
483
+ ];
484
+ for (const [path, content] of files) {
485
+ if (!existsSync(path)) writeFileSync(path, content + '\n', 'utf8');
486
+ }
487
+ }
488
+ }
489
+
490
+ function readJsonSafe(filePath, fallback) {
491
+ try {
492
+ if (!existsSync(filePath)) return fallback;
493
+ const raw = readFileSync(filePath, 'utf8');
494
+ return raw.trim() ? JSON.parse(raw) : fallback;
495
+ } catch { return fallback; }
496
+ }
497
+
498
+ function readJsonl(filePath) {
499
+ try {
500
+ if (!existsSync(filePath)) return [];
501
+ return readFileSync(filePath, 'utf8').split('\n').filter(l => l.trim()).map(l => {
502
+ try { return JSON.parse(l); } catch { return null; }
503
+ }).filter(Boolean);
504
+ } catch { return []; }
505
+ }
506
+
507
+ function writeJsonAtomic(filePath, obj) {
508
+ const tmp = `${filePath}.tmp`;
509
+ writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
510
+ renameSync(tmp, filePath);
511
+ }
512
+
513
+ function clamp01(x) {
514
+ const n = Number(x);
515
+ return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 0;
516
+ }