- License section now states GPL-3.0-or-later (was stale MIT) in README / README.zh-CN / SKILL.
- Remove the maintainer-only "Public Release" section (non-existent npm scripts + internal publish flow).
- EVOLVER_ISSUE_REPO documented default -> EvoMap/evolver (matches code default).
- Point auto-issue hint (index.js) and skill attribution footer (skillPublisher.js) at EvoMap/evolver.
- Normalize `evolver run` -> `evolver`; rewrite Roadmap into directional items.
- Drop .github/CODEOWNERS from the public mirror.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The section only charted ClawHub installs. Add live npm (monthly +
18-month) and GitHub release download badges alongside the ClawHub
history chart so all three distribution channels are visible.
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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>
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>
* 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>
* 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>
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>
The OpenClaw CritPt Physics Solver baseline cited in the paper
(arXiv:2604.15097) is 9.1%, not 0.00%. Update all four language READMEs
to match the paper.
Co-authored-by: autogame-17 <autogame-17@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- Add stratifyContent() to separate code/comments from document text
- Reduce false positives when detecting signals on file edits
- Filter out JSON fields and code comments that contain keywords like 'error'
- Only check document text for signal keywords
Expected impact: 10-15% reduction in spurious signal detection
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>