@evomap/evolver 1.87.2 → 1.87.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.
- package/README.ja-JP.md +1 -1
- package/README.ko-KR.md +1 -1
- package/README.md +9 -8
- package/README.zh-CN.md +9 -8
- package/package.json +1 -1
- package/scripts/build_binaries.js +31 -7
- package/src/adapters/scripts/_runtimePaths.js +178 -1
- package/src/adapters/scripts/evolver-session-end.js +63 -33
- package/src/adapters/scripts/evolver-session-start.js +127 -43
- package/src/atp/atpExecute.js +35 -8
- package/src/atp/autoBuyer.js +71 -16
- package/src/atp/autoDeliver.js +16 -0
- package/src/atp/cliAutobuyPrompt.js +8 -22
- package/src/atp/hubClient.js +42 -4
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetStore.js +52 -5
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/idleScheduler.js +155 -6
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +6 -2
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/sanitize.js +57 -3
- package/src/gep/selector.js +1 -1
- package/src/gep/selfPR.js +34 -1
- package/src/gep/skill2gep.js +108 -29
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -1
- package/src/proxy/lifecycle/manager.js +97 -37
- package/src/proxy/router/messages_route.js +25 -0
- package/src/proxy/sync/engine.js +68 -31
- package/assets/gep/candidates.jsonl +0 -1
- package/assets/gep/capsules.json +0 -4
- package/assets/gep/events.jsonl +0 -0
- package/assets/gep/failed_capsules.json +0 -4
- package/assets/gep/genes.json +0 -245
- package/assets/gep/genes.jsonl +0 -0
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 ベース進化ループによってスコアを
|
|
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)에 걸쳐
|
|
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
|
|
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
|
|
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
|
|
381
|
-
-
|
|
382
|
-
-
|
|
383
|
-
-
|
|
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
|
-
|
|
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
|
|
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 进化闭环如何把得分从
|
|
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.
|
|
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
|
-
-
|
|
355
|
-
-
|
|
356
|
-
-
|
|
357
|
-
-
|
|
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
|
-
|
|
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 没有副本,只能从
|
|
374
|
+
纯本地、从未上传过的资产 Hub 没有副本,只能从 `.evolver/gep/`、旧版 `assets/gep/` checkout 或磁盘快照找回。
|
|
374
375
|
|
|
375
376
|
## 配置与解耦
|
|
376
377
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.87.
|
|
3
|
+
"version": "1.87.4",
|
|
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
|
-
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
275
|
-
|
|
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.`);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const os = require('os');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
22
23
|
|
|
23
24
|
function isEvolverPackageJson(filePath) {
|
|
24
25
|
try {
|
|
@@ -80,6 +81,162 @@ function findEvolverRoot() {
|
|
|
80
81
|
return null;
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
// Resolve the user's PROJECT directory — the workspace the agent is actually
|
|
85
|
+
// working in — for git-diff collection and workspace tagging.
|
|
86
|
+
//
|
|
87
|
+
// Why this exists: hook scripts must NOT assume `process.cwd()` is the project
|
|
88
|
+
// root. Cursor invokes some hook events (e.g. afterFileEdit) with the working
|
|
89
|
+
// directory set to the *plugin* install dir (`~/.cursor/plugins/local/<name>`),
|
|
90
|
+
// not the opened workspace. A hook that runs `git diff` in cwd would then look
|
|
91
|
+
// for changes in the plugin directory and find none — silently recording
|
|
92
|
+
// nothing for every task. Hosts expose the real workspace root via an env var:
|
|
93
|
+
// - Cursor sets CURSOR_PROJECT_DIR (and a CLAUDE_PROJECT_DIR compat alias)
|
|
94
|
+
// - Claude Code sets CLAUDE_PROJECT_DIR
|
|
95
|
+
// Codex / opencode / Kiro and direct CLI usage leave both unset, in which case
|
|
96
|
+
// `process.cwd()` is already the project root and remains the fallback — so
|
|
97
|
+
// this change is a no-op on those platforms.
|
|
98
|
+
//
|
|
99
|
+
// SECURITY: only honor an env value that points at an existing directory. A
|
|
100
|
+
// stale or empty value must not redirect git collection to a bogus path; we
|
|
101
|
+
// fall through to cwd instead. We intentionally do NOT recurse into evolver
|
|
102
|
+
// package discovery here — this is purely "where is the user's code".
|
|
103
|
+
function resolveProjectDir() {
|
|
104
|
+
for (const key of ['CURSOR_PROJECT_DIR', 'CLAUDE_PROJECT_DIR']) {
|
|
105
|
+
const v = process.env[key];
|
|
106
|
+
if (typeof v === 'string' && v.trim()) {
|
|
107
|
+
try {
|
|
108
|
+
if (fs.statSync(v).isDirectory()) return v;
|
|
109
|
+
} catch { /* not a usable dir — try next / fall back to cwd */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return process.cwd();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Determine the workspace ROOT for a project, mirroring src/gep/paths.js
|
|
116
|
+
// getWorkspaceRoot() step-for-step so the FS-only fallback lands its secret at
|
|
117
|
+
// the SAME path paths.js would (what lets an installed @evomap/evolver read the
|
|
118
|
+
// very same id):
|
|
119
|
+
// 1. OPENCLAW_WORKSPACE override.
|
|
120
|
+
// 2. else the git repo root at/above projectDir, BUT if that repo root has a
|
|
121
|
+
// `workspace/` subdirectory, paths.js returns <repoRoot>/workspace — so we
|
|
122
|
+
// must too, or the two land on different .evolver/workspace-id files (the
|
|
123
|
+
// "read back identically" guarantee would break for such projects).
|
|
124
|
+
// 3. else projectDir.
|
|
125
|
+
function _fsWorkspaceRoot(projectDir) {
|
|
126
|
+
if (process.env.OPENCLAW_WORKSPACE) return process.env.OPENCLAW_WORKSPACE;
|
|
127
|
+
// Walk up from projectDir looking for a .git entry (file or dir) = repo root.
|
|
128
|
+
let repoRoot = null;
|
|
129
|
+
let dir = projectDir;
|
|
130
|
+
while (dir) {
|
|
131
|
+
if (fs.existsSync(path.join(dir, '.git'))) { repoRoot = dir; break; }
|
|
132
|
+
const parent = path.dirname(dir);
|
|
133
|
+
if (parent === dir) break;
|
|
134
|
+
dir = parent;
|
|
135
|
+
}
|
|
136
|
+
if (!repoRoot) return projectDir;
|
|
137
|
+
// Mirror getWorkspaceRoot()'s workspace/ subdir step.
|
|
138
|
+
const workspaceDir = path.join(repoRoot, 'workspace');
|
|
139
|
+
if (fs.existsSync(workspaceDir)) return workspaceDir;
|
|
140
|
+
return repoRoot;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// FS-only re-implementation of src/gep/paths.js getWorkspaceId() for the case
|
|
144
|
+
// where the evolver package is not installed (plugin-only installs). It reads
|
|
145
|
+
// — and lazily, atomically creates — the per-workspace secret at
|
|
146
|
+
// <workspaceRoot>/.evolver/workspace-id. The format (16-byte hex), the path,
|
|
147
|
+
// the 0600 mode, the O_EXCL|O_NOFOLLOW atomic create, and the symlink
|
|
148
|
+
// rejection all match paths.js exactly, so a workspace seeded by this fallback
|
|
149
|
+
// is transparently picked up by paths.getWorkspaceId() once the package is
|
|
150
|
+
// present, and vice-versa. Returns null on any read/write error (caller then
|
|
151
|
+
// falls back to legacy cwd-tag matching — no regression).
|
|
152
|
+
// Read <dir>/workspace-id with the same symlink guards paths.js'
|
|
153
|
+
// _readWorkspaceIdFromFs uses: reject a symlinked .evolver dir, reject a
|
|
154
|
+
// symlinked / non-regular id file, and require hex format. Returns the id, or
|
|
155
|
+
// null on any error / missing file. Used for BOTH the initial read and the
|
|
156
|
+
// EEXIST race re-read so a symlink swapped in between our lstat and openSync
|
|
157
|
+
// can never be followed (Bugbot PR #557).
|
|
158
|
+
function _readWsIdGuarded(dir, file) {
|
|
159
|
+
try {
|
|
160
|
+
const dirStat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
161
|
+
if (dirStat && dirStat.isSymbolicLink()) return null;
|
|
162
|
+
const fileStat = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
163
|
+
if (!fileStat) return null;
|
|
164
|
+
if (fileStat.isSymbolicLink() || !fileStat.isFile()) return null;
|
|
165
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
166
|
+
return raw && /^[a-f0-9]{32,}$/i.test(raw) ? raw : null;
|
|
167
|
+
} catch { return null; }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function _fsWorkspaceId(projectDir) {
|
|
171
|
+
// Whole body is wrapped: the documented contract is "returns null on ANY
|
|
172
|
+
// read/write error" so the session-start/-end hooks degrade gracefully
|
|
173
|
+
// rather than crash. throwIfNoEntry:false only suppresses ENOENT; EACCES/EIO
|
|
174
|
+
// and friends still throw, so a bare lstat/mkdir here must not escape
|
|
175
|
+
// (Bugbot PR #557 round-2 — an unguarded lstat could crash the hook).
|
|
176
|
+
try {
|
|
177
|
+
const dir = path.join(_fsWorkspaceRoot(projectDir), '.evolver');
|
|
178
|
+
const file = path.join(dir, 'workspace-id');
|
|
179
|
+
// Read first, with symlink guards.
|
|
180
|
+
const existing = _readWsIdGuarded(dir, file);
|
|
181
|
+
if (existing) return existing;
|
|
182
|
+
// If the file exists but the guards rejected it (symlink / bad format),
|
|
183
|
+
// refuse rather than create over it.
|
|
184
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })) return null;
|
|
185
|
+
// Missing — create atomically. Refuse a symlinked .evolver dir (O_NOFOLLOW
|
|
186
|
+
// only guards the final component, not intermediate dirs).
|
|
187
|
+
const dirStat = fs.lstatSync(dir, { throwIfNoEntry: false });
|
|
188
|
+
if (dirStat && dirStat.isSymbolicLink()) return null;
|
|
189
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
190
|
+
const payload = require('crypto').randomBytes(16).toString('hex');
|
|
191
|
+
const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL |
|
|
192
|
+
(fs.constants.O_NOFOLLOW || 0);
|
|
193
|
+
let fd;
|
|
194
|
+
try {
|
|
195
|
+
fd = fs.openSync(file, flags, 0o600);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Lost a race — re-read WITH the same symlink guards (paths.js does the
|
|
198
|
+
// same). A bare readFileSync here would follow a symlink swapped in
|
|
199
|
+
// after our dir lstat (Bugbot PR #557).
|
|
200
|
+
if (e && e.code === 'EEXIST') return _readWsIdGuarded(dir, file);
|
|
201
|
+
return null; // ELOOP/EMLINK from O_NOFOLLOW hitting a symlink — refuse.
|
|
202
|
+
}
|
|
203
|
+
try { fs.writeSync(fd, payload + '\n', 0, 'utf8'); } finally { fs.closeSync(fd); }
|
|
204
|
+
try { fs.chmodSync(file, 0o600); } catch { /* best-effort */ }
|
|
205
|
+
return payload;
|
|
206
|
+
} catch { return null; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Resolve the current workspace id — the forge-resistant tag the session-end
|
|
210
|
+
// writer stamps on every memory-graph entry (`workspace_id`). This is the
|
|
211
|
+
// SINGLE source of that resolution: the session-end writer stamps it and the
|
|
212
|
+
// session-start reader scopes by it, so both call this one function. Keeping
|
|
213
|
+
// it here (rather than a copy per hook) is what guarantees reader and writer
|
|
214
|
+
// can never drift apart — if they resolved different ids, no entry would ever
|
|
215
|
+
// match the reader's filter and workspace scoping would silently break.
|
|
216
|
+
// Resolution order:
|
|
217
|
+
// 1. EVOLVER_WORKSPACE_ID env override
|
|
218
|
+
// 2. paths.getWorkspaceId() loaded from the resolved evolver root (this is
|
|
219
|
+
// the richer path — it can additionally back the secret with the OS
|
|
220
|
+
// keychain when @napi-rs/keyring is installed).
|
|
221
|
+
// 3. FS-only fallback for plugin-only installs where the evolver package is
|
|
222
|
+
// not reachable. Without this, plugin users got workspace_id=null and the
|
|
223
|
+
// forge-resistant scoping silently degraded to cwd-tag matching (found
|
|
224
|
+
// via real-Cursor end-to-end testing). The fallback writes the same
|
|
225
|
+
// secret file paths.js uses, so installing the package later is seamless.
|
|
226
|
+
// Still returns null if even the FS write fails — callers must then NOT filter
|
|
227
|
+
// (show everything), preserving prior behavior rather than hiding all memory.
|
|
228
|
+
function resolveWorkspaceId(evolverRoot, projectDir) {
|
|
229
|
+
if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
|
|
230
|
+
const root = evolverRoot || findEvolverRoot();
|
|
231
|
+
if (root) {
|
|
232
|
+
try {
|
|
233
|
+
const paths = require(path.join(root, 'src', 'gep', 'paths.js'));
|
|
234
|
+
if (typeof paths.getWorkspaceId === 'function') return paths.getWorkspaceId();
|
|
235
|
+
} catch { /* paths.js unreachable — fall through to FS-only */ }
|
|
236
|
+
}
|
|
237
|
+
return _fsWorkspaceId(projectDir || resolveProjectDir());
|
|
238
|
+
}
|
|
239
|
+
|
|
83
240
|
// Returns a path to the evolution memory graph, or a fallback location that
|
|
84
241
|
// is guaranteed to be writable. Never returns null — when no evolver root is
|
|
85
242
|
// available, we fall back to `~/.evolver/memory/evolution/memory_graph.jsonl`
|
|
@@ -111,4 +268,24 @@ function findMemoryGraph(evolverRoot) {
|
|
|
111
268
|
return path.join(userDir, 'memory_graph.jsonl');
|
|
112
269
|
}
|
|
113
270
|
|
|
114
|
-
|
|
271
|
+
// Is `dir` inside a git work tree? Cheap, no-shell `git rev-parse`. Returns
|
|
272
|
+
// false on any error (git missing, not a repo, timeout) and never throws — the
|
|
273
|
+
// session-start hook uses this only to decide whether to surface a one-line
|
|
274
|
+
// "evolver needs a git workspace" notice, so a false negative just suppresses
|
|
275
|
+
// the notice rather than breaking anything.
|
|
276
|
+
function isGitWorkspace(dir) {
|
|
277
|
+
try {
|
|
278
|
+
const res = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
279
|
+
cwd: dir,
|
|
280
|
+
encoding: 'utf8',
|
|
281
|
+
timeout: 5000,
|
|
282
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
283
|
+
shell: false,
|
|
284
|
+
});
|
|
285
|
+
return res.status === 0 && typeof res.stdout === 'string' && res.stdout.trim() === 'true';
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
module.exports = { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace };
|
|
@@ -13,27 +13,16 @@ const { spawnSync } = require('child_process');
|
|
|
13
13
|
// on large repos). See GHSA reports / issue #451.
|
|
14
14
|
const MAX_EXEC_BUFFER = 10 * 1024 * 1024;
|
|
15
15
|
|
|
16
|
-
const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
|
|
16
|
+
const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId } = require('./_runtimePaths');
|
|
17
17
|
|
|
18
|
-
// Workspace-id
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// round-1 MEDIUM
|
|
26
|
-
// evolver root; fall back to env-only when paths.js is unreachable.
|
|
27
|
-
function resolveWorkspaceIdForWriter() {
|
|
28
|
-
if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
|
|
29
|
-
const evolverRoot = findEvolverRoot();
|
|
30
|
-
if (!evolverRoot) return null;
|
|
31
|
-
try {
|
|
32
|
-
const paths = require(path.join(evolverRoot, 'src', 'gep', 'paths.js'));
|
|
33
|
-
if (typeof paths.getWorkspaceId === 'function') return paths.getWorkspaceId();
|
|
34
|
-
} catch { /* paths.js unreachable — return null */ }
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
18
|
+
// Workspace-id resolution is shared with the session-start reader via
|
|
19
|
+
// _runtimePaths.resolveWorkspaceId(). Reader and writer MUST resolve the SAME
|
|
20
|
+
// id or workspace scoping silently breaks (no entry would ever match the
|
|
21
|
+
// reader's filter), so this logic lives in exactly one place instead of being
|
|
22
|
+
// duplicated here. The shared resolver mirrors src/gep/paths.js#getWorkspaceId()
|
|
23
|
+
// loaded from the evolver root, with an EVOLVER_WORKSPACE_ID env override —
|
|
24
|
+
// consistent with the review-time reader in src/evolve/pipeline/collect.js
|
|
25
|
+
// (Bugbot PR #109 round-1 MEDIUM; reader/writer drift flagged on PR #555).
|
|
37
26
|
|
|
38
27
|
function runGit(args, cwd) {
|
|
39
28
|
// Argv-array form, no shell. Avoids POSIX `2>/dev/null` redirects that
|
|
@@ -55,7 +44,9 @@ function runGit(args, cwd) {
|
|
|
55
44
|
}
|
|
56
45
|
|
|
57
46
|
function getGitDiffStats() {
|
|
58
|
-
|
|
47
|
+
// Use the host-provided workspace root, not process.cwd(): Cursor runs some
|
|
48
|
+
// hook events with cwd set to the plugin dir, where `git diff` finds nothing.
|
|
49
|
+
const cwd = resolveProjectDir();
|
|
59
50
|
// Distinguish "git failed (no HEAD~1, etc.)" from "git succeeded with
|
|
60
51
|
// empty output (e.g. empty merge)". The previous `||` chain treated
|
|
61
52
|
// both as falsy and fell through to the working-tree diff, which can
|
|
@@ -67,11 +58,16 @@ function getGitDiffStats() {
|
|
|
67
58
|
const filesChanged = (stat.match(/\d+ files? changed/) || ['0'])[0];
|
|
68
59
|
const insertions = (stat.match(/(\d+) insertions?/) || [null, '0'])[1];
|
|
69
60
|
const deletions = (stat.match(/(\d+) deletions?/) || [null, '0'])[1];
|
|
61
|
+
// Distinguish "no git repo here" from "repo with no changes" purely for the
|
|
62
|
+
// skip-log message — the diff commands above can't tell the two apart (both
|
|
63
|
+
// yield empty output). A single cheap rev-parse settles it.
|
|
64
|
+
const isRepo = runGit(['rev-parse', '--is-inside-work-tree'], cwd).out === 'true';
|
|
70
65
|
return {
|
|
71
66
|
stat,
|
|
72
67
|
summary: `${filesChanged}, +${insertions}/-${deletions}`,
|
|
73
68
|
diffSnippet: diffContent.slice(0, 2000),
|
|
74
69
|
hasChanges: stat.length > 0,
|
|
70
|
+
isRepo,
|
|
75
71
|
};
|
|
76
72
|
}
|
|
77
73
|
|
|
@@ -152,6 +148,10 @@ function recordToHub(outcome) {
|
|
|
152
148
|
|
|
153
149
|
function recordToLocal(graphPath, outcome) {
|
|
154
150
|
try {
|
|
151
|
+
// Resolve the project dir once so the cwd tag and the workspace_id secret
|
|
152
|
+
// share a single, consistent source (both must agree with the session-start
|
|
153
|
+
// reader's resolveProjectDir()-based scoping).
|
|
154
|
+
const projectDir = resolveProjectDir();
|
|
155
155
|
const entry = {
|
|
156
156
|
timestamp: new Date().toISOString(),
|
|
157
157
|
gene_id: outcome.geneId || 'ad_hoc',
|
|
@@ -174,8 +174,17 @@ function recordToLocal(graphPath, outcome) {
|
|
|
174
174
|
// .evolver/workspace-id file. cwd is retained as a backward-compat
|
|
175
175
|
// tag so older entries written before this hardening still pass
|
|
176
176
|
// the cwd check.
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
//
|
|
178
|
+
// Use resolveProjectDir() (NOT process.cwd()) so the cwd tag records the
|
|
179
|
+
// user's project, consistent with how the diff above is collected and
|
|
180
|
+
// with the session-start reader's cwd fallback. Under Cursor, cwd is the
|
|
181
|
+
// plugin install dir, so a raw process.cwd() tag would never match the
|
|
182
|
+
// reader's resolveProjectDir()-derived currentDir — silently hiding every
|
|
183
|
+
// cwd-only entry (Bugbot PR #555). collect.js only uses cwd as a legacy
|
|
184
|
+
// fallback (disabled once a workspace_id secret exists), so changing the
|
|
185
|
+
// tag's source — still a directory path — does not affect its scoping.
|
|
186
|
+
cwd: projectDir,
|
|
187
|
+
workspace_id: resolveWorkspaceId(undefined, projectDir),
|
|
179
188
|
source: 'hook:session-end',
|
|
180
189
|
};
|
|
181
190
|
fs.appendFileSync(graphPath, JSON.stringify(entry) + '\n', 'utf8');
|
|
@@ -185,6 +194,24 @@ function recordToLocal(graphPath, outcome) {
|
|
|
185
194
|
}
|
|
186
195
|
}
|
|
187
196
|
|
|
197
|
+
// Append a single timestamped line to ~/.evolver/logs/evolution.log (or
|
|
198
|
+
// EVOLVER_HOOK_LOG_DIR). Best-effort: a log-write failure must never break the
|
|
199
|
+
// hook. Used both for recorded outcomes and for the "skipped, nothing to
|
|
200
|
+
// record" notices so a user can always see why a session did or did not
|
|
201
|
+
// produce an entry.
|
|
202
|
+
function appendEvolutionLog(line) {
|
|
203
|
+
try {
|
|
204
|
+
const logDir = process.env.EVOLVER_HOOK_LOG_DIR
|
|
205
|
+
|| path.join(os.homedir(), '.evolver', 'logs');
|
|
206
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
207
|
+
fs.appendFileSync(
|
|
208
|
+
path.join(logDir, 'evolution.log'),
|
|
209
|
+
`${new Date().toISOString()} ${line}\n`,
|
|
210
|
+
'utf8'
|
|
211
|
+
);
|
|
212
|
+
} catch { /* best-effort, never break the hook on log write */ }
|
|
213
|
+
}
|
|
214
|
+
|
|
188
215
|
function main() {
|
|
189
216
|
let inputData = '';
|
|
190
217
|
let handled = false;
|
|
@@ -204,6 +231,18 @@ function main() {
|
|
|
204
231
|
const diffInfo = getGitDiffStats();
|
|
205
232
|
|
|
206
233
|
if (!diffInfo.hasChanges) {
|
|
234
|
+
// No git diff means no signal source — session-end derives the
|
|
235
|
+
// outcome (status/score/signals/summary) entirely from the diff, so
|
|
236
|
+
// there is nothing meaningful to record. This is expected in a
|
|
237
|
+
// non-git workspace or a repo with no changes this session. Rather
|
|
238
|
+
// than fabricate an empty outcome (which would pollute the memory
|
|
239
|
+
// graph), record nothing — but leave a log breadcrumb so the user
|
|
240
|
+
// can tell "evolver ran but had nothing to record" apart from
|
|
241
|
+
// "evolver never fired". (Previously this branch was fully silent.)
|
|
242
|
+
const reason = diffInfo.isRepo
|
|
243
|
+
? 'no changes detected this session'
|
|
244
|
+
: 'not a git workspace';
|
|
245
|
+
appendEvolutionLog(`[Evolution] Session end: nothing recorded (${reason}).`);
|
|
207
246
|
finish({});
|
|
208
247
|
return;
|
|
209
248
|
}
|
|
@@ -254,16 +293,7 @@ function main() {
|
|
|
254
293
|
// The receipt is always appended to ~/.evolver/logs/evolution.log
|
|
255
294
|
// so it is never silently lost; users can opt back in to the inline
|
|
256
295
|
// notification with EVOLVER_HOOK_VERBOSE=1.
|
|
257
|
-
|
|
258
|
-
const logDir = process.env.EVOLVER_HOOK_LOG_DIR
|
|
259
|
-
|| path.join(os.homedir(), '.evolver', 'logs');
|
|
260
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
261
|
-
fs.appendFileSync(
|
|
262
|
-
path.join(logDir, 'evolution.log'),
|
|
263
|
-
`${new Date().toISOString()} ${msg}\n`,
|
|
264
|
-
'utf8'
|
|
265
|
-
);
|
|
266
|
-
} catch { /* best-effort, never break the hook on log write */ }
|
|
296
|
+
appendEvolutionLog(msg);
|
|
267
297
|
|
|
268
298
|
finish(isCursorHost() ? {} : { systemMessage: msg });
|
|
269
299
|
} catch (e) {
|