--- name: decky-dev description: Autonomous develop-deploy-debug loop for Steam Deck Decky Loader plugins — sideload deploys, log pulling, CEF/CDP automation of the running Steam UI (navigate, click, screenshot, key input), sleep inhibition, and on-device verification. Use when developing, deploying, or debugging any Decky plugin against a real Steam Deck. --- # Decky Plugin Development & Debugging (generic) Everything here is generic Decky knowledge. Repo-specific policy (deploy consent, log content rules, commit rules) lives in the project's AGENTS.md and always wins. ## Target environment - SteamOS in a **gamescope session**, user `deck`, reachable over SSH (`DECK_HOST=deck@` — no default; a hardcoded one goes stale and silently targets the wrong host). - Decky Loader = `plugin_loader` systemd unit (root). Restart to reload plugins: `sudo systemctl restart plugin_loader`. - Homebrew tree: `~/homebrew/{plugins,logs,settings,data}/` — directory names use `plugin.json`'s `name` field, spaces included. ## Plugin anatomy - `main.py` `Plugin` class: `async` methods are the RPC surface for the frontend (`@decky/api` `callable`); `decky.emit` pushes events to the UI. Backend runs in Decky's **frozen CPython — stdlib only**, compiled extensions will break on Decky upgrades. - `py_modules/` is the only extra dir added to `sys.path` and packaged by the CLI. - Frontend ships as `dist/index.js` (built from `@decky/api` + `@decky/ui`). - `package.json` `remote_binary`: assets downloaded at install time into `bin/` — **saved verbatim, never unpacked**. Archives (e.g. Nuitka standalone tar.gz) must be self-extracted by plugin code on first use. Pin URLs to a release tag: `releases/latest/...` 404s for prereleases. - Installed permissions: plugin root dir is `root:root` (read-only for deck), but `bin/` and `py_modules/` are `deck:deck` (writable) — the remote_binary downloader itself writes `bin/`. ## Deploy loop (sideload) Sideload = build → rsync the plugin subset → restart `plugin_loader`. Keep it in a repo script (this repo: `scripts/deploy.sh`; target is required: `DECK_HOST=deck@ip`). Two rules that bite: - Deploy scripts **copy prebuilt artifacts, they don't build**. Rebuild changed binaries first, or you ship stale ones. - A sideload `dev_mode` marker file (touched by the deploy script, absent from release zips) is a clean way to switch log verbosity; release zips must not contain it. ## Logs - Backend logging goes through `decky.logger` → `DECKY_PLUGIN_LOG_DIR` → `~/homebrew/logs//`, **one file per plugin_loader restart**; newest = current session. - Pull the live log: ```bash ssh "$DECK_HOST" 'f=$(ls -t "$HOME/homebrew/logs//"*.log | head -1); tail -n 200 "$f"' ``` ## Keep the Deck awake while debugging The Deck auto-suspends on idle and kills SSH/CDP mid-verification. Hang a blocker before long debug sessions, release it when done (don't leave the screen on overnight): ```bash # nohup & dies with the ssh session — use a user systemd unit instead ssh "$DECK_HOST" 'systemd-run --user --unit decky-dev-nosleep --collect \ systemd-inhibit --what=idle:sleep --who=decky-dev --why="debugging" sleep infinity' ssh "$DECK_HOST" 'systemd-inhibit --list --mode=block | grep decky' # verify ssh "$DECK_HOST" 'systemctl --user stop decky-dev-nosleep' # release ``` ## CDP: drive the running Steam UI Steam's CEF exposes remote debugging on **port 8080** in Decky environments. The tunnel belongs to Steam and survives `plugin_loader` restarts: ```bash ssh -N -L 8080:localhost:8080 "$DECK_HOST" & curl -s localhost:8080/json | jq '.[].title' ``` Use the **steam-cdp skill** at `.agents/skills/steam-cdp/` for target aliases, bundled drivers, probes, screenshots, and key input. The visible gamepad window is the screenshot target; `SharedJSContext` has no pixels and is for module/backend probes. For UI verification, navigate to the route, exercise the actual flow, capture the visible window, and sample changing state twice when proving liveness or playback. Re-navigating to the current route does not remount it; reset through another tab/home first. ## The autonomous verification loop ``` rebuild (if binaries changed) → deploy → pull log (clean startup?) → CDP: navigate to plugin route → drive the flow (clicks/keys) → screenshot + assert → pull log again (no errors?) → iterate ``` For long waits (a song finishing, a background job), poll the log for the expected marker over SSH in a background task instead of sleeping blind. ## On-device gotchas - **Audio**: child processes need `XDG_RUNTIME_DIR=/run/user/` to reach PipeWire in the gamescope session; a Rust audio binary should dynamically link only `libasound` (check `ldd`). - Verifying "it plays" without ears: position advancing at exactly 1.0× across two timed screenshots is strong evidence the pipeline is alive. - The release install path (zip + remote_binary download + sha256) differs from sideload — archives arrive unextracted, `dev_mode` is absent, and the plugin root is root-owned. Test it by installing the CI zip via Decky developer mode at least once per release.