@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
package/README.ja-JP.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # 🧬 Evolver
2
2
 
3
- [![GitHub stars](https://img.shields.io/github/stars/EvoMap/evolver?style=social)](https://github.com/EvoMap/evolver/stargazers)
3
+ [![GitHub stars](https://img.shields.io/badge/Stars-8.5k-2b3137?logo=github&logoColor=white)](https://github.com/EvoMap/evolver/stargazers)
4
4
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](https://opensource.org/licenses/GPL-3.0)
5
5
  [![Node.js >= 18](https://img.shields.io/badge/Node.js-%3E%3D%2018-green.svg)](https://nodejs.org/)
6
- [![GitHub last commit](https://img.shields.io/github/last-commit/EvoMap/evolver)](https://github.com/EvoMap/evolver/commits/main)
7
6
  [![npm downloads](https://img.shields.io/npm/dm/@evomap/evolver.svg)](https://www.npmjs.com/package/@evomap/evolver)
8
- [![GitHub issues](https://img.shields.io/github/issues/EvoMap/evolver)](https://github.com/EvoMap/evolver/issues)
9
7
  [![arXiv](https://img.shields.io/badge/arXiv-2604.15097-b31b1b.svg)](https://arxiv.org/abs/2604.15097)
10
8
 
11
9
  ![Evolver Cover](assets/cover.png)
package/README.ko-KR.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # 🧬 Evolver
2
2
 
3
- [![GitHub stars](https://img.shields.io/github/stars/EvoMap/evolver?style=social)](https://github.com/EvoMap/evolver/stargazers)
3
+ [![GitHub stars](https://img.shields.io/badge/Stars-8.5k-2b3137?logo=github&logoColor=white)](https://github.com/EvoMap/evolver/stargazers)
4
4
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](https://opensource.org/licenses/GPL-3.0)
5
5
  [![Node.js >= 18](https://img.shields.io/badge/Node.js-%3E%3D%2018-green.svg)](https://nodejs.org/)
6
- [![GitHub last commit](https://img.shields.io/github/last-commit/EvoMap/evolver)](https://github.com/EvoMap/evolver/commits/main)
7
6
  [![npm downloads](https://img.shields.io/npm/dm/@evomap/evolver.svg)](https://www.npmjs.com/package/@evomap/evolver)
8
- [![GitHub issues](https://img.shields.io/github/issues/EvoMap/evolver)](https://github.com/EvoMap/evolver/issues)
9
7
  [![arXiv](https://img.shields.io/badge/arXiv-2604.15097-b31b1b.svg)](https://arxiv.org/abs/2604.15097)
10
8
 
11
9
  ![Evolver Cover](assets/cover.png)
package/README.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # 🧬 Evolver
2
2
 
3
- [![GitHub stars](https://img.shields.io/github/stars/EvoMap/evolver?style=social)](https://github.com/EvoMap/evolver/stargazers)
3
+ [![GitHub stars](https://img.shields.io/badge/Stars-8.5k-2b3137?logo=github&logoColor=white)](https://github.com/EvoMap/evolver/stargazers)
4
4
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](https://opensource.org/licenses/GPL-3.0)
5
5
  [![Node.js >= 18](https://img.shields.io/badge/Node.js-%3E%3D%2018-green.svg)](https://nodejs.org/)
6
- [![GitHub last commit](https://img.shields.io/github/last-commit/EvoMap/evolver)](https://github.com/EvoMap/evolver/commits/main)
7
6
  [![npm downloads](https://img.shields.io/npm/dm/@evomap/evolver.svg)](https://www.npmjs.com/package/@evomap/evolver)
8
- [![GitHub issues](https://img.shields.io/github/issues/EvoMap/evolver)](https://github.com/EvoMap/evolver/issues)
9
7
  [![arXiv](https://img.shields.io/badge/arXiv-2604.15097-b31b1b.svg)](https://arxiv.org/abs/2604.15097)
10
8
 
11
9
  ![Evolver Cover](assets/cover.png)
package/README.zh-CN.md CHANGED
@@ -1,11 +1,9 @@
1
1
  # 🧬 Evolver
2
2
 
3
- [![GitHub stars](https://img.shields.io/github/stars/EvoMap/evolver?style=social)](https://github.com/EvoMap/evolver/stargazers)
3
+ [![GitHub stars](https://img.shields.io/badge/Stars-8.5k-2b3137?logo=github&logoColor=white)](https://github.com/EvoMap/evolver/stargazers)
4
4
  [![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](https://opensource.org/licenses/GPL-3.0)
5
5
  [![Node.js >= 18](https://img.shields.io/badge/Node.js-%3E%3D%2018-green.svg)](https://nodejs.org/)
6
- [![GitHub last commit](https://img.shields.io/github/last-commit/EvoMap/evolver)](https://github.com/EvoMap/evolver/commits/main)
7
6
  [![npm downloads](https://img.shields.io/npm/dm/@evomap/evolver.svg)](https://www.npmjs.com/package/@evomap/evolver)
8
- [![GitHub issues](https://img.shields.io/github/issues/EvoMap/evolver)](https://github.com/EvoMap/evolver/issues)
9
7
  [![arXiv](https://img.shields.io/badge/arXiv-2604.15097-b31b1b.svg)](https://arxiv.org/abs/2604.15097)
10
8
 
11
9
  ![Evolver Cover](assets/cover.png)
@@ -240,6 +240,257 @@
240
240
  "tier": "cheap",
241
241
  "reasoning_level": "low"
242
242
  }
243
+ },
244
+ {
245
+ "type": "Gene",
246
+ "id": "gene_publish_feishu_doc",
247
+ "category": "innovate",
248
+ "signals_match": [
249
+ "publish_markdown_to_feishu",
250
+ "create_feishu_doc",
251
+ "export_report_to_feishu",
252
+ "把结果发到飞书文档",
253
+ "发布飞书文档",
254
+ "把报告导出到飞书",
255
+ "publish results to a feishu doc",
256
+ "export notes to lark document",
257
+ "飞书文档",
258
+ "发布到飞书",
259
+ "导出到飞书",
260
+ "发到飞书",
261
+ "lark文档",
262
+ "飞书文档|lark doc|feishu doc"
263
+ ],
264
+ "strategy": [
265
+ "Verify the toolchain: run `lark-cli doctor` and require ok:true with at least one ready identity",
266
+ "Always use the Docs v2 API (v1 is deprecated): pass `--api-version v2`",
267
+ "Write the body as Lark-flavored Markdown to a temp file and pass `--content @file.md --doc-format markdown` to avoid shell-escaping bugs",
268
+ "Create with the user identity so the doc is human-owned: `lark-cli docs +create --api-version v2 --as user --doc-format markdown --content @file.md`",
269
+ "To place it in a folder or wiki add `--parent-token <token>` (use `--parent-position my_library` for the personal space)",
270
+ "Parse data.document.url from the JSON response and return it to the user; use `docs +update --api-version v2` with the document_id to amend instead of recreating"
271
+ ],
272
+ "validation": [
273
+ "node --version"
274
+ ],
275
+ "constraints": {
276
+ "max_files": 2,
277
+ "forbidden_paths": [
278
+ ".git",
279
+ "node_modules",
280
+ "~/.lark-cli/config.json"
281
+ ]
282
+ },
283
+ "preconditions": [
284
+ "lark-cli installed and on PATH (npm i -g @larksuite/cli)",
285
+ "lark-cli auth status reports a ready user or bot identity"
286
+ ],
287
+ "summary": "Publish Markdown content as a Feishu/Lark document via the official lark-cli (Docs v2). Use --as user for human-owned docs and @file content for long bodies; return the resulting document URL.",
288
+ "schema_version": "1.6.0",
289
+ "epigenetic_marks": [],
290
+ "learning_history": [],
291
+ "anti_patterns": [],
292
+ "routing_hint": null,
293
+ "tool_policy": null,
294
+ "avoid": [
295
+ "using the deprecated Docs v1 API or the v1 --markdown flag",
296
+ "passing long markdown inline (shell-escaping corrupts it) instead of --content @file",
297
+ "overwriting ~/.lark-cli/config.json (holds the app secret and tokens)"
298
+ ],
299
+ "asset_id": "sha256:9ed275fd6394567d0eb6c0fda45193bbeaba7bd84941ea4e75eb7fc859fb0dcf"
300
+ },
301
+ {
302
+ "type": "Gene",
303
+ "id": "gene_conventional_git_commit",
304
+ "category": "optimize",
305
+ "signals_match": [
306
+ "git_commit",
307
+ "create_commit",
308
+ "commit_changes",
309
+ "conventional_commit",
310
+ "提交代码",
311
+ "生成提交信息",
312
+ "write a commit message",
313
+ "stage and commit"
314
+ ],
315
+ "strategy": [
316
+ "Inspect the change: `git diff --staged` if anything is staged, else `git diff`, plus `git status --porcelain`",
317
+ "Pick a Conventional Commits type (feat/fix/docs/style/refactor/perf/test/build/ci/chore/revert) and optional scope from what actually changed",
318
+ "Stage logically-grouped files explicitly (git add <paths>); NEVER stage or commit secrets (.env, credentials, private keys)",
319
+ "Write a present-tense imperative description under 72 chars; add a body/footer for breaking changes (type! or BREAKING CHANGE:) and issue refs (Closes #N)",
320
+ "Commit one logical change with `git commit -m` (heredoc for multi-line)",
321
+ "Safety: never touch git config, never --force/hard-reset/--no-verify without explicit request, never force-push main; if a hook fails, fix and make a NEW commit (do not amend)"
322
+ ],
323
+ "validation": [
324
+ "node --version"
325
+ ],
326
+ "constraints": {
327
+ "max_files": 50,
328
+ "forbidden_paths": [
329
+ ".git",
330
+ "node_modules"
331
+ ]
332
+ },
333
+ "preconditions": [
334
+ "a git repository with staged or unstaged changes"
335
+ ],
336
+ "summary": "Create a Conventional Commits-style git commit: analyze the diff to pick type/scope, stage logical groups (never secrets), and write an imperative <72-char message.",
337
+ "schema_version": "1.6.0",
338
+ "epigenetic_marks": [],
339
+ "learning_history": [],
340
+ "anti_patterns": [],
341
+ "routing_hint": null,
342
+ "tool_policy": null,
343
+ "avoid": [
344
+ "committing secrets or unrelated changes in one commit",
345
+ "amending or force-pushing to bypass a failing hook",
346
+ "past-tense or vague messages like \"updated stuff\""
347
+ ],
348
+ "asset_id": "sha256:505c207b9984c397255daed61c5f24fb3bfcadedb803d2a4eaa429457f08cd2f"
349
+ },
350
+ {
351
+ "type": "Gene",
352
+ "id": "gene_poll_bugbot_review",
353
+ "category": "optimize",
354
+ "signals_match": [
355
+ "poll_bugbot",
356
+ "bugbot_review",
357
+ "wait_for_ci_review",
358
+ "pr_review_gate",
359
+ "等bugbot",
360
+ "等待评审",
361
+ "check bugbot",
362
+ "review the pr",
363
+ "pr opened"
364
+ ],
365
+ "strategy": [
366
+ "Poll the \"Cursor Bugbot\" check via `gh pr view --json statusCheckRollup` every ~60s until status=COMPLETED (cap ~10min); filter by name, not index",
367
+ "On SUCCESS: safe to merge ONLY if no other required check is red AND zero open inline comments from the cursor[bot] login (note the [bot] suffix)",
368
+ "On NEUTRAL: do NOT treat as pass — fetch inline comments `gh api repos/:o/:r/pulls/:n/comments` filtered to user.login==\"cursor[bot]\", surface path/line/severity, hand back to the human",
369
+ "On FAILURE/ACTION_REQUIRED: surface findings, do not merge",
370
+ "Auto-merge (squash + delete-branch) only when explicitly authorized AND conclusion is SUCCESS; merge conflicts/CI-red/required-review surface verbatim, never auto-fixed here"
371
+ ],
372
+ "validation": [
373
+ "node --version"
374
+ ],
375
+ "constraints": {
376
+ "max_files": 1,
377
+ "forbidden_paths": [
378
+ ".git",
379
+ "node_modules"
380
+ ]
381
+ },
382
+ "preconditions": [
383
+ "an open GitHub PR in a repo where Cursor Bugbot runs"
384
+ ],
385
+ "summary": "Wait for Cursor Bugbot on a GitHub PR, then gate on the conclusion: SUCCESS may merge, NEUTRAL/FAILURE always pauses to surface inline findings to the human.",
386
+ "schema_version": "1.6.0",
387
+ "epigenetic_marks": [],
388
+ "learning_history": [],
389
+ "anti_patterns": [],
390
+ "routing_hint": null,
391
+ "tool_policy": null,
392
+ "avoid": [
393
+ "treating NEUTRAL as pass (it has shipped real bugs before)",
394
+ "filtering comments on \"cursor\" instead of \"cursor[bot]\" (silently returns nothing)",
395
+ "auto-merging without explicit authorization or with CI red"
396
+ ],
397
+ "asset_id": "sha256:0f50f4cfecb0e6f3a9bd3c9c0a426e56f4f1c0230b4836a9471b38e499410ea9"
398
+ },
399
+ {
400
+ "type": "Gene",
401
+ "id": "gene_gateway_timeout_recovery",
402
+ "category": "repair",
403
+ "signals_match": [
404
+ "gateway_timeout",
405
+ "upstream_timeout",
406
+ "http_524",
407
+ "request_timed_out",
408
+ "超时了",
409
+ "网关超时",
410
+ "遇到超时",
411
+ "retry on timeout",
412
+ "operation timed out"
413
+ ],
414
+ "strategy": [
415
+ "Treat it as transient or size-driven, not a logic failure; do not report it as a hard failure before recovering",
416
+ "Retry the SAME operation verbatim exactly ONCE (a large fraction clear on immediate retry); do not loop",
417
+ "If it times out again, STOP retrying the monolith: split the work along a natural seam (per-file/dir/endpoint/record/section/time-window) into small independent units",
418
+ "Dispatch the units as parallel subagents in a single batch so each finishes under the gateway deadline; merge their results",
419
+ "If one unit itself times out, apply this same procedure recursively to that slice"
420
+ ],
421
+ "validation": [
422
+ "node --version"
423
+ ],
424
+ "constraints": {
425
+ "max_files": 1,
426
+ "forbidden_paths": [
427
+ ".git",
428
+ "node_modules"
429
+ ]
430
+ },
431
+ "preconditions": [
432
+ "a tool call / fetch / subagent / long command returned a gateway-class timeout (524/522/502/504)"
433
+ ],
434
+ "summary": "Recover from a gateway/upstream timeout: retry the same call once, and if it still times out, decompose the work into parallel subagents and merge — never loop the monolithic call.",
435
+ "schema_version": "1.6.0",
436
+ "epigenetic_marks": [],
437
+ "learning_history": [],
438
+ "anti_patterns": [],
439
+ "routing_hint": null,
440
+ "tool_policy": null,
441
+ "avoid": [
442
+ "retrying the same large call more than once",
443
+ "serial retries instead of parallel decomposition",
444
+ "surfacing the timeout as a hard failure before recovering"
445
+ ],
446
+ "asset_id": "sha256:63c4251dcd8308030194f797051c08672691b52553e00b0eb33772c215712acc"
447
+ },
448
+ {
449
+ "type": "Gene",
450
+ "id": "gene_github_webhook_listener",
451
+ "category": "innovate",
452
+ "signals_match": [
453
+ "github_webhook_listener",
454
+ "bugbot_webhook",
455
+ "passive_pr_notifications",
456
+ "设置webhook",
457
+ "部署webhook监听",
458
+ "notify when bugbot finishes",
459
+ "webhook tunnel"
460
+ ],
461
+ "strategy": [
462
+ "Run the idempotent deploy: a loopback Python listener (127.0.0.1:8644) validating GitHub X-Hub-Signature-256 HMAC via hmac.compare_digest, writing vetted payloads to ~/.claude/inbox/",
463
+ "Expose it via a cloudflared quick tunnel (outbound-only, no inbound port); a path-watcher re-PATCHes the GitHub webhook config whenever the tunnel URL changes",
464
+ "Keep listener + tunnel alive with systemd --user units hardened (ProtectSystem=strict, NoNewPrivileges, MemoryDenyWriteExecute); a SessionStart hook drains the inbox and re-validates PR state via gh api before surfacing",
465
+ "Security invariants: HMAC on every request, X-GitHub-Delivery dedup against replay, write-only sink (never exec/template/deserialize payload), file modes secret 0600 / inbox 0700",
466
+ "Add repos with deploy.sh --repos; rotate the secret every 90 days (rotate-secret.sh) and immediately on any leak; never trust the inbox payload without re-fetching"
467
+ ],
468
+ "validation": [
469
+ "node --version"
470
+ ],
471
+ "constraints": {
472
+ "max_files": 20,
473
+ "forbidden_paths": [
474
+ ".git",
475
+ "node_modules"
476
+ ]
477
+ },
478
+ "preconditions": [
479
+ "a developer machine with systemd --user and cloudflared available"
480
+ ],
481
+ "summary": "Deploy a per-developer GitHub webhook listener (HMAC-validated, cloudflared tunnel, systemd-kept) that drops PR/Bugbot events into ~/.claude/inbox for the next session to surface.",
482
+ "schema_version": "1.6.0",
483
+ "epigenetic_marks": [],
484
+ "learning_history": [],
485
+ "anti_patterns": [],
486
+ "routing_hint": null,
487
+ "tool_policy": null,
488
+ "avoid": [
489
+ "opening an inbound port instead of an outbound cloudflared tunnel",
490
+ "trusting the webhook payload without HMAC validation and PR re-fetch",
491
+ "execing or deserializing anything from the payload"
492
+ ],
493
+ "asset_id": "sha256:ac2a2f185390aef37996651ef21355f4beb437049e52a6ca3898619a8d648084"
243
494
  }
244
495
  ]
245
496
  }
package/index.js CHANGED
@@ -268,23 +268,17 @@ function getLastSignals(statePath) {
268
268
 
269
269
  // Singleton Guard - prevent multiple evolver daemon instances.
270
270
  //
271
- // Round-4: pidfile location previously defaulted to __dirname, which is a
272
- // DIFFERENT path per install mode -- /usr/local/lib/node_modules/... for a
273
- // global install, the dev-clone path for `node index.js`, a transient
274
- // $NPM_CACHE/_npx/<hash> for `npx evolver`. Two daemons launched under
275
- // different install modes never saw each other's lock and could run
276
- // concurrently against the same ~/.evomap/node_secret, ping-ponging on
277
- // secret rotation and silently entering reauth backoff -- the user-
278
- // reported "first launch ok, idle, then dead forever" pattern. Default
279
- // now lives under the per-user state dir so all install modes converge.
280
- // EVOLVER_LOCK_DIR still overrides for tests / sandboxed runs.
281
- function getLockFilePath() {
282
- if (process.env.EVOLVER_LOCK_DIR) {
283
- return path.join(process.env.EVOLVER_LOCK_DIR, 'evolver.pid');
284
- }
285
- // os.homedir() is cross-platform; process.env.HOME is unset on Windows.
286
- return path.join(os.homedir(), '.evomap', 'instance.lock');
287
- }
271
+ // Lock location + lease tunables live in src/adapters/scripts/_lockPaths.js
272
+ // (issue #176): the session-start hook's auto-restart guard needs the exact
273
+ // same resolution, and inlining it in both places drifted. The Round-4
274
+ // (per-install-mode pidfile convergence) and Round-9 (lease staleness)
275
+ // history notes moved there with the code.
276
+ const {
277
+ getLockFilePath,
278
+ lockIsStaleByLease: _lockIsStaleByLease,
279
+ STALE_LOCK_TTL_MS,
280
+ LOCK_REFRESH_MS,
281
+ } = require('./src/adapters/scripts/_lockPaths');
288
282
 
289
283
  function _writeLockAtomic(lockFile, payload) {
290
284
  // Round-6 (§19.8): the previous implementation used tmp + rename, which
@@ -372,38 +366,11 @@ function _lockPayload() {
372
366
  });
373
367
  }
374
368
 
375
- // Round-9: lease tunables for the daemon lock. A live daemon refreshes the
376
- // lock mtime every LOCK_REFRESH_MS; a lock whose mtime is older than
377
- // STALE_LOCK_TTL_MS (and that was written by a lease-aware daemon) is
378
- // treated as stale even if its PID happens to be alive -- closing the
379
- // "crash + PID reuse -> new daemon silently refuses to start" hole and the
380
- // "SIGKILL leaves a stale lock nobody reclaims" hole. The TTL is well above
381
- // the heartbeat interval (default 6min) so a healthy daemon never trips it.
382
- // On Windows, SIGTERM is implemented as TerminateProcess() (not a catchable
383
- // signal), so the shutdown() handler that calls releaseLock() never runs.
384
- // The lock file stays on disk with the dead PID. Reduce the TTL on Windows
385
- // so a subsequent start doesn't wait 15 minutes to reclaim the stale lock.
386
- // Unix dropped from 15 min -> 5 min so a wedged daemon does not block takeover
387
- // for a quarter hour. 5 min is still 2.5x the 2-min Unix refresh cadence.
388
- // Windows 3 min TTL gets a 1-min refresh (3x margin) since 2-min refresh left
389
- // only 1.5x margin against transient FS hiccups.
390
- const STALE_LOCK_TTL_MS = process.platform === 'win32' ? 3 * 60_000 : 5 * 60_000;
391
- const LOCK_REFRESH_MS = process.platform === 'win32' ? 1 * 60_000 : 2 * 60_000;
369
+ // STALE_LOCK_TTL_MS / LOCK_REFRESH_MS / _lockIsStaleByLease come from
370
+ // src/adapters/scripts/_lockPaths.js (required next to getLockFilePath
371
+ // above) see issue #176 and the Round-9 history note in that module.
392
372
  let _lockRefreshTimer = null;
393
373
 
394
- // Returns true if the lock was written by a lease-aware daemon AND its
395
- // mtime is older than the stale TTL -- i.e. no live owner is refreshing it,
396
- // so it is safe to reclaim regardless of whether the recorded PID resolves.
397
- function _lockIsStaleByLease(lockFile, payload) {
398
- if (!payload || payload.lease !== true) return false;
399
- try {
400
- const ageMs = Date.now() - fs.statSync(lockFile).mtimeMs;
401
- return ageMs > STALE_LOCK_TTL_MS;
402
- } catch (_) {
403
- return false;
404
- }
405
- }
406
-
407
374
  // Start refreshing the lock file's mtime so other processes can tell this
408
375
  // daemon is alive without trusting a (recyclable) PID. unref'd: it never
409
376
  // keeps the event loop open on its own, but fires for as long as the daemon
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.89.2",
3
+ "version": "1.89.3",
4
4
  "description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,168 @@
1
+ // refresh_stars_badge.js -- Refresh the static "Stars" badge count in the
2
+ // public README sources to the live GitHub star count.
3
+ //
4
+ // Why this exists:
5
+ // The README stars badge is intentionally STATIC (a plain shields `badge/`
6
+ // URL, not a dynamic `github/stars` one) so it never renders shields.io's
7
+ // "Unable to select next GitHub token from pool" error when their shared
8
+ // GitHub token pool is rate-limited. The trade-off is that the number goes
9
+ // stale. This script re-stamps it at release time so the badge stays current
10
+ // without ever depending on the GitHub API to *render*.
11
+ //
12
+ // Usage:
13
+ // node scripts/refresh_stars_badge.js # update in place
14
+ // node scripts/refresh_stars_badge.js --dry-run # print, write nothing
15
+ // node scripts/refresh_stars_badge.js --repo=O/N # override repo
16
+ // node scripts/refresh_stars_badge.js --count=12345 # skip fetch (testing)
17
+ //
18
+ // Design notes:
19
+ // - Counts come from the PUBLIC repo (default EvoMap/evolver), not this
20
+ // private dev repo.
21
+ // - Fetch uses the already-authenticated `gh` CLI first (deploy.sh preflight
22
+ // guarantees gh auth), falling back to the unauthenticated GitHub REST API.
23
+ // - A fetch failure is NON-FATAL: we warn and exit 0, leaving the badge as
24
+ // is. A flaky GitHub API must never break a release -- avoiding exactly
25
+ // that fragility is the whole reason the badge is static.
26
+ // - It rewrites every README*.md in the repo root that contains the badge,
27
+ // so new translations are picked up automatically.
28
+
29
+ const fs = require('fs');
30
+ const path = require('path');
31
+ const https = require('https');
32
+ const { execSync } = require('child_process');
33
+
34
+ const REPO_ROOT = path.resolve(__dirname, '..');
35
+ const DEFAULT_REPO = 'EvoMap/evolver';
36
+
37
+ // Matches the value segment of `.../badge/Stars-<value>-<color>...` in a
38
+ // shields static badge URL. The value never contains `-` or `/`. Returns a
39
+ // fresh regex each call so callers never share `lastIndex` state.
40
+ const starsBadgeRe = () => /(img\.shields\.io\/badge\/Stars-)([^-/)]+)(-)/g;
41
+
42
+ // Replace the Stars badge value in `content` with `value`. Pure; returns the
43
+ // rewritten string (unchanged if there is no badge or it already matches).
44
+ function rewriteStarsValue(content, value) {
45
+ return content.replace(starsBadgeRe(), (_m, pre, _old, post) => `${pre}${value}${post}`);
46
+ }
47
+
48
+ function parseArgs(argv) {
49
+ const opts = { dryRun: false, repo: DEFAULT_REPO, count: null };
50
+ for (const arg of argv) {
51
+ if (arg === '--dry-run' || arg === '-n') opts.dryRun = true;
52
+ else if (arg.startsWith('--repo=')) opts.repo = arg.slice('--repo='.length);
53
+ else if (arg.startsWith('--count=')) opts.count = Number(arg.slice('--count='.length));
54
+ }
55
+ return opts;
56
+ }
57
+
58
+ // Format an integer the way shields.io's `metric` text formatter does, so the
59
+ // static badge is visually indistinguishable from a dynamic one:
60
+ // 8336 -> "8.3k", 12345 -> "12k", 1100 -> "1.1k", 999 -> "999".
61
+ function metric(n) {
62
+ n = Number(n);
63
+ if (!Number.isFinite(n) || n < 0) return String(n);
64
+ for (const [suffix, size] of [['G', 1e9], ['M', 1e6], ['k', 1e3]]) {
65
+ if (n >= size) {
66
+ const value = n / size;
67
+ const text = value < 10
68
+ ? value.toFixed(1).replace(/\.0$/, '')
69
+ : String(Math.round(value));
70
+ return text + suffix;
71
+ }
72
+ }
73
+ return String(Math.round(n));
74
+ }
75
+
76
+ function fetchStarsViaGh(repo) {
77
+ try {
78
+ const out = execSync(`gh api repos/${repo} --jq .stargazers_count`, {
79
+ encoding: 'utf8',
80
+ stdio: ['ignore', 'pipe', 'ignore'],
81
+ }).trim();
82
+ const n = Number(out);
83
+ return Number.isFinite(n) && n > 0 ? n : null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ function fetchStarsViaApi(repo) {
90
+ return new Promise((resolve) => {
91
+ const req = https.get(
92
+ `https://api.github.com/repos/${repo}`,
93
+ { headers: { 'User-Agent': 'evolver-refresh-stars-badge', Accept: 'application/vnd.github+json' } },
94
+ (res) => {
95
+ if (res.statusCode !== 200) { res.resume(); return resolve(null); }
96
+ let body = '';
97
+ res.on('data', (c) => (body += c));
98
+ res.on('end', () => {
99
+ try {
100
+ const n = Number(JSON.parse(body).stargazers_count);
101
+ resolve(Number.isFinite(n) && n > 0 ? n : null);
102
+ } catch { resolve(null); }
103
+ });
104
+ },
105
+ );
106
+ req.on('error', () => resolve(null));
107
+ req.setTimeout(10000, () => { req.destroy(); resolve(null); });
108
+ });
109
+ }
110
+
111
+ async function resolveStarCount(opts) {
112
+ if (Number.isFinite(opts.count) && opts.count > 0) return opts.count;
113
+ return fetchStarsViaGh(opts.repo) || (await fetchStarsViaApi(opts.repo));
114
+ }
115
+
116
+ function readmeFilesWithBadge() {
117
+ return fs
118
+ .readdirSync(REPO_ROOT)
119
+ .filter((f) => /^README.*\.md$/.test(f))
120
+ .map((f) => path.join(REPO_ROOT, f))
121
+ .filter((p) => starsBadgeRe().test(fs.readFileSync(p, 'utf8')));
122
+ }
123
+
124
+ async function main() {
125
+ const opts = parseArgs(process.argv.slice(2));
126
+
127
+ const count = await resolveStarCount(opts);
128
+ if (count == null) {
129
+ console.warn('[refresh-stars-badge] could not resolve star count (gh + API both failed); leaving badge unchanged');
130
+ return; // non-fatal
131
+ }
132
+ const value = metric(count);
133
+ console.log(`[refresh-stars-badge] ${opts.repo} = ${count} stars -> "${value}"`);
134
+
135
+ const files = readmeFilesWithBadge();
136
+ if (files.length === 0) {
137
+ console.warn('[refresh-stars-badge] no README*.md with a Stars badge found; nothing to do');
138
+ return;
139
+ }
140
+
141
+ let changed = 0;
142
+ for (const file of files) {
143
+ const before = fs.readFileSync(file, 'utf8');
144
+ const after = rewriteStarsValue(before, value);
145
+ const rel = path.relative(REPO_ROOT, file);
146
+ if (after === before) {
147
+ console.log(` ${rel}: already "${value}"`);
148
+ continue;
149
+ }
150
+ if (opts.dryRun) {
151
+ console.log(` [dry-run] ${rel}: would set Stars -> "${value}"`);
152
+ } else {
153
+ fs.writeFileSync(file, after);
154
+ console.log(` ${rel}: Stars -> "${value}"`);
155
+ }
156
+ changed++;
157
+ }
158
+ console.log(`[refresh-stars-badge] ${changed} file(s) ${opts.dryRun ? 'would change' : 'updated'}`);
159
+ }
160
+
161
+ if (require.main === module) {
162
+ main().catch((e) => {
163
+ // Never fail the release over a badge refresh.
164
+ console.warn(`[refresh-stars-badge] unexpected error (ignored): ${e && e.message}`);
165
+ });
166
+ }
167
+
168
+ module.exports = { metric, rewriteStarsValue };
@@ -168,6 +168,7 @@ function copyHookScripts(destDir, evolverRoot) {
168
168
  const scripts = [
169
169
  '_runtimePaths.js',
170
170
  '_memoryFiltering.js',
171
+ '_lockPaths.js',
171
172
  'evolver-session-start.js',
172
173
  'evolver-signal-detect.js',
173
174
  'evolver-session-end.js',
@@ -255,6 +256,7 @@ function removeHookScripts(hooksDir) {
255
256
  const scripts = [
256
257
  '_runtimePaths.js',
257
258
  '_memoryFiltering.js',
259
+ '_lockPaths.js',
258
260
  'evolver-session-start.js',
259
261
  'evolver-signal-detect.js',
260
262
  'evolver-session-end.js',