Commit Graph

83 Commits

Author SHA1 Message Date
owenshen0907 fab7f4836b docs(codex): clarify Evolver plugin tool setup 2026-06-09 20:14:33 +09:00
evolver-publish 95c26f5b8f Release v1.89.2 2026-06-09 04:03:05 +00:00
evolver-publish 770d59ae51 Release v1.89.1 2026-06-09 03:44:24 +00:00
evolver-publish 1c7fa4cfcb Release v1.88.4 2026-06-08 00:06:58 +08:00
evolver-publish 8e0cbbc97f Release v1.88.3 2026-06-06 18:08:01 +00:00
evolver-publish 534ede263b Release v1.88.2
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: bjw9808 <bjw9808@gmail.com>
Co-authored-by: autogame-17 <166480271+autogame-17@users.noreply.github.com>
2026-06-05 09:29:13 +00:00
evolver-publish a1d22a3d5f Release v1.88.1 2026-06-03 18:04:02 +00:00
autogame-17 a7d564f22b fix(loop): break Ralph-loop when a bridge-mode pending run goes stale (#556) (#559)
* fix(loop): break Ralph-loop when a bridge-mode pending run goes stale (#556)

In `--loop` mode the daemon defaults to EVOLVE_BRIDGE=true (since v1.85.0),
where a sub-agent solidifies asynchronously. If that sub-agent produces no
changes, crashes, or the daemon restarts onto a stale pending state,
`last_solidify.run_id` never catches up to `last_run.run_id`, so
`isPendingSolidify()` stays true and the gate sleeps forever. The existing
auto-reject safety net only ran when EVOLVE_BRIDGE=false, leaving the default
bridge path with no escape.

Rather than unconditionally rejecting every pending run after evolve.run()
(which would wrongly discard runs a live sub-agent is still working, and
re-break the #96 "33 days of zero events" failure mode), this gates the
auto-reject on a staleness TTL: a pending run is only cleared once it is older
than the sub-agent's own hard ceiling (cycleTimeoutMs, default 45 min), at
which point it cannot still be running. A live sub-agent's in-flight pending
state is left untouched.

- add pendingRunAgeMs(state, now): age from last_run.created_at/started_at,
  returns null (never force-reject) on missing/unparseable/future timestamps
- add rejectStalePendingRun(): clears the run with a distinct, auditable reason
  (stale_pending_no_solidify_autoreject_no_rollback), no git rollback
- gate the Ralph-loop sleep on the TTL; on staleness, clear and run a fresh
  cycle. New env EVOLVER_PENDING_STALE_MS overrides (default = cycleTimeoutMs)
- tests: unit coverage for both helpers + a black-box daemon test proving the
  bridge=true default escapes the loop (verified to fail when the fix is reverted)

Closes #556

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(loop): address Bugbot review on #556 stale-pending reject

Two findings from Cursor Bugbot on PR #559:

- High: rejectStalePendingRun() wrote a rejection whenever last_run.run_id
  existed, without re-checking pending status. If the sub-agent solidified
  between the gate's age snapshot and this write, a successful solidify could
  be overwritten with rejected:true. Now re-checks isPendingSolidify() under
  its own fresh read and refuses if no longer pending (TOCTOU guard).
- Medium: the gate fell through to run a fresh cycle regardless of whether
  rejectStalePendingRun() succeeded. A failed write left the run pending while
  the loop started a new cycle on top of it. Now only falls through when the
  reject actually took (cleared===true); otherwise sleeps + continues.

Adds a regression test asserting an already-solidified state is left verbatim.
Full loopMode suite 30/30.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(loop): TTL defaults OFF when cycle timeout is disabled (Bugbot #559)

Bugbot Medium on the round-2 commit: pendingStaleMs defaulted to cycleTimeoutMs
even when EVOLVER_CYCLE_TIMEOUT_ENABLED=false. With the hard ceiling disabled a
sub-agent may legitimately run past 45 min, so a 45-min default TTL could reject
an in-progress async solidify and stack a new evolve.run() on top.

Default the TTL to the cycle ceiling only when that ceiling is actually enforced
(cycleTimeoutEnabled). Otherwise default to 0 = staleness auto-reject OFF; the
user can still opt in explicitly via EVOLVER_PENDING_STALE_MS. The gate already
treats pendingStaleMs <= 0 as "TTL check off" and falls back to the original
sleep behavior.

Adds a black-box regression: with timeout disabled and no explicit TTL, a 1h-old
pending run is NOT auto-rejected. Full loopMode suite 31/31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:02:22 +08:00
evolver-publish f5dbde2836 Release v1.88.0 2026-06-03 08:36:30 +00:00
evolver-publish 12c2992346 Release v1.87.4 2026-06-02 03:52:40 +00:00
autogame-17 63631c1a86 feat(hooks): tell the user when evolution memory is inactive in a non-git folder (#558)
Found via real-Cursor end-to-end testing: in a non-git workspace, session-end
records nothing (every outcome is derived from the git diff) and the only trace
was a line in ~/.evolver/logs/evolution.log the user never sees. So evolver
silently does nothing and the user has no idea why.

session-start now surfaces a one-line notice via additionalContext when the
workspace is not a git repo: "This folder is not a git repository, so evolution
memory is inactive ... run `git init` or open a git project." additionalContext
is injected as opening context and does NOT trigger an extra inference round
(unlike a stop-hook systemMessage, which Cursor mishandles).

The notice is throttled per-folder (30 min) by reusing the session-start dedup
state file; the throttle logic is factored into a shared throttled(key, ttlMs)
helper used by both the Kiro per-prompt dedup and the notice. A new shared
isGitWorkspace() lives in _runtimePaths.js. When a non-git folder DOES have
cwd-tagged memory, the notice and the memory are both shown.

Tests: +4 (notice shown / throttled / not in a git repo / shown alongside
memory). All touched test files pass.

Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:51:13 +08:00
autogame-17 fdebedf3d4 fix(hooks): resolve workspace_id via FS fallback when evolver package is absent (#557)
* fix(hooks): resolve workspace_id via FS fallback when evolver package is absent

Found by real-Cursor end-to-end testing: on plugin-only installs (no
@evomap/evolver package on the machine), resolveWorkspaceId could not reach
paths.getWorkspaceId() and returned null. Every session-end entry was then
stamped workspace_id=null, so the forge-resistant workspace scoping silently
degraded to plain cwd-tag matching — isolation still held (cwd was fixed to
the project dir in #554/#555), but the secret-backed tag the docs promise was
never created.

Add a self-contained FS-only fallback in _runtimePaths.js that reads — and
lazily, atomically creates — the per-workspace secret at
<workspaceRoot>/.evolver/workspace-id. It mirrors src/gep/paths.js exactly:
the workspace root (OPENCLAW_WORKSPACE, else the git repo root at/above the
project dir, else the project dir), the 16-byte hex format, 0600 mode,
O_EXCL|O_NOFOLLOW atomic create, and symlink rejection on both the dir and
file. Because the path and format match, a workspace seeded by the fallback
is read back identically by paths.getWorkspaceId() once the package is
installed — verified: both resolve the same id from the same file.

resolveWorkspaceId now takes an optional projectDir so reader and writer
resolve the secret against the same root; both call sites pass the
resolveProjectDir() value they already compute.

Test: test/resolveWorkspaceId.test.js covers env override, stable hex
generation + persistence + 0600, cross-project isolation, git-repo-root
resolution, OPENCLAW_WORKSPACE override, and symlinked-.evolver refusal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(hooks): close two paths.js-parity gaps in the workspace-id FS fallback

Bugbot (PR #557 round-1) found two ways the FS fallback diverged from
src/gep/paths.js, breaking either the read-back-identically guarantee or
its symlink hardening:

1. (Low, security) The EEXIST race re-read used a bare readFileSync with no
   symlink guards, unlike paths.js which re-reads via _readWorkspaceIdFromFs.
   An attacker swapping .evolver for a symlink between the dir lstat and
   openSync could then have the re-read follow it. Factor the guarded read
   into _readWsIdGuarded() and use it for BOTH the initial read and the
   EEXIST re-read; also refuse a pre-existing non-regular/symlinked id file
   instead of creating over it.

2. paths.js getWorkspaceRoot() returns repoRoot/workspace when that subdir
   exists; _fsWorkspaceRoot omitted that step, so projects with a workspace/
   dir wrote the secret to repoRoot/.evolver while the package reads
   repoRoot/workspace/.evolver -- they would never find each other. Mirror
   the workspace/ step.

Verified: a project with a workspace/ subdir now has the fallback and
paths.getWorkspaceId() resolve the same id from the same file.

Tests: +2 (workspace/ subdir parity; symlinked id-file refusal) -> 8 total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(hooks): guard the whole FS workspace-id resolver against fs throws

Bugbot (PR #557 round-2) found that the bare lstatSync I added in round-1
(to refuse overwriting a guard-rejected id file) sat outside any try-catch.
throwIfNoEntry:false only suppresses ENOENT; EACCES/EIO still throw. Since
evolver-session-start.js main() calls resolveWorkspaceId without a try-catch,
such an error would crash the hook instead of degrading to null -- violating
the documented "returns null on any read/write error" contract.

Wrap the entire _fsWorkspaceId body in a single try-catch so any unexpected
fs throw returns null. The hook then falls back to legacy cwd-tag matching
rather than dying.

Test: regression provoking EACCES via a chmod 000 .evolver dir, asserting
resolveWorkspaceId returns null and does not throw (skipped as root, which
ignores file permissions). 9 tests total.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:01:04 +08:00
autogame-17 f225e30368 fix(hooks): workspace-scope memory recall + log a no-changes breadcrumb (#555)
* fix(hooks): scope session-start memory recall to the current workspace

The session-start hook injected the last 5 memory-graph outcomes with no
workspace filter, while the session-end writer already tags every entry
with workspace_id (forge-resistant) and cwd (backward-compat). On
npm-global installs every project shares the user-level fallback graph
(~/.evolver/memory/evolution/memory_graph.jsonl), so project A's session
start would surface project B's outcomes — the cross-project disclosure /
prompt-injection surface Bugbot flagged on the writer side (PR #105
round-2), which the reader never enforced.

Add resolveWorkspaceId() to _runtimePaths.js mirroring the writer's
resolution (EVOLVER_WORKSPACE_ID, then paths.getWorkspaceId() from the
resolved evolver root). session-start now scopes entries to the current
workspace BEFORE taking the most-recent window — filtering after a tail-N
read would let other projects crowd this workspace out of the window
entirely. Untagged legacy/Hub entries and the can't-resolve-id case both
fall through to 'show it', so there is no regression vs. the old unscoped
behavior; only cross-project leakage is removed.

Tests: test/sessionStartScope.test.js covers isolation, the empty case,
legacy pass-through, cwd fallback, and the belongsToWorkspace predicate
branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(hooks): log a breadcrumb when session-end records nothing

session-end derives the outcome (status/score/signals/summary) entirely
from the git diff. In a non-git workspace, or a repo with no changes this
session, there is no signal source, so the hook correctly records nothing
rather than fabricating an empty outcome that would pollute the memory
graph. But the no-changes branch was fully silent, so a user could not
tell 'evolver ran and had nothing to record' from 'evolver never fired'.

Emit a one-line breadcrumb to evolution.log on that branch, distinguishing
'not a git workspace' from 'no changes detected this session' (a single
cheap rev-parse settles which, since the diff commands can't). No memory
-graph entry is written. Factor the existing inline log-append into a
shared appendEvolutionLog() helper used by both the recorded-outcome and
skip paths.

Tests: two regressions in sessionEndHook.test.js asserting the breadcrumb
is logged and no graph entry is written, for both the non-git and
clean-repo cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(hooks): share one workspace-id resolver between reader and writer

Bugbot (PR #555) flagged that the new _runtimePaths.resolveWorkspaceId
duplicated evolver-session-end.js#resolveWorkspaceIdForWriter. The two
copies were equivalent now, but if either drifted the reader and writer
would resolve different ids — and since the reader filters memory-graph
entries by exact workspace_id match, a drift would silently match nothing
and disable workspace scoping entirely.

Delete resolveWorkspaceIdForWriter and have the writer call the shared
resolveWorkspaceId() (no-arg form is behavior-identical: EVOLVER_WORKSPACE_ID
then paths.getWorkspaceId() from findEvolverRoot()). Single source of truth;
drift is now impossible by construction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(hooks): tag entry.cwd with resolveProjectDir(), not process.cwd()

Bugbot (PR #555 round-2) flagged a reader/writer cwd mismatch: the
session-start reader derives its cwd fallback from resolveProjectDir()
(CURSOR_PROJECT_DIR under Cursor), but the writer still stamped
cwd: process.cwd() — the plugin install dir under Cursor. When an entry
has no workspace_id and belongsToWorkspace() falls to the cwd compare,
the writer's plugin-dir cwd could never equal the reader's project-dir
currentDir, silently hiding every cwd-only entry. The writer also already
used resolveProjectDir() for git-diff collection (#554), making the raw
process.cwd() tag internally inconsistent.

Stamp cwd with resolveProjectDir() so the diff source, the cwd tag, and
the reader's fallback all agree. collect.js uses cwd only as a legacy
fallback (disabled once a workspace_id secret exists), and the tag is
still a directory path, so its scoping contract is unchanged.

Test: regression asserting entry.cwd == CURSOR_PROJECT_DIR (not the hook
process cwd) under a simulated Cursor host.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(hooks): bound session-start memory parse instead of parsing the whole file

Bugbot (PR #555 round-3) flagged that the workspace-scoping fix replaced
readLastN (which read the whole file but parsed only the last 5 lines)
with readAllEntries, which JSON-parses every line. The memory graph can
reach ~100 MB before rotation, so parsing hundreds of thousands of
entries on every session start is real CPU/memory cost.

Replace readAllEntries with readRecentWorkspaceEntries: it still reads the
file (cheap, as readLastN did) but parses lines lazily from the newest end,
keeps only workspace matches, and stops once it has N. Parse count is
bounded by where this workspace's N-th-most-recent entry sits, not by total
file size — while preserving scope-before-trim correctness (other projects
can't crowd this workspace out of the window).

Test: a 200-other-entry graph with this workspace's entries behind them,
asserting the recent own entry still surfaces with no cross-project leak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 03:40:45 +08:00
autogame-17 39c0d3b8a4 fix(hooks): resolve project dir from host env, not process.cwd() (#554)
Cursor invokes hook events (e.g. afterFileEdit) with the working
directory set to the plugin install dir, not the opened workspace.
evolver-session-end.js ran git diff against process.cwd() and therefore
found no changes inside the plugin dir, silently recording nothing for
every Cursor task.

Add resolveProjectDir() to _runtimePaths.js: prefer CURSOR_PROJECT_DIR,
then CLAUDE_PROJECT_DIR (Claude Code, and Cursor compat alias), then fall
back to process.cwd(). Only honor an env value that points at an existing
directory so a stale value cannot redirect git collection. Codex,
opencode, Kiro and direct CLI usage leave both env vars unset, so cwd
remains the source there — a no-op on those platforms.

getGitDiffStats() now uses resolveProjectDir(). The workspace-tag cwd in
recordToLocal() is intentionally left on process.cwd(): the review-time
reader uses it for backward-compat scoping while the forge-resistant tag
is workspace_id, so changing the writer there would risk a silent
reader/writer mismatch.

Tests: new test/resolveProjectDir.test.js (priority order, stale/empty/
file env values) plus two session-end regressions proving a diff is
recorded via CURSOR_PROJECT_DIR / CLAUDE_PROJECT_DIR when cwd is wrong.

Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 00:50:30 +08:00
evolver-publish 4854a3b9b9 Release v1.87.3 2026-06-01 15:20:47 +00:00
evolver-publish 094989c41b Release v1.87.2 2026-05-27 12:50:00 +00:00
evolver-publish 6a7a312a39 Release v1.87.1 2026-05-27 09:01:45 +00:00
evolver-publish 687aebd809 Release v1.87.0 2026-05-26 05:32:31 +00:00
autogame-17 8f28f50d74 docs: align public release workflow (#546)
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
2026-05-26 10:51:19 +08:00
autogame-17 4227d26b7f fix: harden setup-hooks clean install path (#545)
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
2026-05-26 00:05:19 +08:00
evolver-publish 3d5386cfe1 Release v1.86.0 2026-05-23 08:30:01 +00:00
evolver-publish 0d62784919 Release v1.85.3 2026-05-23 04:30:17 +00:00
evolver-publish 3ec75ca16d Release v1.85.2 2026-05-22 14:50:45 +00:00
evolver-publish 2eadd2b618 Release v1.85.1 2026-05-22 11:04:53 +00:00
evolver-publish 211c2f6f87 Release v1.84.2 2026-05-22 03:15:04 +00:00
evolver-publish 4928f05d1d Release v1.84.1 2026-05-18 20:09:38 +00:00
evolver-publish cde44d0ce0 Release v1.84.0
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
2026-05-17 03:24:53 +00:00
evolver-publish 79aac75343 Release v1.83.0 2026-05-16 13:30:17 +00:00
evolver-publish 3dfb136b22 Release v1.82.0 2026-05-16 10:21:43 +00:00
evolver-publish 7c3f103093 Release v1.81.0 2026-05-16 09:19:59 +00:00
evolver-publish 0bca687d5e Release v1.80.9 2026-05-16 07:14:57 +00:00
evolver-publish 65269ec7a0 Release v1.80.8 2026-05-16 05:36:11 +00:00
evolver-publish 17e1c79bbb Release v1.80.7 2026-05-09 10:55:54 +00:00
evolver-publish 9f7b0c5249 Release v1.80.6 2026-05-09 09:04:21 +00:00
evolver-publish eac8f01c3b Release v1.80.5 2026-05-09 05:32:55 +00:00
evolver-publish 1cee702ee9 Release v1.80.4 2026-05-08 12:50:51 +00:00
evolver-publish 147d428cf3 Release v1.80.3 2026-05-07 13:19:12 +00:00
evolver-publish 2cc0f60f9c Release v1.80.2 2026-05-07 13:09:48 +00:00
evolver-publish 4c7857e7e6 Release v1.80.1 2026-05-07 08:36:05 +00:00
evolver-publish ab9d68e25a Release v1.80.0
Co-authored-by: seikiko <17@autogame.ai>
2026-05-07 04:15:29 +00:00
evolver-publish 93e44a3286 Release v1.79.1 2026-05-06 01:46:56 +00:00
evolver-publish 991b39b8cb Release v1.79.0 2026-05-06 00:26:58 +00:00
evolver-publish 4468c9e87b Release v1.78.10 2026-05-05 01:31:13 +00:00
evolver-publish 5304511b1e Release v1.78.9 2026-05-04 07:02:06 +00:00
evolver-publish 2b3c046d5e Release v1.78.8 2026-05-04 02:31:44 +00:00
evolver-publish d2a66203e8 Release v1.78.7 2026-05-03 16:22:32 +00:00
evolver-publish 1f66a0ad3d Release v1.78.3 2026-05-03 13:15:40 +00:00
evolver-publish 0755f608e4 Release v1.78.1
Co-authored-by: seikiko <17@autogame.ai>
2026-05-02 05:53:07 +00:00
evolver-publish 5f276decc0 Release v1.78.0
Co-authored-by: seikiko <17@autogame.ai>
2026-05-01 10:17:22 +00:00
evolver-publish 7f13e5809a Release v1.77.0
Co-authored-by: seikiko <17@autogame.ai>
2026-04-30 13:31:55 +00:00