harness-evolver 3.0.1 → 3.0.3

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/bin/install.js CHANGED
@@ -72,6 +72,67 @@ function checkCommand(cmd) {
72
72
  }
73
73
  }
74
74
 
75
+ function cleanPreviousInstall(runtimeDir, scope) {
76
+ const baseDir = scope === "local"
77
+ ? path.join(process.cwd(), runtimeDir)
78
+ : path.join(HOME, runtimeDir);
79
+
80
+ const skillsDir = path.join(baseDir, "skills");
81
+ const agentsDir = path.join(baseDir, "agents");
82
+ let cleaned = 0;
83
+
84
+ // Remove ALL evolver/harness-evolver skills (any version)
85
+ if (fs.existsSync(skillsDir)) {
86
+ const ours = ["setup", "evolve", "deploy", "status",
87
+ "init", "architect", "compare", "critic", "diagnose",
88
+ "import-traces", "evolve-v3", "deploy-v3", "status-v3",
89
+ "harness-evolver:init", "harness-evolver:evolve",
90
+ "harness-evolver:status", "harness-evolver:deploy",
91
+ "harness-evolver:compare", "harness-evolver:diagnose",
92
+ "harness-evolver:architect", "harness-evolver:critic",
93
+ "harness-evolver:import-traces"];
94
+ for (const name of ours) {
95
+ const p = path.join(skillsDir, name);
96
+ if (fs.existsSync(p)) {
97
+ fs.rmSync(p, { recursive: true, force: true });
98
+ cleaned++;
99
+ }
100
+ }
101
+ }
102
+
103
+ // Remove ALL evolver/harness-evolver agents
104
+ if (fs.existsSync(agentsDir)) {
105
+ for (const f of fs.readdirSync(agentsDir)) {
106
+ if (f.startsWith("evolver-") || f.startsWith("harness-evolver-")) {
107
+ fs.rmSync(path.join(agentsDir, f), { force: true });
108
+ cleaned++;
109
+ }
110
+ }
111
+ }
112
+
113
+ // Remove old commands/ directory (v1)
114
+ const oldCommandsDir = path.join(baseDir, "commands", "harness-evolver");
115
+ if (fs.existsSync(oldCommandsDir)) {
116
+ fs.rmSync(oldCommandsDir, { recursive: true, force: true });
117
+ cleaned++;
118
+ }
119
+
120
+ // Remove old tools directories
121
+ for (const toolsPath of [
122
+ path.join(HOME, ".evolver", "tools"),
123
+ path.join(HOME, ".harness-evolver"),
124
+ ]) {
125
+ if (fs.existsSync(toolsPath)) {
126
+ fs.rmSync(toolsPath, { recursive: true, force: true });
127
+ cleaned++;
128
+ }
129
+ }
130
+
131
+ if (cleaned > 0) {
132
+ console.log(` ${DIM}Cleaned ${cleaned} items from previous install${RESET}`);
133
+ }
134
+ }
135
+
75
136
  function installSkillsAndAgents(runtimeDir, scope) {
76
137
  const baseDir = scope === "local"
77
138
  ? path.join(process.cwd(), runtimeDir)
@@ -100,13 +161,6 @@ function installSkillsAndAgents(runtimeDir, scope) {
100
161
  }
101
162
  }
102
163
 
103
- // Cleanup old v2 commands/ directory
104
- const oldCommandsDir = path.join(baseDir, "commands", "harness-evolver");
105
- if (fs.existsSync(oldCommandsDir)) {
106
- fs.rmSync(oldCommandsDir, { recursive: true, force: true });
107
- console.log(` ${DIM}Cleaned up old commands/ directory${RESET}`);
108
- }
109
-
110
164
  // Agents
111
165
  const agentsSource = path.join(PLUGIN_ROOT, "agents");
112
166
  if (fs.existsSync(agentsSource)) {
@@ -267,6 +321,15 @@ async function configureOptionalIntegrations(rl) {
267
321
  async function main() {
268
322
  console.log(LOGO);
269
323
 
324
+ // Check if running latest version (npx may cache an old one)
325
+ try {
326
+ const latest = execSync("npm view harness-evolver version", { stdio: "pipe", timeout: 5000 }).toString().trim();
327
+ if (latest && latest !== VERSION) {
328
+ console.log(` ${YELLOW}!${RESET} You're running v${VERSION} but v${latest} is available.`);
329
+ console.log(` Run: ${BOLD}npx harness-evolver@${latest}${RESET} or ${BOLD}npx --yes harness-evolver@latest${RESET}\n`);
330
+ }
331
+ } catch {}
332
+
270
333
  if (!checkPython()) {
271
334
  console.error(` ${RED}ERROR:${RESET} python3 not found. Install Python 3.10+ first.`);
272
335
  process.exit(1);
@@ -317,6 +380,12 @@ async function main() {
317
380
  const scopeAnswer = await ask(rl, `\n ${YELLOW}Choice [1]:${RESET} `);
318
381
  const scope = (scopeAnswer.trim() === "2") ? "local" : "global";
319
382
 
383
+ // Clean previous install (remove ALL old files before installing new ones)
384
+ console.log(`\n ${BOLD}Cleaning previous install${RESET}`);
385
+ for (const runtime of selected) {
386
+ cleanPreviousInstall(runtime.dir, scope);
387
+ }
388
+
320
389
  // Install skills + agents
321
390
  console.log(`\n ${BOLD}Installing skills & agents${RESET}\n`);
322
391
  for (const runtime of selected) {
@@ -325,7 +394,7 @@ async function main() {
325
394
  console.log();
326
395
  }
327
396
 
328
- // Install tools
397
+ // Install tools (fresh — old dir was cleaned above)
329
398
  console.log(` ${BOLD}Installing tools${RESET}`);
330
399
  installTools();
331
400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-evolver",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "LangSmith-native autonomous agent optimization for Claude Code",
5
5
  "author": "Raphael Valdetaro",
6
6
  "license": "MIT",
@@ -11,8 +11,31 @@ Set up the Harness Evolver v3 in a project. Explores the codebase, configures La
11
11
 
12
12
  ## Prerequisites
13
13
 
14
- - `LANGSMITH_API_KEY` must be set. If not: "Set your LangSmith API key: `export LANGSMITH_API_KEY=lsv2_pt_...`"
15
- - Python 3.10+ with `langsmith` and `openevals` packages. If missing:
14
+ Check for LangSmith API key — it can be in the environment, the credentials file, or .env:
15
+
16
+ ```bash
17
+ python3 -c "
18
+ import os, platform
19
+ key = os.environ.get('LANGSMITH_API_KEY', '')
20
+ if not key:
21
+ creds = os.path.expanduser('~/Library/Application Support/langsmith-cli/credentials') if platform.system() == 'Darwin' else os.path.expanduser('~/.config/langsmith-cli/credentials')
22
+ if os.path.exists(creds):
23
+ for line in open(creds):
24
+ if line.strip().startswith('LANGSMITH_API_KEY='):
25
+ key = line.strip().split('=',1)[1].strip()
26
+ if not key and os.path.exists('.env'):
27
+ for line in open('.env'):
28
+ if line.strip().startswith('LANGSMITH_API_KEY=') and not line.strip().startswith('#'):
29
+ key = line.strip().split('=',1)[1].strip().strip('\"').strip(\"'\")
30
+ print('OK' if key else 'MISSING')
31
+ "
32
+ ```
33
+
34
+ If `MISSING`: "Set your LangSmith API key: `export LANGSMITH_API_KEY=lsv2_pt_...` or run `npx harness-evolver@latest` to configure."
35
+
36
+ The tools auto-load the key from the credentials file, but the env var takes precedence.
37
+
38
+ Python 3.10+ with `langsmith` and `openevals` packages must be installed:
16
39
 
17
40
  ```bash
18
41
  pip install langsmith openevals 2>/dev/null || uv pip install langsmith openevals
@@ -21,9 +21,33 @@ Requires: pip install langsmith
21
21
  import argparse
22
22
  import json
23
23
  import os
24
+ import platform
24
25
  import sys
25
26
 
26
27
 
28
+ def ensure_langsmith_api_key():
29
+ """Load LANGSMITH_API_KEY from credentials file if not in env."""
30
+ if os.environ.get("LANGSMITH_API_KEY"):
31
+ return True
32
+ if platform.system() == "Darwin":
33
+ creds_path = os.path.expanduser("~/Library/Application Support/langsmith-cli/credentials")
34
+ else:
35
+ creds_path = os.path.expanduser("~/.config/langsmith-cli/credentials")
36
+ if os.path.exists(creds_path):
37
+ try:
38
+ with open(creds_path) as f:
39
+ for line in f:
40
+ line = line.strip()
41
+ if line.startswith("LANGSMITH_API_KEY="):
42
+ key = line.split("=", 1)[1].strip()
43
+ if key:
44
+ os.environ["LANGSMITH_API_KEY"] = key
45
+ return True
46
+ except OSError:
47
+ pass
48
+ return False
49
+
50
+
27
51
  def read_experiment(client, experiment_name):
28
52
  """Read results from a single LangSmith experiment."""
29
53
  try:
@@ -184,6 +208,7 @@ def main():
184
208
  parser.add_argument("--output", default=None, help="Output JSON path")
185
209
  parser.add_argument("--format", default="json", choices=["json", "markdown"], help="Output format")
186
210
  args = parser.parse_args()
211
+ ensure_langsmith_api_key()
187
212
 
188
213
  from langsmith import Client
189
214
  client = Client()
package/tools/run_eval.py CHANGED
@@ -17,11 +17,47 @@ Requires: pip install langsmith openevals
17
17
  import argparse
18
18
  import json
19
19
  import os
20
+ import platform
20
21
  import subprocess
21
22
  import sys
22
23
  import tempfile
23
24
 
24
25
 
26
+ def ensure_langsmith_api_key():
27
+ """Load LANGSMITH_API_KEY from credentials file if not in env."""
28
+ if os.environ.get("LANGSMITH_API_KEY"):
29
+ return True
30
+ if platform.system() == "Darwin":
31
+ creds_path = os.path.expanduser("~/Library/Application Support/langsmith-cli/credentials")
32
+ else:
33
+ creds_path = os.path.expanduser("~/.config/langsmith-cli/credentials")
34
+ if os.path.exists(creds_path):
35
+ try:
36
+ with open(creds_path) as f:
37
+ for line in f:
38
+ line = line.strip()
39
+ if line.startswith("LANGSMITH_API_KEY="):
40
+ key = line.split("=", 1)[1].strip()
41
+ if key:
42
+ os.environ["LANGSMITH_API_KEY"] = key
43
+ return True
44
+ except OSError:
45
+ pass
46
+ if os.path.exists(".env"):
47
+ try:
48
+ with open(".env") as f:
49
+ for line in f:
50
+ line = line.strip()
51
+ if line.startswith("LANGSMITH_API_KEY=") and not line.startswith("#"):
52
+ key = line.split("=", 1)[1].strip().strip("'\"")
53
+ if key:
54
+ os.environ["LANGSMITH_API_KEY"] = key
55
+ return True
56
+ except OSError:
57
+ pass
58
+ return False
59
+
60
+
25
61
  def make_target(entry_point, cwd):
26
62
  """Create a target function that runs the agent from a specific directory."""
27
63
  def target(inputs):
@@ -132,6 +168,7 @@ def main():
132
168
  config = json.load(f)
133
169
 
134
170
  os.environ["EVAL_TASK_TIMEOUT"] = str(args.timeout)
171
+ ensure_langsmith_api_key()
135
172
 
136
173
  from langsmith import Client
137
174
  client = Client()
package/tools/setup.py CHANGED
@@ -25,12 +25,58 @@ Requires: pip install langsmith openevals
25
25
  import argparse
26
26
  import json
27
27
  import os
28
+ import platform
28
29
  import subprocess
29
30
  import sys
30
31
  import tempfile
31
32
  from datetime import datetime, timezone
32
33
 
33
34
 
35
+ def ensure_langsmith_api_key():
36
+ """Load LANGSMITH_API_KEY from credentials file if not in env.
37
+
38
+ The installer saves the key to the langsmith-cli credentials file,
39
+ but the SDK only reads the env var. This bridges the gap.
40
+ """
41
+ if os.environ.get("LANGSMITH_API_KEY"):
42
+ return True
43
+
44
+ # Platform-specific credentials path (matches langsmith-cli)
45
+ if platform.system() == "Darwin":
46
+ creds_path = os.path.expanduser("~/Library/Application Support/langsmith-cli/credentials")
47
+ else:
48
+ creds_path = os.path.expanduser("~/.config/langsmith-cli/credentials")
49
+
50
+ if os.path.exists(creds_path):
51
+ try:
52
+ with open(creds_path) as f:
53
+ for line in f:
54
+ line = line.strip()
55
+ if line.startswith("LANGSMITH_API_KEY="):
56
+ key = line.split("=", 1)[1].strip()
57
+ if key:
58
+ os.environ["LANGSMITH_API_KEY"] = key
59
+ return True
60
+ except OSError:
61
+ pass
62
+
63
+ # Also check .env in current directory
64
+ if os.path.exists(".env"):
65
+ try:
66
+ with open(".env") as f:
67
+ for line in f:
68
+ line = line.strip()
69
+ if line.startswith("LANGSMITH_API_KEY=") and not line.startswith("#"):
70
+ key = line.split("=", 1)[1].strip().strip("'\"")
71
+ if key:
72
+ os.environ["LANGSMITH_API_KEY"] = key
73
+ return True
74
+ except OSError:
75
+ pass
76
+
77
+ return False
78
+
79
+
34
80
  def check_dependencies():
35
81
  """Verify langsmith and openevals are installed."""
36
82
  missing = []
@@ -294,6 +340,12 @@ def main():
294
340
  print(f"Install with: pip install {' '.join(missing)}", file=sys.stderr)
295
341
  sys.exit(1)
296
342
 
343
+ # Load API key from credentials file if not in env
344
+ if not ensure_langsmith_api_key():
345
+ print("LANGSMITH_API_KEY not found in environment, credentials file, or .env", file=sys.stderr)
346
+ print("Set it with: export LANGSMITH_API_KEY=lsv2_pt_...", file=sys.stderr)
347
+ sys.exit(1)
348
+
297
349
  from langsmith import Client
298
350
  client = Client()
299
351