Commit Graph

3 Commits

Author SHA1 Message Date
evolver-publish 1c7fa4cfcb Release v1.88.4 2026-06-08 00:06:58 +08: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 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