@evomap/evolver 1.89.20 → 2.0.0-beta.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 (231) hide show
  1. package/README.md +27 -564
  2. package/bin/evolver-llm-proxy.js +3 -0
  3. package/bin/evolver-mcp.js +2 -0
  4. package/bin/evolver-proxy.js +3 -0
  5. package/bin/evolver.js +4 -0
  6. package/index.js +1 -3586
  7. package/package.json +32 -62
  8. package/CONTRIBUTING.md +0 -19
  9. package/LICENSE +0 -641
  10. package/README.ja-JP.md +0 -521
  11. package/README.ko-KR.md +0 -520
  12. package/README.zh-CN.md +0 -531
  13. package/SKILL.md +0 -365
  14. package/assets/cover.png +0 -0
  15. package/assets/gep/genes.seed.json +0 -496
  16. package/conformance/savings-core/constants.json +0 -30
  17. package/conformance/savings-core/golden-vectors.json +0 -333
  18. package/scripts/a2a_export.js +0 -63
  19. package/scripts/a2a_ingest.js +0 -79
  20. package/scripts/a2a_promote.js +0 -118
  21. package/scripts/analyze_by_skill.js +0 -121
  22. package/scripts/build_binaries.js +0 -479
  23. package/scripts/check-changelog.js +0 -166
  24. package/scripts/extract_log.js +0 -85
  25. package/scripts/generate_history.js +0 -75
  26. package/scripts/gep_append_event.js +0 -96
  27. package/scripts/gep_personality_report.js +0 -234
  28. package/scripts/human_report.js +0 -147
  29. package/scripts/recall-verify-report.js +0 -234
  30. package/scripts/recover_loop.js +0 -61
  31. package/scripts/refresh_stars_badge.js +0 -168
  32. package/scripts/seed-merchants.js +0 -91
  33. package/scripts/skill2recipes.js +0 -118
  34. package/scripts/suggest_version.js +0 -89
  35. package/scripts/validate-modules.js +0 -38
  36. package/scripts/validate-suite.js +0 -78
  37. package/skills/_meta/SKILL.md +0 -41
  38. package/skills/index.json +0 -14
  39. package/src/adapters/claudeCode.js +0 -194
  40. package/src/adapters/codex.js +0 -216
  41. package/src/adapters/cursor.js +0 -91
  42. package/src/adapters/hookAdapter.js +0 -469
  43. package/src/adapters/kiro.js +0 -195
  44. package/src/adapters/opencode.js +0 -326
  45. package/src/adapters/scripts/_lockPaths.js +0 -74
  46. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  47. package/src/adapters/scripts/_runtimePaths.js +0 -440
  48. package/src/adapters/scripts/evolver-session-end.js +0 -321
  49. package/src/adapters/scripts/evolver-session-start.js +0 -587
  50. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  51. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  52. package/src/atp/atpExecute.js +0 -283
  53. package/src/atp/atpTaskPickup.js +0 -233
  54. package/src/atp/autoBuyer.js +0 -382
  55. package/src/atp/autoDeliver.js +0 -215
  56. package/src/atp/cli.js +0 -354
  57. package/src/atp/cliAutobuyPrompt.js +0 -154
  58. package/src/atp/consumerAgent.js +0 -157
  59. package/src/atp/defaultHandler.js +0 -69
  60. package/src/atp/heartbeatSignalsHandler.js +0 -254
  61. package/src/atp/hubClient.js +0 -317
  62. package/src/atp/index.js +0 -38
  63. package/src/atp/merchantAgent.js +0 -118
  64. package/src/atp/protocol.js +0 -41
  65. package/src/atp/questionComposer.js +0 -133
  66. package/src/atp/serviceHelper.js +0 -92
  67. package/src/canary.js +0 -13
  68. package/src/config.js +0 -351
  69. package/src/evolve/guards.js +0 -1
  70. package/src/evolve/pipeline/collect.js +0 -1
  71. package/src/evolve/pipeline/dispatch.js +0 -1
  72. package/src/evolve/pipeline/enrich.js +0 -1
  73. package/src/evolve/pipeline/hub.js +0 -1
  74. package/src/evolve/pipeline/select.js +0 -1
  75. package/src/evolve/pipeline/signals.js +0 -1
  76. package/src/evolve/utils.js +0 -1
  77. package/src/evolve.js +0 -1
  78. package/src/experiment/agentRunner.js +0 -229
  79. package/src/experiment/cli.js +0 -159
  80. package/src/experiment/comparison.js +0 -233
  81. package/src/experiment/metrics.js +0 -75
  82. package/src/forceUpdate.js +0 -991
  83. package/src/gep/a2a.js +0 -173
  84. package/src/gep/a2aProtocol.js +0 -1
  85. package/src/gep/analyzer.js +0 -35
  86. package/src/gep/antiAbuseTelemetry.js +0 -1
  87. package/src/gep/assetCallLog.js +0 -197
  88. package/src/gep/assetStore.js +0 -723
  89. package/src/gep/assets.js +0 -36
  90. package/src/gep/autoDistillConv.js +0 -1
  91. package/src/gep/autoDistillLlm.js +0 -1
  92. package/src/gep/bridge.js +0 -138
  93. package/src/gep/candidateEval.js +0 -1
  94. package/src/gep/candidates.js +0 -1
  95. package/src/gep/claimNudge.js +0 -121
  96. package/src/gep/cliContracts.js +0 -1190
  97. package/src/gep/contentHash.js +0 -1
  98. package/src/gep/conversationDistiller.js +0 -1
  99. package/src/gep/conversationSniffer.js +0 -1
  100. package/src/gep/crypto.js +0 -1
  101. package/src/gep/curriculum.js +0 -1
  102. package/src/gep/deviceId.js +0 -1
  103. package/src/gep/directoryClient.js +0 -115
  104. package/src/gep/envFingerprint.js +0 -1
  105. package/src/gep/epigenetics.js +0 -1
  106. package/src/gep/execBridge.js +0 -1
  107. package/src/gep/executionTrace.js +0 -291
  108. package/src/gep/explore.js +0 -1
  109. package/src/gep/featureFlags.js +0 -121
  110. package/src/gep/gitOps.js +0 -265
  111. package/src/gep/hash.js +0 -1
  112. package/src/gep/hostErrorClassifier.js +0 -34
  113. package/src/gep/hubFetch.js +0 -1
  114. package/src/gep/hubReview.js +0 -1
  115. package/src/gep/hubSearch.js +0 -1
  116. package/src/gep/hubVerify.js +0 -1
  117. package/src/gep/idleScheduler.js +0 -400
  118. package/src/gep/issueReporter.js +0 -416
  119. package/src/gep/learningSignals.js +0 -1
  120. package/src/gep/llmReview.js +0 -92
  121. package/src/gep/localStateAwareness.js +0 -243
  122. package/src/gep/mailboxTransport.js +0 -119
  123. package/src/gep/memoryGraph.js +0 -1
  124. package/src/gep/memoryGraphAdapter.js +0 -1
  125. package/src/gep/mutation.js +0 -1
  126. package/src/gep/narrativeMemory.js +0 -1
  127. package/src/gep/oauthLogin.js +0 -181
  128. package/src/gep/openPRRegistry.js +0 -1
  129. package/src/gep/paths.js +0 -522
  130. package/src/gep/personality.js +0 -1
  131. package/src/gep/policyCheck.js +0 -1
  132. package/src/gep/portable.js +0 -103
  133. package/src/gep/privacyClient.js +0 -235
  134. package/src/gep/prompt.js +0 -1
  135. package/src/gep/questionGenerator.js +0 -518
  136. package/src/gep/recallInject.js +0 -1
  137. package/src/gep/recallVerifier.js +0 -1
  138. package/src/gep/reflection.js +0 -1
  139. package/src/gep/sanitize.js +0 -264
  140. package/src/gep/savingsCore.js +0 -1
  141. package/src/gep/schemas/capsule.js +0 -170
  142. package/src/gep/schemas/gene.js +0 -154
  143. package/src/gep/schemas/index.js +0 -8
  144. package/src/gep/schemas/protocol.js +0 -51
  145. package/src/gep/schemas/task.js +0 -74
  146. package/src/gep/selector.js +0 -1
  147. package/src/gep/selfPR.js +0 -469
  148. package/src/gep/signals.js +0 -776
  149. package/src/gep/skill2gep.js +0 -1056
  150. package/src/gep/skill2gepAudit.js +0 -303
  151. package/src/gep/skill2recipes.js +0 -511
  152. package/src/gep/skillDistiller.js +0 -1
  153. package/src/gep/skillPublisher.js +0 -358
  154. package/src/gep/solidify.js +0 -1
  155. package/src/gep/strategy.js +0 -1
  156. package/src/gep/taskReceiver.js +0 -575
  157. package/src/gep/tokenSavings.js +0 -1
  158. package/src/gep/trajectoryExport.js +0 -1
  159. package/src/gep/validationReport.js +0 -55
  160. package/src/gep/validator/index.js +0 -411
  161. package/src/gep/validator/reporter.js +0 -210
  162. package/src/gep/validator/sandboxExecutor.js +0 -480
  163. package/src/gep/validator/stakeBootstrap.js +0 -357
  164. package/src/gep/workspaceKeychain.js +0 -1
  165. package/src/ops/cleanup.js +0 -80
  166. package/src/ops/commentary.js +0 -60
  167. package/src/ops/health_check.js +0 -104
  168. package/src/ops/index.js +0 -11
  169. package/src/ops/innovation.js +0 -67
  170. package/src/ops/lifecycle.js +0 -798
  171. package/src/ops/self_repair.js +0 -76
  172. package/src/ops/skills_monitor.js +0 -147
  173. package/src/ops/trigger.js +0 -33
  174. package/src/proxy/clientSettings.js +0 -405
  175. package/src/proxy/envelope.js +0 -59
  176. package/src/proxy/extensions/dmHandler.js +0 -45
  177. package/src/proxy/extensions/sessionHandler.js +0 -141
  178. package/src/proxy/extensions/skillUpdater.js +0 -64
  179. package/src/proxy/extensions/traceControl.js +0 -1
  180. package/src/proxy/index.js +0 -1395
  181. package/src/proxy/inject.js +0 -1
  182. package/src/proxy/lifecycle/manager.js +0 -1568
  183. package/src/proxy/mailbox/state.js +0 -207
  184. package/src/proxy/mailbox/store.js +0 -590
  185. package/src/proxy/router/cache_passthrough.js +0 -26
  186. package/src/proxy/router/features.js +0 -84
  187. package/src/proxy/router/gemini_route.js +0 -154
  188. package/src/proxy/router/messages_route.js +0 -535
  189. package/src/proxy/router/model_router.js +0 -113
  190. package/src/proxy/router/models_route.js +0 -52
  191. package/src/proxy/router/ollama_route.js +0 -103
  192. package/src/proxy/router/responses_route.js +0 -170
  193. package/src/proxy/router/vertex_route.js +0 -110
  194. package/src/proxy/server/http.js +0 -363
  195. package/src/proxy/server/routes.js +0 -558
  196. package/src/proxy/server/settings.js +0 -115
  197. package/src/proxy/sync/engine.js +0 -179
  198. package/src/proxy/sync/inbound.js +0 -211
  199. package/src/proxy/sync/outbound.js +0 -320
  200. package/src/proxy/task/monitor.js +0 -131
  201. package/src/proxy/trace/extractor.js +0 -1
  202. package/src/proxy/trace/usage.js +0 -1
  203. package/src/webui/client/clientJs/assets.js +0 -111
  204. package/src/webui/client/clientJs/bootstrap.js +0 -92
  205. package/src/webui/client/clientJs/common.js +0 -77
  206. package/src/webui/client/clientJs/i18n.js +0 -366
  207. package/src/webui/client/clientJs/index.js +0 -35
  208. package/src/webui/client/clientJs/interactions.js +0 -351
  209. package/src/webui/client/clientJs/overview.js +0 -152
  210. package/src/webui/client/clientJs/personality.js +0 -285
  211. package/src/webui/client/clientJs/pipelines.js +0 -330
  212. package/src/webui/client/indexHtml.js +0 -221
  213. package/src/webui/client/static.js +0 -23
  214. package/src/webui/client/stylesCss.js +0 -639
  215. package/src/webui/client/vendor/README.md +0 -15
  216. package/src/webui/client/vendor/echarts.min.js +0 -45
  217. package/src/webui/index.js +0 -14
  218. package/src/webui/observer/assets.js +0 -146
  219. package/src/webui/observer/index.js +0 -37
  220. package/src/webui/observer/interactions.js +0 -127
  221. package/src/webui/observer/jsonl.js +0 -75
  222. package/src/webui/observer/paths.js +0 -46
  223. package/src/webui/observer/personality.js +0 -43
  224. package/src/webui/observer/pipelineEvents.js +0 -58
  225. package/src/webui/observer/redact.js +0 -63
  226. package/src/webui/observer/runs.js +0 -356
  227. package/src/webui/observer/safety.js +0 -57
  228. package/src/webui/observer/skills.js +0 -70
  229. package/src/webui/observer/status.js +0 -71
  230. package/src/webui/server/http.js +0 -138
  231. package/src/webui/server/routes.js +0 -41
@@ -1,587 +0,0 @@
1
- #!/usr/bin/env node
2
- // evolver-session-start.js
3
- // Reads recent evolution memory and injects it as context for the agent session.
4
- // Input: stdin JSON (session context). Output: stdout JSON with agent_message.
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const os = require('os');
9
-
10
- const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace } = require('./_runtimePaths');
11
- const { filterRelevantOutcomes } = require('./_memoryFiltering');
12
- // Top-level on purpose: a missing sibling helper in the deployed hooks dir
13
- // must fail LOUD at load time (the #547 failure mode), not vanish inside
14
- // _maybeRestartDaemon's catch-all and silently disable daemon auto-restart.
15
- const lockPaths = require('./_lockPaths');
16
-
17
- function _readJson(file) {
18
- try {
19
- if (!file || !fs.existsSync(file)) return null;
20
- return JSON.parse(fs.readFileSync(file, 'utf8'));
21
- } catch (_) {
22
- return null;
23
- }
24
- }
25
-
26
- function _isLoopbackProxyUrl(value) {
27
- const raw = String(value || '').trim();
28
- if (!raw) return false;
29
- try {
30
- const u = new URL(raw);
31
- const host = u.hostname.toLowerCase();
32
- return u.protocol === 'http:' && (host === '127.0.0.1' || host === 'localhost' || host === '::1' || host === '[::1]');
33
- } catch (_) {
34
- return false;
35
- }
36
- }
37
-
38
- function _stripTomlComment(line) {
39
- let out = '';
40
- let quote = null;
41
- let escaped = false;
42
- for (const ch of String(line || '')) {
43
- if (escaped) {
44
- out += ch;
45
- escaped = false;
46
- continue;
47
- }
48
- if (ch === '\\' && quote === '"') {
49
- out += ch;
50
- escaped = true;
51
- continue;
52
- }
53
- if ((ch === '"' || ch === "'") && !quote) {
54
- quote = ch;
55
- out += ch;
56
- continue;
57
- }
58
- if (ch === quote) {
59
- quote = null;
60
- out += ch;
61
- continue;
62
- }
63
- if (ch === '#' && !quote) break;
64
- out += ch;
65
- }
66
- return out.trim();
67
- }
68
-
69
- function _tomlStringValue(value) {
70
- const raw = _stripTomlComment(value);
71
- const match = raw.match(/^(['"])([\s\S]*)\1$/);
72
- return match ? match[2] : raw.trim();
73
- }
74
-
75
- function _codexConfigPath() {
76
- if (process.env.CODEX_CONFIG_FILE || process.env.EVOMAP_CODEX_CONFIG_FILE) {
77
- return process.env.CODEX_CONFIG_FILE || process.env.EVOMAP_CODEX_CONFIG_FILE;
78
- }
79
- const home = process.env.HOME || os.homedir();
80
- return home ? path.join(home, '.codex', 'config.toml') : null;
81
- }
82
-
83
- function _codexConfigExpectsProxy() {
84
- const file = _codexConfigPath();
85
- if (!file || !fs.existsSync(file)) return false;
86
- let selectedProvider = null;
87
- let section = '';
88
- const providerUrls = {};
89
- try {
90
- const content = fs.readFileSync(file, 'utf8');
91
- for (const line of content.split(/\r?\n/)) {
92
- const clean = _stripTomlComment(line);
93
- if (!clean) continue;
94
- const sectionMatch = clean.match(/^\[([^\]]+)\]$/);
95
- if (sectionMatch) {
96
- section = sectionMatch[1].trim();
97
- continue;
98
- }
99
- const kv = clean.match(/^([A-Za-z0-9_.-]+)\s*=\s*([\s\S]+)$/);
100
- if (!kv) continue;
101
- const key = kv[1].trim();
102
- const value = _tomlStringValue(kv[2]);
103
- if (!section && key === 'model_provider') {
104
- selectedProvider = value;
105
- continue;
106
- }
107
- const providerMatch = section.match(/^model_providers\.([A-Za-z0-9_.-]+)$/);
108
- if (providerMatch && key === 'base_url') {
109
- providerUrls[providerMatch[1]] = value;
110
- continue;
111
- }
112
- if (!section && key === 'base_url' && _isLoopbackProxyUrl(value)) return true;
113
- }
114
- } catch {
115
- return false;
116
- }
117
- if (selectedProvider && _isLoopbackProxyUrl(providerUrls[selectedProvider])) return true;
118
- return Object.keys(providerUrls).some(name =>
119
- /(?:evomap|proxy)/i.test(name) && _isLoopbackProxyUrl(providerUrls[name])
120
- );
121
- }
122
-
123
- function _proxyExpected() {
124
- if (String(process.env.EVOMAP_PROXY || '').trim() === '1') return true;
125
- if (String(process.env.A2A_TRANSPORT || '').trim().toLowerCase() === 'mailbox') return true;
126
- if (_isLoopbackProxyUrl(process.env.EVOMAP_PROXY_URL) || _isLoopbackProxyUrl(process.env.ANTHROPIC_BASE_URL)) return true;
127
- if (_codexConfigExpectsProxy()) return true;
128
-
129
- const home = process.env.HOME || os.homedir();
130
- const settingsFile = process.env.CLAUDE_SETTINGS_FILE || process.env.EVOMAP_CLAUDE_SETTINGS_FILE ||
131
- (home ? path.join(home, '.claude', 'settings.json') : null);
132
- const settings = _readJson(settingsFile);
133
- const cfg = settings && settings.env;
134
- return !!(cfg && (
135
- _isLoopbackProxyUrl(cfg.EVOMAP_PROXY_URL) ||
136
- (String(cfg.EVOMAP_PROXY_AUTO_INJECTED || '') === '1' && _isLoopbackProxyUrl(cfg.ANTHROPIC_BASE_URL))
137
- ));
138
- }
139
-
140
- function _proxyReachable(url, token) {
141
- if (!_isLoopbackProxyUrl(url) || !token) return false;
142
- try {
143
- const { execFileSync } = require('child_process');
144
- execFileSync(process.execPath, ['-e', `
145
- const fs = require('fs');
146
- const http = require('http');
147
- const url = process.argv[1].replace(/\\/+$/, '') + '/proxy/status';
148
- const token = fs.readFileSync(0, 'utf8').trim();
149
- if (!token) process.exit(1);
150
- const req = http.get(url, { headers: { Authorization: 'Bearer ' + token } }, (res) => {
151
- let body = '';
152
- res.setEncoding('utf8');
153
- res.on('data', (chunk) => {
154
- body += chunk;
155
- if (body.length > 1024 * 1024) req.destroy(new Error('response too large'));
156
- });
157
- res.on('end', () => {
158
- if (res.statusCode < 200 || res.statusCode >= 300) process.exit(1);
159
- let parsed;
160
- try { parsed = JSON.parse(body); } catch (_) { process.exit(1); }
161
- if (parsed && parsed.status === 'running' && (parsed.proxy_protocol_version || parsed.schema_version || parsed.node_id != null)) {
162
- process.exit(0);
163
- }
164
- process.exit(1);
165
- });
166
- });
167
- req.setTimeout(700, () => req.destroy(new Error('timeout')));
168
- req.on('error', () => process.exit(1));
169
- `, url], { input: String(token), stdio: ['pipe', 'ignore', 'ignore'], timeout: 1200, windowsHide: true });
170
- return true;
171
- } catch (_) {
172
- return false;
173
- }
174
- }
175
-
176
- function _proxyHealthyIfExpected() {
177
- if (!_proxyExpected()) return true;
178
- const dir = process.env.EVOLVER_SETTINGS_DIR || path.join(os.homedir(), '.evolver');
179
- const settings = _readJson(path.join(dir, 'settings.json'));
180
- const proxy = settings && settings.proxy;
181
- if (!proxy || !proxy.url) return false;
182
- if (proxy.pid) {
183
- try { process.kill(proxy.pid, 0); } catch (e) {
184
- if (!(e && e.code === 'EPERM')) return false;
185
- }
186
- }
187
- if (!proxy.token) return false;
188
- return _proxyReachable(proxy.url, proxy.token);
189
- }
190
-
191
- // Auto-restart guard: if the evolver daemon is not running when a new agent
192
- // session starts, attempt a background restart. This covers the "idle-death"
193
- // scenario: the user closed the machine (macOS sleep), the process died due to
194
- // event-loop exhaustion or OOM, and now the next agent session finds it gone.
195
- // We delegate to lifecycle.js restart() which is idempotent (no-op if already
196
- // running), detached (does not block session startup), and captures output in
197
- // the existing evolver log.
198
- //
199
- // Guard-rails:
200
- // - Only runs when EVOLVER_SESSION_AUTO_RESTART is not "0" or "false".
201
- // - Skips gracefully if lifecycle.js cannot be found (non-daemon setups,
202
- // npx one-shot mode, etc.).
203
- // - Execution errors are swallowed: this must never cause session-start to
204
- // error out or delay the LLM context injection.
205
- function _maybeRestartDaemon(evolverRoot) {
206
- try {
207
- var autoRestart = String(process.env.EVOLVER_SESSION_AUTO_RESTART || '1').toLowerCase().trim();
208
- if (autoRestart === '0' || autoRestart === 'false') return;
209
-
210
- var lifecyclePath = evolverRoot
211
- ? path.join(evolverRoot, 'src', 'ops', 'lifecycle.js')
212
- : null;
213
- if (!lifecyclePath || !fs.existsSync(lifecyclePath)) return;
214
-
215
- // Check if daemon is running by looking for the PID file / lock file.
216
- // Lock resolution + lease staleness live in ./_lockPaths (issue #176) —
217
- // the same module index.js uses, so the hook can never drift from the
218
- // daemon again. It is fs/os/path-only, keeping index.js out of the
219
- // hook's require graph (R12), and ships in hookAdapter.js's copy list
220
- // for the deployed `.claude/hooks/` layout (PR #163).
221
- var lockFile = lockPaths.getLockFilePath();
222
- var daemonRunning = false;
223
- try {
224
- if (fs.existsSync(lockFile)) {
225
- var raw = fs.readFileSync(lockFile, 'utf8').trim();
226
- var payload = raw && raw[0] === '{' ? JSON.parse(raw) : { pid: parseInt(raw, 10) };
227
- if (payload && payload.pid > 0) {
228
- // R1: PID-reuse defense. process.kill(pid, 0) only proves SOME
229
- // process owns that PID -- after macOS sleep / OOM, the kernel may
230
- // have reused the slain daemon's PID for an unrelated process.
231
- try { process.kill(payload.pid, 0); daemonRunning = true; } catch (e) {
232
- // EPERM = process exists but owned by a different user; still a live daemon.
233
- if (e && e.code === 'EPERM') daemonRunning = true;
234
- }
235
- // Lease staleness overrides kill(0)=alive: a lease-aware daemon
236
- // refreshes the lock mtime on a timer, so an expired lease means
237
- // dead/wedged regardless of kill(0). Pre-lease locks are never
238
- // judged stale by mtime (lockIsStaleByLease handles both).
239
- if (daemonRunning && lockPaths.lockIsStaleByLease(lockFile, payload)) {
240
- daemonRunning = false;
241
- }
242
- }
243
- }
244
- } catch (_) { /* lock file unreadable or absent: assume not running */ }
245
-
246
- if (daemonRunning && _proxyHealthyIfExpected()) return; // already alive, nothing to do
247
-
248
- // Daemon appears dead. Spawn lifecycle.js start in the background so
249
- // this session-start script exits immediately (< 50 ms) and does not
250
- // block the LLM from getting context.
251
- var { spawn } = require('child_process');
252
- var child = spawn(
253
- process.execPath,
254
- [lifecyclePath, 'start'],
255
- {
256
- detached: true,
257
- stdio: 'ignore',
258
- cwd: evolverRoot,
259
- env: Object.assign({}, process.env),
260
- windowsHide: true,
261
- }
262
- );
263
- child.unref();
264
- // Best-effort: log a single-line note to stderr so the session transcript
265
- // shows that a restart was attempted, without affecting stdout JSON output.
266
- try {
267
- process.stderr.write('[evolver-session-start] Daemon was not running; attempted background restart (PID ' + child.pid + ').\n');
268
- } catch (_) {}
269
- } catch (_) {
270
- // Never let this helper block or crash the session-start script.
271
- }
272
- }
273
-
274
- // One-line notice shown (throttled) when the workspace is not a git repo.
275
- // Evolver derives every outcome from the git diff, so in a non-git folder the
276
- // session-end hook records nothing — silently, unless we say so here. We surface
277
- // it in session-start's additionalContext (injected as opening context, which
278
- // does NOT trigger an extra inference round, unlike a stop-hook systemMessage).
279
- const NON_GIT_NOTICE =
280
- '[Evolver] This folder is not a git repository, so evolution memory is inactive ' +
281
- '(outcomes are derived from git diffs). Run `git init` here, or open a git project, ' +
282
- 'to enable recall and recording.';
283
- const NON_GIT_NOTICE_TTL_MS = 30 * 60 * 1000; // once per 30 min per folder
284
-
285
- // Return up to `n` of the current workspace's most-recent entries, in
286
- // chronological (oldest-first) order.
287
- //
288
- // Why scan from the end: a plain tail-N-then-filter read would let outcomes
289
- // from other projects (which share the user-level fallback graph on npm-global
290
- // installs) crowd this workspace's entries out of the window — we must scope
291
- // to the workspace BEFORE trimming. But parsing the ENTIRE file to do that is
292
- // wasteful: the graph can reach ~100 MB before rotation, and JSON-parsing every
293
- // line on each session start is real CPU/memory cost (Bugbot PR #555 round-3).
294
- //
295
- // So we read the file (cheap; the previous readLastN read it whole too) but
296
- // JSON-parse lines lazily from the newest end, keeping only workspace matches,
297
- // and stop as soon as we have `n`. Parse count is bounded by where this
298
- // workspace's n-th-most-recent entry sits, not by total file size.
299
- function readRecentWorkspaceEntries(filePath, currentId, currentDir, n) {
300
- let lines;
301
- try {
302
- lines = fs.readFileSync(filePath, 'utf8').trim().split('\n');
303
- } catch { return []; }
304
- const out = [];
305
- for (let i = lines.length - 1; i >= 0 && out.length < n; i--) {
306
- const line = lines[i];
307
- if (!line) continue;
308
- let entry;
309
- try { entry = JSON.parse(line); } catch { continue; }
310
- if (belongsToWorkspace(entry, currentId, currentDir)) out.push(entry);
311
- }
312
- return out.reverse(); // newest-collected-first -> chronological
313
- }
314
-
315
- // Does this memory-graph entry belong to the current workspace?
316
- //
317
- // The session-end writer stamps two tags: `workspace_id` (forge-resistant,
318
- // preferred) and `cwd` (backward-compat). We scope reads so that one project
319
- // never sees another's outcomes through the shared user-level fallback graph
320
- // (~/.evolver/memory/evolution/memory_graph.jsonl) — the cross-project
321
- // disclosure / prompt-injection surface Bugbot flagged on the writer side
322
- // (PR #105 round-2), which the reader never enforced until now.
323
- //
324
- // Rules, in order:
325
- // - currentId known + entry.workspace_id present -> must match exactly.
326
- // - currentId unknown OR entry has neither tag (pre-hardening / Hub-sourced
327
- // entries) -> do NOT exclude; falling back to "show it" preserves prior
328
- // behavior and avoids hiding all memory when ids can't be resolved.
329
- // - As a softer fallback, when the entry has no workspace_id but does carry a
330
- // cwd, match that against the current project dir.
331
- function belongsToWorkspace(entry, currentId, currentDir) {
332
- if (entry && typeof entry.workspace_id === 'string' && entry.workspace_id) {
333
- if (currentId) return entry.workspace_id === currentId;
334
- return true; // can't compare — don't hide it
335
- }
336
- if (entry && typeof entry.cwd === 'string' && entry.cwd) {
337
- if (currentDir) return entry.cwd === currentDir;
338
- return true;
339
- }
340
- return true; // untagged (legacy / Hub) — never excluded
341
- }
342
-
343
- function formatOutcome(entry) {
344
- const status = entry.outcome ? entry.outcome.status : 'unknown';
345
- const score = entry.outcome && entry.outcome.score != null ? entry.outcome.score : '?';
346
- const note = entry.outcome && entry.outcome.note ? entry.outcome.note : '';
347
- const signals = Array.isArray(entry.signals) ? entry.signals.slice(0, 3).join(', ') : '';
348
- const ts = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
349
- const icon = status === 'success' ? '+' : status === 'failed' ? '-' : '?';
350
- return `[${icon}] ${ts} score=${score} signals=[${signals}] ${note}`.slice(0, 200);
351
- }
352
-
353
- // Dedup guard: on platforms like Kiro, the sessionStart-equivalent event
354
- // (`promptSubmit`) fires on every user message in a session. Without this
355
- // guard, recent memory would be re-injected on every prompt. We key the
356
- // dedup on (platform, cwd) with a short TTL so a fresh agent session within
357
- // the same workspace still gets the injection, but mid-session prompts do
358
- // not. Cursor/Claude Code/Codex have true sessionStart events and should
359
- // bypass this check (controlled by EVOLVER_SESSION_START_DEDUP env var,
360
- // which the Kiro adapter sets on the hook command line implicitly via the
361
- // runtime environment, and other adapters leave unset).
362
- function getDedupStatePath() {
363
- const dir = process.env.EVOLVER_SESSION_STATE_DIR
364
- || path.join(os.homedir(), '.evolver');
365
- try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
366
- return path.join(dir, 'session-start-state.json');
367
- }
368
-
369
- function getNoticeStatePath() {
370
- const dir = process.env.EVOLVER_SESSION_STATE_DIR
371
- || path.join(os.homedir(), '.evolver');
372
- try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
373
- return path.join(dir, 'session-start-notice-state.json');
374
- }
375
-
376
- // Resolve the evolver-marked-sessions registry path. This is the v1 "evolver
377
- // actively marked this session" ledger: only sessions whose session-start hook
378
- // actually fired (i.e. sessions evolver participated in, after hook install)
379
- // land here. The trajectory exporter reads it to gate which runtime-session
380
- // transcripts get collected (strict by default). Lives alongside the proxy
381
- // trace dir so both collection ledgers share one home. The env override keeps
382
- // tests hermetic and lets an operator relocate the ledger.
383
- function getMarkedSessionsPath() {
384
- if (process.env.EVOLVER_MARKED_SESSIONS_FILE) return process.env.EVOLVER_MARKED_SESSIONS_FILE;
385
- const dir = process.env.EVOLVER_SETTINGS_DIR
386
- || process.env.EVOLVER_SESSION_STATE_DIR
387
- || path.join(os.homedir(), '.evolver');
388
- return path.join(dir, 'marked-sessions.jsonl');
389
- }
390
-
391
- // Pull the tool's session_id out of the hook's stdin payload. Claude Code,
392
- // Codex, and Cursor all pass `session_id` (Claude Code / Cursor) on the
393
- // SessionStart stdin JSON; some shapes use `sessionId`. Returns '' when absent
394
- // so callers can skip the registry write without erroring (Kiro's promptSubmit
395
- // and hosts that pass no stdin simply don't mark — fail-open by design).
396
- function _extractHookSessionId(input) {
397
- if (!input || typeof input !== 'object') return '';
398
- const raw = input.session_id ?? input.sessionId ?? input.sessionID;
399
- return typeof raw === 'string' ? raw.trim() : '';
400
- }
401
-
402
- // Append one mark record to the registry. Best-effort and idempotent enough for
403
- // the exporter's needs: the exporter dedupes into a Set, so a session marked on
404
- // every prompt (Kiro) just appends duplicate lines that collapse on read. We do
405
- // NOT read-modify-write to dedupe here — that would race across concurrent
406
- // sessions; append-only is safe and the file is bounded by session count.
407
- function recordMarkedSession(sessionId, info = {}) {
408
- const sid = String(sessionId || '').trim();
409
- if (!sid) return false;
410
- const file = getMarkedSessionsPath();
411
- try {
412
- fs.mkdirSync(path.dirname(file), { recursive: true });
413
- const record = {
414
- session_id: sid,
415
- ...(info.cwd ? { cwd: String(info.cwd) } : {}),
416
- ...(info.source ? { source: String(info.source) } : {}),
417
- marked_at: new Date().toISOString(),
418
- };
419
- fs.appendFileSync(file, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
420
- return true;
421
- } catch (_) {
422
- // Never let a registry write failure break session-start: the context
423
- // injection (stdout JSON) must always be emitted.
424
- return false;
425
- }
426
- }
427
-
428
- // TTL throttle keyed by an arbitrary string. Returns true if `key` fired within
429
- // the last `ttlMs` (caller should suppress); otherwise records "now" for `key`
430
- // and returns false. Best-effort: a state read/write failure just means no
431
- // throttling (fail open). Entries older than 24h are pruned on write.
432
- function throttled(key, ttlMs, statePath) {
433
- let state = {};
434
- try {
435
- if (fs.existsSync(statePath)) {
436
- state = JSON.parse(fs.readFileSync(statePath, 'utf8')) || {};
437
- }
438
- } catch { state = {}; }
439
-
440
- const now = Date.now();
441
- const last = state[key];
442
- if (typeof last === 'number' && now - last < ttlMs) return true;
443
-
444
- state[key] = now;
445
- try {
446
- for (const k of Object.keys(state)) {
447
- if (typeof state[k] !== 'number' || now - state[k] > 24 * 60 * 60 * 1000) {
448
- delete state[k];
449
- }
450
- }
451
- const tmp = statePath + '.tmp';
452
- fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
453
- fs.renameSync(tmp, statePath);
454
- } catch { /* best-effort */ }
455
- return false;
456
- }
457
-
458
- function shouldSkipInjection() {
459
- // Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
460
- // we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
461
- // stdin. The stdin is drained in main(), so we rely on env flag here.
462
- const dedupEnabled = String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === '1'
463
- || String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === 'true';
464
- if (!dedupEnabled) return false;
465
-
466
- const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
467
- return throttled(process.cwd(), ttlMs, getDedupStatePath());
468
- }
469
-
470
- // Drain the hook stdin (session context JSON) before running, so we can read the
471
- // tool's session_id and mark the session in the registry. The host passes the
472
- // SessionStart payload on stdin; we bound the wait with a short watchdog and
473
- // fail-open (mark nothing, still emit context) if stdin never closes or isn't a
474
- // pipe. This mirrors the stdin-draining pattern in evolver-task-recall.js /
475
- // evolver-session-end.js.
476
- function main() {
477
- let done = false;
478
- let buf = '';
479
- const finishWithInput = (input) => {
480
- if (done) return;
481
- done = true;
482
- try {
483
- const sessionId = _extractHookSessionId(input);
484
- if (sessionId) {
485
- recordMarkedSession(sessionId, {
486
- cwd: resolveProjectDir(),
487
- source: String(process.env.EVOLVER_SESSION_SOURCE || (input && input.source) || '').trim() || undefined,
488
- });
489
- }
490
- } catch (_) { /* marking is best-effort; never block injection */ }
491
- runInjection();
492
- };
493
-
494
- // Watchdog: if stdin never ends (host passed no pipe, or hangs), proceed
495
- // without a session_id rather than stalling the agent's session start.
496
- const watchdog = setTimeout(() => finishWithInput(null), 1500);
497
- try {
498
- process.stdin.setEncoding('utf8');
499
- } catch (_) { /* some hosts pass no stdin */ }
500
- process.stdin.on('data', (c) => { buf += c; });
501
- process.stdin.on('error', () => { clearTimeout(watchdog); finishWithInput(null); });
502
- process.stdin.on('end', () => {
503
- clearTimeout(watchdog);
504
- let input = null;
505
- try { input = buf.trim() ? JSON.parse(buf) : null; } catch (_) { input = null; }
506
- finishWithInput(input);
507
- });
508
- // Nudge a resume so a paused stdin stream flushes its data/end events; the
509
- // watchdog covers the case where neither ever arrives (no pipe attached).
510
- try { process.stdin.resume(); } catch (_) { /* ignore */ }
511
- }
512
-
513
- function runInjection() {
514
- if (shouldSkipInjection()) {
515
- process.stdout.write(JSON.stringify({}));
516
- return;
517
- }
518
-
519
- const currentDir = resolveProjectDir();
520
-
521
- // Non-git notice: evolver records nothing in a non-git folder (outcomes come
522
- // from git diffs), so tell the user — once per folder per TTL — instead of
523
- // failing silently. Emitted regardless of whether any memory exists below.
524
- const parts = [];
525
- if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS, getNoticeStatePath())) {
526
- parts.push(NON_GIT_NOTICE);
527
- }
528
-
529
- const evolverRoot = findEvolverRoot();
530
-
531
- // Attempt to restart the daemon in the background if it has died since the
532
- // last session (idle-death / macOS sleep / OOM). Fire-and-forget: errors are
533
- // swallowed and this never delays the JSON output below.
534
- _maybeRestartDaemon(evolverRoot);
535
-
536
- const graphPath = findMemoryGraph(evolverRoot);
537
-
538
- // Scope to the current workspace BEFORE trimming to the most-recent window,
539
- // so other projects sharing the user-level fallback graph can't crowd this
540
- // workspace's outcomes out of view. When the workspace id can't be resolved,
541
- // belongsToWorkspace() falls back to "show it" — no regression vs. the old
542
- // unscoped behavior.
543
- if (graphPath) {
544
- const currentId = resolveWorkspaceId(evolverRoot, currentDir);
545
- const recent = readRecentWorkspaceEntries(graphPath, currentId, currentDir, 5);
546
- const filtered = filterRelevantOutcomes(recent);
547
- if (filtered.length > 0) {
548
- const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
549
- const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
550
- parts.push([
551
- `[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
552
- ...filtered.map(formatOutcome),
553
- '',
554
- 'Use successful approaches. Avoid repeating failed patterns.',
555
- ].join('\n'));
556
- }
557
- }
558
-
559
- if (parts.length === 0) {
560
- process.stdout.write(JSON.stringify({}));
561
- return;
562
- }
563
-
564
- const out = parts.join('\n\n');
565
- process.stdout.write(JSON.stringify({
566
- agent_message: out,
567
- additionalContext: out,
568
- }));
569
- }
570
-
571
- // Run as a hook when invoked directly; expose pure helpers for unit tests when
572
- // required as a module. Guarding on require.main keeps the direct-execution
573
- // behavior (the hosts run `node evolver-session-start.js`) unchanged.
574
- if (require.main === module) {
575
- main();
576
- } else {
577
- module.exports = {
578
- belongsToWorkspace,
579
- _isLoopbackProxyUrl,
580
- _proxyExpected,
581
- _proxyReachable,
582
- _proxyHealthyIfExpected,
583
- _extractHookSessionId,
584
- getMarkedSessionsPath,
585
- recordMarkedSession,
586
- };
587
- }
@@ -1,98 +0,0 @@
1
- #!/usr/bin/env node
2
- // evolver-signal-detect.js
3
- // Lightweight signal detection on file edit events.
4
- // Input: stdin JSON (edit event). Output: stdout JSON with additional_context.
5
-
6
- const SIGNAL_KEYWORDS = {
7
- perf_bottleneck: ['timeout', 'slow', 'latency', 'bottleneck', 'oom', 'out of memory', 'performance'],
8
- capability_gap: ['not supported', 'unsupported', 'not implemented', 'missing feature', 'not available'],
9
- log_error: ['error:', 'exception:', 'typeerror', 'referenceerror', 'syntaxerror', 'failed'],
10
- user_feature_request: ['add feature', 'implement', 'new function', 'new module', 'please add'],
11
- recurring_error: ['same error', 'still failing', 'not fixed', 'keeps failing', 'repeatedly'],
12
- deployment_issue: ['deploy failed', 'build failed', 'ci failed', 'pipeline', 'rollback'],
13
- test_failure: ['test failed', 'test failure', 'assertion', 'expect(', 'assert.'],
14
- };
15
-
16
- function stratifyContent(text) {
17
- // Separate code/comments/documents to avoid false positives
18
- const lines = text.split('\n');
19
- const documentText = [];
20
-
21
- for (const line of lines) {
22
- const trimmed = line.trim();
23
- // Skip lines that are comments or code structure (not document text)
24
- if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*') ||
25
- trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('}') ||
26
- trimmed.startsWith(']') || trimmed.startsWith('/*')) {
27
- continue;
28
- }
29
- documentText.push(line);
30
- }
31
-
32
- return documentText.join('\n');
33
- }
34
-
35
- function detectSignals(text) {
36
- if (!text || typeof text !== 'string') return [];
37
-
38
- // Apply stratification to reduce false positives from code/comments
39
- const stratified = stratifyContent(text);
40
- const lower = stratified.toLowerCase();
41
-
42
- const found = [];
43
- for (const [signal, keywords] of Object.entries(SIGNAL_KEYWORDS)) {
44
- for (const kw of keywords) {
45
- if (lower.includes(kw)) {
46
- found.push(signal);
47
- break;
48
- }
49
- }
50
- }
51
- return [...new Set(found)];
52
- }
53
-
54
- function main() {
55
- let inputData = '';
56
- let handled = false;
57
- process.stdin.setEncoding('utf8');
58
- process.stdin.on('data', chunk => { inputData += chunk; });
59
- process.stdin.on('end', () => {
60
- if (handled) return;
61
- handled = true;
62
- try {
63
- const input = inputData.trim() ? JSON.parse(inputData) : {};
64
- // Claude Code's PostToolUse payload nests tool args under tool_input.
65
- // Older/raw shapes put them at the top level; support both.
66
- const ti = input.tool_input || {};
67
- const tr = input.tool_response || {};
68
- const content = ti.content || ti.new_string || ti.file_content
69
- || input.content || input.file_content || input.diff || '';
70
- const filePath = ti.file_path || tr.filePath
71
- || input.path || input.file_path || '';
72
-
73
- const signals = detectSignals(content);
74
-
75
- if (signals.length === 0) {
76
- process.stdout.write(JSON.stringify({}));
77
- return;
78
- }
79
-
80
- const ctx = `[Evolution Signal] Detected: [${signals.join(', ')}] in ${filePath || 'edited file'}. Consider recording this outcome.`;
81
- process.stdout.write(JSON.stringify({
82
- additional_context: ctx,
83
- additionalContext: ctx,
84
- }));
85
- } catch {
86
- process.stdout.write(JSON.stringify({}));
87
- }
88
- });
89
-
90
- setTimeout(() => {
91
- if (handled) return;
92
- handled = true;
93
- process.stdout.write(JSON.stringify({}));
94
- process.exit(0);
95
- }, 1500);
96
- }
97
-
98
- main();