@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,203 @@
1
+ // ---------------------------------------------------------------------------
2
+ // MemoryGraphAdapter -- stable interface boundary for memory graph operations.
3
+ //
4
+ // Default implementation delegates to the local JSONL-based memoryGraph.js.
5
+ // SaaS providers can supply a remote adapter by setting MEMORY_GRAPH_PROVIDER=remote
6
+ // and configuring MEMORY_GRAPH_REMOTE_URL / MEMORY_GRAPH_REMOTE_KEY.
7
+ //
8
+ // The adapter is designed so that the open-source evolver always works offline
9
+ // with the local implementation. Remote is optional and degrades gracefully.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const localGraph = require('./memoryGraph');
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Adapter interface contract (all methods must be implemented by providers):
16
+ //
17
+ // getAdvice({ signals, genes, driftEnabled }) => { preferredGeneId, bannedGeneIds, currentSignalKey, explanation }
18
+ // recordSignalSnapshot({ signals, observations }) => event
19
+ // recordHypothesis({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, capsulesUsed, observations }) => { hypothesisId, signalKey }
20
+ // recordAttempt({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, hypothesisId, capsulesUsed, observations }) => { actionId, signalKey }
21
+ // recordOutcome({ signals, observations }) => event | null
22
+ // recordExternalCandidate({ asset, source, signals }) => event | null
23
+ // memoryGraphPath() => string
24
+ // computeSignalKey(signals) => string
25
+ // tryReadMemoryGraphEvents(limit) => event[]
26
+ // ---------------------------------------------------------------------------
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Local adapter (default) -- wraps memoryGraph.js without any behavior change
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const localAdapter = {
33
+ name: 'local',
34
+
35
+ getAdvice(opts) {
36
+ return localGraph.getMemoryAdvice(opts);
37
+ },
38
+
39
+ recordSignalSnapshot(opts) {
40
+ return localGraph.recordSignalSnapshot(opts);
41
+ },
42
+
43
+ recordHypothesis(opts) {
44
+ return localGraph.recordHypothesis(opts);
45
+ },
46
+
47
+ recordAttempt(opts) {
48
+ return localGraph.recordAttempt(opts);
49
+ },
50
+
51
+ recordOutcome(opts) {
52
+ return localGraph.recordOutcomeFromState(opts);
53
+ },
54
+
55
+ recordExternalCandidate(opts) {
56
+ return localGraph.recordExternalCandidate(opts);
57
+ },
58
+
59
+ memoryGraphPath() {
60
+ return localGraph.memoryGraphPath();
61
+ },
62
+
63
+ computeSignalKey(signals) {
64
+ return localGraph.computeSignalKey(signals);
65
+ },
66
+
67
+ tryReadMemoryGraphEvents(limit) {
68
+ return localGraph.tryReadMemoryGraphEvents(limit);
69
+ },
70
+ };
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Remote adapter (SaaS) -- calls external KG service with local fallback
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function buildRemoteAdapter() {
77
+ const remoteUrl = process.env.MEMORY_GRAPH_REMOTE_URL || '';
78
+ const remoteKey = process.env.MEMORY_GRAPH_REMOTE_KEY || '';
79
+ const timeoutMs = Number(process.env.MEMORY_GRAPH_REMOTE_TIMEOUT_MS) || 5000;
80
+
81
+ async function remoteCall(endpoint, body) {
82
+ if (!remoteUrl) throw new Error('MEMORY_GRAPH_REMOTE_URL not configured');
83
+ const url = `${remoteUrl.replace(/\/+$/, '')}${endpoint}`;
84
+ const controller = new AbortController();
85
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
86
+ try {
87
+ const res = await fetch(url, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ ...(remoteKey ? { Authorization: `Bearer ${remoteKey}` } : {}),
92
+ },
93
+ body: JSON.stringify(body),
94
+ signal: controller.signal,
95
+ });
96
+ if (!res.ok) {
97
+ throw new Error(`remote_kg_error: ${res.status}`);
98
+ }
99
+ return await res.json();
100
+ } finally {
101
+ clearTimeout(timer);
102
+ }
103
+ }
104
+
105
+ // Wrap remote call with local fallback -- ensures offline resilience.
106
+ function withFallback(localFn, remoteFn) {
107
+ return async function (...args) {
108
+ try {
109
+ return await remoteFn(...args);
110
+ } catch (e) {
111
+ // Fallback to local on any remote failure (network, timeout, config).
112
+ return localFn(...args);
113
+ }
114
+ };
115
+ }
116
+
117
+ return {
118
+ name: 'remote',
119
+
120
+ // getAdvice is the primary candidate for remote enhancement (richer graph reasoning).
121
+ getAdvice: withFallback(
122
+ (opts) => localGraph.getMemoryAdvice(opts),
123
+ async (opts) => {
124
+ const result = await remoteCall('/kg/advice', {
125
+ signals: opts.signals,
126
+ genes: (opts.genes || []).map((g) => ({ id: g.id, category: g.category, type: g.type })),
127
+ driftEnabled: opts.driftEnabled,
128
+ });
129
+ // Normalize remote response to match local contract.
130
+ return {
131
+ currentSignalKey: result.currentSignalKey || localGraph.computeSignalKey(opts.signals),
132
+ preferredGeneId: result.preferredGeneId || null,
133
+ bannedGeneIds: new Set(result.bannedGeneIds || []),
134
+ explanation: Array.isArray(result.explanation) ? result.explanation : [],
135
+ };
136
+ }
137
+ ),
138
+
139
+ // Write operations: always write locally first, then async-sync to remote.
140
+ // This preserves the append-only local graph as source of truth.
141
+ recordSignalSnapshot(opts) {
142
+ const ev = localGraph.recordSignalSnapshot(opts);
143
+ remoteCall('/kg/ingest', { kind: 'signal', event: ev }).catch(() => {});
144
+ return ev;
145
+ },
146
+
147
+ recordHypothesis(opts) {
148
+ const result = localGraph.recordHypothesis(opts);
149
+ remoteCall('/kg/ingest', { kind: 'hypothesis', event: result }).catch(() => {});
150
+ return result;
151
+ },
152
+
153
+ recordAttempt(opts) {
154
+ const result = localGraph.recordAttempt(opts);
155
+ remoteCall('/kg/ingest', { kind: 'attempt', event: result }).catch(() => {});
156
+ return result;
157
+ },
158
+
159
+ recordOutcome(opts) {
160
+ const ev = localGraph.recordOutcomeFromState(opts);
161
+ if (ev) {
162
+ remoteCall('/kg/ingest', { kind: 'outcome', event: ev }).catch(() => {});
163
+ }
164
+ return ev;
165
+ },
166
+
167
+ recordExternalCandidate(opts) {
168
+ const ev = localGraph.recordExternalCandidate(opts);
169
+ if (ev) {
170
+ remoteCall('/kg/ingest', { kind: 'external_candidate', event: ev }).catch(() => {});
171
+ }
172
+ return ev;
173
+ },
174
+
175
+ memoryGraphPath() {
176
+ return localGraph.memoryGraphPath();
177
+ },
178
+
179
+ computeSignalKey(signals) {
180
+ return localGraph.computeSignalKey(signals);
181
+ },
182
+
183
+ tryReadMemoryGraphEvents(limit) {
184
+ return localGraph.tryReadMemoryGraphEvents(limit);
185
+ },
186
+ };
187
+ }
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Provider resolution
191
+ // ---------------------------------------------------------------------------
192
+
193
+ function resolveAdapter() {
194
+ const provider = (process.env.MEMORY_GRAPH_PROVIDER || 'local').toLowerCase().trim();
195
+ if (provider === 'remote') {
196
+ return buildRemoteAdapter();
197
+ }
198
+ return localAdapter;
199
+ }
200
+
201
+ const adapter = resolveAdapter();
202
+
203
+ module.exports = adapter;
@@ -0,0 +1,186 @@
1
+ function clamp01(x) {
2
+ const n = Number(x);
3
+ if (!Number.isFinite(n)) return 0;
4
+ return Math.max(0, Math.min(1, n));
5
+ }
6
+
7
+ function nowTsMs() {
8
+ return Date.now();
9
+ }
10
+
11
+ function uniqStrings(list) {
12
+ const out = [];
13
+ const seen = new Set();
14
+ for (const x of Array.isArray(list) ? list : []) {
15
+ const s = String(x || '').trim();
16
+ if (!s) continue;
17
+ if (seen.has(s)) continue;
18
+ seen.add(s);
19
+ out.push(s);
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function hasErrorishSignal(signals) {
25
+ const list = Array.isArray(signals) ? signals.map(s => String(s || '')) : [];
26
+ if (list.includes('issue_already_resolved') || list.includes('openclaw_self_healed')) return false;
27
+ if (list.includes('log_error')) return true;
28
+ if (list.some(s => s.startsWith('errsig:') || s.startsWith('errsig_norm:'))) return true;
29
+ return false;
30
+ }
31
+
32
+ // Opportunity signals that indicate a chance to innovate (not just fix).
33
+ var OPPORTUNITY_SIGNALS = [
34
+ 'user_feature_request',
35
+ 'user_improvement_suggestion',
36
+ 'perf_bottleneck',
37
+ 'capability_gap',
38
+ 'stable_success_plateau',
39
+ 'external_opportunity',
40
+ 'issue_already_resolved',
41
+ 'openclaw_self_healed',
42
+ 'empty_cycle_loop_detected',
43
+ ];
44
+
45
+ function hasOpportunitySignal(signals) {
46
+ var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : [];
47
+ for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
48
+ var name = OPPORTUNITY_SIGNALS[i];
49
+ if (list.includes(name)) return true;
50
+ if (list.some(function (s) { return s.startsWith(name + ':'); })) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function mutationCategoryFromContext({ signals, driftEnabled }) {
56
+ if (hasErrorishSignal(signals)) return 'repair';
57
+ if (driftEnabled) return 'innovate';
58
+ // Auto-innovate: opportunity signals present and no errors
59
+ if (hasOpportunitySignal(signals)) return 'innovate';
60
+ // Consult strategy preset: if the configured strategy favors innovation,
61
+ // default to innovate instead of optimize when there is nothing specific to do.
62
+ try {
63
+ var strategy = require('./strategy').resolveStrategy();
64
+ if (strategy && typeof strategy.innovate === 'number' && strategy.innovate >= 0.5) return 'innovate';
65
+ } catch (_) {}
66
+ return 'optimize';
67
+ }
68
+
69
+ function expectedEffectFromCategory(category) {
70
+ const c = String(category || '');
71
+ if (c === 'repair') return 'reduce runtime errors, increase stability, and lower failure rate';
72
+ if (c === 'optimize') return 'improve success rate and reduce repeated operational cost';
73
+ if (c === 'innovate') return 'explore new strategy combinations to escape local optimum';
74
+ return 'improve robustness and success probability';
75
+ }
76
+
77
+ function targetFromGene(selectedGene) {
78
+ if (selectedGene && selectedGene.id) return `gene:${String(selectedGene.id)}`;
79
+ return 'behavior:protocol';
80
+ }
81
+
82
+ function isHighRiskPersonality(p) {
83
+ // Conservative definition: low rigor or high risk_tolerance is treated as high-risk personality.
84
+ const rigor = p && Number.isFinite(Number(p.rigor)) ? Number(p.rigor) : null;
85
+ const riskTol = p && Number.isFinite(Number(p.risk_tolerance)) ? Number(p.risk_tolerance) : null;
86
+ if (rigor != null && rigor < 0.5) return true;
87
+ if (riskTol != null && riskTol > 0.6) return true;
88
+ return false;
89
+ }
90
+
91
+ function isHighRiskMutationAllowed(personalityState) {
92
+ const rigor = personalityState && Number.isFinite(Number(personalityState.rigor)) ? Number(personalityState.rigor) : 0;
93
+ const riskTol =
94
+ personalityState && Number.isFinite(Number(personalityState.risk_tolerance))
95
+ ? Number(personalityState.risk_tolerance)
96
+ : 1;
97
+ return rigor >= 0.6 && riskTol <= 0.5;
98
+ }
99
+
100
+ function buildMutation({
101
+ signals,
102
+ selectedGene,
103
+ driftEnabled,
104
+ personalityState,
105
+ allowHighRisk = false,
106
+ target,
107
+ expected_effect,
108
+ } = {}) {
109
+ const ts = nowTsMs();
110
+ const category = mutationCategoryFromContext({ signals, driftEnabled: !!driftEnabled });
111
+ const triggerSignals = uniqStrings(signals);
112
+
113
+ const base = {
114
+ type: 'Mutation',
115
+ id: `mut_${ts}`,
116
+ category,
117
+ trigger_signals: triggerSignals,
118
+ target: String(target || targetFromGene(selectedGene)),
119
+ expected_effect: String(expected_effect || expectedEffectFromCategory(category)),
120
+ risk_level: 'low',
121
+ };
122
+
123
+ // Default risk assignment: innovate is medium; others low.
124
+ if (category === 'innovate') base.risk_level = 'medium';
125
+
126
+ // Optional high-risk escalation (rare, and guarded by strict safety constraints).
127
+ if (allowHighRisk && category === 'innovate') {
128
+ base.risk_level = 'high';
129
+ }
130
+
131
+ // Safety constraints (hard):
132
+ // - forbid innovate + high-risk personality (downgrade innovation to optimize)
133
+ // - forbid high-risk mutation unless personality satisfies constraints
134
+ const highRiskPersonality = isHighRiskPersonality(personalityState || null);
135
+ if (base.category === 'innovate' && highRiskPersonality) {
136
+ base.category = 'optimize';
137
+ base.expected_effect = 'safety downgrade: optimize under high-risk personality (avoid innovate+high-risk combo)';
138
+ base.risk_level = 'low';
139
+ base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:avoid_innovate_with_high_risk_personality']);
140
+ }
141
+
142
+ if (base.risk_level === 'high' && !isHighRiskMutationAllowed(personalityState || null)) {
143
+ // Downgrade rather than emit illegal high-risk mutation.
144
+ base.risk_level = 'medium';
145
+ base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:downgrade_high_risk']);
146
+ }
147
+
148
+ return base;
149
+ }
150
+
151
+ function isValidMutation(obj) {
152
+ if (!obj || typeof obj !== 'object') return false;
153
+ if (obj.type !== 'Mutation') return false;
154
+ if (!obj.id || typeof obj.id !== 'string') return false;
155
+ if (!obj.category || !['repair', 'optimize', 'innovate'].includes(String(obj.category))) return false;
156
+ if (!Array.isArray(obj.trigger_signals)) return false;
157
+ if (!obj.target || typeof obj.target !== 'string') return false;
158
+ if (!obj.expected_effect || typeof obj.expected_effect !== 'string') return false;
159
+ if (!obj.risk_level || !['low', 'medium', 'high'].includes(String(obj.risk_level))) return false;
160
+ return true;
161
+ }
162
+
163
+ function normalizeMutation(obj) {
164
+ const m = obj && typeof obj === 'object' ? obj : {};
165
+ const out = {
166
+ type: 'Mutation',
167
+ id: typeof m.id === 'string' ? m.id : `mut_${nowTsMs()}`,
168
+ category: ['repair', 'optimize', 'innovate'].includes(String(m.category)) ? String(m.category) : 'optimize',
169
+ trigger_signals: uniqStrings(m.trigger_signals),
170
+ target: typeof m.target === 'string' ? m.target : 'behavior:protocol',
171
+ expected_effect: typeof m.expected_effect === 'string' ? m.expected_effect : expectedEffectFromCategory(m.category),
172
+ risk_level: ['low', 'medium', 'high'].includes(String(m.risk_level)) ? String(m.risk_level) : 'low',
173
+ };
174
+ return out;
175
+ }
176
+
177
+ module.exports = {
178
+ clamp01,
179
+ buildMutation,
180
+ isValidMutation,
181
+ normalizeMutation,
182
+ isHighRiskMutationAllowed,
183
+ isHighRiskPersonality,
184
+ hasOpportunitySignal,
185
+ };
186
+
@@ -0,0 +1,108 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getNarrativePath, getEvolutionDir } = require('./paths');
6
+
7
+ const MAX_NARRATIVE_ENTRIES = 30;
8
+ const MAX_NARRATIVE_SIZE = 12000;
9
+
10
+ function ensureDir(dir) {
11
+ try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
12
+ }
13
+
14
+ function recordNarrative({ gene, signals, mutation, outcome, blast, capsule }) {
15
+ const narrativePath = getNarrativePath();
16
+ ensureDir(path.dirname(narrativePath));
17
+
18
+ const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
19
+ const geneId = gene && gene.id ? gene.id : '(auto)';
20
+ const category = (mutation && mutation.category) || (gene && gene.category) || 'unknown';
21
+ const status = outcome && outcome.status ? outcome.status : 'unknown';
22
+ const score = outcome && typeof outcome.score === 'number' ? outcome.score.toFixed(2) : '?';
23
+ const signalsSummary = Array.isArray(signals) ? signals.slice(0, 4).join(', ') : '(none)';
24
+ const filesChanged = blast ? blast.files : 0;
25
+ const linesChanged = blast ? blast.lines : 0;
26
+ const rationale = mutation && mutation.rationale
27
+ ? String(mutation.rationale).slice(0, 200) : '';
28
+ const strategy = gene && Array.isArray(gene.strategy)
29
+ ? gene.strategy.slice(0, 3).map((s, i) => ` ${i + 1}. ${s}`).join('\n') : '';
30
+ const capsuleSummary = capsule && capsule.summary ? String(capsule.summary).slice(0, 200) : '';
31
+
32
+ const entry = [
33
+ `### [${ts}] ${category.toUpperCase()} - ${status}`,
34
+ `- Gene: ${geneId} | Score: ${score} | Scope: ${filesChanged} files, ${linesChanged} lines`,
35
+ `- Signals: [${signalsSummary}]`,
36
+ rationale ? `- Why: ${rationale}` : null,
37
+ strategy ? `- Strategy:\n${strategy}` : null,
38
+ capsuleSummary ? `- Result: ${capsuleSummary}` : null,
39
+ '',
40
+ ].filter(line => line !== null).join('\n');
41
+
42
+ let existing = '';
43
+ try {
44
+ if (fs.existsSync(narrativePath)) {
45
+ existing = fs.readFileSync(narrativePath, 'utf8');
46
+ }
47
+ } catch (_) {}
48
+
49
+ if (!existing.trim()) {
50
+ existing = '# Evolution Narrative\n\nA chronological record of evolution decisions and outcomes.\n\n';
51
+ }
52
+
53
+ const combined = existing + entry;
54
+ const trimmed = trimNarrative(combined);
55
+
56
+ const tmp = narrativePath + '.tmp';
57
+ fs.writeFileSync(tmp, trimmed, 'utf8');
58
+ fs.renameSync(tmp, narrativePath);
59
+ }
60
+
61
+ function trimNarrative(content) {
62
+ if (content.length <= MAX_NARRATIVE_SIZE) return content;
63
+
64
+ const headerEnd = content.indexOf('###');
65
+ if (headerEnd < 0) return content.slice(-MAX_NARRATIVE_SIZE);
66
+
67
+ const header = content.slice(0, headerEnd);
68
+ const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
69
+
70
+ while (entries.length > MAX_NARRATIVE_ENTRIES) {
71
+ entries.shift();
72
+ }
73
+
74
+ let result = header + entries.join('');
75
+ if (result.length > MAX_NARRATIVE_SIZE) {
76
+ const keep = Math.max(1, entries.length - 5);
77
+ result = header + entries.slice(-keep).join('');
78
+ }
79
+
80
+ return result;
81
+ }
82
+
83
+ function loadNarrativeSummary(maxChars) {
84
+ const limit = Number.isFinite(maxChars) ? maxChars : 4000;
85
+ const narrativePath = getNarrativePath();
86
+ try {
87
+ if (!fs.existsSync(narrativePath)) return '';
88
+ const content = fs.readFileSync(narrativePath, 'utf8');
89
+ if (!content.trim()) return '';
90
+
91
+ const headerEnd = content.indexOf('###');
92
+ if (headerEnd < 0) return '';
93
+
94
+ const entries = content.slice(headerEnd).split(/(?=^### \[)/m);
95
+ const recent = entries.slice(-8);
96
+ let summary = recent.join('');
97
+ if (summary.length > limit) {
98
+ summary = summary.slice(-limit);
99
+ const firstEntry = summary.indexOf('### [');
100
+ if (firstEntry > 0) summary = summary.slice(firstEntry);
101
+ }
102
+ return summary.trim();
103
+ } catch (_) {
104
+ return '';
105
+ }
106
+ }
107
+
108
+ module.exports = { recordNarrative, loadNarrativeSummary, trimNarrative };
@@ -0,0 +1,113 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function getRepoRoot() {
5
+ if (process.env.EVOLVER_REPO_ROOT) {
6
+ return process.env.EVOLVER_REPO_ROOT;
7
+ }
8
+
9
+ let dir = path.resolve(__dirname, '..', '..');
10
+ while (dir !== '/' && dir !== '.') {
11
+ const gitDir = path.join(dir, '.git');
12
+ if (fs.existsSync(gitDir)) {
13
+ return dir;
14
+ }
15
+ dir = path.dirname(dir);
16
+ }
17
+
18
+ return path.resolve(__dirname, '..', '..');
19
+ }
20
+
21
+ function getWorkspaceRoot() {
22
+ if (process.env.OPENCLAW_WORKSPACE) {
23
+ return process.env.OPENCLAW_WORKSPACE;
24
+ }
25
+
26
+ const repoRoot = getRepoRoot();
27
+ const workspaceDir = path.join(repoRoot, 'workspace');
28
+ if (fs.existsSync(workspaceDir)) {
29
+ return workspaceDir;
30
+ }
31
+
32
+ return path.resolve(__dirname, '..', '..', '..', '..');
33
+ }
34
+
35
+ function getLogsDir() {
36
+ return process.env.EVOLVER_LOGS_DIR || path.join(getWorkspaceRoot(), 'logs');
37
+ }
38
+
39
+ function getEvolverLogPath() {
40
+ return path.join(getLogsDir(), 'evolver_loop.log');
41
+ }
42
+
43
+ function getMemoryDir() {
44
+ return process.env.MEMORY_DIR || path.join(getWorkspaceRoot(), 'memory');
45
+ }
46
+
47
+ // --- Session Scope Isolation ---
48
+ // When EVOLVER_SESSION_SCOPE is set (e.g., to a Discord channel ID or project name),
49
+ // evolution state, memory graph, and assets are isolated to a per-scope subdirectory.
50
+ // This prevents cross-channel/cross-project memory contamination.
51
+ // When NOT set, everything works as before (global scope, backward compatible).
52
+ function getSessionScope() {
53
+ const raw = String(process.env.EVOLVER_SESSION_SCOPE || '').trim();
54
+ if (!raw) return null;
55
+ // Sanitize: only allow alphanumeric, dash, underscore, dot (prevent path traversal).
56
+ const safe = raw.replace(/[^a-zA-Z0-9_\-\.]/g, '_').slice(0, 128);
57
+ if (!safe || /^\.{1,2}$/.test(safe) || /\.\./.test(safe)) return null;
58
+ return safe;
59
+ }
60
+
61
+ function getEvolutionDir() {
62
+ const baseDir = process.env.EVOLUTION_DIR || path.join(getMemoryDir(), 'evolution');
63
+ const scope = getSessionScope();
64
+ if (scope) {
65
+ return path.join(baseDir, 'scopes', scope);
66
+ }
67
+ return baseDir;
68
+ }
69
+
70
+ function getGepAssetsDir() {
71
+ const repoRoot = getRepoRoot();
72
+ const baseDir = process.env.GEP_ASSETS_DIR || path.join(repoRoot, 'assets', 'gep');
73
+ const scope = getSessionScope();
74
+ if (scope) {
75
+ return path.join(baseDir, 'scopes', scope);
76
+ }
77
+ return baseDir;
78
+ }
79
+
80
+ function getSkillsDir() {
81
+ return process.env.SKILLS_DIR || path.join(getWorkspaceRoot(), 'skills');
82
+ }
83
+
84
+ function getNarrativePath() {
85
+ return path.join(getEvolutionDir(), 'evolution_narrative.md');
86
+ }
87
+
88
+ function getEvolutionPrinciplesPath() {
89
+ const repoRoot = getRepoRoot();
90
+ const custom = path.join(repoRoot, 'EVOLUTION_PRINCIPLES.md');
91
+ if (fs.existsSync(custom)) return custom;
92
+ return path.join(repoRoot, 'assets', 'gep', 'EVOLUTION_PRINCIPLES.md');
93
+ }
94
+
95
+ function getReflectionLogPath() {
96
+ return path.join(getEvolutionDir(), 'reflection_log.jsonl');
97
+ }
98
+
99
+ module.exports = {
100
+ getRepoRoot,
101
+ getWorkspaceRoot,
102
+ getLogsDir,
103
+ getEvolverLogPath,
104
+ getMemoryDir,
105
+ getEvolutionDir,
106
+ getGepAssetsDir,
107
+ getSkillsDir,
108
+ getSessionScope,
109
+ getNarrativePath,
110
+ getEvolutionPrinciplesPath,
111
+ getReflectionLogPath,
112
+ };
113
+