diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..2363d4ca8a3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yml,yaml,json,toml}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d8711e15ea..60bc35bd1b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: CI on: push: @@ -6,37 +6,38 @@ on: pull_request: branches: [main] -# Cancel in-progress runs for the same PR/branch concurrency: - group: tests-${{ github.ref }} + group: ci-${{ github.ref }} cancel-in-progress: true jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 3 + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - run: uvx ruff check run_agent.py model_tools.py toolsets.py cli.py hermes_state.py batch_runner.py tools/ hermes_cli/ gateway/ agent/ cron/ + - run: uvx ruff format --check run_agent.py model_tools.py toolsets.py cli.py hermes_state.py batch_runner.py tools/ hermes_cli/ gateway/ agent/ cron/ + test: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v5 - - - name: Set up Python 3.11 - run: uv python install 3.11 - - - name: Install dependencies - run: | + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - run: uv python install 3.11 + - run: | uv venv .venv --python 3.11 source .venv/bin/activate uv pip install -e ".[all,dev]" - - - name: Run tests - run: | + - run: | source .venv/bin/activate python -m pytest tests/ -q --ignore=tests/integration --tb=short -n auto env: - # Ensure tests don't accidentally call real APIs OPENROUTER_API_KEY: "" OPENAI_API_KEY: "" NOUS_API_KEY: "" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..4b7b6ec298d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.12 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-yaml + args: [--allow-multiple-documents] + - id: check-added-large-files + args: [--maxkb=500] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000000..352d57b990f --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +.DEFAULT_GOAL := help +SHELL := /bin/bash +VENV := .venv +UV := uv + +SRC := run_agent.py model_tools.py toolsets.py cli.py hermes_state.py batch_runner.py \ + tools/ hermes_cli/ gateway/ agent/ cron/ + +# ─── Setup ────────────────────────────────────────────────────────────────────── + +.PHONY: setup sync clean + +setup: ## Full dev setup (venv + deps + pre-commit) + $(UV) venv $(VENV) --python 3.11 + . $(VENV)/bin/activate && $(UV) pip install -e ".[all,dev]" + . $(VENV)/bin/activate && $(UV) pip install -e "./mini-swe-agent" + . $(VENV)/bin/activate && pre-commit install + @echo "\n✅ Setup complete. Run: source $(VENV)/bin/activate" + +sync: ## Reinstall deps into existing venv + . $(VENV)/bin/activate && $(UV) pip install -e ".[all,dev]" + +clean: ## Remove build artifacts and caches + rm -rf .ruff_cache .mypy_cache .pytest_cache dist build *.egg-info + find . -type d -name __pycache__ -not -path "./.venv/*" -exec rm -rf {} + + +# ─── Quality ──────────────────────────────────────────────────────────────────── + +.PHONY: lint fmt check + +lint: ## Check lint + formatting (no changes) + . $(VENV)/bin/activate && ruff check $(SRC) + . $(VENV)/bin/activate && ruff format --check $(SRC) + +fmt: ## Auto-fix lint + format + . $(VENV)/bin/activate && ruff format $(SRC) + . $(VENV)/bin/activate && ruff check --fix $(SRC) + +check: lint test ## Lint + test (mirrors CI) + +# ─── Test ─────────────────────────────────────────────────────────────────────── + +.PHONY: test test-fast test-watch + +test: ## Run full test suite + . $(VENV)/bin/activate && python -m pytest tests/ -q --ignore=tests/integration --tb=short + +test-fast: ## Run tests with fail-fast + . $(VENV)/bin/activate && python -m pytest tests/ -q --ignore=tests/integration --tb=short -x + +test-watch: ## Rerun tests on file changes + . $(VENV)/bin/activate && python -m watchfiles "python -m pytest tests/ -q --ignore=tests/integration --tb=short -x" $(SRC) tests/ + +# ─── Dev Servers ──────────────────────────────────────────────────────────────── + +.PHONY: dev-cli dev-gateway + +dev-cli: ## Auto-restart CLI on file changes + . $(VENV)/bin/activate && python -m watchfiles "python -m hermes_cli.main" $(SRC) + +dev-gateway: ## Auto-restart gateway on file changes + . $(VENV)/bin/activate && python -m watchfiles "python -m gateway.run" $(SRC) + +# ─── Misc ─────────────────────────────────────────────────────────────────────── + +.PHONY: help + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/pyproject.toml b/pyproject.toml index eb1ae9e53f2..3c1368298ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ dependencies = [ [project.optional-dependencies] modal = ["swe-rex[modal]>=1.4.0"] daytona = ["daytona>=0.148.0"] -dev = ["pytest", "pytest-asyncio", "pytest-xdist", "mcp>=1.2.0"] +dev = ["pytest", "pytest-asyncio", "pytest-xdist", "mcp>=1.2.0", "ruff", "pre-commit", "watchfiles"] messaging = ["python-telegram-bot>=20.0", "discord.py>=2.0", "aiohttp>=3.9.0", "slack-bolt>=1.18.0", "slack-sdk>=3.27.0"] cron = ["croniter"] slack = ["slack-bolt>=1.18.0", "slack-sdk>=3.27.0"] @@ -79,6 +79,38 @@ py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajector [tool.setuptools.packages.find] include = ["tools", "hermes_cli", "gateway", "cron", "honcho_integration"] +[tool.ruff] +target-version = "py311" +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "UP", "B", "SIM"] +ignore = [ + "E402", # late imports — intentional throughout codebase + "E501", # line too long — handled by formatter where it can + "E731", # lambda assignments — used in registry pattern + "E741", # ambiguous variable name + "F811", # redefined unused — intentional overrides + "F841", # unused variable + "B007", # unused loop variable + "B904", # raise from + "B905", # zip strict + "B027", # empty method without abstract decorator + "SIM102", # collapsible if + "SIM103", # needless bool + "SIM105", # suppressible exception + "SIM108", # ternary + "SIM110", # reimplemented builtin + "SIM112", # uncapitalized env var + "SIM115", # open file with context handler + "SIM117", # multiple with statements + "SIM118", # in-dict-keys + "SIM212", # if-expr twisted arms +] + +[tool.ruff.lint.isort] +known-first-party = ["tools", "hermes_cli", "gateway", "agent", "cron"] + [tool.pytest.ini_options] testpaths = ["tests"] markers = [