@evomap/evolver 1.87.1 → 1.87.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 (63) hide show
  1. package/README.ja-JP.md +1 -1
  2. package/README.ko-KR.md +1 -1
  3. package/README.md +9 -8
  4. package/README.zh-CN.md +9 -8
  5. package/index.js +30 -11
  6. package/package.json +1 -1
  7. package/scripts/build_binaries.js +31 -7
  8. package/src/atp/atpExecute.js +35 -8
  9. package/src/atp/autoBuyer.js +155 -21
  10. package/src/atp/autoDeliver.js +16 -0
  11. package/src/atp/cli.js +98 -0
  12. package/src/atp/cliAutobuyPrompt.js +57 -64
  13. package/src/atp/hubClient.js +42 -4
  14. package/src/evolve/guards.js +1 -1
  15. package/src/evolve/pipeline/collect.js +1 -1
  16. package/src/evolve/pipeline/dispatch.js +1 -1
  17. package/src/evolve/pipeline/enrich.js +1 -1
  18. package/src/evolve/pipeline/hub.js +1 -1
  19. package/src/evolve/pipeline/select.js +1 -1
  20. package/src/evolve/pipeline/signals.js +1 -1
  21. package/src/evolve/utils.js +1 -1
  22. package/src/evolve.js +1 -1
  23. package/src/forceUpdate.js +2 -1
  24. package/src/gep/a2aProtocol.js +1 -1
  25. package/src/gep/assetStore.js +52 -5
  26. package/src/gep/candidateEval.js +1 -1
  27. package/src/gep/candidates.js +1 -1
  28. package/src/gep/contentHash.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/explore.js +1 -1
  35. package/src/gep/hash.js +1 -1
  36. package/src/gep/hubFetch.js +1 -1
  37. package/src/gep/hubReview.js +1 -1
  38. package/src/gep/hubSearch.js +1 -1
  39. package/src/gep/hubVerify.js +1 -1
  40. package/src/gep/learningSignals.js +1 -1
  41. package/src/gep/memoryGraph.js +1 -1
  42. package/src/gep/memoryGraphAdapter.js +1 -1
  43. package/src/gep/mutation.js +1 -1
  44. package/src/gep/narrativeMemory.js +1 -1
  45. package/src/gep/openPRRegistry.js +1 -1
  46. package/src/gep/paths.js +6 -2
  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/recallVerifier.js +1 -1
  51. package/src/gep/reflection.js +1 -1
  52. package/src/gep/sanitize.js +57 -3
  53. package/src/gep/selector.js +1 -1
  54. package/src/gep/selfPR.js +34 -1
  55. package/src/gep/skill2gep.js +108 -29
  56. package/src/gep/skillDistiller.js +1 -1
  57. package/src/gep/solidify.js +1 -1
  58. package/src/gep/strategy.js +1 -1
  59. package/src/gep/workspaceKeychain.js +1 -1
  60. package/src/proxy/index.js +29 -9
  61. package/src/proxy/lifecycle/manager.js +97 -37
  62. package/src/proxy/router/messages_route.js +105 -5
  63. package/src/proxy/sync/engine.js +68 -31
package/README.ja-JP.md CHANGED
@@ -32,7 +32,7 @@
32
32
  >
33
33
  > Evolver はこの結論を実装に落とし込んだオープンソースエンジンです。GEP プロトコルの下で、エージェントの経験を場当たり的なプロンプトやスキルドキュメントではなく、Gene と Capsule として符号化します。*なぜ* Evolver が長いスキルドキュメントではなく Gene にこだわるのか疑問に思ったことがあるなら、読むべきはこの論文です。
34
34
  >
35
- > 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを 0.00% から 18.57% まで押し上げる全過程を、トークンコストの軌跡、遺伝子活性化マップ、そして推論が再利用可能な Gene に圧縮されるときに現れる「トークンが上昇してから下降する」シグネチャとともに詳述しています。
35
+ > 応用事例を見たい方へ:[OpenClaw x EvoMap:CritPt 評価レポート](https://evomap.ai/blog/openclaw-critpt-report) では、OpenClaw エージェントが CritPt Physics Solver 上の 5 バージョン(Beta → v2.2)にわたって、同じ Gene ベース進化ループによってスコアを 9.1% から 18.57% まで押し上げる全過程を、トークンコストの軌跡、遺伝子活性化マップ、そして推論が再利用可能な Gene に圧縮されるときに現れる「トークンが上昇してから下降する」シグネチャとともに詳述しています。
36
36
 
37
37
  ---
38
38
 
package/README.ko-KR.md CHANGED
@@ -32,7 +32,7 @@
32
32
  >
33
33
  > Evolver는 이 결과를 실제로 구현하는 오픈소스 엔진입니다. GEP 프로토콜 아래 에이전트의 경험을 임시 프롬프트나 스킬 문서가 아니라 Gene과 Capsule로 인코딩합니다. *왜* Evolver가 더 긴 스킬 문서 대신 Gene을 고집하는지 궁금했다면, 바로 이 논문을 읽어야 합니다.
34
34
  >
35
- > 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐 0.00%에서 18.57%까지 끌어올리는 과정을, 전체 토큰 비용 궤적, 유전자 활성화 매핑, 그리고 추론이 재사용 가능한 Gene으로 압축될 때 나타나는 "토큰이 먼저 상승한 뒤 하강하는" 시그니처와 함께 단계별로 보여줍니다.
35
+ > 적용 사례가 궁금하신가요? [OpenClaw x EvoMap: CritPt 평가 보고서](https://evomap.ai/blog/openclaw-critpt-report)는 동일한 Gene 기반 진화 루프가 OpenClaw 에이전트를 CritPt Physics Solver의 5개 버전(Beta → v2.2)에 걸쳐 9.1%에서 18.57%까지 끌어올리는 과정을, 전체 토큰 비용 궤적, 유전자 활성화 매핑, 그리고 추론이 재사용 가능한 Gene으로 압축될 때 나타나는 "토큰이 먼저 상승한 뒤 하강하는" 시그니처와 함께 단계별로 보여줍니다.
36
36
 
37
37
  ---
38
38
 
package/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
  >
33
33
  > Evolver is the open-source engine that puts this result into practice: it encodes agent experience as Genes and Capsules under the GEP protocol, not as ad hoc prompts or skill docs. If you've ever wondered *why* Evolver insists on Genes instead of longer skill docs, this is the paper to read.
34
34
  >
35
- > Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from 0.00% to 18.57% on CritPt Physics Solver across five versions (Beta -> v2.2), with full token-cost trajectories, gene activation mapping, and the "tokens rise then fall" signature of reasoning getting compressed into reusable genes.
35
+ > Want the applied version? [OpenClaw x EvoMap: CritPt Evaluation Report](https://evomap.ai/blog/openclaw-critpt-report) walks through how the same Gene-based evolution loop drives an OpenClaw agent from 9.1% to 18.57% on CritPt Physics Solver across five versions (Beta -> v2.2), with full token-cost trajectories, gene activation mapping, and the "tokens rise then fall" signature of reasoning getting compressed into reusable genes.
36
36
 
37
37
  ---
38
38
 
@@ -184,7 +184,7 @@ Every `evolver <flag>` invocation in the rest of this README maps 1:1 to `node i
184
184
  **Evolver is a prompt generator, not a code patcher.** Each evolution cycle:
185
185
 
186
186
  1. Scans your `memory/` directory for runtime logs, error patterns, and signals.
187
- 2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from `assets/gep/`.
187
+ 2. Selects the best-matching [Gene or Capsule](https://evomap.ai/wiki) from the local GEP asset store.
188
188
  3. Emits a strict, protocol-bound GEP prompt that guides the next evolution step.
189
189
  4. Records an auditable [EvolutionEvent](https://evomap.ai/wiki) for traceability.
190
190
 
@@ -377,16 +377,17 @@ The [evomap.ai](https://evomap.ai) dashboard has a "Worker" toggle on the node d
377
377
 
378
378
  This repo includes a protocol-constrained prompt mode based on [GEP (Genome Evolution Protocol)](https://evomap.ai/wiki).
379
379
 
380
- - **Structured assets** live in `assets/gep/`:
381
- - `assets/gep/genes.json`
382
- - `assets/gep/capsules.json`
383
- - `assets/gep/events.jsonl`
380
+ - **Structured runtime assets** live in `<workspace>/.evolver/gep/` by default:
381
+ - `<workspace>/.evolver/gep/genes.json`
382
+ - `<workspace>/.evolver/gep/capsules.json`
383
+ - `<workspace>/.evolver/gep/events.jsonl`
384
+ - Set `GEP_ASSETS_DIR` to place the runtime asset store elsewhere.
384
385
  - **Selector** logic uses extracted signals to prefer existing Genes/Capsules and emits a JSON selector decision in the prompt.
385
386
  - **Constraints**: Only the DNA emoji is allowed in documentation; all other emoji are disallowed.
386
387
 
387
388
  ### Your local asset store is never overwritten by upgrades
388
389
 
389
- `assets/gep/genes.json`, `assets/gep/capsules.json`, and `assets/gep/events.jsonl` are owned by your runtime. Starting with 1.78.3, the npm tarball no longer contains these files, so `npm i -g @evomap/evolver` (or `git pull` of the public repo) never clobbers your accumulated Genes, Capsules, or EvolutionEvents. New installs still receive the curated starter Genes through `assets/gep/genes.seed.json`, which is applied only when `genes.json` is absent.
390
+ `<workspace>/.evolver/gep/genes.json`, `<workspace>/.evolver/gep/capsules.json`, and `<workspace>/.evolver/gep/events.jsonl` are owned by your runtime and ignored by git. `assets/gep/` is reserved for bundled starter assets. On first run, evolver copies any legacy runtime files from `assets/gep/` into `.evolver/gep/` without deleting the originals, then seeds `genes.json` from the bundled starter genes only when no local `genes.json` exists.
390
391
 
391
392
  If you ran an older evolver version that wiped your local assets, pull back everything you Promoted or published to the Hub with a single command:
392
393
 
@@ -396,7 +397,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
396
397
 
397
398
  This hits `/a2a/assets/purchased` (Promoted-to-you plus self-purchased) and `/a2a/assets/published-by-me` (your own drafts and published assets), re-materializes the full payloads into `genes.json` / `capsules.json`, and packs a portable `.gepx` bundle. Previously-purchased payloads re-fetch at zero cost.
398
399
 
399
- Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from your git history (for example `git show <old_tag>:assets/gep/genes.json > restored.json`) or from disk snapshots.
400
+ Purely local assets that were never uploaded to the Hub have no remote copy -- recover them from `.evolver/gep/`, from an older `assets/gep/` checkout, or from disk snapshots.
400
401
 
401
402
  ## Configuration & Decoupling
402
403
 
package/README.zh-CN.md CHANGED
@@ -32,7 +32,7 @@
32
32
  >
33
33
  > Evolver 正是把这一结论落地的开源引擎:它基于 GEP 协议,把 Agent 的经验沉淀为 Gene 与 Capsule,而不是散落的 prompt 或技能文档。如果你想知道 *为什么* Evolver 坚持使用 Gene 而不是更长的 skill 文档,这就是那篇该读的论文。
34
34
  >
35
- > 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从 0.00% 推到 18.57%,并给出 token 成本轨迹、基因激活映射,以及推理被压缩成可复用基因后所呈现的「token 先升后降」特征。
35
+ > 想看应用落地的样本?[OpenClaw x EvoMap:CritPt 评测报告](https://evomap.ai/blog/openclaw-critpt-report) 以 OpenClaw Agent 在 CritPt Physics Solver 上的五个版本演进(Beta → v2.2)为例,完整拆解了同一套 Gene 进化闭环如何把得分从 9.1% 推到 18.57%,并给出 token 成本轨迹、基因激活映射,以及推理被压缩成可复用基因后所呈现的「token 先升后降」特征。
36
36
 
37
37
  ---
38
38
 
@@ -159,7 +159,7 @@ evolver --loop
159
159
  **Evolver 是一个提示词生成器,不是代码修改器。** 每个进化周期:
160
160
 
161
161
  1. 扫描 `memory/` 目录中的运行日志、错误模式和信号。
162
- 2. `assets/gep/` 中选择最匹配的 [Gene 或 Capsule](https://evomap.ai/wiki)。
162
+ 2. 从本地 GEP 资产库中选择最匹配的 [Gene 或 Capsule](https://evomap.ai/wiki)。
163
163
  3. 输出一份严格的、受协议约束的 GEP 提示词来引导下一步进化。
164
164
  4. 记录可审计的 [EvolutionEvent](https://evomap.ai/wiki) 以便追溯。
165
165
 
@@ -351,16 +351,17 @@ WORKER_ENABLED=1 WORKER_DOMAINS=repair,harden WORKER_MAX_LOAD=3 evolver --loop
351
351
 
352
352
  本仓库内置基于 [GEP(基因组进化协议)](https://evomap.ai/wiki)的协议受限提示词模式。
353
353
 
354
- - **结构化资产目录**:`assets/gep/`
355
- - `assets/gep/genes.json`
356
- - `assets/gep/capsules.json`
357
- - `assets/gep/events.jsonl`
354
+ - **结构化运行时资产目录**:默认位于 `<workspace>/.evolver/gep/`
355
+ - `<workspace>/.evolver/gep/genes.json`
356
+ - `<workspace>/.evolver/gep/capsules.json`
357
+ - `<workspace>/.evolver/gep/events.jsonl`
358
+ - 可通过 `GEP_ASSETS_DIR` 把运行时资产库放到其他位置。
358
359
  - **Selector 选择器**:根据日志提取 signals,优先复用已有 Gene/Capsule,并在提示词中输出可审计的 Selector 决策 JSON。
359
360
  - **约束**:除 🧬 外,禁止使用其他 emoji。
360
361
 
361
362
  ### 升级不再覆盖你的本地资产库
362
363
 
363
- `assets/gep/genes.json`、`assets/gep/capsules.json`、`assets/gep/events.jsonl` 属于你本地运行时。从 1.78.3 起,npm 发行包不再包含这三个文件,`npm i -g @evomap/evolver`(或公共仓库的 `git pull`)不会再覆盖你累积的 Gene、Capsule 和 EvolutionEvent。新装用户依然会通过 `assets/gep/genes.seed.json` 拿到引擎维护的 starter Gene —— 只有在本地 `genes.json` 不存在时才会应用一次。
364
+ `<workspace>/.evolver/gep/genes.json`、`<workspace>/.evolver/gep/capsules.json`、`<workspace>/.evolver/gep/events.jsonl` 属于你本地运行时,并被 git 忽略。`assets/gep/` 保留给随包发布的 starter 资产。首次运行时,evolver 会把旧版遗留在 `assets/gep/` 的运行时文件复制到 `.evolver/gep/`,不会删除原文件;只有在本地 `genes.json` 不存在时,才会从随包 starter Gene 初始化。
364
365
 
365
366
  如果你之前用老版本被覆盖过,现在可以一键把所有被 Promoted 给你、以及你自己上传到 Hub 的资产拉回来:
366
367
 
@@ -370,7 +371,7 @@ A2A_HUB_URL=https://evomap.ai evolver sync --scope=all --export=backup.gepx
370
371
 
371
372
  它会去 `/a2a/assets/purchased`(被 Promoted 给你 + 自购)和 `/a2a/assets/published-by-me`(你自己发布的,含 draft)拉回完整 payload,直接回写 `genes.json` / `capsules.json`,并顺便打成 `.gepx` 整包备份。已购买过的 payload 这次重新拉取不收费。
372
373
 
373
- 纯本地、从未上传过的资产 Hub 没有副本,只能从 git 历史恢复(例如 `git show <老tag>:assets/gep/genes.json > restored.json`)或从磁盘快照找回。
374
+ 纯本地、从未上传过的资产 Hub 没有副本,只能从 `.evolver/gep/`、旧版 `assets/gep/` checkout 或磁盘快照找回。
374
375
 
375
376
  ## 配置与解耦
376
377
 
package/index.js CHANGED
@@ -491,10 +491,11 @@ async function main() {
491
491
  console.warn('[ATP] Auto-init failed: ' + (atpInitErr && atpInitErr.message || atpInitErr));
492
492
  }
493
493
 
494
- // ATP: capability-gap auto-buyer. Default ON as of ATP liquidity
495
- // unlock; disable with EVOLVER_ATP_AUTOBUY=off. Also starts the
496
- // merchant-side auto-deliver daemon so claimed ATP tasks actually
497
- // call submitDelivery and settle instead of expiring.
494
+ // ATP: capability-gap auto-buyer. OPT-IN as of consent-required
495
+ // change new installs do not auto-spend until the user explicitly
496
+ // runs `evolver atp enable` or answers `y` at the first-run prompt.
497
+ // Also starts the merchant-side auto-deliver daemon so claimed ATP
498
+ // tasks actually call submitDelivery and settle instead of expiring.
498
499
  try {
499
500
  try {
500
501
  const { runPrompt } = require('./src/atp/cliAutobuyPrompt');
@@ -502,16 +503,31 @@ async function main() {
502
503
  } catch (promptErr) {
503
504
  console.warn('[ATP-AutoBuyer] first-run prompt failed: ' + (promptErr && promptErr.message || promptErr));
504
505
  }
505
- const autoBuyRaw = (process.env.EVOLVER_ATP_AUTOBUY || 'on').toLowerCase().trim();
506
- const autoBuyOn = autoBuyRaw !== 'off' && autoBuyRaw !== '0' && autoBuyRaw !== 'false';
507
- if (autoBuyOn) {
506
+ const { autoBuyer } = require('./src/atp');
507
+ const consent = autoBuyer.getConsent();
508
+ if (consent.enabled) {
508
509
  const hubUrl = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || '';
509
510
  if (hubUrl) {
510
- const { autoBuyer } = require('./src/atp');
511
511
  autoBuyer.start({
512
512
  dailyCap: Number(process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS) || undefined,
513
513
  perOrderCap: Number(process.env.ATP_AUTOBUY_PER_ORDER_CAP_CREDITS) || undefined,
514
514
  });
515
+ if (consent.source === 'default') {
516
+ // First-run on a non-TTY (daemon, hook, CI) where the prompt
517
+ // could not fire AND no env override + no ack file. autoBuyer
518
+ // is starting with the default-on policy — surface a single
519
+ // WARN per process so users see what is happening and how to
520
+ // opt out, instead of discovering it via a credit balance dip.
521
+ let safeHubUrl;
522
+ try { safeHubUrl = new URL(hubUrl).origin; }
523
+ catch { safeHubUrl = '(configured)'; }
524
+ console.warn('[ATP-AutoBuyer] ATP auto-spend is ON (default for new installs).');
525
+ console.warn(' Hub: ' + safeHubUrl + ' Caps: ' +
526
+ (process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS || '50') + ' credits/day, ' +
527
+ (process.env.ATP_AUTOBUY_PER_ORDER_CAP_CREDITS || '10') + '/order' +
528
+ ' (cold-start half-cap for the first 5 min).');
529
+ console.warn(' To opt out: evolver atp disable (or EVOLVER_ATP_AUTOBUY=off)');
530
+ }
515
531
  } else {
516
532
  console.warn('[ATP-AutoBuyer] autobuy enabled but no hub URL configured, skipping.');
517
533
  }
@@ -1815,7 +1831,7 @@ async function main() {
1815
1831
  process.exit(1);
1816
1832
  }
1817
1833
 
1818
- } else if (command === 'buy' || command === 'orders' || command === 'verify') {
1834
+ } else if (command === 'buy' || command === 'orders' || command === 'verify' || command === 'atp') {
1819
1835
  try {
1820
1836
  const atpCli = require('./src/atp/cli');
1821
1837
  const subArgs = args.slice(1); // drop the command token (e.g. "buy") itself
@@ -1827,9 +1843,12 @@ async function main() {
1827
1843
  } else if (command === 'orders') {
1828
1844
  parsed = atpCli.parseOrdersArgs(subArgs);
1829
1845
  runner = atpCli.runOrders;
1830
- } else {
1846
+ } else if (command === 'verify') {
1831
1847
  parsed = atpCli.parseVerifyArgs(subArgs);
1832
1848
  runner = atpCli.runVerify;
1849
+ } else {
1850
+ parsed = atpCli.parseAtpArgs(subArgs);
1851
+ runner = atpCli.runAtp;
1833
1852
  }
1834
1853
  if (!parsed.ok) {
1835
1854
  console.error('[ATP] ' + parsed.error);
@@ -1844,7 +1863,7 @@ async function main() {
1844
1863
  }
1845
1864
 
1846
1865
  } else {
1847
- console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp-complete] [--loop]
1866
+ console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp|atp-complete] [--loop]
1848
1867
  - fetch flags:
1849
1868
  - --skill=<id> | -s <id> (skill ID to download)
1850
1869
  - --out=<dir> (output directory, default: ./skills/<skill_id>)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.87.1",
3
+ "version": "1.87.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": {
@@ -192,7 +192,14 @@ step('Stage 1 — bun bundle (resolve require tree to one file)');
192
192
  ensureDir(STAGE_DIR);
193
193
  const BUNDLED_JS = path.join(STAGE_DIR, 'bundled.js');
194
194
 
195
- run('bun', ['build', ENTRY, '--target=node', `--outfile=${BUNDLED_JS}`]);
195
+ // `--external '@napi-rs/keyring'`: keyring is an optional dep loaded via
196
+ // dynamic require() in workspace-id; bun otherwise tries to bundle the
197
+ // platform-specific `.node` file as a second output asset, which makes
198
+ // `bun build … --outfile=…` fail with "cannot write multiple output files
199
+ // without an output directory". Treating it as external preserves the
200
+ // existing optional-fallback behaviour (require throws → FS path used) in
201
+ // the standalone binaries.
202
+ run('bun', ['build', ENTRY, '--target=node', `--outfile=${BUNDLED_JS}`, '--external', '@napi-rs/keyring']);
196
203
 
197
204
  const bundleSize = OPTS.dryRun ? 0 : fs.statSync(BUNDLED_JS).size;
198
205
  console.log(` bundled.js: ${(bundleSize / 1024 / 1024).toFixed(2)} MB`);
@@ -266,13 +273,30 @@ if (!OPTS.skipObfuscate) {
266
273
  const obfSecs = ((Date.now() - t0) / 1000).toFixed(1);
267
274
 
268
275
  const check = spawnSync('node', ['--check', OBF_JS], { encoding: 'utf8' });
269
- if (check.status === 0) {
270
- console.log(` obfuscation: ${obfSecs}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB (attempt ${attempt}/${MAX_OBF_ATTEMPTS}, seed=0x${usedSeed.toString(16)})`);
271
- succeeded = true;
272
- break;
276
+ if (check.status !== 0) {
277
+ lastValidationErr = (check.stderr || check.stdout || '').split('\n').slice(0, 3).join(' | ');
278
+ console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output failed node --check (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
279
+ continue;
273
280
  }
274
- lastValidationErr = (check.stderr || check.stdout || '').split('\n').slice(0, 3).join(' | ');
275
- console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output failed node --check (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
281
+ // Second gate: bun's compile-time parser is stricter than node's.
282
+ // 1.87.x (post `@napi-rs/keyring` dep) revealed that ~5% of obfuscator
283
+ // outputs that pass `node --check` still trip bun with errors like
284
+ // `Expected "in" but found ","`. Probe with a cheap bundle-only call
285
+ // (no --compile, native target) to fail fast and feed back into the
286
+ // seed-perturbation loop instead of dying in stage 3.
287
+ const bunProbe = spawnSync('bun', [
288
+ 'build', OBF_JS,
289
+ '--target=bun',
290
+ `--outfile=${path.join(STAGE_DIR, 'bundled.obf.bunprobe.js')}`,
291
+ ], { encoding: 'utf8' });
292
+ if (bunProbe.status !== 0) {
293
+ lastValidationErr = (bunProbe.stderr || bunProbe.stdout || '').split('\n').slice(0, 3).join(' | ');
294
+ console.warn(` attempt ${attempt}/${MAX_OBF_ATTEMPTS}: obfuscator output rejected by bun parser (${lastValidationErr.slice(0, 200)}); retrying with perturbed seed...`);
295
+ continue;
296
+ }
297
+ console.log(` obfuscation: ${obfSecs}s, output ${(obfSize / 1024 / 1024).toFixed(2)} MB (attempt ${attempt}/${MAX_OBF_ATTEMPTS}, seed=0x${usedSeed.toString(16)})`);
298
+ succeeded = true;
299
+ break;
276
300
  }
277
301
  if (!succeeded) {
278
302
  console.error(` ERROR: javascript-obfuscator produced syntactically invalid output in ${MAX_OBF_ATTEMPTS} attempts.`);
@@ -27,6 +27,7 @@ const https = require('https');
27
27
  const crypto = require('crypto');
28
28
 
29
29
  const { computeAssetId } = require('../gep/contentHash');
30
+ const { enforceHubScheme, strictHttpsAgent } = require('../gep/hubFetch');
30
31
  const {
31
32
  getNodeId,
32
33
  getHubUrl,
@@ -114,6 +115,19 @@ function _publishUrl() {
114
115
 
115
116
  function _postJson(urlStr, body, timeoutMs) {
116
117
  return new Promise(function (resolve) {
118
+ // Same TLS posture as hubFetch: refuse plain http:// unless
119
+ // EVOMAP_HUB_ALLOW_INSECURE=1. Before this guard the function
120
+ // silently fell back to `lib = http` for any non-https URL, so an
121
+ // operator override `A2A_HUB_URL=http://...` would send /a2a/publish
122
+ // and /a2a/task/complete in cleartext while hubFetch-routed calls
123
+ // (e.g. /a2a/verify-solidify) refused the same URL — inconsistent
124
+ // TLS enforcement across modules.
125
+ try {
126
+ enforceHubScheme(urlStr);
127
+ } catch (e) {
128
+ resolve({ ok: false, error: 'tls_refused: ' + (e && e.message) });
129
+ return;
130
+ }
117
131
  let parsed;
118
132
  try {
119
133
  parsed = new URL(urlStr);
@@ -128,15 +142,28 @@ function _postJson(urlStr, body, timeoutMs) {
128
142
  { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
129
143
  buildHubHeaders() || {},
130
144
  );
145
+ // Pin TLS cert verification for https calls so a globally-disabled
146
+ // NODE_TLS_REJECT_UNAUTHORIZED=0 cannot weaken the Hub channel
147
+ // (Cursor Security Reviewer #160 Medium). hubFetch enforces the
148
+ // same via its undici dispatcher; this is the Node-native-https
149
+ // equivalent.
150
+ //
151
+ // Skipped under EVOMAP_HUB_ALLOW_INSECURE=1 so local-dev / self-
152
+ // signed mock hubs that legitimately rely on
153
+ // NODE_TLS_REJECT_UNAUTHORIZED=0 still work.
154
+ const requestOpts = {
155
+ hostname: parsed.hostname,
156
+ port: parsed.port || (isHttps ? 443 : 80),
157
+ path: parsed.pathname + (parsed.search || ''),
158
+ method: 'POST',
159
+ headers: headers,
160
+ timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
161
+ };
162
+ if (isHttps && process.env.EVOMAP_HUB_ALLOW_INSECURE !== '1') {
163
+ requestOpts.agent = strictHttpsAgent;
164
+ }
131
165
  const req = lib.request(
132
- {
133
- hostname: parsed.hostname,
134
- port: parsed.port || (isHttps ? 443 : 80),
135
- path: parsed.pathname + (parsed.search || ''),
136
- method: 'POST',
137
- headers: headers,
138
- timeout: timeoutMs || PUBLISH_TIMEOUT_MS,
139
- },
166
+ requestOpts,
140
167
  function (res) {
141
168
  const chunks = [];
142
169
  res.on('data', function (c) { chunks.push(c); });
@@ -1,12 +1,19 @@
1
- // ATP Auto-Buyer (opt-out, default ON as of ATP liquidity unlock)
1
+ // ATP Auto-Buyer (opt-in: requires explicit consent before auto-spending)
2
2
  // Converts capability gaps into ATP orders with strict budget caps and
3
- // 24h question-level deduplication. Disable by setting
4
- // EVOLVER_ATP_AUTOBUY=off. Budget caps:
3
+ // 24h question-level deduplication. Budget caps:
5
4
  // ATP_AUTOBUY_DAILY_CAP_CREDITS (default 50)
6
5
  // ATP_AUTOBUY_PER_ORDER_CAP_CREDITS (default 10)
7
6
  // Cold-start safety: the first 5 minutes after process start use a half-cap
8
7
  // to protect against misconfiguration loops on restart storms.
9
8
  //
9
+ // Consent resolution (in order):
10
+ // 1. EVOLVER_ATP_AUTOBUY=on|off env — explicit operator override wins.
11
+ // 2. ack file at <memory>/atp-autobuy-ack.json with `{enabled: bool}` —
12
+ // written by first-run prompt (cliAutobuyPrompt) or `evolver atp
13
+ // enable|disable`.
14
+ // 3. No signal → OFF. New installs never auto-spend before the user has
15
+ // explicitly opted in (consumer protection: ATP spends real credits).
16
+ //
10
17
  // Integration contract:
11
18
  // 1) Call start({ dailyCap, perOrderCap }) once at Evolver boot. The
12
19
  // evolve loop does this at the top of every cycle; start() is
@@ -29,8 +36,15 @@ const DEFAULT_DAILY_CAP = 50;
29
36
  const DEFAULT_PER_ORDER_CAP = 10;
30
37
  const DEFAULT_ORDER_TIMEOUT_MS = 3000;
31
38
  const COLD_START_WINDOW_MS = 5 * 60 * 1000;
39
+ // Successful orders dedup for 24h so the same capability gap is only paid for
40
+ // once per day. Failed orders dedup for 5 minutes only — long enough to
41
+ // absorb tight retry loops (the original goal of "don't hammer the hub")
42
+ // without making the user wait 24h to retry a question after a transient
43
+ // 503/network blip.
32
44
  const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
45
+ const DEDUP_FAILURE_TTL_MS = 5 * 60 * 1000;
33
46
  const LEDGER_FILENAME = 'atp-autobuyer-ledger.json';
47
+ const ACK_FILENAME = 'atp-autobuy-ack.json';
34
48
 
35
49
  let _started = false;
36
50
  let _startedAt = 0;
@@ -44,18 +58,57 @@ function _ledgerPath() {
44
58
  return path.join(getMemoryDir(), LEDGER_FILENAME);
45
59
  }
46
60
 
61
+ function _ackPath() {
62
+ return path.join(getMemoryDir(), ACK_FILENAME);
63
+ }
64
+
47
65
  function _todayKey(now) {
48
66
  const d = new Date(typeof now === 'number' ? now : Date.now());
49
67
  return d.toISOString().slice(0, 10); // UTC YYYY-MM-DD
50
68
  }
51
69
 
52
- function _isEnabled() {
53
- // Default ON: the evolve loop starts autoBuyer at the top of every cycle
54
- // so new users get ATP buyer routing out of the box. Disable by setting
55
- // EVOLVER_ATP_AUTOBUY=off. Budget caps (DAILY_CAP + PER_ORDER_CAP) keep
56
- // the downside bounded even when this is on.
57
- const raw = (process.env.EVOLVER_ATP_AUTOBUY || 'on').toLowerCase().trim();
58
- return raw !== 'off' && raw !== '0' && raw !== 'false';
70
+ function _readAck() {
71
+ try {
72
+ const p = _ackPath();
73
+ if (!fs.existsSync(p)) return null;
74
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
75
+ if (!parsed || typeof parsed !== 'object') return null;
76
+ if (typeof parsed.enabled !== 'boolean') return null;
77
+ return parsed;
78
+ } catch (_) {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ // Resolve consent. Returns:
84
+ // { enabled: true, source: 'env'|'ack'|'default' }
85
+ // { enabled: false, source: 'env'|'ack' }
86
+ // The `default` case is the new-install path (no env override, no ack file):
87
+ // auto-spend defaults ON, gated by the daily/per-order caps and the cold-start
88
+ // half-cap window. The first-run prompt and `evolver atp disable` remain the
89
+ // opt-out paths for users who do not want auto-spend; once an explicit ack is
90
+ // recorded the source flips to 'ack' and the user's choice (either way) wins
91
+ // over this default.
92
+ function getConsent() {
93
+ const envRaw = process.env.EVOLVER_ATP_AUTOBUY;
94
+ if (typeof envRaw === 'string') {
95
+ // Trim BEFORE the length check so whitespace-only values
96
+ // (e.g. EVOLVER_ATP_AUTOBUY=" ") count as unset, matching the
97
+ // classify() helper in cliAutobuyPrompt.js. Without this alignment a
98
+ // whitespace value would skip the prompt in classify (treats as unset
99
+ // → 'eligible') but still enter this env branch, trim to "", fail to
100
+ // match 'off'/'0'/'false', and silently return enabled=true.
101
+ const s = envRaw.trim().toLowerCase();
102
+ if (s.length > 0) {
103
+ const enabled = s !== 'off' && s !== '0' && s !== 'false';
104
+ return { enabled, source: 'env' };
105
+ }
106
+ }
107
+ const ack = _readAck();
108
+ if (ack) {
109
+ return { enabled: ack.enabled === true, source: 'ack' };
110
+ }
111
+ return { enabled: true, source: 'default' };
59
112
  }
60
113
 
61
114
  function _emptyLedger() {
@@ -98,13 +151,22 @@ function _rotateIfNewDay(ledger, now) {
98
151
  }
99
152
 
100
153
  function _pruneDedup(ledger, now) {
101
- const cutoff = (typeof now === 'number' ? now : Date.now()) - DEDUP_TTL_MS;
154
+ const nowMs = typeof now === 'number' ? now : Date.now();
102
155
  const out = {};
103
156
  const src = ledger.dedup || {};
104
157
  const keys = Object.keys(src);
105
158
  for (let i = 0; i < keys.length; i++) {
106
159
  const k = keys[i];
107
- if (typeof src[k] === 'number' && src[k] >= cutoff) out[k] = src[k];
160
+ const entry = src[k];
161
+ // Legacy ledgers written by older versions stored plain timestamps; treat
162
+ // them as successful orders (the original behaviour) so an upgrade does
163
+ // not suddenly forget recent dedups.
164
+ if (typeof entry === 'number') {
165
+ if (entry >= nowMs - DEDUP_TTL_MS) out[k] = entry;
166
+ } else if (entry && typeof entry.ts === 'number') {
167
+ const ttl = entry.failed ? DEDUP_FAILURE_TTL_MS : DEDUP_TTL_MS;
168
+ if (entry.ts >= nowMs - ttl) out[k] = entry;
169
+ }
108
170
  }
109
171
  ledger.dedup = out;
110
172
  return ledger;
@@ -125,7 +187,8 @@ function _effectiveCap(value, now) {
125
187
 
126
188
  function start(opts) {
127
189
  if (_started) return;
128
- if (!_isEnabled()) return;
190
+ const consent = getConsent();
191
+ if (!consent.enabled) return;
129
192
  _started = true;
130
193
  _startedAt = Date.now();
131
194
  const dailyCap = Math.max(0, Math.floor(Number((opts && opts.dailyCap) || process.env.ATP_AUTOBUY_DAILY_CAP_CREDITS) || DEFAULT_DAILY_CAP));
@@ -157,11 +220,36 @@ function _withTimeout(promise, timeoutMs) {
157
220
  ]);
158
221
  }
159
222
 
160
- async function considerOrder(opts) {
161
- if (!_started) return { ok: false, skipped: true, reason: 'not_started' };
223
+ // Single-flight queue: serialize the read → cap-check → placeOrder → write
224
+ // pipeline so two concurrent considerOrder() calls cannot both pass the cap
225
+ // check on the same ledger snapshot and silently double-spend.
226
+ //
227
+ // Without this, two parallel calls (e.g. user runs Claude Code in two tabs
228
+ // through the same proxy, or two capability gaps fire in the same tick) both
229
+ // read spent=40, both compute remaining=10, both await placeOrder, both
230
+ // increment to spent=50, and write — silently exceeding the daily cap by one
231
+ // full order each. autoBuyer is single-process so an in-memory queue is
232
+ // sufficient; a file lock would only be needed if multiple OS processes
233
+ // shared the same ledger file (not the current deployment model).
234
+ let _orderQueue = Promise.resolve();
235
+
236
+ function considerOrder(opts) {
237
+ if (!_started) return Promise.resolve({ ok: false, skipped: true, reason: 'not_started' });
162
238
  if (!opts || !Array.isArray(opts.capabilities) || opts.capabilities.length === 0) {
163
- return { ok: false, skipped: true, reason: 'no_capabilities' };
239
+ return Promise.resolve({ ok: false, skipped: true, reason: 'no_capabilities' });
164
240
  }
241
+ const next = _orderQueue.then(
242
+ () => _considerOrderSerialized(opts),
243
+ () => _considerOrderSerialized(opts), // never let a prior rejection break the chain
244
+ );
245
+ // Swallow rejection on the queue tail so a single thrown error here does
246
+ // not poison every subsequent call; the original `next` promise still
247
+ // surfaces the error to the caller.
248
+ _orderQueue = next.then(() => {}, () => {});
249
+ return next;
250
+ }
251
+
252
+ async function _considerOrderSerialized(opts) {
165
253
  const now = Date.now();
166
254
  let ledger = _readLedger();
167
255
  ledger = _rotateIfNewDay(ledger, now);
@@ -200,19 +288,49 @@ async function considerOrder(opts) {
200
288
 
201
289
  if (result && result.ok) {
202
290
  ledger.spent = (ledger.spent || 0) + budget;
203
- ledger.dedup[hash] = now;
291
+ ledger.dedup[hash] = { ts: now, failed: false };
204
292
  _writeLedger(ledger);
205
293
  console.log('[ATP-AutoBuyer] Order placed: ' + (result.data && result.data.order_id) + ' budget=' + budget + ' remaining_today=' + Math.max(0, dailyCap - ledger.spent));
206
294
  return { ok: true, data: result.data, spent: budget };
207
295
  }
208
296
 
209
- // On failure still record dedup so we don't hammer the hub for the same
210
- // capability gap within the TTL window (but do NOT charge the spend).
211
- ledger.dedup[hash] = now;
297
+ // On failure record a SHORT-TTL dedup entry (5 min) so we don't hammer the
298
+ // hub for the same capability gap inside a tight retry loop, but the user
299
+ // can retry the same question once the transient error clears — far better
300
+ // than the previous 24h block for a single 503.
301
+ ledger.dedup[hash] = { ts: now, failed: true };
212
302
  _writeLedger(ledger);
213
303
  return { ok: false, error: (result && result.error) || 'unknown_error' };
214
304
  }
215
305
 
306
+ // Write the consent ack file. Used by `evolver atp enable|disable` and the
307
+ // first-run prompt. `enabled=true` persists opt-in; `enabled=false` persists
308
+ // explicit opt-out so the prompt does not re-ask next session. Atomic write
309
+ // via .tmp + rename so a crash mid-write never produces a corrupt ack file.
310
+ function setConsent(enabled) {
311
+ const dir = getMemoryDir();
312
+ const body = {
313
+ enabled: !!enabled,
314
+ acknowledged_at: new Date().toISOString(),
315
+ version: 1,
316
+ };
317
+ const tmp = _ackPath() + '.tmp';
318
+ // Single try/catch over the whole pipeline. Previously the mkdirSync was
319
+ // wrapped in its own swallowing try/catch, so an EACCES on the parent dir
320
+ // would surface to the caller as a confusing ENOENT from writeFileSync.
321
+ // Surface the original error verbatim and best-effort clean up any
322
+ // partial .tmp file so a retry from a TTY prompt sees a clean slate.
323
+ try {
324
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
325
+ fs.writeFileSync(tmp, JSON.stringify(body, null, 2) + '\n', 'utf8');
326
+ fs.renameSync(tmp, _ackPath());
327
+ } catch (err) {
328
+ try { fs.unlinkSync(tmp); } catch (_) {}
329
+ throw err;
330
+ }
331
+ return body;
332
+ }
333
+
216
334
  // Test-only reset, not exported by default.
217
335
  function _resetForTests() {
218
336
  _started = false;
@@ -222,15 +340,30 @@ function _resetForTests() {
222
340
  perOrderCap: DEFAULT_PER_ORDER_CAP,
223
341
  timeoutMs: DEFAULT_ORDER_TIMEOUT_MS,
224
342
  };
343
+ _orderQueue = Promise.resolve();
225
344
  }
226
345
 
227
346
  module.exports = {
347
+ // Lifecycle.
228
348
  start,
229
349
  stop,
230
350
  isStarted,
231
351
  considerOrder,
352
+ // Consent surface — public API. Production callers (CLI runAtp,
353
+ // cliAutobuyPrompt, the daemon run loop) MUST use these, not the
354
+ // __internals duplicates below, so the "test-only" contract on
355
+ // __internals stays honest (Bugbot PR #141 R6).
356
+ getConsent,
357
+ setConsent,
358
+ getAckPath: _ackPath,
359
+ readAck: _readAck,
360
+ ACK_FILENAME,
232
361
  // Exposed for tests and diagnostics only; callers should not depend on
233
- // these internals in production code paths.
362
+ // these internals in production code paths. Ack-file helpers
363
+ // (getAckPath / readAck / ACK_FILENAME) are intentionally NOT mirrored
364
+ // here — production and tests both go through the public surface above,
365
+ // so a single source of truth survives schema changes (Bugbot PR #141 R6
366
+ // follow-up: keep "test-only" honest, no production caller may reach in).
234
367
  __internals: {
235
368
  readLedger: _readLedger,
236
369
  writeLedger: _writeLedger,
@@ -242,6 +375,7 @@ module.exports = {
242
375
  DEFAULT_PER_ORDER_CAP,
243
376
  COLD_START_WINDOW_MS,
244
377
  DEDUP_TTL_MS,
378
+ DEDUP_FAILURE_TTL_MS,
245
379
  LEDGER_FILENAME,
246
380
  },
247
381
  },
@@ -155,6 +155,18 @@ function start(opts) {
155
155
  _pollInterval = setInterval(function () {
156
156
  _tick().catch(function () { /* swallowed in _tick */ });
157
157
  }, _pollMs);
158
+ // .unref() so this background poller does NOT keep the Node event
159
+ // loop alive on its own. `evolver run` (single-shot) writes its
160
+ // artifacts and expects to exit — without unref, the setInterval
161
+ // handle pins the process and the run sits as a residual `node`
162
+ // process until manually killed (public issue #553). `evolver --loop`
163
+ // (daemon) keeps the foreground evolve loop alive on its own
164
+ // schedule, so an unref'd poller still polls — unref only changes
165
+ // whether THIS handle alone keeps the loop alive, not whether the
166
+ // handle fires.
167
+ if (_pollInterval && typeof _pollInterval.unref === 'function') {
168
+ _pollInterval.unref();
169
+ }
158
170
  // Do not await -- fire the first tick asynchronously so start() returns
159
171
  // immediately. This matches the autoBuyer start() semantics.
160
172
  _tick().catch(function () { /* swallowed in _tick */ });
@@ -189,6 +201,10 @@ module.exports = {
189
201
  writeLedger: _writeLedger,
190
202
  buildProofPayload: _buildProofPayload,
191
203
  resetForTests: _resetForTests,
204
+ // Test-only accessor for the active poll Timeout. Used to assert
205
+ // the timer was `.unref()`ed so it does not pin the Node event
206
+ // loop (regression guard for public issue #553).
207
+ getPollIntervalForTest: () => _pollInterval,
192
208
  constants: {
193
209
  DEFAULT_POLL_MS,
194
210
  MIN_POLL_MS,