harness-evolver
Advanced tools
| #!/usr/bin/env python3 | ||
| """Remove orphan git worktrees from .claude/worktrees/ after evaluation. | ||
| Prevents accumulation of 6+ worktrees per session. Cleans up directories | ||
| and runs `git worktree prune` afterward. | ||
| Stdlib-only — no langsmith dependency. | ||
| Usage: | ||
| # Dry run — show what would be removed | ||
| python3 cleanup_worktrees.py --dry-run | ||
| # Remove all worktrees | ||
| python3 cleanup_worktrees.py | ||
| # Keep specific worktrees by name | ||
| python3 cleanup_worktrees.py --keep winner-v003 candidate-v004a | ||
| # Specify project directory | ||
| python3 cleanup_worktrees.py --dir /path/to/project --dry-run | ||
| """ | ||
| import argparse | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| WORKTREE_SUBDIR = os.path.join(".claude", "worktrees") | ||
| def find_worktrees(base_dir): | ||
| """Find all directories under .claude/worktrees/ in the given project. | ||
| Returns a list of absolute paths to worktree directories. | ||
| """ | ||
| worktrees_root = os.path.join(base_dir, WORKTREE_SUBDIR) | ||
| if not os.path.isdir(worktrees_root): | ||
| return [] | ||
| entries = [] | ||
| for name in sorted(os.listdir(worktrees_root)): | ||
| full = os.path.join(worktrees_root, name) | ||
| if os.path.isdir(full): | ||
| entries.append(full) | ||
| return entries | ||
| def remove_worktree(path, dry_run=False): | ||
| """Remove a single worktree directory. | ||
| Tries `git worktree remove --force` first. If that fails (e.g. the | ||
| worktree wasn't registered with git), falls back to shutil.rmtree. | ||
| Returns a dict with keys: path, method, success, error. | ||
| """ | ||
| result = {"path": path, "method": None, "success": False, "error": None} | ||
| if dry_run: | ||
| result["method"] = "dry-run" | ||
| result["success"] = True | ||
| return result | ||
| # Try git worktree remove --force | ||
| try: | ||
| proc = subprocess.run( | ||
| ["git", "worktree", "remove", "--force", path], | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
| ) | ||
| if proc.returncode == 0: | ||
| result["method"] = "git worktree remove" | ||
| result["success"] = True | ||
| return result | ||
| except (subprocess.TimeoutExpired, FileNotFoundError, OSError): | ||
| pass | ||
| # Fallback: shutil.rmtree | ||
| try: | ||
| shutil.rmtree(path) | ||
| result["method"] = "shutil.rmtree" | ||
| result["success"] = True | ||
| except OSError as exc: | ||
| result["method"] = "shutil.rmtree" | ||
| result["error"] = str(exc) | ||
| return result | ||
| def prune_worktrees(project_dir): | ||
| """Run `git worktree prune` to clean up stale worktree bookkeeping.""" | ||
| try: | ||
| subprocess.run( | ||
| ["git", "worktree", "prune"], | ||
| cwd=project_dir, | ||
| capture_output=True, | ||
| text=True, | ||
| timeout=30, | ||
| ) | ||
| except (subprocess.TimeoutExpired, FileNotFoundError, OSError): | ||
| pass | ||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Remove orphan git worktrees from .claude/worktrees/" | ||
| ) | ||
| parser.add_argument( | ||
| "--dir", | ||
| default=".", | ||
| help="Project directory (default: current directory)", | ||
| ) | ||
| parser.add_argument( | ||
| "--keep", | ||
| nargs="*", | ||
| default=[], | ||
| metavar="NAME", | ||
| help="Worktree directory names to keep (basenames, not full paths)", | ||
| ) | ||
| parser.add_argument( | ||
| "--dry-run", | ||
| action="store_true", | ||
| help="Show what would be removed without actually removing", | ||
| ) | ||
| args = parser.parse_args() | ||
| project_dir = os.path.abspath(args.dir) | ||
| keep_set = set(args.keep) | ||
| worktrees = find_worktrees(project_dir) | ||
| if not worktrees: | ||
| print(f"No worktrees found under {os.path.join(project_dir, WORKTREE_SUBDIR)}") | ||
| return | ||
| # Partition into keep / remove | ||
| to_remove = [] | ||
| to_keep = [] | ||
| for wt in worktrees: | ||
| name = os.path.basename(wt) | ||
| if name in keep_set: | ||
| to_keep.append(wt) | ||
| else: | ||
| to_remove.append(wt) | ||
| if to_keep: | ||
| print(f"Keeping {len(to_keep)} worktree(s): {', '.join(os.path.basename(w) for w in to_keep)}") | ||
| if not to_remove: | ||
| print("Nothing to remove.") | ||
| return | ||
| action = "Would remove" if args.dry_run else "Removing" | ||
| print(f"{action} {len(to_remove)} worktree(s):\n") | ||
| results = [] | ||
| for wt in to_remove: | ||
| res = remove_worktree(wt, dry_run=args.dry_run) | ||
| results.append(res) | ||
| name = os.path.basename(wt) | ||
| if res["success"]: | ||
| method = f" ({res['method']})" if res["method"] != "dry-run" else "" | ||
| print(f" [ok] {name}{method}") | ||
| else: | ||
| print(f" [FAIL] {name} — {res['error']}") | ||
| # Prune stale worktree references | ||
| if not args.dry_run: | ||
| prune_worktrees(project_dir) | ||
| print("\nRan `git worktree prune`.") | ||
| failed = [r for r in results if not r["success"]] | ||
| if failed: | ||
| print(f"\n{len(failed)} removal(s) failed.", file=sys.stderr) | ||
| sys.exit(1) | ||
| if __name__ == "__main__": | ||
| main() |
| #!/usr/bin/env python3 | ||
| """Atomic .evolver.json updates after merge. | ||
| Replaces inline Python for config backup/restore/update during the evolve loop. | ||
| Three actions: | ||
| backup — save .evolver.json to .evolver.json.bak before merge | ||
| restore — restore from .bak after merge overwrites config, delete .bak | ||
| update — update best_experiment, best_score, increment iterations, | ||
| append enriched history entry | ||
| Stdlib-only. No external dependencies. | ||
| Usage: | ||
| # Before merge — save config | ||
| python3 update_config.py --config .evolver.json --action backup | ||
| # After merge — restore config (merge brought worktree's stale copy) | ||
| python3 update_config.py --config .evolver.json --action restore | ||
| # Update config with winner data | ||
| python3 update_config.py --config .evolver.json --action update \ | ||
| --winner-experiment v003-abc --winner-score 0.87 \ | ||
| --approach "fixed JSON parsing" --lens "failure_cluster" \ | ||
| --tokens 15000 --latency-ms 4500 --error-count 1 \ | ||
| --passing 18 --total 20 --per-evaluator '{"accuracy":0.9,"format":0.85}' \ | ||
| --code-loc 120 | ||
| """ | ||
| import argparse | ||
| import json | ||
| import os | ||
| import shutil | ||
| import sys | ||
| from datetime import datetime, timezone | ||
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | ||
| from _common import write_config_atomic, load_config | ||
| def action_backup(config_path): | ||
| """Copy .evolver.json to .evolver.json.bak.""" | ||
| bak_path = config_path + ".bak" | ||
| if not os.path.exists(config_path): | ||
| print(f"Error: config not found: {config_path}", file=sys.stderr) | ||
| return False | ||
| shutil.copy2(config_path, bak_path) | ||
| print(json.dumps({"action": "backup", "path": bak_path})) | ||
| return True | ||
| def action_restore(config_path): | ||
| """Restore .evolver.json from .bak, delete .bak.""" | ||
| bak_path = config_path + ".bak" | ||
| if not os.path.exists(bak_path): | ||
| print(f"Error: backup not found: {bak_path}", file=sys.stderr) | ||
| return False | ||
| shutil.copy2(bak_path, config_path) | ||
| os.remove(bak_path) | ||
| print(json.dumps({"action": "restore", "path": config_path})) | ||
| return True | ||
| def action_update(config_path, args): | ||
| """Update best_experiment, best_score, iterations, and append history.""" | ||
| config = load_config(config_path) | ||
| # Update top-level fields | ||
| config["best_experiment"] = args.winner_experiment | ||
| config["best_score"] = args.winner_score | ||
| config["iterations"] = config.get("iterations", 0) + 1 | ||
| # Build enriched history entry | ||
| version = f"v{config['iterations']:03d}" | ||
| entry = { | ||
| "version": version, | ||
| "experiment": args.winner_experiment, | ||
| "score": args.winner_score, | ||
| "timestamp": datetime.now(timezone.utc).isoformat(), | ||
| } | ||
| # Optional enrichment fields | ||
| if args.approach: | ||
| entry["approach"] = args.approach | ||
| if args.lens: | ||
| entry["lens"] = args.lens | ||
| if args.tokens is not None: | ||
| entry["tokens"] = args.tokens | ||
| if args.latency_ms is not None: | ||
| entry["latency_ms"] = args.latency_ms | ||
| if args.error_count is not None: | ||
| entry["error_count"] = args.error_count | ||
| if args.passing is not None: | ||
| entry["passing"] = args.passing | ||
| if args.total is not None: | ||
| entry["total"] = args.total | ||
| if args.per_evaluator: | ||
| try: | ||
| entry["per_evaluator"] = json.loads(args.per_evaluator) | ||
| except json.JSONDecodeError: | ||
| print(f"Warning: --per-evaluator is not valid JSON, skipping", file=sys.stderr) | ||
| if args.code_loc is not None: | ||
| entry["code_loc"] = args.code_loc | ||
| # Append to history | ||
| if "history" not in config: | ||
| config["history"] = [] | ||
| config["history"].append(entry) | ||
| write_config_atomic(config_path, config) | ||
| print(json.dumps({ | ||
| "action": "update", | ||
| "version": version, | ||
| "best_score": args.winner_score, | ||
| "iterations": config["iterations"], | ||
| })) | ||
| return True | ||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Atomic .evolver.json updates after merge." | ||
| ) | ||
| parser.add_argument( | ||
| "--config", required=True, help="Path to .evolver.json" | ||
| ) | ||
| parser.add_argument( | ||
| "--action", required=True, choices=["backup", "restore", "update"], | ||
| help="Action: backup, restore, or update" | ||
| ) | ||
| # Update-specific flags | ||
| parser.add_argument("--winner-experiment", help="Winning experiment name") | ||
| parser.add_argument("--winner-score", type=float, help="Winning score") | ||
| parser.add_argument("--approach", default="", help="Brief description of winning approach") | ||
| parser.add_argument("--lens", default="", help="Investigation lens used") | ||
| parser.add_argument("--tokens", type=int, default=None, help="Token usage") | ||
| parser.add_argument("--latency-ms", type=int, default=None, help="Latency in milliseconds") | ||
| parser.add_argument("--error-count", type=int, default=None, help="Number of errors") | ||
| parser.add_argument("--passing", type=int, default=None, help="Number of passing examples") | ||
| parser.add_argument("--total", type=int, default=None, help="Total number of examples") | ||
| parser.add_argument("--per-evaluator", default=None, help="Per-evaluator scores as JSON string") | ||
| parser.add_argument("--code-loc", type=int, default=None, help="Lines of code changed") | ||
| args = parser.parse_args() | ||
| if args.action == "update": | ||
| if not args.winner_experiment or args.winner_score is None: | ||
| parser.error("--winner-experiment and --winner-score are required for --action update") | ||
| if args.action == "backup": | ||
| ok = action_backup(args.config) | ||
| elif args.action == "restore": | ||
| ok = action_restore(args.config) | ||
| elif args.action == "update": | ||
| ok = action_update(args.config, args) | ||
| else: | ||
| parser.error(f"Unknown action: {args.action}") | ||
| ok = False | ||
| sys.exit(0 if ok else 1) | ||
| if __name__ == "__main__": | ||
| main() |
| { | ||
| "name": "harness-evolver", | ||
| "description": "LangSmith-native autonomous agent optimization — evolves LLM agent code using multi-agent proposers, LangSmith experiments, and git worktrees", | ||
| "version": "6.2.0", | ||
| "version": "6.3.0", | ||
| "author": { | ||
@@ -6,0 +6,0 @@ "name": "Raphael Valdetaro" |
@@ -141,3 +141,4 @@ --- | ||
| Example for one run: | ||
| **Rubric pinning**: Include the rubric text (if available) in the comment. This makes scores reproducible and diagnosable across iterations: | ||
| ```bash | ||
@@ -147,6 +148,8 @@ langsmith-cli --json feedback create "run-uuid-here" \ | ||
| --score 1.0 \ | ||
| --comment "Response correctly identifies the applicable regulation and provides accurate guidance." \ | ||
| --comment "RUBRIC: Should mention null safety and Android. JUDGMENT: Lists all features correctly." \ | ||
| --source model | ||
| ``` | ||
| If no rubric exists, use standard format without the RUBRIC prefix. The `RUBRIC:` prefix lets downstream tools compare rubric interpretations across iterations. | ||
| ### Phase 4: Summary | ||
@@ -153,0 +156,0 @@ |
+1
-1
| { | ||
| "name": "harness-evolver", | ||
| "version": "6.2.0", | ||
| "version": "6.3.0", | ||
| "description": "LangSmith-native autonomous agent optimization for Claude Code", | ||
@@ -5,0 +5,0 @@ "author": "Raphael Valdetaro", |
+18
-11
@@ -238,19 +238,21 @@ --- | ||
| 1. **Save config before merge** (merge will overwrite with worktree's stale copy): | ||
| ```bash | ||
| cp .evolver.json .evolver.json.bak | ||
| ``` | ||
| # 1. Backup config (merge will overwrite with worktree's stale copy) | ||
| $EVOLVER_PY $TOOLS/update_config.py --config .evolver.json --action backup | ||
| 2. **Merge the winner**: | ||
| ```bash | ||
| # 2. Merge | ||
| git merge {winner_branch} --no-edit -m "evolve: merge v{NNN} (score: {score})" | ||
| ``` | ||
| 3. **Restore config and update** (the merge brought the worktree's old .evolver.json — restore ours): | ||
| ```bash | ||
| cp .evolver.json.bak .evolver.json | ||
| # 3. Restore config (merge brought stale copy) | ||
| $EVOLVER_PY $TOOLS/update_config.py --config .evolver.json --action restore | ||
| # 4. Update config with enriched history (one command, no inline Python) | ||
| $EVOLVER_PY $TOOLS/update_config.py --config .evolver.json --action update \ | ||
| --winner-experiment "{winner}" --winner-score {score} \ | ||
| --approach "{approach}" --lens "{lens}" \ | ||
| --tokens {tokens} --latency-ms {latency} --error-count {errors} \ | ||
| --passing {passing} --total {total} \ | ||
| --per-evaluator '{json_dict}' --code-loc {loc} | ||
| ``` | ||
| 4. **Update config** with enriched history (score, tokens, latency, errors, passing, total, per_evaluator, approach, lens, code_loc). | ||
| 5. **Git-tag** for rollback: | ||
@@ -304,2 +306,7 @@ | ||
| **Cleanup worktrees** (free disk space after eval): | ||
| ```bash | ||
| $EVOLVER_PY $TOOLS/cleanup_worktrees.py --dir "$(pwd)" | ||
| ``` | ||
| ### 7. Gate Check | ||
@@ -306,0 +313,0 @@ |
@@ -153,2 +153,4 @@ #!/usr/bin/env python3 | ||
| parser.add_argument("--preflight-only", action="store_true", help="Run preflight checks only (API key, config, canary) then exit") | ||
| parser.add_argument("--retry-on-rate-limit", action="store_true", | ||
| help="If rate-limited, wait 60s and suggest re-run") | ||
| parser.add_argument("--sample", type=int, default=None, help="Evaluate a random sample of N examples instead of all") | ||
@@ -321,2 +323,8 @@ args = parser.parse_args() | ||
| if aborted_early and args.retry_on_rate_limit: | ||
| import time | ||
| print(f"\n Rate-limited. Waiting 60s before suggesting re-run...", file=sys.stderr) | ||
| time.sleep(60) | ||
| print(f" Wait complete. Re-run this command to retry remaining examples.", file=sys.stderr) | ||
| mean_score = sum(scores) / len(scores) if scores else 0.0 | ||
@@ -323,0 +331,0 @@ num_examples = len(per_example) |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
348650
3.58%43
4.88%6400
4.71%