Skip to content

CONVENTIONS

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

Coding Conventions

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


Module structure

Every source module opens with a triple-quoted docstring that names what the module does and what it deliberately does not do:

# src/harnessed/emit.py:1-12
"""Write the assembled artifacts into the mounted build dir (EMIT ONLY).

Pure file emission — no podman/docker, no daemon. ...
"""
# src/harnessed/schema.py:1-14
"""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.
"""

The first sentence is the one-line summary; subsequent paragraphs capture constraints and design rationale.


Future annotations

All source files begin with:

from __future__ import annotations

This appears at line 16 of schema.py, line 14 of emit.py, line 14 of paths.py, and line 13 of launcher.py. It is required in every new module.


Import order

Standard library → third-party → local (relative). Relative imports use from . import module for whole-module imports and from .module import Name for specific symbols:

# src/harnessed/emit.py:14-28
import json
import re
import shutil
from copy import deepcopy
from pathlib import Path

from . import paths
from .schema import McpServer, Recipe, Stack
# src/harnessed/launcher.py:12-41
import json
import os
import re
import shutil
import subprocess
import sys
...
import typer
from rich.console import Console

from . import emit
from . import paths
from . import persist
from .paths import CONTAINER_HOME, instance_name, is_built, profile_dir, project_relpath
from .assemble import assemble
from .schema import (
    HARNESS_CONFIG_DIR,
    InitSpec,
    SchemaError,
    ...
)

Type hints

Type hints are mandatory everywhere — parameters, return types, and local variables when the type is non-obvious.

  • Union types use the X | Y syntax (not Union[X, Y]): str | None, Path | None, dict[str, list[HookCommand]]
  • Generic collections use lowercase built-ins: list[str], dict[str, str], tuple[Stack, list[Recipe]]
  • Optional from typing appears only in older code in launcher.py; new code uses X | None
# src/harnessed/schema.py:30-38
def _resolve_dir(root: Path | None, kind: str, name: str) -> Path: ...

def load_stack_with_recipes(
    root: Path | None, stack_name: str, *, strict: bool = False
) -> tuple[Stack, list[Recipe]]: ...

Dataclasses

All typed value objects use @dataclass. Mutable defaults always use field(default_factory=...):

# src/harnessed/schema.py:88-110
@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)

Every dataclass that carries parsed YAML includes a raw: dict field (forward-compatibility, design D-14):

# src/harnessed/schema.py:584-603
@dataclass
class Recipe:
    name: str
    description: str = ""
    servers: list[McpServer] = field(default_factory=list)
    ...
    raw: dict = field(default_factory=dict)

Properties on dataclasses are used for derived values:

# src/harnessed/schema.py:108-110
@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

Exception hierarchy

Custom exceptions subclass from a base SchemaError:

# src/harnessed/schema.py:65-75
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 floating ref (--branch main/master, :latest, @latest) was detected."""

Exception messages are always actionable — they state what is wrong, what the valid options are, and include the offending value with !r:

# src/harnessed/schema.py:296-301
if scope in _PERSIST_RESERVED_SCOPES:
    raise SchemaError(
        f"persist entry [{i}]: scope: {scope!r} is reserved for a future release "
        "and not yet implemented"
    )
if scope not in _PERSIST_VALID_SCOPES:
    raise SchemaError(
        f"persist entry [{i}]: unknown scope {scope!r} — "
        f"valid values: {', '.join(sorted(_PERSIST_VALID_SCOPES))}"
    )

When a typo is the likely cause, the error suggests the intended field (did you mean ...?):

# src/harnessed/schema.py:745-752
described = [
    f"{f!r}" + (f" (did you mean {s!r}?)" if (s := _suggest_field(f)) else "")
    for f in unknown
]
raise SchemaError(
    f"{manifest}: unknown recipe field(s) in --strict mode: {', '.join(described)}. ..."
)

Constants and module-level values

Public constants: UPPER_CASE. Private module-level values: _lower_case with a leading underscore.

# src/harnessed/paths.py:17-19
CONTAINER_HOME = Path("/home/harnessed")
HATAGO_PORT = 3535

# src/harnessed/schema.py:162-173
_PERSIST_NAME_COMPONENT_RE = re.compile(r"^[A-Za-z0-9._-]+$")
_PERSIST_VALID_SCOPES = {"workspace", "project", "global"}
_PERSIST_RESERVED_SCOPES = {"repo"}

Use frozenset for immutable sets of valid values:

# src/harnessed/schema.py:509-518
_VALID_HOOK_EVENTS = frozenset({
    "SessionStart", "Setup", "SessionEnd",
    "UserPromptSubmit", ...
})

# src/harnessed/schema.py:423
_INIT_VALID_SCOPES = frozenset({"workspace", "project"})

Private vs public helpers

All helper functions that are not part of the public API are prefixed with _:

# src/harnessed/schema.py
def _load_yaml(path: Path) -> dict: ...      # private
def _parse_persist(raw_persist) -> PersistSpec: ...   # private
def load_recipe(recipe_dir: Path, *, strict: bool = False) -> Recipe: ...  # public

Keyword-only arguments

Flags and options that change behavior use keyword-only syntax (* separator):

# src/harnessed/schema.py:756
def load_recipe(recipe_dir: Path, *, strict: bool = False) -> Recipe: ...

# src/harnessed/emit.py:237
def write_derived_dockerfile(
    profile_dir: Path, stack: Stack, recipes: list[Recipe], *, with_scan: bool = True
) -> Path: ...

Warn callbacks

Functions that may produce non-fatal warnings accept an optional warn callable rather than logging directly. The caller decides what to do with warnings:

# src/harnessed/emit.py:120-142
def read_baked_settings(text: str | None, *, warn=None) -> dict | None:
    ...
    _warn = warn or (lambda _m: None)
    ...
    _warn("image settings.json is not valid JSON — keeping harnessed's default")

In tests this is captured with warns.append:

# tests/test_emit.py:163-165
warns: list[str] = []
assert read_baked_settings(None, warn=warns.append) is None
assert warns == []

Subprocess calls

subprocess.run is called with explicit capture_output=True, text=True, check=True. Exception handling always catches the tuple (subprocess.CalledProcessError, FileNotFoundError, OSError):

# src/harnessed/paths.py:146-155
try:
    result = subprocess.run(
        ["git", "-C", str(project_path), "rev-parse", "--path-format=absolute", "--git-common-dir"],
        capture_output=True,
        text=True,
        check=True,
    )
    p = Path(result.stdout.strip())
    return p if p.exists() else None
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
    return None

Boolean probes (image exists, container running) use returncode == 0:

# src/harnessed/launcher.py:81-85
def _image_exists(rt: str, image: str) -> bool:
    return subprocess.run(
        [rt, "image", "inspect", image],
        capture_output=True,
    ).returncode == 0

Output and error reporting

No logging module. All output goes through rich.console.Console. Two module-level instances in launcher.py:

# src/harnessed/launcher.py:49-50
_out = Console()
_err = Console(stderr=True)

Errors use Rich markup: [bold red]error:[/bold red]. Exit with raise typer.Exit(1) — never sys.exit() directly.


YAML parsing

A single module-level YAML instance with safe type and pure=True:

# src/harnessed/schema.py:27
_yaml = YAML(typ="safe", pure=True)

All YAML loads go through _load_yaml(path) which enforces that the result is a dict and returns {} for empty files:

# src/harnessed/schema.py:77-84
def _load_yaml(path: Path) -> dict:
    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

JSON writing

All JSON files are emitted through _write_json which creates parent dirs, indents with 2 spaces, and appends a trailing newline:

# src/harnessed/emit.py:42-44
def _write_json(path: Path, data: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")

Inline design comments

Non-obvious decisions reference the design doc code inline. This keeps the comment localized to the code it explains:

# src/harnessed/schema.py:596
# GAP 2: declarative Claude Code hooks, merged into settings.json by emit.py. {EventName: [...]}.

# src/harnessed/emit.py:29
# hatago's single Streamable-HTTP endpoint (design D-04; default port 3535, `HATAGO_PORT`
# overridable). Single source: `paths.hatago_endpoint()`. The harness `.mcp.json` points ONLY
# here — never at a stdio server directly.

Do not use vague comments like # fix. Use the design/plan code the relevant decision lives in (design §N, plan N-N, BLD-03, T-08-01, etc.).

Clone this wiki locally