Build Dobby
Dobby's house-facing contract is testable without a physical house.
The repository includes a deterministic fake for the full application and a disposable Home Assistant for the real network boundary. Use both. One proves household behavior without a provider or a house. The other proves the wiring against Home Assistant itself.
Two rigs
mix test, and mix phx.server with no house file named, run against a fake Home Assistant that lives in the repository. No HA, no network, no model calls. No environment variable changes that.
The second rig is a real Home Assistant in a container, for testing the real client: connection, token, subscription, service calls. It is disposable. Its state is gitignored, and remaking it costs two commands.
The local Home Assistant
Requires Docker.
cd dev/home-assistant
docker compose up -d
First boot takes a minute or two. The instance listens on http://localhost:8123. It runs configuration.seed.yaml, which turns on HA's demo integration: virtual thermostats (climate.hvac, climate.heatpump, climate.ecobee), lights, and vacuums.
Initialize it once. This creates the owner account, sets US units so HA reports in Fahrenheit, finishes onboarding, and mints the token Dobby logs in with. It prints the credentials and saves them nowhere.
mix run --no-start dev/home-assistant/onboard.exs
docker compose restart # so every entity reports in °F
Point Dobby at it. config/homes/local.yaml is the house file for this rig. It takes its token from .env.
cp .env.example .env # then fill in the token onboard.exs printed
mix phx.server
What you should see
[info] connected to Home Assistant 2026.8.2 at http://localhost:8123 home "local" with 4 device(s) %{ id: "thermostat:main", name: "demo thermostat", type: :thermostat, available: true, target_temperature_f: 70, min_temperature_f: 60, max_temperature_f: 76, current_temperature_f: 72, hvac_mode: :cool } … initial state sync: ok
From mix dobby.ha.verify against the rig, August 22, 2026. Bring Dobby home shows the round trip.
To list what the instance has: Developer Tools → States in HA, or
curl -s -H "Authorization: Bearer $DOBBY_HA_TOKEN" \
http://localhost:8123/api/states | jq -r '.[].entity_id'
Credentials for HA integrations (a cloud thermostat's account, a robot's password) go into the HA UI, not into .env or the repository. Never commit a token.
The Roomba
The one integration that needs a human at the robot. In HA: Settings → Devices & services → Add integration → iRobot Roomba, then enter the robot's IP. When the flow asks for the password, put the robot on its dock and hold its Home button until it plays a tone. HA fetches the credential in that window.
Fill the vacuum.* entity ID HA creates into the commented vacuum:roomba entry in config/homes/local.yaml, uncomment it, and restart. Verify then shows the robot with its battery.
Two kinds of tests
Replay runs on every mix test and in CI, against the fake Home Assistant. It cannot reach a model provider. It proves the machinery.
Eval runs a real model, for real money. It asks whether the model exercises judgment. It has caught a tool description that misled the model, a prompt that let the model act on an ambiguous request, and a model echoing the prompt's framing into a reply.
mix test # replay: no HA, no network, no model calls
DOBBY_EVAL=1 mix test --only eval # eval: real inference, real money
mix precommit # compile --warnings-as-errors, unused deps, format, test
DOBBY_EVAL lifts the guard in config/test.exs. Without it every provider points at a closed loopback port and the whole tier fails on connection refused. --only keeps the replay tier out of a run whose guard is down, where a replay test can reach a model provider and cost real money.
A rule for fixtures: if a test seeds what production builds, the test is lying.
Tidewave
In development, the server also serves Tidewave at /tidewave/mcp, on this machine only. An agent working on Dobby's code can run Elixir in the live node, read its logs, and query the database. The MCP door is for an agent working on the house; this one is for an agent working on Dobby.
claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp
What you should see
get_logs, get_source_location, get_docs, project_eval, execute_sql_query, browser_eval, create_design_canvas
Tidewave's tools. Requests from a browser or from another machine are refused.
Reading the repository
CLAUDE.md at the root is the map. Module docs explain why each thing is the way it is, and what was rejected.
The changelog
Every branch says what it changed for the house before it merges. bin/changelog creates CHANGELOG/unreleased/<branch>.md from a template and stages it. Fill the headings that apply; the ones left empty are dropped when the release is cut. Write the bullets the way the commit messages are written: sentences about what changed for the house, not which module moved.
bin/changelog # create the branch's entry if missing, print its path
bin/changelog -e # the same, opened in $EDITOR
bin/changelog -d # preview the release entry the unreleased files would make
A release folds those files into one entry. Set the version in mix.exs, run the script with it, commit, and push the tag. The tag must name the version in mix.exs, or the build refuses it. The workflow publishes the entry as the release notes, with GitHub's list of pull requests beneath.
bin/changelog -r 0.2.0 # CHANGELOG/0.2.0.md, the top of CHANGELOG.md, unreleased emptied
git commit -m "Dobby 0.2.0"
git tag v0.2.0 && git push origin main v0.2.0
CHANGELOG.md is the whole record, newest first. The upgrade walk is what a household does with it.
This guide is hand-written HTML under docs/, served as committed. Edit the page; there is no build.