@evomap/evolver 1.80.6 → 1.80.8

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 (76) hide show
  1. package/README.zh-CN.md +18 -11
  2. package/assets/gep/candidates.jsonl +3 -2
  3. package/index.js +55 -2
  4. package/package.json +1 -1
  5. package/src/adapters/opencode.js +137 -2
  6. package/src/config.js +5 -0
  7. package/src/evolve/guards.js +1 -1
  8. package/src/evolve/pipeline/collect.js +1 -1
  9. package/src/evolve/pipeline/dispatch.js +1 -1
  10. package/src/evolve/pipeline/enrich.js +1 -1
  11. package/src/evolve/pipeline/hub.js +1 -1
  12. package/src/evolve/pipeline/select.js +1 -1
  13. package/src/evolve/pipeline/signals.js +1 -1
  14. package/src/evolve/utils.js +1 -1
  15. package/src/evolve.js +1 -1
  16. package/src/gep/.integrity +0 -0
  17. package/src/gep/a2aProtocol.js +1 -1
  18. package/src/gep/assetStore.js +59 -5
  19. package/src/gep/candidateEval.js +1 -1
  20. package/src/gep/candidates.js +1 -1
  21. package/src/gep/contentHash.js +1 -1
  22. package/src/gep/crypto.js +1 -1
  23. package/src/gep/curriculum.js +1 -1
  24. package/src/gep/deviceId.js +1 -1
  25. package/src/gep/envFingerprint.js +1 -1
  26. package/src/gep/epigenetics.js +1 -0
  27. package/src/gep/explore.js +1 -1
  28. package/src/gep/hash.js +1 -0
  29. package/src/gep/hubReview.js +1 -1
  30. package/src/gep/hubSearch.js +1 -1
  31. package/src/gep/hubVerify.js +1 -1
  32. package/src/gep/integrityCheck.js +1 -1
  33. package/src/gep/learningSignals.js +1 -1
  34. package/src/gep/memoryGraph.js +1 -1
  35. package/src/gep/memoryGraphAdapter.js +1 -1
  36. package/src/gep/mutation.js +1 -1
  37. package/src/gep/narrativeMemory.js +1 -1
  38. package/src/gep/personality.js +1 -1
  39. package/src/gep/policyCheck.js +1 -1
  40. package/src/gep/prompt.js +1 -1
  41. package/src/gep/reflection.js +1 -1
  42. package/src/gep/selector.js +1 -1
  43. package/src/gep/shield.js +1 -1
  44. package/src/gep/skillDistiller.js +1 -1
  45. package/src/gep/solidify.js +1 -1
  46. package/src/gep/strategy.js +1 -1
  47. package/src/gep/taskReceiver.js +7 -2
  48. package/src/webui/client/clientJs/assets.js +111 -0
  49. package/src/webui/client/clientJs/bootstrap.js +92 -0
  50. package/src/webui/client/clientJs/common.js +77 -0
  51. package/src/webui/client/clientJs/i18n.js +366 -0
  52. package/src/webui/client/clientJs/index.js +35 -0
  53. package/src/webui/client/clientJs/interactions.js +351 -0
  54. package/src/webui/client/clientJs/overview.js +152 -0
  55. package/src/webui/client/clientJs/personality.js +285 -0
  56. package/src/webui/client/clientJs/pipelines.js +330 -0
  57. package/src/webui/client/indexHtml.js +221 -0
  58. package/src/webui/client/static.js +23 -0
  59. package/src/webui/client/stylesCss.js +639 -0
  60. package/src/webui/client/vendor/README.md +15 -0
  61. package/src/webui/client/vendor/echarts.min.js +45 -0
  62. package/src/webui/index.js +14 -0
  63. package/src/webui/observer/assets.js +146 -0
  64. package/src/webui/observer/index.js +37 -0
  65. package/src/webui/observer/interactions.js +120 -0
  66. package/src/webui/observer/jsonl.js +75 -0
  67. package/src/webui/observer/paths.js +46 -0
  68. package/src/webui/observer/personality.js +43 -0
  69. package/src/webui/observer/pipelineEvents.js +58 -0
  70. package/src/webui/observer/redact.js +63 -0
  71. package/src/webui/observer/runs.js +356 -0
  72. package/src/webui/observer/safety.js +57 -0
  73. package/src/webui/observer/skills.js +70 -0
  74. package/src/webui/observer/status.js +71 -0
  75. package/src/webui/server/http.js +138 -0
  76. package/src/webui/server/routes.js +41 -0
@@ -0,0 +1,356 @@
1
+ 'use strict';
2
+
3
+ const { getObserverPaths } = require('./paths');
4
+ const { readJsonSafe, readJsonl, paginate } = require('./jsonl');
5
+ const { redactValue } = require('./redact');
6
+ const { readPipelineEvents } = require('./pipelineEvents');
7
+
8
+ const PHASE_ORDER = [
9
+ 'detect_signals',
10
+ 'select_gene',
11
+ 'asset_search',
12
+ 'mutate_strategy',
13
+ 'validate',
14
+ 'solidify',
15
+ 'confirmation',
16
+ ];
17
+
18
+ // A run that wrote `attempt` but never produced an `outcome` is stuck. We
19
+ // can't tell apart "still running" from "never going to finish" purely from
20
+ // memory_graph alone, so we use a wall-clock threshold: anything older than
21
+ // this with no outcome is reclassified `abandoned`. This stops phantom
22
+ // "running" rows piling up after crashes, kills, or selectors that bailed
23
+ // out early (e.g. no matching gene found). Override via env if needed.
24
+ const STUCK_THRESHOLD_MS = parseInt(process.env.EVOLVER_RUN_STUCK_THRESHOLD_MS, 10) || (30 * 60 * 1000);
25
+
26
+ function listRuns(query = {}) {
27
+ const runs = buildRuns().sort((a, b) => timestampOf(b.updatedAt) - timestampOf(a.updatedAt));
28
+ return paginate(runs, query);
29
+ }
30
+
31
+ function getRun(runId) {
32
+ const runs = buildRuns();
33
+ const run = runs.find((entry) => entry.runId === runId);
34
+ if (!run) return null;
35
+ const paths = getObserverPaths();
36
+ const events = readJsonl(paths.eventsPath).map(redactValue).filter((e) => belongsToRun(e, runId));
37
+ const assetCalls = readJsonl(paths.assetCallLogPath).map(redactValue).filter((e) => belongsToRun(e, runId));
38
+ const pipelineEvents = readPipelineEvents().filter((e) => belongsToRun(e, runId));
39
+ const detail = buildRunDetail(runId);
40
+ return {
41
+ ...run,
42
+ detail,
43
+ phases: buildPhases(run, pipelineEvents, events, assetCalls),
44
+ evidence: events,
45
+ assets: assetCalls,
46
+ pipelineEvents,
47
+ };
48
+ }
49
+
50
+ function buildRunDetail(runId) {
51
+ const paths = getObserverPaths();
52
+ const solidify = readJsonSafe(paths.solidifyStatePath, null);
53
+ const last = solidify && solidify.last_run;
54
+ if (!last || String(last.run_id) !== String(runId)) return null;
55
+ const safe = redactValue(last);
56
+ return {
57
+ parentEventId: safe.parent_event_id || null,
58
+ selectedCapsuleId: safe.selected_capsule_id || null,
59
+ signals: safe.signals || [],
60
+ initialUserPrompt: safe.initial_user_prompt || null,
61
+ activeTaskId: safe.active_task_id || null,
62
+ activeTaskTitle: safe.active_task_title || null,
63
+ blastRadius: safe.blast_radius_estimate || null,
64
+ selector: safe.selector || null,
65
+ mutation: safe.mutation ? simplifyMutation(safe.mutation) : null,
66
+ personalityState: safe.personality_state || null,
67
+ drift: safe.drift || null,
68
+ appliedLessons: safe.applied_lessons || [],
69
+ hubLessons: safe.hub_lessons || [],
70
+ sourceType: safe.source_type || null,
71
+ reusedAssetId: safe.reused_asset_id || null,
72
+ baselineGitHead: safe.baseline_git_head || null,
73
+ };
74
+ }
75
+
76
+ function simplifyMutation(mutation) {
77
+ if (!mutation || typeof mutation !== 'object') return null;
78
+ return {
79
+ id: mutation.id || null,
80
+ category: mutation.category || null,
81
+ triggerSignals: mutation.trigger_signals || [],
82
+ targetType: mutation.target_type || null,
83
+ summary: mutation.summary || null,
84
+ strategySteps: Array.isArray(mutation.strategy) ? mutation.strategy.length : null,
85
+ constraints: mutation.constraints || null,
86
+ };
87
+ }
88
+
89
+ function buildRuns() {
90
+ const paths = getObserverPaths();
91
+ const cycle = readJsonSafe(paths.cycleProgressPath, null);
92
+ const solidify = readJsonSafe(paths.solidifyStatePath, null);
93
+ const events = readJsonl(paths.eventsPath).map(redactValue);
94
+ const assetCalls = readJsonl(paths.assetCallLogPath).map(redactValue);
95
+ const pipelineEvents = readPipelineEvents();
96
+ const runs = new Map();
97
+
98
+ addCycleRun(runs, cycle, solidify);
99
+ for (const event of events) mergeRun(runs, summaryFromEvent(event));
100
+ for (const call of assetCalls) mergeRun(runs, summaryFromAssetCall(call));
101
+ for (const event of pipelineEvents) mergeRun(runs, summaryFromPipelineEvent(event));
102
+
103
+ const now = Date.now();
104
+ return Array.from(runs.values()).map((run) => ({
105
+ ...run,
106
+ status: maybeAbandon(run, now),
107
+ requiresConfirmation: Boolean(run.requiresConfirmation),
108
+ }));
109
+ }
110
+
111
+ function maybeAbandon(run, now) {
112
+ if (run.status !== 'running' || run.finishedAt) return run.status;
113
+ const t = timestampOf(run.updatedAt);
114
+ if (!t) return run.status;
115
+ return now - t > STUCK_THRESHOLD_MS ? 'abandoned' : 'running';
116
+ }
117
+
118
+ function addCycleRun(runs, cycle, solidify) {
119
+ if (!cycle && !(solidify && solidify.last_run)) return;
120
+ const last = solidify && solidify.last_run || {};
121
+ const lastSolidify = solidify && solidify.last_solidify || null;
122
+ const pending = isPending(last, lastSolidify);
123
+ const runId = String(cycle && (cycle.run_id || cycle.outer_cycle) || last.run_id || last.mutation_id || 'current');
124
+ mergeRun(runs, {
125
+ runId,
126
+ cycleId: cycle && String(cycle.outer_cycle || cycle.cycle_id || last.cycleId || ''),
127
+ status: deriveCycleStatus(cycle, last, lastSolidify, pending),
128
+ startedAt: toIso(cycle && cycle.started_at || last.started_at || last.created_at),
129
+ updatedAt: toIso(cycle && cycle.updated_at || lastSolidify && lastSolidify.timestamp || last.finished_at || last.created_at),
130
+ finishedAt: toIso(last.finished_at || lastSolidify && lastSolidify.timestamp),
131
+ activeTaskTitle: last.active_task_title || null,
132
+ selectedGeneId: last.selected_gene_id || null,
133
+ outcome: last.outcome || (lastSolidify && lastSolidify.rejected ? { status: 'failed', reason: lastSolidify.reason } : null),
134
+ validationResult: validationResult(last),
135
+ requiresConfirmation: pending,
136
+ });
137
+ }
138
+
139
+ function isPending(lastRun, lastSolidify) {
140
+ if (!lastRun || !lastRun.run_id) return false;
141
+ if (!lastSolidify || !lastSolidify.run_id) return true;
142
+ return String(lastSolidify.run_id) !== String(lastRun.run_id);
143
+ }
144
+
145
+ function deriveCycleStatus(cycle, lastRun, lastSolidify, pending) {
146
+ if (cycle && cycle.phase && isCycleFresh(cycle)) return 'running';
147
+ if (lastSolidify && String(lastSolidify.run_id) === String(lastRun.run_id)) {
148
+ if (lastSolidify.rejected) return 'failed';
149
+ if (lastSolidify.success || lastSolidify.solidified) return 'completed';
150
+ }
151
+ if (pending) return 'review_pending';
152
+ if (lastRun.outcome) return inferOutcome(lastRun.outcome);
153
+ if (lastRun.selected_gene_id) return 'selected';
154
+ return 'unknown';
155
+ }
156
+
157
+ function isCycleFresh(cycle) {
158
+ const t = Number(cycle && cycle.updated_at);
159
+ if (!Number.isFinite(t)) return false;
160
+ return Date.now() - t < 5 * 60 * 1000;
161
+ }
162
+
163
+ function summaryFromEvent(event) {
164
+ const runId = event.run_id || event.mutation_id || event.id;
165
+ if (!runId) return null;
166
+ const eventTime = event.started_at || event.timestamp || event.created_at;
167
+ return {
168
+ runId: String(runId),
169
+ status: inferOutcome(event.outcome),
170
+ startedAt: toIso(eventTime),
171
+ updatedAt: toIso(event.finished_at || eventTime),
172
+ finishedAt: toIso(event.finished_at),
173
+ selectedGeneId: firstGene(event.genes_used),
174
+ outcome: event.outcome || null,
175
+ validationResult: validationResult(event),
176
+ requiresConfirmation: Boolean(event.requires_confirmation),
177
+ };
178
+ }
179
+
180
+ function summaryFromAssetCall(call) {
181
+ if (!call.run_id) return null;
182
+ // Asset-call entries are historical evidence and do not carry a definitive
183
+ // run status. Authoritative status comes from cycle/event/pipelineEvent
184
+ // sources; leaving status undefined here lets mergeRun keep whatever those
185
+ // sources set (or 'unknown' if this is the only source for the run).
186
+ return {
187
+ runId: String(call.run_id),
188
+ updatedAt: toIso(call.timestamp),
189
+ requiresConfirmation: isConfirmationAction(call.action),
190
+ };
191
+ }
192
+
193
+ function summaryFromPipelineEvent(event) {
194
+ const runId = event.run_id || event.cycle_id;
195
+ if (!runId) return null;
196
+ return {
197
+ runId: String(runId),
198
+ cycleId: event.cycle_id,
199
+ status: phaseStatusToRunStatus(event.status),
200
+ startedAt: toIso(event.started_at || event.timestamp),
201
+ updatedAt: toIso(event.finished_at || event.timestamp),
202
+ finishedAt: toIso(event.finished_at),
203
+ requiresConfirmation: Boolean(event.requires_confirmation),
204
+ };
205
+ }
206
+
207
+ function mergeRun(runs, next) {
208
+ if (!next || !next.runId) return;
209
+ const current = runs.get(next.runId) || { runId: next.runId, status: 'unknown' };
210
+ runs.set(next.runId, {
211
+ ...current,
212
+ ...emptyFiltered(next),
213
+ startedAt: earliest(current.startedAt, next.startedAt),
214
+ updatedAt: latest(current.updatedAt, next.updatedAt),
215
+ finishedAt: latest(current.finishedAt, next.finishedAt),
216
+ requiresConfirmation: current.requiresConfirmation || next.requiresConfirmation,
217
+ });
218
+ }
219
+
220
+ function buildPhases(run, pipelineEvents, events, assetCalls) {
221
+ if (pipelineEvents.length > 0) return pipelineEvents.map(eventToPhase);
222
+ return PHASE_ORDER.map((phase) => inferPhase(phase, run, events, assetCalls));
223
+ }
224
+
225
+ function eventToPhase(event) {
226
+ return {
227
+ phase: event.phase,
228
+ status: event.status,
229
+ startedAt: event.started_at,
230
+ finishedAt: event.finished_at,
231
+ summary: event.summary,
232
+ evidenceRefs: event.evidence_refs || [],
233
+ assetRefs: event.asset_refs || [],
234
+ validationRefs: event.validation_refs || [],
235
+ requiresConfirmation: Boolean(event.requires_confirmation),
236
+ };
237
+ }
238
+
239
+ function inferPhase(phase, run, events, assetCalls) {
240
+ const status = inferPhaseStatus(phase, run, events, assetCalls);
241
+ return {
242
+ phase,
243
+ status,
244
+ startedAt: run.startedAt || null,
245
+ finishedAt: status === 'success' ? run.finishedAt || run.updatedAt || null : null,
246
+ summary: summaryForPhase(phase, run, events, assetCalls),
247
+ evidenceRefs: events.map((event) => ({ type: 'event', id: event.id })).filter((e) => e.id),
248
+ assetRefs: assetCalls.map((call) => ({ type: call.asset_type || 'asset', id: call.asset_id, action: call.action })).filter((a) => a.id || a.action),
249
+ validationRefs: validationRefs(events),
250
+ requiresConfirmation: phase === 'confirmation' && run.requiresConfirmation,
251
+ };
252
+ }
253
+
254
+ function inferPhaseStatus(phase, run, events, assetCalls) {
255
+ if (phase === 'asset_search') return assetCalls.length ? 'success' : 'skipped';
256
+ if (phase === 'confirmation') return run.requiresConfirmation ? 'blocked' : 'skipped';
257
+ if (phase === 'validate') return run.validationResult === 'fail' ? 'failed' : run.validationResult === 'pass' ? 'success' : 'skipped';
258
+ if (phase === 'solidify') {
259
+ if (events.length) return 'success';
260
+ if (run.status === 'review_pending') return 'blocked';
261
+ if (run.status === 'running' || run.status === 'selected') return 'pending';
262
+ return 'skipped';
263
+ }
264
+ if (phase === 'detect_signals' || phase === 'select_gene') {
265
+ return run.selectedGeneId ? 'success' : run.status === 'running' ? 'running' : 'skipped';
266
+ }
267
+ if (phase === 'mutate_strategy') {
268
+ return run.selectedGeneId ? 'success' : 'skipped';
269
+ }
270
+ return run.status === 'running' ? 'running' : 'success';
271
+ }
272
+
273
+ function summaryForPhase(phase, run, events, assetCalls) {
274
+ if (phase === 'select_gene') return run.selectedGeneId ? `Selected ${run.selectedGeneId}` : 'No selected Gene recorded yet.';
275
+ if (phase === 'asset_search') return assetCalls.length ? `${assetCalls.length} asset call(s) recorded.` : 'No asset calls recorded.';
276
+ if (phase === 'solidify') return events.length ? `${events.length} EvolutionEvent record(s) found.` : 'No solidified event recorded.';
277
+ if (phase === 'confirmation') return run.requiresConfirmation ? 'Human confirmation is required.' : 'No confirmation required.';
278
+ return phase.replace(/_/g, ' ');
279
+ }
280
+
281
+ function belongsToRun(entry, runId) {
282
+ if (!entry || runId == null || runId === '') return false;
283
+ const target = String(runId);
284
+ return [entry.run_id, entry.mutation_id, entry.id, entry.cycle_id]
285
+ .filter((value) => value != null && value !== '')
286
+ .map(String)
287
+ .includes(target);
288
+ }
289
+
290
+ function validationRefs(events) {
291
+ return events.flatMap((event) => event.validation || event.validation_results || []).filter(Boolean);
292
+ }
293
+
294
+ function validationResult(entry) {
295
+ if (!entry) return 'unknown';
296
+ if (entry.validation_result) return entry.validation_result;
297
+ if (entry.validation && entry.validation.ok === true) return 'pass';
298
+ if (entry.validation && entry.validation.ok === false) return 'fail';
299
+ return 'unknown';
300
+ }
301
+
302
+ function phaseStatusToRunStatus(status) {
303
+ if (status === 'failed') return 'failed';
304
+ if (status === 'blocked') return 'blocked';
305
+ if (status === 'success') return 'completed';
306
+ return 'running';
307
+ }
308
+
309
+ function inferOutcome(outcome) {
310
+ const value = typeof outcome === 'string' ? outcome : outcome && outcome.status;
311
+ if (value === 'success') return 'completed';
312
+ if (value === 'failed') return 'failed';
313
+ return 'unknown';
314
+ }
315
+
316
+ function firstGene(genes) {
317
+ return Array.isArray(genes) && genes.length ? genes[0] : null;
318
+ }
319
+
320
+ function isConfirmationAction(action) {
321
+ return ['asset_publish', 'asset_fetch', 'task_claim', 'validator_stake'].includes(action);
322
+ }
323
+
324
+ function toIso(value) {
325
+ if (!value) return null;
326
+ if (typeof value === 'string') return value;
327
+ const n = Number(value);
328
+ return Number.isFinite(n) ? new Date(n).toISOString() : null;
329
+ }
330
+
331
+ function timestampOf(value) {
332
+ const t = Date.parse(value || '');
333
+ return Number.isFinite(t) ? t : 0;
334
+ }
335
+
336
+ function emptyFiltered(obj) {
337
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== null && value !== undefined && value !== ''));
338
+ }
339
+
340
+ function earliest(a, b) {
341
+ if (!a) return b || null;
342
+ if (!b) return a;
343
+ return timestampOf(a) <= timestampOf(b) ? a : b;
344
+ }
345
+
346
+ function latest(a, b) {
347
+ if (!a) return b || null;
348
+ if (!b) return a;
349
+ return timestampOf(a) >= timestampOf(b) ? a : b;
350
+ }
351
+
352
+ module.exports = {
353
+ listRuns,
354
+ getRun,
355
+ buildRuns,
356
+ };
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ function getSafetyState() {
4
+ return {
5
+ autobuyEnabled: isEnabled(process.env.EVOLVER_ATP_AUTOBUY) || isEnabled(process.env.ATP_AUTOBUY),
6
+ dailyCreditCap: numberEnv('ATP_AUTOBUY_DAILY_CAP_CREDITS', 0),
7
+ perOrderCreditCap: numberEnv('ATP_AUTOBUY_PER_ORDER_CAP_CREDITS', 0),
8
+ autoPublishEnabled: isEnabled(process.env.EVOLVER_AUTO_PUBLISH),
9
+ validatorEnabled: isEnabled(process.env.EVOLVER_VALIDATOR_ENABLED),
10
+ traceLevel: String(process.env.EVOLVER_TRACE_LEVEL || 'minimal'),
11
+ defaultVisibility: process.env.EVOLVER_DEFAULT_VISIBILITY || 'private',
12
+ safeMode: isSafeMode(),
13
+ warnings: getWarnings(),
14
+ };
15
+ }
16
+
17
+ function isSafeMode() {
18
+ const state = {
19
+ autobuyEnabled: isEnabled(process.env.EVOLVER_ATP_AUTOBUY) || isEnabled(process.env.ATP_AUTOBUY),
20
+ dailyCreditCap: numberEnv('ATP_AUTOBUY_DAILY_CAP_CREDITS', 0),
21
+ perOrderCreditCap: numberEnv('ATP_AUTOBUY_PER_ORDER_CAP_CREDITS', 0),
22
+ autoPublishEnabled: isEnabled(process.env.EVOLVER_AUTO_PUBLISH),
23
+ validatorEnabled: isEnabled(process.env.EVOLVER_VALIDATOR_ENABLED),
24
+ };
25
+ return !state.autobuyEnabled &&
26
+ state.dailyCreditCap === 0 &&
27
+ state.perOrderCreditCap === 0 &&
28
+ !state.autoPublishEnabled &&
29
+ !state.validatorEnabled;
30
+ }
31
+
32
+ function getWarnings() {
33
+ const warnings = [];
34
+ if (isEnabled(process.env.EVOLVER_ATP_AUTOBUY) || isEnabled(process.env.ATP_AUTOBUY)) {
35
+ warnings.push('ATP autobuy is enabled.');
36
+ }
37
+ if (numberEnv('ATP_AUTOBUY_DAILY_CAP_CREDITS', 0) > 0) {
38
+ warnings.push('Daily ATP autobuy credit cap is above zero.');
39
+ }
40
+ if (numberEnv('ATP_AUTOBUY_PER_ORDER_CAP_CREDITS', 0) > 0) {
41
+ warnings.push('Per-order ATP autobuy credit cap is above zero.');
42
+ }
43
+ if (isEnabled(process.env.EVOLVER_AUTO_PUBLISH)) warnings.push('Auto-publish is enabled.');
44
+ if (isEnabled(process.env.EVOLVER_VALIDATOR_ENABLED)) warnings.push('Validator daemon is enabled.');
45
+ return warnings;
46
+ }
47
+
48
+ function isEnabled(value) {
49
+ return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
50
+ }
51
+
52
+ function numberEnv(key, fallback) {
53
+ const n = Number(process.env[key]);
54
+ return Number.isFinite(n) ? n : fallback;
55
+ }
56
+
57
+ module.exports = { getSafetyState };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getSkillsDir } = require('../../gep/paths');
6
+
7
+ function listSkills() {
8
+ const dir = getSkillsDir();
9
+ if (!fs.existsSync(dir)) return { exists: false, items: [] };
10
+
11
+ const entries = safeReaddir(dir).filter((entry) => entry.isDirectory());
12
+ const items = entries.map((entry) => buildSkillSummary(path.join(dir, entry.name), entry.name));
13
+ return { exists: true, items };
14
+ }
15
+
16
+ function buildSkillSummary(skillPath, name) {
17
+ const skillFile = findFirstExisting(skillPath, ['SKILL.md', 'skill.md', 'README.md']);
18
+ let description = '';
19
+ let bytes = 0;
20
+ if (skillFile) {
21
+ try {
22
+ const stat = fs.statSync(skillFile);
23
+ bytes = stat.size;
24
+ description = extractDescription(skillFile);
25
+ } catch {
26
+ // ignore
27
+ }
28
+ }
29
+ const fileCount = safeReaddir(skillPath).length;
30
+ return {
31
+ name,
32
+ description,
33
+ docFile: skillFile ? path.basename(skillFile) : null,
34
+ docBytes: bytes,
35
+ fileCount,
36
+ };
37
+ }
38
+
39
+ function extractDescription(filePath) {
40
+ try {
41
+ const raw = fs.readFileSync(filePath, 'utf8');
42
+ const frontmatter = raw.match(/^---\s*\n([\s\S]*?)\n---/);
43
+ if (frontmatter) {
44
+ const desc = frontmatter[1].match(/description\s*:\s*([^\n]+)/);
45
+ if (desc) return desc[1].trim().replace(/^['"]|['"]$/g, '');
46
+ }
47
+ const firstParagraph = raw.replace(/^---[\s\S]*?---/, '').trim().split(/\n\s*\n/)[0];
48
+ return firstParagraph.replace(/^#+\s*/, '').slice(0, 240);
49
+ } catch {
50
+ return '';
51
+ }
52
+ }
53
+
54
+ function safeReaddir(dir) {
55
+ try {
56
+ return fs.readdirSync(dir, { withFileTypes: true });
57
+ } catch {
58
+ return [];
59
+ }
60
+ }
61
+
62
+ function findFirstExisting(dir, names) {
63
+ for (const name of names) {
64
+ const candidate = path.join(dir, name);
65
+ if (fs.existsSync(candidate)) return candidate;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ module.exports = { listSkills };
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { getObserverPaths } = require('./paths');
5
+ const { readJsonSafe } = require('./jsonl');
6
+ const { getSafetyState } = require('./safety');
7
+ const { redactValue } = require('./redact');
8
+
9
+ function getStatus() {
10
+ const paths = getObserverPaths();
11
+ const cycle = readJsonSafe(paths.cycleProgressPath, null);
12
+ const solidify = readJsonSafe(paths.solidifyStatePath, null);
13
+ const evolution = readJsonSafe(paths.evolutionStatePath, null);
14
+ const proxy = getProxySettings();
15
+ // Everything observer-derived may carry prompt / task context with
16
+ // embedded credentials. Every other observer endpoint (runs.js,
17
+ // assets.js, interactions.js, ...) already pipes its payload through
18
+ // redactValue; /webui/status is the one that skipped it, so it became
19
+ // the easy exfiltration path. Funnel the three readJsonSafe outputs
20
+ // through the same redaction filter before they leave the process.
21
+ return {
22
+ mode: inferMode(cycle, solidify, proxy),
23
+ heartbeat: redactValue(cycle),
24
+ lastRun: redactValue(solidify && solidify.last_run ? solidify.last_run : null),
25
+ evolutionState: redactValue(evolution),
26
+ proxy,
27
+ filesPresent: filesPresent(paths),
28
+ safety: getSafetyState(),
29
+ };
30
+ }
31
+
32
+ function inferMode(cycle, solidify, proxy) {
33
+ if (cycle && isFresh(cycle.updated_at, 120_000)) return 'running';
34
+ if (solidify && solidify.pending) return 'review_pending';
35
+ if (proxy && proxy.running) return 'proxy_only';
36
+ return 'idle';
37
+ }
38
+
39
+ function isFresh(timestamp, maxAgeMs) {
40
+ const t = Number(timestamp);
41
+ return Number.isFinite(t) && Date.now() - t < maxAgeMs;
42
+ }
43
+
44
+ function getProxySettings() {
45
+ try {
46
+ const { readSettings, isStaleProxy } = require('../../proxy/server/settings');
47
+ const settings = readSettings();
48
+ const proxy = settings.proxy || null;
49
+ if (!proxy) return { running: false, url: null };
50
+ return {
51
+ running: !isStaleProxy(),
52
+ url: proxy.url || null,
53
+ pid: proxy.pid || null,
54
+ started_at: proxy.started_at || null,
55
+ };
56
+ } catch {
57
+ return { running: false, url: null };
58
+ }
59
+ }
60
+
61
+ function filesPresent(paths) {
62
+ return {
63
+ cycleProgress: fs.existsSync(paths.cycleProgressPath),
64
+ solidifyState: fs.existsSync(paths.solidifyStatePath),
65
+ events: fs.existsSync(paths.eventsPath),
66
+ assetCallLog: fs.existsSync(paths.assetCallLogPath),
67
+ pipelineEvents: fs.existsSync(paths.pipelineEventsPath),
68
+ };
69
+ }
70
+
71
+ module.exports = { getStatus, getProxySettings };
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ const http = require('http');
4
+ const { buildWebUiRoutes } = require('./routes');
5
+ const { getIndexHtml, getClientJs, getStylesCss, getVendorEcharts } = require('../client/static');
6
+
7
+ const DEFAULT_WEBUI_PORT = 19821;
8
+ const MAX_PORT_ATTEMPTS = 50;
9
+
10
+ class WebUiServer {
11
+ constructor(opts = {}) {
12
+ this.port = opts.port || Number(process.env.EVOLVER_WEBUI_PORT) || DEFAULT_WEBUI_PORT;
13
+ this.logger = opts.logger || console;
14
+ this.routes = opts.routes || buildWebUiRoutes();
15
+ this.server = null;
16
+ this.actualPort = null;
17
+ }
18
+
19
+ async start() {
20
+ this.server = http.createServer((req, res) => this._handle(req, res));
21
+ let port = this.port;
22
+ for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
23
+ const ok = await tryListen(this.server, port);
24
+ if (ok) {
25
+ this.actualPort = port;
26
+ const url = `http://127.0.0.1:${port}`;
27
+ this.logger.log(`[webui] listening on ${url}`);
28
+ return { port, url };
29
+ }
30
+ port++;
31
+ }
32
+ throw new Error(`Could not find free Web UI port after ${MAX_PORT_ATTEMPTS} attempts`);
33
+ }
34
+
35
+ async stop() {
36
+ if (!this.server) return;
37
+ await new Promise((resolve) => this.server.close(resolve));
38
+ this.server = null;
39
+ }
40
+
41
+ async _handle(req, res) {
42
+ const url = new URL(req.url, `http://127.0.0.1:${this.actualPort || this.port}`);
43
+ if (req.method === 'GET' && url.pathname === '/') return sendText(res, 200, 'text/html; charset=utf-8', getIndexHtml());
44
+ if (req.method === 'GET' && url.pathname === '/app.js') return sendText(res, 200, 'application/javascript; charset=utf-8', getClientJs());
45
+ if (req.method === 'GET' && url.pathname === '/app.css') return sendText(res, 200, 'text/css; charset=utf-8', getStylesCss());
46
+ if (req.method === 'GET' && url.pathname === '/vendor/echarts.min.js') return sendText(res, 200, 'application/javascript; charset=utf-8', getVendorEcharts());
47
+
48
+ const matched = matchRoute(this.routes, req.method, url.pathname);
49
+ if (!matched) return sendJson(res, 404, { error: { code: 'NOT_FOUND', message: 'Not found', details: { path: url.pathname } } });
50
+
51
+ try {
52
+ const query = Object.fromEntries(url.searchParams);
53
+ const result = await matched.handler({ query, params: matched.params });
54
+ return sendJson(res, result.status || 200, result.body || result);
55
+ } catch (err) {
56
+ this.logger.error('[webui] request failed:', err && err.message || err);
57
+ return sendJson(res, err.statusCode || 500, {
58
+ error: {
59
+ code: err.code || 'READ_FAILED',
60
+ message: err.message || 'Internal error',
61
+ details: err.details || {},
62
+ },
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ function matchRoute(routes, method, pathname) {
69
+ for (const [pattern, handler] of Object.entries(routes)) {
70
+ const [routeMethod, routePath] = pattern.split(' ');
71
+ if (routeMethod !== method) continue;
72
+ const params = matchPath(routePath, pathname);
73
+ if (params) return { handler, params };
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function matchPath(pattern, pathname) {
79
+ const patternParts = pattern.split('/');
80
+ const pathParts = pathname.split('/');
81
+ if (patternParts.length !== pathParts.length) return null;
82
+ const params = {};
83
+ for (let i = 0; i < patternParts.length; i++) {
84
+ if (patternParts[i].startsWith(':')) {
85
+ // decodeURIComponent throws URIError on malformed percent escapes
86
+ // (e.g. /webui/runs/%E0%A4%A). matchPath runs outside _handle's
87
+ // try/catch, so an unhandled rejection would crash the request and
88
+ // depending on the Node version's unhandled-rejection policy could
89
+ // tip the local WebUI into a denial-of-service. Treat a malformed
90
+ // segment as "no route matches" so the request falls through to a
91
+ // clean 404.
92
+ try {
93
+ params[patternParts[i].slice(1)] = decodeURIComponent(pathParts[i]);
94
+ } catch (_) {
95
+ return null;
96
+ }
97
+ } else if (patternParts[i] !== pathParts[i]) {
98
+ return null;
99
+ }
100
+ }
101
+ return params;
102
+ }
103
+
104
+ function tryListen(server, port) {
105
+ return new Promise((resolve, reject) => {
106
+ const onError = (err) => {
107
+ server.removeListener('listening', onListening);
108
+ if (err.code === 'EADDRINUSE') return resolve(false);
109
+ reject(err);
110
+ };
111
+ const onListening = () => {
112
+ server.removeListener('error', onError);
113
+ resolve(true);
114
+ };
115
+ server.once('error', onError);
116
+ server.once('listening', onListening);
117
+ server.listen(port, '127.0.0.1');
118
+ });
119
+ }
120
+
121
+ function sendJson(res, status, body) {
122
+ const payload = JSON.stringify(body);
123
+ sendText(res, status, 'application/json; charset=utf-8', payload);
124
+ }
125
+
126
+ function sendText(res, status, contentType, text) {
127
+ res.writeHead(status, {
128
+ 'Content-Type': contentType,
129
+ 'Content-Length': Buffer.byteLength(text),
130
+ });
131
+ res.end(text);
132
+ }
133
+
134
+ module.exports = {
135
+ WebUiServer,
136
+ DEFAULT_WEBUI_PORT,
137
+ matchPath,
138
+ };