# Agent Development Guide ## Development Environment This project uses: - **mise** (mise.jdx.dev) - tool version manager and task runner - **uv** - fast Python package installer - **ruff** - linter and formatter (line-length 88, target Python 3.10) - **pytest** - test runner with strict marker enforcement ### Setup ```bash mise run install # Install dependencies # Or: uv sync --all-extras # includes mic, websocket, sixel support ``` ### Available Commands ```bash # Testing mise run test # Run all tests mise run test-cov # Run tests with coverage report pytest tests/test_foo.py::TestClass::test_method # Run single test # Linting & Formatting mise run lint # Run ruff linter mise run lint-fix # Run ruff with auto-fix mise run format # Run ruff formatter # CI mise run ci # Full CI pipeline (topics-init + lint + test-cov) ``` ### Running a Single Test ```bash # Run a specific test function pytest tests/test_eventbus.py::TestEventBusInit::test_init_creates_empty_subscribers # Run all tests in a file pytest tests/test_eventbus.py # Run tests matching a pattern pytest -k "test_subscribe" ``` ### Git Hooks Install hooks at start of session: ```bash ls -la .git/hooks/pre-commit # Verify installed hk init --mise # Install if missing mise run pre-commit # Run manually ``` ## Code Style Guidelines ### Imports (three sections, alphabetical within each) ```python # 1. Standard library import os import threading from collections import defaultdict from collections.abc import Callable from dataclasses import dataclass, field from typing import Any # 2. Third-party from abc import ABC, abstractmethod # 3. Local project from engine.events import EventType ``` ### Type Hints - Use type hints for all function signatures (parameters and return) - Use `|` for unions (Python 3.10+): `EventType | None` - Use `dict[K, V]`, `list[V]` (generic syntax): `dict[str, list[int]]` - Use `Callable[[ArgType], ReturnType]` for callbacks ```python def subscribe(self, event_type: EventType, callback: Callable[[Any], None]) -> None: ... def get_sensor_value(self, sensor_name: str) -> float | None: return self._state.get(f"sensor.{sensor_name}") ``` ### Naming Conventions - **Classes**: `PascalCase` (e.g., `EventBus`, `EffectPlugin`) - **Functions/methods**: `snake_case` (e.g., `get_event_bus`, `process_partial`) - **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `CURSOR_OFF`) - **Private methods**: `_snake_case` prefix (e.g., `_initialize`) - **Type variables**: `PascalCase` (e.g., `T`, `EffectT`) ### Dataclasses Use `@dataclass` for simple data containers: ```python @dataclass class EffectContext: terminal_width: int terminal_height: int scroll_cam: int ticker_height: int = 0 _state: dict[str, Any] = field(default_factory=dict, repr=False) ``` ### Abstract Base Classes Use ABC for interface enforcement: ```python class EffectPlugin(ABC): name: str config: EffectConfig @abstractmethod def process(self, buf: list[str], ctx: EffectContext) -> list[str]: ... @abstractmethod def configure(self, config: EffectConfig) -> None: ... ``` ### Error Handling - Catch specific exceptions, not bare `Exception` - Use `try/except` with fallbacks for optional features - Silent pass in event callbacks to prevent one handler from breaking others ```python # Good: specific exception try: term_size = os.get_terminal_size() except OSError: term_width = 80 # Good: silent pass in callbacks for callback in callbacks: try: callback(event) except Exception: pass ``` ### Thread Safety Use locks for shared state: ```python class EventBus: def __init__(self): self._lock = threading.Lock() def publish(self, event_type: EventType, event: Any = None) -> None: with self._lock: callbacks = list(self._subscribers.get(event_type, [])) ``` ### Comments - **DO NOT ADD comments** unless explicitly required - Let code be self-documenting with good naming - Use docstrings only for public APIs or complex logic ### Testing Patterns Follow pytest conventions: ```python class TestEventBusSubscribe: """Tests for EventBus.subscribe method.""" def test_subscribe_adds_callback(self): """subscribe() adds a callback for an event type.""" bus = EventBus() def callback(e): return None bus.subscribe(EventType.NTFY_MESSAGE, callback) assert bus.subscriber_count(EventType.NTFY_MESSAGE) == 1 ``` - Use classes to group related tests (`Test`, `Test`) - Test docstrings follow `"() "` pattern - Use descriptive assertion messages via pytest behavior ## Workflow Rules ### Before Committing 1. Run tests: `mise run test` 2. Run linter: `mise run lint` 3. Review changes: `git diff` ### On Failing Tests - **Out-of-date test**: Update test to match new expected behavior - **Correctly failing test**: Fix implementation, not the test **Never** modify a test to make it pass without understanding why it failed. ## Architecture Overview - **Pipeline**: source → render → effects → display - **EffectPlugin**: ABC with `process()` and `configure()` methods - **Display backends**: terminal, websocket, sixel, null (for testing) - **EventBus**: thread-safe pub/sub messaging - **Presets**: TOML format in `engine/presets.toml` Key files: - `engine/pipeline/core.py` - Stage base class - `engine/effects/types.py` - EffectPlugin ABC and dataclasses - `engine/display/backends/` - Display backend implementations - `engine/eventbus.py` - Thread-safe event system