Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions evolution_kernel/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@


def main(argv: Sequence[str] | None = None) -> int:
raw = list(sys.argv[1:] if argv is None else argv)
if raw and raw[0] == "init":
from .init_wizard import main as _init_main
return _init_main(raw[1:])
parser = argparse.ArgumentParser(
prog="evolution-kernel",
description="Run Evolution Kernel experiments.",
Expand Down
76 changes: 76 additions & 0 deletions evolution_kernel/init_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Interactive `evolution-kernel init` — 3 questions, drops a valid evolution.yml.

No interactive prompt libraries. No template base class. No Python template
generator. Templates are plain YAML files under `evolution_kernel/templates/`
with `{{mission}}` and `{{allowed_paths_yaml}}` placeholders substituted via
str.replace before the result is fed through `load_config` for validation.
"""
from __future__ import annotations

import sys
from importlib.resources import files
from pathlib import Path

from .config import ConfigError, load_config

TEMPLATES = ("lint", "coverage", "perf", "benchmark", "custom")


def _ask(prompt: str, default: str | None = None) -> str:
suffix = f" [{default}]" if default else ""
raw = input(f"{prompt}{suffix}: ").strip()
return raw or (default or "")


def _render(name: str, mission: str, allowed_paths: list[str]) -> str:
body = files("evolution_kernel.templates").joinpath(f"{name}.yml").read_text(encoding="utf-8")
paths_yaml = "\n".join(f" - \"{p}\"" for p in allowed_paths) or " []"
return body.replace("{{mission}}", mission.replace('"', '\\"')).replace(
"{{allowed_paths_yaml}}", paths_yaml
Comment on lines +26 to +29
)


def main(argv: list[str] | None = None) -> int:
out_path = Path("evolution.yml")
if out_path.exists():
print(f"error: {out_path} already exists — remove it or run init in an empty dir", file=sys.stderr)
return 2

print("evolution-kernel init — 3 questions, drops ./evolution.yml")
mission = _ask("1) Mission (one sentence)", "Improve the target codebase toward its goal")

print("2) Template:")
for i, name in enumerate(TEMPLATES, start=1):
print(f" {i}. {name}")
pick_raw = _ask(" Pick 1-5", "1")
try:
idx = int(pick_raw)
template = TEMPLATES[idx - 1]
except (ValueError, IndexError):
print(f"error: not a valid template choice: {pick_raw!r}", file=sys.stderr)
return 2

paths_raw = _ask("3) Allowed mutation paths (comma-separated)", "src/")
allowed_paths = [p.strip() for p in paths_raw.split(",") if p.strip()]
if not allowed_paths:
print("error: at least one allowed path is required", file=sys.stderr)
return 2

rendered = _render(template, mission, allowed_paths)
out_path.write_text(rendered, encoding="utf-8")

try:
load_config(str(out_path))
except ConfigError as e:
out_path.unlink(missing_ok=True)
print(f"error: rendered template did not validate ({e}) — please file a bug", file=sys.stderr)
return 1
Comment on lines +59 to +67

print(f"\nwrote {out_path.resolve()} (template: {template})")
print("next:")
print(" evolution-kernel --config evolution.yml --repo /path/to/target --ledger /tmp/ledger --loop")
return 0


if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())
39 changes: 39 additions & 0 deletions evolution_kernel/templates/benchmark.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Template: benchmark
# Use when the goal is FunSearch / AlphaEvolve-style population search on a
# numeric benchmark — k-branch parallel exploration with a `fitness` evaluator.
# Substituted by `evolution-kernel init`. Edit freely.

mission: "{{mission}}"

llm:
provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY

coding_agent:
tool: claude-code

history:
max_entries: 20

evidence_sources:
- type: shell
command: "python3 scripts/benchmark.py" # must emit JSON with a numeric `score`

mutation_scope:
allowed_paths:
{{allowed_paths_yaml}}

hard_stops:
max_iterations: 30
max_consecutive_failures: 6
max_total_usd: 5.00
max_total_tokens: 3000000

parallel:
k_branches: 3 # explore 3 candidates per round; best fitness wins

roles:
planner: ["python3", "roles/planner.py"]
executor: ["bash", "roles/executor.sh"]
evaluator: ["python3", "roles/evaluator.py"]
35 changes: 35 additions & 0 deletions evolution_kernel/templates/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Template: coverage
# Use when the goal is to raise test coverage on the target package.
# Substituted by `evolution-kernel init`. Edit freely.

mission: "{{mission}}"

llm:
provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY

coding_agent:
tool: claude-code

history:
max_entries: 10

evidence_sources:
- type: shell
command: "pytest --cov --cov-report=term-missing -q"

mutation_scope:
allowed_paths:
{{allowed_paths_yaml}}

hard_stops:
max_iterations: 15
max_consecutive_failures: 3
max_total_usd: 2.00
max_total_tokens: 1000000

roles:
planner: ["python3", "roles/planner.py"]
executor: ["bash", "roles/executor.sh"]
evaluator: ["python3", "roles/evaluator.py"]
36 changes: 36 additions & 0 deletions evolution_kernel/templates/custom.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Template: custom
# Blank-ish starter. Fill in evidence_sources and roles to fit your project.
# Substituted by `evolution-kernel init`. Edit freely.

mission: "{{mission}}"

llm:
provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY

coding_agent:
tool: claude-code

history:
max_entries: 10

# Add one or more file / shell / http sources below. Example:
# - type: shell
# command: "pytest -q"
evidence_sources: []

mutation_scope:
allowed_paths:
{{allowed_paths_yaml}}

hard_stops:
max_iterations: 10
max_consecutive_failures: 3
max_total_usd: 1.00
max_total_tokens: 500000

roles:
planner: ["python3", "roles/planner.py"]
executor: ["bash", "roles/executor.sh"]
evaluator: ["python3", "roles/evaluator.py"]
35 changes: 35 additions & 0 deletions evolution_kernel/templates/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Template: lint
# Use when the goal is to drive a lint/formatter to zero violations.
# Substituted by `evolution-kernel init`. Edit freely.

mission: "{{mission}}"

llm:
provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY

coding_agent:
tool: claude-code # claude-code | aider

history:
max_entries: 10

evidence_sources:
- type: shell
command: "ruff check ."

mutation_scope:
allowed_paths:
{{allowed_paths_yaml}}

hard_stops:
max_iterations: 10
max_consecutive_failures: 3
max_total_usd: 1.00
max_total_tokens: 500000

roles:
planner: ["python3", "roles/planner.py"]
executor: ["bash", "roles/executor.sh"]
evaluator: ["python3", "roles/evaluator.py"]
35 changes: 35 additions & 0 deletions evolution_kernel/templates/perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Template: perf
# Use when the goal is to improve runtime/throughput on a measurable workload.
# Substituted by `evolution-kernel init`. Edit freely.

mission: "{{mission}}"

llm:
provider: anthropic
model: claude-sonnet-4-6
api_key_env: ANTHROPIC_API_KEY

coding_agent:
tool: claude-code

history:
max_entries: 10

evidence_sources:
- type: shell
command: "python3 scripts/measure_perf.py" # must emit JSON with a numeric `latency_ms` or `throughput`

mutation_scope:
allowed_paths:
{{allowed_paths_yaml}}

hard_stops:
max_iterations: 20
max_consecutive_failures: 5
max_total_usd: 3.00
max_total_tokens: 1500000

roles:
planner: ["python3", "roles/planner.py"]
executor: ["bash", "roles/executor.sh"]
evaluator: ["python3", "roles/evaluator.py"]
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ evolution-kernel = "evolution_kernel.cli:main"
# in a clean environment.
packages = ["evolution_kernel"]

[tool.setuptools.package-data]
evolution_kernel = ["templates/*.yml"]

41 changes: 41 additions & 0 deletions tests/test_init_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Init wizard: every template renders to a config that load_config accepts."""
from __future__ import annotations

import io
from pathlib import Path

import pytest

from evolution_kernel import init_wizard
from evolution_kernel.config import load_config


@pytest.mark.parametrize("idx,name", list(enumerate(init_wizard.TEMPLATES, start=1)))
def test_each_template_round_trips(tmp_path: Path, monkeypatch, capsys, idx: int, name: str) -> None:
monkeypatch.chdir(tmp_path)
answers = iter([f"unit test mission for {name}", str(idx), "src/, tests/"])
monkeypatch.setattr("builtins.input", lambda _prompt: next(answers))

rc = init_wizard.main([])

assert rc == 0, f"init failed for template {name}: {capsys.readouterr().err}"
out = tmp_path / "evolution.yml"
assert out.exists()
cfg = load_config(str(out))
assert cfg.mission.startswith("unit test mission")
assert cfg.mutation_scope.allowed_paths == ("src/", "tests/")


def test_refuses_to_overwrite(tmp_path: Path, monkeypatch, capsys) -> None:
monkeypatch.chdir(tmp_path)
(tmp_path / "evolution.yml").write_text("placeholder\n")
monkeypatch.setattr("builtins.input", lambda _prompt: "x")
assert init_wizard.main([]) == 2
assert "already exists" in capsys.readouterr().err


def test_bad_template_pick(tmp_path: Path, monkeypatch, capsys) -> None:
monkeypatch.chdir(tmp_path)
answers = iter(["mission", "99", "src/"])
monkeypatch.setattr("builtins.input", lambda _prompt: next(answers))
assert init_wizard.main([]) == 2
Loading