Skip to content

CONVENTIONS

Mike Crowe edited this page Aug 15, 2026 · 4 revisions

Coding Conventions

Analysis Date: 2026-08-15

Code Style

  • Linter: ruff >=0.16, configured in pyproject.toml [tool.ruff]
  • Formatter: none — formatting is deliberately NOT enforced. Layout is hand-tuned; do not run ruff format. Only correctness and security rules are enabled.
  • Line length: 100 (ruff line-length = 100)
  • Target version: py312 — the floor of requires-python, not the newest interpreter. Do not use 3.13-only constructs.

Ruff rule set

# pyproject.toml [tool.ruff.lint]
select = ["E9", "F", "B", "S", "PLE", "RUF", "BLE"]
ignore = [
    "S603",  # subprocess without shell=True — intentional throughout
    "S607",  # partial executable path — PATH resolution is by design
]

assert is allowed in tests (tests/** ignores S101). The S rules replace bandit — no separate bandit dependency. BLE is active so existing # noqa: BLE001 markers stay meaningful.

Type Annotations

Use from __future__ import annotations at the top of every module. This is universal across all source files:

# src/harnessed/launcher.py, src/harnessed/schema.py, src/harnessed/paths.py …
from __future__ import annotations
  • Type checker: pyright, typeCheckingMode = "basic", configured in pyproject.toml [tool.pyright]
  • pythonVersion = "3.12" matches the ruff floor
  • include = ["src", "tests", "tools"]; exclude = ["src/harnessed/catalog"] (container scripts)
  • Target: 84 pyright errors across all 128 included files. Do not widen this.

Data Modelling: Dataclasses, Not Pydantic

Use @dataclass (stdlib) with field(default_factory=…) for structured data. Pydantic is not in the dependency list.

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

Naming Patterns

  • Modules: snake_case.py (e.g., hosthome.py, credmounts.py, launchenv.py)
  • Functions: snake_case. Public API uses descriptive verbs (load_stack, find_in_catalog). Internal helpers are prefixed with _ (_load_yaml, _resolve_dir, _bounded).
  • Classes: PascalCase (SchemaError, McpServer, LaunchSpec, _WarnCountingConsole). Private classes may use leading _.
  • Constants: UPPER_SNAKE_CASE (CONTAINER_HOME, USERNS_ARG, HARNESS_CONFIG_DIR)
  • Module-private: prefix with _. Double-underscore is not used.

Exception Hierarchy

Define custom exceptions that extend Exception, not built-ins. Provide a one-line docstring explaining what condition the exception represents:

# 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)."""
# src/harnessed/paths.py
class HomeNotFoundError(RuntimeError):
    """harnessed's catalog could not be located — see `harnessed_home`."""

Raise SchemaError subclasses from parsers and validators. Let them propagate to the CLI layer (launcher.py), which prints a one-line rejection and exits.

Output / Logging

Use the two process-wide rich.Console instances from src/harnessed/console.py. Never construct a Console in any other module.

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

Import at the module level in callers:

from .console import _err, _out

Both consoles count warnings automatically via _WarnCountingConsole.print. rich.markup.escape is used when interpolating untrusted strings into markup. There is no logging module usage — all user-facing output goes through _out/_err.

Subprocess Calls

All subprocess calls go through helpers in src/harnessed/proc.py: _run, _bounded, _say. Never call subprocess.run directly from business logic.

The project drives podman, git, mise, and other host binaries as subprocesses; S603/S607 are suppressed project-wide for this reason. PATH resolution of binaries is intentional.

Import Organization

Imports follow PEP 8 grouping (stdlib → third-party → local), then intra-package relative imports:

# src/harnessed/schema.py
import hashlib          # stdlib
import json
import re
from dataclasses import dataclass, field
from pathlib import Path

from ruamel.yaml import YAML             # third-party
from ruamel.yaml.error import MarkedYAMLError

from . import paths                      # intra-package relative

For large modules (launcher.py), intra-package imports may be deferred into function bodies to avoid circular imports or to keep a module's import footprint small.

Module Docstrings

Every module starts with a docstring explaining its single responsibility, what it does NOT do, and which design decisions it embodies:

# src/harnessed/console.py
"""The two process-wide Consoles harnessed's CLI prints through.

They live here rather than in `launcher` so that a module extracted OUT of launcher can report an
error on the SAME console instance instead of constructing a second one. …
"""
# src/harnessed/schema.py
"""Parse + validate recipe.yaml / stack.yaml into typed objects.

EMIT-ONLY assembler component: this module only reads files and builds in-memory
objects. It never invokes podman/docker and never writes anything. …
"""

Inline Comments

Inline comments are verbose and explain the WHY, including issue/bead IDs as historical provenance (e.g., # bd harnessed-rv2.1). These IDs reference retired tracker entries and are breadcrumbs, not active links. Comments frequently describe what WOULD go wrong if the pattern changed, making them self-documenting for reviewers.

Path Resolution

All host-side and container-side path resolution is centralised in src/harnessed/paths.py. No caller computes profile dirs, instance names, or container paths independently. Always use paths.harnessed_home() as the anchor — never the CWD.

Toolchain

  • pnpm, never raw npm/npx (pnpm dlx replaces npx). Enforced by the RecipeLintError lint.
  • uvx for light Python MCP servers.
  • Every download must be pinned — no @latest, --branch main. Validated by PinValidationError.
  • Python toolchain managed by mise (mise.toml). Always use mise exec -- uv …; uv is not on PATH directly.

Clone this wiki locally