@evomap/evolver 1.89.2 → 1.89.3

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 (65) hide show
  1. package/README.ja-JP.md +1 -3
  2. package/README.ko-KR.md +1 -3
  3. package/README.md +1 -3
  4. package/README.zh-CN.md +1 -3
  5. package/assets/gep/genes.seed.json +251 -0
  6. package/index.js +14 -47
  7. package/package.json +1 -1
  8. package/scripts/refresh_stars_badge.js +168 -0
  9. package/src/adapters/hookAdapter.js +2 -0
  10. package/src/adapters/scripts/_lockPaths.js +74 -0
  11. package/src/adapters/scripts/evolver-session-start.js +19 -27
  12. package/src/evolve/guards.js +1 -1
  13. package/src/evolve/pipeline/collect.js +1 -1
  14. package/src/evolve/pipeline/dispatch.js +1 -1
  15. package/src/evolve/pipeline/enrich.js +1 -1
  16. package/src/evolve/pipeline/hub.js +1 -1
  17. package/src/evolve/pipeline/select.js +1 -1
  18. package/src/evolve/pipeline/signals.js +1 -1
  19. package/src/evolve/utils.js +1 -1
  20. package/src/evolve.js +1 -1
  21. package/src/forceUpdate.js +200 -7
  22. package/src/gep/a2aProtocol.js +1 -1
  23. package/src/gep/autoDistillConv.js +1 -1
  24. package/src/gep/autoDistillLlm.js +1 -1
  25. package/src/gep/candidateEval.js +1 -1
  26. package/src/gep/candidates.js +1 -1
  27. package/src/gep/contentHash.js +1 -1
  28. package/src/gep/conversationSniffer.js +1 -1
  29. package/src/gep/crypto.js +1 -1
  30. package/src/gep/curriculum.js +1 -1
  31. package/src/gep/deviceId.js +1 -1
  32. package/src/gep/envFingerprint.js +1 -1
  33. package/src/gep/epigenetics.js +1 -1
  34. package/src/gep/execBridge.js +1 -1
  35. package/src/gep/explore.js +1 -1
  36. package/src/gep/hash.js +1 -1
  37. package/src/gep/hubFetch.js +1 -1
  38. package/src/gep/hubReview.js +1 -1
  39. package/src/gep/hubSearch.js +1 -1
  40. package/src/gep/hubVerify.js +1 -1
  41. package/src/gep/learningSignals.js +1 -1
  42. package/src/gep/memoryGraph.js +1 -1
  43. package/src/gep/memoryGraphAdapter.js +1 -1
  44. package/src/gep/mutation.js +1 -1
  45. package/src/gep/narrativeMemory.js +1 -1
  46. package/src/gep/openPRRegistry.js +1 -1
  47. package/src/gep/personality.js +1 -1
  48. package/src/gep/policyCheck.js +1 -1
  49. package/src/gep/prompt.js +1 -1
  50. package/src/gep/recallInject.js +1 -1
  51. package/src/gep/recallVerifier.js +1 -1
  52. package/src/gep/reflection.js +1 -1
  53. package/src/gep/selector.js +1 -1
  54. package/src/gep/skillDistiller.js +1 -1
  55. package/src/gep/solidify.js +1 -1
  56. package/src/gep/strategy.js +1 -1
  57. package/src/gep/tokenSavings.js +1 -1
  58. package/src/gep/workspaceKeychain.js +1 -1
  59. package/src/proxy/extensions/traceControl.js +1 -1
  60. package/src/proxy/index.js +4 -4
  61. package/src/proxy/inject.js +1 -1
  62. package/src/proxy/lifecycle/manager.js +11 -0
  63. package/src/proxy/router/messages_route.js +8 -0
  64. package/src/proxy/trace/extractor.js +1 -1
  65. package/src/proxy/trace/usage.js +1 -1
@@ -38,6 +38,102 @@ const FORCE_UPDATE_NOOP = Symbol('FORCE_UPDATE_NOOP');
38
38
  // fire its own reportForceUpdateOutcome. See test/forceUpdateConcurrencyGuard.test.js.
39
39
  const FORCE_UPDATE_BUSY = Symbol('FORCE_UPDATE_BUSY');
40
40
 
41
+ // Structured failure taxonomy. Historically every failing branch of
42
+ // _executeForceUpdateInner just `return false`, so the only thing that ever
43
+ // reached the hub (via reportForceUpdateOutcome) was the literal string
44
+ // "executeForceUpdate returned false" — degit-missing, tag-404, version
45
+ // mismatch and copy-EPERM were all indistinguishable in EvolverUpgradeAttempt.
46
+ // Each branch now returns _fail(code, detail); the reporter encodes it as
47
+ // `error = code + ': ' + detail`, so operators can GROUP BY the code prefix
48
+ // without any hub schema / DB migration. Codes are a small stable set — keep
49
+ // new ones coarse and additive so historical `error LIKE 'code%'` queries
50
+ // don't churn.
51
+ const FORCE_UPDATE_FAIL_CODES = Object.freeze({
52
+ INSTALL_GUARD_NAME_MISMATCH: 'install_guard_name_mismatch',
53
+ INSTALL_GUARD_UNREADABLE: 'install_guard_unreadable',
54
+ BAD_REQUIRED_VERSION: 'bad_required_version',
55
+ CURRENT_VERSION_UNPARSABLE: 'current_version_unparsable',
56
+ NPX_NOT_FOUND: 'npx_not_found',
57
+ DEGIT_TIMEOUT: 'degit_timeout',
58
+ DEGIT_FAILED: 'degit_failed',
59
+ DOWNLOAD_INCOMPLETE: 'download_incomplete',
60
+ DOWNLOADED_VERSION_MISMATCH: 'downloaded_version_mismatch',
61
+ COPY_FAILED: 'copy_failed',
62
+ ALL_CHANNELS_EXHAUSTED: 'all_channels_exhausted',
63
+ });
64
+
65
+ // Build the structured failure result that replaces a bare `return false`.
66
+ // Shape: { ok:false, code, detail }. Distinct from `true`, FORCE_UPDATE_NOOP
67
+ // and FORCE_UPDATE_BUSY, so the three call sites' `result === true` /
68
+ // `result === SENTINEL` checks keep classifying it as "failed" unchanged —
69
+ // this is backward compatible. Frozen so a downstream consumer cannot mutate
70
+ // the code/detail before it is reported. detail is best-effort context (an
71
+ // errno, a version delta, an entry name); it is redacted + truncated to
72
+ // ERROR_MAX by the reporter before it leaves the process.
73
+ function _fail(code, detail) {
74
+ return Object.freeze({
75
+ ok: false,
76
+ code: String(code),
77
+ detail: detail == null ? '' : String(detail),
78
+ });
79
+ }
80
+
81
+ // Compact "CODE: message" rendering of a thrown error for the detail field.
82
+ function _errStr(e) {
83
+ if (!e) return 'unknown';
84
+ var code = e.code ? String(e.code) + ': ' : '';
85
+ return code + (e.message != null ? String(e.message) : String(e));
86
+ }
87
+
88
+ // Map a Channel 1 (GitHub Release / degit) throw to a structured failure.
89
+ // `phase` records how far the try block got before throwing, so a readFileSync
90
+ // ENOENT (truncated download) is not misread as an npx ENOENT (npx missing):
91
+ // 'degit' -> the npx/degit spawn itself
92
+ // 'parse' -> degit exited 0 but the downloaded package.json is missing/invalid
93
+ // 'copy' -> the staged tree downloaded fine but cpSync into INSTALL_ROOT failed
94
+ function _classifyChannel1Error(e, phase) {
95
+ if (phase === 'copy') {
96
+ var entry = e && e._evolverEntry ? String(e._evolverEntry) + ': ' : '';
97
+ return _fail(FORCE_UPDATE_FAIL_CODES.COPY_FAILED, entry + _errStr(e));
98
+ }
99
+ if (phase === 'parse') {
100
+ return _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
101
+ 'missing/invalid package.json in downloaded tree: ' + _errStr(e));
102
+ }
103
+ // phase === 'degit' (the spawn). ENOENT here is the npx binary itself, not a
104
+ // file inside the download — that distinction is exactly why `phase` exists.
105
+ if (e && e.code === 'ENOENT') {
106
+ return _fail(FORCE_UPDATE_FAIL_CODES.NPX_NOT_FOUND, _errStr(e));
107
+ }
108
+ // execFileSync timeout kills the child with SIGTERM (and sets .killed); some
109
+ // platforms surface ETIMEDOUT instead. Either way it is a 60s timeout.
110
+ if (e && (e.killed || e.signal === 'SIGTERM' || e.code === 'ETIMEDOUT')) {
111
+ return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_TIMEOUT,
112
+ 'degit timed out after 60s' + (e.signal ? ' (signal=' + e.signal + ')' : ''));
113
+ }
114
+ // Generic degit/network/tag-not-found failure. degit prints the real reason
115
+ // ("could not find commit hash for v…", "could not resolve host") to stderr,
116
+ // so keep a tail of it. Redact + strip control chars HERE, before the tail
117
+ // slice: the downstream reporter redact (a2aProtocol.reportForceUpdateOutcome)
118
+ // runs after this, so slicing first could chop a token's prefix anchor and
119
+ // let the bare value slip past the prefix-anchored redact patterns. Stripping
120
+ // ANSI/NUL/newlines also keeps the persisted error free of terminal-injection
121
+ // sequences and log-line noise.
122
+ var detail = _errStr(e);
123
+ var stderr = '';
124
+ if (e && e.stderr != null) {
125
+ try {
126
+ var redactString = require('./gep/sanitize').redactString;
127
+ stderr = redactString(String(e.stderr)).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
128
+ } catch (_) {
129
+ // sanitize unavailable — still strip control chars so logs stay clean.
130
+ stderr = String(e.stderr).replace(/[\x00-\x1f\x7f]/g, ' ').trim();
131
+ }
132
+ }
133
+ if (stderr) detail += ' | stderr=' + stderr.slice(-300);
134
+ return _fail(FORCE_UPDATE_FAIL_CODES.DEGIT_FAILED, detail);
135
+ }
136
+
41
137
  // Module-level mutex: shared by every caller that requires('../forceUpdate'),
42
138
  // so the heartbeat-thread trigger in a2aProtocol.js and the evolve-tick path
43
139
  // in enrich/pipeline cannot run executeForceUpdate concurrently. This is a
@@ -163,12 +259,14 @@ function _executeForceUpdateInner(forceUpdate) {
163
259
  console.warn('[ForceUpdate] Refusing — ' + INSTALL_ROOT +
164
260
  '/package.json has name="' + (pkg && pkg.name) +
165
261
  '", expected "@evomap/evolver". Aborting to avoid data loss.');
166
- return false;
262
+ return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_NAME_MISMATCH,
263
+ 'install root package.json name="' + (pkg && pkg.name) + '", expected "@evomap/evolver"');
167
264
  }
168
265
  } catch (e) {
169
266
  console.warn('[ForceUpdate] Refusing — cannot read ' + INSTALL_ROOT +
170
267
  '/package.json: ' + (e && e.message || e));
171
- return false;
268
+ return _fail(FORCE_UPDATE_FAIL_CODES.INSTALL_GUARD_UNREADABLE,
269
+ 'cannot read install root package.json: ' + _errStr(e));
172
270
  }
173
271
 
174
272
  const requiredVersion = normalizeRequiredVersion(forceUpdate.required_version);
@@ -176,7 +274,8 @@ function _executeForceUpdateInner(forceUpdate) {
176
274
  console.warn('[ForceUpdate] Refusing — required_version "' +
177
275
  String(forceUpdate.required_version || '').replace(/^[>=^~\s]+/, '') +
178
276
  '" is not a concrete semver (ranges not accepted).');
179
- return false;
277
+ return _fail(FORCE_UPDATE_FAIL_CODES.BAD_REQUIRED_VERSION,
278
+ 'required_version=' + JSON.stringify(forceUpdate && forceUpdate.required_version) + ' is not a concrete semver');
180
279
  }
181
280
 
182
281
  function getCurrentVersion() {
@@ -207,7 +306,8 @@ function _executeForceUpdateInner(forceUpdate) {
207
306
  if (versionCmp === null) {
208
307
  console.warn('[ForceUpdate] Refusing — current installed version "' +
209
308
  currentVersion + '" is not a concrete semver.');
210
- return false;
309
+ return _fail(FORCE_UPDATE_FAIL_CODES.CURRENT_VERSION_UNPARSABLE,
310
+ 'current installed version "' + currentVersion + '" is not a concrete semver');
211
311
  }
212
312
  if (versionCmp >= 0) {
213
313
  console.log('[ForceUpdate] already satisfies required version, no-op (current=' +
@@ -230,6 +330,14 @@ function _executeForceUpdateInner(forceUpdate) {
230
330
  const TMP_TARGET = fs.mkdtempSync(path.join(os.tmpdir(), '.evolver-update-tmp-'));
231
331
 
232
332
  // Channel 1: GitHub Release (via degit pinned to exact version tag)
333
+ //
334
+ // channel1Failure captures the structured reason this channel failed, so the
335
+ // terminal `return` can surface it instead of a bare `false`. `phase` tracks
336
+ // how far we got before any throw, so _classifyChannel1Error can tell a
337
+ // degit-spawn failure (phase 'degit') from a truncated download (phase
338
+ // 'parse') from a copy-into-INSTALL_ROOT failure (phase 'copy').
339
+ var channel1Failure = null;
340
+ var phase = 'degit';
233
341
  try {
234
342
  console.log('[ForceUpdate] Channel 1: GitHub Release download (v' + requiredVersion + ')...');
235
343
  var npxBin = process.platform === 'win32' ? 'npx.cmd' : 'npx';
@@ -240,19 +348,34 @@ function _executeForceUpdateInner(forceUpdate) {
240
348
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
241
349
  timeout: 60000, windowsHide: true, maxBuffer: MAX_EXEC_BUFFER,
242
350
  });
351
+ phase = 'parse';
243
352
  var tmpPkg = JSON.parse(fs.readFileSync(path.join(TMP_TARGET, 'package.json'), 'utf8'));
244
353
  // Require exact version match — a ">=" check would allow a compromised hub to
245
354
  // request version "0.0.1" and install any version including unreleased HEAD code.
246
355
  if (tmpPkg.version && tmpPkg.version === requiredVersion) {
356
+ phase = 'copy';
247
357
  var entries = fs.readdirSync(INSTALL_ROOT, { withFileTypes: true });
248
358
  for (var ei = 0; ei < entries.length; ei++) {
249
359
  var eName = entries[ei].name;
360
+ // package.json is the install's commit marker: keep the OLD one in
361
+ // place through the entire delete+copy below and swap in the new one
362
+ // atomically at the very end (see "commit marker" block). If it were
363
+ // deleted here and any later cpSync threw (ENOSPC, a Windows lock that
364
+ // outlasts the retries, a kill), the install root would be left with
365
+ // no package.json — and the install-guard at the top of this function
366
+ // refuses on an unreadable package.json, wedging the node in
367
+ // install_guard_unreadable on every subsequent attempt with no path
368
+ // that ever re-copies it. Deferring it keeps the install self-healing.
250
369
  if (eName === 'node_modules' || eName === 'memory' || eName === '.git' || eName === 'MEMORY.md'
251
- || eName === '.env' || eName === '.env.local' || eName === 'USER.md' || eName === '.evolver') continue;
370
+ || eName === '.env' || eName === '.env.local' || eName === 'USER.md' || eName === '.evolver'
371
+ || eName === 'package.json') continue;
252
372
  try { fs.rmSync(path.join(INSTALL_ROOT, eName), { recursive: true, force: true }); } catch (_) {}
253
373
  }
254
374
  var newEntries = fs.readdirSync(TMP_TARGET, { withFileTypes: true });
255
375
  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;
256
379
  var src = path.join(TMP_TARGET, newEntries[ni].name);
257
380
  var dst = path.join(INSTALL_ROOT, newEntries[ni].name);
258
381
  // On Windows, files held open by antivirus or the OS itself raise EPERM/EBUSY.
@@ -275,16 +398,70 @@ function _executeForceUpdateInner(forceUpdate) {
275
398
  }
276
399
  if (copyErr) {
277
400
  console.warn('[ForceUpdate] cpSync failed for ' + newEntries[ni].name + ': ' + (copyErr.message || copyErr));
401
+ // Tag the failing entry so _classifyChannel1Error can name it in the
402
+ // copy_failed detail. phase is already 'copy' here.
403
+ try { copyErr._evolverEntry = newEntries[ni].name; } catch (_) {}
278
404
  throw copyErr;
279
405
  }
280
406
  }
407
+ // 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.
418
+ var pkgSrc = path.join(TMP_TARGET, 'package.json');
419
+ var pkgDst = path.join(INSTALL_ROOT, 'package.json');
420
+ var pkgTmp = pkgDst + '.' + process.pid + '.evolver-tmp';
421
+ var pkgErr = null;
422
+ for (var pa = 0; pa < 3; pa++) {
423
+ try {
424
+ fs.cpSync(pkgSrc, pkgTmp);
425
+ if (process.platform === 'win32') {
426
+ try { fs.unlinkSync(pkgDst); } catch (ue) { if (ue && ue.code !== 'ENOENT') throw ue; }
427
+ }
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) {
441
+ console.warn('[ForceUpdate] package.json commit (atomic replace) failed: ' + (pkgErr.message || pkgErr));
442
+ throw pkgErr;
443
+ }
281
444
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
282
445
  console.log('[ForceUpdate] GitHub Release update successful: ' + tmpPkg.version);
283
446
  return true;
284
447
  }
448
+ // degit succeeded and produced a parseable package.json, but it did not
449
+ // satisfy the exact-version check above. Two distinct causes, two codes:
450
+ if (!tmpPkg.version) {
451
+ // degit produced a parseable package.json with no version field — a
452
+ // malformed/incomplete download, not a stale/tampered tag mismatch.
453
+ channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOAD_INCOMPLETE,
454
+ 'downloaded package.json has no version field');
455
+ } else {
456
+ // version present but not the exact tag we asked for (stale tag, mirror
457
+ // lag, or a tampered/redirected tag). Refuse and record the delta.
458
+ channel1Failure = _fail(FORCE_UPDATE_FAIL_CODES.DOWNLOADED_VERSION_MISMATCH,
459
+ 'downloaded version=' + JSON.stringify(tmpPkg.version) + ', expected ' + requiredVersion);
460
+ }
285
461
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
286
462
  } catch (e) {
287
- console.warn('[ForceUpdate] GitHub Release failed:', e && e.message || e);
463
+ channel1Failure = _classifyChannel1Error(e, phase);
464
+ console.warn('[ForceUpdate] GitHub Release failed (' + channel1Failure.code + '):', e && e.message || e);
288
465
  try { fs.rmSync(TMP_TARGET, { recursive: true, force: true }); } catch (_) {}
289
466
  // Fall through to Channel 2 (manual download URL hint) instead of
290
467
  // returning. A Channel 1 error (degit missing, network down, tag not
@@ -301,7 +478,12 @@ function _executeForceUpdateInner(forceUpdate) {
301
478
  } catch (_) {}
302
479
 
303
480
  console.warn('[ForceUpdate] All automatic channels exhausted. Current version: ' + getCurrentVersion());
304
- return false;
481
+ // Surface the concrete Channel 1 failure when we have one (the common case:
482
+ // degit/network/copy/version-mismatch). channel1Failure is null only when
483
+ // Channel 1 was never entered, which cannot happen here — but fall back to a
484
+ // terminal code so the reporter never lands on the legacy "returned false".
485
+ return channel1Failure || _fail(FORCE_UPDATE_FAIL_CODES.ALL_CHANNELS_EXHAUSTED,
486
+ 'no automatic channel succeeded; current=' + getCurrentVersion() + ' target=' + requiredVersion);
305
487
  }
306
488
 
307
489
  // Test-only hook: re-implements the EXACT same operator-strip + semver
@@ -318,10 +500,21 @@ function _isAcceptedRequiredVersionForTesting(raw) {
318
500
  return normalizeRequiredVersion(raw) !== '';
319
501
  }
320
502
 
503
+ // Type guard: is `result` a structured failure (vs true / NOOP / BUSY)?
504
+ // Call sites use this to decide whether to forward result as opts.failure to
505
+ // reportForceUpdateOutcome. Kept tiny and dependency-free so all three
506
+ // duplicated triggers (a2aProtocol heartbeat, proxy manager, enrich tick) can
507
+ // share one definition.
508
+ function isForceUpdateFailure(result) {
509
+ return !!result && typeof result === 'object' && result.ok === false && typeof result.code === 'string';
510
+ }
511
+
321
512
  module.exports = {
322
513
  executeForceUpdate,
323
514
  FORCE_UPDATE_NOOP,
324
515
  FORCE_UPDATE_BUSY,
516
+ FORCE_UPDATE_FAIL_CODES,
517
+ isForceUpdateFailure,
325
518
  // Test-only hook: reset the in-flight mutex so unit tests do not leak state
326
519
  // across cases. Production callers must NOT touch this -- the mutex is the
327
520
  // load-bearing invariant that prevents concurrent state-file writes.