Skip to content

Commit 2678ba5

Browse files
autogame-17claude
andcommitted
fix: address Bugbot review (cursor/plugins#110) — 5 findings
- session-end: watchdog timeout now runs proceed() instead of emitting {} bare, so a session outcome is no longer silently dropped when stdin stays open past the timeout (was a real data-loss path). - session-start: gate main() on stdin end/2s-watchdog with an emit-once guard, consistent with the other hooks; no behavior change but no pipe race. - session-start: belongsToWorkspace no longer blanket-includes foreign workspace_id-tagged entries when our own id can't be resolved — falls back to cwd matching, so a shared user-level graph can't leak another project's recall. - _paths: findMemoryGraph(projectDir) now resolves a project-local <projectDir>/memory/evolution/memory_graph.jsonl when it exists (matching the docs), before the user-level fallback; both hooks pass their projectDir. - README/SKILL: corrected the claim that hooks auto-detect/invoke the installed engine — they don't; @evomap/evolver gives you the CLI (evolver run/review) to run the pipeline separately, fed by the memory the hooks record. Also fixed stale hook filenames in the README table and the bundling description. Verified: node --check all hooks; watchdog path now records; project-local path resolution + user-level fallback; official validate-plugins.mjs passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 89e18a6 commit 2678ba5

5 files changed

Lines changed: 134 additions & 59 deletions

File tree

README.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ Three hooks run automatically — you don't invoke them:
1919

2020
| Hook | Event | Effect |
2121
|---|---|---|
22-
| `evolver-session-start.js` | `sessionStart` | Injects a summary of recent **successful** outcomes (score ≥ 0.5, < 7 days, max 3) as context. |
23-
| `evolver-signal-detect.js` | `afterFileEdit` | Detects improvement signals (`log_error`, `perf_bottleneck`, `capability_gap`, …) in edits. |
24-
| `evolver-session-end.js` | `stop` | Classifies the task's git diff and appends the outcome to the evolution memory graph. |
22+
| `session-start.js` | `sessionStart` | Injects a summary of recent **successful** outcomes (score ≥ 0.5, < 7 days, max 3) as context. |
23+
| `signal-detect.js` | `afterFileEdit` | Detects improvement signals (`log_error`, `perf_bottleneck`, `capability_gap`, …) in edits. |
24+
| `session-end.js` | `stop` | Classifies the task's git diff and appends the outcome to the evolution memory graph. |
2525

2626
It also ships:
2727

@@ -64,11 +64,14 @@ immediately. **No account, no key, no network.**
6464
npm install -g @evomap/evolver
6565
```
6666

67-
Once `@evomap/evolver` is on `PATH`, the same hooks automatically find it and
68-
run the **full pipeline** — automated log analysis and the review-and-solidify
69-
cycle that proposes and applies code improvements — instead of the local-only
70-
fallback. Set `EVOLVER_ROOT` to point at a specific install if you have more
71-
than one.
67+
The bundled hooks always do lightweight **local** recall/record — local git
68+
diff + JSONL append, plus optional Hub sync. Installing `@evomap/evolver` does
69+
**not** change what the hooks do and they do not auto-detect or invoke it.
70+
What it adds is the engine's **CLI** — e.g. `evolver run` (the full automated
71+
review-and-solidify pipeline that analyzes logs and proposes/applies code
72+
improvements) and `evolver review` — which you run separately. The memory the
73+
hooks record feeds that pipeline, so the two compose without the hooks ever
74+
shelling out to the engine.
7275

7376
### EvoMap Hub (community strategies)
7477

@@ -90,21 +93,22 @@ registration.
9093
EvoMap deliberately splits two products:
9194

9295
- **`@evomap/evolver`** — the GPL-licensed, source-available evolution engine
93-
(daemon + CLI). Its hook scripts are what this plugin bundles.
96+
(daemon + CLI). This plugin does **not** bundle it; the plugin's own hooks are
97+
an independent MIT clean-room implementation that records memory in the same
98+
format the engine reads, so the two interoperate when you install it.
9499
- **`@evomap/gep-mcp-server`** — an Apache-licensed, standalone **protocol
95100
layer** that exposes GEP capabilities as MCP tools to any MCP client.
96101

97-
This plugin bundles the **evolver hooks** because they are exactly the
98-
session-lifecycle glue Cursor needs, and they degrade gracefully when the
99-
engine isn't installed. If you also want the `gep_*` MCP tools inside Cursor,
100-
add `@evomap/gep-mcp-server` to your Cursor MCP config directly — it is not
101-
re-bundled here to avoid duplicating that separately-maintained product.
102+
This plugin ships its own lightweight session-lifecycle hooks (the glue Cursor
103+
needs), which work standalone and degrade gracefully. If you also want the
104+
`gep_*` MCP tools inside Cursor, add `@evomap/gep-mcp-server` to your Cursor MCP
105+
config directly — it is not re-bundled here to avoid duplicating that
106+
separately-maintained product.
102107

103108
## Environment variables
104109

105110
| Variable | Default | Purpose |
106111
|---|---|---|
107-
| `EVOLVER_ROOT` | (auto-detected) | Path to the `@evomap/evolver` install. |
108112
| `MEMORY_GRAPH_PATH` | (auto) | Override the memory graph file location. |
109113
| `EVOMAP_HUB_URL` / `EVOMAP_API_KEY` / `EVOMAP_NODE_ID` | (unset) | Enable Hub recording. |
110114
| `EVOLVER_HOOK_VERBOSE` | `0` | Set `1` to surface the session-end receipt inline (suppressed on Cursor by default). |

hooks/_paths.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,37 @@ function isGitWorkspace(dir) {
8181
}
8282

8383
/**
84-
* Return the path to the evolution memory graph (a JSONL file). Honours the
85-
* MEMORY_GRAPH_PATH override; otherwise defaults to a well-known location under
86-
* the home directory and best-effort creates the parent directory.
84+
* Return the path to the evolution memory graph (a JSONL file).
85+
*
86+
* Resolution order:
87+
* 1. MEMORY_GRAPH_PATH override, if set.
88+
* 2. `<projectDir>/memory/evolution/memory_graph.jsonl` — but only if it
89+
* already EXISTS (an evolver-managed project owns this file). We never
90+
* create a project-local graph in an arbitrary folder, so plain projects
91+
* fall through to the user-level path.
92+
* 3. The user-level `~/.evolver/memory/evolution/memory_graph.jsonl`, whose
93+
* parent directory is best-effort created.
8794
*/
88-
function findMemoryGraph() {
95+
function findMemoryGraph(projectDir) {
8996
const override = process.env.MEMORY_GRAPH_PATH;
9097
if (typeof override === 'string' && override.length > 0) {
9198
return override;
9299
}
100+
if (looksLikeDir(projectDir)) {
101+
const projectPath = path.join(
102+
projectDir,
103+
'memory',
104+
'evolution',
105+
'memory_graph.jsonl'
106+
);
107+
try {
108+
if (fs.statSync(projectPath).isFile()) {
109+
return projectPath;
110+
}
111+
} catch (_err) {
112+
// Not present — fall through to the user-level default.
113+
}
114+
}
93115
const defaultPath = path.join(
94116
os.homedir(),
95117
'.evolver',

hooks/session-end.js

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ function recordToHub(payload) {
181181
* contract consumed by external tooling — keep it exact. Returns true on
182182
* success.
183183
*/
184-
function recordToLocal(entry) {
184+
function recordToLocal(entry, projectDir) {
185185
try {
186-
const graphPath = findMemoryGraph();
186+
const graphPath = findMemoryGraph(projectDir);
187187
fs.mkdirSync(path.dirname(graphPath), { recursive: true });
188188
fs.appendFileSync(graphPath, `${JSON.stringify(entry)}\n`);
189189
return true;
@@ -230,15 +230,18 @@ function finish(projectDir, diff) {
230230
});
231231

232232
// Always also attempt a local record.
233-
const localOk = recordToLocal({
234-
timestamp: new Date().toISOString(),
235-
gene_id: 'ad_hoc',
236-
signals,
237-
outcome: { status, score, note: summary },
238-
cwd: projectDir,
239-
workspace_id: resolveWorkspaceId(projectDir),
240-
source: 'hook:session-end',
241-
});
233+
const localOk = recordToLocal(
234+
{
235+
timestamp: new Date().toISOString(),
236+
gene_id: 'ad_hoc',
237+
signals,
238+
outcome: { status, score, note: summary },
239+
cwd: projectDir,
240+
workspace_id: resolveWorkspaceId(projectDir),
241+
source: 'hook:session-end',
242+
},
243+
projectDir
244+
);
242245

243246
let destination;
244247
if (hubOk) {
@@ -280,11 +283,9 @@ function finish(projectDir, diff) {
280283
};
281284

282285
const watchdog = setTimeout(() => {
283-
// Stdin never closed in time — emit a bare object and bail.
284-
if (!done) {
285-
done = true;
286-
emit({});
287-
}
286+
// Stdin never closed in time — still do the work (proceed() is guarded
287+
// by `done`, so it runs at most once whether the timeout or `end` fires).
288+
proceed();
288289
}, STDIN_WATCHDOG_MS);
289290
if (typeof watchdog.unref === 'function') {
290291
watchdog.unref();

hooks/session-start.js

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,20 @@ const NONGIT_NOTICE =
3232
'inactive (outcomes are derived from git diffs). Run `git init` here, or ' +
3333
'open a git project, to enable recall and recording.';
3434

35+
// The hook's own timeout is 3s; give stdin a slightly shorter window to drain.
36+
const STDIN_WATCHDOG_MS = 2000;
37+
38+
let alreadyEmitted = false;
39+
3540
/**
36-
* Emit a JSON object and exit cleanly. Falls back to `{}` if serialization
37-
* somehow fails.
41+
* Emit a JSON object exactly once and exit cleanly. Falls back to `{}` if
42+
* serialization somehow fails.
3843
*/
3944
function emit(obj) {
45+
if (alreadyEmitted) {
46+
return;
47+
}
48+
alreadyEmitted = true;
4049
let text = '{}';
4150
try {
4251
text = JSON.stringify(obj);
@@ -97,15 +106,25 @@ function throttled(key, ttlMs) {
97106

98107
/**
99108
* Decide whether a memory entry belongs to the current workspace.
100-
* - tagged with workspace_id: match iff we know our id and it's equal
101-
* (if we don't know our id, don't exclude on this basis)
102-
* - else tagged with cwd: match iff equal (likewise lenient when unknown)
103-
* - untagged: always include
109+
* - tagged with workspace_id, our id known: match iff equal.
110+
* - tagged with workspace_id, our id UNKNOWN: do not blanket-include (that
111+
* would leak other workspaces' entries from a shared graph). Fall back to
112+
* cwd matching: in-scope iff the entry's cwd equals currentDir; if the
113+
* entry has no cwd and currentDir is unknown too, then (and only then)
114+
* don't exclude.
115+
* - else tagged with cwd: match iff equal (lenient only when currentDir
116+
* is unknown).
117+
* - untagged (no workspace_id and no cwd): always include (legacy).
104118
*/
105119
function belongsToWorkspace(entry, currentId, currentDir) {
106120
if (entry && typeof entry.workspace_id === 'string' && entry.workspace_id) {
107121
if (currentId === null || currentId === undefined) {
108-
return true;
122+
// Cannot resolve our own id — fall back to cwd matching rather than
123+
// surfacing a possibly-foreign workspace's entries.
124+
if (typeof entry.cwd === 'string' && entry.cwd) {
125+
return currentDir ? entry.cwd === currentDir : false;
126+
}
127+
return !currentDir;
109128
}
110129
return entry.workspace_id === currentId;
111130
}
@@ -217,7 +236,7 @@ function main() {
217236

218237
// 2. Workspace-scoped evolution memory.
219238
try {
220-
const graphPath = findMemoryGraph();
239+
const graphPath = findMemoryGraph(currentDir);
221240
const currentId = resolveWorkspaceId(currentDir);
222241
const candidates = gatherWorkspaceEntries(graphPath, currentId, currentDir);
223242
const relevant = filterRelevant(candidates);
@@ -238,13 +257,41 @@ function main() {
238257
}
239258

240259
// This hook does not need stdin, but Cursor still pipes a JSON object. Drain it
241-
// so the writer never gets EPIPE, then run. Guard everything.
242-
try {
243-
// Consume stdin without blocking; we don't actually use its contents.
244-
process.stdin.on('data', () => {});
245-
process.stdin.on('error', () => {});
246-
process.stdin.resume();
247-
main();
248-
} catch (_err) {
249-
emit({});
250-
}
260+
// (so the writer never races a half-drained pipe / gets EPIPE) and only then
261+
// run main(). A short watchdog guarantees we still run if stdin never closes.
262+
// main() stays synchronous; we only gate when it is invoked. Guard everything.
263+
(function run() {
264+
try {
265+
let started = false;
266+
const start = () => {
267+
if (started) {
268+
return;
269+
}
270+
started = true;
271+
try {
272+
main();
273+
} catch (_err) {
274+
emit({});
275+
}
276+
};
277+
278+
const watchdog = setTimeout(start, STDIN_WATCHDOG_MS);
279+
if (typeof watchdog.unref === 'function') {
280+
watchdog.unref();
281+
}
282+
283+
// Consume stdin without blocking; we don't actually use its contents.
284+
process.stdin.on('data', () => {});
285+
process.stdin.on('end', () => {
286+
clearTimeout(watchdog);
287+
start();
288+
});
289+
process.stdin.on('error', () => {
290+
clearTimeout(watchdog);
291+
start();
292+
});
293+
process.stdin.resume();
294+
} catch (_err) {
295+
emit({});
296+
}
297+
})();

skills/capability-evolver/SKILL.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,15 @@ outcomes in terms the memory graph indexes well:
5959
## Full pipeline (optional)
6060

6161
The bundled hooks record outcomes and recall them — that works on its own. To
62-
unlock the **full evolution engine** (automated log analysis, the
63-
review-and-solidify cycle that proposes and applies code improvements, and
64-
EvoMap Hub sync for community strategies), install the engine:
62+
get the **full evolution engine** (automated log analysis, the
63+
review-and-solidify cycle that proposes and applies code improvements), install
64+
it:
6565

6666
```bash
6767
npm install -g @evomap/evolver
6868
```
6969

70-
Once it's on `PATH`, the same hooks automatically find it and run the complete
71-
pipeline instead of the local-only fallback. See the plugin README for
72-
connecting an EvoMap Hub node.
70+
This gives you the engine's CLI (e.g. `evolver run`) to run that pipeline
71+
separately — the hooks do not auto-detect or invoke it. The memory the hooks
72+
record is what the pipeline consumes. See the plugin README for connecting an
73+
EvoMap Hub node for community strategies.

0 commit comments

Comments
 (0)