Files
feedBack/AGENTS.md
T
Miguel_LZPFandBret Mogilefsky b45751164f fix: address PR #332 review feedback
Addresses 12 of 13 review comments from Copilot and CodeRabbit on
PR #332. One comment (no-manifests in validate-plugins.yml) is
declined and answered inline; the rest are applied here.

Substantive fixes:

- .github/workflows/validate-plugins.yml — add --noconftest to the
  schema-tests step. tests/conftest.py imports structlog at module
  level, but the CI job only installs requirements-test.txt
  (pytest/httpx/jsonschema), so pytest collection would fail at
  conftest import. The schema tests don't use shared fixtures, so
  skipping conftest is safe and avoids dragging the full runtime
  requirements into a 2 KB validation job. (Copilot)

- schema/plugin.schema.json — tighten the server_files regex on both
  settings.server_files and diagnostics.server_files to match the
  runtime _validate_relpath rules in plugins/__init__.py. The
  previous regex only blocked absolute paths, drive letters,
  backslashes, and "..". The runtime also rejects "//", "./",
  "/./", and leading-dotfile segments. Schema-valid manifests are
  now also load-time-valid. Verified the regex against 12 cases:
  the 3 in-tree manifests still validate. (Copilot)

- .claude/skills/plugin-validate/SKILL.md — add a per-iteration
  plugin_ok flag so we no longer print "OK <path>" after an earlier
  FAIL in the same manifest. Schema-pass + id-mismatch previously
  produced both FAIL and OK lines for one plugin. (CodeRabbit)

- docs/websocket-protocol.md — clarify song_info.tuning array length
  is source-dependent (typically 6 guitar, 4 bass, but extended-range
  GP imports can be 7/8/5/6). Recommend highway.getStringCount() for
  the authoritative count. Line 30 already said this; the table row
  on line 12 was the stale half. (CodeRabbit)

Trivial fixes:

- .claude/rules/plugin-author.md — "wants included" -> "wants to
  include" in the settings.server_files rule. (CodeRabbit)

- Markdown MD040 — add `text` language tags to 7 bare-fence code
  blocks across AGENTS.md, docs/PLUGIN_AUTHORING.md,
  docs/testing-plugins.md, docs/plugin-logging.md, .claude/README.md,
  .claude/agents/slopsmith-reviewer.md, and
  .claude/skills/plugin-validate/SKILL.md (two fences). (CodeRabbit)

Declined:

- .github/workflows/validate-plugins.yml no-manifests -> exit 0
  (CodeRabbit suggested exit 1). Plugins in this repo are in-tree,
  not submodules (no .gitmodules, git submodule status empty), and
  the workflow has a path filter on plugins/**/plugin.json so it
  only runs when a manifest actually changes. Exit 0 is correct.
  Answered inline on the PR.

Verification:
  pytest tests/test_plugin_schema.py -v --noconftest    # 8 passed
  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'))]"
  # ok — all 3 in-tree manifests validate against tightened schema
Signed-off-by: Miguel_LZPF <mgcdreamer@gmail.com>
2026-06-18 00:38:51 -07:00

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 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/ 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)
  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
  <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_LEVELDEBUG | INFO | WARNING | ERROR (default INFO)
  • LOG_FORMATjson | 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.<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 -s appends Signed-off-by:. Forgot? git commit --amend -s. See 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.mdKeep a Changelog 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. Key code: lib/sloppak.py, lib/sloppak_convert.py, lib/song.py.

Frontend conventions

  • No frameworks — vanilla JS, fetch API, DOM manipulation
  • Globalshighway, audio, playSong(), showScreen(), createHighway(), window.slopsmith
  • StoragelocalStorage 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
  • 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. 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.

Topic Doc
Manifest reference (plugin.json fields) docs/plugin-manifest.md
Visualization (setRenderer / overlay / note-state) docs/plugin-visualization-contracts.md
Audio mixer fader registration docs/plugin-audio-mixer.md
Backend context["log"] logging docs/plugin-logging.md
Diagnostics opt-in (export bundle) docs/plugin-diagnostics.md
Keyboard shortcuts (registerShortcut) docs/plugin-keyboard-shortcuts.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)

  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.

  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/<id> 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)

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.py must use context["log"], never print(). See docs/plugin-logging.md.
  • Plugin Python imports. Multi-file backends use context["load_sibling"]("<module>"), not bare from <module> import. See docs/plugin-sibling-imports.md.
  • 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 — 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 reads AGENTS.md natively. Cursor-specific rules, if ever needed, would go under .cursor/rules/.