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

5.6 KiB

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

mise run install          # Install dependencies
# Or: uv sync --all-extras   # includes mic, websocket, sixel support

Available Commands

# 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

# 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:

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)

# 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
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:

@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:

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
# 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:

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:

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