PL

plenum-architecture-contract

A source-of-truth reference for Plenum system design, invariants, and architectural constraints to guide safe development.

Install

mkdir -p .claude/skills/plenum-architecture-contract && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/16699" && unzip -o skill.zip -d .claude/skills/plenum-architecture-contract && rm skill.zip

Installs to .claude/skills/plenum-architecture-contract

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

The load-bearing design decisions of Plenum (smart-thermostat-with-vents), the invariants that must hold, WHY each exists, and the known-weak points. Load when you need the canonical temperature-unit contract (°F storage, single write boundary, #231), the component map with real file paths, the cycle-engine invariants (mode lock, in-place trigger updates, 60s tick), the MCP loopback design, or a "before you change X, understand Y" dependency check. Also load when CLAUDE.md prose seems to disagree with the repo — this skill flags the known drift.
551 chars✓ has a “when” triggerlonger than Claude Code's old 250-char listing cap (fine on current versions)
Advanced

Key capabilities

  • Identify component responsibilities within the Plenum system
  • Understand the temperature-unit contract (all °F internally)
  • Trace the cycle-engine invariants and state machine
  • Analyze the MCP loopback design
  • Identify known weak points and dependencies

How it works

This skill documents Plenum's load-bearing design decisions, architectural invariants, and component map, explaining the rationale behind each and highlighting known weak points, with verification against the current codebase.

Inputs & outputs

You give it
Plenum system code or design documents
You get back
Understanding of Plenum's architectural invariants, component map, and design decisions

When to use plenum-architecture-contract

  • Understanding architectural invariants
  • Checking system dependencies
  • Validating design changes

About this skill

Plenum architecture contract

The design decisions this system stands on, stated as invariants with their rationale. Every claim below was verified against the repo at v0.22.1 (2026-07-04). Where DESIGN.md prose has drifted behind the code (or CLAUDE.md had, before the 2026-07-05 corrections in PR #388), the drift is called out — the repo files win.

When NOT to use this skill:

  • Domain theory (why short-cycling kills compressors, what a deadband is, HA entity semantics) → hvac-zoning-reference.
  • The blow-by-blow story of a past incident → plenum-failure-archaeology.
  • Which CI gates a change must clear, PR/release mechanics → plenum-change-control.
  • Diagnosing a live symptom → plenum-debugging-playbook.
  • Adding/locating a config knob → plenum-config-and-flags.

Jargon (once): write boundary — the POST/PUT/PATCH handlers in smart_vent/backend/api/routes.py, the only place display-unit input becomes storage °F. Delta — a temperature difference (deadband, offset): °C↔°F scales by 9/5 with no 32 offset. Ingress — Home Assistant's authenticated reverse proxy that serves the add-on UI. Zone — one thermostat plus its rooms.


1. Component map (verified file paths and responsibilities)

All backend paths are under smart_vent/backend/.

HA Core ◄─ WebSocket + REST ─► ha_client.py ◄── engine/ ◄── scheduler.py ◄── api/routes.py ◄── frontend / MCP
                                                    │              │                │
                                                    └──────────────┴── db.py ── app.db (SQLite, DATA_DIR)
FileResponsibility (verified)
main.pyEntry point. Binds aiohttp on PORT (default 8099), starts the HA client task, the Scheduler, and the HTTP MCP server (uvicorn) on MCP_PORT (default 9099). One-shot flair.db → app.db rename (#89). Security-headers middleware on every response.
ha_client.pyRaw HA WebSocket client + state cache. Normalises sensor readings to °F via per-entity unit_of_measurement (get_numeric_state, line ~172), resolves HA's system unit into ha_temp_unit from /api/config (#281), and converts stored-°F setpoints back to HA-native units on write via _setpoint_to_native (#280). Dev mode intercepts all writes here and logs [DEV] Would … instead.
scheduler.pyOrchestrator. Owns _engines: dict[thermostat_id → CycleEngine], the 60s APScheduler main_tick, log purge (24h), schedule-expiry sweep (60s, #359), daily/monthly metrics rollups (#85). Owns _active_unit and the system_enabled / developer_mode flags (persisted in system_settings).
engine/cycle_engine.pyOne instance per thermostat. The IDLE → RUNNING → TERMINATING → IDLE state machine; all safety guards (short-cycle lockout, cooling lockout, staleness filter, cycle timeout, airflow floor, #367 envelope enforcement) live in its tick. ~3600 lines.
engine/room_manager.pyResolves each room's active trigger: override > schedule > presence holdover.
engine/vent_controller.pyIssues cover open/close commands per the room-vent control_method.
api/routes.pyAll REST endpoints. The write boundary: every temperature field is converted via the units.py helpers here, then bounds-checked on the °F value. Also TEMPERATURE_FIELDS (Python side of the parity system).
api/ws_handler.pyThin WebSocket broadcaster (WSManager). Event payloads are built in scheduler.py / the engine — temperatures in them are raw °F.
units.pyThe four converters: to_f / delta_to_f / from_f / from_f_delta. Single source of truth shared by routes.py, ha_client.py, and the engine's HA-ingest helper.
db.pyaiosqlite layer. SCHEMA script (WAL mode, FK on) + append-only _MIGRATIONS list of ALTER TABLE statements + two one-shot data migrations gated by system_settings sentinels.
tz.pySingle source of local-time truth: TZ env (from the timezone add-on option via run.sh). Schedule matching and metrics bucketing use it; storage timestamps are UTC.
mcp_http.py / mcp_openapi.pyHTTP (Streamable) MCP server on port 9099; tools generated from the OpenAPI spec, each call dispatched over loopback HTTP to the REST API.
frontend/src/contexts.tsbuildUnitContext(unit) — the frontend display-conversion layer (toDisplay, toDisplayDelta, fmtTemp).

DESIGN.md is the original design doc and is stale in places (it still describes a stdio/SSE MCP server, min_open_vents as a count, a 30s monitor tick). Treat it as historical intent; this skill and the code are current.


2. The temperature-unit contract (canonical statement — owned here)

Invariant: every temperature at rest or in flight inside the system is °F. SQLite rows, engine arithmetic, scheduler state, WebSocket event payloads, API GET responses — all °F, always (Issue #123). Exactly three places convert, and nothing else may:

  1. Backend write boundary (api/routes.py): display-unit input → °F via to_f / delta_to_f (imported from units.py as _to_f / _delta_to_f), using the active unit from scheduler.get_temperature_unit(). Bounds (40–90 °F for targets, 40–100 °F for setpoints, 0–10 °F for deadband_override) are checked after conversion, on the °F value — checking the raw input is meaningless when the unit varies (.jules/sentinel.md, 2026-05-05).
  2. Frontend display layer (contexts.ts): °F → display via toDisplay / toDisplayDelta / fmtTemp, for rendering and form initialisation only. Forms hold display-unit values and submit them raw.
  3. HA I/O boundary (ha_client.py + _climate_temp_to_f in cycle_engine.py): HA-native values ↔ °F. Sensor entities carry a per-entity unit_of_measurement; °C readings are normalised through the shared to_f (#251). Climate entities report in HA's system unit (ha_temp_unit, resolved from /api/config — #281 fixed the detection), so climate reads pass through _climate_temp_to_f and setpoint writes through _setpoint_to_native (#280 — sending raw °F to a metric HA would be interpreted as °C and drive the HVAC wildly wrong).

The frontend NEVER calls toStorage / toStorageDelta on an outgoing payload. That was #231: the Thermostats form converted 16 °C → 60.8 °F via toStorage, then the backend's _to_f converted again → 141.44 °F stored. Each side's unit tests passed because each asserted a different contract. The conversion belongs to exactly one side — the backend. The toStorage helpers still exist in contexts.ts but are reserved for local validation-bound math only.

Absolute vs delta is a type distinction, not a detail. Absolute uses (f−32)×5/9; a delta uses only ×5/9. Feeding a delta (deadband, temp_offset, overshoot_delta) through the absolute converter renders a 2 °F deadband as a negative °C number (#291 was exactly this in a chart subtitle). units.py keeps all four converters side by side to make the axis visible.

The parity system keeps the contract enforced: every write-boundary temperature field is registered in TEMPERATURE_FIELDS in both routes.py (12 fields as of v0.22.1) and e2e/tests/temperature-fields.ts, with // @covers: round-trip tests, and backend/tests/test_temperature_field_parity.py fails CI on any drift. The add-a-field procedure and gates are owned by plenum-change-control; the parity-rule details by plenum-validation-and-qa.

History note: older CLAUDE.md copies placed _to_f/_delta_to_f/_from_f in backend/api/routes.py; CLAUDE.md was corrected 2026-07-05 (PR #388). They live in backend/units.py and are imported into routes.py under those aliases (routes.py lines 35–38). Same semantics; trust units.py.


3. Engine and scheduler invariants

InvariantWhyWhere verified
One CycleEngine per thermostat; 60s tick. The scheduler's _sync_engines creates/removes engines as thermostats appear in room configs; main_tick runs every 60s (max_instances=1, coalesce=True) and also fires on HA state changes. Each engine serialises its work behind an internal asyncio.Lock.Zones are independent failure domains; one thermostat's outage must not stall another's control loop (#286 hardened the per-engine exception isolation).scheduler.py lines ~53, 130–137, 470–481; cycle_engine.py tick() line 176.
Mode is locked at cycle start (_cycle_mode, cycle_engine line 100). Monitoring, at-target checks, and the setpoint all use the locked mode, not live hvac_action.During HVAC idle phases the naively inferred mode can flip tick-to-tick → heat/cool oscillation. A room that now needs the opposite direction is dropped by the mode filter; if no compatible rooms remain the cycle ends cleanly and a new one may start opposite on the next tick.docs/cycle-engine.md §"Why the mode is locked"; cycle_engine.py lines ~404, 546, 566.
Mid-cycle trigger changes are applied in place, never by teardown (#215). A changed source/target updates the running cycle and re-derives the setpoint; the cycle log stays open with a trigger updated in place setpoint-history entry.The old teardown was itself a compressor stop/start — and with the #208/#214 off-time lockout enabled, the rebuild was then blocked for the lockout window, leaving a room unconditioned (back-to-back schedule blocks at 08:00 caused a full furnace stop→start for a target that only moved up).cycle_engine.py lines ~886–911, 972; docs/safety.md §"In-place cycle updates".
The engine never converts display units. It receives °F from the DB and commands °F; ha_client.py translates to HA-native at the wire. The only conversion helpers it touches are the HA-ingest normalisers (_climate_temp_to_f, to_f on °C sensors).Conversion in two layers is how #231 happened; the engine staying unit-

Content truncated.

When not to use it

  • When seeking domain theory (use hvac-zoning-reference)
  • When wanting the story of a past incident (use plenum-failure-archaeology)
  • When needing CI gates or PR mechanics (use plenum-change-control)

Limitations

  • The repo files win if CLAUDE.md prose seems to disagree with the repo.
  • DESIGN.md is the original design doc and is stale in places.

How it compares

This approach provides a canonical statement of Plenum's architecture, including specific file paths and responsibilities, unlike general system documentation that might be outdated or less detailed.

Compared to similar skills

plenum-architecture-contract side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
plenum-architecture-contract (this skill)01moNo flagsAdvanced
llm-council47moReviewAdvanced
project-development13moReviewAdvanced
decision-critic16moNo flagsAdvanced

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

llm-council

am-will

Orchestrate a configurable, multi-member CLI planning council (Codex, Claude Code, Gemini, OpenCode, or custom) to produce independent implementation plans, anonymize and randomize them, then judge and merge into one final plan. Use when you need a robust, bias-resistant planning workflow, structured JSON outputs, retries, and failure handling across multiple CLI agents.

410

project-development

muratcankoylan

This skill should be used when the user asks to "start an LLM project", "design batch pipeline", "evaluate task-model fit", "structure agent project", or mentions pipeline architecture, agent-assisted development, cost estimation, or choosing between LLM and traditional approaches.

13

decision-critic

solatis

Invoke IMMEDIATELY via python script to stress-test decisions and reasoning. Do NOT analyze first - the script orchestrates the critique workflow.

11

cc-skill-project-guidelines-example

davila7

Project Guidelines Skill (Example)

10

mcp-builder

anthropics

Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).

136215

architecture-patterns

wshobson

Implement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use when architecting complex backend systems or refactoring existing applications for better maintainability.

55214

Search skills

Search the agent skills registry