@evomap/evolver 1.89.18 → 1.89.19

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 (64) hide show
  1. package/index.js +183 -1
  2. package/package.json +7 -2
  3. package/src/adapters/claudeCode.js +8 -39
  4. package/src/adapters/hookAdapter.js +68 -5
  5. package/src/adapters/scripts/evolver-session-start.js +98 -0
  6. package/src/evolve/guards.js +1 -1
  7. package/src/evolve/pipeline/collect.js +1 -1
  8. package/src/evolve/pipeline/dispatch.js +1 -1
  9. package/src/evolve/pipeline/enrich.js +1 -1
  10. package/src/evolve/pipeline/hub.js +1 -1
  11. package/src/evolve/pipeline/select.js +1 -1
  12. package/src/evolve/pipeline/signals.js +1 -1
  13. package/src/evolve/utils.js +1 -1
  14. package/src/evolve.js +1 -1
  15. package/src/gep/a2aProtocol.js +1 -1
  16. package/src/gep/antiAbuseTelemetry.js +1 -1
  17. package/src/gep/autoDistillConv.js +1 -1
  18. package/src/gep/autoDistillLlm.js +1 -1
  19. package/src/gep/candidateEval.js +1 -1
  20. package/src/gep/candidates.js +1 -1
  21. package/src/gep/contentHash.js +1 -1
  22. package/src/gep/conversationDistiller.js +1 -1
  23. package/src/gep/conversationSniffer.js +1 -1
  24. package/src/gep/crypto.js +1 -1
  25. package/src/gep/curriculum.js +1 -1
  26. package/src/gep/deviceId.js +1 -1
  27. package/src/gep/envFingerprint.js +1 -1
  28. package/src/gep/epigenetics.js +1 -1
  29. package/src/gep/execBridge.js +1 -1
  30. package/src/gep/explore.js +1 -1
  31. package/src/gep/hash.js +1 -1
  32. package/src/gep/hubFetch.js +1 -1
  33. package/src/gep/hubReview.js +1 -1
  34. package/src/gep/hubSearch.js +1 -1
  35. package/src/gep/hubVerify.js +1 -1
  36. package/src/gep/learningSignals.js +1 -1
  37. package/src/gep/memoryGraph.js +1 -1
  38. package/src/gep/memoryGraphAdapter.js +1 -1
  39. package/src/gep/mutation.js +1 -1
  40. package/src/gep/narrativeMemory.js +1 -1
  41. package/src/gep/openPRRegistry.js +1 -1
  42. package/src/gep/personality.js +1 -1
  43. package/src/gep/policyCheck.js +1 -1
  44. package/src/gep/prompt.js +1 -1
  45. package/src/gep/recallInject.js +1 -1
  46. package/src/gep/recallVerifier.js +1 -1
  47. package/src/gep/reflection.js +1 -1
  48. package/src/gep/savingsCore.js +1 -1
  49. package/src/gep/selector.js +1 -1
  50. package/src/gep/skill2gep.js +329 -43
  51. package/src/gep/skill2gepAudit.js +303 -0
  52. package/src/gep/skillDistiller.js +1 -1
  53. package/src/gep/solidify.js +1 -1
  54. package/src/gep/strategy.js +1 -1
  55. package/src/gep/tokenSavings.js +1 -1
  56. package/src/gep/trajectoryExport.js +1 -0
  57. package/src/gep/workspaceKeychain.js +1 -1
  58. package/src/proxy/extensions/traceControl.js +1 -1
  59. package/src/proxy/index.js +3 -0
  60. package/src/proxy/inject.js +1 -1
  61. package/src/proxy/router/messages_route.js +52 -1
  62. package/src/proxy/sync/outbound.js +7 -2
  63. package/src/proxy/trace/extractor.js +1 -1
  64. package/src/proxy/trace/usage.js +1 -1
package/index.js CHANGED
@@ -2817,6 +2817,165 @@ async function main() {
2817
2817
  console.log('');
2818
2818
  }
2819
2819
 
2820
+ } else if (command === 'trajectory-export') {
2821
+ const allowPartial = args.includes('--allow-partial');
2822
+ const runtimeSessions = args.includes('--runtime-sessions');
2823
+ // Strict-by-default marking gate (v1): runtime-session discovery only
2824
+ // collects sessions evolver actively marked (session-start hook), minus
2825
+ // those the gateway already captured. These flags open each gate back up.
2826
+ const includeUnmarked = args.includes('--include-unmarked');
2827
+ const includeGatewayCaptured = args.includes('--include-gateway-captured');
2828
+ try {
2829
+ const optionValue = (name) => {
2830
+ let value;
2831
+ for (let i = 0; i < args.length; i += 1) {
2832
+ const arg = args[i];
2833
+ if (arg === name) {
2834
+ if (i + 1 >= args.length || String(args[i + 1]).startsWith('--')) {
2835
+ throw new Error('missing value for ' + name);
2836
+ }
2837
+ value = String(args[i + 1]);
2838
+ } else if (typeof arg === 'string' && arg.startsWith(name + '=')) {
2839
+ value = arg.slice(name.length + 1);
2840
+ }
2841
+ }
2842
+ return value;
2843
+ };
2844
+ const optionValues = (name) => {
2845
+ const values = [];
2846
+ for (let i = 0; i < args.length; i += 1) {
2847
+ const arg = args[i];
2848
+ if (arg === name) {
2849
+ if (i + 1 >= args.length || String(args[i + 1]).startsWith('--')) {
2850
+ throw new Error('missing value for ' + name);
2851
+ }
2852
+ values.push(String(args[i + 1]));
2853
+ } else if (typeof arg === 'string' && arg.startsWith(name + '=')) {
2854
+ values.push(arg.slice(name.length + 1));
2855
+ }
2856
+ }
2857
+ return values;
2858
+ };
2859
+ const input = optionValue('--input');
2860
+ const output = optionValue('--output');
2861
+ const runtimeSessionDirs = optionValues('--runtime-session-dir');
2862
+ const nodeSecretInline = optionValue('--node-secret');
2863
+ const nodeSecretFile = optionValue('--node-secret-file');
2864
+ const nodeSecretEnv = optionValue('--node-secret-env');
2865
+ const hubPrivateKeyFile = optionValue('--hub-private-key');
2866
+ const nodeSecretKeyringFile = optionValue('--node-secret-keyring');
2867
+ const markedSessionsFile = optionValue('--marked-sessions-file');
2868
+ let nodeSecret;
2869
+ let hubPrivateKey;
2870
+ let nodeSecretKeyring;
2871
+ const nodeSecretSourceCount = [nodeSecretInline, nodeSecretFile, nodeSecretEnv]
2872
+ .filter((value) => value !== undefined).length;
2873
+ if (nodeSecretSourceCount > 1) {
2874
+ throw new Error('use only one node secret source');
2875
+ }
2876
+ if (nodeSecretInline !== undefined) {
2877
+ if (!nodeSecretInline) throw new Error('missing value for --node-secret');
2878
+ if (fs.existsSync(nodeSecretInline)) {
2879
+ try {
2880
+ nodeSecret = fs.readFileSync(nodeSecretInline, 'utf8').trim();
2881
+ } catch (readErr) {
2882
+ const err = new Error('node secret file is not readable: ' + nodeSecretInline);
2883
+ err.cause = readErr;
2884
+ throw err;
2885
+ }
2886
+ if (!nodeSecret) throw new Error('node secret file is empty');
2887
+ } else {
2888
+ nodeSecret = String(nodeSecretInline).trim();
2889
+ if (!nodeSecret) throw new Error('node secret is empty');
2890
+ }
2891
+ }
2892
+ if (nodeSecretFile !== undefined) {
2893
+ if (!nodeSecretFile) throw new Error('missing path for --node-secret-file');
2894
+ try {
2895
+ nodeSecret = fs.readFileSync(nodeSecretFile, 'utf8').trim();
2896
+ } catch (readErr) {
2897
+ const err = new Error('node secret file is not readable: ' + nodeSecretFile);
2898
+ err.cause = readErr;
2899
+ throw err;
2900
+ }
2901
+ if (!nodeSecret) throw new Error('node secret file is empty');
2902
+ }
2903
+ if (nodeSecretEnv !== undefined) {
2904
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(nodeSecretEnv)) {
2905
+ throw new Error('invalid environment variable name for --node-secret-env');
2906
+ }
2907
+ nodeSecret = String(process.env[nodeSecretEnv] || '').trim();
2908
+ if (!nodeSecret) throw new Error('node secret environment variable is empty or unset');
2909
+ }
2910
+ if (hubPrivateKeyFile !== undefined) {
2911
+ if (!hubPrivateKeyFile) throw new Error('missing path for --hub-private-key');
2912
+ try {
2913
+ hubPrivateKey = fs.readFileSync(hubPrivateKeyFile, 'utf8');
2914
+ } catch (readErr) {
2915
+ const err = new Error('hub private key file is not readable: ' + hubPrivateKeyFile);
2916
+ err.cause = readErr;
2917
+ throw err;
2918
+ }
2919
+ if (!String(hubPrivateKey || '').trim()) throw new Error('hub private key file is empty');
2920
+ }
2921
+ if (nodeSecretKeyringFile !== undefined) {
2922
+ if (!nodeSecretKeyringFile) throw new Error('missing path for --node-secret-keyring');
2923
+ try {
2924
+ nodeSecretKeyring = JSON.parse(fs.readFileSync(nodeSecretKeyringFile, 'utf8'));
2925
+ } catch (readErr) {
2926
+ const err = new Error('node secret keyring file is not readable or invalid JSON: ' + nodeSecretKeyringFile);
2927
+ err.cause = readErr;
2928
+ throw err;
2929
+ }
2930
+ }
2931
+ const { writeTrajectories } = require('./src/gep/trajectoryExport');
2932
+ const result = writeTrajectories({
2933
+ input,
2934
+ output,
2935
+ nodeSecret,
2936
+ nodeSecretKeyring,
2937
+ hubPrivateKey,
2938
+ allowPartial,
2939
+ runtimeSessions: runtimeSessions ? true : undefined,
2940
+ runtimeSessionDirs,
2941
+ markedSessionsFile,
2942
+ includeUnmarked: includeUnmarked ? true : undefined,
2943
+ includeGatewayCaptured: includeGatewayCaptured ? true : undefined,
2944
+ });
2945
+ console.log('[trajectory-export] Wrote ' + result.trajectories.length + ' trajector' + (result.trajectories.length === 1 ? 'y' : 'ies') + ' to ' + result.outputPath);
2946
+ console.log('[trajectory-export] Read ' + result.rowsRead + ' trace row(s) and ' + (result.sessionTurnsRead || 0) + ' session turn(s) from ' + (result.filesRead || 0) + ' file(s).');
2947
+ if (result.sessionFilesRead > 0) {
2948
+ console.log('[trajectory-export] Converted ' + result.sessionFilesRead + ' runtime session file(s).');
2949
+ }
2950
+ if (result.runtimeSessionDiscovery && result.runtimeSessionDiscovery.enabled) {
2951
+ console.log('[trajectory-export] Runtime session discovery is local-only; no Hub upload is performed.');
2952
+ console.log('[trajectory-export] Runtime discovery scanned '
2953
+ + result.runtimeSessionDiscovery.dirsScanned + ' Codex/Claude dir(s), matched '
2954
+ + result.runtimeSessionDiscovery.filesMatched + ' workspace session file(s).');
2955
+ const mg = result.runtimeSessionDiscovery.markGate;
2956
+ if (mg) {
2957
+ console.log('[trajectory-export] Marking gate: enforce_marked=' + mg.enforceMarked
2958
+ + ', exclude_gateway_captured=' + mg.excludeGatewayCaptured
2959
+ + '; marked_registry=' + mg.markedSessionCount
2960
+ + ', gateway_captured=' + mg.gatewayCapturedCount
2961
+ + ', excluded_unmarked=' + mg.excludedByMark
2962
+ + ', excluded_gateway=' + mg.excludedByGateway + '.');
2963
+ }
2964
+ }
2965
+ if (result.stats) {
2966
+ console.log('[trajectory-export] Scanned ' + result.stats.rowsScanned
2967
+ + ' row(s); encrypted=' + result.stats.encryptedRows
2968
+ + ', skipped_missing_secret=' + result.stats.skippedMissingSecret
2969
+ + ', decrypt_failures=' + result.stats.decryptFailures
2970
+ + ', invalid_json=' + result.stats.invalidJson
2971
+ + ', session_invalid_json=' + (result.stats.sessionInvalidJson || 0)
2972
+ + ', non_prism=' + result.stats.nonPrismSkipped + '.');
2973
+ }
2974
+ } catch (error) {
2975
+ console.error('[trajectory-export] Failed: ' + (error && error.message || error));
2976
+ process.exit(1);
2977
+ }
2978
+
2820
2979
  } else if (command === 'webui') {
2821
2980
  const portFlag = args.find(a => typeof a === 'string' && a.startsWith('--port='));
2822
2981
  const port = portFlag ? Number(portFlag.slice('--port='.length)) : undefined;
@@ -3296,7 +3455,7 @@ async function main() {
3296
3455
  }
3297
3456
 
3298
3457
  } else {
3299
- console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|webui|setup-hooks|reuse|publish|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
3458
+ console.log(`Usage: node index.js [run|/evolve|login|logout|proxy-token|solidify|review|distill|fetch|sync|asset-log|trajectory-export|webui|setup-hooks|reuse|publish|recipe|buy|orders|verify|atp|atp-complete|experiment] [--loop]
3300
3459
  - login (authorize this device via the hub, gh-auth-login style; stores an OAuth token used instead of node_secret)
3301
3460
  - logout (remove the stored OAuth token)
3302
3461
  - proxy-token (print the local proxy bearer token for command-backed client auth)
@@ -3345,6 +3504,29 @@ async function main() {
3345
3504
  - --last=<N> (show last N entries)
3346
3505
  - --since=<ISO_date> (entries after date)
3347
3506
  - --json (raw JSON output)
3507
+ - trajectory-export flags:
3508
+ - --input=<path>|--input <path>
3509
+ (proxy trace JSONL, Claude/Codex/Cursor session JSONL, or directory; default: platform trace file)
3510
+ - --output=<path>|--output <path>
3511
+ (output JSONL; default: ./coding-trajectories.jsonl)
3512
+ - --runtime-sessions (local-only: also scan current-workspace Codex/Claude JSONL from ~/.codex/sessions and ~/.claude/projects; env: EVOLVER_TRAJECTORY_RUNTIME_SESSIONS=1)
3513
+ - --runtime-session-dir=<path>|--runtime-session-dir <path>
3514
+ (local-only: extra Codex/Claude runtime session directory; may be repeated)
3515
+ - --include-unmarked (collect runtime sessions even if evolver did not actively mark them; default: strict — only marked sessions are collected; env: EVOLVER_TRAJECTORY_INCLUDE_UNMARKED=1)
3516
+ - --include-gateway-captured (collect runtime sessions even if the proxy gateway already captured them; default: strict — gateway-captured sessions are skipped; env: EVOLVER_TRAJECTORY_INCLUDE_GATEWAY_CAPTURED=1)
3517
+ - --marked-sessions-file=<path>|--marked-sessions-file <path>
3518
+ (override the evolver-marked-sessions registry path; default: <trace-dir>/marked-sessions.jsonl; env: EVOLVER_MARKED_SESSIONS_FILE)
3519
+ - --allow-partial (skip unreadable encrypted rows instead of failing the whole export)
3520
+ - --node-secret=<hex|path>|--node-secret <hex|path>
3521
+ (read node secret from an existing local file, otherwise use the literal value)
3522
+ - --node-secret-file=<path>|--node-secret-file <path>
3523
+ (read node secret from a local file)
3524
+ - --node-secret-env=<name>|--node-secret-env <name>
3525
+ (read node secret from an environment variable)
3526
+ - --node-secret-keyring=<path>|--node-secret-keyring <path>
3527
+ (read versioned node secrets from a JSON file)
3528
+ - --hub-private-key=<path>|--hub-private-key <path>
3529
+ (decrypt hub_key_envelope trace rows with a local private key)
3348
3530
  - webui flags:
3349
3531
  - --port=<N> (local Web UI port, default 19821)
3350
3532
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver",
3
- "version": "1.89.18",
3
+ "version": "1.89.19",
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": {
@@ -41,7 +41,7 @@
41
41
  "@evomap/atp-sdk": "^0.1.0",
42
42
  "@evomap/gep-sdk": "^1.5.0",
43
43
  "dotenv": "^16.4.7",
44
- "undici": "^7.0.0"
44
+ "undici": "^7.28.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "javascript-obfuscator": "^5.4.1"
@@ -49,6 +49,11 @@
49
49
  "optionalDependencies": {
50
50
  "@napi-rs/keyring": "^1.1.6"
51
51
  },
52
+ "overrides": {
53
+ "@vercel/blob": {
54
+ "undici": "6.27.0"
55
+ }
56
+ },
52
57
  "files": [
53
58
  "assets/",
54
59
  "index.js",
@@ -1,41 +1,20 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand } = require('./hookAdapter');
3
+ const { mergeJsonFile, copyHookScripts, appendSectionToFile, removeHookScripts, removeMarkedSection, assertSafeConfigDir, isEvolverHookCommand, buildSafeNodeHookCommand } = require('./hookAdapter');
4
4
 
5
5
  const HOOK_SCRIPTS_DIR_NAME = 'hooks';
6
6
  const EVOLVER_MARKER = '<!-- evolver-evolution-memory -->';
7
7
 
8
- function shellSingleQuote(value) {
9
- return `'${String(value).replace(/'/g, "'\\''")}'`;
10
- }
11
-
12
- function quoteEvalScript(script) {
13
- if (process.platform === 'win32') {
14
- if (script.includes('"')) {
15
- throw new Error('unexpected double quote in generated hook wrapper');
16
- }
17
- return `"${script}"`;
18
- }
19
- return shellSingleQuote(script);
20
- }
21
-
22
- function buildSafeNodeHookCommand(scriptPath) {
23
- const encodedPath = Buffer.from(String(scriptPath), 'utf8').toString('base64');
24
- const js = [
25
- "const {spawnSync}=require('child_process')",
26
- `const p=Buffer.from('${encodedPath}','base64').toString('utf8')`,
27
- "const r=spawnSync(process.execPath,[p],{stdio:'inherit',shell:false})",
28
- "if(r.error){throw r.error}",
29
- 'process.exit(r.status == null ? 1 : r.status)',
30
- ].join(';');
31
- return `node -e ${quoteEvalScript(js)} ${path.basename(scriptPath)}`;
32
- }
33
-
34
8
  function buildClaudeHooks(evolverRoot, configRoot) {
9
+ // Resolve hook scripts to an absolute path rooted at the real config dir so
10
+ // the command works regardless of the cwd Claude Code is launched from
11
+ // (#590). Falls back to the legacy relative base only when configRoot is
12
+ // absent (callers should always pass it).
35
13
  const scriptsBase = configRoot
36
- ? path.join(configRoot, '.claude', 'hooks')
14
+ ? path.resolve(configRoot, '.claude', 'hooks')
37
15
  : path.join('.claude', 'hooks');
38
- const hookCommand = (scriptName) => buildSafeNodeHookCommand(path.join(scriptsBase, scriptName));
16
+ const hookCommand = (scriptName) =>
17
+ buildSafeNodeHookCommand(path.join(scriptsBase, scriptName));
39
18
  return {
40
19
  hooks: {
41
20
  SessionStart: [
@@ -116,16 +95,6 @@ function install({ configRoot, evolverRoot, force }) {
116
95
  const claudeMdPath = path.join(configRoot, 'CLAUDE.md');
117
96
  assertSafeConfigDir(claudeDir, '.claude', { subdirs: [HOOK_SCRIPTS_DIR_NAME] });
118
97
 
119
- if (!force && fs.existsSync(settingsPath)) {
120
- try {
121
- const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
122
- if (existing._evolver_managed) {
123
- console.log('[claude-code] Evolver hooks already installed. Use --force to overwrite.');
124
- return { ok: true, skipped: true };
125
- }
126
- } catch { /* proceed */ }
127
- }
128
-
129
98
  fs.mkdirSync(claudeDir, { recursive: true });
130
99
 
131
100
  const hooksCfg = buildClaudeHooks(evolverRoot, configRoot);
@@ -10,6 +10,14 @@ const PLATFORMS = {
10
10
  opencode: { name: 'opencode', configDir: '.opencode', detector: '.opencode' },
11
11
  };
12
12
 
13
+ // Detect the host agent from runtime environment signals, which are far more
14
+ // reliable than scanning for `.claude` / `.cursor` directories when both exist
15
+ // on disk (e.g. under $HOME). Precedence when signals conflict (#590):
16
+ // 1. strong Claude Code signals (set only by the Claude Code runtime)
17
+ // 2. Cursor signals
18
+ // 3. Codex signals
19
+ // 4. CLAUDE_PROJECT_DIR — a weak Claude alias some hosts (incl. Cursor's
20
+ // Claude integration) export, so it loses to explicit Cursor signals.
13
21
  function detectPlatformFromEnv(env = process.env) {
14
22
  const hasStrongClaudeSignal = env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT;
15
23
  if (hasStrongClaudeSignal) {
@@ -20,10 +28,19 @@ function detectPlatformFromEnv(env = process.env) {
20
28
  env.CURSOR_TRACE_ID ||
21
29
  env.CURSOR_SESSION_ID ||
22
30
  env.CURSOR_PROJECT_DIR ||
31
+ env.CURSOR_AGENT ||
23
32
  String(env.TERM_PROGRAM || '').toLowerCase() === 'cursor';
24
33
  if (hasCursorSignal) {
25
34
  return 'cursor';
26
35
  }
36
+ const hasCodexSignal =
37
+ env.CODEX_THREAD_ID ||
38
+ env.CODEX_SHELL ||
39
+ env.CODEX_CI ||
40
+ env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE;
41
+ if (hasCodexSignal) {
42
+ return 'codex';
43
+ }
27
44
  if (env.CLAUDE_PROJECT_DIR) {
28
45
  return 'claude-code';
29
46
  }
@@ -45,6 +62,37 @@ function detectPlatform(cwd) {
45
62
  return null;
46
63
  }
47
64
 
65
+ // Quote `value` as a single shell argument for the host that runs hook
66
+ // `command` strings (POSIX sh, or cmd.exe on Windows).
67
+ function shellQuoteForHost(value) {
68
+ const raw = String(value);
69
+ if (process.platform === 'win32') {
70
+ return `"${raw.replace(/"/g, '\\"')}"`;
71
+ }
72
+ return `'${raw.replace(/'/g, "'\\''")}'`;
73
+ }
74
+
75
+ // Build a hook `command` that runs `scriptPath` with node WITHOUT exposing the
76
+ // path bytes to the shell. The absolute path is base64-encoded into a `node -e`
77
+ // wrapper that decodes it and spawns the real script with `shell:false`, so
78
+ // spaces / `$()` / backticks / `%VAR%` in the path can never be expanded by sh
79
+ // or cmd.exe (#590). The plaintext basename is appended as a final, inert arg
80
+ // so uninstall/reinstall can still match evolver-owned hooks by command
81
+ // substring (see isEvolverHookCommand) even though the path itself is encoded.
82
+ //
83
+ // Shared across adapters so there is exactly one implementation to audit.
84
+ function buildSafeNodeHookCommand(scriptPath) {
85
+ const encodedPath = Buffer.from(String(scriptPath), 'utf8').toString('base64');
86
+ const js = [
87
+ "const{spawnSync}=require('child_process')",
88
+ `const p=Buffer.from('${encodedPath}','base64').toString('utf8')`,
89
+ "const r=spawnSync(process.execPath,[p],{stdio:'inherit',shell:false})",
90
+ "if(r.error){console.error(r.error.message||String(r.error));process.exit(1)}",
91
+ "process.exit(r.status==null?1:r.status)",
92
+ ].join(';');
93
+ return `node -e ${shellQuoteForHost(js)} ${path.basename(scriptPath)}`;
94
+ }
95
+
48
96
  function resolveConfigRoot(platformId, cwd) {
49
97
  const root = cwd || process.cwd();
50
98
  const home = os.homedir();
@@ -97,11 +145,9 @@ function mergeWithHooksUnion(target, source) {
97
145
  const tArr = Array.isArray(target.hooks[event]) ? target.hooks[event] : null;
98
146
  const sArr = Array.isArray(source.hooks[event]) ? source.hooks[event] : null;
99
147
  if (tArr && sArr) {
100
- const isEvolverOwned = (entry) => {
101
- const cmds = collectCommands(entry);
102
- return cmds.some(isEvolverHookCommand);
103
- };
104
- const userEntries = tArr.filter(e => !isEvolverOwned(e));
148
+ const userEntries = tArr
149
+ .map(stripEvolverCommands)
150
+ .filter(Boolean);
105
151
  result.hooks[event] = [...userEntries, ...sArr];
106
152
  }
107
153
  }
@@ -109,6 +155,22 @@ function mergeWithHooksUnion(target, source) {
109
155
  return result;
110
156
  }
111
157
 
158
+ function stripEvolverCommands(entry) {
159
+ if (!entry || typeof entry !== 'object') return entry;
160
+ if (typeof entry.command === 'string' && isEvolverHookCommand(entry.command)) {
161
+ return null;
162
+ }
163
+ if (!Array.isArray(entry.hooks)) return entry;
164
+
165
+ const hooks = entry.hooks.filter(h => {
166
+ const cmd = h && h.command;
167
+ return !(typeof cmd === 'string' && isEvolverHookCommand(cmd));
168
+ });
169
+ if (hooks.length === 0) return null;
170
+ if (hooks.length === entry.hooks.length) return entry;
171
+ return { ...entry, hooks };
172
+ }
173
+
112
174
  // Pull all `command` strings out of an event entry, supporting both flat
113
175
  // shape (Codex: `{type, command}`) and Claude Code matcher shape
114
176
  // (`{matcher, hooks: [{type, command}]}`). Returns [] when neither applies.
@@ -387,6 +449,7 @@ async function setupHooks({ platform, cwd, force, uninstall, evolverRoot } = {})
387
449
  module.exports = {
388
450
  detectPlatformFromEnv,
389
451
  detectPlatform,
452
+ buildSafeNodeHookCommand,
390
453
  resolveConfigRoot,
391
454
  loadAdapter,
392
455
  mergeJsonFile,
@@ -373,6 +373,58 @@ function getNoticeStatePath() {
373
373
  return path.join(dir, 'session-start-notice-state.json');
374
374
  }
375
375
 
376
+ // Resolve the evolver-marked-sessions registry path. This is the v1 "evolver
377
+ // actively marked this session" ledger: only sessions whose session-start hook
378
+ // actually fired (i.e. sessions evolver participated in, after hook install)
379
+ // land here. The trajectory exporter reads it to gate which runtime-session
380
+ // transcripts get collected (strict by default). Lives alongside the proxy
381
+ // trace dir so both collection ledgers share one home. The env override keeps
382
+ // tests hermetic and lets an operator relocate the ledger.
383
+ function getMarkedSessionsPath() {
384
+ if (process.env.EVOLVER_MARKED_SESSIONS_FILE) return process.env.EVOLVER_MARKED_SESSIONS_FILE;
385
+ const dir = process.env.EVOLVER_SETTINGS_DIR
386
+ || process.env.EVOLVER_SESSION_STATE_DIR
387
+ || path.join(os.homedir(), '.evolver');
388
+ return path.join(dir, 'marked-sessions.jsonl');
389
+ }
390
+
391
+ // Pull the tool's session_id out of the hook's stdin payload. Claude Code,
392
+ // Codex, and Cursor all pass `session_id` (Claude Code / Cursor) on the
393
+ // SessionStart stdin JSON; some shapes use `sessionId`. Returns '' when absent
394
+ // so callers can skip the registry write without erroring (Kiro's promptSubmit
395
+ // and hosts that pass no stdin simply don't mark — fail-open by design).
396
+ function _extractHookSessionId(input) {
397
+ if (!input || typeof input !== 'object') return '';
398
+ const raw = input.session_id ?? input.sessionId ?? input.sessionID;
399
+ return typeof raw === 'string' ? raw.trim() : '';
400
+ }
401
+
402
+ // Append one mark record to the registry. Best-effort and idempotent enough for
403
+ // the exporter's needs: the exporter dedupes into a Set, so a session marked on
404
+ // every prompt (Kiro) just appends duplicate lines that collapse on read. We do
405
+ // NOT read-modify-write to dedupe here — that would race across concurrent
406
+ // sessions; append-only is safe and the file is bounded by session count.
407
+ function recordMarkedSession(sessionId, info = {}) {
408
+ const sid = String(sessionId || '').trim();
409
+ if (!sid) return false;
410
+ const file = getMarkedSessionsPath();
411
+ try {
412
+ fs.mkdirSync(path.dirname(file), { recursive: true });
413
+ const record = {
414
+ session_id: sid,
415
+ ...(info.cwd ? { cwd: String(info.cwd) } : {}),
416
+ ...(info.source ? { source: String(info.source) } : {}),
417
+ marked_at: new Date().toISOString(),
418
+ };
419
+ fs.appendFileSync(file, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
420
+ return true;
421
+ } catch (_) {
422
+ // Never let a registry write failure break session-start: the context
423
+ // injection (stdout JSON) must always be emitted.
424
+ return false;
425
+ }
426
+ }
427
+
376
428
  // TTL throttle keyed by an arbitrary string. Returns true if `key` fired within
377
429
  // the last `ttlMs` (caller should suppress); otherwise records "now" for `key`
378
430
  // and returns false. Best-effort: a state read/write failure just means no
@@ -415,7 +467,50 @@ function shouldSkipInjection() {
415
467
  return throttled(process.cwd(), ttlMs, getDedupStatePath());
416
468
  }
417
469
 
470
+ // Drain the hook stdin (session context JSON) before running, so we can read the
471
+ // tool's session_id and mark the session in the registry. The host passes the
472
+ // SessionStart payload on stdin; we bound the wait with a short watchdog and
473
+ // fail-open (mark nothing, still emit context) if stdin never closes or isn't a
474
+ // pipe. This mirrors the stdin-draining pattern in evolver-task-recall.js /
475
+ // evolver-session-end.js.
418
476
  function main() {
477
+ let done = false;
478
+ let buf = '';
479
+ const finishWithInput = (input) => {
480
+ if (done) return;
481
+ done = true;
482
+ try {
483
+ const sessionId = _extractHookSessionId(input);
484
+ if (sessionId) {
485
+ recordMarkedSession(sessionId, {
486
+ cwd: resolveProjectDir(),
487
+ source: String(process.env.EVOLVER_SESSION_SOURCE || (input && input.source) || '').trim() || undefined,
488
+ });
489
+ }
490
+ } catch (_) { /* marking is best-effort; never block injection */ }
491
+ runInjection();
492
+ };
493
+
494
+ // Watchdog: if stdin never ends (host passed no pipe, or hangs), proceed
495
+ // without a session_id rather than stalling the agent's session start.
496
+ const watchdog = setTimeout(() => finishWithInput(null), 1500);
497
+ try {
498
+ process.stdin.setEncoding('utf8');
499
+ } catch (_) { /* some hosts pass no stdin */ }
500
+ process.stdin.on('data', (c) => { buf += c; });
501
+ process.stdin.on('error', () => { clearTimeout(watchdog); finishWithInput(null); });
502
+ process.stdin.on('end', () => {
503
+ clearTimeout(watchdog);
504
+ let input = null;
505
+ try { input = buf.trim() ? JSON.parse(buf) : null; } catch (_) { input = null; }
506
+ finishWithInput(input);
507
+ });
508
+ // Nudge a resume so a paused stdin stream flushes its data/end events; the
509
+ // watchdog covers the case where neither ever arrives (no pipe attached).
510
+ try { process.stdin.resume(); } catch (_) { /* ignore */ }
511
+ }
512
+
513
+ function runInjection() {
419
514
  if (shouldSkipInjection()) {
420
515
  process.stdout.write(JSON.stringify({}));
421
516
  return;
@@ -485,5 +580,8 @@ if (require.main === module) {
485
580
  _proxyExpected,
486
581
  _proxyReachable,
487
582
  _proxyHealthyIfExpected,
583
+ _extractHookSessionId,
584
+ getMarkedSessionsPath,
585
+ recordMarkedSession,
488
586
  };
489
587
  }