@evomap/evolver 1.85.3 → 1.86.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/index.js +70 -64
  2. package/package.json +4 -1
  3. package/scripts/check-changelog.js +166 -0
  4. package/src/adapters/hookAdapter.js +4 -4
  5. package/src/adapters/scripts/_memoryFiltering.js +35 -0
  6. package/src/adapters/scripts/evolver-session-start.js +59 -6
  7. package/src/adapters/scripts/evolver-signal-detect.js +52 -1
  8. package/src/evolve/guards.js +1 -1
  9. package/src/evolve/pipeline/collect.js +1 -1
  10. package/src/evolve/pipeline/dispatch.js +1 -1
  11. package/src/evolve/pipeline/enrich.js +1 -1
  12. package/src/evolve/pipeline/hub.js +1 -1
  13. package/src/evolve/pipeline/select.js +1 -1
  14. package/src/evolve/pipeline/signals.js +1 -1
  15. package/src/evolve/utils.js +1 -1
  16. package/src/evolve.js +1 -1
  17. package/src/gep/a2aProtocol.js +1 -1
  18. package/src/gep/candidateEval.js +1 -1
  19. package/src/gep/candidates.js +1 -1
  20. package/src/gep/contentHash.js +1 -1
  21. package/src/gep/crypto.js +1 -1
  22. package/src/gep/curriculum.js +1 -1
  23. package/src/gep/deviceId.js +1 -1
  24. package/src/gep/envFingerprint.js +1 -1
  25. package/src/gep/epigenetics.js +1 -1
  26. package/src/gep/explore.js +1 -1
  27. package/src/gep/hash.js +1 -1
  28. package/src/gep/hubFetch.js +1 -1
  29. package/src/gep/hubReview.js +1 -1
  30. package/src/gep/hubSearch.js +1 -1
  31. package/src/gep/hubVerify.js +1 -1
  32. package/src/gep/learningSignals.js +1 -1
  33. package/src/gep/memoryGraph.js +1 -1
  34. package/src/gep/memoryGraphAdapter.js +1 -1
  35. package/src/gep/mutation.js +1 -1
  36. package/src/gep/narrativeMemory.js +1 -1
  37. package/src/gep/openPRRegistry.js +1 -1
  38. package/src/gep/paths.js +124 -31
  39. package/src/gep/personality.js +1 -1
  40. package/src/gep/policyCheck.js +1 -1
  41. package/src/gep/prompt.js +1 -1
  42. package/src/gep/recallVerifier.js +1 -1
  43. package/src/gep/reflection.js +1 -1
  44. package/src/gep/selector.js +1 -1
  45. package/src/gep/skillDistiller.js +1 -1
  46. package/src/gep/solidify.js +1 -1
  47. package/src/gep/strategy.js +1 -1
  48. package/src/gep/workspaceKeychain.js +1 -0
  49. package/skills/_meta/SKILL.md +0 -41
  50. package/skills/index.json +0 -14
package/index.js CHANGED
@@ -39,6 +39,74 @@ try {
39
39
  else process.env.EVOLVER_QUIET_PARENT_GIT = _prevQuiet;
40
40
  } catch (e) { /* dotenv is optional */ }
41
41
 
42
+ async function runSetupHooksCli(args) {
43
+ const hookAdapter = require('./src/adapters/hookAdapter');
44
+ const { setupHooks, resolveConfigRoot, detectPlatform, loadAdapter } = hookAdapter;
45
+
46
+ const platformFlag = args.find(a => typeof a === 'string' && a.startsWith('--platform='));
47
+ const platform = platformFlag ? platformFlag.slice('--platform='.length) : undefined;
48
+ const force = args.includes('--force');
49
+ const uninstall = args.includes('--uninstall');
50
+ const verifyOnly = args.includes('--verify');
51
+
52
+ if (verifyOnly) {
53
+ try {
54
+ const platformId = platform || detectPlatform(process.cwd());
55
+ if (!platformId) {
56
+ console.error('[setup-hooks] --verify: could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro');
57
+ process.exit(2);
58
+ }
59
+ const adapter = loadAdapter(platformId);
60
+ if (!adapter || typeof adapter.verify !== 'function') {
61
+ console.error('[setup-hooks] --verify: platform ' + platformId + ' does not support verification yet.');
62
+ process.exit(2);
63
+ }
64
+ const configRoot = resolveConfigRoot(platformId, process.cwd());
65
+ const report = adapter.verify({ configRoot });
66
+ if (typeof adapter.printVerifyReport === 'function') {
67
+ adapter.printVerifyReport(report);
68
+ } else {
69
+ console.log(JSON.stringify(report, null, 2));
70
+ }
71
+ process.exit(report.ok ? 0 : 1);
72
+ } catch (verifyErr) {
73
+ console.error('[setup-hooks] --verify error:', verifyErr && verifyErr.message || verifyErr);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ try {
79
+ const result = await setupHooks({
80
+ platform,
81
+ cwd: process.cwd(),
82
+ force,
83
+ uninstall,
84
+ evolverRoot: __dirname,
85
+ });
86
+ if (result && result.ok) {
87
+ if (!uninstall && result.files) {
88
+ console.log('\n[setup-hooks] Files created/updated:');
89
+ for (const f of result.files) {
90
+ console.log(' ' + f);
91
+ }
92
+ }
93
+ process.exit(0);
94
+ } else {
95
+ console.error('[setup-hooks] Failed: ' + (result && result.error || 'unknown'));
96
+ process.exit(1);
97
+ }
98
+ } catch (error) {
99
+ console.error('[setup-hooks] Error:', error && error.message || error);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ if (require.main === module && process.argv[2] === 'setup-hooks') {
105
+ runSetupHooksCli(process.argv.slice(3)).catch(function (err) {
106
+ console.error('[setup-hooks] Error:', err && err.stack ? err.stack : String(err));
107
+ process.exitCode = 1;
108
+ });
109
+ } else {
42
110
  const evolve = require('./src/evolve');
43
111
  const { solidify } = require('./src/gep/solidify');
44
112
  const path = require('path');
@@ -1660,70 +1728,6 @@ async function main() {
1660
1728
  process.exit(1);
1661
1729
  }
1662
1730
 
1663
- } else if (command === 'setup-hooks') {
1664
- const hookAdapter = require('./src/adapters/hookAdapter');
1665
- const { setupHooks, resolveConfigRoot, detectPlatform, loadAdapter } = hookAdapter;
1666
-
1667
- const platformFlag = args.find(a => typeof a === 'string' && a.startsWith('--platform='));
1668
- const platform = platformFlag ? platformFlag.slice('--platform='.length) : undefined;
1669
- const force = args.includes('--force');
1670
- const uninstall = args.includes('--uninstall');
1671
- const verifyOnly = args.includes('--verify');
1672
-
1673
- if (verifyOnly) {
1674
- // Read-only verification: do not touch any files, just report whether
1675
- // the previously-installed hooks/plugin look healthy. Lets users answer
1676
- // "is the plugin actually loaded?" without grepping opencode logs.
1677
- try {
1678
- const platformId = platform || detectPlatform(process.cwd());
1679
- if (!platformId) {
1680
- console.error('[setup-hooks] --verify: could not detect platform. Pass --platform=opencode|cursor|claude-code|codex|kiro');
1681
- process.exit(2);
1682
- }
1683
- const adapter = loadAdapter(platformId);
1684
- if (!adapter || typeof adapter.verify !== 'function') {
1685
- console.error('[setup-hooks] --verify: platform ' + platformId + ' does not support verification yet.');
1686
- process.exit(2);
1687
- }
1688
- const configRoot = resolveConfigRoot(platformId, process.cwd());
1689
- const report = adapter.verify({ configRoot });
1690
- if (typeof adapter.printVerifyReport === 'function') {
1691
- adapter.printVerifyReport(report);
1692
- } else {
1693
- console.log(JSON.stringify(report, null, 2));
1694
- }
1695
- process.exit(report.ok ? 0 : 1);
1696
- } catch (verifyErr) {
1697
- console.error('[setup-hooks] --verify error:', verifyErr && verifyErr.message || verifyErr);
1698
- process.exit(1);
1699
- }
1700
- }
1701
-
1702
- try {
1703
- const result = await setupHooks({
1704
- platform,
1705
- cwd: process.cwd(),
1706
- force,
1707
- uninstall,
1708
- evolverRoot: __dirname,
1709
- });
1710
- if (result && result.ok) {
1711
- if (!uninstall && result.files) {
1712
- console.log('\n[setup-hooks] Files created/updated:');
1713
- for (const f of result.files) {
1714
- console.log(' ' + f);
1715
- }
1716
- }
1717
- process.exit(0);
1718
- } else {
1719
- console.error('[setup-hooks] Failed: ' + (result && result.error || 'unknown'));
1720
- process.exit(1);
1721
- }
1722
- } catch (error) {
1723
- console.error('[setup-hooks] Error:', error && error.message || error);
1724
- process.exit(1);
1725
- }
1726
-
1727
1731
  } else if (command === 'reset-local-secret') {
1728
1732
  // Wipe every local store of node_secret in one shot, so a daemon stuck
1729
1733
  // after a manual web reset (https://evomap.ai/account -> Reset Secret)
@@ -1923,6 +1927,7 @@ if (require.main === module) {
1923
1927
 
1924
1928
  module.exports = {
1925
1929
  main,
1930
+ runSetupHooksCli,
1926
1931
  readJsonSafe,
1927
1932
  rejectPendingRun,
1928
1933
  isPendingSolidify,
@@ -1931,3 +1936,4 @@ module.exports = {
1931
1936
  writeCycleProgressAtomic,
1932
1937
  spawnReplacementProcess,
1933
1938
  };
1939
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.85.3",
3
+ "version": "1.86.1",
4
4
  "description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -43,6 +43,9 @@
43
43
  "devDependencies": {
44
44
  "javascript-obfuscator": "^5.4.1"
45
45
  },
46
+ "optionalDependencies": {
47
+ "@napi-rs/keyring": "^1.1.6"
48
+ },
46
49
  "files": [
47
50
  "assets/",
48
51
  "index.js",
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CHANGELOG release-section integrity guard.
5
+ *
6
+ * Catches the misattribution pattern that bit us with #540 / PR #107:
7
+ * an entry filed under `## [X.Y.Z]` AFTER v1.85.0 was already published
8
+ * to npm, so the changelog claimed a fix the binary didn't contain.
9
+ *
10
+ * Algorithm: for every `## [X.Y.Z]` heading in CHANGELOG.md that has a
11
+ * matching git tag (`vX.Y.Z`), compare the section content at HEAD
12
+ * against the section content at that tag. If they differ, somebody
13
+ * edited a frozen-and-released section — fail loud.
14
+ *
15
+ * Notes:
16
+ * - `## [Unreleased]` is exempt (it's the staging area, no tag).
17
+ * - Version headings without a corresponding tag are exempt — that's
18
+ * usually the "preparing X.Y.Z" state right before the tag exists.
19
+ * - Tag lookup is local-only (`git rev-parse`); CI must `git fetch
20
+ * --tags` first if it runs on a shallow clone.
21
+ * - `repoRoot` is injectable so tests don't need to monkey-patch the
22
+ * module by re-evaluating source (autogame-17 PR #115 review).
23
+ *
24
+ * Usage:
25
+ * node scripts/check-changelog.js # CLI mode, exits 0/1
26
+ * const { checkChangelogIntegrity } = require('./check-changelog');
27
+ * const result = checkChangelogIntegrity({ repoRoot });
28
+ */
29
+
30
+ const { execFileSync } = require('child_process');
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+
34
+ const DEFAULT_REPO_ROOT = path.resolve(__dirname, '..');
35
+
36
+ function readChangelogAtHead(repoRoot) {
37
+ return fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
38
+ }
39
+
40
+ function readChangelogAtRef(repoRoot, ref) {
41
+ try {
42
+ return execFileSync('git', ['show', `${ref}:CHANGELOG.md`], {
43
+ cwd: repoRoot,
44
+ encoding: 'utf8',
45
+ stdio: ['ignore', 'pipe', 'ignore'],
46
+ });
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function tagExists(repoRoot, tag) {
53
+ try {
54
+ execFileSync('git', ['rev-parse', '--verify', `refs/tags/${tag}`], {
55
+ cwd: repoRoot,
56
+ stdio: ['ignore', 'ignore', 'ignore'],
57
+ });
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ // Pull every `## [X.Y.Z]` heading from the file, skipping `[Unreleased]`.
65
+ function listReleasedVersionHeadings(text) {
66
+ const versions = [];
67
+ const re = /^## \[(\d+\.\d+\.\d+(?:[-+][\w.]+)?)\]/gm;
68
+ let m;
69
+ while ((m = re.exec(text)) !== null) {
70
+ versions.push(m[1]);
71
+ }
72
+ return versions;
73
+ }
74
+
75
+ // Extract the body between `## [X.Y.Z]` and the next `## [` (or EOF).
76
+ // Normalises trailing whitespace and trailing blank lines so a stray
77
+ // newline doesn't fail the equality check.
78
+ //
79
+ // Heading match is line-anchored (`/^## \[X\.Y\.Z\]/m`) so a fenced
80
+ // code block or quoted text containing `## [X.Y.Z]` mid-line cannot be
81
+ // mistaken for the section start (Bugbot PR #115 review).
82
+ function extractSection(text, version) {
83
+ const escaped = version.replace(/[.+]/g, (c) => '\\' + c);
84
+ const re = new RegExp(`^## \\[${escaped}\\]`, 'm');
85
+ const match = re.exec(text);
86
+ if (!match) return null;
87
+ const after = match.index + match[0].length;
88
+ const rest = text.slice(after);
89
+ const nextRel = rest.search(/\n## \[/);
90
+ const raw = nextRel === -1 ? rest : rest.slice(0, nextRel);
91
+ return raw
92
+ .split('\n')
93
+ .map((line) => line.replace(/\s+$/, ''))
94
+ .join('\n')
95
+ .replace(/\n+$/, '');
96
+ }
97
+
98
+ function checkChangelogIntegrity(opts) {
99
+ const repoRoot = (opts && opts.repoRoot) || DEFAULT_REPO_ROOT;
100
+ const head = readChangelogAtHead(repoRoot);
101
+ const versions = listReleasedVersionHeadings(head);
102
+
103
+ const drift = [];
104
+ const skipped = [];
105
+
106
+ for (const version of versions) {
107
+ const tag = `v${version}`;
108
+ if (!tagExists(repoRoot, tag)) {
109
+ skipped.push({ version, reason: 'no matching git tag (probably preparing this release)' });
110
+ continue;
111
+ }
112
+ const tagText = readChangelogAtRef(repoRoot, tag);
113
+ if (tagText == null) {
114
+ skipped.push({ version, reason: `tag ${tag} exists but its CHANGELOG.md is unreadable` });
115
+ continue;
116
+ }
117
+ const headSection = extractSection(head, version);
118
+ const tagSection = extractSection(tagText, version);
119
+ if (headSection == null || tagSection == null) {
120
+ skipped.push({ version, reason: 'section parse failed' });
121
+ continue;
122
+ }
123
+ if (headSection !== tagSection) {
124
+ drift.push({ version, tag });
125
+ }
126
+ }
127
+
128
+ return { drift, skipped, checked: versions.length - skipped.length };
129
+ }
130
+
131
+ function main() {
132
+ const result = checkChangelogIntegrity();
133
+
134
+ process.stdout.write(`\n=== CHANGELOG release-section guard ===\n`);
135
+ process.stdout.write(`Checked ${result.checked} released version section(s); skipped ${result.skipped.length}.\n`);
136
+
137
+ for (const s of result.skipped) {
138
+ process.stdout.write(` [skip] ${s.version}: ${s.reason}\n`);
139
+ }
140
+
141
+ if (result.drift.length === 0) {
142
+ process.stdout.write(`\n[OK] No released CHANGELOG section was edited after its release tag.\n`);
143
+ return 0;
144
+ }
145
+
146
+ process.stderr.write(`\n[FAIL] ${result.drift.length} CHANGELOG section(s) diverged from their release tag:\n`);
147
+ for (const d of result.drift) {
148
+ process.stderr.write(` - ## [${d.version}] differs from ${d.tag}:CHANGELOG.md\n`);
149
+ }
150
+ process.stderr.write(
151
+ `\nReleased sections must stay frozen. Move any new entries under ## [Unreleased],\n` +
152
+ `or, if the entry was genuinely missing from the release, amend it on a hotfix\n` +
153
+ `branch and tag a patch release.\n`
154
+ );
155
+ return 1;
156
+ }
157
+
158
+ if (require.main === module) {
159
+ process.exit(main());
160
+ }
161
+
162
+ module.exports = {
163
+ checkChangelogIntegrity,
164
+ extractSection, // for tests
165
+ listReleasedVersionHeadings, // for tests
166
+ };
@@ -153,12 +153,11 @@ function assertNotSymlink(p, label) {
153
153
 
154
154
  function copyHookScripts(destDir, evolverRoot) {
155
155
  const scriptsDir = path.join(evolverRoot || __dirname, 'scripts');
156
- // _runtimePaths.js is required by the two session-* scripts via
157
- // `require('./_runtimePaths')`, which resolves relative to the *destination*
158
- // (__dirname after copy). It MUST be copied alongside or both hooks crash
159
- // with MODULE_NOT_FOUND at runtime. Caught in PR #94 review.
156
+ // Helper modules are required by copied hook scripts via relative require()
157
+ // calls, which resolve against the destination hook directory at runtime.
160
158
  const scripts = [
161
159
  '_runtimePaths.js',
160
+ '_memoryFiltering.js',
162
161
  'evolver-session-start.js',
163
162
  'evolver-signal-detect.js',
164
163
  'evolver-session-end.js',
@@ -237,6 +236,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
237
236
  function removeHookScripts(hooksDir) {
238
237
  const scripts = [
239
238
  '_runtimePaths.js',
239
+ '_memoryFiltering.js',
240
240
  'evolver-session-start.js',
241
241
  'evolver-signal-detect.js',
242
242
  'evolver-session-end.js',
@@ -0,0 +1,35 @@
1
+ // _memoryFiltering.js
2
+ // Shared memory filtering logic for evolver hooks (platform-independent).
3
+ //
4
+ // Responsibility: Filter evolution memory outcomes to reduce noise in Claude/Codex context.
5
+ // - Removes failed outcomes (no learning value)
6
+ // - Filters low-confidence outcomes (score < 0.5)
7
+ // - Enforces time bounds (< 7 days old)
8
+ // - Limits result size (max 3 outcomes)
9
+
10
+ const DEFAULT_MIN_SCORE = 0.5;
11
+ const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
12
+ const DEFAULT_MAX_OUTCOMES = 3;
13
+
14
+ function filterRelevantOutcomes(entries, opts = {}) {
15
+ const minScore = opts.minScore !== undefined ? opts.minScore : DEFAULT_MIN_SCORE;
16
+ const maxAgeMs = opts.maxAgeMs !== undefined ? opts.maxAgeMs : DEFAULT_MAX_AGE_MS;
17
+ const maxOutcomes = opts.maxOutcomes !== undefined ? opts.maxOutcomes : DEFAULT_MAX_OUTCOMES;
18
+
19
+ const now = Date.now();
20
+
21
+ return entries
22
+ .filter(e => {
23
+ // Only keep 'success' outcomes (failed ones don't provide learning value)
24
+ if (e.outcome?.status !== 'success') return false;
25
+ // Only keep high-confidence outcomes
26
+ if ((e.outcome?.score ?? 0) < minScore) return false;
27
+ // Only keep recent outcomes
28
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
29
+ if (now - ts > maxAgeMs) return false;
30
+ return true;
31
+ })
32
+ .slice(-maxOutcomes);
33
+ }
34
+
35
+ module.exports = { filterRelevantOutcomes, DEFAULT_MIN_SCORE, DEFAULT_MAX_AGE_MS, DEFAULT_MAX_OUTCOMES };
@@ -8,6 +8,56 @@ const path = require('path');
8
8
  const os = require('os');
9
9
 
10
10
  const { findEvolverRoot, findMemoryGraph } = require('./_runtimePaths');
11
+ const { filterRelevantOutcomes } = require('./_memoryFiltering');
12
+
13
+ function findGitRoot(start) {
14
+ let dir = path.resolve(start || process.cwd());
15
+ while (dir !== path.dirname(dir)) {
16
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
17
+ dir = path.dirname(dir);
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function resolveWorkspaceRootForReader() {
23
+ if (process.env.OPENCLAW_WORKSPACE) return process.env.OPENCLAW_WORKSPACE;
24
+ const repoRoot = process.env.EVOLVER_REPO_ROOT || findGitRoot(process.cwd()) || process.cwd();
25
+ const workspaceDir = path.join(repoRoot, 'workspace');
26
+ if (fs.existsSync(workspaceDir)) return workspaceDir;
27
+ return repoRoot;
28
+ }
29
+
30
+ function resolveWorkspaceIdForReader() {
31
+ if (process.env.EVOLVER_WORKSPACE_ID) return String(process.env.EVOLVER_WORKSPACE_ID);
32
+ const file = path.join(resolveWorkspaceRootForReader(), '.evolver', 'workspace-id');
33
+ try {
34
+ const dirStat = fs.lstatSync(path.dirname(file), { throwIfNoEntry: false });
35
+ if (dirStat && dirStat.isSymbolicLink()) return null;
36
+ const fileStat = fs.lstatSync(file, { throwIfNoEntry: false });
37
+ if (!fileStat || fileStat.isSymbolicLink() || !fileStat.isFile()) return null;
38
+ const raw = fs.readFileSync(file, 'utf8').trim();
39
+ if (raw && /^[a-f0-9]{32,}$/i.test(raw)) return raw;
40
+ } catch { /* workspace id is best-effort in copied hooks */ }
41
+ return null;
42
+ }
43
+
44
+ function filterWorkspaceEntries(entries) {
45
+ const cwd = process.cwd();
46
+ const workspaceId = resolveWorkspaceIdForReader();
47
+
48
+ return entries.filter(entry => {
49
+ if (!entry || typeof entry !== 'object') return false;
50
+ if (workspaceId && entry.workspace_id) {
51
+ return String(entry.workspace_id) === String(workspaceId);
52
+ }
53
+ if (entry.cwd) {
54
+ return path.resolve(String(entry.cwd)) === path.resolve(cwd);
55
+ }
56
+ // Older entries did not carry a workspace tag. Do not inject them from
57
+ // hooks because copied hooks often share a user-level fallback memory file.
58
+ return false;
59
+ });
60
+ }
11
61
 
12
62
  function readLastN(filePath, n) {
13
63
  try {
@@ -99,18 +149,21 @@ function main() {
99
149
  return;
100
150
  }
101
151
 
102
- const entries = readLastN(graphPath, 5);
103
- if (entries.length === 0) {
152
+ const entries = readLastN(graphPath, 20);
153
+ const scoped = filterWorkspaceEntries(entries);
154
+ const filtered = filterRelevantOutcomes(scoped);
155
+
156
+ if (filtered.length === 0) {
104
157
  process.stdout.write(JSON.stringify({}));
105
158
  return;
106
159
  }
107
160
 
108
- const successCount = entries.filter(e => e.outcome && e.outcome.status === 'success').length;
109
- const failCount = entries.filter(e => e.outcome && e.outcome.status === 'failed').length;
161
+ const successCount = filtered.filter(e => e.outcome && e.outcome.status === 'success').length;
162
+ const failCount = filtered.filter(e => e.outcome && e.outcome.status === 'failed').length;
110
163
 
111
- const lines = entries.map(formatOutcome);
164
+ const lines = filtered.map(formatOutcome);
112
165
  const summary = [
113
- `[Evolution Memory] Recent ${entries.length} outcomes (${successCount} success, ${failCount} failed):`,
166
+ `[Evolution Memory] Recent ${filtered.length} outcomes (${successCount} success, ${failCount} failed):`,
114
167
  ...lines,
115
168
  '',
116
169
  'Use successful approaches. Avoid repeating failed patterns.',
@@ -13,9 +13,32 @@ const SIGNAL_KEYWORDS = {
13
13
  test_failure: ['test failed', 'test failure', 'assertion', 'expect(', 'assert.'],
14
14
  };
15
15
 
16
+ function stratifyContent(text) {
17
+ // Separate code/comments/documents to avoid false positives
18
+ const lines = text.split('\n');
19
+ const documentText = [];
20
+
21
+ for (const line of lines) {
22
+ const trimmed = line.trim();
23
+ // Skip lines that are comments or code structure (not document text)
24
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*') ||
25
+ trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('}') ||
26
+ trimmed.startsWith(']') || trimmed.startsWith('/*')) {
27
+ continue;
28
+ }
29
+ documentText.push(line);
30
+ }
31
+
32
+ return documentText.join('\n');
33
+ }
34
+
16
35
  function detectSignals(text) {
17
36
  if (!text || typeof text !== 'string') return [];
18
- const lower = text.toLowerCase();
37
+
38
+ // Apply stratification to reduce false positives from code/comments
39
+ const stratified = stratifyContent(text);
40
+ const lower = stratified.toLowerCase();
41
+
19
42
  const found = [];
20
43
  for (const [signal, keywords] of Object.entries(SIGNAL_KEYWORDS)) {
21
44
  for (const kw of keywords) {
@@ -28,6 +51,30 @@ function detectSignals(text) {
28
51
  return [...new Set(found)];
29
52
  }
30
53
 
54
+ function getToolName(input) {
55
+ const raw = input.tool_name || input.toolName || input.name || input.tool;
56
+ if (typeof raw === 'string') return raw;
57
+ if (raw && typeof raw.name === 'string') return raw.name;
58
+ return '';
59
+ }
60
+
61
+ function isWriteLikeTool(input) {
62
+ const name = getToolName(input).toLowerCase();
63
+ // Older hook payloads did not include a tool name. Keep those compatible
64
+ // and let the content/path checks below decide whether there is work to do.
65
+ if (!name) return true;
66
+ return (
67
+ name === 'write' ||
68
+ name === 'edit' ||
69
+ name === 'multiedit' ||
70
+ name === 'notebookedit' ||
71
+ name === 'apply_patch' ||
72
+ name.includes('write') ||
73
+ name.includes('edit') ||
74
+ name.includes('patch')
75
+ );
76
+ }
77
+
31
78
  function main() {
32
79
  let inputData = '';
33
80
  let handled = false;
@@ -38,6 +85,10 @@ function main() {
38
85
  handled = true;
39
86
  try {
40
87
  const input = inputData.trim() ? JSON.parse(inputData) : {};
88
+ if (!isWriteLikeTool(input)) {
89
+ process.stdout.write(JSON.stringify({}));
90
+ return;
91
+ }
41
92
  // Claude Code's PostToolUse payload nests tool args under tool_input.
42
93
  // Older/raw shapes put them at the top level; support both.
43
94
  const ti = input.tool_input || {};