v2.9 — mode-from-off auto-on, night-light dimming, Pedestal Fan timer & oscillation range#16
Conversation
…ck in, Gemini deferred, +§5c parallel external Per the agent-pipeline handoff doc's latest §1 / §5b-bis / §5b-ter / §5c: - Defer Gemini from the active fan-out (cost-uncompetitive on its required paid Google AI/Vertex tier vs the OpenCode pack); recipe kept dormant in the handoff doc. Removes the --yolo/worktree/paid-tier wiring. - Add the OpenCode multi-model pack as the 2nd external family: family-gate pre-check (opencode --version + rg), dispatch ALL members if YES (no cherry-pick), --dir . scoped reading a pre-staged workdir diff (--dir . auto-rejects /tmp), ANSI-strip on parse, per-member auth-fail/cost drop, intra-pack split adjudication. - Add CLAUDE.md pipeline Rule 12 (§5c): SHOULD dispatch one OpenCode cheap-tier reviewer alongside the Sonnet QA agent every round; reuse the Step-4c shared prompt; Bash-not-subagent; QA-vs-external reconciliation; cost ceiling. - Stale-narration findings get extra verification (open the file/line — doc-drift claims hallucinate more than code claims). - Sync CLAUDE.md reviewer table + cost cell. Codex path unchanged. Effort/model frontmatter unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…andoff §5b-bis.5) The handoff doc replaced the abstract pack placeholders with a named standard 5-pack; mirror it: - /final-review Step 4b: concrete 5-pack dispatch — mimo-v2.5-free + big-pickle (FREE) + deepseek-v4-flash + kimi-k2.6 + deepseek-v4-pro (paid Go) ≈ $0.85/run; reviewer table + cost rows updated. - CLAUDE.md Rule 12 (§5c): pin the per-round pipeline reviewer to opencode-go/deepseek-v4-flash (PAID ~$0.033); free seats explicitly reserved for ship-gate only. - CLAUDE.md ship-gate cost cell + reviewer row: concrete 5-pack + $0.85. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ixes headless hang) opencode run under the Claude Code Bash tool on Windows/Git-Bash blocks on stdin indefinitely -- it bootstraps, logs to "vcs initialized", then never calls the model and writes 0 bytes to both streams. Redirecting stdin from /dev/null unblocks it (empirically isolated 2026-06-05: 7 runs without it hung, every run with it completed; confirmed on a real fan-line review at ~3 min, $0, with substantive findings). - SKILL.md: add `< /dev/null` to all 5 OpenCode pack dispatch lines; bump timeout 1200000 -> 2700000 (a model was observed at ~33 min wall-clock, so a short timeout kills legitimate runs); document the stdin fix and that --dangerously-skip-permissions is a no-op Claude Code flag OpenCode drops. Handoff doc (local) updated in parallel; the bare `> out 2>&1` form it previously documented was never validated headless on this host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…seek-flash-free) The handoff doc's latest revision promotes the OpenCode ship-gate pack from 5 to 6 (adds free `deepseek-v4-flash-free` as a 3rd $0 seat) and changes the per-round iteration reviewer (§5c) from a single paid Flash to a Flash-PAIR: paid `deepseek-v4-flash` + free `deepseek-v4-flash-free` run concurrently with Sonnet QA, reconciled across three voices. Mirror both: - CLAUDE.md Rule 12 (§5c): single paid Flash -> Flash-pair; reconciliation rewritten for three voices (Sonnet + paid + free Flash); free Flash 429 is benign/best-effort. Only the SLOW free seats (mimo ~2m, big-pickle ~3m) stay reserved for ship-gate (latency, not cost). Invocation note carries the mandatory `< /dev/null`. - CLAUDE.md two-tier table + reviewer rows: 5-pack -> 6-pack, 3 free seats. - SKILL.md: add 6th dispatch line (deepseek-v4-flash-free, FREE); 5->6 in counts, cost lines, and reviewer row; cost unchanged at ~$0.85 (free adds $0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…leet-wide) Calling setMode/setAutoMode on a powered-off purifier/humidifier/fan now turns the device on, then applies the mode -- matching how speed/level setters already behave. Previously the command was a no-op (Vital line) or sent-but-ignored (other families), so "set it to auto" left the device off. - All mode setters across Core / Vital / Classic / V2-humidifier / Fan families call ensureSwitchOn() after input validation (invalid input never wakes an off device). Vital setMode converted from skip-when-off and gains case-insensitive normalization. - Core setMode + setAutoMode commit state.mode/auto_mode/room_size and emit events ONLY when the cloud accepted the change (state-vs-reality); setAutoMode gains allowed-set enum validation before auto-on. - BP24 classification comments on every touched setter; 8 stale RULE32 exemptions removed. - Spock from-off + state-vs-reality regression specs added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…+ forbid git diff) Pre-staging the review diff into the workdir is necessary but not sufficient for sandboxed reviewers (OpenCode pack): the prompt must also positively direct the model to read the staged file AND explicitly forbid `git diff` + scratch-writes, or some models still reach for `git diff > /tmp/x`, get auto-rejected, and abandon the review. Adds the named DO-NOT lines to the /final-review shared prompt + operational note, and mirrors the rule into the CLAUDE.md §5c per-round invocation. Source: agent-pipeline-handoff §6. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ning readout - LevoitCore200S Light: capability "Switch Level" (two words -- an invalid Hubitat identifier silently ignored) -> "SwitchLevel". The night-light's dimmer/level control now registers, so it can be dimmed from a dashboard tile or a Rule Machine setLevel action (previously only the explicit command worked). - Pedestal Fan: applyStatus now emits the declared timerRemain attribute (was never populated -> always blank), mirroring Tower Fan; folded into the info tile when active. - New lint rule RULE42 flags any capability name containing a space (closed-mechanism guard for the typo class) with must-catch / must-not-catch fixtures. - Spock regression specs for both fixes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rrency mechanic) A Claude sub-agent dispatched in its own message holds the foreground turn, so the follow-up Flash-pair Bash calls don't fire until it's force-backgrounded -- the "parallel" pair sits idle. Dispatch the qa Agent and both Bash calls as multiple tool blocks in a SINGLE response (no dependency once the diff is staged). Mirrors the handoff-doc §5c rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ght doc - Core 200S + Core 200S Light: drop stale "/ 300S" from driver descriptions (these have been separate drivers since v1.1). - Core 200S: expose `command "update"` (on-demand refresh), matching the Core 300S/400S/600S models; wires to the existing no-arg update() poll path. - readme: document `update` on all four Core models' command lists; correct the SproutAir setNightlightMode list to on/off/dim (the driver never supported "auto"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fleet-wide, #220) When a Levoit device is powered off, VeSync rejects bypassV2 write commands with inner result code 11005000 (BYPASS_DEVICE_IS_OFF). Preference setters (display, child-lock, target-humidity, drying, nightlight, color, etc.) are NO-ON by design — they must NOT auto-power-on the device — so a scheduled or dashboard-driven preference write to an off device is an expected non-fault, not a hardware error. Previously every such rejection logged an ERROR and appended a diagnostics ring-buffer record, producing recurring ERROR spam in users' logs (originally surfaced on Superior 6000S; the shape is fleet-wide). Fix: a stateless reportWriteFailure(tag, resp, ctx) helper in LevoitChildBaseLib. On a write-failure branch it inspects the passed response at call time — device-off (11005000) => one WARN (no ERROR, no record); any other inner code / HTTP failure => the prior logError + recordError. The decision carries NO cross-call state, so a device-off rejection on one command can never suppress a different command's genuine error. 40 NO-ON write-failure branches across 14 drivers route through it; SHOULD-ON intent setters (setLevel/setSpeed/setMistLevel) are unaffected (they auto-power-on first, so device-off is not their expected failure). pyvesync flags 11005000 critical_error=True; this fork deliberately treats a powered-off device as a normal user state (WARN), documented at the helper and in the BP29 catalog entry. Regression coverage: LevoitChildBaseLibSpec adds the stateless-helper specs plus a load-bearing leak-regression spec (device-off on command A must not suppress a later unrelated error on command B). Both proven non-vacuous via orchestrator both-ways (revert => RED, restore => GREEN). VeSyncIntegrationVirtual gains a device-off fixture op for A2 virtual-parent exercise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…c off() re-entrance guards (#247) Two internal-consistency changes, no user-visible behavior change. recordError ctx-key normalization: the recordError(msg, ctx) map key drifted between `site:` (21) and `method:` (76) with no functional difference (the key is a diagnostic label stored verbatim; nothing reads it). Normalized to `method:` fleet-wide — 27 call sites across 9 files (a paren-balanced, multi-line-aware scan found 6 sites a single-line grep missed). New lint rule RULE43 (tests/lint_rules/recordError_key_style.py) enforces it going forward, with must-catch (incl. multi-line + paren-in-message forms) and must-not-catch fixtures; CONTRIBUTING.md example updated to match. off() re-entrance guard symmetry: LevoitFanLib (Tower/Pedestal Fan) and LevoitHumidifierLib (Classic 200S/300S, Dual 200S, LV600S, OasisMist 450S/1000S, Sprout Humidifier) had a guarded on() but an unguarded off(). Added the matching state.turningOff guard (mirroring the Vital-lib reference and the EverestAir/ SproutAir guards added earlier this batch), closing the guarded-on/unguarded-off class fleet-wide. Defensive symmetry — no active re-entrance vector into off() today — so a no-op under normal operation and protective under any future toggle-race. Regression specs added to LevoitTowerFanSpec + LevoitClassic300SSpec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xit-wedge) Under Git-Bash, `opencode run` frequently leaves a wrapper process that never exits AFTER the model has already written its full output — so a backgrounded call blocks forever and its completion notification never fires (a free-audit sweep hung ~9h with every output already on disk). The Bash-tool `timeout:` param does not save you: a detached wrapper outlives it. The only effective ceiling is a command-level `timeout <sec>` wrapper (output lands before it fires, so nothing is lost). Wrap every opencode dispatch: CLAUDE.md §5c Flash-pair invocation (timeout 1200) and the /final-review skill's 6-pack lines (timeout 2700), with the wedge explanation inline. Sourced from the agent-pipeline handoff doc gotcha (d). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ation following #77 — LevoitTowerFanSpec + LevoitPedestalFanSpec: the BP23 spec asserted only that state.turningOn was CLEARED after on() (would pass even if on() never set it). Now a request-time metaClass hook captures state.turningOn at the moment the setSwitch request fires and asserts it was true, keeping the cleared-after + switch=on assertions. Proven both-ways: removing `state.turningOn = true` from LevoitFanLib.on() turns both specs RED. #242 — RULE32 (bp24_auto_on_guard_missing) followed delegation one hop only, so an unguarded API call reached via a 2+-hop chain (setSpeed -> helperA -> helperB) escaped. Replaced with transitive BFS following (visited-set, cycle-safe, guard-prunes-subtree). Added 4 fixtures (2-hop must-catch, guard-on-middle/leaf must-not-catch, cycle-safety must-catch); existing 1-hop fixtures retained. Real-corpus finding set unchanged (0->0 -- no production gap was escaping). Proven both-ways: reverting to first-hop-only turns the 2-hop must-catch fixtures RED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…review The OpenCode HARD free cap (FreeUsageLimitError) does not error cleanly — the free tier swallows the provider 429 and retries internally to timeout, so the member HANGS and returns a banner-only stub rather than a clean failure. (Surfaced this session: both free-Flash voices returned 47-byte banner-only stubs across two rounds — the hard cap, not transients.) CLAUDE.md §5c: document the free-Flash no-show signature (banner-only stub = hard cap, bounded by the timeout wrapper + benign drop); note the cheap DEBUG cap-detector belongs to a free-only sweep pre-flight, not the per-round pair. final-review SKILL.md: note the same hang for the 3 free pack seats — already handled by the per-dispatch timeout wrapper + parallel-background drop at Step 6; the DEBUG cap-detector is an optional pre-drop, not a mandatory probe (respecting the don't-burn-a-probe rule; paid seats + Claude lenses carry it). Source: agent-pipeline handoff §5b-bis free-cap detector. Companion to the /free-audit skill's new Step 2.5 (free-only sweep, where the probe IS mandatory — that skill is local/global, not in this repo). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…atus + 450S RGB variant (#243, #244)
Enables hardware-free A2 verification of the nightlight code paths #220 touched.
Test-harness tooling only; no driver behavior change.
#243 — LEH-B381S (Sprout Humidifier): added the driver-side setLightStatus op
(keys {brightness, colorTemperature, nightLightSwitch}, matching
LevoitSproutHumidifier.setNightlight) to tools/virtual_parent_extensions.json,
plus a nightLight response sub-object so a spawned child parses it back.
#244 — new fixture key LUH-O451S-WEU (RGB-nightlight 450S variant): the 450S
RGB nightlight is runtime-gated by isRgbVariant() == true only for deviceType
LUH-O451S-WEU, so extending the existing WUS fixture would no-op. Added the
8-key setLightStatus op {action, brightness, red, green, blue, colorMode, speed,
colorSliderLocation} (shared by setNightlightSwitch + setColor), the WEU entry
across all FIXTURE_TO_* maps + the spawn ENUM, an rgbNightLight response
sub-object, and tests/pyvesync-fixtures/LUH-O451S-WEU.yaml. WUS unchanged
(no-nightlight hardware).
FIXTURE_OPS regenerated via tools/regenerate_virtual_parent.py (--check clean,
19->20 fixtures / 130 ops). Two VeSyncIntegrationVirtualSpec A2 specs added,
proven non-vacuous both-ways (dropping a payload key -> key-set validation
rejects -> both specs RED).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + classify oscillation setters NO-ON (#249, #127)
#249 — live capture on a real Pedestal Fan (LPF-R432S) showed the device's
oscillation angular range is wider than the driver assumed: horizontal 0-90°,
vertical 0-120°. setHorizontalRange/setVerticalRange clamped both axes to 0-100
(a guess from pyvesync's fixture sample), which silently TRUNCATED legitimate
vertical settings (a 120° down-tilt set in the VeSync app clamped to 100° on any
Hubitat range write). Corrected the clamps to the empirical maxima — H 0-90,
V 0-120 — and updated the stale 0-100 references in command-parameter
descriptions, attribute comments, and readme. safeIntArg clamp mechanism
unchanged; only the bounds.
#127 — live capture also resolved the long-deferred BP24 classification of the
5 oscillation setters (Pedestal setHorizontalOscillation/setVerticalOscillation/
setHorizontalRange/setVerticalRange + Tower setOscillation): they are NO-ON
(calling them on an off fan does NOT auto-power-on, and the cloud accepts but
does not persist an oscillation write while off — it applies only while running).
Added explicit BP24: NO-ON comments + a shared noteOscillationOffState() helper
that logs an informational note ("Fan is off — oscillation setting will apply
when the fan is powered on") when a setter is called while off; the command is
still sent (send-and-let-the-cloud-decide). Regression coverage: 5 from-off
NO-ON specs (no auto-power-on) + 4 clamp specs (90/120 bounds), all proven
non-vacuous via orchestrator both-ways (revert => RED).
ROADMAP: removed the now-resolved oscillation-classification item.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…actored in #142) The "nested-ternary speed-step" this item cited no longer exists — #142's table-driven mapSpeedToInteger/mapIntegerToSpeed rewrite eliminated it (their comments note the "old ternary/switch default"). The three surviving cycleSpeed bodies are already idiomatic (Core list-rotation, Vital map lookup, Fan single wraparound ternary); a switch would reduce readability, not improve it. Nothing to refactor — removing the resolved item. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…red hourly WARN During a hub internet outage the parent already logs the outage once and re-surfaces it hourly, but each child's command-triggered write-failure branch still logged its own error + diagnostic-record on every retry, so a device re-driven by a rule on a short cadence multiplied the noise across every child. Route child write-failures through a new stateless LevoitChildBase.reportWriteError(tag, ctx): while a network outage is known it downgrades to a single DEBUG line and skips the diagnostic ring-buffer; otherwise it falls through to the prior logError + recordError. networkOutageKnown() is parent-null-safe (respondsTo guard + try/catch) and reads the parent's new public isNetworkUnreachable(). httpOk() and the BP29 device-off reportWriteFailure() gained the same downgrade, with device-off still checked first so a device-off rejection WARNs even mid-outage. Also fixes the hourly "still unreachable" WARN (and the 5-minute reconnect probe) occasionally firing several times within a one-minute burst: during long polling cycles concurrent reads could see a stale last-warn timestamp before the first write flushed. Replace the elapsed-delta timestamps with epoch-bucket counters (now()/3600000 for the hourly WARN, now()/300000 for the probe) so same-bucket re-reads compare equal and can't re-fire. Regression coverage: LevoitChildBaseLibSpec proves reportWriteError / httpOk / reportWriteFailure downgrade to DEBUG when the outage flag is set and emit ERROR when it is clear, plus device-off-before-outage ordering and the parent-null default; VeSyncIntegrationSpec proves emitNetworkWarnIfDue() fires once per new hour bucket and suppresses same-bucket retries — all proven non-vacuous both-ways. BP22 catalog entry updated to the bucket fields + child-dedup mechanism. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The drivers #include a set of shared libraries at save time, so a manual (non-HPM) install must create the libraries before pasting any driver or every driver fails to save with an unresolved-library / MissingMethodException error. The prior one-line "paste each driver" instruction omitted this. Add the libs-first install order, the 7 library files with their level99.<Name> #include names, the separate Libraries Code page location, and a troubleshooting note for the unresolved-library symptom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bug-pattern catalog had drifted into three hand-synced copies — the
numbered list in CLAUDE.md, the detailed entries in the QA agent definition,
and a dense block of rows in CONTRIBUTING.md's conventions table — so every BP
edit meant three edits that silently diverged. Consolidate to one canonical
file; everything else references it.
- docs/BUG-PATTERNS.md (new): single source of truth, BP1–BP29, each entry with
symptom, root cause, fix scope, canonical fix, lint rule, and regression
coverage. Numbering matches the lint-rule filenames + Spock spec names
(BP24 = auto-on, BP25 = case-sensitivity, BP26 = safeIntArg), which are
authoritative; CLAUDE.md's old prose list was off-by-one at #24/#25 and is
removed.
- CLAUDE.md: inline catalog → a pointer + a lazy-load row in the "Project
documentation files — when to load" table (read on-demand, not re-injected
into every session).
- CONTRIBUTING.md: drop the duplicated BP rows from the conventions table (keep
the lint-hygiene + driver-authoring convention rows); the "Bug-pattern catalog
references" section now points at the canonical file.
- Agent defs that reason about the catalog (developer + qa + qa-coverage /
platform / protocol / adversarial / design): a top-of-prompt "Read
docs/BUG-PATTERNS.md FIRST" directive; the QA def's ~818-line inline catalog is
replaced by that pointer. Mechanical / cite-only agents (preflight, tester,
operations, operator) are left lazy by design. ('@'-import doesn't expand
inside agent-def bodies — only in CLAUDE.md — so the explicit read directive is
the reliable mechanism.)
- final-review external-reviewer prompt: docs/BUG-PATTERNS.md added to the shared
"Required reading FIRST" list (covers Codex + the OpenCode pack + the per-round
Flash pair).
Lint --strict PASS (RULE24 agent-pointer integrity intact); no driver code or
specs changed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the catalog consolidation: six references still named the old home (CLAUDE.md / the QA agent definition) for where the bug-pattern catalog lives or where a new entry should be folded in. Repoint all at the canonical docs/BUG-PATTERNS.md. - CONTRIBUTING.md: "the catalog lives in CLAUDE.md (root)" and "fold it into CLAUDE.md on merge" -> docs/BUG-PATTERNS.md. - .claude/commands/cut-release.md: cross-check BP references against docs/BUG-PATTERNS.md (was CLAUDE.md "Bug-pattern catalog"). - vesync-driver-developer.md: BP20 doc-placement pointer -> docs/BUG-PATTERNS.md. - vesync-driver-qa-coverage.md: new-pattern entries land in docs/BUG-PATTERNS.md (was "CLAUDE.md AND the vesync-driver-qa definition"). - vesync-driver-qa-design.md: asymmetry-rationale BP entry -> docs/BUG-PATTERNS.md. Lint --strict PASS; no driver code or specs changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three "User-visible polish" items shipped to release/v2.9 this cycle and no longer belong on the unscheduled backlog: - Child-side error-log dedup during outages (shipped: reportWriteError) - BP22 long-outage WARN cadence + probe-interval stalls (shipped: epoch bucket) - Manual-install path documentation (shipped: README libraries-first section) (The community-thread OP half of the manual-install item was always external, not a repo doc task.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bug-pattern consolidation (09380e5) inserted a top-of-body "Read docs/BUG-PATTERNS.md FIRST" directive into the 5 ship-gate QA lenses (coverage/platform/protocol/adversarial/design). Those defs' description: frontmatter has always contained a ": " (colon-space, e.g. "Does NOT cover: …") — invalid as an unquoted YAML scalar. The agent loader tolerated the malformed frontmatter when the body began with a "# Heading" (original), but the inserted directive as the new first body line tipped the lenient fallback over, so the 5 agents silently stopped registering as dispatchable subagent types — breaking the /final-review fan-out. Fix: wrap the description: value in a proper double-quoted YAML string in every agent def whose frontmatter failed to parse (the 5 lenses + operator + tester, which had the same latent invalid-YAML but still happened to load). Content unchanged; the read-FIRST directive is preserved. All 11 agent defs now parse via yaml.safe_load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…drift, mode-from-off specs Addresses all findings from the v2.9 /final-review ship gate (no production behavior invariant broke; findings were library-hygiene, doc-drift, test-coverage). - BP20: relocate the file-scope comment block out of LevoitChildBaseLib.groovy (between library() and the BYPASS_DEVICE_IS_OFF constant). File-scope commentary in a library risks Hubitat's parser rejecting the save, breaking HPM install for every user (all drivers #include this lib). Rationale moves into the reportWriteFailure method body; a one-line inline comment stays on the constant. - BP24 catalog truth-up: docs/BUG-PATTERNS.md now classifies Vital / EverestAir / SproutAir setMode as SHOULD-ON (v2.9 made them auto-on-from-off; catalog still said SKIP-OK / "pending live-capture"). Removed the dead Vital setMode SKIP-OK exemptions from lint_config.yaml. - recordError ctx key [site:] -> [method:] (RULE43) in the developer-agent instruction, Vital100S/200S history comments, and a spec fixture. - RULE42 (malformed_capability) widened to catch single-quoted capability decls (was double-quote-only) + single-quote must-catch / must-not-catch fixtures. - Repointed every stale bug-pattern-catalog location reference to docs/BUG-PATTERNS.md (oauth-flow.md, CONTRIBUTING.md, vesync-driver-operations agent, RULE16/24/32 lint-rule fix messages); scrubbed a ~/.claude personal path. grep-to-zero verified. - readme: Pedestal Fan timerRemain row + reworded "Timer is omitted" prose. CHANGELOG: mode-auto-on bullet gains a "check your automations" note. - Regression specs: invalid-mode-from-off no-wake guards on Vital 100S/200S + Classic 200S / Dual 200S / LV600S / OasisMist 450S, and a night-light SwitchLevel capability-presence spec on Core 200S Light. Proven non-vacuous both-ways (inverting validate->ensureSwitchOn order turns them RED). Lint --strict PASS; lint pytest 399; Spock 2046/2046. No version bump (pre-cut). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User-facing summary (full detail in CHANGELOG [2.9]): - mode setters auto-turn-on an off device, fleet-wide (with keep-it-off caveat) - uppercase setMode on Vital 100S/200S; Core 200S/300S night-light dimmer registers - Core 200S update command; Pedestal Fan timerRemain + full oscillation range - device-off write rejections + outage retries log one WARN, not error spam Version lockstep 2.8 -> 2.9 across 24 drivers + FORK_RELEASE_VERSION; manifest version/date/bundle-URL/releaseNotes; CHANGELOG [2.9]; ROADMAP next-release header -> v2.10; README "beyond" pointer -> v2.9. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reorders the /cut-release Step-7 "Next steps" so the PR opens (and Gemini fires) BEFORE gh release create, instead of after. The hard invariant was always "asset live before the MERGE," not "before the PR" — opening the PR early creates the review record + auto-review, and cutting the release only after review settles means the tag captures the reviewed HEAD instead of a pre-review commit (which otherwise forced a gh release delete + re-cut). - Next steps resequenced: push -> open PR -> iterate review -> build bundle -> gh release create (tags reviewed HEAD) -> verify asset -> MERGE GATE - Step 9 marked the one hard ordering invariant (asset 302 before squash-merge) - ORDERING WHY + Notes rewritten to match; Artifact C.6 step cross-refs fixed (build 4->6, post-upload verify 6->8) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Notes bullet claimed the tagged release-branch commit "lives on main's history (fast-forward or squash-merge equivalent)" — false for a squash, where main gets a new commit and the tagged commit is not an ancestor of main. The tag stays valid for HPM anyway, for an unrelated reason: the bundle resolves by release tag (independent of main's history) and the manifest + driver source resolve by the main branch (which carries the squashed content). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! The v2.9 release is a fix-and-polish update across the Levoit driver fleet. Key changes include standardizing mode-change behavior to power on devices, improving log signal-to-noise ratios during outages and device-off states, and enhancing Pedestal Fan reporting and oscillation accuracy. The release also includes significant internal improvements to the bug-pattern catalog and linting infrastructure. Highlights
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request updates the Hubitat Elevation Levoit driver codebase to version 2.9, introducing auto-on-from-off compliance (BP24) for mode-setting commands across all device families, stateless handling of device-off rejections (BP29) to eliminate log noise, and epoch-bucket cadence hardening for network outages (BP22). It also adds an update command to the Core 200S, fixes night-light dimmer registration on Core 200S/300S, populates the Pedestal Fan's remaining timer, corrects its oscillation ranges, adds new virtual fixtures, and introduces new lint rules (RULE42 and RULE43) with accompanying Spock tests. The review feedback correctly identifies a potential MissingPropertyException in the stateless isDeviceOffResp helper if the API returns a raw string response (such as an HTML error page) instead of a Map, and the suggested explicit type check should be implemented to prevent driver crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
isDeviceOffResp and the hubBypass async callback peeled resp?.data?.result?.code with only null-guards. Groovy's ?. guards null, not type: on a non-JSON error response (CDN/gateway HTTP 502/504 with a raw HTML body) resp.data is a non-null String, so .result is a property access on a String -> MissingPropertyException. hubBypass's callback is the worse site -- it runs unconditionally on every call, no try/catch, so it threw before reportWriteFailure/isDeviceOffResp could run. (httpOk was already safe: it peels only inside its status in [200,201,204] guard.) Both sites now instanceof-Map-guarded before the peel. Regression specs: String-body no-throw for isDeviceOffResp AND the hubBypass callback, plus genuine-device-off-still-detected + normal/null nets. Both load-bearing specs proven both-ways (revert guard -> RED with MissingPropertyException at the named site; restore -> GREEN). No version bump / no CHANGELOG bullet: hardens BP29 code new-and-unreleased in v2.9; the crash was never user-reachable on a shipped version. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v2.9
Fix-and-polish release across the Levoit driver fleet. No new device drivers; upgrades in place via HPM (no re-pairing). One behavior change to be aware of (mode-from-off, below).
Highlights (user-facing)
Fixed
setMode("AUTO")now matched case-insensitively on Vital 100S/200S.autowhen an auto-mode change actually failed (mode updates only on success).Added / Changed
update(refresh) command.timerRemain, and its oscillation range now matches hardware (vertical ≤120°, horizontal ≤90°; was capped at 100°).Full detail in
CHANGELOG.md[2.9].Release mechanics
2.8 → 2.9: 24 drivers +FORK_RELEASE_VERSION;levoitManifest.jsonversion/date/releaseNotes + bundle URL →v2.9; CHANGELOG[2.9]; ROADMAP next-release → v2.10; README "beyond" pointer → v2.9./cut-releaseskill ordering fix (open-PR-before-tag) — pipeline doc only, no driver impact.Verification
lint --strictclean (RULE20 lockstep green); lint pytest 399; Spock harness 2046/2046./final-reviewship gate (6 Claude lenses + Codex + OpenCode pack) — findings remediated, re-gated clean.🤖 Generated with Claude Code