@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.
- package/index.js +70 -64
- package/package.json +4 -1
- package/scripts/check-changelog.js +166 -0
- package/src/adapters/hookAdapter.js +4 -4
- package/src/adapters/scripts/_memoryFiltering.js +35 -0
- package/src/adapters/scripts/evolver-session-start.js +59 -6
- package/src/adapters/scripts/evolver-signal-detect.js +52 -1
- package/src/evolve/guards.js +1 -1
- package/src/evolve/pipeline/collect.js +1 -1
- package/src/evolve/pipeline/dispatch.js +1 -1
- package/src/evolve/pipeline/enrich.js +1 -1
- package/src/evolve/pipeline/hub.js +1 -1
- package/src/evolve/pipeline/select.js +1 -1
- package/src/evolve/pipeline/signals.js +1 -1
- package/src/evolve/utils.js +1 -1
- package/src/evolve.js +1 -1
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/crypto.js +1 -1
- package/src/gep/curriculum.js +1 -1
- package/src/gep/deviceId.js +1 -1
- package/src/gep/envFingerprint.js +1 -1
- package/src/gep/epigenetics.js +1 -1
- package/src/gep/explore.js +1 -1
- package/src/gep/hash.js +1 -1
- package/src/gep/hubFetch.js +1 -1
- package/src/gep/hubReview.js +1 -1
- package/src/gep/hubSearch.js +1 -1
- package/src/gep/hubVerify.js +1 -1
- package/src/gep/learningSignals.js +1 -1
- package/src/gep/memoryGraph.js +1 -1
- package/src/gep/memoryGraphAdapter.js +1 -1
- package/src/gep/mutation.js +1 -1
- package/src/gep/narrativeMemory.js +1 -1
- package/src/gep/openPRRegistry.js +1 -1
- package/src/gep/paths.js +124 -31
- package/src/gep/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallVerifier.js +1 -1
- package/src/gep/reflection.js +1 -1
- package/src/gep/selector.js +1 -1
- package/src/gep/skillDistiller.js +1 -1
- package/src/gep/solidify.js +1 -1
- package/src/gep/strategy.js +1 -1
- package/src/gep/workspaceKeychain.js +1 -0
- package/skills/_meta/SKILL.md +0 -41
- 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.
|
|
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
|
-
//
|
|
157
|
-
//
|
|
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,
|
|
103
|
-
|
|
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 =
|
|
109
|
-
const failCount =
|
|
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 =
|
|
164
|
+
const lines = filtered.map(formatOutcome);
|
|
112
165
|
const summary = [
|
|
113
|
-
`[Evolution Memory] Recent ${
|
|
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
|
-
|
|
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 || {};
|