@evomap/evolver 1.88.0 → 1.88.2
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 +232 -1
- package/package.json +2 -1
- package/src/adapters/claudeCode.js +21 -1
- package/src/adapters/hookAdapter.js +4 -2
- package/src/adapters/scripts/evolver-session-start.js +14 -10
- package/src/adapters/scripts/evolver-task-recall.js +173 -0
- package/src/atp/atpExecute.js +20 -7
- package/src/atp/cli.js +17 -9
- package/src/atp/protocol.js +41 -0
- package/src/config.js +23 -0
- 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/forceUpdate.js +108 -3
- package/src/gep/a2aProtocol.js +1 -1
- package/src/gep/assetCallLog.js +40 -1
- package/src/gep/autoDistillConv.js +1 -0
- package/src/gep/autoDistillLlm.js +1 -0
- package/src/gep/bridge.js +69 -2
- package/src/gep/candidateEval.js +1 -1
- package/src/gep/candidates.js +1 -1
- package/src/gep/contentHash.js +1 -1
- package/src/gep/conversationSniffer.js +1 -0
- 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/execBridge.js +1 -0
- 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/personality.js +1 -1
- package/src/gep/policyCheck.js +1 -1
- package/src/gep/prompt.js +1 -1
- package/src/gep/recallInject.js +1 -0
- 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 -1
- package/src/proxy/index.js +46 -6
- package/src/proxy/lifecycle/manager.js +456 -2
package/index.js
CHANGED
|
@@ -1332,6 +1332,7 @@ async function main() {
|
|
|
1332
1332
|
// Hoist module refs used inside the loop to avoid repeated module lookups per cycle
|
|
1333
1333
|
const idleScheduler = require('./src/gep/idleScheduler');
|
|
1334
1334
|
const { shouldDistillFromFailures: shouldDF, autoDistillFromFailures: autoDF } = require('./src/gep/skillDistiller');
|
|
1335
|
+
const { autoDistillLlm } = require('./src/gep/autoDistillLlm'); // P3: autonomous LLM distillation (shadow-first, off by default)
|
|
1335
1336
|
const { tryExplore } = require('./src/gep/explore');
|
|
1336
1337
|
|
|
1337
1338
|
let currentSleepMs = minSleepMs;
|
|
@@ -1454,6 +1455,21 @@ async function main() {
|
|
|
1454
1455
|
} catch (e) {
|
|
1455
1456
|
if (isVerbose) console.warn('[OMLS] Distill error: ' + (e.message || e));
|
|
1456
1457
|
}
|
|
1458
|
+
// P3: autonomous LLM-quality distillation of SUCCESS capsules.
|
|
1459
|
+
// Default off; shadow logs a candidate; enforce upserts (after a
|
|
1460
|
+
// real run-green gate). Reuses the P1 exec bridge under the hood.
|
|
1461
|
+
if ((process.env.EVOLVER_AUTO_DISTILL_LLM || 'off') !== 'off') {
|
|
1462
|
+
try {
|
|
1463
|
+
const llmRes = await autoDistillLlm();
|
|
1464
|
+
if (llmRes && llmRes.ok && llmRes.gene) {
|
|
1465
|
+
console.log('[OMLS] Idle-window LLM distillation enforced gene: ' + llmRes.gene.id);
|
|
1466
|
+
} else if (llmRes && llmRes.reason === 'shadow_logged') {
|
|
1467
|
+
console.log('[OMLS] LLM distillation shadow candidate: ' + (llmRes.candidate && llmRes.candidate.id));
|
|
1468
|
+
}
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
if (isVerbose) console.warn('[OMLS] LLM distill error (non-fatal): ' + (e.message || e));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1457
1473
|
}
|
|
1458
1474
|
if (schedule.should_explore) {
|
|
1459
1475
|
try {
|
|
@@ -1465,6 +1481,22 @@ async function main() {
|
|
|
1465
1481
|
if (isVerbose) console.warn('[OMLS] Explore error: ' + (e.message || e));
|
|
1466
1482
|
}
|
|
1467
1483
|
}
|
|
1484
|
+
// P2: conversation capability -> distilled gene (shadow-only v1).
|
|
1485
|
+
// Deliberately OUTSIDE the should_distill guard: should_distill is
|
|
1486
|
+
// true only at aggressive/deep intensity, but headless/air-gapped
|
|
1487
|
+
// hosts fall back to 'normal', which would make P2 a dead feature.
|
|
1488
|
+
// A freshly-discovered capability is time-relevant; gate it solely on
|
|
1489
|
+
// its own flag + the per-slug cooldown + a non-empty queue (all of
|
|
1490
|
+
// which already bound spend). Default off => zero behavior change.
|
|
1491
|
+
if ((process.env.EVOLVER_CONV_DISTILL_ENABLED || 'off') !== 'off') {
|
|
1492
|
+
try {
|
|
1493
|
+
const { autoDistillConversation } = require('./src/gep/autoDistillConv');
|
|
1494
|
+
const convRes = await autoDistillConversation();
|
|
1495
|
+
if (convRes && convRes.ok) console.log('[P2] conv-distill ' + convRes.mode + ' candidate: ' + (convRes.gene_id || convRes.reason));
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
if (isVerbose) console.warn('[P2] conv-distill error (non-fatal): ' + (e.message || e));
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1468
1500
|
if (isVerbose && schedule.idle_seconds >= 0) {
|
|
1469
1501
|
console.log(`[OMLS] idle=${schedule.idle_seconds}s intensity=${schedule.intensity} multiplier=${omlsMultiplier}`);
|
|
1470
1502
|
}
|
|
@@ -1691,6 +1723,60 @@ async function main() {
|
|
|
1691
1723
|
console.error('[SOLIDIFY] Error:', error);
|
|
1692
1724
|
process.exit(2);
|
|
1693
1725
|
}
|
|
1726
|
+
} else if (command === 'exec') {
|
|
1727
|
+
// node index.js exec --harness=claude-code [--once] [--max-cycles N]
|
|
1728
|
+
// P1 auto-exec bridge: run the Brain, scrape its sessions_spawn(...), spawn
|
|
1729
|
+
// the Hand (headless claude) to apply + solidify. Shadow-first opt-in.
|
|
1730
|
+
if (String(process.env.EVOLVE_EXEC_BRIDGE || '').toLowerCase() !== 'true') {
|
|
1731
|
+
console.error('[exec] EVOLVE_EXEC_BRIDGE is not "true". The auto-exec bridge is opt-in. Refusing.');
|
|
1732
|
+
process.exit(2);
|
|
1733
|
+
}
|
|
1734
|
+
const getFlag = (n) => {
|
|
1735
|
+
const i = args.findIndex(a => a === `--${n}` || a.startsWith(`--${n}=`));
|
|
1736
|
+
if (i === -1) return undefined;
|
|
1737
|
+
const h = args[i];
|
|
1738
|
+
if (h.includes('=')) return h.split('=').slice(1).join('='); // --n=value
|
|
1739
|
+
// bare --n: if the next token is a value (not another --flag), consume it
|
|
1740
|
+
// (#179 r6: support `--max-cycles N` space-separated, not just =N). A
|
|
1741
|
+
// trailing bare flag with no following value stays boolean true (e.g. --once).
|
|
1742
|
+
const next = args[i + 1];
|
|
1743
|
+
return (next !== undefined && !next.startsWith('--')) ? next : true;
|
|
1744
|
+
};
|
|
1745
|
+
const harness = String(getFlag('harness') || 'claude-code');
|
|
1746
|
+
const once = getFlag('once') === true;
|
|
1747
|
+
// #179 r7: validate --max-cycles. Number('foo')||0 silently became 0 =
|
|
1748
|
+
// unbounded daemon — a typo must fail fast, not run forever. Absent flag =>
|
|
1749
|
+
// 0 (intentional unbounded). A present value must be a non-negative integer.
|
|
1750
|
+
const rawMaxCycles = getFlag('max-cycles');
|
|
1751
|
+
let maxCycles = 0;
|
|
1752
|
+
if (rawMaxCycles !== undefined && rawMaxCycles !== true) {
|
|
1753
|
+
const n = Number(rawMaxCycles);
|
|
1754
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1755
|
+
console.error(`[exec] invalid --max-cycles '${rawMaxCycles}' (expected a non-negative integer; 0 or omit = unbounded)`);
|
|
1756
|
+
process.exit(2);
|
|
1757
|
+
}
|
|
1758
|
+
maxCycles = n;
|
|
1759
|
+
} else if (rawMaxCycles === true) {
|
|
1760
|
+
console.error('[exec] --max-cycles requires a value (e.g. --max-cycles 5 or --max-cycles=5)');
|
|
1761
|
+
process.exit(2);
|
|
1762
|
+
}
|
|
1763
|
+
if (!['claude-code', 'openclaw', 'codex', 'opencode'].includes(harness)) {
|
|
1764
|
+
console.error(`[exec] unknown --harness '${harness}' (expected claude-code | openclaw | codex | opencode)`);
|
|
1765
|
+
process.exit(2);
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
const { runExecBridge } = require('./src/gep/execBridge');
|
|
1769
|
+
const res = await runExecBridge({ harness, once, maxCycles });
|
|
1770
|
+
console.log(`[exec] done: cycles=${res.cycles} lastOutcome=${res.lastOutcome}`);
|
|
1771
|
+
// Exit 0 only on a genuine success. A bounded/daemon run that ended in
|
|
1772
|
+
// hand_failed/brain_failed/no_spawn must report non-zero to shells & CI
|
|
1773
|
+
// (Bugbot #179: do not exit 0 on failure just because cycles>0).
|
|
1774
|
+
process.exit(res.lastOutcome === 'success' ? 0 : 1);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
console.error('[exec] bridge error:', error && error.message ? error.message : error);
|
|
1777
|
+
process.exit(1);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1694
1780
|
} else if (command === 'distill') {
|
|
1695
1781
|
const responseFileFlag = args.find(a => typeof a === 'string' && a.startsWith('--response-file='));
|
|
1696
1782
|
if (!responseFileFlag) {
|
|
@@ -2677,8 +2763,153 @@ async function main() {
|
|
|
2677
2763
|
process.exit(1);
|
|
2678
2764
|
}
|
|
2679
2765
|
|
|
2766
|
+
} else if (command === 'recipe') {
|
|
2767
|
+
// recipe build — assemble a DNA blueprint from owned Gene/Capsule assets
|
|
2768
|
+
// recipe reuse — fetch + express an existing recipe into an organism
|
|
2769
|
+
const sub = args[1];
|
|
2770
|
+
const {
|
|
2771
|
+
getHubUrl, getNodeId, getHubNodeSecret, sendHelloToHub, rotateNodeSecret,
|
|
2772
|
+
hubCreateRecipe, hubPublishRecipe, hubGetRecipe, hubExpressRecipe,
|
|
2773
|
+
} = require('./src/gep/a2aProtocol');
|
|
2774
|
+
|
|
2775
|
+
const hubUrl = getHubUrl();
|
|
2776
|
+
if (!hubUrl) {
|
|
2777
|
+
console.error('[recipe] A2A_HUB_URL is not configured. Set A2A_HUB_URL (e.g. https://evomap.ai).');
|
|
2778
|
+
process.exit(1);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
function flagVal(name) {
|
|
2782
|
+
const eq = args.find(a => typeof a === 'string' && a.startsWith('--' + name + '='));
|
|
2783
|
+
return eq ? eq.split('=').slice(1).join('=') : null;
|
|
2784
|
+
}
|
|
2785
|
+
async function ensureRegistered(tag) {
|
|
2786
|
+
if (!getHubNodeSecret()) {
|
|
2787
|
+
console.log('[' + tag + '] No node_secret found. Registering with Hub...');
|
|
2788
|
+
const hello = await sendHelloToHub();
|
|
2789
|
+
if (!hello || !hello.ok) {
|
|
2790
|
+
console.error('[' + tag + '] Failed to register with Hub:', (hello && hello.error) || 'unknown');
|
|
2791
|
+
process.exit(1);
|
|
2792
|
+
}
|
|
2793
|
+
console.log('[' + tag + '] Registered as ' + getNodeId());
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
// True when the hub rejected our node_secret as stale/invalid — the one
|
|
2797
|
+
// case where a rotate-and-retry is the documented recovery.
|
|
2798
|
+
function isStaleSecret(result) {
|
|
2799
|
+
if (!result || result.ok) return false;
|
|
2800
|
+
if (result.status !== 401 && result.status !== 403) return false;
|
|
2801
|
+
const e = String(result.error || '');
|
|
2802
|
+
return e.includes('node_secret_invalid') || e.includes('node_secret_not_set');
|
|
2803
|
+
}
|
|
2804
|
+
// Run a hub call; if it fails because our node_secret is stale, rotate
|
|
2805
|
+
// once and retry. Rotation only works when the CURRENT secret is still
|
|
2806
|
+
// server-valid (the hub authenticates the rotate with it). If the secret
|
|
2807
|
+
// has fully diverged from the server, rotation cannot recover it — that
|
|
2808
|
+
// requires re-registering, so we surface the actionable recovery path
|
|
2809
|
+
// instead of silently looping.
|
|
2810
|
+
let _authRecoveryFailed = false;
|
|
2811
|
+
async function callWithAuthRetry(tag, fn) {
|
|
2812
|
+
let result = await fn();
|
|
2813
|
+
if (isStaleSecret(result) && typeof rotateNodeSecret === 'function' && !_authRecoveryFailed) {
|
|
2814
|
+
console.log('[' + tag + '] node_secret stale; rotating via /a2a/hello and retrying...');
|
|
2815
|
+
const rot = await rotateNodeSecret();
|
|
2816
|
+
if (rot && rot.ok) {
|
|
2817
|
+
result = await fn();
|
|
2818
|
+
} else {
|
|
2819
|
+
_authRecoveryFailed = true;
|
|
2820
|
+
console.error('[' + tag + '] Could not auto-rotate: the local node_secret has diverged from the Hub and can no longer authenticate a rotate.');
|
|
2821
|
+
console.error(' Recover by either:');
|
|
2822
|
+
console.error(' 1. Reset Secret on the web (Account -> Reset Secret), then run: node index.js reset-local-secret');
|
|
2823
|
+
console.error(' 2. Or register a fresh node: set a new A2A_NODE_ID and retry (auto-provisions).');
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
return result;
|
|
2827
|
+
}
|
|
2828
|
+
function reportHubError(tag, result) {
|
|
2829
|
+
console.error('[' + tag + '] Hub call failed' + (result.status ? ' (HTTP ' + result.status + ')' : '') + ': ' + (result.error || 'unknown'));
|
|
2830
|
+
if (result.status === 401 || result.status === 403) console.error(' Auth failed. If this persists, delete ~/.evomap/node_secret and retry.');
|
|
2831
|
+
else if (result.status === 402) console.error(' Insufficient credits. Check your balance at ' + hubUrl);
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
if (sub === 'build') {
|
|
2835
|
+
// --genes=<asset_id,...> ordered; types resolved from the local asset store.
|
|
2836
|
+
const genesArg = flagVal('genes');
|
|
2837
|
+
const title = flagVal('title');
|
|
2838
|
+
const description = flagVal('description');
|
|
2839
|
+
const doPublish = args.includes('--publish');
|
|
2840
|
+
if (!genesArg || !title) {
|
|
2841
|
+
console.error('Usage: node index.js recipe build --title="..." --genes=<asset_id,...> [--description="..."] [--price=N] [--publish]');
|
|
2842
|
+
console.error(' Builds a DRAFT recipe by default. --publish is opt-in and pushes it live.');
|
|
2843
|
+
process.exit(1);
|
|
2844
|
+
}
|
|
2845
|
+
const { loadGenes, loadCapsules } = require('./src/gep/assetStore');
|
|
2846
|
+
const typeById = new Map();
|
|
2847
|
+
try {
|
|
2848
|
+
for (const g of (loadGenes() || [])) if (g && g.asset_id) typeById.set(g.asset_id, 'Gene');
|
|
2849
|
+
for (const c of (loadCapsules() || [])) if (c && c.asset_id) typeById.set(c.asset_id, 'Capsule');
|
|
2850
|
+
} catch (e) { /* fall back to Gene below */ }
|
|
2851
|
+
|
|
2852
|
+
const ids = genesArg.split(',').map(s => s.trim()).filter(Boolean);
|
|
2853
|
+
if (ids.length === 0) { console.error('[recipe build] --genes is empty.'); process.exit(1); }
|
|
2854
|
+
if (ids.length > 20) { console.error('[recipe build] at most 20 steps per recipe.'); process.exit(1); }
|
|
2855
|
+
const steps = ids.map((asset_id, i) => ({
|
|
2856
|
+
asset_id,
|
|
2857
|
+
asset_type: typeById.get(asset_id) || 'Gene',
|
|
2858
|
+
position: i,
|
|
2859
|
+
}));
|
|
2860
|
+
|
|
2861
|
+
await ensureRegistered('recipe build');
|
|
2862
|
+
const priceVal = flagVal('price');
|
|
2863
|
+
const createRes = await callWithAuthRetry('recipe build', () => hubCreateRecipe({
|
|
2864
|
+
title, steps, description: description || undefined,
|
|
2865
|
+
pricePerExecution: priceVal ? Number(priceVal) : undefined,
|
|
2866
|
+
}));
|
|
2867
|
+
if (!createRes.ok) { reportHubError('recipe build', createRes); process.exit(1); }
|
|
2868
|
+
const recipe = (createRes.data && (createRes.data.recipe || createRes.data)) || {};
|
|
2869
|
+
const recipeId = recipe.id;
|
|
2870
|
+
console.log('[recipe build] Created DRAFT recipe ' + recipeId + ' ("' + title + '", ' + steps.length + ' steps).');
|
|
2871
|
+
|
|
2872
|
+
if (doPublish && recipeId) {
|
|
2873
|
+
const pubRes = await callWithAuthRetry('recipe build', () => hubPublishRecipe(recipeId));
|
|
2874
|
+
if (!pubRes.ok) { reportHubError('recipe build', pubRes); process.exit(1); }
|
|
2875
|
+
console.log('[recipe build] Published recipe ' + recipeId + ' to the marketplace.');
|
|
2876
|
+
} else if (!doPublish) {
|
|
2877
|
+
console.log('[recipe build] Left as draft. Re-run with --publish to make it live.');
|
|
2878
|
+
}
|
|
2879
|
+
process.exit(0);
|
|
2880
|
+
} else if (sub === 'reuse') {
|
|
2881
|
+
const recipeId = flagVal('id') || (args[2] && !String(args[2]).startsWith('-') ? args[2] : null);
|
|
2882
|
+
if (!recipeId) {
|
|
2883
|
+
console.error('Usage: node index.js recipe reuse --id=<recipe_id> [--input=<json>]');
|
|
2884
|
+
process.exit(1);
|
|
2885
|
+
}
|
|
2886
|
+
await ensureRegistered('recipe reuse');
|
|
2887
|
+
const getRes = await hubGetRecipe(recipeId);
|
|
2888
|
+
if (!getRes.ok) { reportHubError('recipe reuse', getRes); process.exit(1); }
|
|
2889
|
+
let inputPayload = {};
|
|
2890
|
+
const inputArg = flagVal('input');
|
|
2891
|
+
if (inputArg) {
|
|
2892
|
+
try { inputPayload = JSON.parse(inputArg); }
|
|
2893
|
+
catch (e) { console.error('[recipe reuse] --input must be valid JSON.'); process.exit(1); }
|
|
2894
|
+
}
|
|
2895
|
+
const expRes = await callWithAuthRetry('recipe reuse', () => hubExpressRecipe(recipeId, inputPayload));
|
|
2896
|
+
if (!expRes.ok) { reportHubError('recipe reuse', expRes); process.exit(1); }
|
|
2897
|
+
console.log('[recipe reuse] Expressed recipe ' + recipeId + '.');
|
|
2898
|
+
if (isVerbose) console.log(JSON.stringify(expRes.data, null, 2));
|
|
2899
|
+
process.exit(0);
|
|
2900
|
+
} else {
|
|
2901
|
+
console.error('Usage: node index.js recipe <build|reuse> [flags]');
|
|
2902
|
+
console.error(' build --title="..." --genes=<asset_id,...> [--publish] (draft unless --publish)');
|
|
2903
|
+
console.error(' reuse --id=<recipe_id> [--input=<json>]');
|
|
2904
|
+
process.exit(1);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2680
2907
|
} else {
|
|
2681
|
-
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|buy|orders|verify|atp|atp-complete] [--loop]
|
|
2908
|
+
console.log(`Usage: node index.js [run|/evolve|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|recipe|buy|orders|verify|atp|atp-complete] [--loop]
|
|
2909
|
+
- recipe flags:
|
|
2910
|
+
- build --title="..." --genes=<asset_id,...> [--description] [--price=N] [--publish]
|
|
2911
|
+
(builds a DRAFT DNA blueprint; --publish is opt-in)
|
|
2912
|
+
- reuse --id=<recipe_id> [--input=<json>] (express a recipe into an organism)
|
|
2682
2913
|
- fetch flags:
|
|
2683
2914
|
- --skill=<id> | -s <id> (skill ID to download)
|
|
2684
2915
|
- --out=<dir> (output directory, default: ./skills/<skill_id>)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver",
|
|
3
|
-
"version": "1.88.
|
|
3
|
+
"version": "1.88.2",
|
|
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": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@aws-sdk/client-bedrock-runtime": "^3.1053.0",
|
|
40
|
+
"@evomap/atp-sdk": "^0.1.0",
|
|
40
41
|
"@evomap/gep-sdk": "^1.5.0",
|
|
41
42
|
"dotenv": "^16.4.7",
|
|
42
43
|
"undici": "^7.0.0"
|
|
@@ -20,6 +20,24 @@ function buildClaudeHooks(evolverRoot) {
|
|
|
20
20
|
],
|
|
21
21
|
},
|
|
22
22
|
],
|
|
23
|
+
UserPromptSubmit: [
|
|
24
|
+
{
|
|
25
|
+
hooks: [
|
|
26
|
+
{
|
|
27
|
+
type: 'command',
|
|
28
|
+
// Runtime asset injection (P4-c). DEFAULT off (EVOLVER_RECALL_MODE
|
|
29
|
+
// unset/off -> emits {} without reading the prompt). The script
|
|
30
|
+
// owns an absolute 3.3s watchdog that always exits 0 with valid
|
|
31
|
+
// JSON (fail-open); this host timeout (5s) is a strict backstop
|
|
32
|
+
// ABOVE that watchdog, so the host never kills the script
|
|
33
|
+
// mid-write. A stuck/slow recall can never block or erase the
|
|
34
|
+
// user's prompt.
|
|
35
|
+
command: `node ${scriptsBase}/evolver-task-recall.js`,
|
|
36
|
+
timeout: 5,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
23
41
|
PostToolUse: [
|
|
24
42
|
{
|
|
25
43
|
matcher: 'Write',
|
|
@@ -55,6 +73,8 @@ This project uses evolver for self-evolution. Hooks automatically:
|
|
|
55
73
|
1. Inject recent evolution memory at session start
|
|
56
74
|
2. Detect evolution signals during file edits
|
|
57
75
|
3. Record outcomes at session end
|
|
76
|
+
4. (Opt-in) Surface matching distilled capabilities for each prompt — set
|
|
77
|
+
\`EVOLVER_RECALL_MODE=shadow\` to preview, \`enforce\` to inject (default off).
|
|
58
78
|
|
|
59
79
|
For substantive tasks, call \`gep_recall\` before work and \`gep_record_outcome\` after.
|
|
60
80
|
Signals: log_error, perf_bottleneck, user_feature_request, capability_gap, deployment_issue, test_failure.`;
|
|
@@ -126,7 +146,7 @@ function uninstall({ configRoot }) {
|
|
|
126
146
|
const innerBefore = matcher.hooks.length;
|
|
127
147
|
const filtered = matcher.hooks.filter(h => {
|
|
128
148
|
const cmd = (h && h.command) || '';
|
|
129
|
-
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
149
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
|
|
130
150
|
});
|
|
131
151
|
// A matcher containing both evolver and user hooks shrinks
|
|
132
152
|
// its inner array without changing the outer matcher count.
|
|
@@ -76,7 +76,7 @@ function mergeWithHooksUnion(target, source) {
|
|
|
76
76
|
if (tArr && sArr) {
|
|
77
77
|
const isEvolverOwned = (entry) => {
|
|
78
78
|
const cmds = collectCommands(entry);
|
|
79
|
-
return cmds.some(c => c.includes('evolver-session') || c.includes('evolver-signal'));
|
|
79
|
+
return cmds.some(c => c.includes('evolver-session') || c.includes('evolver-signal') || c.includes('evolver-task-recall'));
|
|
80
80
|
};
|
|
81
81
|
const userEntries = tArr.filter(e => !isEvolverOwned(e));
|
|
82
82
|
result.hooks[event] = [...userEntries, ...sArr];
|
|
@@ -171,6 +171,7 @@ function copyHookScripts(destDir, evolverRoot) {
|
|
|
171
171
|
'evolver-session-start.js',
|
|
172
172
|
'evolver-signal-detect.js',
|
|
173
173
|
'evolver-session-end.js',
|
|
174
|
+
'evolver-task-recall.js',
|
|
174
175
|
];
|
|
175
176
|
fs.mkdirSync(destDir, { recursive: true });
|
|
176
177
|
const copied = [];
|
|
@@ -224,7 +225,7 @@ function removeEvolverHooks(filePath, { markerKey = '_evolver_managed' } = {}) {
|
|
|
224
225
|
const before = data.hooks[event].length;
|
|
225
226
|
data.hooks[event] = data.hooks[event].filter(h => {
|
|
226
227
|
const cmd = h.command || '';
|
|
227
|
-
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal');
|
|
228
|
+
return !cmd.includes('evolver-session') && !cmd.includes('evolver-signal') && !cmd.includes('evolver-task-recall');
|
|
228
229
|
});
|
|
229
230
|
if (data.hooks[event].length !== before) changed = true;
|
|
230
231
|
if (data.hooks[event].length === 0) delete data.hooks[event];
|
|
@@ -257,6 +258,7 @@ function removeHookScripts(hooksDir) {
|
|
|
257
258
|
'evolver-session-start.js',
|
|
258
259
|
'evolver-signal-detect.js',
|
|
259
260
|
'evolver-session-end.js',
|
|
261
|
+
'evolver-task-recall.js',
|
|
260
262
|
];
|
|
261
263
|
let removed = 0;
|
|
262
264
|
for (const name of scripts) {
|
|
@@ -199,14 +199,18 @@ function getDedupStatePath() {
|
|
|
199
199
|
return path.join(dir, 'session-start-state.json');
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
202
|
+
function getNoticeStatePath() {
|
|
203
|
+
const dir = process.env.EVOLVER_SESSION_STATE_DIR
|
|
204
|
+
|| path.join(os.homedir(), '.evolver');
|
|
205
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch { /* ignore */ }
|
|
206
|
+
return path.join(dir, 'session-start-notice-state.json');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// TTL throttle keyed by an arbitrary string. Returns true if `key` fired within
|
|
210
|
+
// the last `ttlMs` (caller should suppress); otherwise records "now" for `key`
|
|
211
|
+
// and returns false. Best-effort: a state read/write failure just means no
|
|
212
|
+
// throttling (fail open). Entries older than 24h are pruned on write.
|
|
213
|
+
function throttled(key, ttlMs, statePath) {
|
|
210
214
|
let state = {};
|
|
211
215
|
try {
|
|
212
216
|
if (fs.existsSync(statePath)) {
|
|
@@ -241,7 +245,7 @@ function shouldSkipInjection() {
|
|
|
241
245
|
if (!dedupEnabled) return false;
|
|
242
246
|
|
|
243
247
|
const ttlMs = Number(process.env.EVOLVER_SESSION_START_DEDUP_TTL_MS) || (30 * 60 * 1000);
|
|
244
|
-
return throttled(process.cwd(), ttlMs);
|
|
248
|
+
return throttled(process.cwd(), ttlMs, getDedupStatePath());
|
|
245
249
|
}
|
|
246
250
|
|
|
247
251
|
function main() {
|
|
@@ -256,7 +260,7 @@ function main() {
|
|
|
256
260
|
// from git diffs), so tell the user — once per folder per TTL — instead of
|
|
257
261
|
// failing silently. Emitted regardless of whether any memory exists below.
|
|
258
262
|
const parts = [];
|
|
259
|
-
if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS)) {
|
|
263
|
+
if (!isGitWorkspace(currentDir) && !throttled('nongit:' + currentDir, NON_GIT_NOTICE_TTL_MS, getNoticeStatePath())) {
|
|
260
264
|
parts.push(NON_GIT_NOTICE);
|
|
261
265
|
}
|
|
262
266
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// evolver-task-recall.js
|
|
3
|
+
// UserPromptSubmit hook: on each user prompt, find GEP assets (Hub genes +
|
|
4
|
+
// local genes) that match the task and inject a distilled hint so a GENERAL
|
|
5
|
+
// agent benefits from prior distilled capabilities without manually calling
|
|
6
|
+
// MCP tools. This is the per-harness shell around src/gep/recallInject.js.
|
|
7
|
+
//
|
|
8
|
+
// Input (stdin JSON, Claude Code UserPromptSubmit):
|
|
9
|
+
// { "prompt": "...", "session_id", "cwd", "transcript_path", ... }
|
|
10
|
+
// Output (stdout JSON, exit 0 ALWAYS):
|
|
11
|
+
// enforce + match -> { agent_message, additionalContext,
|
|
12
|
+
// hookSpecificOutput: { hookEventName, additionalContext } }
|
|
13
|
+
// everything else -> {} (injects nothing)
|
|
14
|
+
//
|
|
15
|
+
// DESIGN CONTRACT (the fail-open core — see also recallInject.js invariants):
|
|
16
|
+
// - DEFAULT off. off finishes {} WITHOUT parsing the prompt body.
|
|
17
|
+
// - shadow computes + logs what WOULD inject but injects nothing.
|
|
18
|
+
// - enforce injects the distilled hint.
|
|
19
|
+
// - FAIL-OPEN: any error/timeout/empty -> exactly one finish({}). The hook
|
|
20
|
+
// blocks the user's prompt, so it must NEVER hang or crash the session.
|
|
21
|
+
// Claude Code's timeout fail-open behaviour is UNDOCUMENTED, so we own the
|
|
22
|
+
// deadline ourselves with a single watchdog + latch (never rely on the
|
|
23
|
+
// host to kill us gracefully).
|
|
24
|
+
// - STDOUT is a single JSON object. Any stray console.log() from a
|
|
25
|
+
// transitively-required module (e.g. signals._mergeSignals, hubSearch
|
|
26
|
+
// fetch-cost log, assetStore seeding) would corrupt it — so we redirect
|
|
27
|
+
// console.* to stderr before requiring anything heavy.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
// --- stdout-poison defense: route all console.* to stderr ------------------
|
|
32
|
+
// Modules we require (hubSearch, signals, assetStore, …) call console.log,
|
|
33
|
+
// which writes to stdout. The hook contract is ONE JSON object on stdout, so
|
|
34
|
+
// we redirect every console method to stderr. stderr on exit 0 is not fed to
|
|
35
|
+
// the model for UserPromptSubmit; on non-2 exit only its first line shows in
|
|
36
|
+
// the transcript as a hook-error notice — acceptable and we exit 0 anyway.
|
|
37
|
+
for (const m of ['log', 'info', 'warn', 'error', 'debug']) {
|
|
38
|
+
try { console[m] = function () { try { process.stderr.write(''); } catch (_) {} }; } catch (_) {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const path = require('path');
|
|
42
|
+
|
|
43
|
+
// ---- Timing budget, coherent with the host kill ---------------------------
|
|
44
|
+
// The host (Claude Code) kills this process at the hook's `timeout` (5s in
|
|
45
|
+
// buildClaudeHooks -> 5000ms). Our OWN absolute watchdog MUST fire comfortably
|
|
46
|
+
// BEFORE that, or the host could kill us mid-write and break the fail-open
|
|
47
|
+
// stdout contract. So:
|
|
48
|
+
// ABSOLUTE_DEADLINE_MS (3300) < host 5000ms -> ~1.7s headroom for finish().
|
|
49
|
+
// EVOLVER_RECALL_TIMEOUT_MS is the Hub SEARCH budget only (clamped well under
|
|
50
|
+
// the absolute deadline). The actual budget handed to the search is computed
|
|
51
|
+
// DYNAMICALLY at search-start as (deadline - already-elapsed - safety), so
|
|
52
|
+
// slow stdin/require can never let the search run past the watchdog (which
|
|
53
|
+
// would otherwise spuriously return {} — Bugbot #183 medium findings).
|
|
54
|
+
const T0 = Date.now();
|
|
55
|
+
const ABSOLUTE_DEADLINE_MS = 3300; // watchdog; strictly < host timeout (5000ms)
|
|
56
|
+
const SEARCH_SAFETY_MS = 250; // leave room for finish() after the search
|
|
57
|
+
const MIN_SEARCH_MS = 300; // below this, skip the search (finish {})
|
|
58
|
+
|
|
59
|
+
function getSearchBudgetMs() {
|
|
60
|
+
const n = parseInt(process.env.EVOLVER_RECALL_TIMEOUT_MS, 10);
|
|
61
|
+
// Hard cap at 2800 so even a max-budget search starts and ends before the
|
|
62
|
+
// 3300ms watchdog under any realistic startup cost.
|
|
63
|
+
return Number.isFinite(n) && n >= 500 && n <= 2800 ? n : 2000;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getMode() {
|
|
67
|
+
const v = String(process.env.EVOLVER_RECALL_MODE || '').toLowerCase().trim();
|
|
68
|
+
return v === 'shadow' || v === 'enforce' ? v : 'off';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let handled = false;
|
|
72
|
+
let watchdog = null;
|
|
73
|
+
|
|
74
|
+
// Single-writer latch: exactly one stdout write, exactly one exit. Mirrors the
|
|
75
|
+
// proven pattern in evolver-signal-detect.js / evolver-session-end.js.
|
|
76
|
+
function finish(obj) {
|
|
77
|
+
if (handled) return;
|
|
78
|
+
handled = true;
|
|
79
|
+
if (watchdog) { try { clearTimeout(watchdog); } catch (_) {} }
|
|
80
|
+
try { process.stdout.write(JSON.stringify(obj || {})); } catch (_) {}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function main() {
|
|
85
|
+
const mode = getMode();
|
|
86
|
+
|
|
87
|
+
// Absolute watchdog, armed at process entry and INDEPENDENT of the search
|
|
88
|
+
// budget. Fires at ABSOLUTE_DEADLINE_MS (3300ms) — strictly under the host's
|
|
89
|
+
// timeout (5000ms) so the host can never kill us mid-write. If stdin never
|
|
90
|
+
// closes OR anything hangs, we emit {} and exit cleanly first.
|
|
91
|
+
watchdog = setTimeout(() => finish({}), ABSOLUTE_DEADLINE_MS);
|
|
92
|
+
|
|
93
|
+
let buf = '';
|
|
94
|
+
try {
|
|
95
|
+
process.stdin.setEncoding('utf8');
|
|
96
|
+
} catch (_) { /* some hosts pass no stdin */ }
|
|
97
|
+
process.stdin.on('data', (c) => { buf += c; });
|
|
98
|
+
process.stdin.on('error', () => finish({}));
|
|
99
|
+
process.stdin.on('end', () => {
|
|
100
|
+
if (handled) return;
|
|
101
|
+
|
|
102
|
+
// off: do NOT even parse the prompt body (privacy: nothing read/sent).
|
|
103
|
+
if (mode === 'off') return finish({});
|
|
104
|
+
|
|
105
|
+
let prompt = '';
|
|
106
|
+
let sessionId = '';
|
|
107
|
+
try {
|
|
108
|
+
const input = buf.trim() ? JSON.parse(buf) : {};
|
|
109
|
+
prompt = String(input.prompt || '').trim();
|
|
110
|
+
sessionId = String(input.session_id || input.sessionId || '').trim();
|
|
111
|
+
} catch (_) {
|
|
112
|
+
return finish({});
|
|
113
|
+
}
|
|
114
|
+
if (prompt.length < 8) return finish({}); // trivial prompt -> skip
|
|
115
|
+
|
|
116
|
+
// Heavy require INSIDE try/catch: a broken require graph must fail open,
|
|
117
|
+
// not crash the user's prompt (this is also why the e2e test runs the
|
|
118
|
+
// copied hook with mode=off and asserts exit 0 + parseable stdout).
|
|
119
|
+
let core;
|
|
120
|
+
try {
|
|
121
|
+
const { findEvolverRoot } = require('./_runtimePaths');
|
|
122
|
+
const root = findEvolverRoot();
|
|
123
|
+
if (!root) return finish({});
|
|
124
|
+
core = require(path.join(root, 'src', 'gep', 'recallInject.js'));
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return finish({});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pass the ABSOLUTE deadline (T0 + watchdog window) plus the configured
|
|
130
|
+
// search cap. The core bounds the Hub call by the time REMAINING to that
|
|
131
|
+
// deadline (minus its own post-await safety margin) and runs local-gene
|
|
132
|
+
// disk I/O BEFORE the Hub await — so neither the Hub search nor the
|
|
133
|
+
// post-processing can overrun the watchdog (Bugbot #183: slow startup or a
|
|
134
|
+
// budget-eating Hub call must not let the timer fire mid-work). If too
|
|
135
|
+
// little time remains even before starting, skip and fail open.
|
|
136
|
+
const elapsed = Date.now() - T0;
|
|
137
|
+
const remaining = ABSOLUTE_DEADLINE_MS - elapsed - SEARCH_SAFETY_MS;
|
|
138
|
+
if (remaining < MIN_SEARCH_MS) return finish({});
|
|
139
|
+
const deadlineMs = T0 + ABSOLUTE_DEADLINE_MS;
|
|
140
|
+
|
|
141
|
+
Promise.resolve()
|
|
142
|
+
.then(() => core.recallForTask({ prompt, mode, sessionId, timeoutMs: getSearchBudgetMs(), deadlineMs }))
|
|
143
|
+
.then((r) => {
|
|
144
|
+
if (r && r.inject && r.text) {
|
|
145
|
+
// Emit BOTH shapes:
|
|
146
|
+
// - nested hookSpecificOutput.additionalContext is the DOCUMENTED
|
|
147
|
+
// canonical UserPromptSubmit injection shape (system-reminder,
|
|
148
|
+
// no transcript noise).
|
|
149
|
+
// - flat additionalContext / agent_message match the in-repo
|
|
150
|
+
// precedent (session-start.js) for hosts that read the flat key.
|
|
151
|
+
// Extra keys are tolerated/ignored by hosts; whichever wins, the
|
|
152
|
+
// other is harmless.
|
|
153
|
+
return finish({
|
|
154
|
+
agent_message: r.text,
|
|
155
|
+
additionalContext: r.text,
|
|
156
|
+
hookSpecificOutput: {
|
|
157
|
+
hookEventName: 'UserPromptSubmit',
|
|
158
|
+
additionalContext: r.text,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// shadow (logged inside the core) and no-match both inject nothing.
|
|
163
|
+
return finish({});
|
|
164
|
+
})
|
|
165
|
+
.catch(() => finish({}));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (require.main === module) {
|
|
170
|
+
main();
|
|
171
|
+
} else {
|
|
172
|
+
module.exports = { getMode, getSearchBudgetMs };
|
|
173
|
+
}
|
package/src/atp/atpExecute.js
CHANGED
|
@@ -59,7 +59,7 @@ function _buildGene(capabilities, signals) {
|
|
|
59
59
|
: ['atp_task'];
|
|
60
60
|
const gene = {
|
|
61
61
|
type: 'Gene',
|
|
62
|
-
schema_version: '1.0',
|
|
62
|
+
schema_version: '1.0.0',
|
|
63
63
|
id: 'gene_atp_answer_' + caps.sort().join('_').slice(0, 40),
|
|
64
64
|
summary: 'Deliver an ATP task answer for capabilities: ' + caps.join(', '),
|
|
65
65
|
signals_match: sig,
|
|
@@ -69,6 +69,9 @@ function _buildGene(capabilities, signals) {
|
|
|
69
69
|
'Produce a concrete, actionable answer addressing the question directly.',
|
|
70
70
|
'Return the answer as Capsule content for verifiable delivery.',
|
|
71
71
|
],
|
|
72
|
+
// gep-sdk Gene schema requires `constraints`; an ATP answer edits no
|
|
73
|
+
// files, so the blast radius is empty rather than left unbounded.
|
|
74
|
+
constraints: { max_files: 0, forbidden_paths: [] },
|
|
72
75
|
validation: [
|
|
73
76
|
'Answer is non-empty and directly addresses the buyer question.',
|
|
74
77
|
'Answer references the requested capabilities where relevant.',
|
|
@@ -86,7 +89,7 @@ function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, s
|
|
|
86
89
|
|| 'ATP merchant delivery for order ' + String(orderId || '').slice(0, 24);
|
|
87
90
|
const capsule = {
|
|
88
91
|
type: 'Capsule',
|
|
89
|
-
schema_version: '1.0',
|
|
92
|
+
schema_version: '1.0.0',
|
|
90
93
|
id: 'capsule_atp_' + String(orderId || taskId || Date.now()).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 40),
|
|
91
94
|
trigger: sig,
|
|
92
95
|
gene: gene.id,
|
|
@@ -96,11 +99,21 @@ function _buildCapsule({ gene, answer, summary, orderId, taskId, capabilities, s
|
|
|
96
99
|
outcome: { status: 'success', score: confidence },
|
|
97
100
|
env_fingerprint: { platform: process.platform, arch: process.arch, runtime: 'evolver-atp' },
|
|
98
101
|
content: answer,
|
|
99
|
-
|
|
100
|
-
atp
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
// 'generated' is the gep-sdk source_type enum value for a freshly
|
|
103
|
+
// produced asset; the ATP-specific provenance rides in `a2a.atp` below.
|
|
104
|
+
source_type: 'generated',
|
|
105
|
+
// The order/task association MUST live under `a2a`, not as a top-level
|
|
106
|
+
// `atp` key: the Hub's payload sanitizer allow-lists `a2a` but not `atp`
|
|
107
|
+
// (CAPSULE_ALLOWED_FIELDS), so a top-level `atp` was being silently
|
|
108
|
+
// stripped on publish and the association never reached the Hub. `a2a`
|
|
109
|
+
// is also an open object in gep-sdk's Capsule schema, so this keeps the
|
|
110
|
+
// bundle GEP-valid.
|
|
111
|
+
a2a: {
|
|
112
|
+
atp: {
|
|
113
|
+
order_id: orderId || null,
|
|
114
|
+
task_id: taskId || null,
|
|
115
|
+
capabilities: caps,
|
|
116
|
+
},
|
|
104
117
|
},
|
|
105
118
|
};
|
|
106
119
|
capsule.asset_id = computeAssetId(capsule);
|