autoevolve-cli 0.1.0__py3-none-any.whl

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.
autoevolve/scaffold.py ADDED
@@ -0,0 +1,88 @@
1
+ from pathlib import Path
2
+
3
+ from autoevolve.git import find_repo_root
4
+ from autoevolve.harnesses import HARNESS_SPECS, Harness, get_harness_spec
5
+ from autoevolve.models.experiment import PromptFile
6
+ from autoevolve.problem import parse_problem_spec
7
+ from autoevolve.prompt import build_harness_skill_prompt, build_problem_template
8
+ from autoevolve.repository import (
9
+ EXPERIMENT_FILE,
10
+ JOURNAL_FILE,
11
+ PROBLEM_FILE,
12
+ parse_experiment_document,
13
+ )
14
+
15
+
16
+ class Scaffolder:
17
+ def __init__(self, cwd: str | Path = ".") -> None:
18
+ self.root = find_repo_root(cwd)
19
+
20
+ def apply_init(self, harness: Harness, continue_hook: bool) -> list[str]:
21
+ spec = get_harness_spec(harness)
22
+ written: list[str] = []
23
+ if not (self.root / PROBLEM_FILE).exists():
24
+ (self.root / PROBLEM_FILE).write_text(build_problem_template(), encoding="utf-8")
25
+ written.append(PROBLEM_FILE)
26
+ prompt_path = self.root / spec.prompt_path
27
+ prompt_path.parent.mkdir(parents=True, exist_ok=True)
28
+ prompt_path.write_text(build_harness_skill_prompt(harness), encoding="utf-8")
29
+ written.append(spec.prompt_path)
30
+ if continue_hook:
31
+ for file_spec in spec.continue_hook_files:
32
+ path = self.root / file_spec.path
33
+ existing = path.read_text(encoding="utf-8") if path.exists() else None
34
+ path.parent.mkdir(parents=True, exist_ok=True)
35
+ path.write_text(file_spec.build_contents(existing), encoding="utf-8")
36
+ written.append(file_spec.path)
37
+ return written
38
+
39
+ def prompt_files(self) -> list[PromptFile]:
40
+ files: list[PromptFile] = []
41
+ for harness, spec in HARNESS_SPECS.items():
42
+ path = self.root / spec.prompt_path
43
+ if path.exists():
44
+ files.append(PromptFile(harness=harness.value, path=path))
45
+ return files
46
+
47
+ def update_prompt(self, prompt_file: PromptFile) -> None:
48
+ harness = Harness(prompt_file.harness)
49
+ prompt_file.path.parent.mkdir(parents=True, exist_ok=True)
50
+ prompt_file.path.write_text(build_harness_skill_prompt(harness), encoding="utf-8")
51
+
52
+ def validate(self) -> list[str]:
53
+ problems: list[str] = []
54
+ problem_path = self.root / PROBLEM_FILE
55
+ if not problem_path.exists():
56
+ problems.append(f"Missing {PROBLEM_FILE}. Run autoevolve init first.")
57
+ problem = None
58
+ else:
59
+ try:
60
+ problem = parse_problem_spec(problem_path.read_text(encoding="utf-8"))
61
+ except ValueError as error:
62
+ problems.append(str(error))
63
+ problem = None
64
+ if not self.prompt_files():
65
+ problems.append(
66
+ "Missing prompt file. Expected PROGRAM.md or a supported harness skill file."
67
+ )
68
+ journal_path = self.root / JOURNAL_FILE
69
+ experiment_path = self.root / EXPERIMENT_FILE
70
+ if journal_path.exists() or experiment_path.exists():
71
+ if not journal_path.exists():
72
+ problems.append(f"Missing {JOURNAL_FILE}.")
73
+ if not experiment_path.exists():
74
+ problems.append(f"Missing {EXPERIMENT_FILE}.")
75
+ if experiment_path.exists():
76
+ try:
77
+ document = parse_experiment_document(
78
+ experiment_path.read_text(encoding="utf-8")
79
+ )
80
+ except ValueError as error:
81
+ problems.append(str(error))
82
+ else:
83
+ if problem is not None and problem.metric not in document.metrics:
84
+ problems.append(
85
+ f'{EXPERIMENT_FILE} must record the primary metric "{problem.metric}" '
86
+ f"declared in {PROBLEM_FILE} ({problem.raw})."
87
+ )
88
+ return problems
autoevolve/worktree.py ADDED
@@ -0,0 +1,186 @@
1
+ import json
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+ from git.exc import GitCommandError
6
+
7
+ from autoevolve.git import find_repo_root, list_linked_worktrees, open_repo
8
+ from autoevolve.models.experiment import ExperimentWorktree
9
+ from autoevolve.models.worktree import (
10
+ CleanedWorktrees,
11
+ RecordedExperiment,
12
+ StartedExperiment,
13
+ )
14
+ from autoevolve.repository import (
15
+ EXPERIMENT_FILE,
16
+ JOURNAL_FILE,
17
+ WORKTREE_ROOT,
18
+ ExperimentRepository,
19
+ parse_experiment_document,
20
+ )
21
+
22
+ _REF_PREFIX = "autoevolve/"
23
+
24
+
25
+ class ExperimentWorktreeManager:
26
+ def __init__(self, cwd: str | Path = ".") -> None:
27
+ self.root = find_repo_root(cwd)
28
+ self.repo = open_repo(self.root)
29
+ self.cwd = Path(cwd).resolve()
30
+
31
+ def start(self, name: str, summary: str, from_ref: str | None) -> StartedExperiment:
32
+ name = self._validate_name(name)
33
+ ref_name = f"{_REF_PREFIX}{name}"
34
+ try:
35
+ self.repo.git.check_ref_format(f"refs/heads/{ref_name}")
36
+ except GitCommandError as error:
37
+ raise ValueError(
38
+ f'"{ref_name}" is not a valid managed experiment branch name.'
39
+ ) from error
40
+ if any(head.name == ref_name for head in self.repo.heads):
41
+ raise RuntimeError(f'Branch "{ref_name}" already exists.')
42
+
43
+ path = (WORKTREE_ROOT / name).resolve()
44
+ if path.exists():
45
+ raise RuntimeError(f"Worktree path already exists: {path}")
46
+
47
+ current_branch = self.repo.git.branch("--show-current").strip()
48
+ base_ref = from_ref or current_branch or "HEAD"
49
+ path.parent.mkdir(parents=True, exist_ok=True)
50
+ self.repo.git.worktree("add", "-b", ref_name, str(path), self.repo.commit(base_ref).hexsha)
51
+ (path / JOURNAL_FILE).write_text(self._journal_stub_text(name), encoding="utf-8")
52
+ (path / EXPERIMENT_FILE).write_text(
53
+ json.dumps({"summary": summary, "metrics": {}, "references": []}, indent=2) + "\n",
54
+ encoding="utf-8",
55
+ )
56
+ return StartedExperiment(branch=ref_name, base_ref=base_ref, path=path)
57
+
58
+ def record(self) -> RecordedExperiment:
59
+ current_worktree = next(
60
+ worktree
61
+ for worktree in list_linked_worktrees(self.repo, current_path=self.cwd)
62
+ if worktree.is_current
63
+ )
64
+ branch_name = current_worktree.branch
65
+ if not branch_name:
66
+ raise RuntimeError("record requires an attached branch.")
67
+ if not branch_name.startswith(_REF_PREFIX):
68
+ raise RuntimeError(
69
+ f"record only works on managed autoevolve experiment branches ({_REF_PREFIX}<name>)."
70
+ )
71
+
72
+ root = current_worktree.path.resolve()
73
+ managed_root = WORKTREE_ROOT.resolve()
74
+ if root != managed_root and managed_root not in root.parents:
75
+ raise RuntimeError(
76
+ f"record must be run from a managed autoevolve worktree under {managed_root}."
77
+ )
78
+
79
+ worktree_repo = open_repo(root)
80
+ git_dir = Path(worktree_repo.git.rev_parse("--git-dir").strip())
81
+ if not git_dir.is_absolute():
82
+ git_dir = (root / git_dir).resolve()
83
+ common_git_dir = Path(worktree_repo.git.rev_parse("--git-common-dir").strip())
84
+ if not common_git_dir.is_absolute():
85
+ common_git_dir = (root / common_git_dir).resolve()
86
+ if git_dir == common_git_dir:
87
+ raise RuntimeError("record refuses to remove the primary worktree.")
88
+
89
+ journal_path = root / JOURNAL_FILE
90
+ experiment_path = root / EXPERIMENT_FILE
91
+ if not journal_path.exists() or not experiment_path.exists():
92
+ raise RuntimeError(f"record requires both {JOURNAL_FILE} and {EXPERIMENT_FILE}.")
93
+
94
+ experiment_name = branch_name.removeprefix(_REF_PREFIX)
95
+ journal_text = journal_path.read_text(encoding="utf-8").strip()
96
+ if journal_text == self._journal_stub_text(experiment_name).strip():
97
+ raise RuntimeError(f"Replace the {JOURNAL_FILE} stub before committing.")
98
+
99
+ document = parse_experiment_document(experiment_path.read_text(encoding="utf-8"))
100
+ if not worktree_repo.is_dirty(untracked_files=True):
101
+ raise RuntimeError("No changes to commit.")
102
+
103
+ message = next((line.strip() for line in document.summary.splitlines() if line.strip()), "")
104
+ if not message:
105
+ raise RuntimeError(f"{EXPERIMENT_FILE} summary must not be empty.")
106
+
107
+ worktree_repo.git.add("-A")
108
+ worktree_repo.git.commit("-m", message)
109
+ sha = worktree_repo.head.commit.hexsha
110
+ self.repo.git.worktree("remove", str(root))
111
+ return RecordedExperiment(branch=branch_name, sha=sha, path=root)
112
+
113
+ def clean(self, name: str | None, force: bool) -> CleanedWorktrees:
114
+ worktrees = ExperimentRepository(self.root).active_worktrees()
115
+ managed = [
116
+ worktree for worktree in worktrees if worktree.is_managed and not worktree.is_primary
117
+ ]
118
+ experiment_name = ""
119
+ if name is not None:
120
+ experiment_name = self._normalize_name(name)
121
+ target_path = (WORKTREE_ROOT / experiment_name).resolve()
122
+ target = next(
123
+ (worktree for worktree in worktrees if worktree.path == target_path), None
124
+ )
125
+ if target is None or target.is_primary or not target.is_managed:
126
+ raise RuntimeError(
127
+ f'No managed experiment worktree named "{experiment_name}" found for this repository.'
128
+ )
129
+ managed = [target]
130
+
131
+ if not managed:
132
+ return CleanedWorktrees(experiment_name=experiment_name, removed=())
133
+
134
+ blocked = [worktree for worktree in managed if worktree.is_missing or worktree.dirty]
135
+ if blocked and not force:
136
+ reason = (
137
+ "Refusing to remove a dirty or missing linked worktree without --force:"
138
+ if len(blocked) == 1
139
+ else "Refusing to remove dirty or missing linked worktrees without --force:"
140
+ )
141
+ details = "\n".join(
142
+ f" {self._describe_worktree_for_removal(worktree)}" for worktree in blocked
143
+ )
144
+ raise RuntimeError(f"{reason}\n{details}")
145
+
146
+ removed = []
147
+ for worktree in managed:
148
+ if worktree.is_missing:
149
+ shutil.rmtree(worktree.path, ignore_errors=True)
150
+ self.repo.git.worktree("prune", "--expire", "now")
151
+ else:
152
+ args = ["worktree", "remove"]
153
+ if force or worktree.dirty:
154
+ args.append("--force")
155
+ args.append(str(worktree.path))
156
+ self.repo.git.execute(["git", *args])
157
+ if worktree.branch and any(head.name == worktree.branch for head in self.repo.heads):
158
+ self.repo.git.branch("-D", worktree.branch)
159
+ removed.append(worktree)
160
+
161
+ return CleanedWorktrees(experiment_name=experiment_name, removed=tuple(removed))
162
+
163
+ @staticmethod
164
+ def _normalize_name(name: str) -> str:
165
+ value = name.strip()
166
+ if value.startswith(_REF_PREFIX):
167
+ value = value.removeprefix(_REF_PREFIX)
168
+ return value
169
+
170
+ @staticmethod
171
+ def _validate_name(name: str) -> str:
172
+ value = ExperimentWorktreeManager._normalize_name(name)
173
+ if not value:
174
+ raise ValueError("Experiment name must not be empty.")
175
+ if WORKTREE_ROOT.resolve() not in (WORKTREE_ROOT / value).resolve().parents:
176
+ raise ValueError(f'"{name}" is not a valid experiment name.')
177
+ return value
178
+
179
+ @staticmethod
180
+ def _journal_stub_text(name: str) -> str:
181
+ return f"# {name}\n\nTODO: fill this in once you're done with your experiment.\n"
182
+
183
+ @staticmethod
184
+ def _describe_worktree_for_removal(worktree: ExperimentWorktree) -> str:
185
+ state = "missing" if worktree.is_missing else "dirty" if worktree.dirty else "clean"
186
+ return f"{worktree.path} ({worktree.branch or '(detached HEAD)'}, {state}, {worktree.head[:7]})"
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: autoevolve-cli
3
+ Version: 0.1.0
4
+ Summary: Git-backed experiment loops for coding agents
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: gitpython>=3.1.46
7
+ Requires-Dist: rich>=14
8
+ Requires-Dist: textual>=1
9
+ Requires-Dist: typer>=0.24.1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # autoevolve
13
+
14
+ ![screenshot](./assets/screenshot.png)
15
+
16
+ `autoevolve` lets coding agents run git-backed experiment loops autonomously. It gives agents a lightweight workflow for branching, recording results, and comparing experiments without needing heavy external dependencies, databases, or services.
17
+
18
+ Run it inside an existing project, let it set up the files your coding agent needs, and then let the agent iterate through experiments, branch into new research directions, and explore ideas on its own.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install autoevolve-cli
24
+ ```
25
+
26
+ ## Quickstart
27
+
28
+ **1. Initialize `autoevolve` in an existing git repo**:
29
+
30
+ ```bash
31
+ autoevolve init
32
+ ```
33
+
34
+ `autoevolve init` walks you through the setup for your coding harness and problem:
35
+
36
+ - `SKILL.md` or `PROGRAM.md`: the instructions your coding agent reads to use `autoevolve`
37
+ - `PROBLEM.md`: the goal, metric, constraints, and validation setup for your problem
38
+
39
+ For an example problem to try, see the [circle packing example repo](https://github.com/wiskojo/autoevolve-circle-packing-example).
40
+
41
+ **2. Tell your agent to read `PROGRAM.md` or activate the skill depending on your setup**:
42
+
43
+ ```
44
+ Read PROGRAM.md, then start working.
45
+
46
+ # If using skills
47
+ $autoevolve # Codex
48
+ /autoevolve # Claude Code
49
+ ```
50
+
51
+ From there, your agent should start working in the repo as usual. Experiment commits will include:
52
+
53
+ - `EXPERIMENT.json`: the structured record of the experiment, including summary, metrics, and any references to other experiments
54
+ - `JOURNAL.md`: the narrative record of the experiment, which could include the hypothesis, changes made, outcomes, reflections, etc.
55
+
56
+ **3. Start the TUI to monitor your agent's progress**:
57
+
58
+ ```bash
59
+ autoevolve dashboard
60
+ ```
61
+
62
+ ## CLI
63
+
64
+ Here’s the CLI surface: `Human` commands handle setup and monitoring, `Lifecycle` manages experiments, and `Inspect` and `Analytics` help your agents review the experiment state.
65
+
66
+ ```
67
+ Usage: autoevolve [OPTIONS] COMMAND [ARGS]...
68
+
69
+ Git-backed experiment loops for coding agents.
70
+
71
+ Options:
72
+ --help Show this message and exit.
73
+
74
+ Human:
75
+ init Set up PROBLEM.md and agent instructions.
76
+ validate Check that the repo is ready for autoevolve.
77
+ update Update detected prompt files to the latest version.
78
+ dashboard Open the experiment dashboard.
79
+
80
+ Lifecycle:
81
+ start Create a managed experiment branch and worktree.
82
+ record Validate, commit, and remove the current managed worktree.
83
+ clean Remove stale managed worktrees for this repository.
84
+
85
+ Inspect:
86
+ status Show the current experiment status.
87
+ log Show experiment logs.
88
+ show Show experiment details.
89
+ compare Compare two experiments.
90
+ lineage Show experiment lineage around one ref.
91
+
92
+ Analytics:
93
+ recent List the most recent recorded experiments.
94
+ best List the top experiments for one metric.
95
+ pareto List the Pareto frontier for selected metrics.
96
+
97
+ Examples:
98
+ autoevolve start tune-thresholds "Try a tighter threshold sweep" --from 07f1844
99
+ autoevolve record
100
+ autoevolve log
101
+ autoevolve recent --limit 5
102
+ autoevolve best --max benchmark_score --limit 5
103
+
104
+ Run "autoevolve <command> --help" for command-specific details.
105
+ ```
@@ -0,0 +1,25 @@
1
+ autoevolve/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ autoevolve/app.py,sha256=cpTr6_6mltOqsHqeJdrM_cWR9NdLX0SDIJ8UuCvFDl4,3138
3
+ autoevolve/dashboard.py,sha256=M3awNBIJ_Ls75Sv1iI7fA5K7ZUl7k9BabWLofBTasf8,70002
4
+ autoevolve/git.py,sha256=p4cDwdWYUrSvxB73WtD9LsceIWQpJUHkQeWhI-QDCXM,6074
5
+ autoevolve/harnesses.py,sha256=2yfOimWwYiNMKDjpKxfraIbOHJy0UfHkYjPdJrjIUQE,5148
6
+ autoevolve/problem.py,sha256=FZxikK1_yThXGmSVHovPtGQCxnR3WK7WJrdNgTz8ZJI,1700
7
+ autoevolve/prompt.py,sha256=cDRdKUzvsbdC1WkhlLkCyeoGohvL7I5qlHTBIkvG0Cg,10446
8
+ autoevolve/repository.py,sha256=GN_jTGDXn33O-hYSRf9Ra44TSXFAZBJ2cMDstFBKXBM,17576
9
+ autoevolve/scaffold.py,sha256=T_7rIvfCDq7lAKl9366JZeLEwzl5VCFi6hQGIlYKaJA,3886
10
+ autoevolve/worktree.py,sha256=v6GH_tZN8Zqfx7S4zR8r-We5ypmW6FFFdqYgYCo2VxI,8055
11
+ autoevolve/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ autoevolve/commands/analytics.py,sha256=W0NWnV--ppjali7bCwEMkDQ4H7mKzxyZjnKQZe9lOA8,5590
13
+ autoevolve/commands/human.py,sha256=KtZXy-B2MaB0RmaufBgaDWbaDWp77vdGgpXzXiMZfn4,5981
14
+ autoevolve/commands/inspect.py,sha256=4r9lXn_gDEBDsVeBj8dK13k-pZSnzBofC_Oa9UO0Z34,16855
15
+ autoevolve/commands/lifecycle.py,sha256=nP2dzvKfChp3xaiLprL7Q_7r1VrfMMnF6uSRhI5yQMw,2694
16
+ autoevolve/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ autoevolve/models/experiment.py,sha256=vtEFvNdPMHbn-7Lgc5C8JMvKdbyXoaPiTU4R5JOXp-c,1084
18
+ autoevolve/models/git.py,sha256=3OjAbDwfwbc6AVyRHEkaJVRWgGEoogo6D9R-Q38H438,533
19
+ autoevolve/models/lineage.py,sha256=RdN7IW68uu53zpP46e-YcOr3pHLUKzE69Rd108X79rE,359
20
+ autoevolve/models/types.py,sha256=l0Q0WJluBOqWdGUM0CFZni5eGTo2Djc-yuA2_e0TasI,404
21
+ autoevolve/models/worktree.py,sha256=LFsU9s7IM-wPA5Fcl9P7bXw_fuqRGWPbTJpSpip86EM,435
22
+ autoevolve_cli-0.1.0.dist-info/METADATA,sha256=ziyxO9BYaGxXf93tI5b-YFKFP5RiXYlEx6KGCN7Va20,3465
23
+ autoevolve_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
24
+ autoevolve_cli-0.1.0.dist-info/entry_points.txt,sha256=z2i3w3IUGOocuocPfKRknQrF3tczuNtRcVcEuSqYL7c,51
25
+ autoevolve_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ autoevolve = autoevolve.app:main