@evomap/evolver 1.88.1 → 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.
Files changed (59) hide show
  1. package/index.js +148 -3
  2. package/package.json +2 -1
  3. package/src/adapters/claudeCode.js +21 -1
  4. package/src/adapters/hookAdapter.js +4 -2
  5. package/src/adapters/scripts/evolver-session-start.js +14 -10
  6. package/src/adapters/scripts/evolver-task-recall.js +173 -0
  7. package/src/atp/atpExecute.js +20 -7
  8. package/src/atp/cli.js +17 -9
  9. package/src/atp/protocol.js +41 -0
  10. package/src/config.js +23 -0
  11. package/src/evolve/guards.js +1 -1
  12. package/src/evolve/pipeline/collect.js +1 -1
  13. package/src/evolve/pipeline/dispatch.js +1 -1
  14. package/src/evolve/pipeline/enrich.js +1 -1
  15. package/src/evolve/pipeline/hub.js +1 -1
  16. package/src/evolve/pipeline/select.js +1 -1
  17. package/src/evolve/pipeline/signals.js +1 -1
  18. package/src/evolve/utils.js +1 -1
  19. package/src/evolve.js +1 -1
  20. package/src/forceUpdate.js +108 -3
  21. package/src/gep/a2aProtocol.js +1 -1
  22. package/src/gep/assetCallLog.js +40 -1
  23. package/src/gep/autoDistillConv.js +1 -1
  24. package/src/gep/autoDistillLlm.js +1 -1
  25. package/src/gep/candidateEval.js +1 -1
  26. package/src/gep/candidates.js +1 -1
  27. package/src/gep/contentHash.js +1 -1
  28. package/src/gep/conversationSniffer.js +1 -1
  29. package/src/gep/crypto.js +1 -1
  30. package/src/gep/curriculum.js +1 -1
  31. package/src/gep/deviceId.js +1 -1
  32. package/src/gep/envFingerprint.js +1 -1
  33. package/src/gep/epigenetics.js +1 -1
  34. package/src/gep/execBridge.js +1 -1
  35. package/src/gep/explore.js +1 -1
  36. package/src/gep/hash.js +1 -1
  37. package/src/gep/hubFetch.js +1 -1
  38. package/src/gep/hubReview.js +1 -1
  39. package/src/gep/hubSearch.js +1 -1
  40. package/src/gep/hubVerify.js +1 -1
  41. package/src/gep/learningSignals.js +1 -1
  42. package/src/gep/memoryGraph.js +1 -1
  43. package/src/gep/memoryGraphAdapter.js +1 -1
  44. package/src/gep/mutation.js +1 -1
  45. package/src/gep/narrativeMemory.js +1 -1
  46. package/src/gep/openPRRegistry.js +1 -1
  47. package/src/gep/personality.js +1 -1
  48. package/src/gep/policyCheck.js +1 -1
  49. package/src/gep/prompt.js +1 -1
  50. package/src/gep/recallInject.js +1 -0
  51. package/src/gep/recallVerifier.js +1 -1
  52. package/src/gep/reflection.js +1 -1
  53. package/src/gep/selector.js +1 -1
  54. package/src/gep/skillDistiller.js +1 -1
  55. package/src/gep/solidify.js +1 -1
  56. package/src/gep/strategy.js +1 -1
  57. package/src/gep/workspaceKeychain.js +1 -1
  58. package/src/proxy/index.js +22 -1
  59. package/src/proxy/lifecycle/manager.js +456 -2
package/index.js CHANGED
@@ -1760,8 +1760,8 @@ async function main() {
1760
1760
  console.error('[exec] --max-cycles requires a value (e.g. --max-cycles 5 or --max-cycles=5)');
1761
1761
  process.exit(2);
1762
1762
  }
1763
- if (!['claude-code', 'openclaw'].includes(harness)) {
1764
- console.error(`[exec] unknown --harness '${harness}' (expected claude-code | openclaw)`);
1763
+ if (!['claude-code', 'openclaw', 'codex', 'opencode'].includes(harness)) {
1764
+ console.error(`[exec] unknown --harness '${harness}' (expected claude-code | openclaw | codex | opencode)`);
1765
1765
  process.exit(2);
1766
1766
  }
1767
1767
  try {
@@ -2763,8 +2763,153 @@ async function main() {
2763
2763
  process.exit(1);
2764
2764
  }
2765
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
+
2766
2907
  } else {
2767
- 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)
2768
2913
  - fetch flags:
2769
2914
  - --skill=<id> | -s <id> (skill ID to download)
2770
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.1",
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
- // TTL throttle keyed by an arbitrary string, persisted in session-start-state
203
- // .json. Returns true if `key` fired within the last `ttlMs` (caller should
204
- // suppress); otherwise records "now" for `key` and returns false. Best-effort:
205
- // a state read/write failure just means no throttling (fail open). Shared by
206
- // the Kiro per-prompt dedup and the non-git notice so both age out of the same
207
- // file (entries older than 24h are pruned on write).
208
- function throttled(key, ttlMs) {
209
- const statePath = getDedupStatePath();
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
+ }
@@ -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
- source_type: 'atp_task_executor',
100
- atp: {
101
- order_id: orderId || null,
102
- task_id: taskId || null,
103
- capabilities: caps,
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);
package/src/atp/cli.js CHANGED
@@ -14,6 +14,14 @@
14
14
  // injectable for tests. Each runner returns a Promise that resolves to
15
15
  // { exitCode: number, output?: string, data?: object }.
16
16
 
17
+ const {
18
+ ATP_VERIFY_MODES,
19
+ ATP_VERIFY_ACTIONS,
20
+ ATP_ROUTING_MODES,
21
+ ATP_PROOF_STATUSES,
22
+ ATP_ROLES,
23
+ } = require('./protocol');
24
+
17
25
  function _parseNamed(args, longFlag, shortFlag) {
18
26
  const long = args.findIndex(a => typeof a === 'string' && (a === longFlag || a.startsWith(longFlag + '=')));
19
27
  if (long !== -1) {
@@ -83,11 +91,11 @@ function parseBuyArgs(args) {
83
91
 
84
92
  function parseOrdersArgs(args) {
85
93
  const role = _parseNamed(args, '--role', null);
86
- if (role && !['consumer', 'merchant'].includes(role)) {
87
- return { ok: false, error: 'invalid --role: ' + role + ' (expected consumer|merchant)' };
94
+ if (role && !ATP_ROLES.includes(role)) {
95
+ return { ok: false, error: 'invalid --role: ' + role + ' (expected ' + ATP_ROLES.join('|') + ')' };
88
96
  }
89
97
  const status = _parseNamed(args, '--status', null);
90
- if (status && !['pending', 'verified', 'disputed', 'settled'].includes(status)) {
98
+ if (status && !ATP_PROOF_STATUSES.includes(status)) {
91
99
  return { ok: false, error: 'invalid --status: ' + status };
92
100
  }
93
101
  const limitRaw = _parseNamed(args, '--limit', null);
@@ -112,8 +120,8 @@ function parseVerifyArgs(args) {
112
120
  return { ok: false, error: 'verify requires <orderId>' };
113
121
  }
114
122
  const action = _parseNamed(args, '--action', null) || 'confirm';
115
- if (!['confirm', 'ai_judge'].includes(action)) {
116
- return { ok: false, error: 'invalid --action: ' + action + ' (expected confirm|ai_judge)' };
123
+ if (!ATP_VERIFY_ACTIONS.includes(action)) {
124
+ return { ok: false, error: 'invalid --action: ' + action + ' (expected ' + ATP_VERIFY_ACTIONS.join('|') + ')' };
117
125
  }
118
126
  return { ok: true, opts: { orderId, action } };
119
127
  }
@@ -324,11 +332,11 @@ async function runAtp(opts, deps) {
324
332
  function printUsage() {
325
333
  return [
326
334
  'ATP subcommands:',
327
- ' evolver buy <caps> [--budget=N] [--question "..."] [--routing=fastest|cheapest|auction|swarm]',
328
- ' [--verify=auto|ai_judge|bilateral] [--no-wait] [--timeout=<seconds>]',
329
- ' evolver orders [--role=consumer|merchant] [--status=pending|verified|disputed|settled]',
335
+ ' evolver buy <caps> [--budget=N] [--question "..."] [--routing=' + ATP_ROUTING_MODES.join('|') + ']',
336
+ ' [--verify=' + ATP_VERIFY_MODES.join('|') + '] [--no-wait] [--timeout=<seconds>]',
337
+ ' evolver orders [--role=' + ATP_ROLES.join('|') + '] [--status=' + ATP_PROOF_STATUSES.join('|') + ']',
330
338
  ' [--limit=N] [--json]',
331
- ' evolver verify <orderId> [--action=confirm|ai_judge]',
339
+ ' evolver verify <orderId> [--action=' + ATP_VERIFY_ACTIONS.join('|') + ']',
332
340
  ' evolver atp <enable|disable|status> -- manage auto-spend consent',
333
341
  ].join('\n');
334
342
  }
@@ -0,0 +1,41 @@
1
+ // Protocol-level enum constants for the Agent Transaction Protocol.
2
+ //
3
+ // These values (verify modes, routing modes, proof statuses, roles,
4
+ // execution modes) live in @evomap/atp-sdk. This module is a thin
5
+ // CommonJS facade so callsites in src/atp/ can `require('./protocol')`
6
+ // for the authoritative sets instead of re-spelling the literals.
7
+ //
8
+ // Why move them out: ATP is the wire contract between this engine, the
9
+ // EvoMap Hub, and (in future) evox-Rust. Hand-maintaining the allowed
10
+ // value sets in each implementation is exactly the drift that the
11
+ // v1.80.8 "explore" enum incident taught us to avoid for GEP. The ATP
12
+ // contract is extracted into its own SDK before a second runtime wires
13
+ // in, while it is still cheap. If you find yourself writing an enum
14
+ // list literal here again (e.g. ['pending','verified',...]), stop --
15
+ // import the constant from this facade instead, and bump
16
+ // @evomap/atp-sdk if the set itself needs to change.
17
+ //
18
+ // Implementation note: @evomap/atp-sdk is published as ESM
19
+ // (`"type": "module"`). Node supports `require()` of synchronous ESM
20
+ // packages on 22.12.0 and later. The SDK itself stays permissive
21
+ // (`engines.node >=18`) so `import`-based consumers on 18/20 aren't
22
+ // blocked; the `>=22.12` guarantee that makes the require() below work
23
+ // is pinned in THIS package's (evolver's) `engines.node`, not the SDK's.
24
+
25
+ const {
26
+ ATP_VERIFY_MODES,
27
+ ATP_VERIFY_ACTIONS,
28
+ ATP_ROUTING_MODES,
29
+ ATP_PROOF_STATUSES,
30
+ ATP_ROLES,
31
+ ATP_EXECUTION_MODES,
32
+ } = require('@evomap/atp-sdk');
33
+
34
+ module.exports = {
35
+ ATP_VERIFY_MODES,
36
+ ATP_VERIFY_ACTIONS,
37
+ ATP_ROUTING_MODES,
38
+ ATP_PROOF_STATUSES,
39
+ ATP_ROLES,
40
+ ATP_EXECUTION_MODES,
41
+ };