Document · doc-0002

Wave operating model

Everything the campaign model itself defines lives in the Agent fan-out protocol (canonical) doc. This document adds only what is true of this repository and would be wrong to paste into another one. If a section here could survive being copied to a different project, it belongs in the protocol, not here.

Rules this project added, and the failure behind each

A lane may not widen device scope. v1.0.0 (2026-07-03) deliberately narrowed the integration to MT sensors and deleted MR/MS/MV support across six lanes. The pre-existing todos-final.txt queue still described building MX, MV, MG, switch/PoE, SSID and 6 GHz features, and survived in the repo for seven months after the decision that killed it — an agent picking work off it would have spent a wave rebuilding what a previous wave removed. That file is gone; scope now widens only by an explicit decision recorded as a task, never by a lane inferring it.

Assume every Meraki SDK response is a shape you have not seen. See the defect section — this is the repo’s most expensive recurring failure, and it is a rule rather than advice because the failure mode is silent.

A lane that adds a sensor must add its translation key in the same change. translations/ carries en.json plus de/es/fr. A new entity whose key is missing renders as a raw slug in the UI, and nothing in make lint or make test catches it.

Do not put customer log excerpts in a task. Bug reports arrive as pasted Home Assistant debug logs containing Meraki network IDs, device serials and network names. Those are exactly what the identifier sweep in AGENTS.md exists to keep out of backlog/, and this tracker is committed to a public repository. Quote the shape and the API call, never the customer’s line.

Recurring defects in this codebase, with instances

Envelope-shape assumptions — the expensive one. Meraki endpoints return either a bare list or a {"items": [...], "meta": {...}} envelope, and which one is not stable across SDK versions. Issue #303: after 0.38.0, getOrganizationSensorGatewaysConnectionsLatest began returning the dict form. The code iterated it as a list, and every MT reading silently stopped — no exception, no log, just empty sensors, for every user. The fix unwraps items and wraps the loop defensively. The current handler in hubs/organization.py is the pattern to copy: it isinstance-checks each row, reads the serial from the nested sensor.serial rather than a top-level key, and leaves absent values as None rather than 0. Never assume a top-level key; never coerce a missing reading to zero, because a zeroed temperature reads as real data in Home Assistant.

Model matching by string prefix. utils/device_info.py:31 maps devices with normalized_model.startswith(prefix.upper()). Issue #218: a CW9171I was initially reported by the Meraki dashboard as an MR36, worked fine, and then broke the moment Meraki corrected the model name — because the new name matched no prefix. Meraki renames and introduces models on its own schedule, so prefix tables go stale silently and the symptom is “entities became unavailable”, not an error. Changes here need a fallback path, not just another prefix.

Supported model, unsupported metrics. Issues #143/#144: the MT20 was recognised as a device but had no door or battery metrics mapped, so the integration logged “No binary sensors created for device … no supported metrics found” and created nothing. Device support and metric support are two separate tables and adding one without the other produces a device with no entities.

Org-scoped assumptions against network-scoped keys. Issue #132: a network-level API key produced a cascade of 404s from getOrganizationDevicesSystemMemoryUsageHistoryByInterval, getNetworkDevices and getOrganizationClientsOverview that read like an integration bug. Only org-level keys are supported. New org-wide calls inherit this, and the 404 message Meraki returns (“please wait a minute if the key or org was just newly created”) actively misleads.

The through-line: this integration fails quietly. Four of the five defects above surface as missing or unavailable entities rather than as an error. When a lane touches data fetching or entity creation, the acceptance criterion is what does a user see when this path returns nothing — an empty result that logs nothing is a defect here even when it is technically correct.

Lanes and the shared resource

Natural lane boundaries, which are also the file-ownership boundaries:

Lane Owns
API / hubs hubs/organization.py, hubs/network.py
Device + metric mapping devices/mt.py, devices/organization.py, utils/device_info.py
Entity platforms sensor.py, binary_sensor.py, button.py, entities/base.py
Config + migration config/, config_flow.py, repairs.py
Services + events services/, events.py, coordinator.py
Docs docs/, README.md

Wiring files are never edited in parallelconst.py, entities/factory.py, __init__.py, config/schemas.py, manifest.json and everything under translations/ are registries that every lane wants to touch. One lane owns them for a wave, or a dedicated wiring pass stitches them at the end.

The exclusive resource is the live Meraki organization, and it is a rate budget, not a lock. utils/rate_limiter.py deliberately spends only budget_fraction = 0.8 of the org’s call allowance. That fraction is per-process and knows nothing about other agents, so N lanes each exercising the integration against the live org spend N × 80% of a budget that only has 100% in it, and the symptom is 429s that look like a code defect in whichever lane is unlucky. Only one lane at a time may run against the live org. Everything else uses tests/builders/device_builder, sensor_builder, hub_builder, presets — which is why those exist.

There is no test organization containing every device model. The CW9171I in #218 had to be fixed without one, on a reporter’s word. A lane that cannot reproduce against real hardware says so in its final summary rather than claiming verification it did not do.

Ownership and the escape hatch

One file, one owner, for the duration of a wave. A lane that needs a change in another lane’s file does not edit it and does not stop: it records the exact change it needs — file, symbol, and the signature or key it expects — in its own task notes with --append-notes, and continues against its assumption. The wiring pass reconciles. A boundary with no escape hatch is a stop condition wearing a safety label.

Where a lane hits something its brief does not cover — a scope question, an API shape nobody has seen, a decision about user-visible behaviour — it stops and returns the question rather than inventing an answer. One round-trip is cheaper than the rewrite.

Run-end against this tracker

Task state is the record; there is no run-report file.

The gate is make lint and make test, inherited by every task as its definition of done. Claiming green without having seen the output is the one unforgivable step.

The run’s closing message goes to the terminal as a covering note answering what did this run learn that no single task captures. Nothing durable may live only there — if it matters, it is already in a task or a doc before the note is written. Writing the note is the last unit of work, not a reply to a request.

View the source file on GitHub