# AGENTS.md
Project orientation for AI coding assistants (Cursor, GitHub Copilot, OpenAI Codex, Aider, Claude Code, Cline, Continue, Cody, Devin, …) and human contributors.
Slopsmith is a self-hosted web app for browsing, playing, and practicing Rocksmith 2014 Custom DLC. It runs as a Docker container with a FastAPI backend (`server.py`), vanilla JavaScript frontend (`static/`), shared Python libraries (`lib/`), and an extensive plugin system (`plugins/`). No frontend frameworks — plain JS, HTML, Tailwind CSS. AGPL-3.0-only.
This file is the canonical orientation. Tool-specific automation (Claude skills/subagents/rules, Copilot instructions, etc.) lives in [`.claude/`](.claude/) and [`.github/copilot-instructions.md`](.github/copilot-instructions.md); both point back here. For plugin work, start at [`docs/PLUGIN_AUTHORING.md`](docs/PLUGIN_AUTHORING.md).
## Architecture quick reference
```text
server.py FastAPI app — library API, WebSocket highway, plugin loading
main.py Programmatic uvicorn entrypoint — installs structlog before boot
logging_setup.py Structured logging + correlation IDs (LOG_LEVEL/LOG_FORMAT/LOG_FILE)
static/
app.js Main frontend — screens, library views, player, plugin loader
highway.js Canvas note highway renderer (createHighway factory)
diagnostics.js window.slopsmith.diagnostics namespace (loaded first in
)
index.html Single-page app shell
lib/
song.py Core data models (Note, Chord, Arrangement, Song)
psarc.py PSARC archive reading and extraction
sloppak.py Sloppak format support
sloppak_convert.py PSARC → sloppak conversion + Demucs stem split
audio.py WEM/OGG/MP3 audio handling
retune.py Pitch-shifting logic
tunings.py Tuning name/offset utilities
gp2rs.py Guitar Pro to Rocksmith XML conversion
gp2midi.py Guitar Pro to MIDI
plugins/
__init__.py Plugin discovery, loading, requirements install, load_sibling
/ Each plugin is its own directory (often a git submodule)
schema/
plugin.schema.json JSON Schema for plugin.json (validated in CI)
docs/ Plugin contracts + format specs (see Plugin authoring below)
tests/ pytest + tests/js/ (node --test) + tests/browser/ (Playwright)
specs/ Active spec-kit features (specs/001-slopsmith-platform/...)
```
## Running the app
Canonical dev path is Docker Compose — `docker-compose.yml` live-mounts `static/`, `server.py`, `lib/`, `plugins/`, and `VERSION` into the container, so frontend edits are visible on refresh and backend edits trigger uvicorn auto-reload.
```bash
docker compose up # build + run on :8000
DLC_PATH=/path/to/dlc docker compose up # override default Steam DLC path
```
For host-side runs (tests, scripts, no Docker), the programmatic entry point is `main.py`:
```bash
python main.py # HOST=0.0.0.0 PORT=8000 default
HOST=127.0.0.1 PORT=8001 python main.py
```
`main.py` installs the structlog pipeline via `logging_setup.configure_logging()` **before** uvicorn boots and passes `log_config=None` so uvicorn's `dictConfig` never overwrites it. Do not invoke `uvicorn server:app` directly during development — early lifecycle log lines will bypass the structured pipeline and correlation IDs.
**Logging env vars** (read by `logging_setup`):
- `LOG_LEVEL` — `DEBUG | INFO | WARNING | ERROR` (default `INFO`)
- `LOG_FORMAT` — `json | text` (default `text` — coloured console)
- `LOG_FILE` — optional path for a persistent log file (e.g. `/config/slopsmith.log`)
Plugin backend code receives a pre-configured `context["log"]` logger namespaced to `slopsmith.plugin.` — never use `print()`. See [`docs/plugin-logging.md`](docs/plugin-logging.md).
## Testing
```bash
pytest # All Python tests
pytest tests/test_plugins.py -v # Specific file
pytest -k "load_sibling" -v # Pattern match
npm run test:js # Node-native JS plugin-API contract tests (tests/js/)
npm run install:playwright # One-time: install Chromium for Playwright
npm test # Playwright browser tests (tests/browser/)
```
Pytest config in `pyproject.toml` sets `pythonpath = [".", "lib"]` and `testpaths = ["tests"]`. CI runs `pytest` on every push/PR to `main` (Python 3.12). See [`docs/testing-plugins.md`](docs/testing-plugins.md) for fixtures (`isolate_logging`, `reset_plugin_state`) and Playwright patterns.
## Git workflow
- **Never push directly to `main`** — always create a feature branch and open a PR.
- **DCO sign-off is mandatory.** `git commit -s` appends `Signed-off-by:`. Forgot? `git commit --amend -s`. See [`CONTRIBUTING.md`](CONTRIBUTING.md).
- **Upstream remote** — set `upstream` to the canonical Slopsmith repository; `origin` is your fork.
- **Plugins are gitlinks** — each plugin in `plugins/` is typically its own git repo (submodule or clone). Branch switches on the main repo can clobber plugin directories. Use `git update-index --assume-unchanged` for plugin dirs if needed.
- **Commit style** — short imperative subject line, blank line, then body explaining *why*. Conventional-commit prefixes (`feat(scope):`, `fix(scope):`, `chore:`, `docs:`) are conventional in the log but not enforced.
## Versioning
- **`VERSION`** (repo root) — single source of truth; plain semver string. Bind-mounted into the container and copied by the Dockerfile so it's always available at `/app/VERSION`.
- **`GET /api/version`** — returns `{"version", "source_url", "license_url"}`. `source_url` is overridable via `APP_SOURCE_URL` (default `https://github.com/byrongamatos/slopsmith`); `license_url` falls back to `source_url + "/blob/main/LICENSE"` and is overridable via `APP_LICENSE_URL`. Both must be `http(s)`; non-http(s) values are rejected.
- **Auto-sync** — `.github/workflows/sync-version.yml` rewrites `VERSION` via a `repository_dispatch` (`desktop-released`) from `slopsmith-desktop`'s release job. As an explicit automation-only exception to "Never push directly to main", the sync job commits straight to `main` as `github-actions[bot]`. Humans still go through PRs.
- **`CHANGELOG.md`** — [Keep a Changelog](https://keepachangelog.com/) format. Update `[Unreleased]` on each PR; release renames it.
## Song formats
Slopsmith supports two:
- **PSARC** (Rocksmith native) — encrypted archive. Read-only. Fast metadata scan via `lib/psarc.py` (`read_psarc_entries`); full unpack via `unpack_psarc()` for playback. Audio via `vgmstream-cli` + `ffmpeg`.
- **Sloppak** (open format) — hand-editable, two interchangeable forms: `.sloppak` zip or `.sloppak/` directory. Preferred for new features. Full spec: [`docs/sloppak-spec.md`](docs/sloppak-spec.md). Key code: `lib/sloppak.py`, `lib/sloppak_convert.py`, `lib/song.py`.
## Frontend conventions
- **No frameworks** — vanilla JS, fetch API, DOM manipulation
- **Globals** — `highway`, `audio`, `playSong()`, `showScreen()`, `createHighway()`, `window.slopsmith`
- **Storage** — `localStorage` for all user preferences, prefixed with plugin id
- **Styling** — Tailwind utility classes; dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`)
- **Naming** — camelCase for JS, kebab-case for CSS, snake_case for plugin IDs
- **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you must hide it.
## Backend conventions
- **Framework** — FastAPI + uvicorn (boot via `main.py`)
- **Imports** — flat imports from `lib/` (no `__init__.py`): `from song import Song`
- **Database** — SQLite via `MetadataDB` class with `threading.Lock`
- **WebSocket** — JSON frames, try/except `WebSocketDisconnect`. Protocol: [`docs/websocket-protocol.md`](docs/websocket-protocol.md)
- **Error handling** — graceful fallbacks (audio conversion errors don't crash the song; missing art returns a placeholder)
- **Type hints** — used sparingly (`Path | None`, `dict`, `list`)
- **Docstrings** — minimal; code is self-documenting
## Plugin authoring — see [`docs/PLUGIN_AUTHORING.md`](docs/PLUGIN_AUTHORING.md)
Plugins are the primary extension point. Each lives in `plugins//` with a `plugin.json` manifest. Curated plugins must be AGPL-3.0 or AGPL-compatible — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the allowlist. Manifest is validated in CI against [`schema/plugin.schema.json`](schema/plugin.schema.json), including `capability-pipelines.v1` metadata for native capability declarations.
Topic | Doc
--- | ---
Manifest reference (`plugin.json` fields) | [`docs/plugin-manifest.md`](docs/plugin-manifest.md)
Capability declarations (`standards`, `capabilities`, `ui`) | [`docs/plugin-manifest.md#capabilities`](docs/plugin-manifest.md#capabilities)
Visualization (setRenderer / overlay / note-state) | [`docs/plugin-visualization-contracts.md`](docs/plugin-visualization-contracts.md)
Plugin styles (`styles: "assets/plugin.css"`) | [`docs/plugin-styles.md`](docs/plugin-styles.md)
Audio mixer fader registration | [`docs/plugin-audio-mixer.md`](docs/plugin-audio-mixer.md)
Backend `context["log"]` logging | [`docs/plugin-logging.md`](docs/plugin-logging.md)
Diagnostics opt-in (export bundle) | [`docs/plugin-diagnostics.md`](docs/plugin-diagnostics.md)
Keyboard shortcuts (`registerShortcut`) | [`docs/plugin-keyboard-shortcuts.md`](docs/plugin-keyboard-shortcuts.md)
Multi-file backends (`load_sibling`) | [`docs/plugin-sibling-imports.md`](docs/plugin-sibling-imports.md)
WebSocket highway protocol | [`docs/websocket-protocol.md`](docs/websocket-protocol.md)
Testing plugins (pytest + Playwright) | [`docs/testing-plugins.md`](docs/testing-plugins.md)
Diagnostics bundle layout | [`docs/diagnostics-bundle-spec.md`](docs/diagnostics-bundle-spec.md)
Sloppak format spec | [`docs/sloppak-spec.md`](docs/sloppak-spec.md)
Tuning the note_detect plugin | [`docs/note-detect-tuning.md`](docs/note-detect-tuning.md)
## First-hour pitfalls (read these before your first PR)
1. **`load_sibling` for cross-file backend plugins.** Bare `from extractor import X` in `routes.py` collides across plugins because Python caches by module name in `sys.modules`. Use `context["load_sibling"]("extractor")` — gets a per-plugin namespaced module. Full explanation: [`docs/plugin-sibling-imports.md`](docs/plugin-sibling-imports.md).
2. **`playSong` wrapper race condition.** Plugins commonly wrap `window.playSong`. Wrappers chain outermost-first (last-loaded runs first). If an inner wrapper does `await import(CDN)`, it yields to the event loop and WebSocket messages (`song_info`, `ready`) can arrive before outer wrappers finish setup. Use `getSongInfo()` as a fallback, not `_onReady` alone.
3. **Highway flex layout.** `#highway` has `flex:1` in the player. Hiding it with `display:none` removes the flex child and `#player-controls` floats to the top. If you must hide the highway, add `margin-top: auto` to the controls div.
4. **Plugin gitlinks bite on branch switches.** Plugins are separate git repos cloned into `plugins/`. `git checkout` / `git clean` on the main repo can delete or clobber them. Use `git update-index --assume-unchanged plugins/` if needed.
5. **DCO sign-off is mandatory.** Every commit needs `Signed-off-by:`. Add via `git commit -s` (or `git commit --amend -s` if you forgot). PRs without DCO won't merge.
## Verification (run before claiming done)
```bash
pytest -q # backend tests
npm run test:js # JS plugin-API contract tests
npm test # Playwright browser tests (slow; defers to CI)
```
If you touched `plugins/*/plugin.json` or `schema/`:
```bash
python -c "import json,glob,jsonschema; s=json.load(open('schema/plugin.schema.json')); [jsonschema.validate(json.load(open(p)), s) for p in sorted(glob.glob('plugins/*/plugin.json'))]"
```
## House rules
- **AGPL-3.0-only.** Inbound contributions are inbound under the same terms. Don't paste from incompatible sources.
- **DCO sign-off mandatory** on every commit (`git commit -s`).
- **No frontend frameworks.** Vanilla JS, fetch API, Tailwind classes. Don't add React/Vue/Svelte.
- **Backend logging.** Plugin `routes.py` must use `context["log"]`, never `print()`. See [`docs/plugin-logging.md`](docs/plugin-logging.md).
- **Plugin Python imports.** Multi-file backends use `context["load_sibling"]("")`, not bare `from import`. See [`docs/plugin-sibling-imports.md`](docs/plugin-sibling-imports.md).
- **Capability metadata.** New plugin integrations should declare `standards: ["capability-pipelines.v1"]` plus redaction-safe `capabilities`/`ui` metadata rather than relying only on private globals.
- **Spec-kit owns `.specify/` and `specs/`.** Don't modify those without explicit instruction; the `/speckit-*` skills own that surface.
## Tool-specific surfaces (optional reading)
- [`CLAUDE.md`](CLAUDE.md) — points back here. Exists for Claude Code's auto-loading convention. Claude-specific *automation* lives in [`.claude/`](.claude/) (skills, subagents, rules, settings).
- [`.github/copilot-instructions.md`](.github/copilot-instructions.md) — points back here. Exists for GitHub Copilot's native instructions format.
- No `.cursorrules` — Cursor reads `AGENTS.md` natively. Cursor-specific rules, if ever needed, would go under `.cursor/rules/`.