agentleFS
NodeJSmithNodeJSmith/hassette/CLAUDE.md

hassette

CLAUDE.md81 starsChanged 7 months agoMITView on GitHub

Hassette is an async-first Python framework for building Home Assistant automations. It emphasizes type safety (Pydantic models), dependency injection (FastAPI-style), and async/await patterns. Python 3.11+. The repo is a uv workspace of three packages released in lockstep from one release-please train (single "." component, one release PR, one tag): hassette (root), wire/ → hassette-wire, and client/ → hassette-client. Each pins the ones it depends on exactly (hassette-wire==X). The root version is bumped by release-please's python release-type; the members' version lines…

  • Installs packages

We split this CLAUDE.md into the rules it gives an agent

9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  • could be automatic: a setting could enforce it
  1. Very specific:

    After publish-docker, the bump-addon job commits the add-on's version and a CHANGELOG entry to NodeJSmith/hassette-addon (spec 129 D12), …

    line 49
  2. Distinctive:

    A pointer names something that exists: When you move or rename a doc section, grep for references to it (including code comments) and …

    line 91
  3. Very specific:

    Test infrastructure, factories, and mock strategy: .claude/rules/test-conventions.md and tests/TESTING.md.

    line 112
  4. Very specific:

    Run pytest with -n 4, never -n auto.

    line 113
  5. Distinctive:

    Run any test you fix or modify before committing — code inspection misses marker filtering, warning config, fixture scoping, and async …

    line 114
  6. Very specific:

    UI changes: use the demo stack (mise run demo) for visual QA, and capture screenshots only via scripts/capture_screenshots.py, never raw …

    line 120
  7. Very specific:

    Issues: read .claude/reference/github-workflow.md before filing — titles, required type/area/size labels, topic labels, body sections.

    line 124
  8. Very specific:

    Read .claude/rules/changelog-quality.md before creating a PR with a breaking change or reviewing a release-please PR.

    line 138
  9. Very specific:

    No blanket # type: ignore — use # pyright: ignore[reportXxx].

    line 149

≈ “Common” is an estimate: at least 1 in 5 of the 25,680 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

Hassette is an async-first Python framework for building Home Assistant automations. It emphasizes type safety (Pydantic models), dependency injection (FastAPI-style), and async/await patterns. Python 3.11+.

Common Commands

uv sync                                   # install dependencies
uv run nox -s dev                         # tests, local dev loop
uv run nox -s tests                       # tests across Python 3.11–3.14 (CI)
uv run nox -s tests_with_coverage         # backend coverage — never `pytest --cov` (under-reports; see tests/TESTING.md)
uv run pytest tests/integration/test_api.py::test_function_name -v
cd frontend && npm install                # once per worktree — worktrees don't share node_modules
cd frontend && npm run test:coverage      # frontend coverage

prek -a                                   # lint + format (all pre-commit hooks)
prek run pyright -a --stage pre-push      # type check — pyright is a pre-push hook, so `prek -a` skips it

uv run mkdocs serve                       # docs site
hassette run                              # start the server
hassette status | app | listener | log | job   # query a running instance (--app, --instance, --since, --json)

# Deterministic telemetry DB for QA/screenshots/demos (scenarios: healthy, empty, degraded,
# error, large-volume, lifecycle, adversarial; generators in scripts/seed_scenarios/)
uv run python scripts/seed_db.py --scenario healthy --output /tmp/hassette-healthy.db

Workspace Packages

The repo is a uv workspace of three packages released in lockstep from one release-please train (single "." component, one release PR, one tag): hassette (root), wire/ → hassette-wire, and client/ → hassette-client. Each pins the ones it depends on exactly (hassette-wire==X). The root version is bumped by release-please's python release-type; the members' version lines and every exact pin carry an # x-release-please-version marker that a generic extra-file rewrites — don't remove the marker when editing those lines. release-please-config.json lists every file carrying a marker under extra-files, and the release build fails if a member's version differs from hassette's.

The release workflow builds each package into its own dist/<name>/ directory and publishes them in three separate jobs, chained with needs: in order: publish-pypi-wire, then publish-pypi-client, then publish-pypi-hassette. The order is load-bearing (each pins the previous), so don't collapse them back into one job. Each package gets its own GitHub Environment — release-wire, release-client, release — not a shared one: PyPI's pending trusted-publisher match (for a project that doesn't exist on PyPI yet) is keyed on (repository, workflow file, environment name) alone, since there's no project to disambiguate by, so two packages sharing one environment register as the same pending publisher and PyPI rejects the second. release-wire/release-client already exist (created via gh api, deployment branch policy main + v** tags copied from release; no secrets/variables — trusted publishing is OIDC-only). Each new package still needs its own trusted publisher registered by hand on PyPI before its first publish (workflow release-please.yml, its own environment name) — that side isn't automatable from here. Run a member's tests with uv run nox -s wire / uv run nox -s client; uv run nox -s client_compat checks that the newest published client release reachable from HEAD handles every web API response HEAD sends (parsing, probe bodies, error mapping; fixtures generated per seed scenario). A new workspace member touches every one of these:

  • wire/**-style path filters in the tests, lint, e2e-tests and docker-tests workflows, plus a uv run nox -s <member> line in tests.yml's workspace-members job
  • pyrightconfig.json, prek.toml, and [tool.house-lint]/norecursedirs in pyproject.toml
  • ruff.toml's per-file-ignores, if the member's tests use assert (a <member>/tests/**/*.py → ["S101"] entry)
  • release-please-config.json's extra-files (the # x-release-please-version marker on the member's pyproject.toml does nothing without this)
  • Dockerfile, if the member is a runtime dependency of hassette rather than a dev-only tool
  • renovate.json, if the member is pinned exactly by another workspace member (stops Renovate from bumping a pin release-please owns)
  • release-please.yml: a new publish-pypi-<name> job reusing the publish-pypi-steps YAML anchor with its own PACKAGE_NAME/PACKAGES_DIR env (the comment above those jobs says why it can't be a reusable workflow or composite action), chained after the member it depends on via needs:, with its own GitHub Environment (created the same way as release-wire/release-client, same branch policy) — and add the job to release-verify's needs: and result checks

After publish-docker, the bump-addon job commits the add-on's version and a CHANGELOG entry to NodeJSmith/hassette-addon (spec 129 D12), using the release-please app's token scoped to that repo; tools/release/bump_addon.py holds its stable-only, no-downgrade and equal-is-a-no-op rules, and release-verify fails the release if it fails.

Architecture

ComponentLocationRole
Hassettesrc/hassette/core/core.pyCoordinator: connects to HA over WebSocket, manages app lifecycle and services
Appsrc/hassette/app/app.pyBase class for user automations, generic over AppConfig. Hooks: on_initialize, on_shutdown. Each app gets its own Bus, Scheduler, Api, StateManager
Bussrc/hassette/bus/Event pub/sub: on_state_change, on_attribute_change, on_call_service, on, and one-shot wait_for. Glob patterns, predicates, conditions, debounce, throttle
Schedulersrc/hassette/scheduler/schedule(func, trigger) plus run_in/run_once/run_every/run_daily/run_cron. Triggers in hassette.scheduler.triggers; custom ones implement TriggerProtocol. Job groups and jitter
Apisrc/hassette/api/HA REST/WebSocket: get_state, get_states, call_service, set_state, fire_event. Api.helpers (HelperClient) does CRUD on HA helper entities with per-domain @overloads
StateManagersrc/hassette/state_manager/Cached, typed state: self.states.light, self.states[CustomState], self.states.get("light.kitchen")
Event handlingsrc/hassette/event_handling/predicates (P), conditions (C), accessors (A), dependencies (D)
Resourcessrc/hassette/resources/Resource/Service base classes: lifecycle hooks, child tracking, supervision
Websrc/hassette/web/FastAPI backend for the monitoring UI; SPA in frontend/

Type conversion registries: STATE_REGISTRY (HA entity types → model classes), TYPE_REGISTRY (scalar field conversion).

Rules that bite everywhere:

  • Bus registration methods are async and must be awaited. name= is required on every DB-registered listener (ListenerNameRequiredError otherwise); wait_for is the exception — its name is auto-generated.
  • Handlers get only the parameters their signature declares. Don't pad a handler with an unused event: Event[Any] just to match siblings.
  • One live Hassette() per process at a time. Process-global state assumes it; don't add concurrency guards for it.

Subsystem internals live in path-scoped rules under .claude/rules/ and load automatically when you work in the matching files: core-startup.md (startup readiness, app bootstrap gate, StateProxy), resource-lifecycle.md (lifecycle, teardown reports), bus-internals.md, web-api.md, frontend-css.md, demo-and-screenshots.md, and tests-*.md rules for several test directories with notable local fixtures (not every directory has one — check its conftest.py). New subsystem guidance goes there too, not in nested CLAUDE.md files.

Designing the Framework

Hassette is a framework. Its real callers are user apps in other repositories, which this repo never sees.

  • In-repo usage is not evidence of demand. Grepping this repo measures blast radius (call sites to update), not whether users rely on something or struggle with it. examples/, docs/, and internal apps only show what they happen to demonstrate. Don't dismiss an edge case or rank one API path over another because nothing here exercises it — assume users will. API changes still need changelog entries and migration guidance with zero in-repo callers.
  • Keep the public surface small. App carries almost nothing; a new helper goes on the most specific component whose role it extends (e.g. presence helpers on StateManager, not Api or App delegators). AppDaemon's broad self.* surface is a cautionary tale — diverge from its names and signatures where it chose poorly.
  • Convenience APIs must earn their place. Ask what boilerplate it saves, whether callers could write it trivially against the primitives, and whether the wrapper adds failure modes (races, weaker typing, untestable paths) the primitive doesn't have. If it saves a few lines and adds new failure modes, document the pattern instead.
  • Every new Api method is testable through the harness. Define how it behaves on RecordingApi (records an ApiCall for writes, reads a harness seed surface for reads) and add the seed API if needed (harness mechanics: .claude/rules/test-conventions.md). Never stub it with NotImplementedError.
  • HA-facing design follows Home Assistant / HACS conventions (Integration Quality Scale, current core idioms). A divergence needs a stated, strong reason.

Internal Documentation

Applies to CLAUDE.md, .claude/, design/, and code comments — not the user-facing docs site, where stating defaults and values is the point.

  • State decisions; point to code for values. Write rules, rationale, and which file, component, token, or function is canonical. Don't restate a value, class list, signature, or output format that lives in code — a copied value drifts silently and nothing catches it. A pointer can't drift and steers the reader to reuse instead of reimplement.
    • Exception: a command may carry the literal value it needs to be copy-pasteable. When that value is defined in code, name the source next to it (e.g. -n 4 — XDIST_WORKERS in noxfile.py) so drift can be checked.
  • A pointer names something that exists. When you move or rename a doc section, grep for references to it (including code comments) and update them.

App Pattern

class MyConfig(AppConfig):
    model_config = SettingsConfigDict(env_prefix="my_")
    setting_name: str = "default"

class MyApp(App[MyConfig]):
    async def on_initialize(self):
        await self.bus.on_state_change("light.kitchen", handler=self.on_light_change, name="kitchen_light")
        await self.scheduler.run_in(self.my_task, 5)

    async def on_light_change(self, event: RawStateChangeEvent):
        pass

Testing

  • Bugs: reproduce, write a failing test (RED), fix (GREEN), run the full file. Patterns for races, timeouts, and sentinel filtering are in .claude/rules/regression-test-patterns.md (loads under tests/).
  • Test infrastructure, factories, and mock strategy: .claude/rules/test-conventions.md and tests/TESTING.md.
  • Run pytest with -n 4, never -n auto. That matches CI (XDIST_WORKERS in noxfile.py), and --dist loadscope makes the worker count change which modules share a worker.
  • Run any test you fix or modify before committing — code inspection misses marker filtering, warning config, fixture scoping, and async timing.

Before Shipping

  • Core changes (src/hassette/core/, src/hassette/resources/, src/hassette/types/enums.py): the local gate is the unit/integration suite, lint + pyright, and the schema-freshness check. CI runs nox -s system_with_coverage and nox -s e2e on every push, so don't run those locally unless you're debugging (uv run nox -s system needs Docker).
  • Docs changes (docs/pages/): run doc-persona-review and doc-accuracy-review on the touched pages. See .claude/rules/doc-rules.md.
  • UI changes: use the demo stack (mise run demo) for visual QA, and capture screenshots only via scripts/capture_screenshots.py, never raw Playwright. Details in .claude/rules/demo-and-screenshots.md.

GitHub

  • Issues: read .claude/reference/github-workflow.md before filing — titles, required type/area/size labels, topic labels, body sections. For pre-existing findings from /mine-clean-code or review runs, also read .claude/reference/clean-code-findings.md.
  • PR checks: a red file-sizes or duplicate-code check always means this PR regressed something; fix it in-PR. Details in the same reference file.
  • Closing issues: repeat the keyword for each issue (closes #1, closes #2); Closes #1, #2 only closes the first.
  • CodeRabbit auto-review is disabled. Once the PR is created and marked ready, comment @coderabbitai review.
  • Roadmap: design/roadmap.md is the source of truth for Now/Next and forced orderings — read it when asked what to pick up next. Each initiative is a GitHub milestone whose description holds its Done-when. A follow-up issue joins an initiative's milestone only if the Done-when fails without it; otherwise leave it milestone-less (epic:*/topic:* labels are fine).

Changelog

Don't edit CHANGELOG.md by hand in feature PRs — release-please generates it from conventional commits, and the squash-merged PR title becomes the changelog line. The only hand edit is curating CHANGELOG.md on a release-please PR's branch; changelog-quality.md covers that procedure, including why that PR's body is never touched.

  • feat, fix, perf, refactor, and docs appear in the changelog; use chore: for internal work (design/, .claude/, research, tooling), ci:, or test:. docs: is only for user-facing docs.
  • PR titles describe the user-visible outcome, not implementation details or a bundle of fixes.
  • A breaking change (feat!: etc.) needs exactly one BREAKING CHANGE: footer at the end of the PR body — release-please drops every footer after the first.

Read .claude/rules/changelog-quality.md before creating a PR with a breaking change or reviewing a release-please PR.

Design Artifacts

Internal design documents live in design/, not docs/ (the published docs site). design/context.md is the canonical UI design direction. See design/README.md for what goes where.

Code Style

  • Line length 120, type hints everywhere, Google-style docstrings.
  • Ruff + Pyright (commands above).
  • No from __future__ import annotations.
  • No blanket # type: ignore — use # pyright: ignore[reportXxx].
  • Mixins declare the attributes they expect from their host as class-level annotations. Don't add a runtime Protocol to validate the host; a TYPE_CHECKING-only one is fine when annotations can't type it (see _LifecycleHostP in resources/mixins.py).
  • User-facing API methods keep their full docstrings even when siblings repeat them (scheduler run_*, bus on_* delegates) — users read the method they call. Don't flag that as duplication; duplicated bodies are still fair game.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.