{"owner":"AprilNEA","repo":"OpenLogi","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# OpenLogi — Agent Guide\n\nOpenLogi is a native, local-first alternative to Logitech Options+ written in Rust:\nbutton remapping, DPI, SmartShift, and per-app profiles for Logitech HID++ devices\n(Bolt/Unifying receiver, Bluetooth-direct, wired) — no account, no telemetry, plain-TOML\nconfig. macOS and Linux are first-class; Windows is a young but shipping port.\nDual-licensed MIT/Apache-2.0; the `design/` brand assets are proprietary.\n\nThe developer handbook (toolchain, packaging, release pipeline) is\n[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). This file is the agent-facing contract;\nsubsystem deep-rules are indexed at the bottom.\n\n## Architecture\n\nThree tiers ship in one install: the **GUI** is a pure IPC client, the **agent** is a\nbackground server owning the input hook and ALL device I/O, and shared orchestration\nsits beneath both.\n\n| Crate | Role |\n|---|---|\n| `openlogi` (root package, `src/`) | The CLI binary — thin wrapper over `openlogi-cli` |\n| `crates/openlogi-core` | Pure types: TOML config, device model, action catalog. No I/O, no async |\n| `crates/openlogi-hidpp` | Vendored fork of the `hidpp` protocol crate (**lib name `hidpp`**, 0BSD) |\n| `crates/openlogi-hid` | Device discovery + HID++ writes over `async-hid` |\n| `crates/openlogi-assets` | Device-render registry + cached fetch from OpenLogi asset mirrors |\n| `crates/openlogi-cli` | `clap` command tree: `list`, `assets`, `diag` |\n| `crates/openlogi-hook` | OS input capture: CGEventTap / evdev+uinput / WH_MOUSE_LL |\n| `crates/openlogi-inject` | OS input synthesis: CGEvent / uinput+MPRIS / SendInput |\n| `crates/openlogi-agent-core` | Shared agent orchestration: hook runtime, HID++ writes, DPI cycle, Actions Ring session state |\n| `crates/openlogi-ipc` | The tarpc IPC contract (`src/ipc.rs`) + its local-socket transport, shared by agent and GUI |\n| `crates/openlogi-agent` | The `openlogi-agent` binary — hook + device I/O server |\n| `crates/openlogi-gui` | GPUI + gpui-component desktop app — polls the agent, no device I/O |\n| `xtask` | `cargo xtask` maintenance: bundling, packaging, release manifest |\n\n- GUI ↔ agent speak tarpc/bincode over an `interprocess` local socket. The wire format\n  is versioned and **append-only** — read `.claude/rules/ipc-protocol.md` before touching it.\n- Platform code is cfg-gated per crate (`[target.'cfg(target_os = …)'.dependencies]`).\n  The workspace's ObjC FFI is centralized in `crates/openlogi-gui/src/platform/` — read\n  that directory's `AGENTS.md` before editing it.\n\n## Build, run, verify\n\nNix/devenv is optional — rustup + `rust-toolchain.toml` is enough. If devenv is\ninstalled, direnv loads it; otherwise `.envrc` prints a notice and leaves PATH\nalone so system `cargo` works. With devenv active, cargo may only be on PATH\ninside the shell — run from the repo root (or `direnv exec . …`), including\ngit (the hooks need cargo):\n\n```sh\ncargo clippy --workspace --all-targets -- -D warnings\n# when cargo is only inside devenv:\ndirenv exec . cargo clippy --workspace --all-targets -- -D warnings\ndirenv exec . git commit …\n```\n\n### Local gate (hard stop — do this before every push)\n\n**Never `git push` until the final tree has passed the full local gate.**\n`cargo check` alone is not enough. Conflict resolution + \"it compiles on my\nMac\" is not enough. Run **all three** on the commit you are about to push:\n\n```sh\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets -- -D warnings\ncargo test --workspace\n# or: devenv tasks run openlogi:check\n```\n\nExit non-zero on any of those → fix, re-run the **whole** triple, then push.\nDo not push \"to see if CI likes it.\" CI is confirmation, not the first compile.\n\nprek hooks (`prek.toml`): `cargo fmt` at commit; full-workspace clippy at push\n(rust-scoped, so non-Rust pushes skip it). Hooks are a backstop, not a substitute\nfor running the gate yourself after a rebase.\n\n### Platform / cfg-gated code (macOS-green is a trap)\n\nmacOS-green proves **nothing** about `#[cfg(target_os = \"linux\")]` /\n`windows` code. Recent agent failures that only showed up on CI Linux:\n\n- Shadowing a crate-level constant with a local `const` of a different type\n  (e.g. `LOGITECH_VENDOR_ID: u16` next to `use crate::LOGITECH_VENDOR_ID`\n  which is `u32`) — E0255 / E0308, **only compiles on Linux**.\n- Importing a name that only exists on another OS, or redefining one that\n  master already exports from `lib.rs`.\n\nWhen the diff touches any of:\n\n- `crates/openlogi-hook/src/linux.rs` / `windows.rs`\n- `crates/openlogi-inject/src/inject/linux.rs` / `windows.rs`\n- `crates/openlogi-hid/src/transport.rs` (has `#[cfg]` branches)\n- any `#[cfg(target_os = …)]` block\n\nyou MUST either:\n\n1. Cross-check with devenv when available:\n   `devenv tasks run openlogi:check-windows` (and any linux check the repo has), or\n2. Manually re-read every changed cfg-gated file against **current master** for:\n   - name collisions with existing `pub use` / `pub const` items\n   - type mismatches (`u16` vs `u32`, `Option` arity, new enum fields)\n   - call sites that gained args on master (e.g. `with_runtime`, `build_device_list`,\n     `dispatch_action`) but the PR still uses the old signature\n\nDo not claim \"cross-platform green\" without CI (or a local cross-lint) having\nactually run those targets. `RUSTFLAGS=-D warnings` is global in CI — plain\nwarnings fail there too.\n\n### Wire format / IPC (another silent CI red)\n\nIf the change touches anything that crosses the agent↔GUI boundary\n(`crates/openlogi-ipc/src/ipc.rs`, serde enums in hid write errors, `DeviceKind`, …):\n\n- Enums are **append-only** (serde index = wire). New variants go at the end.\n- Bump `PROTOCOL_VERSION` and regenerate\n  `crates/openlogi-ipc/tests/wire_format.rs` goldens from the failure\n  message (`left` is the new encoding).\n- Run `cargo test -p openlogi-ipc --test wire_format` before push.\n\n### i18n\n\nNew GUI strings: insert the same key in the **same position** in every\n`crates/openlogi-gui/locales/*.yml` (parity is required). Run\n`cargo test -p openlogi-gui i18n`.\n\n### App / agent runtime notes\n\n- The macOS GUI build needs full Xcode for GPUI's Metal shaders. devenv sets\n  `DEVELOPER_DIR`/`SDKROOT` when present; without it, use system Xcode. If the\n  shader compile fails under devenv, `direnv reload` first.\n- Dev-run the app with `cargo run -p openlogi-gui` — a cargo runner wraps it\n  into `target/dev/OpenLogi.app`. `cargo build` does NOT refresh that bundle,\n  and a second instance exits on the singleton lock: quit the old instance and\n  re-`run` before judging a UI change \"not applied\".\n- No hardware attached? `cargo run -p openlogi-agent --bin openlogi-agent-mock`\n  serves a scripted inventory (both route kinds, every capability-gated panel, a\n  pairing flow) over the IPC socket, so the GUI runs unmodified. It defaults to\n  the `openlogi-dev` profile — same socket the dev bundle uses, production app\n  untouched. Details in `docs/DEVELOPMENT.md`.\n\n## Rust standards\n\nEdition 2024, MSRV 1.96. There is exactly **one** lint table, in the root `Cargo.toml`,\nand every crate inherits it with `[lints] workspace = true` — never a private copy, or\nthe next lint added to the workspace silently skips that crate. A crate needing a\ndifferent level opts out **in source** (the `openlogi-hook` platform modules carry\n`#![allow(unsafe_code, reason = \"…\")]`), because Cargo rejects mixing `workspace = true`\nwith local overrides. `openlogi-hidpp` deliberately stays out of the table (vendored).\n\nThe table: `unsafe_code = \"deny\"` (opt out per item with `#[expect(unsafe_code,\nreason = \"…\")]` plus a `// SAFETY:` comment), `clippy::pedantic` at warn,\n`unwrap_used`/`expect_used` at warn, plus the shared lint set —\n`assertions_on_result_states`, `cast_possible_truncation`, `cast_possible_wrap`,\n`cast_sign_loss`, `error_impl_error`, `exit`, `or_fun_call`, `ptr_as_ptr`,\n`tests_outside_test_module`, `undocumented_unsafe_blocks`. Any lint suppression carries\na `reason`. What that changes day to day:\n\n- Every `unsafe` block needs a `// SAFETY:` comment saying why it is sound.\n- `assert!(r.is_ok())` / `assert!(r.is_err())` are rejected — unwrap the `Result` (in a\n  test module that already allows it) or give the assertion a message.\n- A test module that wants `expect`/`unwrap` says so:\n  `#[allow(clippy::expect_used, reason = \"expect/unwrap are idiomatic in tests\")]` on the\n  module (or on its `mod tests;` declaration). Never route around the lint with\n  `unwrap_or_else(|e| panic!(\"…: {e}\"))` — that is the same panic with the check switched\n  off. The one honest use of that form is a *dynamic* panic message, where `expect` would\n  need a `format!` that allocates on the happy path (`expect_fun_call`).\n- A test module gated on more than `test` needs stacked attributes (`#[cfg(test)]` then\n  `#[cfg(unix)]`), not `#[cfg(all(test, unix))]`, which clippy reads as a test outside a\n  test module. Integration tests under `tests/` carry a file-level\n  `#![expect(clippy::tests_outside_test_module, reason = \"…\")]`.\n- `std::process::exit` needs `#[expect(clippy::exit, reason = \"…\")]` naming why that call\n  site cannot hand an `ExitCode` back to `main` instead.\n\nEncode invariants in the type system instead of checking them at runtime:\n\n- Wire/firmware values get typed wrappers: `num_enum` for discriminants, `bitflags`\n  (`from_bits_retain` when unknown bits are legal) for flag sets. Unknown wire values\n  surface as **errors** (`UnsupportedResponse`-style), never as silent fallbacks.\n- Replace long parameter lists with Change/Params structs; make illegal combinations\n  unrepresentable rather than validated.\n- Ownership models resources (`Retained<T>` in the ObjC FFI) and thread affinity is\n  proven by types (`MainThreadMarker`, `!Send` handles), not by runtime checks.\n- Libraries return `thiserror` types; binaries may use `anyhow`.\n\nHouse style:\n\n- **Root-cause fixes only.** Never layer compatibility shims over a broken abstraction —\n  refactor it. Never change product code to work around a dev-environment quirk; debug\n  the environment (or a release build) instead.\n- **Prefer mature crates over hand-rolled logic** (retry/backoff, hashing, paths, …).\n  Check `cargo tree | grep <candidate>` before adding a dependency and use `cargo add`\n  so versions come from the registry. After ANY dependency change, verify the\n  `gpui`/`gpui-component` git pins in `Cargo.lock` didn't move (they are held only by\n  the lock; restore with `cargo update -p gpui --precise <rev>`).\n- Module layout: a module with its own semantics is `foo.rs` (children in a sibling\n  `foo/`); `foo/mod.rs` is only for pure namespace shells. Never both for one module.\n- Keep files reasonably sized (split around ~500 lines) into real modules — never\n  simulate structure with `// ---- section ----` banner comments. But don't\n  over-extract either: inline single-use helpers.\n- rustdoc every public item. Comments state non-obvious constraints only.\n- Tests cover failure and edge paths, not just the happy path (state machines\n  especially). No tautological tests that mirror the implementation; never weaken an\n  assertion or special-case an input to make a test pass.\n\n## Git & GitHub\n\n- Conventional commits: `type(scope): imperative lowercase description`. Types in use:\n  `feat fix refactor chore docs ci perf style build test`. Scopes are crate short names\n  (`gui agent hidpp hid core hook ipc cli assets xtask`) or cross-cutting concerns\n  (`release ci i18n windows linux macos tray infra`). `i18n` is a scope, not a type.\n- Branches: `type/kebab-description` off `master`. Substantial or risky work goes in a\n  worktree so parallel work doesn't collide; trivial fixes may go straight to master.\n- Commits are small and focused — split unrelated concerns into separate commits; never\n  one giant unreviewable diff.\n- **Always `git fetch upstream master` (or origin) immediately before a rebase.** Rebase\n  onto the refreshed tip, not a stale local `master`.\n- Merging PRs: **squash by default** with a hand-written subject\n  `type(scope): description (#N)` (release-plz parses it; merge commits are disabled).\n  Rebase-merge only when every commit on the branch is already release-quality\n  conventional. Wait for the Greptile review check and CI before merging — findings get\n  fixed, replied to, and resolved, not ignored.\n- PR bodies: `## Summary`, `## Changes` (per-crate bullets), `## Testing` listing the\n  exact commands run plus hardware-verification status (say \"not runtime-tested on\n  hardware\" when true), and a closing `Fixes #N` line. Screenshots for UI changes.\n- **All GitHub artifacts — PR titles/bodies, commits, issues, reviews, comments — are\n  written in English.**\n- **Never add AI attribution** (\"Generated with …\", AI co-author trailers) to commits,\n  PRs, or issues — including when adopting contributors' work.\n- Never post to external repos or reply publicly on the maintainer's behalf — draft the\n  text for approval. Keep public drafts short, casual, and problem-focused.\n- Contributor PRs are adopted, not rejected: check `maintainerCanModify`, rebase onto\n  **fresh** master in a worktree, fix review findings, run the **full local gate** on\n  the rebased tip, **then** push to the fork branch; preserve authorship\n  (`Co-authored-by` when re-homing work). Squash-then-rebase is fine when the PR is\n  far behind and commit-by-commit conflicts thrash.\n- Issues use the bug/feature/device forms and the `type:`/`area:`/`platform:`/`needs:`/\n  `status:` label families. Deferred or out-of-scope work becomes a linked issue, not a\n  TODO comment.\n\n### CI / Actions when adopting PRs\n\n- CI concurrency is **per branch** (`ci-${{ workflow }}-${{ ref }}` with\n  `cancel-in-progress: true`). Approving or re-running an **old SHA** on the same\n  branch cancels the current-head run. Only approve / re-run workflows whose\n  `head_sha` equals the PR's current head.\n- After a force-push, wait for the new runs; do not re-approve stale\n  `action_required` jobs from earlier commits on that branch.\n- First-time-fork PRs may sit in `action_required` until a maintainer approves the\n  workflow run — that is fine; still do not push until the local gate is green.\n\n## Releases\n\nrelease-plz drives releases: one unified workspace version, ONE root `CHANGELOG.md`\n(never per-crate changelogs), and a single `v{version}` tag that only release-plz\ncreates — **never hand-create the tag**. Published GitHub releases are immutable:\nnever re-run a failed release job or re-dispatch on an existing tag.\n`release-plz.toml` is the versioning contract — don't trim it.\n\n## Verification\n\nDefine the concrete check that proves a change works before writing it — a failing test\nthat should pass, a command whose output should change, a behavior in the running app —\nand loop on that check. Real-hardware verification (physical mice, receivers) is the\nmaintainer's job: every fix PR states how to test it. Report outcomes honestly,\nincluding what was NOT verified.\n\n**Push checklist (agents):**\n\n1. Rebase/merge conflicts fully resolved — no `<<<<<<<` left, no half-ported APIs.\n2. Full local gate green on the **final** tree (fmt + clippy `-D warnings` + test).\n3. If cfg-gated files changed: cross-lint or hand-audit against master (see above).\n4. If wire types changed: `wire_format` tests green + `PROTOCOL_VERSION` bumped.\n5. If locales changed: every `locales/*.yml` must have the same keys as\n   `en.yml`; run `cargo test -p openlogi-gui i18n`.\n6. Only then `git push` / force-push to the PR branch.\n\n## i18n (all locale files, then Crowdin)\n\n- Add or change UI strings in **every** `crates/openlogi-gui/locales/*.yml` in\n  the same PR. `en.yml` is the English source of truth (the English text IS the\n  key); other files must not lag — the parity test fails the build.\n- Crowdin improves non-English **values** over time. The sync job **merges**\n  downloads into complete catalogs (`scripts/i18n/merge_crowdin_download.py`):\n  only real translations apply; English fill-in and sparse exports never wipe\n  keys or open noise PRs.\n- Details: [`.claude/rules/i18n.md`](.claude/rules/i18n.md).\n\n## Subsystem rules — read before touching\n\nClaude Code loads these automatically per path; other agents: read the listed file\nbefore editing that area.\n\n| Area | Rule file |\n|---|---|\n| `crates/openlogi-gui/**` (GPUI app) | `.claude/rules/gui.md` |\n| `crates/openlogi-gui/locales/**`, `src/i18n.rs` | `.claude/rules/i18n.md` |\n| `crates/openlogi-agent-core/**`, `crates/openlogi-agent/**`, `crates/openlogi-ipc/**` (IPC wire) | `.claude/rules/ipc-protocol.md` |\n| `crates/openlogi-hidpp/**`, `crates/openlogi-hid/**` | `.claude/rules/hidpp.md` |\n| `crates/openlogi-hook/**` (event taps) | `.claude/rules/hook.md` |\n| `xtask/**`, `packaging/**`, `scripts/**` | `.claude/rules/xtask.md` (+ `xtask/README.md`) |\n| `crates/openlogi-gui/src/platform/**` (ObjC FFI) | `crates/openlogi-gui/src/platform/AGENTS.md` |\n"},"files":{"AGENTS.md":"# OpenLogi — Agent Guide\n\nOpenLogi is a native, local-first alternative to Logitech Options+ written in Rust:\nbutton remapping, DPI, SmartShift, and per-app profiles for Logitech HID++ devices\n(Bolt/Unifying receiver, Bluetooth-direct, wired) — no account, no telemetry, plain-TOML\nconfig. macOS and Linux are first-class; Windows is a young but shipping port.\nDual-licensed MIT/Apache-2.0; the `design/` brand assets are proprietary.\n\nThe developer handbook (toolchain, packaging, release pipeline) is\n[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). This file is the agent-facing contract;\nsubsystem deep-rules are indexed at the bottom.\n\n## Architecture\n\nThree tiers ship in one install: the **GUI** is a pure IPC client, the **agent** is a\nbackground server owning the input hook and ALL device I/O, and shared orchestration\nsits beneath both.\n\n| Crate | Role |\n|---|---|\n| `openlogi` (root package, `src/`) | The CLI binary — thin wrapper over `openlogi-cli` |\n| `crates/openlogi-core` | Pure types: TOML config, device model, action catalog. No I/O, no async |\n| `crates/openlogi-hidpp` | Vendored fork of the `hidpp` protocol crate (**lib name `hidpp`**, 0BSD) |\n| `crates/openlogi-hid` | Device discovery + HID++ writes over `async-hid` |\n| `crates/openlogi-assets` | Device-render registry + cached fetch from OpenLogi asset mirrors |\n| `crates/openlogi-cli` | `clap` command tree: `list`, `assets`, `diag` |\n| `crates/openlogi-hook` | OS input capture: CGEventTap / evdev+uinput / WH_MOUSE_LL |\n| `crates/openlogi-inject` | OS input synthesis: CGEvent / uinput+MPRIS / SendInput |\n| `crates/openlogi-agent-core` | Shared agent orchestration: hook runtime, HID++ writes, DPI cycle, Actions Ring session state |\n| `crates/openlogi-ipc` | The tarpc IPC contract (`src/ipc.rs`) + its local-socket transport, shared by agent and GUI |\n| `crates/openlogi-agent` | The `openlogi-agent` binary — hook + device I/O server |\n| `crates/openlogi-gui` | GPUI + gpui-component desktop app — polls the agent, no device I/O |\n| `xtask` | `cargo xtask` maintenance: bundling, packaging, release manifest |\n\n- GUI ↔ agent speak tarpc/bincode over an `interprocess` local socket. The wire format\n  is versioned and **append-only** — read `.claude/rules/ipc-protocol.md` before touching it.\n- Platform code is cfg-gated per crate (`[target.'cfg(target_os = …)'.dependencies]`).\n  The workspace's ObjC FFI is centralized in `crates/openlogi-gui/src/platform/` — read\n  that directory's `AGENTS.md` before editing it.\n\n## Build, run, verify\n\nNix/devenv is optional — rustup + `rust-toolchain.toml` is enough. If devenv is\ninstalled, direnv loads it; otherwise `.envrc` prints a notice and leaves PATH\nalone so system `cargo` works. With devenv active, cargo may only be on PATH\ninside the shell — run from the repo root (or `direnv exec . …`), including\ngit (the hooks need cargo):\n\n```sh\ncargo clippy --workspace --all-targets -- -D warnings\n# when cargo is only inside devenv:\ndirenv exec . cargo clippy --workspace --all-targets -- -D warnings\ndirenv exec . git commit …\n```\n\n### Local gate (hard stop — do this before every push)\n\n**Never `git push` until the final tree has passed the full local gate.**\n`cargo check` alone is not enough. Conflict resolution + \"it compiles on my\nMac\" is not enough. Run **all three** on the commit you are about to push:\n\n```sh\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets -- -D warnings\ncargo test --workspace\n# or: devenv tasks run openlogi:check\n```\n\nExit non-zero on any of those → fix, re-run the **whole** triple, then push.\nDo not push \"to see if CI likes it.\" CI is confirmation, not the first compile.\n\nprek hooks (`prek.toml`): `cargo fmt` at commit; full-workspace clippy at push\n(rust-scoped, so non-Rust pushes skip it). Hooks are a backstop, not a substitute\nfor running the gate yourself after a rebase.\n\n### Platform / cfg-gated code (macOS-green is a trap)\n\nmacOS-green proves **nothing** about `#[cfg(target_os = \"linux\")]` /\n`windows` code. Recent agent failures that only showed up on CI Linux:\n\n- Shadowing a crate-level constant with a local `const` of a different type\n  (e.g. `LOGITECH_VENDOR_ID: u16` next to `use crate::LOGITECH_VENDOR_ID`\n  which is `u32`) — E0255 / E0308, **only compiles on Linux**.\n- Importing a name that only exists on another OS, or redefining one that\n  master already exports from `lib.rs`.\n\nWhen the diff touches any of:\n\n- `crates/openlogi-hook/src/linux.rs` / `windows.rs`\n- `crates/openlogi-inject/src/inject/linux.rs` / `windows.rs`\n- `crates/openlogi-hid/src/transport.rs` (has `#[cfg]` branches)\n- any `#[cfg(target_os = …)]` block\n\nyou MUST either:\n\n1. Cross-check with devenv when available:\n   `devenv tasks run openlogi:check-windows` (and any linux check the repo has), or\n2. Manually re-read every changed cfg-gated file against **current master** for:\n   - name collisions with existing `pub use` / `pub const` items\n   - type mismatches (`u16` vs `u32`, `Option` arity, new enum fields)\n   - call sites that gained args on master (e.g. `with_runtime`, `build_device_list`,\n     `dispatch_action`) but the PR still uses the old signature\n\nDo not claim \"cross-platform green\" without CI (or a local cross-lint) having\nactually run those targets. `RUSTFLAGS=-D warnings` is global in CI — plain\nwarnings fail there too.\n\n### Wire format / IPC (another silent CI red)\n\nIf the change touches anything that crosses the agent↔GUI boundary\n(`crates/openlogi-ipc/src/ipc.rs`, serde enums in hid write errors, `DeviceKind`, …):\n\n- Enums are **append-only** (serde index = wire). New variants go at the end.\n- Bump `PROTOCOL_VERSION` and regenerate\n  `crates/openlogi-ipc/tests/wire_format.rs` goldens from the failure\n  message (`left` is the new encoding).\n- Run `cargo test -p openlogi-ipc --test wire_format` before push.\n\n### i18n\n\nNew GUI strings: insert the same key in the **same position** in every\n`crates/openlogi-gui/locales/*.yml` (parity is required). Run\n`cargo test -p openlogi-gui i18n`.\n\n### App / agent runtime notes\n\n- The macOS GUI build needs full Xcode for GPUI's Metal shaders. devenv sets\n  `DEVELOPER_DIR`/`SDKROOT` when present; without it, use system Xcode. If the\n  shader compile fails under devenv, `direnv reload` first.\n- Dev-run the app with `cargo run -p openlogi-gui` — a cargo runner wraps it\n  into `target/dev/OpenLogi.app`. `cargo build` does NOT refresh that bundle,\n  and a second instance exits on the singleton lock: quit the old instance and\n  re-`run` before judging a UI change \"not applied\".\n- No hardware attached? `cargo run -p openlogi-agent --bin openlogi-agent-mock`\n  serves a scripted inventory (both route kinds, every capability-gated panel, a\n  pairing flow) over the IPC socket, so the GUI runs unmodified. It defaults to\n  the `openlogi-dev` profile — same socket the dev bundle uses, production app\n  untouched. Details in `docs/DEVELOPMENT.md`.\n\n## Rust standards\n\nEdition 2024, MSRV 1.96. There is exactly **one** lint table, in the root `Cargo.toml`,\nand every crate inherits it with `[lints] workspace = true` — never a private copy, or\nthe next lint added to the workspace silently skips that crate. A crate needing a\ndifferent level opts out **in source** (the `openlogi-hook` platform modules carry\n`#![allow(unsafe_code, reason = \"…\")]`), because Cargo rejects mixing `workspace = true`\nwith local overrides. `openlogi-hidpp` deliberately stays out of the table (vendored).\n\nThe table: `unsafe_code = \"deny\"` (opt out per item with `#[expect(unsafe_code,\nreason = \"…\")]` plus a `// SAFETY:` comment), `clippy::pedantic` at warn,\n`unwrap_used`/`expect_used` at warn, plus the shared lint set —\n`assertions_on_result_states`, `cast_possible_truncation`, `cast_possible_wrap`,\n`cast_sign_loss`, `error_impl_error`, `exit`, `or_fun_call`, `ptr_as_ptr`,\n`tests_outside_test_module`, `undocumented_unsafe_blocks`. Any lint suppression carries\na `reason`. What that changes day to day:\n\n- Every `unsafe` block needs a `// SAFETY:` comment saying why it is sound.\n- `assert!(r.is_ok())` / `assert!(r.is_err())` are rejected — unwrap the `Result` (in a\n  test module that already allows it) or give the assertion a message.\n- A test module that wants `expect`/`unwrap` says so:\n  `#[allow(clippy::expect_used, reason = \"expect/unwrap are idiomatic in tests\")]` on the\n  module (or on its `mod tests;` declaration). Never route around the lint with\n  `unwrap_or_else(|e| panic!(\"…: {e}\"))` — that is the same panic with the check switched\n  off. The one honest use of that form is a *dynamic* panic message, where `expect` would\n  need a `format!` that allocates on the happy path (`expect_fun_call`).\n- A test module gated on more than `test` needs stacked attributes (`#[cfg(test)]` then\n  `#[cfg(unix)]`), not `#[cfg(all(test, unix))]`, which clippy reads as a test outside a\n  test module. Integration tests under `tests/` carry a file-level\n  `#![expect(clippy::tests_outside_test_module, reason = \"…\")]`.\n- `std::process::exit` needs `#[expect(clippy::exit, reason = \"…\")]` naming why that call\n  site cannot hand an `ExitCode` back to `main` instead.\n\nEncode invariants in the type system instead of checking them at runtime:\n\n- Wire/firmware values get typed wrappers: `num_enum` for discriminants, `bitflags`\n  (`from_bits_retain` when unknown bits are legal) for flag sets. Unknown wire values\n  surface as **errors** (`UnsupportedResponse`-style), never as silent fallbacks.\n- Replace long parameter lists with Change/Params structs; make illegal combinations\n  unrepresentable rather than validated.\n- Ownership models resources (`Retained<T>` in the ObjC FFI) and thread affinity is\n  proven by types (`MainThreadMarker`, `!Send` handles), not by runtime checks.\n- Libraries return `thiserror` types; binaries may use `anyhow`.\n\nHouse style:\n\n- **Root-cause fixes only.** Never layer compatibility shims over a broken abstraction —\n  refactor it. Never change product code to work around a dev-environment quirk; debug\n  the environment (or a release build) instead.\n- **Prefer mature crates over hand-rolled logic** (retry/backoff, hashing, paths, …).\n  Check `cargo tree | grep <candidate>` before adding a dependency and use `cargo add`\n  so versions come from the registry. After ANY dependency change, verify the\n  `gpui`/`gpui-component` git pins in `Cargo.lock` didn't move (they are held only by\n  the lock; restore with `cargo update -p gpui --precise <rev>`).\n- Module layout: a module with its own semantics is `foo.rs` (children in a sibling\n  `foo/`); `foo/mod.rs` is only for pure namespace shells. Never both for one module.\n- Keep files reasonably sized (split around ~500 lines) into real modules — never\n  simulate structure with `// ---- section ----` banner comments. But don't\n  over-extract either: inline single-use helpers.\n- rustdoc every public item. Comments state non-obvious constraints only.\n- Tests cover failure and edge paths, not just the happy path (state machines\n  especially). No tautological tests that mirror the implementation; never weaken an\n  assertion or special-case an input to make a test pass.\n\n## Git & GitHub\n\n- Conventional commits: `type(scope): imperative lowercase description`. Types in use:\n  `feat fix refactor chore docs ci perf style build test`. Scopes are crate short names\n  (`gui agent hidpp hid core hook ipc cli assets xtask`) or cross-cutting concerns\n  (`release ci i18n windows linux macos tray infra`). `i18n` is a scope, not a type.\n- Branches: `type/kebab-description` off `master`. Substantial or risky work goes in a\n  worktree so parallel work doesn't collide; trivial fixes may go straight to master.\n- Commits are small and focused — split unrelated concerns into separate commits; never\n  one giant unreviewable diff.\n- **Always `git fetch upstream master` (or origin) immediately before a rebase.** Rebase\n  onto the refreshed tip, not a stale local `master`.\n- Merging PRs: **squash by default** with a hand-written subject\n  `type(scope): description (#N)` (release-plz parses it; merge commits are disabled).\n  Rebase-merge only when every commit on the branch is already release-quality\n  conventional. Wait for the Greptile review check and CI before merging — findings get\n  fixed, replied to, and resolved, not ignored.\n- PR bodies: `## Summary`, `## Changes` (per-crate bullets), `## Testing` listing the\n  exact commands run plus hardware-verification status (say \"not runtime-tested on\n  hardware\" when true), and a closing `Fixes #N` line. Screenshots for UI changes.\n- **All GitHub artifacts — PR titles/bodies, commits, issues, reviews, comments — are\n  written in English.**\n- **Never add AI attribution** (\"Generated with …\", AI co-author trailers) to commits,\n  PRs, or issues — including when adopting contributors' work.\n- Never post to external repos or reply publicly on the maintainer's behalf — draft the\n  text for approval. Keep public drafts short, casual, and problem-focused.\n- Contributor PRs are adopted, not rejected: check `maintainerCanModify`, rebase onto\n  **fresh** master in a worktree, fix review findings, run the **full local gate** on\n  the rebased tip, **then** push to the fork branch; preserve authorship\n  (`Co-authored-by` when re-homing work). Squash-then-rebase is fine when the PR is\n  far behind and commit-by-commit conflicts thrash.\n- Issues use the bug/feature/device forms and the `type:`/`area:`/`platform:`/`needs:`/\n  `status:` label families. Deferred or out-of-scope work becomes a linked issue, not a\n  TODO comment.\n\n### CI / Actions when adopting PRs\n\n- CI concurrency is **per branch** (`ci-${{ workflow }}-${{ ref }}` with\n  `cancel-in-progress: true`). Approving or re-running an **old SHA** on the same\n  branch cancels the current-head run. Only approve / re-run workflows whose\n  `head_sha` equals the PR's current head.\n- After a force-push, wait for the new runs; do not re-approve stale\n  `action_required` jobs from earlier commits on that branch.\n- First-time-fork PRs may sit in `action_required` until a maintainer approves the\n  workflow run — that is fine; still do not push until the local gate is green.\n\n## Releases\n\nrelease-plz drives releases: one unified workspace version, ONE root `CHANGELOG.md`\n(never per-crate changelogs), and a single `v{version}` tag that only release-plz\ncreates — **never hand-create the tag**. Published GitHub releases are immutable:\nnever re-run a failed release job or re-dispatch on an existing tag.\n`release-plz.toml` is the versioning contract — don't trim it.\n\n## Verification\n\nDefine the concrete check that proves a change works before writing it — a failing test\nthat should pass, a command whose output should change, a behavior in the running app —\nand loop on that check. Real-hardware verification (physical mice, receivers) is the\nmaintainer's job: every fix PR states how to test it. Report outcomes honestly,\nincluding what was NOT verified.\n\n**Push checklist (agents):**\n\n1. Rebase/merge conflicts fully resolved — no `<<<<<<<` left, no half-ported APIs.\n2. Full local gate green on the **final** tree (fmt + clippy `-D warnings` + test).\n3. If cfg-gated files changed: cross-lint or hand-audit against master (see above).\n4. If wire types changed: `wire_format` tests green + `PROTOCOL_VERSION` bumped.\n5. If locales changed: every `locales/*.yml` must have the same keys as\n   `en.yml`; run `cargo test -p openlogi-gui i18n`.\n6. Only then `git push` / force-push to the PR branch.\n\n## i18n (all locale files, then Crowdin)\n\n- Add or change UI strings in **every** `crates/openlogi-gui/locales/*.yml` in\n  the same PR. `en.yml` is the English source of truth (the English text IS the\n  key); other files must not lag — the parity test fails the build.\n- Crowdin improves non-English **values** over time. The sync job **merges**\n  downloads into complete catalogs (`scripts/i18n/merge_crowdin_download.py`):\n  only real translations apply; English fill-in and sparse exports never wipe\n  keys or open noise PRs.\n- Details: [`.claude/rules/i18n.md`](.claude/rules/i18n.md).\n\n## Subsystem rules — read before touching\n\nClaude Code loads these automatically per path; other agents: read the listed file\nbefore editing that area.\n\n| Area | Rule file |\n|---|---|\n| `crates/openlogi-gui/**` (GPUI app) | `.claude/rules/gui.md` |\n| `crates/openlogi-gui/locales/**`, `src/i18n.rs` | `.claude/rules/i18n.md` |\n| `crates/openlogi-agent-core/**`, `crates/openlogi-agent/**`, `crates/openlogi-ipc/**` (IPC wire) | `.claude/rules/ipc-protocol.md` |\n| `crates/openlogi-hidpp/**`, `crates/openlogi-hid/**` | `.claude/rules/hidpp.md` |\n| `crates/openlogi-hook/**` (event taps) | `.claude/rules/hook.md` |\n| `xtask/**`, `packaging/**`, `scripts/**` | `.claude/rules/xtask.md` (+ `xtask/README.md`) |\n| `crates/openlogi-gui/src/platform/**` (ObjC FFI) | `crates/openlogi-gui/src/platform/AGENTS.md` |\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# OpenLogi — Agent Guide\n\nOpenLogi is a native, local-first alternative to Logitech Options+ written in Rust:\nbutton remapping, DPI, SmartShift, and per-app profiles for Logitech HID++ devices\n(Bolt/Unifying receiver, Bluetooth-direct, wired) — no account, no telemetry, plain-TOML\nconfig. macOS and Linux are first-class; Windows is a young but shipping port.\nDual-licensed MIT/Apache-2.0; the `design/` brand assets are proprietary.\n\nThe developer handbook (toolchain, packaging, release pipeline) is\n[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). This file is the agent-facing contract;\nsubsystem deep-rules are indexed at the bottom.\n\n## Architecture\n\nThree tiers ship in one install: the **GUI** is a pure IPC client, the **agent** is a\nbackground server owning the input hook and ALL device I/O, and shared orchestration\nsits beneath both.\n\n| Crate | Role |\n|---|---|\n| `openlogi` (root package, `src/`) | The CLI binary — thin wrapper over `openlogi-cli` |\n| `crates/openlogi-core` | Pure types: TOML config, device model, action catalog. No I/O, no async |\n| `crates/openlogi-hidpp` | Vendored fork of the `hidpp` protocol crate (**lib name `hidpp`**, 0BSD) |\n| `crates/openlogi-hid` | Device discovery + HID++ writes over `async-hid` |\n| `crates/openlogi-assets` | Device-render registry + cached fetch from OpenLogi asset mirrors |\n| `crates/openlogi-cli` | `clap` command tree: `list`, `assets`, `diag` |\n| `crates/openlogi-hook` | OS input capture: CGEventTap / evdev+uinput / WH_MOUSE_LL |\n| `crates/openlogi-inject` | OS input synthesis: CGEvent / uinput+MPRIS / SendInput |\n| `crates/openlogi-agent-core` | Shared agent orchestration: hook runtime, HID++ writes, DPI cycle, Actions Ring session state |\n| `crates/openlogi-ipc` | The tarpc IPC contract (`src/ipc.rs`) + its local-socket transport, shared by agent and GUI |\n| `crates/openlogi-agent` | The `openlogi-agent` binary — hook + device I/O server |\n| `crates/openlogi-gui` | GPUI + gpui-component desktop app — polls the agent, no device I/O |\n| `xtask` | `cargo xtask` maintenance: bundling, packaging, release manifest |\n\n- GUI ↔ agent speak tarpc/bincode over an `interprocess` local socket. The wire format\n  is versioned and **append-only** — read `.claude/rules/ipc-protocol.md` before touching it.\n- Platform code is cfg-gated per crate (`[target.'cfg(target_os = …)'.dependencies]`).\n  The workspace's ObjC FFI is centralized in `crates/openlogi-gui/src/platform/` — read\n  that directory's `AGENTS.md` before editing it.\n\n## Build, run, verify\n\nNix/devenv is optional — rustup + `rust-toolchain.toml` is enough. If devenv is\ninstalled, direnv loads it; otherwise `.envrc` prints a notice and leaves PATH\nalone so system `cargo` works. With devenv active, cargo may only be on PATH\ninside the shell — run from the repo root (or `direnv exec . …`), including\ngit (the hooks need cargo):\n\n```sh\ncargo clippy --workspace --all-targets -- -D warnings\n# when cargo is only inside devenv:\ndirenv exec . cargo clippy --workspace --all-targets -- -D warnings\ndirenv exec . git commit …\n```\n\n### Local gate (hard stop — do this before every push)\n\n**Never `git push` until the final tree has passed the full local gate.**\n`cargo check` alone is not enough. Conflict resolution + \"it compiles on my\nMac\" is not enough. Run **all three** on the commit you are about to push:\n\n```sh\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets -- -D warnings\ncargo test --workspace\n# or: devenv tasks run openlogi:check\n```\n\nExit non-zero on any of those → fix, re-run the **whole** triple, then push.\nDo not push \"to see if CI likes it.\" CI is confirmation, not the first compile.\n\nprek hooks (`prek.toml`): `cargo fmt` at commit; full-workspace clippy at push\n(rust-scoped, so non-Rust pushes skip it). Hooks are a backstop, not a substitute\nfor running the gate yourself after a rebase.\n\n### Platform / cfg-gated code (macOS-green is a trap)\n\nmacOS-green proves **nothing** about `#[cfg(target_os = \"linux\")]` /\n`windows` code. Recent agent failures that only showed up on CI Linux:\n\n- Shadowing a crate-level constant with a local `const` of a different type\n  (e.g. `LOGITECH_VENDOR_ID: u16` next to `use crate::LOGITECH_VENDOR_ID`\n  which is `u32`) — E0255 / E0308, **only compiles on Linux**.\n- Importing a name that only exists on another OS, or redefining one that\n  master already exports from `lib.rs`.\n\nWhen the diff touches any of:\n\n- `crates/openlogi-hook/src/linux.rs` / `windows.rs`\n- `crates/openlogi-inject/src/inject/linux.rs` / `windows.rs`\n- `crates/openlogi-hid/src/transport.rs` (has `#[cfg]` branches)\n- any `#[cfg(target_os = …)]` block\n\nyou MUST either:\n\n1. Cross-check with devenv when available:\n   `devenv tasks run openlogi:check-windows` (and any linux check the repo has), or\n2. Manually re-read every changed cfg-gated file against **current master** for:\n   - name collisions with existing `pub use` / `pub const` items\n   - type mismatches (`u16` vs `u32`, `Option` arity, new enum fields)\n   - call sites that gained args on master (e.g. `with_runtime`, `build_device_list`,\n     `dispatch_action`) but the PR still uses the old signature\n\nDo not claim \"cross-platform green\" without CI (or a local cross-lint) having\nactually run those targets. `RUSTFLAGS=-D warnings` is global in CI — plain\nwarnings fail there too.\n\n### Wire format / IPC (another silent CI red)\n\nIf the change touches anything that crosses the agent↔GUI boundary\n(`crates/openlogi-ipc/src/ipc.rs`, serde enums in hid write errors, `DeviceKind`, …):\n\n- Enums are **append-only** (serde index = wire). New variants go at the end.\n- Bump `PROTOCOL_VERSION` and regenerate\n  `crates/openlogi-ipc/tests/wire_format.rs` goldens from the failure\n  message (`left` is the new encoding).\n- Run `cargo test -p openlogi-ipc --test wire_format` before push.\n\n### i18n\n\nNew GUI strings: insert the same key in the **same position** in every\n`crates/openlogi-gui/locales/*.yml` (parity is required). Run\n`cargo test -p openlogi-gui i18n`.\n\n### App / agent runtime notes\n\n- The macOS GUI build needs full Xcode for GPUI's Metal shaders. devenv sets\n  `DEVELOPER_DIR`/`SDKROOT` when present; without it, use system Xcode. If the\n  shader compile fails under devenv, `direnv reload` first.\n- Dev-run the app with `cargo run -p openlogi-gui` — a cargo runner wraps it\n  into `target/dev/OpenLogi.app`. `cargo build` does NOT refresh that bundle,\n  and a second instance exits on the singleton lock: quit the old instance and\n  re-`run` before judging a UI change \"not applied\".\n- No hardware attached? `cargo run -p openlogi-agent --bin openlogi-agent-mock`\n  serves a scripted inventory (both route kinds, every capability-gated panel, a\n  pairing flow) over the IPC socket, so the GUI runs unmodified. It defaults to\n  the `openlogi-dev` profile — same socket the dev bundle uses, production app\n  untouched. Details in `docs/DEVELOPMENT.md`.\n\n## Rust standards\n\nEdition 2024, MSRV 1.96. There is exactly **one** lint table, in the root `Cargo.toml`,\nand every crate inherits it with `[lints] workspace = true` — never a private copy, or\nthe next lint added to the workspace silently skips that crate. A crate needing a\ndifferent level opts out **in source** (the `openlogi-hook` platform modules carry\n`#![allow(unsafe_code, reason = \"…\")]`), because Cargo rejects mixing `workspace = true`\nwith local overrides. `openlogi-hidpp` deliberately stays out of the table (vendored).\n\nThe table: `unsafe_code = \"deny\"` (opt out per item with `#[expect(unsafe_code,\nreason = \"…\")]` plus a `// SAFETY:` comment), `clippy::pedantic` at warn,\n`unwrap_used`/`expect_used` at warn, plus the shared lint set —\n`assertions_on_result_states`, `cast_possible_truncation`, `cast_possible_wrap`,\n`cast_sign_loss`, `error_impl_error`, `exit`, `or_fun_call`, `ptr_as_ptr`,\n`tests_outside_test_module`, `undocumented_unsafe_blocks`. Any lint suppression carries\na `reason`. What that changes day to day:\n\n- Every `unsafe` block needs a `// SAFETY:` comment saying why it is sound.\n- `assert!(r.is_ok())` / `assert!(r.is_err())` are rejected — unwrap the `Result` (in a\n  test module that already allows it) or give the assertion a message.\n- A test module that wants `expect`/`unwrap` says so:\n  `#[allow(clippy::expect_used, reason = \"expect/unwrap are idiomatic in tests\")]` on the\n  module (or on its `mod tests;` declaration). Never route around the lint with\n  `unwrap_or_else(|e| panic!(\"…: {e}\"))` — that is the same panic with the check switched\n  off. The one honest use of that form is a *dynamic* panic message, where `expect` would\n  need a `format!` that allocates on the happy path (`expect_fun_call`).\n- A test module gated on more than `test` needs stacked attributes (`#[cfg(test)]` then\n  `#[cfg(unix)]`), not `#[cfg(all(test, unix))]`, which clippy reads as a test outside a\n  test module. Integration tests under `tests/` carry a file-level\n  `#![expect(clippy::tests_outside_test_module, reason = \"…\")]`.\n- `std::process::exit` needs `#[expect(clippy::exit, reason = \"…\")]` naming why that call\n  site cannot hand an `ExitCode` back to `main` instead.\n\nEncode invariants in the type system instead of checking them at runtime:\n\n- Wire/firmware values get typed wrappers: `num_enum` for discriminants, `bitflags`\n  (`from_bits_retain` when unknown bits are legal) for flag sets. Unknown wire values\n  surface as **errors** (`UnsupportedResponse`-style), never as silent fallbacks.\n- Replace long parameter lists with Change/Params structs; make illegal combinations\n  unrepresentable rather than validated.\n- Ownership models resources (`Retained<T>` in the ObjC FFI) and thread affinity is\n  proven by types (`MainThreadMarker`, `!Send` handles), not by runtime checks.\n- Libraries return `thiserror` types; binaries may use `anyhow`.\n\nHouse style:\n\n- **Root-cause fixes only.** Never layer compatibility shims over a broken abstraction —\n  refactor it. Never change product code to work around a dev-environment quirk; debug\n  the environment (or a release build) instead.\n- **Prefer mature crates over hand-rolled logic** (retry/backoff, hashing, paths, …).\n  Check `cargo tree | grep <candidate>` before adding a dependency and use `cargo add`\n  so versions come from the registry. After ANY dependency change, verify the\n  `gpui`/`gpui-component` git pins in `Cargo.lock` didn't move (they are held only by\n  the lock; restore with `cargo update -p gpui --precise <rev>`).\n- Module layout: a module with its own semantics is `foo.rs` (children in a sibling\n  `foo/`); `foo/mod.rs` is only for pure namespace shells. Never both for one module.\n- Keep files reasonably sized (split around ~500 lines) into real modules — never\n  simulate structure with `// ---- section ----` banner comments. But don't\n  over-extract either: inline single-use helpers.\n- rustdoc every public item. Comments state non-obvious constraints only.\n- Tests cover failure and edge paths, not just the happy path (state machines\n  especially). No tautological tests that mirror the implementation; never weaken an\n  assertion or special-case an input to make a test pass.\n\n## Git & GitHub\n\n- Conventional commits: `type(scope): imperative lowercase description`. Types in use:\n  `feat fix refactor chore docs ci perf style build test`. Scopes are crate short names\n  (`gui agent hidpp hid core hook ipc cli assets xtask`) or cross-cutting concerns\n  (`release ci i18n windows linux macos tray infra`). `i18n` is a scope, not a type.\n- Branches: `type/kebab-description` off `master`. Substantial or risky work goes in a\n  worktree so parallel work doesn't collide; trivial fixes may go straight to master.\n- Commits are small and focused — split unrelated concerns into separate commits; never\n  one giant unreviewable diff.\n- **Always `git fetch upstream master` (or origin) immediately before a rebase.** Rebase\n  onto the refreshed tip, not a stale local `master`.\n- Merging PRs: **squash by default** with a hand-written subject\n  `type(scope): description (#N)` (release-plz parses it; merge commits are disabled).\n  Rebase-merge only when every commit on the branch is already release-quality\n  conventional. Wait for the Greptile review check and CI before merging — findings get\n  fixed, replied to, and resolved, not ignored.\n- PR bodies: `## Summary`, `## Changes` (per-crate bullets), `## Testing` listing the\n  exact commands run plus hardware-verification status (say \"not runtime-tested on\n  hardware\" when true), and a closing `Fixes #N` line. Screenshots for UI changes.\n- **All GitHub artifacts — PR titles/bodies, commits, issues, reviews, comments — are\n  written in English.**\n- **Never add AI attribution** (\"Generated with …\", AI co-author trailers) to commits,\n  PRs, or issues — including when adopting contributors' work.\n- Never post to external repos or reply publicly on the maintainer's behalf — draft the\n  text for approval. Keep public drafts short, casual, and problem-focused.\n- Contributor PRs are adopted, not rejected: check `maintainerCanModify`, rebase onto\n  **fresh** master in a worktree, fix review findings, run the **full local gate** on\n  the rebased tip, **then** push to the fork branch; preserve authorship\n  (`Co-authored-by` when re-homing work). Squash-then-rebase is fine when the PR is\n  far behind and commit-by-commit conflicts thrash.\n- Issues use the bug/feature/device forms and the `type:`/`area:`/`platform:`/`needs:`/\n  `status:` label families. Deferred or out-of-scope work becomes a linked issue, not a\n  TODO comment.\n\n### CI / Actions when adopting PRs\n\n- CI concurrency is **per branch** (`ci-${{ workflow }}-${{ ref }}` with\n  `cancel-in-progress: true`). Approving or re-running an **old SHA** on the same\n  branch cancels the current-head run. Only approve / re-run workflows whose\n  `head_sha` equals the PR's current head.\n- After a force-push, wait for the new runs; do not re-approve stale\n  `action_required` jobs from earlier commits on that branch.\n- First-time-fork PRs may sit in `action_required` until a maintainer approves the\n  workflow run — that is fine; still do not push until the local gate is green.\n\n## Releases\n\nrelease-plz drives releases: one unified workspace version, ONE root `CHANGELOG.md`\n(never per-crate changelogs), and a single `v{version}` tag that only release-plz\ncreates — **never hand-create the tag**. Published GitHub releases are immutable:\nnever re-run a failed release job or re-dispatch on an existing tag.\n`release-plz.toml` is the versioning contract — don't trim it.\n\n## Verification\n\nDefine the concrete check that proves a change works before writing it — a failing test\nthat should pass, a command whose output should change, a behavior in the running app —\nand loop on that check. Real-hardware verification (physical mice, receivers) is the\nmaintainer's job: every fix PR states how to test it. Report outcomes honestly,\nincluding what was NOT verified.\n\n**Push checklist (agents):**\n\n1. Rebase/merge conflicts fully resolved — no `<<<<<<<` left, no half-ported APIs.\n2. Full local gate green on the **final** tree (fmt + clippy `-D warnings` + test).\n3. If cfg-gated files changed: cross-lint or hand-audit against master (see above).\n4. If wire types changed: `wire_format` tests green + `PROTOCOL_VERSION` bumped.\n5. If locales changed: every `locales/*.yml` must have the same keys as\n   `en.yml`; run `cargo test -p openlogi-gui i18n`.\n6. Only then `git push` / force-push to the PR branch.\n\n## i18n (all locale files, then Crowdin)\n\n- Add or change UI strings in **every** `crates/openlogi-gui/locales/*.yml` in\n  the same PR. `en.yml` is the English source of truth (the English text IS the\n  key); other files must not lag — the parity test fails the build.\n- Crowdin improves non-English **values** over time. The sync job **merges**\n  downloads into complete catalogs (`scripts/i18n/merge_crowdin_download.py`):\n  only real translations apply; English fill-in and sparse exports never wipe\n  keys or open noise PRs.\n- Details: [`.claude/rules/i18n.md`](.claude/rules/i18n.md).\n\n## Subsystem rules — read before touching\n\nClaude Code loads these automatically per path; other agents: read the listed file\nbefore editing that area.\n\n| Area | Rule file |\n|---|---|\n| `crates/openlogi-gui/**` (GPUI app) | `.claude/rules/gui.md` |\n| `crates/openlogi-gui/locales/**`, `src/i18n.rs` | `.claude/rules/i18n.md` |\n| `crates/openlogi-agent-core/**`, `crates/openlogi-agent/**`, `crates/openlogi-ipc/**` (IPC wire) | `.claude/rules/ipc-protocol.md` |\n| `crates/openlogi-hidpp/**`, `crates/openlogi-hid/**` | `.claude/rules/hidpp.md` |\n| `crates/openlogi-hook/**` (event taps) | `.claude/rules/hook.md` |\n| `xtask/**`, `packaging/**`, `scripts/**` | `.claude/rules/xtask.md` (+ `xtask/README.md`) |\n| `crates/openlogi-gui/src/platform/**` (ObjC FFI) | `crates/openlogi-gui/src/platform/AGENTS.md` |\n","category":"root","tokens":4216}]}