13 KiB
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 interactive music notation, built around its own open .sloppak chart format. Charts come from importing Guitar Pro (GP5/GP8) or MusicXML, or from authoring in the built-in editor. 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/ and .github/copilot-instructions.md; both point back here. For plugin work, start at docs/PLUGIN_AUTHORING.md.
Architecture quick reference
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 <head>)
index.html Single-page app shell
lib/
song.py Core data models (Note, Chord, Arrangement, Song)
sloppak.py Sloppak format support
sloppak_convert.py Import conversion + Demucs stem split
loosefolder.py Loose-folder XML chart support
audio.py WEM/OGG/MP3 audio handling
retune.py Pitch-shifting logic
tunings.py Tuning name/offset utilities
gp2rs.py Guitar Pro to arrangement XML conversion
gp2midi.py Guitar Pro to MIDI
plugins/
__init__.py Plugin discovery, loading, requirements install, load_sibling
<plugin>/ 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.
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:
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(defaultINFO)LOG_FORMAT—json | text(defaulttext— 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.<id> — never use print(). See docs/plugin-logging.md.
Testing
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 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 -sappendsSigned-off-by:. Forgot?git commit --amend -s. SeeCONTRIBUTING.md. - Upstream remote — set
upstreamto the canonical Slopsmith repository;originis 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. Usegit update-index --assume-unchangedfor 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_urlis overridable viaAPP_SOURCE_URL(defaulthttps://github.com/byrongamatos/slopsmith);license_urlfalls back tosource_url + "/blob/main/LICENSE"and is overridable viaAPP_LICENSE_URL. Both must behttp(s); non-http(s) values are rejected.- Auto-sync —
.github/workflows/sync-version.ymlrewritesVERSIONvia arepository_dispatch(desktop-released) fromslopsmith-desktop's release job. As an explicit automation-only exception to "Never push directly to main", the sync job commits straight tomainasgithub-actions[bot]. Humans still go through PRs. CHANGELOG.md— Keep a Changelog format. Update[Unreleased]on each PR; release renames it.
Song formats
Slopsmith supports two:
- Legacy archive (encrypted, read-only) — fast metadata scan via
lib/sloppak.py; full unpack for playback. Audio viavgmstream-cli+ffmpeg. - Sloppak (open format) — hand-editable, two interchangeable forms:
.sloppakzip or.sloppak/directory. Preferred for new features. Full spec: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
- Plugin integration — prefer documented capability domains, provider APIs, and redaction-safe UI contributions over private globals
- Storage —
localStoragefor 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 —
#playerisdisplay:flex; flex-direction:column; position:fixed; inset:0.#highwayisflex:1. Hiding the highway collapses the layout — usemargin-top: autoon 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
MetadataDBclass withthreading.Lock - WebSocket — JSON frames, try/except
WebSocketDisconnect. Protocol: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
Plugins are the primary extension point. Each lives in plugins/<name>/ with a plugin.json manifest that declares its capability-pipelines.v1 participation. Curated plugins must be AGPL-3.0 or AGPL-compatible — see CONTRIBUTING.md for the allowlist. Manifest is validated in CI against schema/plugin.schema.json, including capability metadata.
| Topic | Doc |
|---|---|
Manifest reference (plugin.json fields) |
docs/plugin-manifest.md |
Capability declarations (standards, capabilities, ui) |
docs/plugin-manifest.md#capabilities |
| Visualization contracts | docs/plugin-visualization-contracts.md |
Plugin styles (styles: "assets/plugin.css") |
docs/plugin-styles.md |
Backend context["log"] logging |
docs/plugin-logging.md |
| Diagnostics opt-in (export bundle) | docs/plugin-diagnostics.md |
Multi-file backends (load_sibling) |
docs/plugin-sibling-imports.md |
| WebSocket highway protocol | docs/websocket-protocol.md |
| Testing plugins (pytest + Playwright) | docs/testing-plugins.md |
| Diagnostics bundle layout | docs/diagnostics-bundle-spec.md |
| Sloppak format spec | docs/sloppak-spec.md |
| Tuning the note_detect plugin | docs/note-detect-tuning.md |
First-hour pitfalls (read these before your first PR)
-
load_siblingfor cross-file backend plugins. Barefrom extractor import Xinroutes.pycollides across plugins because Python caches by module name insys.modules. Usecontext["load_sibling"]("extractor")— gets a per-plugin namespaced module. Full explanation:docs/plugin-sibling-imports.md. -
Capability declarations are the integration map. Plugin behavior should be visible in
standards,capabilities, anduimetadata before runtime code hydrates. If the domain you need is missing, document it as a capability gap instead of adding another private global contract. -
Highway flex layout.
#highwayhasflex:1in the player. Hiding it withdisplay:noneremoves the flex child and#player-controlsfloats to the top. If you must hide the highway, addmargin-top: autoto the controls div. -
Plugin gitlinks bite on branch switches. Plugins are separate git repos cloned into
plugins/.git checkout/git cleanon the main repo can delete or clobber them. Usegit update-index --assume-unchanged plugins/<id>if needed. -
DCO sign-off is mandatory. Every commit needs
Signed-off-by:. Add viagit commit -s(orgit commit --amend -sif you forgot). PRs without DCO won't merge.
Verification (run before claiming done)
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/:
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.pymust usecontext["log"], neverprint(). Seedocs/plugin-logging.md. - Plugin Python imports. Multi-file backends use
context["load_sibling"]("<module>"), not barefrom <module> import. Seedocs/plugin-sibling-imports.md. - Capability metadata. Plugin integrations declare
standards: ["capability-pipelines.v1"]plus redaction-safecapabilities/uimetadata as the primary integration contract. - Spec-kit owns
.specify/andspecs/. Don't modify those without explicit instruction; the/speckit-*skills own that surface.
Tool-specific surfaces (optional reading)
CLAUDE.md— points back here. Exists for Claude Code's auto-loading convention. Claude-specific automation lives in.claude/(skills, subagents, rules, settings)..github/copilot-instructions.md— points back here. Exists for GitHub Copilot's native instructions format.- No
.cursorrules— Cursor readsAGENTS.mdnatively. Cursor-specific rules, if ever needed, would go under.cursor/rules/.