Document · doc-0002

Wave operating model

This project’s own rules. The campaign model itself — run contract, run modes, routing, the thread pool, child lane briefs, contract freezing, the goal-file template, the pre-flight checklist — lives in the Agent fan-out protocol (canonical) doc and is not repeated here. If something below could be pasted into another repository unchanged, it is in the wrong document.

The exclusive resource: there is one modem

http://192.168.100.1 is a single physical Sagemcom F3896 on Rob’s line. It is the only instance that exists, it serves the household’s actual internet connection, and it cannot be cloned, containerised or stood up per lane.

At most one lane may hold the modem at a time, and holding it is stated in the goal file, not assumed. Three properties make this stricter than a normal shared fixture:

For anything that is not specifically about talking to hardware, do not take the modem — build against recorded response shapes instead.

The trap that will bite a wave first: 64% of the suite skips silently

uv run pytest tests with no MODEM_PASSWORD in the environment reports 9 passed, 16 skipped (measured 2026-08-14). The integration tests in tests/test_client.py and tests/test_client_session.py skip themselves when the variable is absent. Exit code 0. Green.

So a lane can change client.py substantially, run the gate, see it pass, and have exercised none of the code it touched. This is the repo’s most expensive failure mode because the wrong signal is indistinguishable from the right one at a glance.

Every lane that reports the gate green must state which run it got9 passed, 16 skipped (no hardware) or the full run against the modem. A bare “tests pass” is not an acceptable lane report here and should be sent back. When a change touches client.py, models.py or conftest.py, hardware-backed verification is required before the task reaches Done; if the modem was unavailable, that is a Parked task with the boundary recorded, not a Done one.

Recurring defects in this codebase

tests/test_exporter.py is an empty file — 0 lines. exporter.py is 711 lines, the largest module in the project and the one carrying all the metric-naming, label and aggregation logic, and it has no unit tests at all. Two consequences for wave design: a lane adding metrics has no regression net, so its own tests are part of the deliverable rather than optional; and the file’s emptiness is easy to mistake for coverage that exists, because the file is there and pytest collects it without complaint.

Module-level metrics register into the global REGISTRY at import. MODEM_METRICS_DURATION, MODEM_UPDATE_COUNT and MODEM_LAST_UPDATE are created at exporter.py module scope, so importing the module has the side effect of registering three collectors globally. Per-scrape metrics go into a fresh CollectorRegistry instead, and metrics() concatenates the two. Anything that imports exporter twice, or that adds a module-level metric whose name already exists, raises a duplicate registration error at import time — which surfaces as a collection error across the whole suite, not as a failure in the file that caused it. Prefer the per-scrape registry; adding a module-level metric is a change to a shared seam and needs stating in the goal file.

exporter.py’s imports are deliberately not at the top of the file, and ruff is configured to allow it. warnings.filterwarnings("ignore", message="'return' in a 'finally' block") must run before prometheus_async is imported, because its Twisted module emits a SyntaxWarning on Python 3.14+. pyproject.toml carries a per-file E402 ignore for exactly this. A lane doing a tidy-up pass — “move the imports to the top”, “sort the imports” — silently reintroduces the warning while leaving the linter happy. Do not reorder those lines. The commit that established this is 8d3ea2e.

The instructions themselves drifted and were wrong. Until this migration, CLAUDE.md documented the formatter as black with flake8 and isort. .pre-commit-config.yaml has only ever configured ruff and ruff-format. A lane that trusted the instructions would have written config for three tools the project does not use. AGENTS.md is now the single canonical file and CLAUDE.md imports it, so that class of drift cannot recur — but the lesson generalises: check a claim in the instructions against the config file that would implement it before building on it.

Lane conventions

exporter.py is a single-owner file for the whole wave. At 711 lines it holds the gather loop, every metric definition and every label decision, so parallel metric work converges on it by construction. Either one lane owns all exporter changes, or metric work is serialised behind a wiring pass. Do not split it by metric family and hope the edits do not overlap — they will, in the gather block.

The natural parallel seams that do not collide: log_parser.py + test_log_parser.py, profile_messages.py + test_profile_messages.py, models.py + test_models.py, and CI/workflow files. client.py and models.py move together often enough that giving them to two lanes needs the model dataclass signatures frozen in the goal file first.

pyproject.toml, uv.lock, .pre-commit-config.yaml and .github/workflows/ are wiring files — one lane or the wiring pass, never concurrent. uv.lock in particular produces conflicts that are tedious rather than interesting, and Renovate is already the main author of changes to it.

Ownership and the escape hatch

A lane owns its files and does not edit outside them. The escape hatch: when a lane finds that finishing requires a change to a file it does not own, it stops and returns the question with the specific file and the specific change. It does not make the edit, and it does not silently narrow its own task to the part that avoids the file. A boundary with no escape hatch is a stop condition wearing a safety label.

Only the root agent commits. Lanes leave the working tree dirty and describe what they changed. This matters more than usual here because the repo is on main with a push-straight-to-main policy and no PR gate to catch a bad commit.

If the working tree carries changes that are not the wave’s, stage explicit pathspecs. Never git add -A or git commit -a.

Run-end against this tracker

Task state is the record; nothing durable may live only in the terminal.

The covering note to the terminal carries only what no single task captures — what the run learned about the codebase as a whole. It is the last unit of work, not a reply to a request.

View the source file on GitHub