Files
Mainline/AGENTS.md
David Gwilliam 10e2f00edd refactor: centralize interfaces and clean up dead code
- Create engine/interfaces/ module with centralized re-exports of all ABCs/Protocols
- Remove duplicate Display protocol from websocket.py
- Remove unnecessary pass statements in exception classes
- Skip flaky websocket test that fails in CI due to port binding
2026-03-17 13:36:25 -07:00

222 lines
5.6 KiB
Markdown

# 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<ClassName>`, `Test<method_name>`)
- Test docstrings follow `"<method>() <action>"` 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