Skip to content

Commit dd09921

Browse files
autogame-17claude
andcommitted
sync hooks with evolver #558 (non-git workspace notice)
Brings in the session-start non-git notice + shared isGitWorkspace() that landed in @evomap/evolver after the previous sync. The plugin's bundled hook scripts now match evolver main (and the published 1.87.4 npm package). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f084c27 commit dd09921

2 files changed

Lines changed: 83 additions & 40 deletions

File tree

hooks/_runtimePaths.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
const fs = require('fs');
2020
const path = require('path');
2121
const os = require('os');
22+
const { spawnSync } = require('child_process');
2223

2324
function isEvolverPackageJson(filePath) {
2425
try {
@@ -267,4 +268,24 @@ function findMemoryGraph(evolverRoot) {
267268
return path.join(userDir, 'memory_graph.jsonl');
268269
}
269270

270-
module.exports = { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId };
271+
// Is `dir` inside a git work tree? Cheap, no-shell `git rev-parse`. Returns
272+
// false on any error (git missing, not a repo, timeout) and never throws — the
273+
// session-start hook uses this only to decide whether to surface a one-line
274+
// "evolver needs a git workspace" notice, so a false negative just suppresses
275+
// the notice rather than breaking anything.
276+
function isGitWorkspace(dir) {
277+
try {
278+
const res = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
279+
cwd: dir,
280+
encoding: 'utf8',
281+
timeout: 5000,
282+
stdio: ['ignore', 'pipe', 'pipe'],
283+
shell: false,
284+
});
285+
return res.status === 0 && typeof res.stdout === 'string' && res.stdout.trim() === 'true';
286+
} catch {
287+
return false;
288+
}
289+
}
290+
291+
module.exports = { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace };

hooks/evolver-session-start.js

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@ const fs = require('fs');
77
const path = require('path');
88
const os = require('os');
99

10-
const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId } = require('./_runtimePaths');
10+
const { findEvolverRoot, findMemoryGraph, resolveProjectDir, resolveWorkspaceId, isGitWorkspace } = require('./_runtimePaths');
1111
const { filterRelevantOutcomes } = require('./_memoryFiltering');
1212

13+
// One-line notice shown (throttled) when the workspace is not a git repo.
14+
// Evolver derives every outcome from the git diff, so in a non-git folder the
15+
// session-end hook records nothing — silently, unless we say so here. We surface
16+
// it in session-start's additionalContext (injected as opening context, which
17+
// does NOT trigger an extra inference round, unlike a stop-hook systemMessage).
18+
const NON_GIT_NOTICE =
19+
'[Evolver] This folder is not a git repository, so evolution memory is inactive ' +
20+
'(outcomes are derived from git diffs). Run `git init` here, or open a git project, ' +
21+
'to enable recall and recording.';
22+
const NON_GIT_NOTICE_TTL_MS = 30 * 60 * 1000; // once per 30 min per folder
23+
1324
// Return up to `n` of the current workspace's most-recent entries, in
1425
// chronological (oldest-first) order.
1526
//
@@ -94,18 +105,14 @@ function getDedupStatePath() {
94105
return path.join(dir, 'session-start-state.json');
95106
}
96107

97-
function shouldSkipInjection() {
98-
// Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
99-
// we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
100-
// stdin. The stdin is drained in main(), so we rely on env flag here.
101-
const dedupEnabled = String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === '1'
102-
|| String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === 'true';
103-
if (!dedupEnabled) return false;
104-
105-
const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
106-
const key = process.cwd();
108+
// TTL throttle keyed by an arbitrary string, persisted in session-start-state
109+
// .json. Returns true if `key` fired within the last `ttlMs` (caller should
110+
// suppress); otherwise records "now" for `key` and returns false. Best-effort:
111+
// a state read/write failure just means no throttling (fail open). Shared by
112+
// the Kiro per-prompt dedup and the non-git notice so both age out of the same
113+
// file (entries older than 24h are pruned on write).
114+
function throttled(key, ttlMs) {
107115
const statePath = getDedupStatePath();
108-
109116
let state = {};
110117
try {
111118
if (fs.existsSync(statePath)) {
@@ -115,9 +122,7 @@ function shouldSkipInjection() {
115122

116123
const now = Date.now();
117124
const last = state[key];
118-
if (typeof last === 'number' && now - last < ttlMs) {
119-
return true;
120-
}
125+
if (typeof last === 'number' && now - last < ttlMs) return true;
121126

122127
state[key] = now;
123128
try {
@@ -130,53 +135,70 @@ function shouldSkipInjection() {
130135
fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
131136
fs.renameSync(tmp, statePath);
132137
} catch { /* best-effort */ }
133-
134138
return false;
135139
}
136140

141+
function shouldSkipInjection() {
142+
// Only apply dedup when explicitly enabled (set by Kiro adapter) OR when
143+
// we detect a per-prompt-firing platform via PROMPT_SUBMIT heuristic in
144+
// stdin. The stdin is drained in main(), so we rely on env flag here.
145+
const dedupEnabled = String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === '1'
146+
|| String(process.env.EVOLVER_SESSION_START_DEDUP || '').toLowerCase() === 'true';
147+
if (!dedupEnabled) return false;
148+
149+
const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
150+
return throttled(process.cwd(), ttlMs);
151+
}
152+
137153
function main() {
138154
if (shouldSkipInjection()) {
139155
process.stdout.write(JSON.stringify({}));
140156
return;
141157
}
142158

143-
const evolverRoot = findEvolverRoot();
144-
const graphPath = findMemoryGraph(evolverRoot);
159+
const currentDir = resolveProjectDir();
145160

146-
if (!graphPath) {
147-
process.stdout.write(JSON.stringify({}));
148-
return;
161+
// Non-git notice: evolver records nothing in a non-git folder (outcomes come
162+
// from git diffs), so tell the user — once per folder per TTL — instead of
163+
// failing silently. Emitted regardless of whether any memory exists below.
164+
const parts = [];
165+
if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS)) {
166+
parts.push(NON_GIT_NOTICE);
149167
}
150168

169+
const evolverRoot = findEvolverRoot();
170+
const graphPath = findMemoryGraph(evolverRoot);
171+
151172
// Scope to the current workspace BEFORE trimming to the most-recent window,
152173
// so other projects sharing the user-level fallback graph can't crowd this
153174
// workspace's outcomes out of view. When the workspace id can't be resolved,
154175
// belongsToWorkspace() falls back to "show it" — no regression vs. the old
155176
// unscoped behavior.
156-
const currentDir = resolveProjectDir();
157-
const currentId = resolveWorkspaceId(evolverRoot, currentDir);
158-
const recent = readRecentWorkspaceEntries(graphPath, currentId, currentDir, 5);
159-
const filtered = filterRelevantOutcomes(recent);
177+
if (graphPath) {
178+
const currentId = resolveWorkspaceId(evolverRoot, currentDir);
179+
const recent = readRecentWorkspaceEntries(graphPath, currentId, currentDir, 5);
180+
const filtered = filterRelevantOutcomes(recent);
181+
if (filtered.length > 0) {
182+
const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
183+
const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
184+
parts.push([
185+
`[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
186+
...filtered.map(formatOutcome),
187+
'',
188+
'Use successful approaches. Avoid repeating failed patterns.',
189+
].join('\n'));
190+
}
191+
}
160192

161-
if (filtered.length === 0) {
193+
if (parts.length === 0) {
162194
process.stdout.write(JSON.stringify({}));
163195
return;
164196
}
165197

166-
const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
167-
const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
168-
169-
const lines = filtered.map(formatOutcome);
170-
const summary = [
171-
`[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
172-
...lines,
173-
'',
174-
'Use successful approaches. Avoid repeating failed patterns.',
175-
].join('\n');
176-
198+
const out = parts.join('\n\n');
177199
process.stdout.write(JSON.stringify({
178-
agent_message: summary,
179-
additionalContext: summary,
200+
agent_message: out,
201+
additionalContext: out,
180202
}));
181203
}
182204

0 commit comments

Comments
 (0)