@evomap/evolver 1.90.0 → 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 -3670
  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/src/adapters/claudeCode.js +0 -194
  38. package/src/adapters/codex.js +0 -216
  39. package/src/adapters/cursor.js +0 -91
  40. package/src/adapters/hookAdapter.js +0 -469
  41. package/src/adapters/kiro.js +0 -195
  42. package/src/adapters/opencode.js +0 -326
  43. package/src/adapters/scripts/_lockPaths.js +0 -74
  44. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  45. package/src/adapters/scripts/_runtimePaths.js +0 -440
  46. package/src/adapters/scripts/evolver-session-end.js +0 -321
  47. package/src/adapters/scripts/evolver-session-start.js +0 -587
  48. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  49. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  50. package/src/atp/atpExecute.js +0 -283
  51. package/src/atp/atpTaskPickup.js +0 -233
  52. package/src/atp/autoBuyer.js +0 -382
  53. package/src/atp/autoDeliver.js +0 -215
  54. package/src/atp/cli.js +0 -354
  55. package/src/atp/cliAutobuyPrompt.js +0 -154
  56. package/src/atp/consumerAgent.js +0 -157
  57. package/src/atp/defaultHandler.js +0 -69
  58. package/src/atp/heartbeatSignalsHandler.js +0 -254
  59. package/src/atp/hubClient.js +0 -317
  60. package/src/atp/index.js +0 -38
  61. package/src/atp/merchantAgent.js +0 -118
  62. package/src/atp/protocol.js +0 -41
  63. package/src/atp/questionComposer.js +0 -133
  64. package/src/atp/serviceHelper.js +0 -92
  65. package/src/canary.js +0 -13
  66. package/src/config.js +0 -351
  67. package/src/evolve/guards.js +0 -1
  68. package/src/evolve/pipeline/collect.js +0 -1
  69. package/src/evolve/pipeline/dispatch.js +0 -1
  70. package/src/evolve/pipeline/enrich.js +0 -1
  71. package/src/evolve/pipeline/hub.js +0 -1
  72. package/src/evolve/pipeline/select.js +0 -1
  73. package/src/evolve/pipeline/signals.js +0 -1
  74. package/src/evolve/utils.js +0 -1
  75. package/src/evolve.js +0 -1
  76. package/src/experiment/agentRunner.js +0 -229
  77. package/src/experiment/cli.js +0 -159
  78. package/src/experiment/comparison.js +0 -233
  79. package/src/experiment/metrics.js +0 -75
  80. package/src/forceUpdate.js +0 -991
  81. package/src/gep/a2a.js +0 -173
  82. package/src/gep/a2aProtocol.js +0 -1
  83. package/src/gep/analyzer.js +0 -35
  84. package/src/gep/antiAbuseTelemetry.js +0 -1
  85. package/src/gep/assetCallLog.js +0 -197
  86. package/src/gep/assetStore.js +0 -723
  87. package/src/gep/assets.js +0 -36
  88. package/src/gep/autoDistillConv.js +0 -1
  89. package/src/gep/autoDistillLlm.js +0 -1
  90. package/src/gep/bridge.js +0 -138
  91. package/src/gep/candidateEval.js +0 -1
  92. package/src/gep/candidates.js +0 -1
  93. package/src/gep/claimNudge.js +0 -121
  94. package/src/gep/cliContracts.js +0 -1190
  95. package/src/gep/contentHash.js +0 -1
  96. package/src/gep/conversationDistiller.js +0 -1
  97. package/src/gep/conversationSniffer.js +0 -1
  98. package/src/gep/crypto.js +0 -1
  99. package/src/gep/curriculum.js +0 -1
  100. package/src/gep/deviceId.js +0 -1
  101. package/src/gep/directoryClient.js +0 -115
  102. package/src/gep/envFingerprint.js +0 -1
  103. package/src/gep/epigenetics.js +0 -1
  104. package/src/gep/execBridge.js +0 -1
  105. package/src/gep/executionTrace.js +0 -291
  106. package/src/gep/explore.js +0 -1
  107. package/src/gep/featureFlags.js +0 -121
  108. package/src/gep/gitOps.js +0 -265
  109. package/src/gep/hash.js +0 -1
  110. package/src/gep/hostErrorClassifier.js +0 -34
  111. package/src/gep/hubFetch.js +0 -1
  112. package/src/gep/hubReview.js +0 -1
  113. package/src/gep/hubSearch.js +0 -1
  114. package/src/gep/hubVerify.js +0 -1
  115. package/src/gep/idleScheduler.js +0 -400
  116. package/src/gep/issueReporter.js +0 -416
  117. package/src/gep/learningSignals.js +0 -1
  118. package/src/gep/llmReview.js +0 -92
  119. package/src/gep/localStateAwareness.js +0 -243
  120. package/src/gep/mailboxTransport.js +0 -119
  121. package/src/gep/memoryGraph.js +0 -1
  122. package/src/gep/memoryGraphAdapter.js +0 -1
  123. package/src/gep/mutation.js +0 -1
  124. package/src/gep/narrativeMemory.js +0 -1
  125. package/src/gep/oauthLogin.js +0 -181
  126. package/src/gep/openPRRegistry.js +0 -1
  127. package/src/gep/paths.js +0 -522
  128. package/src/gep/personality.js +0 -1
  129. package/src/gep/policyCheck.js +0 -1
  130. package/src/gep/portable.js +0 -103
  131. package/src/gep/privacyClient.js +0 -235
  132. package/src/gep/prompt.js +0 -1
  133. package/src/gep/questionGenerator.js +0 -518
  134. package/src/gep/recallInject.js +0 -1
  135. package/src/gep/recallVerifier.js +0 -1
  136. package/src/gep/reflection.js +0 -1
  137. package/src/gep/sanitize.js +0 -264
  138. package/src/gep/savingsCore.js +0 -1
  139. package/src/gep/schemas/capsule.js +0 -170
  140. package/src/gep/schemas/gene.js +0 -154
  141. package/src/gep/schemas/index.js +0 -8
  142. package/src/gep/schemas/protocol.js +0 -51
  143. package/src/gep/schemas/task.js +0 -74
  144. package/src/gep/selector.js +0 -1
  145. package/src/gep/selfPR.js +0 -469
  146. package/src/gep/signals.js +0 -776
  147. package/src/gep/skill2gep.js +0 -1056
  148. package/src/gep/skill2gepAudit.js +0 -303
  149. package/src/gep/skill2recipes.js +0 -511
  150. package/src/gep/skillDistiller.js +0 -1
  151. package/src/gep/skillPublisher.js +0 -358
  152. package/src/gep/solidify.js +0 -1
  153. package/src/gep/strategy.js +0 -1
  154. package/src/gep/taskReceiver.js +0 -575
  155. package/src/gep/tokenSavings.js +0 -1
  156. package/src/gep/trajectoryExport.js +0 -1
  157. package/src/gep/validationReport.js +0 -55
  158. package/src/gep/validator/index.js +0 -411
  159. package/src/gep/validator/reporter.js +0 -210
  160. package/src/gep/validator/sandboxExecutor.js +0 -480
  161. package/src/gep/validator/stakeBootstrap.js +0 -357
  162. package/src/gep/workspaceKeychain.js +0 -1
  163. package/src/ops/cleanup.js +0 -80
  164. package/src/ops/commentary.js +0 -60
  165. package/src/ops/health_check.js +0 -104
  166. package/src/ops/index.js +0 -11
  167. package/src/ops/innovation.js +0 -67
  168. package/src/ops/lifecycle.js +0 -798
  169. package/src/ops/self_repair.js +0 -76
  170. package/src/ops/skills_monitor.js +0 -147
  171. package/src/ops/trigger.js +0 -33
  172. package/src/proxy/clientSettings.js +0 -405
  173. package/src/proxy/envelope.js +0 -59
  174. package/src/proxy/extensions/dmHandler.js +0 -45
  175. package/src/proxy/extensions/sessionHandler.js +0 -141
  176. package/src/proxy/extensions/skillUpdater.js +0 -64
  177. package/src/proxy/extensions/traceControl.js +0 -1
  178. package/src/proxy/index.js +0 -1395
  179. package/src/proxy/inject.js +0 -1
  180. package/src/proxy/lifecycle/manager.js +0 -1568
  181. package/src/proxy/mailbox/state.js +0 -207
  182. package/src/proxy/mailbox/store.js +0 -602
  183. package/src/proxy/router/cache_passthrough.js +0 -26
  184. package/src/proxy/router/features.js +0 -84
  185. package/src/proxy/router/gemini_route.js +0 -154
  186. package/src/proxy/router/messages_route.js +0 -535
  187. package/src/proxy/router/model_router.js +0 -113
  188. package/src/proxy/router/models_route.js +0 -52
  189. package/src/proxy/router/ollama_route.js +0 -103
  190. package/src/proxy/router/responses_route.js +0 -170
  191. package/src/proxy/router/vertex_route.js +0 -110
  192. package/src/proxy/server/http.js +0 -363
  193. package/src/proxy/server/routes.js +0 -558
  194. package/src/proxy/server/settings.js +0 -115
  195. package/src/proxy/sync/engine.js +0 -179
  196. package/src/proxy/sync/inbound.js +0 -211
  197. package/src/proxy/sync/outbound.js +0 -361
  198. package/src/proxy/task/monitor.js +0 -131
  199. package/src/proxy/trace/extractor.js +0 -1
  200. package/src/proxy/trace/usage.js +0 -1
  201. package/src/solo/breaker.js +0 -25
  202. package/src/solo/gitGuard.js +0 -65
  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,991 +0,0 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const zlib = require('zlib');
5
- const { execFileSync } = require('child_process');
6
- const { getEvolverInstallRoot } = require('./gep/paths');
7
-
8
- const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
9
-
10
- const SEMVER_NUMERIC_IDENTIFIER = '0|[1-9]\\d*';
11
- const SEMVER_PRERELEASE_IDENTIFIER = '(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)';
12
- const SEMVER_BUILD_IDENTIFIER = '[0-9A-Za-z-]+';
13
- const CONCRETE_SEMVER_RE = new RegExp(
14
- '^(' + SEMVER_NUMERIC_IDENTIFIER + ')\\.(' + SEMVER_NUMERIC_IDENTIFIER + ')\\.(' +
15
- SEMVER_NUMERIC_IDENTIFIER + ')(?:-(' + SEMVER_PRERELEASE_IDENTIFIER +
16
- '(?:\\.' + SEMVER_PRERELEASE_IDENTIFIER + ')*))?(?:\\+(' +
17
- SEMVER_BUILD_IDENTIFIER + '(?:\\.' + SEMVER_BUILD_IDENTIFIER + ')*))?$'
18
- );
19
-
20
- // Sentinel returned by executeForceUpdate when the no-op short-circuit fires
21
- // (current installed version already satisfies required_version). Distinct from
22
- // `true` so callers can suppress phantom "success" telemetry and avoid the
23
- // gratuitous process.exit(78) restart that follows a real upgrade. Callers
24
- // MUST detect this with === identity comparison; do not use truthy/falsy
25
- // checks (the sentinel IS truthy).
26
- const FORCE_UPDATE_NOOP = Symbol('FORCE_UPDATE_NOOP');
27
-
28
- // Sentinel returned by executeForceUpdate when a concurrent invocation is
29
- // already running in this process. The two callers (enrich.js's evolve tick
30
- // and a2aProtocol's heartbeat-thread trigger) can both observe a pending
31
- // force_update directive in the same scheduler tick. Without a shared
32
- // in-process mutex, both would call executeForceUpdate and both would fire
33
- // reportForceUpdateOutcome, causing two atomic-rename writes to the same
34
- // state file -- last writer wins, the first attempt's telemetry is lost.
35
- //
36
- // Callers MUST detect this with === identity comparison and treat it as a
37
- // no-op (do NOT write a state file, do NOT trigger process.exit(78), do NOT
38
- // emit failure telemetry). The in-flight invocation owns the outcome and will
39
- // fire its own reportForceUpdateOutcome. See test/forceUpdateConcurrencyGuard.test.js.
40
- const FORCE_UPDATE_BUSY = Symbol('FORCE_UPDATE_BUSY');
41
-
42
- // Structured failure taxonomy. Historically every failing branch of
43
- // _executeForceUpdateInner just `return false`, so the only thing that ever
44
- // reached the hub (via reportForceUpdateOutcome) was the literal string
45
- // "executeForceUpdate returned false" — degit-missing, tag-404, version
46
- // mismatch and copy-EPERM were all indistinguishable in EvolverUpgradeAttempt.
47
- // Each branch now returns _fail(code, detail); the reporter encodes it as
48
- // `error = code + ': ' + detail`, so operators can GROUP BY the code prefix
49
- // without any hub schema / DB migration. Codes are a small stable set — keep
50
- // new ones coarse and additive so historical `error LIKE 'code%'` queries
51
- // don't churn.
52
- const FORCE_UPDATE_FAIL_CODES = Object.freeze({
53
- INSTALL_GUARD_NAME_MISMATCH: 'install_guard_name_mismatch',
54
- INSTALL_GUARD_UNREADABLE: 'install_guard_unreadable',
55
- BAD_REQUIRED_VERSION: 'bad_required_version',
56
- CURRENT_VERSION_UNPARSABLE: 'current_version_unparsable',
57
- NPX_NOT_FOUND: 'npx_not_found',
58
- DEGIT_TIMEOUT: 'degit_timeout',
59
- DEGIT_FAILED: 'degit_failed',
60
- DOWNLOAD_INCOMPLETE: 'download_incomplete',
61
- DOWNLOADED_PACKAGE_NAME_MISMATCH: 'downloaded_package_name_mismatch',
62
- DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
63
- DELETE_FAILED: 'delete_failed',
64
- COPY_FAILED: 'copy_failed',
65
- FALLBACK_DOWNLOAD_INCOMPLETE: 'fallback_download_incomplete',
66
- FALLBACK_DELETE_FAILED: 'fallback_delete_failed',
67
- FALLBACK_COPY_FAILED: 'fallback_copy_failed',
68
- FALLBACK_DOWNLOADED_PACKAGE_NAME_MISMATCH: 'fallback_downloaded_package_name_mismatch',
69
- FALLBACK_DOWNLOADED_VERSION_MISMATCH: 'fallback_downloaded_version_mismatch',
70
- ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
71
- });
72
-
73
- const EVOLVER_INSTALL_MARKERS = Object.freeze([
74
- Object.freeze({
75
- relPath: path.join('src', 'forceUpdate.js'),
76
- required: true,
77
- tokens: Object.freeze(['executeForceUpdate', 'FORCE_UPDATE_FAIL_CODES']),
78
- }),
79
- Object.freeze({
80
- relPath: path.join('src', 'gep', 'paths.js'),
81
- tokens: Object.freeze(['getRepoRoot', 'getEvolverInstallRoot']),
82
- }),
83
- Object.freeze({
84
- relPath: path.join('src', 'gep', 'a2aProtocol.js'),
85
- tokens: Object.freeze(['GEP A2A Protocol', 'reportForceUpdateOutcome']),
86
- }),
87
- Object.freeze({
88
- relPath: 'index.js',
89
- tokens: Object.freeze(['proxy-token', './src/evolve']),
90
- }),
91
- ]);
92
- const MAX_INSTALL_MARKER_BYTES = 1024 * 1024;
93
- const FORCE_UPDATE_BACKUP_PREFIX = '.evolver-force-update-backup-';
94
- const FORCE_UPDATE_JOURNAL_FILE = '.evolver-force-update-journal.json';
95
-
96
- // Build the structured failure result that replaces a bare `return false`.
97
- // Shape: { ok:false, code, detail }. Distinct from `true`, FORCE_UPDATE_NOOP
98
- // and FORCE_UPDATE_BUSY, so the three call sites' `result === true` /
99
- // `result === SENTINEL` checks keep classifying it as "failed" unchanged —
100
- // this is backward compatible. Frozen so a downstream consumer cannot mutate
101
- // the code/detail before it is reported. detail is best-effort context (an
102
- // errno, a version delta, an entry name); it is redacted + truncated to
103
- // ERROR_MAX by the reporter before it leaves the process.
104
- function _fail(code, detail) {
105
- return Object.freeze({
106
- ok: false,
107
- code: String(code),
108
- detail: detail == null ? '' : String(detail),
109
- });
110
- }
111
-
112
- // Compact "CODE: message" rendering of a thrown error for the detail field.
113
- function _errStr(e) {
114
- if (!e) return 'unknown';
115
- var code = e.code ? String(e.code) + ': ' : '';
116
- return code + (e.message != null ? String(e.message) : String(e));
117
- }
118
-
119
- function _isEvolverPackageName(name) {
120
- return name === '@evomap/evolver' || name === 'evolver';
121
- }
122
-
123
- function _fileMatchesInstallMarker(root, marker) {
124
- try {
125
- var markerPath = path.join(root, marker.relPath);
126
- var st = fs.statSync(markerPath);
127
- if (!st.isFile()) return false;
128
- if (st.size > MAX_INSTALL_MARKER_BYTES) return false;
129
- var content = fs.readFileSync(markerPath, 'utf8');
130
- for (var i = 0; i < marker.tokens.length; i++) {
131
- if (!content.includes(marker.tokens[i])) return false;
132
- }
133
- return true;
134
- } catch (_) {
135
- return false;
136
- }
137
- }
138
-
139
- function _hasStrongEvolverInstallMarkers(root) {
140
- var matched = 0;
141
- var requiredMatched = false;
142
- for (var i = 0; i < EVOLVER_INSTALL_MARKERS.length; i++) {
143
- var marker = EVOLVER_INSTALL_MARKERS[i];
144
- if (!_fileMatchesInstallMarker(root, marker)) continue;
145
- matched++;
146
- if (marker.required) requiredMatched = true;
147
- }
148
- return requiredMatched && matched >= 3;
149
- }
150
-
151
- // Map a Channel 1 (GitHub Release / degit) throw to a structured failure.
152
- // `phase` records how far the try block got before throwing, so a readFileSync
153
- // ENOENT (truncated download) is not misread as an npx ENOENT (npx missing):
154
- // 'degit' -> the npx/degit spawn itself
155
- // 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
156
- // 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
157
- function _classifyChannel1Error(e, phase) {
158
- if (phase === 'delete') {
159
- var deleteEntry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
160
- return _fail(FORCE_UPDATE_FAIL_CODES.DELETE_FAILED, deleteEntry + _errStr(e));
161
- }
162
- if (phase === 'copy') {
163
- var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
164
- return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
165
- }
166
- if (phase === 'parse') {
167
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
168
- 'missing/invalid package.json in downloaded tree: ' + _errStr(e));
169
- }
170
- // phase === 'degit' (the spawn). ENOENT here is the npx binary itself, not a
171
- // file inside the download — that distinction is exactly why `phase` exists.
172
- if (e && e.code === 'ENOENT') {
173
- return _fail(FORCE_UPDATE_FAIL_CODES.NPX_NOT_FOUND, _errStr(e));
174
- }
175
- // execFileSync timeout kills the child with SIGTERM (and sets .killed); some
176
- // platforms surface ETIMEDOUT instead. Either way it is a 60s timeout.
177
- if (e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT')) {
178
- return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_TIMEOUT,
179
- 'degit timed out after 60s' + (e.signal ? ' (signal=' + e.signal + ')' : ''));
180
- }
181
- // Generic degit/network/tag-not-found failure. degit prints the real reason
182
- // ("could not find commit hash for v…", "could not resolve host") to stderr,
183
- // so keep a tail of it. Redact + strip control chars HERE, before the tail
184
- // slice: the downstream reporter redact (a2aProtocol.reportForceUpdateOutcome)
185
- // runs after this, so slicing first could chop a token's prefix anchor and
186
- // let the bare value slip past the prefix-anchored redact patterns. Stripping
187
- // ANSI/NUL/newlines also keeps the persisted error free of terminal-injection
188
- // sequences and log-line noise.
189
- var detail = _errStr(e);
190
- var stderr = '';
191
- if (e && e.stderr != null) {
192
- try {
193
- var redactString = require('./gep/sanitize').redactString;
194
- stderr = redactString(String(e.stderr)).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
195
- } catch (_) {
196
- // sanitize unavailable — still strip control chars so logs stay clean.
197
- stderr = String(e.stderr).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
198
- }
199
- }
200
- if (stderr) detail += ' | stderr=' + stderr.slice(-300);
201
- return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
202
- }
203
-
204
- function _withFallbackFailure(primaryFailure, fallbackFailure) {
205
- if (!primaryFailure) return fallbackFailure;
206
- if (!fallbackFailure) return primaryFailure;
207
- var primaryCode = String(primaryFailure.code || FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED);
208
- var fallbackCode = String(fallbackFailure.code || FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED);
209
- var terminalCode = 'fallback_' + fallbackCode.replace(/[^a-z0-9_]/gi, '_').toLowerCase();
210
- var detail = 'primary_failed=' + primaryCode + ' | fallback_failed=' + fallbackCode;
211
- if (fallbackFailure.detail) detail += ': ' + fallbackFailure.detail;
212
- if (primaryFailure.detail) detail += ' | primary_detail=' + primaryFailure.detail;
213
- return _fail(terminalCode, detail);
214
- }
215
-
216
- function _isRetryableFsLockError(e) {
217
- var code = e && e.code;
218
- return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' ||
219
- code === 'ENOTEMPTY' || code === 'EMFILE' || code === 'ENFILE';
220
- }
221
-
222
- function _waitForFsLockRetry() {
223
- var until = Date.now() + 200;
224
- while (Date.now() < until) { /* spin */ }
225
- }
226
-
227
- function _retryFsLockOperation(fn) {
228
- var err = null;
229
- for (var attempt = 0; attempt < 3; attempt++) {
230
- try {
231
- return fn();
232
- } catch (e) {
233
- err = e;
234
- if (!_isRetryableFsLockError(e)) break;
235
- if (attempt < 2) _waitForFsLockRetry();
236
- }
237
- }
238
- throw err;
239
- }
240
-
241
- function _downloadUrlWithNode(url, destPath) {
242
- var script = [
243
- "const fs=require('fs');",
244
- "const https=require('https');",
245
- "const url=process.argv[1];",
246
- "const dest=process.argv[2];",
247
- "function fail(msg){try{fs.rmSync(dest,{force:true});}catch(_){};console.error(msg);process.exit(1);}",
248
- "function get(u, redirects){",
249
- " const parsed=new URL(u);",
250
- " if(parsed.protocol!=='https:') fail('refusing non-https download URL');",
251
- " const req=https.get(parsed,{headers:{'User-Agent':'evomap-evolver-force-update'}},(res)=>{",
252
- " if(res.statusCode>=300&&res.statusCode<400&&res.headers.location){",
253
- " if(redirects<=0) fail('too many redirects');",
254
- " res.resume();",
255
- " return get(new URL(res.headers.location, parsed).toString(), redirects-1);",
256
- " }",
257
- " if(res.statusCode!==200) fail('download status '+res.statusCode);",
258
- " const out=fs.createWriteStream(dest,{flags:'wx',mode:0o600});",
259
- " res.pipe(out);",
260
- " out.on('finish',()=>out.close(()=>process.exit(0)));",
261
- " out.on('error',(e)=>fail(e&&e.message||e));",
262
- " });",
263
- " req.setTimeout(60000,()=>req.destroy(new Error('download timeout')));",
264
- " req.on('error',(e)=>fail(e&&e.message||e));",
265
- "}",
266
- "get(url,5);",
267
- ].join('');
268
- execFileSync(process.execPath, ['-e', script, url, destPath], {
269
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
270
- timeout: 70000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
271
- });
272
- }
273
-
274
- function _readTarString(block, start, length) {
275
- var end = start;
276
- var max = start + length;
277
- while (end < max && block[end] !== 0) end++;
278
- return block.toString('utf8', start, end);
279
- }
280
-
281
- function _readTarOctal(block, start, length) {
282
- var raw = _readTarString(block, start, length).trim();
283
- if (!raw) return 0;
284
- var parsed = parseInt(raw, 8);
285
- if (!Number.isFinite(parsed) || parsed < 0) throw new Error('invalid tar numeric field');
286
- return parsed;
287
- }
288
-
289
- function _isZeroTarBlock(block) {
290
- for (var i = 0; i < block.length; i++) {
291
- if (block[i] !== 0) return false;
292
- }
293
- return true;
294
- }
295
-
296
- function _stripTarPathComponents(entryName, count) {
297
- var normalized = String(entryName || '').replace(/\\/g, '/');
298
- if (!normalized || normalized.startsWith('/')) return '';
299
- var parts = normalized.split('/').filter(function (part) { return part !== '' && part !== '.'; });
300
- if (parts.length <= count) return '';
301
- return parts.slice(count).join('/');
302
- }
303
-
304
- function _safeTarOutputPath(root, relativeName) {
305
- var parts = String(relativeName || '').split('/');
306
- for (var i = 0; i < parts.length; i++) {
307
- if (!parts[i] || parts[i] === '.' || parts[i] === '..') return '';
308
- }
309
- var rootPath = path.resolve(root);
310
- var outPath = path.resolve(rootPath, relativeName);
311
- if (outPath !== rootPath && outPath.startsWith(rootPath + path.sep)) return outPath;
312
- return '';
313
- }
314
-
315
- function _extractTarGzWithNode(archivePath, tempTarget) {
316
- var archive = zlib.gunzipSync(fs.readFileSync(archivePath));
317
- var offset = 0;
318
- while (offset + 512 <= archive.length) {
319
- var header = archive.subarray(offset, offset + 512);
320
- offset += 512;
321
- if (_isZeroTarBlock(header)) break;
322
-
323
- var name = _readTarString(header, 0, 100);
324
- var prefix = _readTarString(header, 345, 155);
325
- if (prefix) name = prefix + '/' + name;
326
- var size = _readTarOctal(header, 124, 12);
327
- var typeFlag = _readTarString(header, 156, 1) || '0';
328
- if (offset + size > archive.length) throw new Error('truncated tar entry: ' + name);
329
- var mode = 0;
330
- try { mode = _readTarOctal(header, 100, 8) & 0o777; } catch (_) { mode = 0; }
331
-
332
- var stripped = _stripTarPathComponents(name, 1);
333
- var outPath = _safeTarOutputPath(tempTarget, stripped);
334
- if (outPath) {
335
- if (typeFlag === '5') {
336
- fs.mkdirSync(outPath, { recursive: true });
337
- if (mode && process.platform !== 'win32') {
338
- try { fs.chmodSync(outPath, mode); } catch (_) {}
339
- }
340
- } else if (typeFlag === '0' || typeFlag === '\0') {
341
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
342
- fs.writeFileSync(outPath, archive.subarray(offset, offset + size));
343
- if (mode && process.platform !== 'win32') {
344
- try { fs.chmodSync(outPath, mode); } catch (_) {}
345
- }
346
- }
347
- }
348
-
349
- offset += Math.ceil(size / 512) * 512;
350
- }
351
- }
352
-
353
- function _clearTempTarget(tempTarget) {
354
- fs.rmSync(tempTarget, { recursive: true, force: true });
355
- fs.mkdirSync(tempTarget, { recursive: true });
356
- }
357
-
358
- function _tryDownloadReleaseTarball(requiredVersion, tempTarget) {
359
- var archiveDir = null;
360
- var archivePath = null;
361
- var url = 'https://codeload.github.com/EvoMap/evolver/tar.gz/refs/tags/v' + requiredVersion;
362
- try {
363
- archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-archive-'));
364
- archivePath = path.join(archiveDir, 'archive.tar.gz');
365
- _clearTempTarget(tempTarget);
366
- console.log('[ForceUpdate] Channel 1b: GitHub tarball fallback (v' + requiredVersion + ')...');
367
- _downloadUrlWithNode(url, archivePath);
368
- _extractTarGzWithNode(archivePath, tempTarget);
369
- return null;
370
- } catch (e) {
371
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE, _errStr(e));
372
- } finally {
373
- if (archiveDir) {
374
- try { fs.rmSync(archiveDir, { recursive: true, force: true }); } catch (_) {}
375
- }
376
- }
377
- }
378
-
379
- function _recoverPackageCommitMarkerIfMissing(installRoot) {
380
- var pkgDst = path.join(installRoot, 'package.json');
381
- if (fs.existsSync(pkgDst)) return false;
382
- var entries;
383
- try {
384
- entries = fs.readdirSync(installRoot);
385
- } catch (_) {
386
- return false;
387
- }
388
- var backups = entries
389
- .filter(function (name) { return /^package\.json\.\d+\.evolver-old$/.test(name); })
390
- .sort();
391
- for (var i = backups.length - 1; i >= 0; i--) {
392
- try {
393
- fs.renameSync(path.join(installRoot, backups[i]), pkgDst);
394
- console.warn('[ForceUpdate] Recovered package.json commit marker from ' + backups[i]);
395
- return true;
396
- } catch (_) {}
397
- }
398
- return false;
399
- }
400
-
401
- function _isForceUpdateKeepEntry(name) {
402
- return name === 'node_modules' || name === 'memory' || name === '.git' || name === 'MEMORY.md' ||
403
- name === '.env' || name === '.env.local' || name === 'USER.md' || name === '.evolver' ||
404
- name === 'logs';
405
- }
406
-
407
- function _isForceUpdateInternalEntry(name) {
408
- return name === FORCE_UPDATE_JOURNAL_FILE || String(name || '').startsWith(FORCE_UPDATE_BACKUP_PREFIX);
409
- }
410
-
411
- function _isForceUpdateBootstrapEntry(name) {
412
- return name === 'index.js';
413
- }
414
-
415
- function _readInstallPackageVersion(installRoot) {
416
- try {
417
- var pkg = JSON.parse(fs.readFileSync(path.join(installRoot, 'package.json'), 'utf8'));
418
- return pkg && pkg.version ? String(pkg.version) : '';
419
- } catch (_) {
420
- return '';
421
- }
422
- }
423
-
424
- function _writeForceUpdateRecoveryJournal(backupRoot, requiredVersion, previousVersion) {
425
- var journal = {
426
- state: 'precommit',
427
- requiredVersion: String(requiredVersion || ''),
428
- previousVersion: String(previousVersion || ''),
429
- createdAt: Date.now(),
430
- };
431
- fs.writeFileSync(
432
- path.join(backupRoot, FORCE_UPDATE_JOURNAL_FILE),
433
- JSON.stringify(journal),
434
- { encoding: 'utf8', mode: 0o600 },
435
- );
436
- }
437
-
438
- function _removeInstallEntryIfPresent(entryPath) {
439
- _retryFsLockOperation(function () {
440
- fs.rmSync(entryPath, {
441
- recursive: true, force: true, maxRetries: 3, retryDelay: 200,
442
- });
443
- });
444
- }
445
-
446
- function _copyFileIfPresent(src, dst) {
447
- if (!fs.existsSync(src)) return false;
448
- fs.copyFileSync(src, dst);
449
- return true;
450
- }
451
-
452
- function _restoreFileBackupIfPresent(backupPath, dstPath) {
453
- if (!backupPath || !fs.existsSync(backupPath)) return false;
454
- try { fs.rmSync(dstPath, { recursive: true, force: true }); } catch (_) {}
455
- fs.copyFileSync(backupPath, dstPath);
456
- return true;
457
- }
458
-
459
- function _commitAtomicFileReplacement(srcPath, dstPath, tmpPath, backupPath) {
460
- try { fs.rmSync(tmpPath, { force: true }); } catch (_) {}
461
- try { fs.rmSync(backupPath, { force: true }); } catch (_) {}
462
- fs.copyFileSync(srcPath, tmpPath);
463
- _copyFileIfPresent(dstPath, backupPath);
464
- _retryFsLockOperation(function () {
465
- fs.renameSync(tmpPath, dstPath);
466
- });
467
- }
468
-
469
- function _restoreMovedInstallEntries(installRoot, movedEntries, copiedEntryNames) {
470
- var ok = true;
471
- var seenCopied = Object.create(null);
472
- for (var ci = copiedEntryNames.length - 1; ci >= 0; ci--) {
473
- var copiedName = copiedEntryNames[ci];
474
- if (seenCopied[copiedName]) continue;
475
- seenCopied[copiedName] = true;
476
- try {
477
- _removeInstallEntryIfPresent(path.join(installRoot, copiedName));
478
- } catch (copyCleanupErr) {
479
- ok = false;
480
- console.warn('[ForceUpdate] rollback cleanup failed for ' + copiedName + ': ' +
481
- (copyCleanupErr.message || copyCleanupErr));
482
- }
483
- }
484
-
485
- for (var mi = movedEntries.length - 1; mi >= 0; mi--) {
486
- var moved = movedEntries[mi];
487
- try {
488
- if (fs.existsSync(moved.livePath)) _removeInstallEntryIfPresent(moved.livePath);
489
- fs.renameSync(moved.backupPath, moved.livePath);
490
- } catch (restoreErr) {
491
- ok = false;
492
- console.warn('[ForceUpdate] rollback restore failed for ' + moved.name + ': ' +
493
- (restoreErr.message || restoreErr));
494
- }
495
- }
496
- return ok;
497
- }
498
-
499
- function _installDownloadedTree(installRoot, tempTarget, requiredVersion, successLabel) {
500
- var phase = 'parse';
501
- var backupRoot = null;
502
- var movedEntries = [];
503
- var copiedEntryNames = [];
504
- var committedIndex = false;
505
- var indexBackup = null;
506
- var packageBackup = null;
507
- try {
508
- var tmpPkg = JSON.parse(fs.readFileSync(path.join(tempTarget, 'package.json'), 'utf8'));
509
- if (!_isEvolverPackageName(tmpPkg && tmpPkg.name)) {
510
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_PACKAGE_NAME_MISMATCH,
511
- 'downloaded package.json name="' + (tmpPkg && tmpPkg.name) + '", expected "@evomap/evolver"');
512
- }
513
- if (!tmpPkg.version) {
514
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
515
- 'downloaded package.json has no version field');
516
- }
517
- if (tmpPkg.version !== requiredVersion) {
518
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH,
519
- 'downloaded version=' + JSON.stringify(tmpPkg.version) + ', expected ' + requiredVersion);
520
- }
521
- try {
522
- if (!fs.statSync(path.join(tempTarget, 'index.js')).isFile()) {
523
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
524
- 'downloaded index.js is not a file');
525
- }
526
- } catch (indexReadErr) {
527
- return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
528
- 'missing/unreadable index.js in downloaded tree: ' + _errStr(indexReadErr));
529
- }
530
-
531
- phase = 'delete';
532
- var entries = fs.readdirSync(installRoot, { withFileTypes: true });
533
- backupRoot = fs.mkdtempSync(path.join(installRoot, FORCE_UPDATE_BACKUP_PREFIX));
534
- _writeForceUpdateRecoveryJournal(backupRoot, requiredVersion, _readInstallPackageVersion(installRoot));
535
- var indexSrc = path.join(tempTarget, 'index.js');
536
- var indexDst = path.join(installRoot, 'index.js');
537
- var indexTmp = indexDst + '.' + process.pid + '.evolver-tmp';
538
- indexBackup = path.join(backupRoot, 'index.js');
539
- try {
540
- phase = 'copy';
541
- _commitAtomicFileReplacement(indexSrc, indexDst, indexTmp, indexBackup);
542
- committedIndex = true;
543
- } catch (indexErr) {
544
- _restoreFileBackupIfPresent(indexBackup, indexDst);
545
- try { fs.rmSync(indexTmp, { force: true }); } catch (_) {}
546
- console.warn('[ForceUpdate] index.js commit (atomic replace) failed: ' + (indexErr.message || indexErr));
547
- try { indexErr._evolverEntry = 'index.js commit'; } catch (_) {}
548
- throw indexErr;
549
- }
550
-
551
- for (var ei = 0; ei < entries.length; ei++) {
552
- var eName = entries[ei].name;
553
- // package.json is the install's commit marker: keep the OLD one in
554
- // place through the entire delete+copy below and swap in the new one
555
- // atomically at the very end. index.js was already atomically replaced
556
- // after the recovery journal was written, before moving payload entries,
557
- // so an interrupted update restarts through the recovery-capable
558
- // bootstrap. Keep-list entries are local state and must not be deleted or
559
- // overwritten by a downloaded release.
560
- if (_isForceUpdateKeepEntry(eName) || _isForceUpdateBootstrapEntry(eName) ||
561
- _isForceUpdateInternalEntry(eName) || eName === 'package.json') continue;
562
- try {
563
- (function (entryName) {
564
- phase = 'delete';
565
- var livePath = path.join(installRoot, entryName);
566
- var backupPath = path.join(backupRoot, entryName);
567
- _retryFsLockOperation(function () {
568
- fs.renameSync(livePath, backupPath);
569
- });
570
- movedEntries.push({ name: entryName, livePath: livePath, backupPath: backupPath });
571
- })(eName);
572
- } catch (rmErr) {
573
- console.warn('[ForceUpdate] backup move failed for ' + eName + ': ' + (rmErr.message || rmErr));
574
- try { rmErr._evolverEntry = eName; } catch (_) {}
575
- throw rmErr;
576
- }
577
- }
578
-
579
- phase = 'copy';
580
- var newEntries = fs.readdirSync(tempTarget, { withFileTypes: true });
581
- for (var ni = 0; ni < newEntries.length; ni++) {
582
- if (newEntries[ni].name === 'package.json' || _isForceUpdateKeepEntry(newEntries[ni].name) ||
583
- _isForceUpdateBootstrapEntry(newEntries[ni].name) || _isForceUpdateInternalEntry(newEntries[ni].name)) continue;
584
- var src = path.join(tempTarget, newEntries[ni].name);
585
- var dst = path.join(installRoot, newEntries[ni].name);
586
- try {
587
- (function (copySrc, copyDst) {
588
- copiedEntryNames.push(path.basename(copyDst));
589
- _retryFsLockOperation(function () {
590
- fs.cpSync(copySrc, copyDst, { recursive: true });
591
- });
592
- })(src, dst);
593
- } catch (copyErr) {
594
- console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' +
595
- (copyErr.message || copyErr));
596
- try { copyErr._evolverEntry = newEntries[ni].name; } catch (_) {}
597
- throw copyErr;
598
- }
599
- }
600
-
601
- var pkgSrc = path.join(tempTarget, 'package.json');
602
- var pkgDst = path.join(installRoot, 'package.json');
603
- var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
604
- packageBackup = path.join(backupRoot, 'package.json');
605
-
606
- try {
607
- _commitAtomicFileReplacement(pkgSrc, pkgDst, pkgTmp, packageBackup);
608
- } catch (pkgErr) {
609
- _restoreFileBackupIfPresent(packageBackup, pkgDst);
610
- if (committedIndex) _restoreFileBackupIfPresent(indexBackup, indexDst);
611
- try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
612
- console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
613
- try { pkgErr._evolverEntry = 'package.json commit'; } catch (_) {}
614
- throw pkgErr;
615
- }
616
-
617
- try { fs.rmSync(tempTarget, { recursive: true, force: true }); } catch (_) {}
618
- if (backupRoot) {
619
- try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (backupCleanupErr) {
620
- console.warn('[ForceUpdate] backup cleanup failed: ' + (backupCleanupErr.message || backupCleanupErr));
621
- }
622
- }
623
- console.log('[ForceUpdate] ' + successLabel + ': ' + tmpPkg.version);
624
- return { ok: true, version: tmpPkg.version };
625
- } catch (e) {
626
- if (backupRoot) {
627
- var rollbackOk = true;
628
- if (committedIndex) {
629
- try {
630
- if (!_restoreFileBackupIfPresent(indexBackup, path.join(installRoot, 'index.js'))) rollbackOk = false;
631
- } catch (indexRestoreErr) {
632
- rollbackOk = false;
633
- console.warn('[ForceUpdate] rollback restore failed for index.js: ' +
634
- (indexRestoreErr.message || indexRestoreErr));
635
- }
636
- }
637
- try {
638
- _restoreFileBackupIfPresent(packageBackup, path.join(installRoot, 'package.json'));
639
- } catch (packageRestoreErr) {
640
- rollbackOk = false;
641
- console.warn('[ForceUpdate] rollback restore failed for package.json: ' +
642
- (packageRestoreErr.message || packageRestoreErr));
643
- }
644
- var restored = _restoreMovedInstallEntries(installRoot, movedEntries, copiedEntryNames);
645
- if (rollbackOk && restored) {
646
- try { fs.rmSync(backupRoot, { recursive: true, force: true }); } catch (_) {}
647
- }
648
- }
649
- return _classifyChannel1Error(e, phase);
650
- }
651
- }
652
-
653
- // Module-level mutex: shared by every caller that requires('../forceUpdate'),
654
- // so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
655
- // in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
656
- // process-local guard only; it does not protect against two separate node
657
- // processes upgrading the same install root simultaneously (out of scope --
658
- // distinct processes have distinct install layouts in practice).
659
- let _inFlight = false;
660
-
661
- function parseConcreteSemver(version) {
662
- var match = CONCRETE_SEMVER_RE.exec(normalizeConcreteSemver(version));
663
- if (!match) return null;
664
- return {
665
- major: match[1],
666
- minor: match[2],
667
- patch: match[3],
668
- prerelease: match[4] ? match[4].split('.') : [],
669
- };
670
- }
671
-
672
- function normalizeConcreteSemver(version) {
673
- var normalized = String(version || '').replace(/^v(?=\d)/, '');
674
- return CONCRETE_SEMVER_RE.test(normalized) ? normalized : '';
675
- }
676
-
677
- function normalizeRequiredVersion(raw) {
678
- return normalizeConcreteSemver(String(raw || '').replace(/^[>=^~\s]+/, ''));
679
- }
680
-
681
- function isNumericPrereleaseIdentifier(value) {
682
- return /^\d+$/.test(value);
683
- }
684
-
685
- function compareNumericIdentifierStrings(left, right) {
686
- if (left.length !== right.length) return left.length - right.length;
687
- if (left < right) return -1;
688
- if (left > right) return 1;
689
- return 0;
690
- }
691
-
692
- function comparePrereleaseIdentifiers(left, right) {
693
- var leftNumeric = isNumericPrereleaseIdentifier(left);
694
- var rightNumeric = isNumericPrereleaseIdentifier(right);
695
- if (leftNumeric && rightNumeric) return compareNumericIdentifierStrings(left, right);
696
- if (leftNumeric) return -1;
697
- if (rightNumeric) return 1;
698
- if (left < right) return -1;
699
- if (left > right) return 1;
700
- return 0;
701
- }
702
-
703
- function compareConcreteSemver(left, right) {
704
- var a = parseConcreteSemver(left);
705
- var b = parseConcreteSemver(right);
706
- if (!a || !b) return null;
707
- var majorCmp = compareNumericIdentifierStrings(a.major, b.major);
708
- if (majorCmp !== 0) return majorCmp;
709
- var minorCmp = compareNumericIdentifierStrings(a.minor, b.minor);
710
- if (minorCmp !== 0) return minorCmp;
711
- var patchCmp = compareNumericIdentifierStrings(a.patch, b.patch);
712
- if (patchCmp !== 0) return patchCmp;
713
- if (!a.prerelease.length && !b.prerelease.length) return 0;
714
- if (!a.prerelease.length) return 1;
715
- if (!b.prerelease.length) return -1;
716
- var max = Math.max(a.prerelease.length, b.prerelease.length);
717
- for (var i = 0; i < max; i++) {
718
- if (a.prerelease[i] === undefined) return -1;
719
- if (b.prerelease[i] === undefined) return 1;
720
- var cmp = comparePrereleaseIdentifiers(a.prerelease[i], b.prerelease[i]);
721
- if (cmp !== 0) return cmp;
722
- }
723
- return 0;
724
- }
725
-
726
- // Force Update: triggered by Hub when version is critically outdated.
727
- // Extracted from src/evolve.js so both the evolve main loop and heartbeat
728
- // thread can trigger it independently (heartbeat-only workers need this
729
- // because they never reach the evolve run() loop that consumes the pending
730
- // force_update directive).
731
- //
732
- // CRITICAL (issue #51): this function MUST operate on the evolver INSTALL
733
- // directory, NOT getRepoRoot(). getRepoRoot() preferentially returns the
734
- // user's surrounding project (process.cwd()'s nearest .git ancestor).
735
- // Using it here would delete the user's project files and copy the
736
- // evolver package on top of them. Always use getEvolverInstallRoot(),
737
- // which resolves to the package containing this file regardless of
738
- // install layout (global npm / local node_modules / dev clone).
739
- function executeForceUpdate(forceUpdate) {
740
- // Concurrency guard: if a prior invocation is still in flight, refuse and
741
- // return the BUSY sentinel. The in-flight caller owns the outcome (state
742
- // file write, process.exit(78) on success); a second concurrent attempt
743
- // would (a) race the atomic-rename state-file writes and clobber the first
744
- // attempt's telemetry row, and (b) potentially double-exit. See
745
- // FORCE_UPDATE_BUSY docstring above for context.
746
- if (_inFlight) {
747
- console.log('[ForceUpdate] BUSY: another invocation already in flight, skipping');
748
- return FORCE_UPDATE_BUSY;
749
- }
750
- _inFlight = true;
751
- try {
752
- return _executeForceUpdateInner(forceUpdate);
753
- } finally {
754
- // Always release the mutex, even on throw. Callers may rely on retrying
755
- // after a failure (e.g. heartbeat cooldown), so the flag MUST NOT remain
756
- // set after the function returns/throws. Note: on a successful upgrade,
757
- // _executeForceUpdateInner returns true and the caller invokes
758
- // process.exit(78); the finally still runs before exit -- which is fine,
759
- // there is nothing else to coordinate with at that point.
760
- _inFlight = false;
761
- }
762
- }
763
-
764
- function _executeForceUpdateInner(forceUpdate) {
765
- const INSTALL_ROOT = getEvolverInstallRoot();
766
-
767
- // Defense in depth: if a future refactor breaks path resolution and
768
- // INSTALL_ROOT no longer points at the evolver package (no package.json
769
- // / wrong package name), refuse the update rather than risk
770
- // overwriting an unrelated directory. This is the last guard between
771
- // the deletion loop and the user's data.
772
- try {
773
- const pkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
774
- if (!pkg || !_isEvolverPackageName(pkg.name)) {
775
- console.warn('[ForceUpdate] Refusing — ' + INSTALL_ROOT +
776
- '/package.json has name="' + (pkg && pkg.name) +
777
- '", expected "@evomap/evolver". Aborting to avoid data loss.');
778
- return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
779
- 'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
780
- }
781
- } catch (e) {
782
- if (_recoverPackageCommitMarkerIfMissing(INSTALL_ROOT)) {
783
- try {
784
- const recoveredPkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
785
- if (!recoveredPkg || !_isEvolverPackageName(recoveredPkg.name)) {
786
- console.warn('[ForceUpdate] Refusing — recovered ' + INSTALL_ROOT +
787
- '/package.json has name="' + (recoveredPkg && recoveredPkg.name) +
788
- '", expected "@evomap/evolver". Aborting to avoid data loss.');
789
- return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
790
- 'recovered install root package.json name="' + (recoveredPkg && recoveredPkg.name) +
791
- '", expected "@evomap/evolver"');
792
- }
793
- } catch (recoverReadErr) {
794
- console.warn('[ForceUpdate] Refusing — cannot read recovered ' + INSTALL_ROOT +
795
- '/package.json: ' + (recoverReadErr && recoverReadErr.message || recoverReadErr));
796
- return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
797
- 'cannot read recovered install root package.json: ' + _errStr(recoverReadErr));
798
- }
799
- } else if (_hasStrongEvolverInstallMarkers(INSTALL_ROOT)) {
800
- console.warn('[ForceUpdate] install package.json is unreadable, but strong evolver install markers are present; bootstrap recovery allowed');
801
- } else {
802
- console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
803
- '/package.json: ' + (e && e.message || e));
804
- return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
805
- 'cannot read install root package.json: ' + _errStr(e));
806
- }
807
- }
808
-
809
- const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
810
- if (!requiredVersion) {
811
- console.warn('[ForceUpdate] Refusing — required_version "' +
812
- String(forceUpdate.required_version || '').replace(/^[>=^~\s]+/, '') +
813
- '" is not a concrete semver (ranges not accepted).');
814
- return _fail(FORCE_UPDATE_FAIL_CODES.BAD_REQUIRED_VERSION,
815
- 'required_version=' + JSON.stringify(forceUpdate && forceUpdate.required_version) + ' is not a concrete semver');
816
- }
817
-
818
- function getCurrentVersion() {
819
- try {
820
- var pkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
821
- return pkg.version || '0.0.0';
822
- } catch (_) { return '0.0.0'; }
823
- }
824
-
825
- // Idempotency / anti-downgrade short-circuit: the hub keeps re-issuing the
826
- // same force_update directive until the node reports success. After a
827
- // successful upgrade + restart (process.exit(78)), the next heartbeat may
828
- // still carry the same directive. Without this early return, a transient
829
- // Channel 1 failure (npx unavailable, network blip, EBUSY) would cause
830
- // executeForceUpdate to return false and overwrite the previous successful
831
- // run's state file with a bogus "failed" -- even though we are already at or
832
- // above the target version.
833
- //
834
- // Compare the ACTUAL current running version (which reflects the new
835
- // version post-restart) against the parsed requiredVersion. Force-update is
836
- // a minimum-version floor, not an exact-version pin: a node running 1.88.4
837
- // must not be downgraded to satisfy a 1.88.3 floor. Only reached after the
838
- // strip+validate above, so a garbage / unparseable required_version will NOT
839
- // short-circuit -- it falls into the validation failure branch above and
840
- // returns false safely.
841
- var currentVersion = getCurrentVersion();
842
- var versionCmp = compareConcreteSemver(currentVersion, requiredVersion);
843
- if (versionCmp === null) {
844
- console.warn('[ForceUpdate] Refusing — current installed version "' +
845
- currentVersion + '" is not a concrete semver.');
846
- return _fail(FORCE_UPDATE_FAIL_CODES.CURRENT_VERSION_UNPARSABLE,
847
- 'current installed version "' + currentVersion + '" is not a concrete semver');
848
- }
849
- if (versionCmp >= 0) {
850
- console.log('[ForceUpdate] already satisfies required version, no-op (current=' +
851
- currentVersion + ', required=' + requiredVersion + ')');
852
- // Return the dedicated sentinel rather than `true`. Callers use this to
853
- // (a) emit status="skipped" telemetry instead of a phantom "success"
854
- // row in EvolverUpgradeAttempt with from_version == to_version, and
855
- // (b) skip the process.exit(78) restart — there is nothing to restart
856
- // for when the binary didn't change.
857
- return FORCE_UPDATE_NOOP;
858
- }
859
-
860
- console.log('[ForceUpdate] Starting update (target: ' + requiredVersion +
861
- ', install root: ' + INSTALL_ROOT + ')');
862
-
863
- // Use os.tmpdir() for staging — INSTALL_ROOT's parent (e.g.
864
- // /usr/lib/node_modules/@evomap when globally installed) is often not
865
- // writable, unlike the previous user-project parent.
866
- // mkdtempSync produces a random suffix, preventing predictable-path pre-population.
867
- const TMP_TARGET = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-tmp-'));
868
-
869
- // Channel 1: GitHub Release (via degit pinned to exact version tag)
870
- //
871
- // channel1Failure captures the structured reason this channel failed, so the
872
- // terminal `return` can surface it instead of a bare `false`. `phase` tracks
873
- // how far we got before any throw, so _classifyChannel1Error can tell a
874
- // degit-spawn failure (phase 'degit') from a truncated download (phase
875
- // 'parse') from a delete/copy-into-INSTALL_ROOT failure.
876
- var channel1Failure = null;
877
- var phase = 'degit';
878
- try {
879
- console.log('[ForceUpdate] Channel 1: GitHub Release download (v' + requiredVersion + ')...');
880
- var npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
881
- // Pin to exact git tag so we download a specific published release, not
882
- // whatever is currently at HEAD (which could be a different, unreviewed commit).
883
- // --force: mkdtempSync pre-creates TMP_TARGET; some degit versions refuse a pre-existing dest.
884
- execFileSync(npxBin, ['-y', 'degit', '--force', 'EvoMap/evolver#v' + requiredVersion, TMP_TARGET], {
885
- encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
886
- timeout: 60000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
887
- });
888
- var installResult = _installDownloadedTree(
889
- INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub Release update successful',
890
- );
891
- if (installResult.ok === true) {
892
- return true;
893
- }
894
- channel1Failure = installResult;
895
- if (
896
- channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE ||
897
- channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOADED_PACKAGE_NAME_MISMATCH ||
898
- channel1Failure.code === FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH
899
- ) {
900
- var postDownloadFallbackFailure = _tryDownloadReleaseTarball(requiredVersion, TMP_TARGET);
901
- if (!postDownloadFallbackFailure) {
902
- var postDownloadFallbackResult = _installDownloadedTree(
903
- INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub tarball fallback update successful',
904
- );
905
- if (postDownloadFallbackResult.ok === true) {
906
- return true;
907
- }
908
- postDownloadFallbackFailure = postDownloadFallbackResult;
909
- }
910
- console.warn('[ForceUpdate] GitHub tarball fallback failed (' + postDownloadFallbackFailure.code + '): ' +
911
- postDownloadFallbackFailure.detail);
912
- channel1Failure = _withFallbackFailure(channel1Failure, postDownloadFallbackFailure);
913
- }
914
- try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
915
- } catch (e) {
916
- channel1Failure = _classifyChannel1Error(e, phase);
917
- console.warn('[ForceUpdate] GitHub Release failed (' + channel1Failure.code + '):', e && e.message || e);
918
- var fallbackFailure = _tryDownloadReleaseTarball(requiredVersion, TMP_TARGET);
919
- if (!fallbackFailure) {
920
- var fallbackInstallResult = _installDownloadedTree(
921
- INSTALL_ROOT, TMP_TARGET, requiredVersion, 'GitHub tarball fallback update successful',
922
- );
923
- if (fallbackInstallResult.ok === true) {
924
- return true;
925
- }
926
- fallbackFailure = fallbackInstallResult;
927
- }
928
- if (fallbackFailure) {
929
- console.warn('[ForceUpdate] GitHub tarball fallback failed (' + fallbackFailure.code + '): ' +
930
- fallbackFailure.detail);
931
- channel1Failure = _withFallbackFailure(channel1Failure, fallbackFailure);
932
- }
933
- try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
934
- // Fall through to Channel 2 (manual download URL hint) instead of
935
- // returning. A Channel 1 error (degit missing, network down, tag not
936
- // found) still leaves the user a path forward via the release_url.
937
- }
938
-
939
- // Channel 2: GitHub release (manual download URL only)
940
- try {
941
- var releaseUrl = forceUpdate.release_url;
942
- if (releaseUrl) {
943
- console.log('[ForceUpdate] Channel 2: GitHub release -- manual download required');
944
- console.log('[ForceUpdate] Visit: ' + releaseUrl);
945
- }
946
- } catch (_) {}
947
-
948
- console.warn('[ForceUpdate] All automatic channels exhausted. Current version: ' + getCurrentVersion());
949
- // Surface the concrete Channel 1 failure when we have one (the common case:
950
- // degit/network/copy/version-mismatch). channel1Failure is null only when
951
- // Channel 1 was never entered, which cannot happen here — but fall back to a
952
- // terminal code so the reporter never lands on the legacy "returned false".
953
- return channel1Failure || _fail(FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED,
954
- 'no automatic channel succeeded; current=' + getCurrentVersion() + ' target=' + requiredVersion);
955
- }
956
-
957
- // Test-only hook: re-implements the EXACT same operator-strip + semver
958
- // validation as the runtime force_update check. Exists
959
- // so test/forceUpdateLastUpdateReport.test.js can build a parity sweep
960
- // proving that _extractTargetVersion's (a2aProtocol.js) verdict matches
961
- // forceUpdate.js's verdict byte-for-byte on any input -- the comment at
962
- // a2aProtocol.js:823-833 claims this invariant but a hand-maintained
963
- // regex copy can silently drift. Anything that changes this function
964
- // MUST also update _extractTargetVersion (and vice versa) or the
965
- // parity test breaks.
966
- function _isAcceptedRequiredVersionForTesting(raw) {
967
- if (typeof raw !== 'string') return false;
968
- return normalizeRequiredVersion(raw) !== '';
969
- }
970
-
971
- // Type guard: is `result` a structured failure (vs true / NOOP / BUSY)?
972
- // Call sites use this to decide whether to forward result as opts.failure to
973
- // reportForceUpdateOutcome. Kept tiny and dependency-free so all three
974
- // duplicated triggers (a2aProtocol heartbeat, proxy manager, enrich tick) can
975
- // share one definition.
976
- function isForceUpdateFailure(result) {
977
- return !!result && typeof result === 'object' && result.ok === false && typeof result.code === 'string';
978
- }
979
-
980
- module.exports = {
981
- executeForceUpdate,
982
- FORCE_UPDATE_NOOP,
983
- FORCE_UPDATE_BUSY,
984
- FORCE_UPDATE_FAIL_CODES,
985
- isForceUpdateFailure,
986
- // Test-only hook: reset the in-flight mutex so unit tests do not leak state
987
- // across cases. Production callers must NOT touch this -- the mutex is the
988
- // load-bearing invariant that prevents concurrent state-file writes.
989
- _resetInFlightForTesting: function () { _inFlight = false; },
990
- _isAcceptedRequiredVersionForTesting: _isAcceptedRequiredVersionForTesting,
991
- };