Skip to content

CONVENTIONS

Mike Crowe edited this page Jul 31, 2026 · 4 revisions

Coding Conventions

Analysis Date: 2026-07-31

Code style and patterns observed across src/harnessed/*.py. Follow these exactly when adding or editing source files.


Code Style

  • Formatter / linter: No ruff config or pre-commit config is present. Style is enforced by code review and the rules below, not a formatter.
  • Python version: 3.12 required (requires-python = ">=3.12"). Use 3.12+ syntax throughout.
  • No .editorconfig: Not present.

Import Organization

Every source module begins with from __future__ import annotations as the very first line. This is present in all 10 source files and is not optional.

After the future import, use this order with blank lines between each group:

  1. from __future__ import annotations
  2. stdlib imports (alphabetical within the block)
  3. Third-party imports (rich, ruamel.yaml, typer)
  4. Local imports (from . import …, from .module import …)

Example from src/harnessed/assemble.py:

from __future__ import annotations

import hashlib
import os
from dataclasses import dataclass
from pathlib import Path

from . import emit, staleness
from .schema import (
    McpServer,
    Recipe,
    Stack,
    load_service,
    load_stack_with_recipes,
    validate_pin,
)

Test files do not use from __future__ import annotations — only source modules do.


Naming Patterns

  • Functions: snake_case. Private (module-internal) functions have a leading underscore: _load_yaml, _parse_expect, _merge_servers.
  • Public functions: no leading underscore: assemble, compute_recipe_hash, harnessed_home.
  • Classes: PascalCase: SchemaError, McpServer, AssembleResult, CapabilityReport.
  • Constants: ALL_CAPS at module level: CONTAINER_HOME, HATAGO_PORT. Private constants keep the underscore prefix: _BASE_IMAGE, _CLAUDE_IMAGE.
  • Variables and parameters: snake_case.

Data Objects: Dataclasses, Not Pydantic

Use @dataclass for all structured data objects. Pydantic is not a dependency and is not used.

  • Mutable defaults use field(default_factory=…).
  • The raw: dict field pattern preserves unknown YAML fields for forward compatibility. The parser is deliberately tolerant of unknown fields (design D-14).

Example from src/harnessed/schema.py:

@dataclass
class McpServer:
    name: str
    command: str | None = None
    args: list[str] = field(default_factory=list)
    transport: str = "stdio"
    url: str | None = None
    env: dict = field(default_factory=dict)
    raw: dict = field(default_factory=dict)

    @property
    def is_stdio_child(self) -> bool:
        """A stdio server hatago must bake + spawn (vs a network-native URL proxy)."""
        return self.transport == "stdio" and self.command is not None

Use @property for computed boolean or derived attributes — never compute them in the caller.


Type Hints

Type hints are required on every function signature and every dataclass field. Use Python 3.12+ union syntax:

def _resolve_dir(root: Path | None, kind: str, name: str) -> Path: ...
def source_checkout() -> Path | None: ...
def print_report(report: CapabilityReport, console: Console | None = None) -> None: ...

Never write Optional[X] — write X | None instead.


Error Handling

Define a custom exception hierarchy. Every module with a distinct failure mode declares its own exception class, subclassed from a meaningful base:

# src/harnessed/schema.py
class SchemaError(Exception):
    """A recipe/stack manifest is missing a required field or is malformed."""

class RecipeLintError(SchemaError):
    """A recipe uses raw npm/npx instead of the pnpm equivalent (BLD-03 supply-chain lint)."""

class PinValidationError(SchemaError):
    """A recipe Dockerfile contains a floating ref (--branch main/master, :latest, @latest)."""

Other exceptions by module:

  • HomeNotFoundError(RuntimeError)paths.py
  • CollisionError(Exception)synclinks.py
  • ScanError(Exception)scan.py
  • StaleProfileError(Exception)staleness.py
  • ResolveError(RuntimeError)update.py
  • PersistDeniedError(SchemaError), PersistNotAllowlistedError(SchemaError), PersistOwnershipError(SchemaError)persist.py

Error messages must include context. Always name the path, field, and what was wrong:

raise SchemaError(f"{path}: expected a YAML mapping at the top level")
raise SchemaError(f"persist entry [{entry_idx}]: 'name' must be a non-empty string")

Rich Console and Output

There is no logging framework. All user-facing output goes through rich.console.Console.

In launcher.py, two module-level consoles are constructed at import time. This is intentional: rich reads FORCE_COLOR when a Console is constructed, and these consoles are built at module import — before any environment patches can run. Moving them into a function or fixture would break the FORCE_COLOR guard in tests/conftest.py:

# src/harnessed/launcher.py
_out = _WarnCountingConsole()
_err = _WarnCountingConsole(stderr=True)

Other modules accept console: Console | None = None and construct a local default. This lets callers inject a specific console without polluting the module namespace:

# src/harnessed/report.py
def print_report(report: CapabilityReport, console: Console | None = None) -> None:
    (console or Console()).print(Markdown(render_markdown(report)))

YAML Parsing

Always construct a fresh YAML instance per load call. A shared instance is not thread-safeharnessed build -j uses threads and a shared instance interleaves parses:

# src/harnessed/schema.py
def _load_yaml(path: Path) -> dict:
    # One instance per load — NOT a module-level shared instance.
    yaml = YAML(typ="safe", pure=True)
    with path.open("r", encoding="utf-8") as fh:
        data = yaml.load(fh)
    if data is None:
        return {}
    if not isinstance(data, dict):
        raise SchemaError(f"{path}: expected a YAML mapping at the top level")
    return data

Paths

All paths are pathlib.Path objects. Never construct paths by string concatenation.

src/harnessed/paths.py is the single source of truth. Use its functions; never re-derive:

  • harnessed_home() — the directory containing catalog/; anchors every catalog lookup and the podman build context. Never derive this from os.getcwd().
  • xdg_data_home(), xdg_config_home(), xdg_state_home(), xdg_cache_home() — XDG dirs that honor the XDG_* environment variables before defaulting.
  • profile_dir(stack, harness), host_home(stack, harness) — per-stack profile and host-config locations.

Module Docstrings

Every source module has a module-level docstring. The docstring states what the module does and what it does not do — especially "EMIT ONLY — never invokes podman":

# src/harnessed/assemble.py
"""Orchestrate the emit-only assembly of a stack into a committed profile + hatago config.

EMIT ONLY: nothing here invokes podman/docker or mounts a daemon socket.
"""

Design references are cited inline (e.g., "design §7", "D-04", "bd harnessed-8px.3") to link code to the decision that motivated it. Use the same style when adding new code that has a design ref.

Function Docstrings

Write docstrings for all public functions and for non-obvious private functions. Describe the contract (what the caller can rely on), not the implementation:

# src/harnessed/paths.py
def harnessed_home() -> Path:
    """harnessed's home: the directory that CONTAINS `catalog/`. Never derived from the CWD.

    Raises when no catalog can be found, rather than returning a plausible-looking directory
    that has none — that used to surface as a baffling "unknown stack '<x>'" for every stack.
    """

Inline comments explain why, not what. Use them for non-obvious constraints, thread-safety warnings, and trap documentation.


Catalog and Wheel Packaging

catalog/ is shipped inside the wheel via the symlink src/harnessed/catalog → ../../catalog. Two rules follow:

  • Never put host-local content inside catalog/. setuptools follows symlinks and would package private user data into a distributed wheel. Host-local symlinks live in catalog-local/, never inside catalog/.
  • Never key any path off os.getcwd(). Always anchor to harnessed_home(), so harnessed build <stack> behaves the same from any directory.

Clone this wiki locally