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/__init__.py ADDED
File without changes
autoevolve/app.py ADDED
@@ -0,0 +1,100 @@
1
+ from collections.abc import Sequence
2
+
3
+ import click
4
+ import typer
5
+ from typer.core import TyperGroup
6
+ from typer.main import get_command
7
+
8
+ from autoevolve.commands.analytics import app as analytics_app
9
+ from autoevolve.commands.human import app as human_app
10
+ from autoevolve.commands.inspect import app as inspect_app
11
+ from autoevolve.commands.lifecycle import app as lifecycle_app
12
+
13
+
14
+ class AutoevolveGroup(TyperGroup):
15
+ def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
16
+ command_names = self.list_commands(ctx)
17
+ sections: dict[str, list[tuple[str, str]]] = {
18
+ title: [] for title in ("Human", "Lifecycle", "Inspect", "Analytics")
19
+ }
20
+ command_width = max((len(name) for name in command_names), default=0)
21
+
22
+ for command_name in command_names:
23
+ command = self.get_command(ctx, command_name)
24
+ if command is None or command.hidden:
25
+ continue
26
+ section = getattr(command, "rich_help_panel", None) or "Other"
27
+ sections.setdefault(section, []).append(
28
+ (
29
+ command_name.ljust(command_width),
30
+ command.get_short_help_str(formatter.width),
31
+ )
32
+ )
33
+
34
+ for title, rows in sections.items():
35
+ if not rows:
36
+ continue
37
+ with formatter.section(title):
38
+ formatter.write_dl(rows)
39
+
40
+ def format_epilog(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
41
+ if self.epilog is None:
42
+ return
43
+ formatter.write_paragraph()
44
+ formatter.write(f"{self.epilog}\n")
45
+
46
+
47
+ app = typer.Typer(
48
+ cls=AutoevolveGroup,
49
+ help="Git-backed experiment loops for coding agents.",
50
+ epilog="""Examples:
51
+ autoevolve start tune-thresholds "Try a tighter threshold sweep" --from 07f1844
52
+ autoevolve record
53
+ autoevolve log
54
+ autoevolve recent --limit 5
55
+ autoevolve best --max benchmark_score --limit 5
56
+
57
+ Run "autoevolve <command> --help" for command-specific details.""",
58
+ invoke_without_command=True,
59
+ add_completion=False,
60
+ rich_markup_mode=None,
61
+ pretty_exceptions_enable=False,
62
+ )
63
+
64
+ app.add_typer(human_app)
65
+ app.add_typer(lifecycle_app)
66
+ app.add_typer(inspect_app)
67
+ app.add_typer(analytics_app)
68
+
69
+
70
+ @app.callback()
71
+ def main_callback(ctx: typer.Context) -> None:
72
+ if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
73
+ typer.echo(ctx.get_help())
74
+ raise typer.Exit()
75
+
76
+
77
+ def main(argv: Sequence[str] | None = None) -> int:
78
+ command = get_command(app)
79
+ try:
80
+ command.main(
81
+ args=list(argv) if argv is not None else None,
82
+ prog_name="autoevolve",
83
+ standalone_mode=False,
84
+ )
85
+ return 0
86
+ except click.ClickException as error:
87
+ error.show()
88
+ return error.exit_code
89
+ except typer.Abort:
90
+ typer.echo("Aborted!", err=True)
91
+ return 1
92
+ except typer.Exit as error:
93
+ return error.exit_code
94
+ except Exception as error:
95
+ typer.echo(str(error), err=True)
96
+ return 1
97
+
98
+
99
+ if __name__ == "__main__":
100
+ raise SystemExit(main())
File without changes
@@ -0,0 +1,163 @@
1
+ import json
2
+ from typing import Annotated
3
+
4
+ import typer
5
+
6
+ from autoevolve.models.experiment import ExperimentIndexEntry, Objective
7
+ from autoevolve.models.types import SetOutputFormat
8
+ from autoevolve.repository import ExperimentRepository
9
+
10
+ app = typer.Typer()
11
+
12
+
13
+ @app.command(
14
+ "recent",
15
+ rich_help_panel="Analytics",
16
+ short_help="List the most recent recorded experiments.",
17
+ help=(
18
+ "List the most recent recorded experiments.\n\n"
19
+ "recent emits recent experiments in TSV or JSONL format for scripting "
20
+ "and analysis."
21
+ ),
22
+ )
23
+ def recent(
24
+ limit: Annotated[int, typer.Option(min=1, help="Number of experiments to show.")] = 10,
25
+ output_format: Annotated[
26
+ SetOutputFormat,
27
+ typer.Option("--format", help="Output format."),
28
+ ] = SetOutputFormat.TSV,
29
+ ) -> None:
30
+ _print_records(ExperimentRepository().recent_index(limit), output_format)
31
+
32
+
33
+ @app.command(
34
+ "best",
35
+ rich_help_panel="Analytics",
36
+ short_help="List the top experiments for one metric.",
37
+ help=(
38
+ "List the top experiments for one metric.\n\n"
39
+ "best ranks recorded experiments by one metric. If no metric is "
40
+ "provided, it defaults to the primary metric from PROBLEM.md."
41
+ ),
42
+ )
43
+ def best(
44
+ max_metric: Annotated[str | None, typer.Option("--max", help="Metric to maximize.")] = None,
45
+ min_metric: Annotated[str | None, typer.Option("--min", help="Metric to minimize.")] = None,
46
+ limit: Annotated[int, typer.Option(min=1, help="Number of experiments to show.")] = 5,
47
+ output_format: Annotated[
48
+ SetOutputFormat,
49
+ typer.Option("--format", help="Output format."),
50
+ ] = SetOutputFormat.TSV,
51
+ ) -> None:
52
+ if max_metric and min_metric:
53
+ raise typer.BadParameter("Use either --max <metric> or --min <metric>, not both.")
54
+
55
+ objective = None
56
+ if max_metric is not None:
57
+ objective = Objective(direction="max", metric=max_metric)
58
+ if min_metric is not None:
59
+ objective = Objective(direction="min", metric=min_metric)
60
+
61
+ repository = ExperimentRepository()
62
+ if objective is None:
63
+ try:
64
+ problem = repository.problem()
65
+ except (FileNotFoundError, ValueError) as error:
66
+ raise RuntimeError(
67
+ "best requires an explicit objective, or a valid PROBLEM.md primary metric."
68
+ ) from error
69
+ resolved = Objective(direction=problem.direction, metric=problem.metric)
70
+ else:
71
+ resolved = objective
72
+ records = repository.best_records(resolved, limit)
73
+ if not records:
74
+ typer.echo(f'No experiments found with a numeric "{resolved.metric}" metric.')
75
+ return
76
+ _print_records(records, output_format)
77
+
78
+
79
+ @app.command(
80
+ "pareto",
81
+ rich_help_panel="Analytics",
82
+ short_help="List the Pareto frontier for selected metrics.",
83
+ help=(
84
+ "List the Pareto frontier for selected metrics.\n\n"
85
+ "pareto returns the non-dominated recorded experiments for the selected "
86
+ "metrics in TSV or JSONL format."
87
+ ),
88
+ )
89
+ def pareto(
90
+ max_metrics: Annotated[
91
+ list[str] | None,
92
+ typer.Option("--max", help="Metric to maximize. Repeat as needed."),
93
+ ] = None,
94
+ min_metrics: Annotated[
95
+ list[str] | None,
96
+ typer.Option("--min", help="Metric to minimize. Repeat as needed."),
97
+ ] = None,
98
+ limit: Annotated[int | None, typer.Option(min=1, help="Number of experiments to show.")] = None,
99
+ output_format: Annotated[
100
+ SetOutputFormat,
101
+ typer.Option("--format", help="Output format."),
102
+ ] = SetOutputFormat.TSV,
103
+ ) -> None:
104
+ objectives = [Objective(direction="max", metric=metric) for metric in max_metrics or ()]
105
+ objectives.extend(Objective(direction="min", metric=metric) for metric in min_metrics or ())
106
+ if not objectives:
107
+ raise typer.BadParameter(
108
+ "pareto requires at least one metric, for example: --max primary_metric --min runtime_sec"
109
+ )
110
+
111
+ records = ExperimentRepository().pareto_records(objectives, limit)
112
+ if not records:
113
+ typer.echo("No experiments found with numeric metrics for the requested Pareto objectives.")
114
+ return
115
+ _print_records(records, output_format)
116
+
117
+
118
+ def _print_records(records: list[ExperimentIndexEntry], output_format: SetOutputFormat) -> None:
119
+ if not records:
120
+ typer.echo("No experiments found.")
121
+ return
122
+ if output_format is SetOutputFormat.TSV:
123
+ typer.echo("sha\tdate\tmetrics\tsummary")
124
+ for record in records:
125
+ typer.echo(_tsv_row(record))
126
+ return
127
+ for record in records:
128
+ typer.echo(json.dumps(_json_record(record)))
129
+
130
+
131
+ def _tsv_row(record: ExperimentIndexEntry) -> str:
132
+ return "\t".join(
133
+ [
134
+ record.sha[:7],
135
+ record.date,
136
+ _clean(_metric_pairs(record)),
137
+ _clean(record.document.summary),
138
+ ]
139
+ )
140
+
141
+
142
+ def _json_record(record: ExperimentIndexEntry) -> dict[str, object]:
143
+ return {
144
+ "sha": record.sha,
145
+ "short_sha": record.sha[:7],
146
+ "date": record.date,
147
+ "summary": record.document.summary,
148
+ "metrics": record.document.metrics,
149
+ "references": [
150
+ {"commit": reference.commit, "why": reference.why}
151
+ for reference in record.document.references
152
+ ],
153
+ }
154
+
155
+
156
+ def _metric_pairs(record: ExperimentIndexEntry) -> str:
157
+ return ", ".join(
158
+ f"{name}={json.dumps(value)}" for name, value in record.document.metrics.items()
159
+ )
160
+
161
+
162
+ def _clean(value: str) -> str:
163
+ return value.replace("\t", " ").replace("\r", " ").replace("\n", " ").strip()
@@ -0,0 +1,166 @@
1
+ from typing import Annotated
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.prompt import Confirm, Prompt
6
+
7
+ from autoevolve.harnesses import Harness, get_harness_spec
8
+ from autoevolve.repository import PROBLEM_FILE
9
+ from autoevolve.scaffold import Scaffolder
10
+
11
+ app = typer.Typer()
12
+ console = Console(highlight=False)
13
+
14
+
15
+ @app.command(
16
+ "init",
17
+ rich_help_panel="Human",
18
+ short_help="Set up PROBLEM.md and agent instructions.",
19
+ help=(
20
+ "Set up PROBLEM.md and agent instructions.\n\n"
21
+ f"If {PROBLEM_FILE} does not exist, init writes a stub. If it already exists, "
22
+ "init leaves it unchanged. If no harness is provided, init prompts for one. "
23
+ "Use --yes to skip confirmation prompts and write files immediately."
24
+ ),
25
+ )
26
+ def init(
27
+ harness: Annotated[Harness | None, typer.Option(help="Target agent harness.")] = None,
28
+ continue_hook: Annotated[
29
+ bool,
30
+ typer.Option(help="Install a continue-forever stop hook for supported harnesses."),
31
+ ] = False,
32
+ yes: Annotated[bool, typer.Option(help="Skip confirmation prompts.")] = False,
33
+ ) -> None:
34
+ scaffolder = Scaffolder()
35
+ if harness is None:
36
+ choice = Prompt.ask(
37
+ "Harness",
38
+ choices=[item.value for item in Harness],
39
+ default=Harness.CLAUDE.value,
40
+ console=console,
41
+ )
42
+ selected = Harness(choice.strip())
43
+ else:
44
+ selected = harness
45
+ spec = get_harness_spec(selected)
46
+ if continue_hook and not spec.supports_continue_hook:
47
+ raise RuntimeError(f'Continue hooks are not supported for harness "{selected.value}".')
48
+ if spec.supports_continue_hook and not continue_hook and not yes:
49
+ continue_hook = Confirm.ask(
50
+ f"Install a continue hook for {selected.value}?",
51
+ default=False,
52
+ console=console,
53
+ )
54
+
55
+ problem_exists = (scaffolder.root / PROBLEM_FILE).exists()
56
+ files = [PROBLEM_FILE, spec.prompt_path]
57
+ if continue_hook:
58
+ files.extend(item.path for item in spec.continue_hook_files)
59
+
60
+ console.print("[bold]Setup[/bold]")
61
+ console.print(f"[bold]{'Repository':<14}[/bold]{scaffolder.root}", soft_wrap=True)
62
+ console.print(f"[bold]{'Harness':<14}[/bold]{selected.value}")
63
+ console.print(
64
+ f"[bold]{'Problem':<14}[/bold]"
65
+ f"{'keep existing' if problem_exists else 'write'} {PROBLEM_FILE}"
66
+ )
67
+ if continue_hook:
68
+ console.print(f"[bold]{'Continue hook':<14}[/bold]enabled")
69
+ console.print()
70
+ console.print("[bold]Files[/bold]")
71
+ for path in files:
72
+ action = "keep" if path == PROBLEM_FILE and problem_exists else "write"
73
+ console.print(f"[dim]{action:<6}[/dim]{path}", soft_wrap=True)
74
+ if not yes and not Confirm.ask("Write these files?", default=True, console=console):
75
+ raise typer.Exit()
76
+
77
+ written = scaffolder.apply_init(selected, continue_hook)
78
+ console.print()
79
+ console.print("[bold green]autoevolve initialized[/bold green]")
80
+ if written:
81
+ console.print(f"[bold]{'Written':<14}[/bold]{written[0]}", soft_wrap=True)
82
+ for path in written[1:]:
83
+ console.print(f"{'':14}{path}", soft_wrap=True)
84
+ _print_next_step(selected, spec.display_name, spec.handoff_prompt)
85
+
86
+
87
+ def _print_next_step(harness: Harness, display_name: str, handoff_prompt: str) -> None:
88
+ console.print()
89
+ console.print("[bold cyan]Next Step[/bold cyan]")
90
+ if harness is Harness.OTHER:
91
+ console.print("Tell your coding agent to:")
92
+ console.print(f' "{handoff_prompt}"', soft_wrap=True)
93
+ return
94
+ console.print(f"Open {display_name} and type:")
95
+ console.print(f" [bold]{handoff_prompt}[/bold]", soft_wrap=True)
96
+
97
+
98
+ @app.command(
99
+ "validate",
100
+ rich_help_panel="Human",
101
+ short_help="Check that the repo is ready for autoevolve.",
102
+ help=(
103
+ "Check that the repo is ready for autoevolve.\n\n"
104
+ "validate checks the required autoevolve files and validates the current "
105
+ "experiment record when one is present."
106
+ ),
107
+ )
108
+ def validate() -> None:
109
+ problems = Scaffolder().validate()
110
+ if problems:
111
+ raise RuntimeError("\n".join(problems))
112
+ typer.echo("OK: repository is ready for autoevolve.")
113
+
114
+
115
+ @app.command(
116
+ "update",
117
+ rich_help_panel="Human",
118
+ short_help="Update detected prompt files to the latest version.",
119
+ help=(
120
+ "Update detected prompt files to the latest version.\n\n"
121
+ "update refreshes any detected harness prompt files in the current "
122
+ "repository. It asks before overwriting PROGRAM.md unless --yes is set."
123
+ ),
124
+ )
125
+ def update(
126
+ yes: Annotated[bool, typer.Option(help="Skip confirmation prompts.")] = False,
127
+ ) -> None:
128
+ scaffolder = Scaffolder()
129
+ prompt_files = scaffolder.prompt_files()
130
+ if not prompt_files:
131
+ raise RuntimeError("No prompt files found. Run autoevolve init first.")
132
+
133
+ updated: list[str] = []
134
+ skipped: list[str] = []
135
+ typer.echo("detected prompts:")
136
+ for prompt_file in prompt_files:
137
+ relative = prompt_file.path.relative_to(scaffolder.root).as_posix()
138
+ typer.echo(f" - {relative} ({prompt_file.harness})")
139
+ if relative == "PROGRAM.md" and not yes:
140
+ if not typer.confirm("Overwrite PROGRAM.md?", default=False):
141
+ skipped.append(relative)
142
+ continue
143
+ scaffolder.update_prompt(prompt_file)
144
+ updated.append(relative)
145
+
146
+ typer.echo("")
147
+ if updated:
148
+ typer.echo("updated:")
149
+ for path in updated:
150
+ typer.echo(f" - {path}")
151
+ if skipped:
152
+ typer.echo("skipped:")
153
+ for path in skipped:
154
+ typer.echo(f" - {path}")
155
+
156
+
157
+ @app.command(
158
+ "dashboard",
159
+ rich_help_panel="Human",
160
+ short_help="Open the experiment dashboard.",
161
+ help="Open the experiment dashboard.\n\nMonitor experiment progress in an interactive TUI.",
162
+ )
163
+ def dashboard() -> None:
164
+ from autoevolve.dashboard import DashboardApp
165
+
166
+ DashboardApp(cwd=".").run()