# Agent Guidelines — QMK RGB Tool ## Read README.md First `README.md` is the single source of truth for this project's behaviour: the command surface, the effect catalog, zone semantics, the protocol, the keyboard list and platform setup. **Read it in full before you work on this repository.** This file deliberately does not repeat any of it. A second copy is a second thing that can go stale, and that has happened: a command prefix that had not existed for many commits survived in eleven lines of prose, and a `keyboards.json` field was documented here as load-bearing while the code never read it. So the split is: - `README.md` — what the software does. Reference. - `AGENTS.md` — how to work on it. Discipline. - `.claude/skills/qmk-rgb/SKILL.md` — the live-query workflow for the tool. For the effect catalog specifically, read it from no document at all. Run `qmk-rgb-tool effect --list` and use the output. Logo and Side accept fewer effects than Backlight, and only the tool knows what the current firmware supports. ## Objective Cross-platform Go CLI library for programmatic/agent-friendly control of QMK keyboard RGB lighting. ## Architecture ## Device Selection Discovery matches every connected keyboard exposing the QMK Raw HID signature (Usage Page `0xFF60`, Usage `0x61`) — the same check `qmk/qmk_udev` performs. `keyboards.json` is only consulted to attach a name to a recognized model and to name its channels; an absent or malformed file costs names, not the ability to drive the keyboard. No command validates `--zone` or `--device` before the command that needs it does. A root-level pre-run would make `keyboard info`, `list`, `delete` and `completion` demand a keyboard, and the one you run to choose a keyboard cannot require that you have chosen one. Where two errors apply, the board wins: a keyboard that cannot be selected is reported before a zone name, because the vocabulary of a board cannot be known without the board. `keyboard info` numbers the connected keyboards from 1 in a stable order (vendor ID, product ID, path). `--device` accepts that number, never a HID path. Without `--device`, commands proceed only when exactly one keyboard is connected; otherwise they fail and list the selectable numbers, so a command never targets an unintended keyboard. Device numbers are stable for the current session only — HID paths are reassigned on reboot and most keyboards report no serial number. `--zone` takes a VIA lighting channel, named by its QMK subsystem, and a board in `keyboards.json` may give a channel a display name. README.md carries the vocabulary. It is a persistent flag in cobra's sense only — accepted on the root and inherited by subcommands — and nothing is remembered between runs, so it must be repeated on every invocation. Without `--zone`, commands target every channel the keyboard reports, in channel order; the channels are discovered by asking the keyboard, not assumed. With `--zone`, commands target exactly one channel. A default command skips a target that does not support an effect and warns on stderr; an explicitly named channel that does not support it, and an unknown name, fail before the device is opened. ## Stack - Go 1.26+ (cross-platform: Linux, macOS, Windows) - `github.com/sstallion/go-hid` for HID access - `github.com/spf13/cobra` for CLI framework ## Constraints - CLI-first, no GUI — designed for automation and agent consumption - Output should be machine-parseable (JSON when possible) - Cross-platform from day one ## Consistency Documentation describes real, verifiable behavior. Names are only the start — behavior counts too. Before any edit: 1. **Binary name** — the `go build -o` target is `qmk-rgb-tool`, which is not the module's last path element. It must match every example in README, AGENTS.md, comments, and docs. 2. **CLI command** — the root command name (e.g. `qmk-rgb-tool`) is the binary name; every usage example, CLI reference table, and doc must use the same string. 3. **Module path** — the `go.mod` module path is the source of truth for `go install` targets. 4. **Zone names** — the vocabulary is QMK's subsystem names, so they must be identical in code, docs, and examples: `backlight`, `rgblight`, `rgb_matrix`, `audio`, `led_matrix`. A board's own names (`logo`, `backlight`, `side` on the Impact 80) are **data** in `keyboards.json`, not code and not part of the vocabulary: do not hardcode one into Go, and do not put one in a `--zone` help string. Documented behavior must also match the code: 5. **Flags and accepted values** — what a flag accepts and rejects, its type, its default, and whether it persists across commands. A doc promising `--device 1` must not accept a path. 6. **Error messages and exit codes** — the exact strings a user is told to look for, which error takes precedence when several apply, and when JSON is still printed before a non-zero exit. 7. **JSON output shape** — field names, types, and whether a field is always present, omitted, or `null`. Agents and scripts parse this. 8. **Ranges, defaults, and stability** — accepted ranges, default values, boundary behavior, and any ordering or stability guarantee together with the scope it holds over (a device number is stable for one session, not across reboots). ## Firmware Transforms Values The Impact 80 firmware rescales or clamps brightness and speed per channel, so an accepted 0–255 request is not the value the keyboard holds. `logo` and `side` cap brightness at 160 and have only three reachable speeds — 0 freezes the animation, 1 is the slowest movement, anything above 1 becomes 4 — while `backlight` scales brightness up to 255 and applies speed as given. README.md tabulates the per-channel behaviour; the rule that follows from it is the part that matters when you write code here: `brightness`, `speed`, `color`, `effect` and `mode` read every selected zone back and print what was actually applied; where all zones match they print the plain success line, and where any zone differs they print one summary line naming each zone's real value and the request. Never let a command report success for a value the keyboard did not accept. All of them read back through the shared `readBackValues`, so a future parameter needs no new read-back path — but the helper above it is not as shared as it looks. `setValueVerified` writes one value to every channel, which fits `brightness` and `speed` and fits nothing else: `effect` carries a different ID per channel, because one effect name is a different index on each subsystem, and so has its own `setEffectVerified`. Assume the next parameter needs its own wrapper, and share only the read. A board can refuse an ID that looks valid. The Impact 80's `logo` and `side` channels read effect ID 0 as "lighting off" and leave the mode register alone, so `effect none` there does nothing to the effect and is reported as the effect still running. `disable` is the command that turns a channel off, because it also writes brightness 0. Above the top the behaviour is the opposite: the backlight channel does not refuse ID 47 or ID 99 but clamps it to the 46 it holds, so `mode` there is reported as the index the keyboard ended up with. Read the register back; never report the number that was asked for. The effect names are a board's catalog in `internal/rgb/catalog.go`, which transcribes the arrays `internal/rgb/impact80.go` holds from the two sources its comment names. They look like data the tool invented and are not; do not edit a list on a hunch, and do not add a second one. The backlight list is the QMK catalog **of the VIA era**, not of current QMK master: the two differ in length, naming and numbering, so upstream is not a repair guide for it. The `logo` and `side` lists are not QMK's at all. The live register proves which IDs a board takes, never which name belongs to one, and the board takes one ID more (46 on the backlight) than the catalog names. When you see a name or a behavior in one file, grep for it across the whole repo before deciding if a change is consistent. ## Read the Firmware When Something Is Unclear Unclear firmware behaviour is answered by reading source, not by inferring it from names, from a general QMK assumption, or from a doc line that nobody has checked. Two sources settle nearly everything, and both are cheap to reach. Upstream QMK is public: `quantum/rgb_matrix/rgb_matrix.h` for the value API, `quantum/rgb_matrix/animations/` for what an effect actually does to hue, saturation and value. The vendor's VIA definition JSON for the board settles what the interface exposes at all — which custom value IDs exist, which ranges they take, and for which effects a color control is offered. It is the authority on the wire format, more than upstream is, because the ID mapping is the vendor's. For the Impact 80 that JSON is `https://drive.wobkey.com/f/d/6BtO/Impact_80.JSON`, reached from the vendor's [Driver & Firmware page](https://wiki.wobkey.com/en/Products/PMOKEY-Impact-80/Driver-Firmware); the page is the place to re-read, because it also carries what a wrong flash does to this tool: the vendor ships a proprietary firmware alongside the VIA one, it disables VIA completely, and a board on it is invisible rather than unsupported. It also documents that VIA only sees the board in wired mode. A keyboard the tool cannot find is therefore not yet evidence of a bug here. The board's own firmware is a vendor binary with no public source, so both sources describe the family, not the unit on the desk. Confirm against the hardware where that is cheap, then put the observation in README.md. One trap this session paid for: the vendor channels are not all the same QMK subsystem. One is `rgb_matrix`, one is the lightweight `rgblight`, and one is the audio effect subsystem. An upstream file that governs one channel does not govern another, so "the QMK source says" has to name the channel it was read for. A formula from the wrong subsystem is how this repo ended up claiming that speed 0 and 1 behaved identically. ## Code vs Documentation All documentation (README.md, comments, AGENTS.md) must stay in sync with the code. When code and documentation conflict, **ask the user** before deciding which one to change. Do not silently pick a winner — explicitly state the conflict and get direction. ## Go Development ### Tooling - `gopls` — Go Language Server, installed separately with `go install golang.org/x/tools/gopls@latest` (LSP, diagnostics, go-to-def, rename, references) - `gofmt` / `goimports` — formatting. `goimports` adds/removes imports automatically. Always run before committing. - `go test` — built-in test framework. Tests live in `*_test.go` files alongside source. - `go vet` — static analysis. Run before committing. - `go build` — compiles without installing. Fast, cached. - `go mod tidy` — adds missing deps, removes unused ones. Run after every import change. ### Style (Effective Go + Code Review Comments) - **Formatting**: always `gofmt` or `goimports`. Tabs for indentation, no line-length limit but avoid uncomfortably long lines. - **Names**: `MixedCaps` / `mixedCaps`, no underscores. Short local variables (`i`, `c`, `r`). Descriptive for globals. - **Initialisms**: consistent case — `URL`, `ID`, `HTTP` → `appID`, `urlPony`, `ServeHTTP`. - **Comments**: doc comments start with the name, end with period. `// Package rgb provides RGB effect handling.` - **Imports**: standard library first, blank line, then third-party. Grouped. ### Error Handling - Return errors, never `panic` for normal control flow. - Indent error flow early — happy path at minimal indentation: ```go if err != nil { return err } // normal code ``` - Error strings: lowercase, no period — `fmt.Errorf("device not found")`. - Do not discard errors with `_`. ### Interfaces & Types - Define interfaces in the **consumer** package, not the producer. - Use pointer receivers when in doubt (mutation, large structs, sync fields). - Return concrete types from constructors — let consumers mock if needed. - Empty slices: `var t []string` (nil), not `t := []string{}` (non-nil), unless JSON encoding requires `[]`. ### Concurrency - Pass `context.Context` as **first parameter**. Never store in structs. - Clear goroutine lifetimes — document when/why they exit. Avoid leaks via channels. - Prefer synchronous functions — callers can add concurrency, not remove it. ### Testing - Table-driven tests preferred for multiple cases. - Failure messages: `t.Errorf("Effect(%q) = %d, want %d", input, got, want)`. - Use `*_test.go` files. Test package should match source package (internal tests). - Example functions (`func Example...`) double as docs and tests. ### Modules & Packages - Module path: `github.com//` (when published). - Package names: short, single-word, lowercase. No `util`, `common`, `api`, `types`. - Zero values should be useful (e.g. `bytes.Buffer`, `sync.Mutex`). ## graphify This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. When the user types `/graphify`, use the installed graphify skill or instructions before doing anything else. Rules: - For codebase questions, first run `graphify query ""` when graphify-out/graph.json exists. Use `graphify path "" ""` for relationships and `graphify explain ""` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output. - Dirty graphify-out/ files are expected after hooks or incremental updates; dirty graph files are not a reason to skip graphify. Only skip graphify if the task is about stale or incorrect graph output, or the user explicitly says not to use it. - If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing. - Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context. - After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).