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 +0 -0
- autoevolve/app.py +100 -0
- autoevolve/commands/__init__.py +0 -0
- autoevolve/commands/analytics.py +163 -0
- autoevolve/commands/human.py +166 -0
- autoevolve/commands/inspect.py +512 -0
- autoevolve/commands/lifecycle.py +79 -0
- autoevolve/dashboard.py +2127 -0
- autoevolve/git.py +175 -0
- autoevolve/harnesses.py +153 -0
- autoevolve/models/__init__.py +0 -0
- autoevolve/models/experiment.py +63 -0
- autoevolve/models/git.py +32 -0
- autoevolve/models/lineage.py +18 -0
- autoevolve/models/types.py +22 -0
- autoevolve/models/worktree.py +24 -0
- autoevolve/problem.py +44 -0
- autoevolve/prompt.py +157 -0
- autoevolve/repository.py +459 -0
- autoevolve/scaffold.py +88 -0
- autoevolve/worktree.py +186 -0
- autoevolve_cli-0.1.0.dist-info/METADATA +105 -0
- autoevolve_cli-0.1.0.dist-info/RECORD +25 -0
- autoevolve_cli-0.1.0.dist-info/WHEEL +4 -0
- autoevolve_cli-0.1.0.dist-info/entry_points.txt +2 -0
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()
|