From 83d881cecc5c890531569a524b13a9af49cced7f Mon Sep 17 00:00:00 2001 From: autogame-17 Date: Tue, 2 Jun 2026 10:06:49 +0000 Subject: [PATCH 1/3] Add evolver plugin: self-evolving agent memory Adds the evolver plugin (MIT) to the marketplace: session hooks that recall what worked on past tasks, detect improvement signals while editing, and record outcomes at session end, scoped per workspace. Degrades gracefully without any extra install; the optional @evomap/evolver npm package unlocks the full review-and-solidify pipeline. - evolver/: plugin dir (.cursor-plugin/plugin.json, hooks, skills, command, rule) - .cursor-plugin/marketplace.json: register evolver Co-Authored-By: Claude Opus 4.8 --- .cursor-plugin/marketplace.json | 5 + evolver/.cursor-plugin/plugin.json | 33 +++ evolver/.gitignore | 5 + evolver/LICENSE | 21 ++ evolver/README.md | 118 ++++++++ evolver/assets/logo.svg | 13 + evolver/commands/evolve.md | 31 +++ evolver/hooks/_filter.js | 65 +++++ evolver/hooks/_paths.js | 300 ++++++++++++++++++++ evolver/hooks/_signals.js | 120 ++++++++ evolver/hooks/hooks.json | 24 ++ evolver/hooks/session-end.js | 306 +++++++++++++++++++++ evolver/hooks/session-start.js | 250 +++++++++++++++++ evolver/hooks/signal-detect.js | 134 +++++++++ evolver/rules/evolution-memory.mdc | 25 ++ evolver/skills/capability-evolver/SKILL.md | 72 +++++ 16 files changed, 1522 insertions(+) create mode 100644 evolver/.cursor-plugin/plugin.json create mode 100644 evolver/.gitignore create mode 100644 evolver/LICENSE create mode 100644 evolver/README.md create mode 100644 evolver/assets/logo.svg create mode 100644 evolver/commands/evolve.md create mode 100644 evolver/hooks/_filter.js create mode 100644 evolver/hooks/_paths.js create mode 100644 evolver/hooks/_signals.js create mode 100644 evolver/hooks/hooks.json create mode 100644 evolver/hooks/session-end.js create mode 100644 evolver/hooks/session-start.js create mode 100644 evolver/hooks/signal-detect.js create mode 100644 evolver/rules/evolution-memory.mdc create mode 100644 evolver/skills/capability-evolver/SKILL.md diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 49f6d8e..f4ef9a5 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -72,6 +72,11 @@ "name": "pstack", "source": "pstack", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence." + }, + { + "name": "evolver", + "source": "evolver", + "description": "Persistent, auditable evolution memory for the agent: recalls what worked on past tasks at session start, detects improvement signals while you edit, and records outcomes when a task ends. Powered by the Genome Evolution Protocol (GEP)." } ] } diff --git a/evolver/.cursor-plugin/plugin.json b/evolver/.cursor-plugin/plugin.json new file mode 100644 index 0000000..6e2332f --- /dev/null +++ b/evolver/.cursor-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "evolver", + "displayName": "Evolver — Self-Evolving Agent Memory", + "description": "Gives the agent a persistent, auditable evolution memory. Recalls what worked on past tasks at session start, detects improvement signals while you edit, and records outcomes when a task ends — powered by the Genome Evolution Protocol (GEP). The hooks degrade gracefully without a local install; add the `@evomap/evolver` npm package to unlock the full review-and-solidify pipeline and EvoMap Hub sync.", + "version": "0.1.0", + "author": { + "name": "EvoMap", + "email": "team@evomap.ai" + }, + "publisher": "EvoMap", + "homepage": "https://evomap.ai", + "repository": "https://github.com/EvoMap/evolver-cursor-plugin", + "license": "MIT", + "logo": "assets/logo.svg", + "keywords": [ + "evolution", + "self-improvement", + "agent-memory", + "gep", + "meta-learning", + "evomap" + ], + "category": "developer-tools", + "tags": [ + "memory", + "automation", + "self-improvement" + ], + "skills": "./skills/", + "commands": "./commands/", + "rules": "./rules/", + "hooks": "./hooks/hooks.json" +} diff --git a/evolver/.gitignore b/evolver/.gitignore new file mode 100644 index 0000000..9208f89 --- /dev/null +++ b/evolver/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +*.log +.env +.env.* +.DS_Store diff --git a/evolver/LICENSE b/evolver/LICENSE new file mode 100644 index 0000000..6c7514c --- /dev/null +++ b/evolver/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 EvoMap + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/evolver/README.md b/evolver/README.md new file mode 100644 index 0000000..db3e713 --- /dev/null +++ b/evolver/README.md @@ -0,0 +1,118 @@ +# Evolver — Self-Evolving Agent Memory (Cursor Plugin) + +Give the Cursor agent a **persistent, auditable evolution memory**. Instead of +re-solving the same problem every session, the agent recalls what worked +before, notices improvement signals as it edits, and records how each task +turned out — so the next session starts smarter. + +Powered by the [Genome Evolution Protocol (GEP)](https://evomap.ai) and the +[`@evomap/evolver`](https://github.com/EvoMap/evolver) engine. + +> **Status:** v0.1.0 — hooks + skill + rule. Works standalone (local memory). +> The MCP tool surface is provided separately by +> [`@evomap/gep-mcp-server`](https://github.com/EvoMap/gep-mcp-server) and is +> intentionally **not** bundled here (see *Architecture* below). + +## What it does + +Three hooks run automatically — you don't invoke them: + +| Hook | Event | Effect | +|---|---|---| +| `evolver-session-start.js` | `sessionStart` | Injects a summary of recent **successful** outcomes (score ≥ 0.5, < 7 days, max 3) as context. | +| `evolver-signal-detect.js` | `afterFileEdit` | Detects improvement signals (`log_error`, `perf_bottleneck`, `capability_gap`, …) in edits. | +| `evolver-session-end.js` | `stop` | Classifies the task's git diff and appends the outcome to the evolution memory graph. | + +It also ships: + +- A **`capability-evolver` skill** describing the recall → work → record loop. +- An **`/evolve` command** for a deliberate evolution checkpoint. +- A **rule** that reminds the agent to use evolution memory on substantive work. + +## Install + +### From the Cursor Marketplace + +Search for **Evolver** in the Cursor plugin marketplace and install. + +### Local development + +```bash +git clone https://github.com/EvoMap/evolver-cursor-plugin +ln -s "$(pwd)/evolver-cursor-plugin" ~/.cursor/plugins/local/evolver +``` + +Reload Cursor. The hooks activate on the next session. + +## Requirements + +- **Node.js** (the hooks are Node scripts; Cursor invokes them via `node`). +- Nothing else for local memory. + +## Modes + +### Local mode (default, zero config) + +Out of the box the hooks write outcomes to +`~/.evolver/memory/evolution/memory_graph.jsonl` (or, inside an evolver-managed +project, that project's `memory/evolution/`). Recall and record work +immediately. **No account, no key, no network.** + +### Full engine + +```bash +npm install -g @evomap/evolver +``` + +Once `@evomap/evolver` is on `PATH`, the same hooks automatically find it and +run the **full pipeline** — automated log analysis and the review-and-solidify +cycle that proposes and applies code improvements — instead of the local-only +fallback. Set `EVOLVER_ROOT` to point at a specific install if you have more +than one. + +### EvoMap Hub (community strategies) + +To sync outcomes and search strategies published by other agents, register an +EvoMap node and set the Hub credentials in your environment: + +```bash +export EVOMAP_HUB_URL="https://evomap.ai" +export EVOMAP_API_KEY="…" # from your EvoMap node +export EVOMAP_NODE_ID="…" +``` + +The `stop` hook will then record outcomes to the Hub (with a local fallback if +the Hub is unreachable). See the [evolver docs](https://evomap.ai) for node +registration. + +## Architecture (why no bundled MCP server) + +EvoMap deliberately splits two products: + +- **`@evomap/evolver`** — the GPL-licensed, source-available evolution engine + (daemon + CLI). Its hook scripts are what this plugin bundles. +- **`@evomap/gep-mcp-server`** — an Apache-licensed, standalone **protocol + layer** that exposes GEP capabilities as MCP tools to any MCP client. + +This plugin bundles the **evolver hooks** because they are exactly the +session-lifecycle glue Cursor needs, and they degrade gracefully when the +engine isn't installed. If you also want the `gep_*` MCP tools inside Cursor, +add `@evomap/gep-mcp-server` to your Cursor MCP config directly — it is not +re-bundled here to avoid duplicating that separately-maintained product. + +## Environment variables + +| Variable | Default | Purpose | +|---|---|---| +| `EVOLVER_ROOT` | (auto-detected) | Path to the `@evomap/evolver` install. | +| `MEMORY_GRAPH_PATH` | (auto) | Override the memory graph file location. | +| `EVOMAP_HUB_URL` / `EVOMAP_API_KEY` / `EVOMAP_NODE_ID` | (unset) | Enable Hub recording. | +| `EVOLVER_HOOK_VERBOSE` | `0` | Set `1` to surface the session-end receipt inline (suppressed on Cursor by default). | + +## License + +MIT © EvoMap. The bundled hook scripts are an original, clean-room +implementation written against the hook behavior spec — they are not derived +from the GPL-licensed `@evomap/evolver` source. Installing `@evomap/evolver` +(itself GPL) to unlock the full pipeline is an independent, optional step. See +`LICENSE`. diff --git a/evolver/assets/logo.svg b/evolver/assets/logo.svg new file mode 100644 index 0000000..0d630b7 --- /dev/null +++ b/evolver/assets/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/evolver/commands/evolve.md b/evolver/commands/evolve.md new file mode 100644 index 0000000..cb69e67 --- /dev/null +++ b/evolver/commands/evolve.md @@ -0,0 +1,31 @@ +--- +description: Run an evolution cycle — recall relevant past outcomes, reflect on the current task, and record what was learned. +--- + +# /evolve + +Trigger a deliberate evolution step for the current task. + +1. **Recall.** Look at the evolution memory the session-start hook injected (or + read the tail of the memory graph at + `~/.evolver/memory/evolution/memory_graph.jsonl`, or the project's + `memory/evolution/memory_graph.jsonl` if present). Summarize any recent + outcome — success or failure — that is relevant to what we're working on. + +2. **Reflect.** Given the current diff / task state, state in one or two lines: + what worked, what didn't, and what the durable lesson is. + +3. **Record.** The `stop` hook records outcomes automatically at task end. If + the user wants to record *now*, and the full engine is installed + (`@evomap/evolver` on `PATH`), run: + + ```bash + evolver run + ``` + + to execute a full evolution cycle. If it is not installed, tell the user the + outcome will still be captured automatically by the stop hook, and that + `npm install -g @evomap/evolver` unlocks the full review-and-solidify cycle. + +Keep this lightweight — `/evolve` is for an explicit checkpoint, not a ceremony +on every turn. diff --git a/evolver/hooks/_filter.js b/evolver/hooks/_filter.js new file mode 100644 index 0000000..d36a8fa --- /dev/null +++ b/evolver/hooks/_filter.js @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Relevance filter for evolution memory entries. Decides which recorded +// outcomes are worth surfacing back to the agent at session start. + +'use strict'; + +const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000; +const MIN_SCORE = 0.5; +const MAX_RESULTS = 3; + +/** + * Parse an entry timestamp into epoch milliseconds, or NaN if absent/invalid. + */ +function timestampMs(entry) { + if (!entry || typeof entry.timestamp !== 'string') { + return NaN; + } + return Date.parse(entry.timestamp); +} + +/** + * Keep only recent, successful, high-scoring outcomes — at most the latest 3. + * + * An entry survives when ALL of the following hold: + * - outcome.status === 'success' + * - outcome.score >= 0.5 + * - its timestamp is within the last 7 days + * + * @param {Array} entries + * @returns {Array} + */ +function filterRelevant(entries) { + if (!Array.isArray(entries)) { + return []; + } + + const now = Date.now(); + const cutoff = now - SEVEN_DAYS_MS; + + const relevant = entries.filter((entry) => { + const outcome = entry && entry.outcome; + if (!outcome || outcome.status !== 'success') { + return false; + } + if (typeof outcome.score !== 'number' || outcome.score < MIN_SCORE) { + return false; + } + const ts = timestampMs(entry); + if (Number.isNaN(ts)) { + return false; + } + return ts >= cutoff && ts <= now; + }); + + // The input arrives chronologically; the most useful items are the latest, + // so keep the tail. + if (relevant.length > MAX_RESULTS) { + return relevant.slice(relevant.length - MAX_RESULTS); + } + return relevant; +} + +module.exports = { filterRelevant }; diff --git a/evolver/hooks/_paths.js b/evolver/hooks/_paths.js new file mode 100644 index 0000000..04bb6c5 --- /dev/null +++ b/evolver/hooks/_paths.js @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Shared path / workspace helpers for the Evolver Cursor plugin hooks. +// Pure Node.js built-ins, no external dependencies. Every exported helper is +// defensive: it must never throw, because the hooks that call it are expected +// to emit valid JSON and exit 0 even under failure conditions. + +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const crypto = require('crypto'); +const { spawnSync } = require('child_process'); + +// Pattern an external tool relies on for the workspace identifier: a lowercase +// hex string of at least 32 characters. Keep this in sync with the contract. +const WORKSPACE_ID_PATTERN = /^[a-f0-9]{32,}$/i; + +/** + * Return true when `candidate` is a string pointing at an existing directory. + * Any stat failure is swallowed and treated as "not a directory". + */ +function looksLikeDir(candidate) { + if (typeof candidate !== 'string' || candidate.length === 0) { + return false; + } + try { + return fs.statSync(candidate).isDirectory(); + } catch (_err) { + return false; + } +} + +/** + * Resolve the directory of the user's current project. + * + * Preference order: + * 1. CURSOR_PROJECT_DIR (if it names an existing directory) + * 2. CLAUDE_PROJECT_DIR (if it names an existing directory) + * 3. the process working directory + */ +function resolveProjectDir() { + const fromCursor = process.env.CURSOR_PROJECT_DIR; + if (looksLikeDir(fromCursor)) { + return fromCursor; + } + const fromClaude = process.env.CLAUDE_PROJECT_DIR; + if (looksLikeDir(fromClaude)) { + return fromClaude; + } + return process.cwd(); +} + +/** + * Determine whether `dir` lives inside a git working tree. + * Shells out to `git rev-parse --is-inside-work-tree`. Returns false on any + * problem (git missing, not a repo, timeout, etc.). + */ +function isGitWorkspace(dir) { + try { + const result = spawnSync( + 'git', + ['rev-parse', '--is-inside-work-tree'], + { + cwd: looksLikeDir(dir) ? dir : undefined, + shell: false, + timeout: 5000, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + } + ); + if (result.status !== 0 || typeof result.stdout !== 'string') { + return false; + } + return result.stdout.trim() === 'true'; + } catch (_err) { + return false; + } +} + +/** + * Return the path to the evolution memory graph (a JSONL file). Honours the + * MEMORY_GRAPH_PATH override; otherwise defaults to a well-known location under + * the home directory and best-effort creates the parent directory. + */ +function findMemoryGraph() { + const override = process.env.MEMORY_GRAPH_PATH; + if (typeof override === 'string' && override.length > 0) { + return override; + } + const defaultPath = path.join( + os.homedir(), + '.evolver', + 'memory', + 'evolution', + 'memory_graph.jsonl' + ); + try { + fs.mkdirSync(path.dirname(defaultPath), { recursive: true }); + } catch (_err) { + // Best effort only; callers tolerate a missing directory. + } + return defaultPath; +} + +/** + * Walk upward from `start` looking for the directory that directly contains a + * `.git` entry (either a directory for normal repos or a file for worktrees / + * submodules). Returns the repo root, or null if none is found. + */ +function findRepoRoot(start) { + let current = path.resolve(start); + // Guard against pathological loops on weird filesystems. + let guard = 0; + while (guard < 256) { + guard += 1; + try { + if (fs.existsSync(path.join(current, '.git'))) { + return current; + } + } catch (_err) { + // Ignore and keep climbing. + } + const parent = path.dirname(current); + if (parent === current) { + break; // reached filesystem root + } + current = parent; + } + return null; +} + +/** + * Read the workspace-id file at `idFile`, applying symlink / regular-file + * guards. Returns the validated id string, or null if the file is missing, + * a symlink, not a regular file, or malformed. + * + * `dotEvolverDir` is the `.evolver` directory; if it is itself a symlink we + * refuse to trust anything beneath it. + */ +function readWorkspaceIdFile(dotEvolverDir, idFile) { + // Refuse to follow a symlinked `.evolver` directory. + let dirStat; + try { + dirStat = fs.lstatSync(dotEvolverDir); + } catch (_err) { + return { ok: false, missing: true }; + } + if (dirStat.isSymbolicLink()) { + return { ok: false, missing: false }; + } + + let fileStat; + try { + fileStat = fs.lstatSync(idFile); + } catch (_err) { + // ENOENT (or similar) => treat as missing so the caller may create it. + return { ok: false, missing: true }; + } + if (fileStat.isSymbolicLink() || !fileStat.isFile()) { + return { ok: false, missing: false }; + } + + let raw; + try { + raw = fs.readFileSync(idFile, 'utf8'); + } catch (_err) { + return { ok: false, missing: false }; + } + const value = raw.trim(); + if (WORKSPACE_ID_PATTERN.test(value)) { + return { ok: true, id: value }; + } + return { ok: false, missing: false }; +} + +/** + * Compute the workspace root used to anchor the workspace-id file. + * - OPENCLAW_WORKSPACE wins if set. + * - Otherwise find the git repo root above `projectDir`; if that root has a + * `workspace/` subdirectory use it, else the root itself. + * - If no repo root exists, fall back to `projectDir`. + */ +function computeWorkspaceRoot(projectDir) { + const explicit = process.env.OPENCLAW_WORKSPACE; + if (typeof explicit === 'string' && explicit.length > 0) { + return explicit; + } + const repoRoot = findRepoRoot(projectDir); + if (!repoRoot) { + return projectDir; + } + const nestedWorkspace = path.join(repoRoot, 'workspace'); + if (looksLikeDir(nestedWorkspace)) { + return nestedWorkspace; + } + return repoRoot; +} + +/** + * Resolve (or lazily create) the forge-resistant workspace identifier. + * + * Contract with external tooling — do not change without coordination: + * - file path: /.evolver/workspace-id + * - file mode: 0600 + * - format: a single 32+ char hex string + * + * Returns the id string, or null when it cannot be safely read or created. + * Never throws. + */ +function resolveWorkspaceId(projectDir) { + try { + const fromEnv = process.env.EVOLVER_WORKSPACE_ID; + if (typeof fromEnv === 'string' && fromEnv.length > 0) { + return String(fromEnv); + } + + const workspaceRoot = computeWorkspaceRoot(projectDir); + const dotEvolverDir = path.join(workspaceRoot, '.evolver'); + const idFile = path.join(dotEvolverDir, 'workspace-id'); + + // First attempt: read an existing, trusted file. + const existing = readWorkspaceIdFile(dotEvolverDir, idFile); + if (existing.ok) { + return existing.id; + } + if (!existing.missing) { + // A file (or `.evolver`) is present but failed the guards. Never clobber + // it — surface "unknown" instead. + return null; + } + + // File is genuinely missing: create it. Re-check the `.evolver` symlink + // guard right before writing. + try { + const dirStat = fs.lstatSync(dotEvolverDir); + if (dirStat.isSymbolicLink()) { + return null; + } + } catch (_err) { + // Does not exist yet — that is fine, mkdir below. + } + + try { + fs.mkdirSync(dotEvolverDir, { recursive: true }); + } catch (_err) { + return null; + } + + const fresh = crypto.randomBytes(16).toString('hex'); // 32 hex chars + let fd; + try { + // O_EXCL + O_NOFOLLOW: fail rather than follow a symlink or overwrite a + // racing writer's file. + const flags = + fs.constants.O_WRONLY | + fs.constants.O_CREAT | + fs.constants.O_EXCL | + fs.constants.O_NOFOLLOW; + fd = fs.openSync(idFile, flags, 0o600); + fs.writeSync(fd, fresh); + } catch (err) { + if (err && err.code === 'EEXIST') { + // Someone created it between our check and write — re-read it through + // the same guards. + const raced = readWorkspaceIdFile(dotEvolverDir, idFile); + return raced.ok ? raced.id : null; + } + return null; + } finally { + if (fd !== undefined) { + try { + fs.closeSync(fd); + } catch (_err) { + // ignore + } + } + } + + // Tighten permissions in case the umask widened them. + try { + fs.chmodSync(idFile, 0o600); + } catch (_err) { + // best effort + } + return fresh; + } catch (_err) { + // EACCES / EIO / anything else: degrade to "unknown workspace". + return null; + } +} + +module.exports = { + resolveProjectDir, + isGitWorkspace, + findMemoryGraph, + resolveWorkspaceId, +}; diff --git a/evolver/hooks/_signals.js b/evolver/hooks/_signals.js new file mode 100644 index 0000000..056e86e --- /dev/null +++ b/evolver/hooks/_signals.js @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Keyword-based evolution signal detection. Shared by the afterFileEdit and +// stop hooks. Deliberately simple: substring matching against a small, +// hand-curated keyword table, with a heuristic to skip code/comment lines. + +'use strict'; + +// Each signal category maps to a set of lowercase trigger phrases. A category +// fires if any of its phrases appears as a substring of the (lowercased) text. +const SIGNAL_KEYWORDS = { + perf_bottleneck: [ + 'timeout', + 'slow', + 'latency', + 'bottleneck', + 'oom', + 'out of memory', + 'performance', + ], + capability_gap: [ + 'not supported', + 'unsupported', + 'not implemented', + 'missing feature', + 'not available', + ], + log_error: [ + 'error:', + 'exception:', + 'typeerror', + 'referenceerror', + 'syntaxerror', + 'failed', + ], + user_feature_request: [ + 'add feature', + 'implement', + 'new function', + 'new module', + 'please add', + ], + recurring_error: [ + 'same error', + 'still failing', + 'not fixed', + 'keeps failing', + 'repeatedly', + ], + deployment_issue: [ + 'deploy failed', + 'build failed', + 'ci failed', + 'pipeline', + 'rollback', + ], + test_failure: [ + 'test failed', + 'test failure', + 'assertion', + 'expect(', + 'assert.', + ], +}; + +// Prefixes that mark a line as "probably code or a comment" — we skip those to +// cut down on false positives from source files that merely mention keywords. +const CODE_LINE_PREFIXES = ['//', '#', '*', '{', '[', '}', ']', '/*']; + +function looksLikeCode(trimmedLine) { + for (const prefix of CODE_LINE_PREFIXES) { + if (trimmedLine.startsWith(prefix)) { + return true; + } + } + return false; +} + +/** + * Detect evolution signals within free-form text. + * + * @param {string} text + * @returns {string[]} sorted, de-duplicated list of signal category names + */ +function detectSignals(text) { + if (typeof text !== 'string' || text.length === 0) { + return []; + } + + // Build the prose-only corpus: drop lines that look like code/comments. + const prose = text + .split('\n') + .filter((line) => { + const trimmed = line.trim(); + if (!trimmed) { + return false; + } + return !looksLikeCode(trimmed); + }) + .join('\n') + .toLowerCase(); + + if (!prose) { + return []; + } + + const found = new Set(); + for (const [category, phrases] of Object.entries(SIGNAL_KEYWORDS)) { + for (const phrase of phrases) { + if (prose.indexOf(phrase) !== -1) { + found.add(category); + break; + } + } + } + return Array.from(found).sort(); +} + +module.exports = { detectSignals, SIGNAL_KEYWORDS }; diff --git a/evolver/hooks/hooks.json b/evolver/hooks/hooks.json new file mode 100644 index 0000000..99c4da3 --- /dev/null +++ b/evolver/hooks/hooks.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "command": "node \"${CURSOR_PLUGIN_ROOT}/hooks/session-start.js\"", + "timeout": 3 + } + ], + "afterFileEdit": [ + { + "command": "node \"${CURSOR_PLUGIN_ROOT}/hooks/signal-detect.js\"", + "timeout": 2 + } + ], + "stop": [ + { + "command": "node \"${CURSOR_PLUGIN_ROOT}/hooks/session-end.js\"", + "timeout": 8, + "loop_limit": 1 + } + ] + } +} diff --git a/evolver/hooks/session-end.js b/evolver/hooks/session-end.js new file mode 100644 index 0000000..b62084a --- /dev/null +++ b/evolver/hooks/session-end.js @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Cursor hook: stop. +// Records the outcome of the session by inspecting the git diff of the project +// directory, writing a memory-graph entry (and optionally posting to a Hub), +// and leaving a breadcrumb in the evolution log. +// +// Invocation: `node session-end.js` with a JSON object on stdin. +// Output: a JSON object on stdout, exit 0. On any failure: `{}`. + +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const { resolveProjectDir, findMemoryGraph, resolveWorkspaceId } = require('./_paths'); +const { detectSignals } = require('./_signals'); + +const STDIN_WATCHDOG_MS = 7000; +const GIT_TIMEOUT_MS = 5000; +const GIT_MAX_BUFFER = 10 * 1024 * 1024; // 10 MB +const HUB_TIMEOUT_MS = 8000; + +let alreadyEmitted = false; + +/** Emit JSON exactly once and exit. */ +function emit(obj) { + if (alreadyEmitted) { + return; + } + alreadyEmitted = true; + let text = '{}'; + try { + text = JSON.stringify(obj); + } catch (_err) { + text = '{}'; + } + process.stdout.write(text); + process.exit(0); +} + +/** + * Append a timestamped line to the evolution log. Best effort; never throws. + */ +function appendEvolutionLog(line) { + try { + const dir = + process.env.EVOLVER_HOOK_LOG_DIR || + path.join(os.homedir(), '.evolver', 'logs'); + fs.mkdirSync(dir, { recursive: true }); + const file = path.join(dir, 'evolution.log'); + fs.appendFileSync(file, `${new Date().toISOString()} ${line}\n`); + } catch (_err) { + // best effort + } +} + +/** Run a git subcommand in `cwd`, returning { status, stdout } (stdout = ''). */ +function git(args, cwd) { + try { + const result = spawnSync('git', args, { + cwd, + shell: false, + timeout: GIT_TIMEOUT_MS, + maxBuffer: GIT_MAX_BUFFER, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + return { + status: typeof result.status === 'number' ? result.status : 1, + stdout: typeof result.stdout === 'string' ? result.stdout : '', + }; + } catch (_err) { + return { status: 1, stdout: '' }; + } +} + +/** + * Collect the git diff for the session. + * - statText: output of `git diff --stat HEAD~1`, falling back to plain diff. + * - body: output of `git diff --no-color HEAD~1`, likewise with fallback. + * - isRepo: whether we're inside a git work tree. + */ +function collectDiff(projectDir) { + const insideTree = git(['rev-parse', '--is-inside-work-tree'], projectDir); + const isRepo = insideTree.status === 0 && insideTree.stdout.trim() === 'true'; + + let stat = git(['diff', '--stat', 'HEAD~1'], projectDir); + if (stat.status !== 0) { + stat = git(['diff', '--stat'], projectDir); + } + + let body = git(['diff', '--no-color', 'HEAD~1'], projectDir); + if (body.status !== 0) { + body = git(['diff', '--no-color'], projectDir); + } + + return { + isRepo, + statText: stat.stdout || '', + body: body.stdout || '', + }; +} + +/** + * Parse "N files changed, A insertions(+), D deletions(-)" from a --stat tail. + * Missing pieces default to 0. + */ +function parseStat(statText) { + const files = (statText.match(/(\d+)\s+files?\s+changed/) || [])[1]; + const ins = (statText.match(/(\d+)\s+insertions?\(\+\)/) || [])[1]; + const del = (statText.match(/(\d+)\s+deletions?\(-\)/) || [])[1]; + return { + files: files ? parseInt(files, 10) : 0, + insertions: ins ? parseInt(ins, 10) : 0, + deletions: del ? parseInt(del, 10) : 0, + }; +} + +/** True when running inside the Cursor host (unless explicitly forced verbose). */ +function isCursorHost() { + const verbose = process.env.EVOLVER_HOOK_VERBOSE; + if (verbose === '1' || verbose === 'true') { + return false; // escape hatch: always speak up + } + return ( + process.env.EVOLVER_HOOK_HOST === 'cursor' || + process.env.TERM_PROGRAM === 'cursor' || + typeof process.env.CURSOR_TRACE_ID === 'string' || + typeof process.env.CURSOR_SESSION_ID === 'string' + ); +} + +/** + * Attempt to POST the outcome to a configured Hub via curl. Returns true on a + * zero-exit curl. Never throws. + */ +function recordToHub(payload) { + try { + const hubUrl = process.env.EVOMAP_HUB_URL || process.env.A2A_HUB_URL; + const apiKey = process.env.EVOMAP_API_KEY || process.env.A2A_NODE_SECRET; + if (!hubUrl || !apiKey) { + return false; + } + const url = `${hubUrl.replace(/\/+$/, '')}/a2a/evolution/record`; + const result = spawnSync( + 'curl', + [ + '-s', + '-S', + '-X', + 'POST', + '-H', + 'Content-Type: application/json', + '-H', + `Authorization: Bearer ${apiKey}`, + '--max-time', + '8', + '--data-binary', + JSON.stringify(payload), + url, + ], + { + shell: false, + timeout: HUB_TIMEOUT_MS, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + } + ); + return result.status === 0; + } catch (_err) { + return false; + } +} + +/** + * Append one JSON entry to the memory graph. The field shape here is a hard + * contract consumed by external tooling — keep it exact. Returns true on + * success. + */ +function recordToLocal(entry) { + try { + const graphPath = findMemoryGraph(); + fs.mkdirSync(path.dirname(graphPath), { recursive: true }); + fs.appendFileSync(graphPath, `${JSON.stringify(entry)}\n`); + return true; + } catch (_err) { + return false; + } +} + +function finish(projectDir, diff) { + const stats = parseStat(diff.statText); + const hasChanges = diff.statText.trim().length > 0; + + // No changes: just leave a breadcrumb, never a memory-graph entry. + if (!hasChanges) { + const reason = diff.isRepo + ? 'no changes detected this session' + : 'not a git workspace'; + appendEvolutionLog(`[Evolution] Session end: nothing recorded (${reason}).`); + emit({}); + return; + } + + // Changes present: derive signals / status / score. + let signals = detectSignals(diff.body); + if (signals.length === 0) { + signals = ['stable_success_plateau']; + } + const failed = signals.includes('log_error') || signals.includes('test_failure'); + const status = failed ? 'failed' : 'success'; + const score = failed ? 0.3 : 0.8; + + const summary = + `Session end: ${stats.files} files changed, ` + + `+${stats.insertions}/-${stats.deletions}. Signals: [${signals.join(', ')}]`; + + // Try the Hub first (if configured). + const hubOk = recordToHub({ + gene_id: 'ad_hoc', + signals, + status, + score, + summary, + sender_id: process.env.EVOMAP_NODE_ID || process.env.A2A_NODE_ID, + }); + + // Always also attempt a local record. + const localOk = recordToLocal({ + timestamp: new Date().toISOString(), + gene_id: 'ad_hoc', + signals, + outcome: { status, score, note: summary }, + cwd: projectDir, + workspace_id: resolveWorkspaceId(projectDir), + source: 'hook:session-end', + }); + + let destination; + if (hubOk) { + destination = 'Hub'; + } else if (localOk) { + destination = 'local memory'; + } else { + destination = 'nowhere (no Hub or local path)'; + } + const receipt = `[Evolution] Session outcome recorded to ${destination}: ${summary}`; + appendEvolutionLog(receipt); + + // Cursor mishandles a stop-hook systemMessage (it re-injects it as a prompt), + // so suppress output entirely when we detect the Cursor host. + if (isCursorHost()) { + emit({}); + return; + } + emit({ systemMessage: receipt }); +} + +// Drain stdin (we don't use it) with a watchdog, then do the work. +(function run() { + try { + const projectDir = resolveProjectDir(); + let done = false; + + const proceed = () => { + if (done) { + return; + } + done = true; + try { + const diff = collectDiff(projectDir); + finish(projectDir, diff); + } catch (_err) { + emit({}); + } + }; + + const watchdog = setTimeout(() => { + // Stdin never closed in time — emit a bare object and bail. + if (!done) { + done = true; + emit({}); + } + }, STDIN_WATCHDOG_MS); + if (typeof watchdog.unref === 'function') { + watchdog.unref(); + } + + process.stdin.on('data', () => {}); + process.stdin.on('end', () => { + clearTimeout(watchdog); + proceed(); + }); + process.stdin.on('error', () => { + clearTimeout(watchdog); + proceed(); + }); + process.stdin.resume(); + } catch (_err) { + emit({}); + } +})(); diff --git a/evolver/hooks/session-start.js b/evolver/hooks/session-start.js new file mode 100644 index 0000000..dea7b65 --- /dev/null +++ b/evolver/hooks/session-start.js @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Cursor hook: sessionStart. +// Surfaces recent, workspace-scoped evolution memory (and a one-time notice if +// the folder isn't a git repo) to the agent as additional context. +// +// Invocation: `node session-start.js` with a JSON object on stdin. +// Output: a JSON object on stdout, exit 0. On any failure: `{}`. + +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { + resolveProjectDir, + isGitWorkspace, + findMemoryGraph, + resolveWorkspaceId, +} = require('./_paths'); +const { filterRelevant } = require('./_filter'); + +const MAX_SCAN_ENTRIES = 5; // how many workspace-matched entries to gather +const LINE_MAX = 200; // per-outcome line truncation +const NONGIT_TTL_MS = 30 * 60 * 1000; // throttle the non-git notice +const THROTTLE_PRUNE_MS = 24 * 60 * 60 * 1000; + +const NONGIT_NOTICE = + '[Evolver] This folder is not a git repository, so evolution memory is ' + + 'inactive (outcomes are derived from git diffs). Run `git init` here, or ' + + 'open a git project, to enable recall and recording.'; + +/** + * Emit a JSON object and exit cleanly. Falls back to `{}` if serialization + * somehow fails. + */ +function emit(obj) { + let text = '{}'; + try { + text = JSON.stringify(obj); + } catch (_err) { + text = '{}'; + } + process.stdout.write(text); + process.exit(0); +} + +/** + * Lightweight throttle backed by a small JSON map of key -> last-fired epoch. + * Returns true when `key` fired within `ttlMs` (caller should suppress). + * Otherwise records "now" and returns false. Fails open (false) on any error. + */ +function throttled(key, ttlMs) { + try { + const base = + process.env.EVOLVER_SESSION_STATE_DIR || + path.join(os.homedir(), '.evolver'); + const stateFile = path.join(base, 'session-start-state.json'); + + let state = {}; + try { + state = JSON.parse(fs.readFileSync(stateFile, 'utf8')); + if (!state || typeof state !== 'object') { + state = {}; + } + } catch (_err) { + state = {}; + } + + const now = Date.now(); + const last = state[key]; + if (typeof last === 'number' && now - last < ttlMs) { + return true; // recently fired -> suppress + } + + // Record this firing and prune stale entries. + state[key] = now; + for (const k of Object.keys(state)) { + if (typeof state[k] !== 'number' || now - state[k] > THROTTLE_PRUNE_MS) { + delete state[k]; + } + } + + try { + fs.mkdirSync(base, { recursive: true }); + fs.writeFileSync(stateFile, JSON.stringify(state)); + } catch (_err) { + // best effort + } + return false; + } catch (_err) { + return false; // fail open + } +} + +/** + * Decide whether a memory entry belongs to the current workspace. + * - tagged with workspace_id: match iff we know our id and it's equal + * (if we don't know our id, don't exclude on this basis) + * - else tagged with cwd: match iff equal (likewise lenient when unknown) + * - untagged: always include + */ +function belongsToWorkspace(entry, currentId, currentDir) { + if (entry && typeof entry.workspace_id === 'string' && entry.workspace_id) { + if (currentId === null || currentId === undefined) { + return true; + } + return entry.workspace_id === currentId; + } + if (entry && typeof entry.cwd === 'string' && entry.cwd) { + if (!currentDir) { + return true; + } + return entry.cwd === currentDir; + } + return true; +} + +/** + * Read the JSONL graph and gather up to MAX_SCAN_ENTRIES entries belonging to + * this workspace, scanning from newest (end) to oldest. Returns them in + * chronological order. + */ +function gatherWorkspaceEntries(graphPath, currentId, currentDir) { + let content; + try { + content = fs.readFileSync(graphPath, 'utf8'); + } catch (_err) { + return []; + } + + const lines = content.split('\n'); + const collected = []; + for (let i = lines.length - 1; i >= 0; i -= 1) { + const line = lines[i].trim(); + if (!line) { + continue; + } + let entry; + try { + entry = JSON.parse(line); + } catch (_err) { + continue; // skip malformed lines + } + if (belongsToWorkspace(entry, currentId, currentDir)) { + collected.push(entry); + if (collected.length >= MAX_SCAN_ENTRIES) { + break; + } + } + } + + collected.reverse(); // newest-first -> chronological + return collected; +} + +/** + * Format the human-readable outcome summary block from filtered entries. + */ +function formatSummary(outcomes) { + const successes = outcomes.filter( + (o) => o.outcome && o.outcome.status === 'success' + ).length; + const failures = outcomes.filter( + (o) => o.outcome && o.outcome.status === 'failed' + ).length; + + const header = + `[Evolution Memory] Recent ${outcomes.length} outcomes ` + + `(${successes} success, ${failures} failed):`; + + const rows = outcomes.map((entry) => { + const outcome = entry.outcome || {}; + let icon = '?'; + if (outcome.status === 'success') { + icon = '+'; + } else if (outcome.status === 'failed') { + icon = '-'; + } + const date = + typeof entry.timestamp === 'string' + ? entry.timestamp.slice(0, 10) + : '??????????'; + const score = + typeof outcome.score === 'number' ? outcome.score : '?'; + const signals = Array.isArray(entry.signals) + ? entry.signals.slice(0, 3).join(', ') + : ''; + const note = + typeof outcome.note === 'string' ? outcome.note : ''; + const line = `[${icon}] ${date} score=${score} signals=[${signals}] ${note}`; + return line.length > LINE_MAX ? line.slice(0, LINE_MAX) : line; + }); + + return ( + [header, ...rows].join('\n') + + '\n\nUse successful approaches. Avoid repeating failed patterns.' + ); +} + +function main() { + const parts = []; + const currentDir = resolveProjectDir(); + + // 1. Non-git notice (throttled per directory). + try { + if (!isGitWorkspace(currentDir)) { + if (!throttled(`nongit:${currentDir}`, NONGIT_TTL_MS)) { + parts.push(NONGIT_NOTICE); + } + } + } catch (_err) { + // ignore — notice is optional + } + + // 2. Workspace-scoped evolution memory. + try { + const graphPath = findMemoryGraph(); + const currentId = resolveWorkspaceId(currentDir); + const candidates = gatherWorkspaceEntries(graphPath, currentId, currentDir); + const relevant = filterRelevant(candidates); + if (relevant.length > 0) { + parts.push(formatSummary(relevant)); + } + } catch (_err) { + // ignore — memory injection is optional + } + + if (parts.length === 0) { + emit({}); + return; + } + + const joined = parts.join('\n\n'); + emit({ agent_message: joined, additionalContext: joined }); +} + +// This hook does not need stdin, but Cursor still pipes a JSON object. Drain it +// so the writer never gets EPIPE, then run. Guard everything. +try { + // Consume stdin without blocking; we don't actually use its contents. + process.stdin.on('data', () => {}); + process.stdin.on('error', () => {}); + process.stdin.resume(); + main(); +} catch (_err) { + emit({}); +} diff --git a/evolver/hooks/signal-detect.js b/evolver/hooks/signal-detect.js new file mode 100644 index 0000000..bf96540 --- /dev/null +++ b/evolver/hooks/signal-detect.js @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2026 EvoMap +// +// Cursor hook: afterFileEdit. +// Inspects freshly edited content for evolution signals (errors, perf hints, +// feature requests, ...) and, when any are found, nudges the agent to consider +// recording the outcome. +// +// Invocation: `node signal-detect.js` with a JSON object on stdin. +// Output: a JSON object on stdout, exit 0. On any failure / timeout: `{}`. + +'use strict'; + +const { detectSignals } = require('./_signals'); + +const STDIN_WATCHDOG_MS = 1500; + +let alreadyEmitted = false; + +/** Emit JSON exactly once and exit. */ +function emit(obj) { + if (alreadyEmitted) { + return; + } + alreadyEmitted = true; + let text = '{}'; + try { + text = JSON.stringify(obj); + } catch (_err) { + text = '{}'; + } + process.stdout.write(text); + process.exit(0); +} + +/** + * Pull the edited content out of the various shapes Cursor / Claude may use. + */ +function extractContent(input) { + if (!input || typeof input !== 'object') { + return ''; + } + const ti = input.tool_input; + if (ti && typeof ti === 'object') { + if (typeof ti.content === 'string') return ti.content; + if (typeof ti.new_string === 'string') return ti.new_string; + if (typeof ti.file_content === 'string') return ti.file_content; + } + if (typeof input.content === 'string') return input.content; + if (typeof input.file_content === 'string') return input.file_content; + if (typeof input.diff === 'string') return input.diff; + return ''; +} + +/** + * Pull the edited file path out of the various shapes. + */ +function extractFilePath(input) { + if (!input || typeof input !== 'object') { + return ''; + } + const ti = input.tool_input; + if (ti && typeof ti === 'object' && typeof ti.file_path === 'string') { + return ti.file_path; + } + const tr = input.tool_response; + if (tr && typeof tr === 'object' && typeof tr.filePath === 'string') { + return tr.filePath; + } + if (typeof input.path === 'string') return input.path; + if (typeof input.file_path === 'string') return input.file_path; + return ''; +} + +function process_(raw) { + let input = {}; + try { + input = raw ? JSON.parse(raw) : {}; + } catch (_err) { + input = {}; + } + + const content = extractContent(input); + const signals = detectSignals(content); + + if (signals.length === 0) { + emit({}); + return; + } + + const where = extractFilePath(input) || 'edited file'; + const ctx = + `[Evolution Signal] Detected: [${signals.join(', ')}] in ${where}. ` + + 'Consider recording this outcome.'; + + emit({ additional_context: ctx, additionalContext: ctx }); +} + +// Drain stdin with a watchdog so we always exit promptly with valid JSON. +(function run() { + try { + let buffer = ''; + const watchdog = setTimeout(() => { + try { + process_(buffer); + } catch (_err) { + emit({}); + } + }, STDIN_WATCHDOG_MS); + if (typeof watchdog.unref === 'function') { + watchdog.unref(); + } + + process.stdin.setEncoding('utf8'); + process.stdin.on('data', (chunk) => { + buffer += chunk; + }); + process.stdin.on('end', () => { + clearTimeout(watchdog); + try { + process_(buffer); + } catch (_err) { + emit({}); + } + }); + process.stdin.on('error', () => { + clearTimeout(watchdog); + emit({}); + }); + process.stdin.resume(); + } catch (_err) { + emit({}); + } +})(); diff --git a/evolver/rules/evolution-memory.mdc b/evolver/rules/evolution-memory.mdc new file mode 100644 index 0000000..d368b58 --- /dev/null +++ b/evolver/rules/evolution-memory.mdc @@ -0,0 +1,25 @@ +--- +description: Use evolution memory on substantive tasks — recall relevant past outcomes before starting, and let outcomes be recorded at the end. +alwaysApply: false +--- + +# Evolution memory + +This workspace has the Evolver plugin installed. It maintains a persistent, +auditable memory of past task outcomes. + +For any **substantive** task (a feature, a non-trivial fix, a refactor): + +- **Before starting**, consult the evolution memory injected at session start. + If a recent *successful* outcome matches the task, reuse that approach. If a + recent *failure* matches, do not repeat it. +- **After finishing**, the outcome is recorded automatically by the stop hook. + If the task carried a clear, durable lesson, state it plainly in your final + message so it is captured. + +Skip this for trivial or purely conversational turns — evolution memory is for +work worth learning from, not ceremony on every reply. + +The `capability-evolver` skill and the `/evolve` command provide the full +workflow. Installing `@evomap/evolver` (`npm install -g @evomap/evolver`) +upgrades the local-only memory into the full review-and-solidify pipeline. diff --git a/evolver/skills/capability-evolver/SKILL.md b/evolver/skills/capability-evolver/SKILL.md new file mode 100644 index 0000000..2297da9 --- /dev/null +++ b/evolver/skills/capability-evolver/SKILL.md @@ -0,0 +1,72 @@ +--- +name: capability-evolver +description: Self-evolution workflow for the agent. Before a substantive task, recall what worked on similar past tasks from evolution memory; after it, record the outcome so future sessions learn from it. Use when the user starts non-trivial work (a feature, a fix, a refactor) or asks the agent to "evolve", "learn from this", or "remember how this went". +--- + +# Capability Evolver + +This plugin gives the agent a **persistent, auditable evolution memory** built on the +Genome Evolution Protocol (GEP). The goal is simple: stop re-solving the same +problem from scratch. Past outcomes — what worked, what failed — are carried +forward into future sessions. + +## How it works (automatic) + +Three hooks run on their own; you don't invoke them: + +- **`sessionStart`** — injects a short summary of recent **successful** outcomes + (filtered to score ≥ 0.5, < 7 days old, max 3) as context. The agent sees + "here's what worked recently" before it starts. +- **`afterFileEdit`** — scans edits for improvement signals (`log_error`, + `perf_bottleneck`, `capability_gap`, `test_failure`, …) and nudges the agent + to record the outcome when relevant. +- **`stop`** — at the end of a task, collects the git diff, classifies the + outcome, and appends it to the evolution memory graph. + +Memory is written to a local JSONL graph. With no extra setup it lands in +`~/.evolver/memory/evolution/memory_graph.jsonl`; inside an evolver-managed +project it lands under that project's `memory/evolution/`. + +## What you (the agent) should do + +For any **substantive** task — a feature, a non-trivial fix, a refactor: + +1. **Before starting**, check the injected evolution memory (it arrives as + session-start context). If a recent successful outcome matches the task, + reuse that approach. If a recent *failure* matches, avoid repeating it. +2. **Do the work.** +3. **After finishing**, the `stop` hook records the outcome automatically. You + don't need to call anything — but if the task had a clear lesson worth a + one-line note, say so in your final message so it's captured in the diff + context the hook reads. + +Trivial or purely conversational turns don't need this — skip it. + +## Signals + +The hooks classify work by signal. Knowing the vocabulary helps you describe +outcomes in terms the memory graph indexes well: + +| Signal | Fires on | +|---|---| +| `log_error` | errors, exceptions, failures in the diff | +| `perf_bottleneck` | timeout / slow / latency / OOM | +| `capability_gap` | "not supported" / "not implemented" | +| `user_feature_request` | adding a feature / new module | +| `test_failure` | failing tests / assertions | +| `deployment_issue` | build / CI / pipeline / rollback | + +## Full pipeline (optional) + +The bundled hooks record outcomes and recall them — that works on its own. To +unlock the **full evolution engine** (automated log analysis, the +review-and-solidify cycle that proposes and applies code improvements, and +EvoMap Hub sync for community strategies), install the engine: + +```bash +npm install -g @evomap/evolver +``` + +Once it's on `PATH`, the same hooks automatically find it and run the complete +pipeline instead of the local-only fallback. See the plugin README for +connecting an EvoMap Hub node. From 9c7bde46fdb308fd265803419e55a115bf5d76e9 Mon Sep 17 00:00:00 2001 From: autogame-17 Date: Tue, 2 Jun 2026 10:54:05 +0000 Subject: [PATCH 2/3] =?UTF-8?q?evolver:=20address=20Bugbot=20review=20?= =?UTF-8?q?=E2=80=94=205=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - session-end watchdog now records on timeout (was dropping the outcome) - session-start gates on stdin drain; workspace recall no longer leaks foreign workspaces when our id can't be resolved (cwd fallback) - findMemoryGraph resolves project-local graph before user-level - README/SKILL: hooks don't auto-invoke the engine; corrected claims + stale filenames Co-Authored-By: Claude Opus 4.8 --- evolver/README.md | 34 +++++---- evolver/hooks/_paths.js | 30 ++++++-- evolver/hooks/session-end.js | 33 ++++----- evolver/hooks/session-start.js | 83 +++++++++++++++++----- evolver/skills/capability-evolver/SKILL.md | 13 ++-- 5 files changed, 134 insertions(+), 59 deletions(-) diff --git a/evolver/README.md b/evolver/README.md index db3e713..9f1f81d 100644 --- a/evolver/README.md +++ b/evolver/README.md @@ -19,9 +19,9 @@ Three hooks run automatically — you don't invoke them: | Hook | Event | Effect | |---|---|---| -| `evolver-session-start.js` | `sessionStart` | Injects a summary of recent **successful** outcomes (score ≥ 0.5, < 7 days, max 3) as context. | -| `evolver-signal-detect.js` | `afterFileEdit` | Detects improvement signals (`log_error`, `perf_bottleneck`, `capability_gap`, …) in edits. | -| `evolver-session-end.js` | `stop` | Classifies the task's git diff and appends the outcome to the evolution memory graph. | +| `session-start.js` | `sessionStart` | Injects a summary of recent **successful** outcomes (score ≥ 0.5, < 7 days, max 3) as context. | +| `signal-detect.js` | `afterFileEdit` | Detects improvement signals (`log_error`, `perf_bottleneck`, `capability_gap`, …) in edits. | +| `session-end.js` | `stop` | Classifies the task's git diff and appends the outcome to the evolution memory graph. | It also ships: @@ -64,11 +64,14 @@ immediately. **No account, no key, no network.** npm install -g @evomap/evolver ``` -Once `@evomap/evolver` is on `PATH`, the same hooks automatically find it and -run the **full pipeline** — automated log analysis and the review-and-solidify -cycle that proposes and applies code improvements — instead of the local-only -fallback. Set `EVOLVER_ROOT` to point at a specific install if you have more -than one. +The bundled hooks always do lightweight **local** recall/record — local git +diff + JSONL append, plus optional Hub sync. Installing `@evomap/evolver` does +**not** change what the hooks do and they do not auto-detect or invoke it. +What it adds is the engine's **CLI** — e.g. `evolver run` (the full automated +review-and-solidify pipeline that analyzes logs and proposes/applies code +improvements) and `evolver review` — which you run separately. The memory the +hooks record feeds that pipeline, so the two compose without the hooks ever +shelling out to the engine. ### EvoMap Hub (community strategies) @@ -90,21 +93,22 @@ registration. EvoMap deliberately splits two products: - **`@evomap/evolver`** — the GPL-licensed, source-available evolution engine - (daemon + CLI). Its hook scripts are what this plugin bundles. + (daemon + CLI). This plugin does **not** bundle it; the plugin's own hooks are + an independent MIT clean-room implementation that records memory in the same + format the engine reads, so the two interoperate when you install it. - **`@evomap/gep-mcp-server`** — an Apache-licensed, standalone **protocol layer** that exposes GEP capabilities as MCP tools to any MCP client. -This plugin bundles the **evolver hooks** because they are exactly the -session-lifecycle glue Cursor needs, and they degrade gracefully when the -engine isn't installed. If you also want the `gep_*` MCP tools inside Cursor, -add `@evomap/gep-mcp-server` to your Cursor MCP config directly — it is not -re-bundled here to avoid duplicating that separately-maintained product. +This plugin ships its own lightweight session-lifecycle hooks (the glue Cursor +needs), which work standalone and degrade gracefully. If you also want the +`gep_*` MCP tools inside Cursor, add `@evomap/gep-mcp-server` to your Cursor MCP +config directly — it is not re-bundled here to avoid duplicating that +separately-maintained product. ## Environment variables | Variable | Default | Purpose | |---|---|---| -| `EVOLVER_ROOT` | (auto-detected) | Path to the `@evomap/evolver` install. | | `MEMORY_GRAPH_PATH` | (auto) | Override the memory graph file location. | | `EVOMAP_HUB_URL` / `EVOMAP_API_KEY` / `EVOMAP_NODE_ID` | (unset) | Enable Hub recording. | | `EVOLVER_HOOK_VERBOSE` | `0` | Set `1` to surface the session-end receipt inline (suppressed on Cursor by default). | diff --git a/evolver/hooks/_paths.js b/evolver/hooks/_paths.js index 04bb6c5..1d83819 100644 --- a/evolver/hooks/_paths.js +++ b/evolver/hooks/_paths.js @@ -81,15 +81,37 @@ function isGitWorkspace(dir) { } /** - * Return the path to the evolution memory graph (a JSONL file). Honours the - * MEMORY_GRAPH_PATH override; otherwise defaults to a well-known location under - * the home directory and best-effort creates the parent directory. + * Return the path to the evolution memory graph (a JSONL file). + * + * Resolution order: + * 1. MEMORY_GRAPH_PATH override, if set. + * 2. `/memory/evolution/memory_graph.jsonl` — but only if it + * already EXISTS (an evolver-managed project owns this file). We never + * create a project-local graph in an arbitrary folder, so plain projects + * fall through to the user-level path. + * 3. The user-level `~/.evolver/memory/evolution/memory_graph.jsonl`, whose + * parent directory is best-effort created. */ -function findMemoryGraph() { +function findMemoryGraph(projectDir) { const override = process.env.MEMORY_GRAPH_PATH; if (typeof override === 'string' && override.length > 0) { return override; } + if (looksLikeDir(projectDir)) { + const projectPath = path.join( + projectDir, + 'memory', + 'evolution', + 'memory_graph.jsonl' + ); + try { + if (fs.statSync(projectPath).isFile()) { + return projectPath; + } + } catch (_err) { + // Not present — fall through to the user-level default. + } + } const defaultPath = path.join( os.homedir(), '.evolver', diff --git a/evolver/hooks/session-end.js b/evolver/hooks/session-end.js index b62084a..c198f69 100644 --- a/evolver/hooks/session-end.js +++ b/evolver/hooks/session-end.js @@ -181,9 +181,9 @@ function recordToHub(payload) { * contract consumed by external tooling — keep it exact. Returns true on * success. */ -function recordToLocal(entry) { +function recordToLocal(entry, projectDir) { try { - const graphPath = findMemoryGraph(); + const graphPath = findMemoryGraph(projectDir); fs.mkdirSync(path.dirname(graphPath), { recursive: true }); fs.appendFileSync(graphPath, `${JSON.stringify(entry)}\n`); return true; @@ -230,15 +230,18 @@ function finish(projectDir, diff) { }); // Always also attempt a local record. - const localOk = recordToLocal({ - timestamp: new Date().toISOString(), - gene_id: 'ad_hoc', - signals, - outcome: { status, score, note: summary }, - cwd: projectDir, - workspace_id: resolveWorkspaceId(projectDir), - source: 'hook:session-end', - }); + const localOk = recordToLocal( + { + timestamp: new Date().toISOString(), + gene_id: 'ad_hoc', + signals, + outcome: { status, score, note: summary }, + cwd: projectDir, + workspace_id: resolveWorkspaceId(projectDir), + source: 'hook:session-end', + }, + projectDir + ); let destination; if (hubOk) { @@ -280,11 +283,9 @@ function finish(projectDir, diff) { }; const watchdog = setTimeout(() => { - // Stdin never closed in time — emit a bare object and bail. - if (!done) { - done = true; - emit({}); - } + // Stdin never closed in time — still do the work (proceed() is guarded + // by `done`, so it runs at most once whether the timeout or `end` fires). + proceed(); }, STDIN_WATCHDOG_MS); if (typeof watchdog.unref === 'function') { watchdog.unref(); diff --git a/evolver/hooks/session-start.js b/evolver/hooks/session-start.js index dea7b65..2cebcc1 100644 --- a/evolver/hooks/session-start.js +++ b/evolver/hooks/session-start.js @@ -32,11 +32,20 @@ const NONGIT_NOTICE = 'inactive (outcomes are derived from git diffs). Run `git init` here, or ' + 'open a git project, to enable recall and recording.'; +// The hook's own timeout is 3s; give stdin a slightly shorter window to drain. +const STDIN_WATCHDOG_MS = 2000; + +let alreadyEmitted = false; + /** - * Emit a JSON object and exit cleanly. Falls back to `{}` if serialization - * somehow fails. + * Emit a JSON object exactly once and exit cleanly. Falls back to `{}` if + * serialization somehow fails. */ function emit(obj) { + if (alreadyEmitted) { + return; + } + alreadyEmitted = true; let text = '{}'; try { text = JSON.stringify(obj); @@ -97,15 +106,25 @@ function throttled(key, ttlMs) { /** * Decide whether a memory entry belongs to the current workspace. - * - tagged with workspace_id: match iff we know our id and it's equal - * (if we don't know our id, don't exclude on this basis) - * - else tagged with cwd: match iff equal (likewise lenient when unknown) - * - untagged: always include + * - tagged with workspace_id, our id known: match iff equal. + * - tagged with workspace_id, our id UNKNOWN: do not blanket-include (that + * would leak other workspaces' entries from a shared graph). Fall back to + * cwd matching: in-scope iff the entry's cwd equals currentDir; if the + * entry has no cwd and currentDir is unknown too, then (and only then) + * don't exclude. + * - else tagged with cwd: match iff equal (lenient only when currentDir + * is unknown). + * - untagged (no workspace_id and no cwd): always include (legacy). */ function belongsToWorkspace(entry, currentId, currentDir) { if (entry && typeof entry.workspace_id === 'string' && entry.workspace_id) { if (currentId === null || currentId === undefined) { - return true; + // Cannot resolve our own id — fall back to cwd matching rather than + // surfacing a possibly-foreign workspace's entries. + if (typeof entry.cwd === 'string' && entry.cwd) { + return currentDir ? entry.cwd === currentDir : false; + } + return !currentDir; } return entry.workspace_id === currentId; } @@ -217,7 +236,7 @@ function main() { // 2. Workspace-scoped evolution memory. try { - const graphPath = findMemoryGraph(); + const graphPath = findMemoryGraph(currentDir); const currentId = resolveWorkspaceId(currentDir); const candidates = gatherWorkspaceEntries(graphPath, currentId, currentDir); const relevant = filterRelevant(candidates); @@ -238,13 +257,41 @@ function main() { } // This hook does not need stdin, but Cursor still pipes a JSON object. Drain it -// so the writer never gets EPIPE, then run. Guard everything. -try { - // Consume stdin without blocking; we don't actually use its contents. - process.stdin.on('data', () => {}); - process.stdin.on('error', () => {}); - process.stdin.resume(); - main(); -} catch (_err) { - emit({}); -} +// (so the writer never races a half-drained pipe / gets EPIPE) and only then +// run main(). A short watchdog guarantees we still run if stdin never closes. +// main() stays synchronous; we only gate when it is invoked. Guard everything. +(function run() { + try { + let started = false; + const start = () => { + if (started) { + return; + } + started = true; + try { + main(); + } catch (_err) { + emit({}); + } + }; + + const watchdog = setTimeout(start, STDIN_WATCHDOG_MS); + if (typeof watchdog.unref === 'function') { + watchdog.unref(); + } + + // Consume stdin without blocking; we don't actually use its contents. + process.stdin.on('data', () => {}); + process.stdin.on('end', () => { + clearTimeout(watchdog); + start(); + }); + process.stdin.on('error', () => { + clearTimeout(watchdog); + start(); + }); + process.stdin.resume(); + } catch (_err) { + emit({}); + } +})(); diff --git a/evolver/skills/capability-evolver/SKILL.md b/evolver/skills/capability-evolver/SKILL.md index 2297da9..eb2e2f4 100644 --- a/evolver/skills/capability-evolver/SKILL.md +++ b/evolver/skills/capability-evolver/SKILL.md @@ -59,14 +59,15 @@ outcomes in terms the memory graph indexes well: ## Full pipeline (optional) The bundled hooks record outcomes and recall them — that works on its own. To -unlock the **full evolution engine** (automated log analysis, the -review-and-solidify cycle that proposes and applies code improvements, and -EvoMap Hub sync for community strategies), install the engine: +get the **full evolution engine** (automated log analysis, the +review-and-solidify cycle that proposes and applies code improvements), install +it: ```bash npm install -g @evomap/evolver ``` -Once it's on `PATH`, the same hooks automatically find it and run the complete -pipeline instead of the local-only fallback. See the plugin README for -connecting an EvoMap Hub node. +This gives you the engine's CLI (e.g. `evolver run`) to run that pipeline +separately — the hooks do not auto-detect or invoke it. The memory the hooks +record is what the pipeline consumes. See the plugin README for connecting an +EvoMap Hub node for community strategies. From abae3d44752d066e163c935b9ebe652680d45855 Mon Sep 17 00:00:00 2001 From: autogame-17 Date: Tue, 2 Jun 2026 14:49:29 +0000 Subject: [PATCH 3/3] evolver: use EvoMap org avatar (PNG) as logo Co-Authored-By: Claude Opus 4.8 --- evolver/.cursor-plugin/plugin.json | 2 +- evolver/assets/logo.png | Bin 0 -> 11469 bytes evolver/assets/logo.svg | 13 ------------- 3 files changed, 1 insertion(+), 14 deletions(-) create mode 100644 evolver/assets/logo.png delete mode 100644 evolver/assets/logo.svg diff --git a/evolver/.cursor-plugin/plugin.json b/evolver/.cursor-plugin/plugin.json index 6e2332f..1a1c88a 100644 --- a/evolver/.cursor-plugin/plugin.json +++ b/evolver/.cursor-plugin/plugin.json @@ -11,7 +11,7 @@ "homepage": "https://evomap.ai", "repository": "https://github.com/EvoMap/evolver-cursor-plugin", "license": "MIT", - "logo": "assets/logo.svg", + "logo": "assets/logo.png", "keywords": [ "evolution", "self-improvement", diff --git a/evolver/assets/logo.png b/evolver/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..783ff807bdaf2f3c8b14dc0c31c8655d40369c5a GIT binary patch literal 11469 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 C*x~a4 literal 0 HcmV?d00001 diff --git a/evolver/assets/logo.svg b/evolver/assets/logo.svg deleted file mode 100644 index 0d630b7..0000000 --- a/evolver/assets/logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - -