@evomap/evolver 1.89.3 → 1.89.4

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 (104) hide show
  1. package/.cursor/BUGBOT.md +182 -0
  2. package/.env.example +68 -0
  3. package/.git-commit-guard-token +1 -0
  4. package/.github/CODEOWNERS +63 -0
  5. package/.github/ISSUE_TEMPLATE/good_first_issue.md +23 -0
  6. package/.github/pull_request_template.md +45 -0
  7. package/.github/workflows/test.yml +75 -0
  8. package/CHANGELOG.md +1237 -0
  9. package/README.md +86 -528
  10. package/README.public.md +569 -0
  11. package/SECURITY.md +108 -0
  12. package/assets/gep/events.jsonl +3 -0
  13. package/examples/atp-consumer-quickstart.md +100 -0
  14. package/examples/hello-world.md +38 -0
  15. package/index.js +30 -1
  16. package/package.json +6 -17
  17. package/proxy-package.json +39 -0
  18. package/public.manifest.json +143 -0
  19. package/src/config.js +23 -0
  20. package/src/evolve/guards.js +721 -1
  21. package/src/evolve/pipeline/collect.js +1283 -1
  22. package/src/evolve/pipeline/dispatch.js +421 -1
  23. package/src/evolve/pipeline/enrich.js +440 -1
  24. package/src/evolve/pipeline/hub.js +319 -1
  25. package/src/evolve/pipeline/select.js +274 -1
  26. package/src/evolve/pipeline/signals.js +206 -1
  27. package/src/evolve/utils.js +264 -1
  28. package/src/evolve.js +350 -1
  29. package/src/experiment/agentRunner.js +229 -0
  30. package/src/experiment/cli.js +159 -0
  31. package/src/experiment/comparison.js +233 -0
  32. package/src/experiment/metrics.js +75 -0
  33. package/src/forceUpdate.js +147 -59
  34. package/src/gep/a2aProtocol.js +4455 -1
  35. package/src/gep/antiAbuseTelemetry.js +233 -0
  36. package/src/gep/autoDistillConv.js +205 -1
  37. package/src/gep/autoDistillLlm.js +315 -1
  38. package/src/gep/candidateEval.js +92 -1
  39. package/src/gep/candidates.js +198 -1
  40. package/src/gep/contentHash.js +30 -1
  41. package/src/gep/conversationSniffer.js +266 -1
  42. package/src/gep/crypto.js +89 -1
  43. package/src/gep/curriculum.js +163 -1
  44. package/src/gep/deviceId.js +218 -1
  45. package/src/gep/envFingerprint.js +118 -1
  46. package/src/gep/epigenetics.js +31 -1
  47. package/src/gep/execBridge.js +711 -1
  48. package/src/gep/explore.js +289 -1
  49. package/src/gep/hash.js +15 -1
  50. package/src/gep/hubFetch.js +359 -1
  51. package/src/gep/hubReview.js +207 -1
  52. package/src/gep/hubSearch.js +526 -1
  53. package/src/gep/hubVerify.js +306 -1
  54. package/src/gep/learningSignals.js +89 -1
  55. package/src/gep/memoryGraph.js +1374 -1
  56. package/src/gep/memoryGraphAdapter.js +203 -1
  57. package/src/gep/mutation.js +203 -1
  58. package/src/gep/narrativeMemory.js +108 -1
  59. package/src/gep/openPRRegistry.js +205 -1
  60. package/src/gep/personality.js +423 -1
  61. package/src/gep/policyCheck.js +599 -1
  62. package/src/gep/prompt.js +836 -1
  63. package/src/gep/recallInject.js +409 -1
  64. package/src/gep/recallVerifier.js +318 -1
  65. package/src/gep/reflection.js +177 -1
  66. package/src/gep/sanitize.js +9 -0
  67. package/src/gep/selector.js +602 -1
  68. package/src/gep/skillDistiller.js +1294 -1
  69. package/src/gep/solidify.js +1699 -1
  70. package/src/gep/strategy.js +136 -1
  71. package/src/gep/tokenSavings.js +88 -1
  72. package/src/gep/validator/sandboxExecutor.js +29 -1
  73. package/src/gep/workspaceKeychain.js +174 -1
  74. package/src/proxy/extensions/traceControl.js +99 -1
  75. package/src/proxy/index.js +10 -1
  76. package/src/proxy/inject.js +52 -1
  77. package/src/proxy/lifecycle/manager.js +19 -0
  78. package/src/proxy/mailbox/store.js +2 -1
  79. package/src/proxy/router/messages_route.js +5 -2
  80. package/src/proxy/trace/extractor.js +646 -1
  81. package/src/proxy/trace/usage.js +105 -1
  82. package/CONTRIBUTING.md +0 -19
  83. package/assets/cover.png +0 -0
  84. package/scripts/a2a_export.js +0 -63
  85. package/scripts/a2a_ingest.js +0 -79
  86. package/scripts/a2a_promote.js +0 -118
  87. package/scripts/analyze_by_skill.js +0 -121
  88. package/scripts/build_binaries.js +0 -479
  89. package/scripts/check-changelog.js +0 -166
  90. package/scripts/extract_log.js +0 -85
  91. package/scripts/generate_history.js +0 -75
  92. package/scripts/gep_append_event.js +0 -96
  93. package/scripts/gep_personality_report.js +0 -234
  94. package/scripts/human_report.js +0 -147
  95. package/scripts/recall-verify-report.js +0 -234
  96. package/scripts/recover_loop.js +0 -61
  97. package/scripts/refresh_stars_badge.js +0 -168
  98. package/scripts/seed-merchants.js +0 -91
  99. package/scripts/suggest_version.js +0 -89
  100. package/scripts/validate-modules.js +0 -38
  101. package/scripts/validate-suite.js +0 -78
  102. package/skills/index.json +0 -14
  103. /package/assets/gep/{genes.seed.json → genes.json} +0 -0
  104. /package/{skills → bundled-skills}/_meta/SKILL.md +0 -0
@@ -0,0 +1,75 @@
1
+ // src/experiment/metrics.js
2
+ //
3
+ // Pure, table-driven mapping from a human metric label (e.g. "完成耗时 (s)",
4
+ // "轮次", "token", "通过率") onto a per-arm field + comparison direction.
5
+ // No I/O, no side effects -- safe to unit-test in isolation.
6
+ 'use strict';
7
+
8
+ function num(v, fallback) {
9
+ const n = Number(v);
10
+ return Number.isFinite(n) ? n : (fallback === undefined ? 0 : fallback);
11
+ }
12
+
13
+ function round(n, digits) {
14
+ const f = Math.pow(10, digits);
15
+ return Math.round((num(n) + Number.EPSILON) * f) / f;
16
+ }
17
+
18
+ // Ordered rules. The FIRST rule whose any keyword is a (case-insensitive)
19
+ // substring of the metric label wins. Order matters: pass-rate / rounds /
20
+ // tokens / cost are checked before duration so a label like "通过率" is not
21
+ // swallowed by a looser rule.
22
+ const METRIC_RULES = [
23
+ { keys: ['通过率', 'pass', 'success', 'accuracy', '准确', '正确率'], field: 'passRate', lowerIsBetter: false },
24
+ { keys: ['轮次', 'turn', 'round', 'step', 'iteration', '迭代'], field: 'rounds', lowerIsBetter: true },
25
+ { keys: ['token', '令牌'], field: 'tokensTotal', lowerIsBetter: true },
26
+ { keys: ['成本', 'cost', 'usd', '价格', '费用'], field: 'costUsd', lowerIsBetter: true },
27
+ { keys: ['耗时', 'duration', 'latency', '延迟', '秒', 'second', '(s)', 'time'], field: 'durationMs', lowerIsBetter: true },
28
+ ];
29
+
30
+ /**
31
+ * Resolve a metric label to the per-arm field used for scoring, the
32
+ * comparison direction, and the display unit.
33
+ *
34
+ * @param {string} metricStr
35
+ * @returns {{ metricField: string, lowerIsBetter: boolean, scoreUnit: string, recognized: boolean }}
36
+ */
37
+ function deriveMetric(metricStr) {
38
+ const m = String(metricStr || '').toLowerCase();
39
+ for (const rule of METRIC_RULES) {
40
+ if (rule.keys.some((k) => m.includes(String(k).toLowerCase()))) {
41
+ if (rule.field === 'durationMs') {
42
+ // Seconds-flavoured labels ("(s)", "秒", "seconds") -> report in seconds.
43
+ if (/\(s\)|秒|second/.test(m)) {
44
+ return { metricField: 'durationSec', lowerIsBetter: true, scoreUnit: 'seconds', recognized: true };
45
+ }
46
+ return { metricField: 'durationMs', lowerIsBetter: true, scoreUnit: 'ms', recognized: true };
47
+ }
48
+ return { metricField: rule.field, lowerIsBetter: rule.lowerIsBetter, scoreUnit: 'raw', recognized: true };
49
+ }
50
+ }
51
+ // Unrecognized -> degrade to pass-rate (higher is better). Caller records a warning.
52
+ return { metricField: 'passRate', lowerIsBetter: false, scoreUnit: 'raw', recognized: false };
53
+ }
54
+
55
+ /**
56
+ * Pull the scalar score for one arm given the resolved metric field.
57
+ *
58
+ * @param {object} arm a normalized arm (see comparison.normalizeArm)
59
+ * @param {string} metricField
60
+ * @returns {number}
61
+ */
62
+ function scoreArm(arm, metricField) {
63
+ if (!arm) return 0;
64
+ switch (metricField) {
65
+ case 'durationSec': return round(num(arm.durationMs) / 1000, 2);
66
+ case 'durationMs': return num(arm.durationMs);
67
+ case 'rounds': return num(arm.rounds);
68
+ case 'tokensTotal': return num(arm.tokensTotal);
69
+ case 'costUsd': return round(num(arm.costUsd), 4);
70
+ case 'passRate': return round(num(arm.passRate), 4);
71
+ default: return round(num(arm.passRate), 4);
72
+ }
73
+ }
74
+
75
+ module.exports = { deriveMetric, scoreArm, METRIC_RULES, round, num };
@@ -58,6 +58,7 @@ const FORCE_UPDATE_FAIL_CODES = Object.freeze({
58
58
  DEGIT_FAILED: 'degit_failed',
59
59
  DOWNLOAD_INCOMPLETE: 'download_incomplete',
60
60
  DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
61
+ DELETE_FAILED: 'delete_failed',
61
62
  COPY_FAILED: 'copy_failed',
62
63
  ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
63
64
  });
@@ -92,6 +93,10 @@ function _errStr(e) {
92
93
  // 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
93
94
  // 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
94
95
  function _classifyChannel1Error(e, phase) {
96
+ if (phase === 'delete') {
97
+ var deleteEntry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
98
+ return _fail(FORCE_UPDATE_FAIL_CODES.DELETE_FAILED, deleteEntry + _errStr(e));
99
+ }
95
100
  if (phase === 'copy') {
96
101
  var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
97
102
  return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
@@ -134,6 +139,72 @@ function _classifyChannel1Error(e, phase) {
134
139
  return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
135
140
  }
136
141
 
142
+ function _isRetryableFsLockError(e) {
143
+ var code = e && e.code;
144
+ return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' ||
145
+ code === 'ENOTEMPTY' || code === 'EMFILE' || code === 'ENFILE';
146
+ }
147
+
148
+ function _waitForFsLockRetry() {
149
+ var until = Date.now() + 200;
150
+ while (Date.now() < until) { /* spin */ }
151
+ }
152
+
153
+ function _retryFsLockOperation(fn) {
154
+ var err = null;
155
+ for (var attempt = 0; attempt < 3; attempt++) {
156
+ try {
157
+ return fn();
158
+ } catch (e) {
159
+ err = e;
160
+ if (!_isRetryableFsLockError(e)) break;
161
+ if (attempt < 2) _waitForFsLockRetry();
162
+ }
163
+ }
164
+ throw err;
165
+ }
166
+
167
+ function _recoverPackageCommitMarkerIfMissing(installRoot) {
168
+ var pkgDst = path.join(installRoot, 'package.json');
169
+ if (fs.existsSync(pkgDst)) return false;
170
+ var entries;
171
+ try {
172
+ entries = fs.readdirSync(installRoot);
173
+ } catch (_) {
174
+ return false;
175
+ }
176
+ var backups = entries
177
+ .filter(function (name) { return /^package\.json\.\d+\.evolver-old$/.test(name); })
178
+ .sort();
179
+ for (var i = backups.length - 1; i >= 0; i--) {
180
+ try {
181
+ fs.renameSync(path.join(installRoot, backups[i]), pkgDst);
182
+ console.warn('[ForceUpdate] Recovered package.json commit marker from ' + backups[i]);
183
+ return true;
184
+ } catch (_) {}
185
+ }
186
+ return false;
187
+ }
188
+
189
+ function _restorePackageBackup(pkgBackup, pkgDst) {
190
+ if (!fs.existsSync(pkgBackup)) return false;
191
+ if (fs.existsSync(pkgDst)) {
192
+ try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
193
+ return false;
194
+ }
195
+ try {
196
+ fs.renameSync(pkgBackup, pkgDst);
197
+ return true;
198
+ } catch (_) {
199
+ return false;
200
+ }
201
+ }
202
+
203
+ function _isForceUpdateKeepEntry(name) {
204
+ return name === 'node_modules' || name === 'memory' || name === '.git' || name === 'MEMORY.md' ||
205
+ name === '.env' || name === '.env.local' || name === 'USER.md' || name === '.evolver';
206
+ }
207
+
137
208
  // Module-level mutex: shared by every caller that requires('../forceUpdate'),
138
209
  // so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
139
210
  // in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
@@ -263,10 +334,29 @@ function _executeForceUpdateInner(forceUpdate) {
263
334
  'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
264
335
  }
265
336
  } catch (e) {
266
- console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
267
- '/package.json: ' + (e && e.message || e));
268
- return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
269
- 'cannot read install root package.json: ' + _errStr(e));
337
+ if (_recoverPackageCommitMarkerIfMissing(INSTALL_ROOT)) {
338
+ try {
339
+ const recoveredPkg = JSON.parse(fs.readFileSync(path.join(INSTALL_ROOT, 'package.json'), 'utf8'));
340
+ if (!recoveredPkg || (recoveredPkg.name !== '@evomap/evolver' && recoveredPkg.name !== 'evolver')) {
341
+ console.warn('[ForceUpdate] Refusing — recovered ' + INSTALL_ROOT +
342
+ '/package.json has name="' + (recoveredPkg && recoveredPkg.name) +
343
+ '", expected "@evomap/evolver". Aborting to avoid data loss.');
344
+ return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
345
+ 'recovered install root package.json name="' + (recoveredPkg && recoveredPkg.name) +
346
+ '", expected "@evomap/evolver"');
347
+ }
348
+ } catch (recoverReadErr) {
349
+ console.warn('[ForceUpdate] Refusing — cannot read recovered ' + INSTALL_ROOT +
350
+ '/package.json: ' + (recoverReadErr && recoverReadErr.message || recoverReadErr));
351
+ return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
352
+ 'cannot read recovered install root package.json: ' + _errStr(recoverReadErr));
353
+ }
354
+ } else {
355
+ console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
356
+ '/package.json: ' + (e && e.message || e));
357
+ return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
358
+ 'cannot read install root package.json: ' + _errStr(e));
359
+ }
270
360
  }
271
361
 
272
362
  const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
@@ -335,7 +425,7 @@ function _executeForceUpdateInner(forceUpdate) {
335
425
  // terminal `return` can surface it instead of a bare `false`. `phase` tracks
336
426
  // how far we got before any throw, so _classifyChannel1Error can tell a
337
427
  // degit-spawn failure (phase 'degit') from a truncated download (phase
338
- // 'parse') from a copy-into-INSTALL_ROOT failure (phase 'copy').
428
+ // 'parse') from a delete/copy-into-INSTALL_ROOT failure.
339
429
  var channel1Failure = null;
340
430
  var phase = 'degit';
341
431
  try {
@@ -353,7 +443,6 @@ function _executeForceUpdateInner(forceUpdate) {
353
443
  // Require exact version match — a ">=" check would allow a compromised hub to
354
444
  // request version "0.0.1" and install any version including unreleased HEAD code.
355
445
  if (tmpPkg.version && tmpPkg.version === requiredVersion) {
356
- phase = 'copy';
357
446
  var entries = fs.readdirSync(INSTALL_ROOT, { withFileTypes: true });
358
447
  for (var ei = 0; ei < entries.length; ei++) {
359
448
  var eName = entries[ei].name;
@@ -366,37 +455,38 @@ function _executeForceUpdateInner(forceUpdate) {
366
455
  // refuses on an unreadable package.json, wedging the node in
367
456
  // install_guard_unreadable on every subsequent attempt with no path
368
457
  // that ever re-copies it. Deferring it keeps the install self-healing.
369
- if (eName === 'node_modules' || eName === 'memory' || eName === '.git' || eName === 'MEMORY.md'
370
- || eName === '.env' || eName === '.env.local' || eName === 'USER.md' || eName === '.evolver'
371
- || eName === 'package.json') continue;
372
- try { fs.rmSync(path.join(INSTALL_ROOT, eName), { recursive: true, force: true }); } catch (_) {}
458
+ if (_isForceUpdateKeepEntry(eName) || eName === 'package.json') continue;
459
+ try {
460
+ (function (entryName) {
461
+ phase = 'delete';
462
+ _retryFsLockOperation(function () {
463
+ fs.rmSync(path.join(INSTALL_ROOT, entryName), {
464
+ recursive: true, force: true, maxRetries: 3, retryDelay: 200,
465
+ });
466
+ });
467
+ })(eName);
468
+ } catch (rmErr) {
469
+ console.warn('[ForceUpdate] rmSync failed for ' + eName + ': ' + (rmErr.message || rmErr));
470
+ try { rmErr._evolverEntry = eName; } catch (_) {}
471
+ throw rmErr;
472
+ }
373
473
  }
474
+ phase = 'copy';
374
475
  var newEntries = fs.readdirSync(TMP_TARGET, { withFileTypes: true });
375
476
  for (var ni = 0; ni < newEntries.length; ni++) {
376
- // Deferred: package.json is the commit marker, written last + atomically
377
- // after every other entry has copied successfully (see below).
378
- if (newEntries[ni].name === 'package.json') continue;
477
+ // Deferred: package.json is the commit marker, written last after every
478
+ // other entry has copied successfully (see below). Keep-list entries are
479
+ // local state and must not be overwritten by the downloaded release.
480
+ if (newEntries[ni].name === 'package.json' || _isForceUpdateKeepEntry(newEntries[ni].name)) continue;
379
481
  var src = path.join(TMP_TARGET, newEntries[ni].name);
380
482
  var dst = path.join(INSTALL_ROOT, newEntries[ni].name);
381
- // On Windows, files held open by antivirus or the OS itself raise EPERM/EBUSY.
382
- // Retry up to 3 times with a short delay before propagating the error.
383
- var copyErr = null;
384
- for (var attempt = 0; attempt < 3; attempt++) {
385
- try {
386
- fs.cpSync(src, dst, { recursive: true });
387
- copyErr = null;
388
- break;
389
- } catch (cpErr) {
390
- copyErr = cpErr;
391
- var code = cpErr && cpErr.code;
392
- if (code !== 'EPERM' && code !== 'EBUSY' && code !== 'EACCES') break;
393
- // Brief busy-wait — execFileSync has already blocked the event loop,
394
- // so a synchronous spin is acceptable here.
395
- var until = Date.now() + 200;
396
- while (Date.now() < until) { /* spin */ }
397
- }
398
- }
399
- if (copyErr) {
483
+ try {
484
+ (function (copySrc, copyDst) {
485
+ _retryFsLockOperation(function () {
486
+ fs.cpSync(copySrc, copyDst, { recursive: true });
487
+ });
488
+ })(src, dst);
489
+ } catch (copyErr) {
400
490
  console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' + (copyErr.message || copyErr));
401
491
  // Tag the failing entry so _classifyChannel1Error can name it in the
402
492
  // copy_failed detail. phase is already 'copy' here.
@@ -405,40 +495,38 @@ function _executeForceUpdateInner(forceUpdate) {
405
495
  }
406
496
  }
407
497
  // Commit marker: every other entry copied successfully, so swap in the
408
- // new package.json LAST and atomically. The old package.json was kept in
409
- // place above; only this rename makes the new version visible. Net effect:
410
- // - any throw before this point leaves the OLD package.json intact, so
411
- // the install-guard still reads a valid package.json next tick and the
412
- // force-update simply retries (no install_guard_unreadable wedge);
413
- // - the new version becomes "current" only once the tree is fully in
414
- // place, so a partial install never reports as already-satisfied.
415
- // tmp + rename in INSTALL_ROOT (same filesystem) is an atomic replace on
416
- // POSIX; Windows renameSync throws EPERM over an existing dest, so unlink
417
- // first there. Mirrors src/proxy/mailbox/store.js _persistState.
498
+ // new package.json LAST. POSIX gets an atomic rename-over-existing. Windows
499
+ // cannot rename over an existing destination, so it first renames the old
500
+ // package.json to a recoverable same-directory backup; the install guard
501
+ // restores that backup on the next tick if the process dies mid-commit.
418
502
  var pkgSrc = path.join(TMP_TARGET, 'package.json');
419
503
  var pkgDst = path.join(INSTALL_ROOT, 'package.json');
420
504
  var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
421
- var pkgErr = null;
422
- for (var pa = 0; pa < 3; pa++) {
423
- try {
505
+ var pkgBackup = pkgDst + '.' + process.pid + '.evolver-old';
506
+ try {
507
+ _retryFsLockOperation(function () {
508
+ try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
424
509
  fs.cpSync(pkgSrc, pkgTmp);
425
510
  if (process.platform === 'win32') {
426
- try { fs.unlinkSync(pkgDst); } catch (ue) { if (ue && ue.code !== 'ENOENT') throw ue; }
511
+ if (!fs.existsSync(pkgDst)) _recoverPackageCommitMarkerIfMissing(INSTALL_ROOT);
512
+ try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
513
+ fs.renameSync(pkgDst, pkgBackup);
514
+ try {
515
+ fs.renameSync(pkgTmp, pkgDst);
516
+ } catch (commitErr) {
517
+ _restorePackageBackup(pkgBackup, pkgDst);
518
+ throw commitErr;
519
+ }
520
+ try { fs.rmSync(pkgBackup, { force: true }); } catch (_) {}
521
+ } else {
522
+ fs.renameSync(pkgTmp, pkgDst);
427
523
  }
428
- fs.renameSync(pkgTmp, pkgDst);
429
- pkgErr = null;
430
- break;
431
- } catch (pErr) {
432
- pkgErr = pErr;
433
- try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
434
- var pcode = pErr && pErr.code;
435
- if (pcode !== 'EPERM' && pcode !== 'EBUSY' && pcode !== 'EACCES') break;
436
- var puntil = Date.now() + 200;
437
- while (Date.now() < puntil) { /* spin */ }
438
- }
439
- }
440
- if (pkgErr) {
524
+ });
525
+ } catch (pkgErr) {
526
+ _restorePackageBackup(pkgBackup, pkgDst);
527
+ try { fs.rmSync(pkgTmp, { force: true }); } catch (_) {}
441
528
  console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
529
+ try { pkgErr._evolverEntry = 'package.json commit'; } catch (_) {}
442
530
  throw pkgErr;
443
531
  }
444
532
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}