Releases: EvoMap/evolver
v1.86.0
v1.86.0 β Optional OS-keychain backing for workspace-id (issue #111 Phase 1)
Highlights
- Workspace-id can now live in the OS keychain instead of
<workspace>/.evolver/workspace-id. Closes the same-uid readability gap left by v1.83's FS-only secret. Optional@napi-rs/keyringaddon backs the secret onto macOS Keychain Services / libsecret on Linux / Windows Credential Manager. Behaviour is identical to v1.85.x for any deployment that doesn't install the optional addon.
Added
EVOLVER_WORKSPACE_KEYCHAIN={auto,force,off}(default:auto) β controls howgetWorkspaceId()resolves the per-workspace secret:autoβ try keychain, fall back to FS on any failure. Existing FS secrets migrate into the keychain on first call. The FS file is intentionally kept so bun-compiled binaries (which can't sideload the.nodeaddon yet β Phase 2) keep agreeing with node-CLI sessions on the same id.forceβ keychain only. Throws if the addon isn't loaded, the keyring reports unavailable mid-call, or write fails. Use in CI to assert the addon is present and never falls back to FS plaintext.offβ skip keychain entirely; FS only.
EVOLVER_WORKSPACE_IDenv override is unchanged and still wins over both keychain and FS.
Fixed
- Lifecycle
hellono longer mints a freshnode_idwhen MailboxStore is empty but~/.evomap/node_idstill exists. Any install whosestate.jsonwas created or wiped after the legacy file existed (upgrade from a pre-lifecycle version, partial recovery flow) registered a brand-new A2ANode under the same owner on the next daemon boot, abandoning the original record's stake / reputation / aliases as an orphan in the web UI. The reader now consults the legacy hex file between the store lookup and thecrypto.randomBytes(6)fallback so both paths agree on a single identity.
Refactored
- Lifecycle's legacy
node_idreader now routes throughpaths.getEvomapPath()soEVOLVER_HOMEhonours both reader and writer in lockstep (the writer insrc/gep/a2aProtocol.jswas consolidated onto the same helper in #114). No behaviour change beyond unifying the override semantics.
Internal
- End-to-end test for the issue #540 advisory-signal fix and a new CHANGELOG release-section integrity guard (#113 / #115). PR #105 added function-level tests; the new e2e tests run
collect.jsoutputs throughgep/signals.jsto lock in the full pipeline so a future refactor that breaks either end fails loudly. The newscripts/check-changelog.js+pre_publish_check.jsintegration catches the misattribution pattern that bit us in #540 / PR #107 (an entry filed under## [X.Y.Z]after vX.Y.Z was already published).extractSectionis line-anchored so fenced code samples don't trigger false drift;EVOLVER_CHANGELOG_GUARD_SOFT=1is available as an interim escape hatch while private-dev still relies on public-mirror tags.
Migration
- Existing v1.85.x installs that haven't installed
@napi-rs/keyringcontinue to use the FS path with no behaviour change. - To opt in:
npm install @napi-rs/keyring(already inoptionalDependencies, so a freshnpm installwill pull it on supported platforms automatically). - To enforce keychain-only on CI:
export EVOLVER_WORKSPACE_KEYCHAIN=force.
Issue / PR
v1.85.3
v1.85.3 β Hotfix release
This release fixes two install-path regressions reported in #540 and #541 / #542. Both were silent breakages that landed in the v1.85.1 / v1.85.2 line and only surfaced under specific install layouts.
Fixed
-
Codex
session-endhook no longer crashes withSyntaxError: Identifier 'path' has already been declaredon fresh install (#542). Previous releases ranevolver setup-hooks --platform=codexand produced a parse-broken hook β Codexsession-endwas entirely blocked with no user-side workaround. The duplicateconst path = require('path')is removed, and a newnode --checkregression suite now guards every file insrc/adapters/scripts/plus the CLI entry, so a parse-time failure in entry-point scripts (which the existing vitest suite never loads) cannot reach npm again. -
getRepoRoot()no longer escapesnode_modulesto pick up an unrelated outer.git(#541). A global install (e.g. macOS Homebrew at/opt/homebrew/lib/node_modules/@evomap/evolver) used to walk past its ownnode_modulesand resolverepoRootto/opt/homebrew/.git, silently sendingworkspaceRoot/memoryDir/evolutionDirto a directory that doesn't belong to the user. The walk now stops at the parent of the nearestnode_modulesancestor: local installs still find the user project's.git, global installs fall back to the install dir, dev clones keep the original unbounded walk.EVOLVER_REPO_ROOTremains the explicit override.
Upgrade path
npm install -g @evomap/evolver@latestAfter upgrade, re-run evolver setup-hooks --platform=codex if you previously hit the SyntaxError to regenerate a clean hook script.
v1.85.2
Release v1.85.2
v1.85.1
Fixed
-
Stop hook no longer re-injects the evolution receipt as a user prompt.
The hook used to emitfollowup_message, which by Claude Code spec
feeds its value back into the next inference round. Agents kept
"responding" to their own evolution log line β visible to users as an
unexplained extra reasoning turn after every task. The hook now emits
onlysystemMessage, a UI-only notification. -
Cursor compatibility for the Stop hook. Cursor's Claude Code-compat
runtime currently treatssystemMessageas a user prompt as well.
When Cursor is detected (viaTERM_PROGRAM=cursor,CURSOR_TRACE_ID,
CURSOR_SESSION_ID, or the manual overrideEVOLVER_HOOK_HOST=cursor),
the hook omitssystemMessagetoo. The receipt is always appended to
~/.evolver/logs/evolution.log(override path with
EVOLVER_HOOK_LOG_DIR) so it is never silently lost. Set
EVOLVER_HOOK_VERBOSE=1to force the inline notification on under
Cursor for debugging. -
Stop hook process now exits promptly. A 7 s
setTimeoutwatchdog
was held open for the full duration on every session end because
it was never cleared after stdin closed. Now cleared explicitly. -
evolver --reviewno longer raisesmemory_missing/user_missing/
session_logs_missingon every cycle when running on Codex (#540).
Codex doesn't generate a workspace-rootMEMORY.md/USER.mdand
doesn't expose readable session-transcript files.collect.jsnow
falls back, in order:MEMORY.md/USER.mdβ the
<!-- evolver-evolution-memory -->section thatsetup-hooksinjects
into the workspace'sAGENTS.md/CLAUDE.mdβ the tail of
memory_graph.jsonl(last 5 outcomes). README adds a "Codex caveats"
subsection.
Security
- Per-workspace random secret replaces plain-text
cwdself-tag in
memory_graph.jsonl(#109). v1.85.0 + PR #108 introduced acwd
field on every memory-graph entry to scope reads at the user-level
fallback path. The Cursor Bugbot round-3 advisory pointed out that
cwdis a self-report β any process under the same uid could write
entries claiming a different workspace'scwdand poison its reads.
This release addspaths.getWorkspaceId(), which lazily creates
<workspace>/.evolver/workspace-id(mode 0600, 32-hex random) and
stamps every new entry withworkspace_id. The reader uses a
three-tier policy: preferworkspace_idmatching when the current
workspace has a secret, fall back to legacycwd-only matching when
it doesn't (clean upgrade for pre-existing entries), drop entries
that have neither. The secret file is created with
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOWafter anlstatpre-check,
so a pre-placed symlink can't redirect the write outside the
workspace.EVOLVER_WORKSPACE_IDenv var remains as an explicit
override / escape hatch. Note: in the co-uid threat model, any process
under the same uid can still read the FS secret β migrating to an
OS keychain is tracked as a follow-up.
π€ Generated with Claude Code
v1.85.0
Release v1.85.0
v1.84.2
v1.84.2
Fixes for Codex / Claude Code hook adapters reported on the public
issue tracker (#536β#540), plus security hardening surfaced during
review.
Hook adapter fixes
- #536 Codex
session-endno longer "records to nowhere" on
npm-global installs._runtimePaths.jsis now copied alongside the
hook scripts and resolves@evomap/evolvervia system roots; falls
back to~/.evolver/memory/evolution/memory_graph.jsonlif the
evolver root is read-only. - #537 Codex
session-endno longer printsThe system cannot find the path specified.on Windows.execSyncshell strings
with POSIX2>/dev/nullredirects are replaced with argv-array
spawnSync, so cmd.exe never sees/dev/null. - #538
evolver setup-hooks --uninstallnow removes everything
it installs: hook scripts (including_runtimePaths.js), the
evolution-memory section inAGENTS.md/CLAUDE.md, and
codex_hooks = trueplus the empty[features]block in
.codex/config.toml. Uninstall also catches evolver-owned hook
entries even when the_evolver_managedmarker has been stripped. - #539
evolver setup-hooksno longer overwrites user-installed
hooks under the same event.mergeWithHooksUnionkeeps existing
userStop/SessionStart/PostToolUseentries and appends
evolver entries alongside them. - #540 The review engine no longer hard-depends on repo-local
MEMORY.md/USER.md. It first reads platform-supplied
additionalContext/ session_state, then falls back to
evolver-managedmemory_graph.jsonl, then to the markdown files.
Codex's lack of session-log hooks is documented as a platform
limitation (not an evolver bug).
Security hardening
- Refuse to follow symlinks at adapter config dirs (
.codex,
.claude,.cursor,.kiro,.opencode) or at their nested
hooks/andplugins/subdirs. - Refuse to copy a hook script when the destination path is a
pre-planted symlink (would otherwise let a hostile workspace
redirect overwrites to arbitrary writable files). findEvolverRoot()no longer trustsprocess.cwd()when
resolving@evomap/evolverβ only system module roots are
searched, so a workspace-controllednode_modules/@evomap/evolver
cannot redirect the memory graph and inject prompt-injection
content viaadditionalContext.- Codex
cleanConfigTomlonly drops the[features]header when
the section is genuinely empty; user entries under[features]
are preserved across uninstall. evolver-session-endrunGitdistinguishes "command failed"
from "command succeeded with empty output", so an empty merge
doesn't fall through to the working-tree diff and surface
unrelated unstaged changes as a session outcome.
Reviewed by
Cursor Bugbot agentic security review (7 rounds), final commit
f32f709b75eeb0187780318a9a8a602a53214c4d. 99 / 99 adapter tests
passing.
v1.84.1
Release v1.84.1
v1.84.0
Release v1.84.0
v1.83.0
[1.83.0] - 2026-05-16
Added β Publish β recall round-trip verification (#53, #56)
After every successful Hub publish (Capsule, AntiPattern, or SkillBundle),
the daemon now confirms that the asset can actually be recalled from
Hub. A new src/gep/recallVerifier.js worker runs Phase 2 deterministic
asset_id lookups on a sampling of published assets, with exponential
backoff to absorb indexing latency. Outcomes (roundtrip_ok /
roundtrip_missing / roundtrip_mismatch / verification_skipped) land
in memory_graph.jsonl as kind=recall_verify events.
A new scripts/recall-verify-report.js aggregates those events into a
Markdown table grouped by asset type with success rate + p50/p95/p99
latency. Exit code is a ship-gate: 0 when every asset type meets β₯ 0.95
success and zero mismatches, exit 2 otherwise. Designed to plug into
deploy.sh as a pre-publish guard.
This closes the "daemon knows results" theme set up by v1.82.0's open-PR
overlap detection: v1.82.0 made the daemon aware of in-flight work so it
stops re-inventing it; v1.83.0 makes the daemon aware of whether its own
uploaded experience is actually retrievable later. Together they bound
the daemon's two main blind spots β "what other people are doing" and
"whether what I just uploaded actually stuck."
Architecture
| Layer | Behavior |
|---|---|
src/gep/recallVerifier.js (new) |
Bounded ring queue (default 256), single-flight by asset_id, sample-rate gate, async setInterval worker (unref'd so process exit is unblocked). Retries on missing with [5s, 15s, 60s] backoff up to 3 attempts. |
src/gep/hubSearch.js |
Phase 2 fetch logic extracted into a reusable fetchAssetById(assetId, opts) helper. Cache-first ordering preserved so hot payloads return instantly even when the search-phase budget is exhausted. |
| Three publish hooks | solidify.js capsule + anti-pattern, skill2gep.js publishBundleChannel. Each enqueues only after res.ok && !res.dry_run, and uses the sanitized capsule's asset_id so the verifier looks up the same hash the Hub indexed. |
scripts/recall-verify-report.js (new) |
Markdown report with monotonic gate severity (RED never downgrades to YELLOW). |
index.js |
[RecallVerify] ENABLED/DISABLED startup banner, starts the worker once, with sample-rate range clamping. |
Mismatch detection
verifyOnce recomputes computeAssetId on the recalled body and
compares to results[0].asset_id. A mismatch means the Hub re-encoded or
corrupted the asset between publish and fetch β surfaced as
roundtrip_mismatch and flips the gate to RED.
Configuration (all default-on)
| Env var | Default | Purpose |
|---|---|---|
EVOLVE_RECALL_VERIFY |
1 |
Master switch |
EVOLVE_RECALL_VERIFY_SAMPLE_RATE |
1.0 |
Fraction of publishes to verify (clamped to [0, 1]) |
EVOLVE_RECALL_VERIFY_QUEUE_MAX |
256 |
Ring queue size |
EVOLVE_RECALL_VERIFY_INITIAL_WAIT_MS |
5000 |
First-attempt delay |
EVOLVE_RECALL_VERIFY_POLL_MS |
5000 |
Worker tick rate |
EVOLVE_RECALL_VERIFY_ATTEMPTS |
3 |
Retries on roundtrip_missing |
EVOLVE_RECALL_VERIFY_FETCH_TIMEOUT_MS |
8000 |
Phase 2 timeout |
Hub mirror staging
recall_verify is intentionally not in HUB_SYNC_KIND_ALLOWLIST on
first ship β local-only until Hub side confirms it accepts the schema.
One-line follow-up patch enables mirroring later.
Hardened β five Bugbot review iterations on the publish/verify
data flow
PR #53 + #56 went through five Cursor Bugbot rounds. Each surfaced a
real defect that would have shipped silently:
- schemas/protocol obfuscation gap.
recallVerifier.jswas missing
frompublic.manifest.json:obfuscate; would have shipped in plaintext
to npm. - Phase 2 cache bypass regression. Refactored time-budget check
ordered before cache lookup, so hot payloads were silently re-fetched
(or worse, dropped) under deadline pressure. - Gate severity could downgrade.
recall-verify-report.js's gate
loop overwrote later rows, so AntiPattern@RED followed by Capsule@YELLOW
reported YELLOW β misleading dashboards even though exit code was
correct. Replaced with monotonic escalation (RANK ordinal). - Wrong asset_id used for recall lookup.
solidify.js's capsule and
anti-pattern paths enqueued the pre-sanitizeasset_idfor
verification, but Hub indexes by the sanitized hash. Any PII
redaction would have produced persistent falseroundtrip_missing
results, dragging the ship gate toward RED forever. Both paths now
recompute on the sanitized capsule and pass that hash to the verifier. - Sample-rate banner-vs-implementation drift.
index.jsclamped
sample rate to [0, 1] but_getSampleRate()did not, so a negative
env value silently disabled all verification while the banner still
reported 1.0. - Dry-run guard masked Hub rejections.
else if (res && res.dry_run)
classified{ok:false, dry_run:true}(a real Hub reject during
dry-run) as a clean dry-run skip. Now requiresres.ok && res.dry_run.
Notes for operators
- This release is enabled-by-default; no opt-in required to start
collecting recall_verify events. To disable, set
EVOLVE_RECALL_VERIFY=0before the daemon starts. - A 30-minute real-Hub daemon smoke +
recall-verify-reportship-gate
read is the recommended pre-deploy ritual once a non-trivial volume of
publishes has accumulated. - Memory graph events with
kind=recall_verifyare local-only this
release; the next release will add them to the Hub mirror allowlist
once Hub-side schema acceptance is confirmed.
v1.82.1
[1.82.1] - 2026-05-16
Fixed β P0: force-update no longer overwrites user projects (#51, #52)
executeForceUpdate previously used getRepoRoot() to decide which
directory to update. getRepoRoot() preferentially returns the user's
surrounding git project β correct for evolution signals, catastrophic
for "delete everything except .git/node_modules/memory/MEMORY.md and copy
the evolver tarball on top of it." When the Hub pushed a force_update
directive (via heartbeat or event poll) to a node whose process.cwd() was
inside any other git repository, the deletion loop ran inside that repo
and the user lost their files.
This was reproducible end-to-end via cd /tmp/anything-with-.git; trigger executeForceUpdate β the user's files were replaced with the
evolver package contents and the user's package.json was rewritten to
@evomap/evolver. We confirmed the bug actually triggered against
evolver-private-dev itself during fix development (recovered via
git reset --hard HEAD) β that's the strongest possible evidence for
the severity.
The fix introduces a new paths.getEvolverInstallRoot() that returns
the evolver package directory (path.resolve(__dirname, '..', '..'))
regardless of process.cwd() or EVOLVER_REPO_ROOT. forceUpdate.js
now uses this INSTALL_ROOT for every read, delete, and copy. A
defense-in-depth guard refuses the update entirely if
INSTALL_ROOT/package.json does not name @evomap/evolver β so any
future path-resolution regression fails closed instead of wiping an
unrelated directory. The staging tmp directory also moves to
os.tmpdir() because INSTALL_ROOT's parent is typically not writable
under a global npm install.
Recommended action: upgrade immediately. Anyone running
evolver --loop from inside any git repository is at risk under
v1.82.0 and earlier.
Credit: issue reported by cloudcarver, codex POC reproducer
diagnosed by cloudcarver, fix landed in #52.