From 7d0021d04a1686d0fcce743f65d4f9984bae6c17 Mon Sep 17 00:00:00 2001 From: barlind Date: Wed, 3 Jun 2026 10:32:16 +0200 Subject: [PATCH] docs: make plugin guidance capability-first Signed-off-by: barlind --- .claude/agents/slopsmith-reviewer.md | 9 +- .claude/rules/plugin-author.md | 7 +- .claude/skills/plugin-scaffold/SKILL.md | 8 +- .github/copilot-instructions.md | 4 +- AGENTS.md | 10 +- CLAUDE.md | 2 +- docs/PLUGIN_AUTHORING.md | 41 ++--- docs/capability-domains.md | 124 +++++-------- docs/capability-recipes.md | 220 +----------------------- docs/plugin-manifest.md | 35 ++-- schema/plugin.schema.json | 8 +- tests/test_plugin_schema.py | 6 +- 12 files changed, 97 insertions(+), 377 deletions(-) diff --git a/.claude/agents/slopsmith-reviewer.md b/.claude/agents/slopsmith-reviewer.md index 57c00ad..a810cec 100644 --- a/.claude/agents/slopsmith-reviewer.md +++ b/.claude/agents/slopsmith-reviewer.md @@ -1,6 +1,6 @@ --- name: slopsmith-reviewer -description: Plugin-aware code reviewer for Slopsmith. USE WHEN reviewing plugin changes, auditing a plugin against the manifest contract, checking that a plugin uses load_sibling / context["log"] / scoped shortcuts correctly, or verifying that a `plugin.json` matches the schema and the directory it lives in. Returns a structured pass/fail report with specific file:line citations. +description: Plugin-aware code reviewer for Slopsmith. USE WHEN reviewing plugin changes, auditing a plugin against the manifest contract, checking that a plugin uses capability metadata / load_sibling / context["log"] correctly, or verifying that a `plugin.json` matches the schema and the directory it lives in. Returns a structured pass/fail report with specific file:line citations. tools: Read, Grep, Glob, Bash model: sonnet --- @@ -39,10 +39,9 @@ Run each item; structure the output as `PASS` / `FAIL` / `N/A` with file:line ci 6. **Backend logging.** Grep `plugins//*.py` for `print(`, `traceback.print_exc(`, `logging.getLogger(`. Suggest `context["log"]` replacements. 7. **Sibling imports.** If `routes.py` exists and grep finds bare `from import` for any sibling Python file in the plugin dir, flag and recommend `context["load_sibling"]`. 8. **Frontend IIFE.** If `script` exists, check the top of the file isn't running top-level statements that leak to global scope. Wrapping in `(function () { 'use strict'; ... })();` is the convention. -9. **`playSong` wrapper discipline.** If the script reassigns `window.playSong`, confirm it calls the original and `await`s it. -10. **Shortcut scope discipline.** If the script calls `window.registerShortcut`, confirm `scope` is set (not relying on the `'global'` default) and that an `unregisterShortcut` / `panel.clearShortcuts()` cleanup path exists when the plugin can be torn down. -11. **`localStorage` prefix.** Grep for `localStorage.` usage; keys must start with ``. -12. **`settings.server_files` paths are safe.** Each entry must be a relpath — no leading `/`, no `..`, no backslashes. The schema enforces this but call it out. +9. **Capability-first frontend integration.** If the script wraps host globals or polls another plugin's globals, flag it as a capability gap unless the PR explicitly documents why no active domain can model the integration yet. +10. **`localStorage` prefix.** Grep for `localStorage.` usage; keys must start with ``. +11. **`settings.server_files` paths are safe.** Each entry must be a relpath — no leading `/`, no `..`, no backslashes. The schema enforces this but call it out. ## Output format diff --git a/.claude/rules/plugin-author.md b/.claude/rules/plugin-author.md index 573ddaf..8ac660b 100644 --- a/.claude/rules/plugin-author.md +++ b/.claude/rules/plugin-author.md @@ -12,7 +12,7 @@ These rules apply only when editing files under `plugins/**`. They encode the co ## Manifest - **`plugin.json` is required** and must validate against [`schema/plugin.schema.json`](../../schema/plugin.schema.json). Required fields: `id`, `name`. The `id` must match the parent directory name (the loader keys discovery by directory; drift breaks plugin lookup). -- **Capability-aware plugins declare intent** with `standards: ["capability-pipelines.v1"]` and redaction-safe `capabilities` / `ui` metadata. Legacy fields still work, but don't strip or reject native metadata when editing manifests. +- **Capability-aware plugins declare intent** with `standards: ["capability-pipelines.v1"]` and redaction-safe `capabilities` / `ui` metadata. Treat these declarations as the plugin's primary contract with Slopsmith. - **License must come from the curated allowlist** if the plugin is intended for the curated list. See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) "Plugin licensing". - **`type: "visualization"`** requires a `script` field exporting `window.slopsmithViz_`. See [`docs/plugin-visualization-contracts.md`](../../docs/plugin-visualization-contracts.md). @@ -25,10 +25,7 @@ These rules apply only when editing files under `plugins/**`. They encode the co ## Frontend (`screen.js`) - **Wrap in an IIFE** — `(function () { 'use strict'; ... })();`. Frontend scripts share global scope; leaking variables collides with other plugins. -- **Hook `window.playSong` carefully** — always call the original, always `await` it. Wrappers run outermost-first; awaiting yields to the event loop and WebSocket messages can arrive before the outer wrapper finishes setup. Use `highway.getSongInfo()` as a fallback rather than relying solely on `_onReady`. -- **Hook `window.showScreen`** — clean up your plugin's state when the user leaves the player screen. -- **Use `window.slopsmith.emit` / `on`** for cross-plugin communication. Don't poll other plugins' globals. -- **Register shortcuts with `window.registerShortcut({ key, scope, handler })`** and clean up with `window.unregisterShortcut(key, scope)` — pass the same scope you registered with (default `'global'` won't match `'player'` / `'plugin-*'`). For panel-scoped registries, prefer `panel.clearShortcuts()`. See [`docs/plugin-keyboard-shortcuts.md`](../../docs/plugin-keyboard-shortcuts.md). +- **Prefer capability commands, events, and provider APIs** for app coordination. Don't add new private global wrappers or cross-plugin polling; if the needed domain is missing, call that out as a capability gap. ## State and config diff --git a/.claude/skills/plugin-scaffold/SKILL.md b/.claude/skills/plugin-scaffold/SKILL.md index 1445e7a..38b39b1 100644 --- a/.claude/skills/plugin-scaffold/SKILL.md +++ b/.claude/skills/plugin-scaffold/SKILL.md @@ -1,6 +1,6 @@ --- name: plugin-scaffold -description: Scaffold a new Slopsmith plugin skeleton. USE WHEN the user asks to create a new plugin, scaffold a plugin, bootstrap a plugin, new visualization plugin, new overlay plugin, new settings-only plugin, plugin starter, plugin skeleton. Args needed - plugin slug (snake_case) and type (visualization / overlay / settings-only / routes-only). Generates plugins// with plugin.json, screen.js, and optional routes.py / settings.html / Playwright test stub matching the requested type. +description: Scaffold a new capability-aware Slopsmith plugin skeleton. USE WHEN the user asks to create a new plugin, scaffold a plugin, bootstrap a plugin, new visualization plugin, new overlay plugin, new settings-only plugin, plugin starter, plugin skeleton. Args needed - plugin slug (snake_case) and type (visualization / overlay / settings-only / routes-only). Generates plugins// with plugin.json, capability-pipelines metadata, screen.js, and optional routes.py / settings.html / Playwright test stub matching the requested type. --- # plugin-scaffold @@ -34,15 +34,15 @@ If the plugin slug or type is missing, ask once. **`type=visualization`** — adds: - `"type": "visualization"` and `"script": "screen.js"` to manifest -- `"capabilities": { "visualization": { "roles": ["provider"], "operations": ["renderer.create", "renderer.destroy", "renderer.inspect"], "mode": "active", "compatibility": "shim-allowed", "ownership": "multi-provider", "safety": "safe", "version": 1 } }` +- `"capabilities": { "visualization": { "roles": ["provider"], "operations": ["renderer.create", "renderer.destroy", "renderer.inspect"], "mode": "active", "compatibility": "none", "ownership": "multi-provider", "safety": "safe", "version": 1 } }` - `screen.js` exporting `window.slopsmithViz_ = function () { return { contextType: '2d', init(canvas, bundle) { this.ctx = canvas.getContext('2d'); }, draw(bundle) { /* TODO */ }, destroy() {} }; };` plus a static `matchesArrangement` example commented out - `tests/browser/.spec.ts` — Playwright stub that loads the app and asserts the plugin's factory is registered **`type=overlay`** — adds: - `"script": "screen.js"` to manifest (no `type` declared — overlays don't use the picker) -- `"capabilities": { "ui.player-overlays": { "roles": ["provider"], "mode": "active", "compatibility": "shim-allowed", "ownership": "multi-provider", "safety": "safe", "version": 1 } }` +- `"capabilities": { "ui.player-overlays": { "roles": ["provider"], "mode": "active", "compatibility": "none", "ownership": "multi-provider", "safety": "safe", "version": 1 } }` - a matching `"ui"` contribution with a stable overlay id and redaction-safe label -- `screen.js` scaffolding a navbar toggle, an own-canvas + own-rAF loop reading `highway.getNotes()` / `getChords()` / `getTime()`, and respecting `highway.isDefaultRenderer()` if using `highway.project` / `fretX` +- `screen.js` scaffolding an own-canvas + own-rAF loop for the declared overlay contribution, and respecting renderer ownership if it uses highway geometry helpers - `tests/browser/.spec.ts` — toggle on / off test **`type=settings-only`** — adds: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6701d88..106acc5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,9 +14,9 @@ This file customizes GitHub Copilot Chat and Copilot inline suggestions for the - **No frontend frameworks.** Vanilla JS, Canvas, Tailwind classes. Do not suggest React/Vue/Svelte additions. - **Plugin backend logging.** Suggest `context["log"]`, never `print()`. - **Plugin Python imports.** For cross-file backend plugins, suggest `context["load_sibling"]("module_name")` instead of bare `from module_name import X`. -- **Capability metadata.** For new plugin integrations, suggest `standards: ["capability-pipelines.v1"]` and redaction-safe `capabilities` / `ui` metadata instead of legacy globals alone. +- **Capability metadata.** For new plugin integrations, suggest `standards: ["capability-pipelines.v1"]` and redaction-safe `capabilities` / `ui` metadata as the primary contract. - **DCO/license headers.** When creating a new file in the main repo, no license header is needed (the LICENSE file at root governs). Plugin authors should add an SPDX-License-Identifier comment to their plugin's source files; the `license` field in `plugin.json` must match the allowlist in [`CONTRIBUTING.md`](../CONTRIBUTING.md). ## Validation -When suggesting changes to a `plugin.json`, validate against [`schema/plugin.schema.json`](../schema/plugin.schema.json). The schema accepts current legacy fields and native `capability-pipelines.v1` metadata. +When suggesting changes to a `plugin.json`, validate against [`schema/plugin.schema.json`](../schema/plugin.schema.json), including native `capability-pipelines.v1` metadata. diff --git a/AGENTS.md b/AGENTS.md index aab4eaf..dcaf412 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -101,7 +101,7 @@ Slopsmith supports two: ## Frontend conventions - **No frameworks** — vanilla JS, fetch API, DOM manipulation -- **Globals** — `highway`, `audio`, `playSong()`, `showScreen()`, `createHighway()`, `window.slopsmith` +- **Plugin integration** — prefer documented capability domains, provider APIs, and redaction-safe UI contributions over private globals - **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 @@ -125,12 +125,10 @@ 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) +Visualization contracts | [`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) @@ -142,7 +140,7 @@ Tuning the note_detect plugin | [`docs/note-detect-tuning.md`](docs/note-detect- 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. +2. **Capability declarations are the integration map.** New plugin behavior should be visible in `standards`, `capabilities`, and `ui` metadata before runtime code hydrates. If the domain you need is missing, document it as a capability gap instead of adding another private global contract. 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. @@ -171,7 +169,7 @@ python -c "import json,glob,jsonschema; s=json.load(open('schema/plugin.schema.j - **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. +- **Capability metadata.** New plugin integrations should declare `standards: ["capability-pipelines.v1"]` plus redaction-safe `capabilities`/`ui` metadata as the primary integration contract. - **Spec-kit owns `.specify/` and `specs/`.** Don't modify those without explicit instruction; the `/speckit-*` skills own that surface. ## Tool-specific surfaces (optional reading) diff --git a/CLAUDE.md b/CLAUDE.md index ae2faaa..1d1e98a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ Claude Code memory file. This repo's canonical project orientation lives in [`AG The rest of this file is content that *only* makes sense for Claude Code (other AI tools have their own incompatible automation mechanisms). Skills, subagent, rule, and settings live under [`.claude/`](.claude/): -- [`.claude/skills/plugin-scaffold/`](.claude/skills/plugin-scaffold/SKILL.md) - generates a new plugin skeleton (visualization / overlay / settings-only / routes-only). +- [`.claude/skills/plugin-scaffold/`](.claude/skills/plugin-scaffold/SKILL.md) - generates a capability-aware plugin skeleton. - [`.claude/skills/plugin-validate/`](.claude/skills/plugin-validate/SKILL.md) - validates `plugin.json` against `schema/plugin.schema.json` locally before push. - [`.claude/skills/speckit-*/`](.claude/skills/) - spec-kit skills (auto-generated from `.specify/`; don't edit manually). - [`.claude/rules/plugin-author.md`](.claude/rules/plugin-author.md) - glob-scoped to `plugins/**`; encodes the contracts from `docs/PLUGIN_AUTHORING.md` so suggestions don't drift from them. diff --git a/docs/PLUGIN_AUTHORING.md b/docs/PLUGIN_AUTHORING.md index 250b498..83e28b4 100644 --- a/docs/PLUGIN_AUTHORING.md +++ b/docs/PLUGIN_AUTHORING.md @@ -1,6 +1,6 @@ # Plugin Authoring Guide -Slopsmith's plugin system is the primary extension point. Each plugin lives in `plugins//` with a `plugin.json` manifest and can provide any combination of frontend (HTML/JS), backend (Python routes), settings UI, diagnostics, and visualization renderers. +Slopsmith's plugin system is the primary extension point. Each plugin lives in `plugins//` with a `plugin.json` manifest that declares the capability domains, UI contributions, settings metadata, diagnostics, and runtime files the plugin participates in. This guide is the entry point. Each topic below has a dedicated doc — read what's relevant to what you're building. @@ -9,14 +9,14 @@ This guide is the entry point. Each topic below has a dedicated doc — read wha ```text plugins/my_plugin/ ├── plugin.json Manifest (required) — see docs/plugin-manifest.md -├── screen.html Optional — markup mounted at #plugin-my_plugin -├── screen.js Optional — runs in global scope on page load -├── routes.py Optional — exports setup(app, context) -├── settings.html Optional — settings-panel HTML +├── screen.html Optional — UI declared through `ui` contributions +├── screen.js Optional — hydrates declared frontend capabilities +├── routes.py Optional — backend provider/requester implementation +├── settings.html Optional — settings UI declared through `ui.settings` └── requirements.txt Optional — pip deps auto-installed on load ``` -The minimum viable plugin is a `plugin.json` with just `id` and `name`. Everything else is opt-in. +Start every new plugin by describing its Slopsmith-facing behavior in the manifest. A plugin with no behavior beyond metadata can be this small: ```json { @@ -26,7 +26,7 @@ The minimum viable plugin is a `plugin.json` with just `id` and `name`. Everythi } ``` -Capability-aware plugins should also declare the `capability-pipelines.v1` standard and the domains they participate in. Legacy fields such as `nav`, `screen`, `settings`, `type: "visualization"`, shortcuts, overlays, and mixer faders still work, but native metadata lets diagnostics, the Capability Inspector, and migration tooling explain plugin behavior without scraping private globals. +Any plugin that participates in app behavior should also declare `standards: ["capability-pipelines.v1"]`, native `capabilities`, and redaction-safe `ui` metadata. Capability declarations are the source of truth for diagnostics, the Capability Inspector, and migration tooling. ## Topics @@ -34,12 +34,12 @@ Capability-aware plugins should also declare the `capability-pipelines.v1` stand |---|---|---| | **Manifest reference** | [plugin-manifest.md](plugin-manifest.md) | Field-by-field reference for `plugin.json`. Read first. | | **Capability declarations** | [plugin-manifest.md#capabilities](plugin-manifest.md#capabilities) | Declaring provider/requester/observer intent with `capability-pipelines.v1`. | +| **Capability domains** | [capability-domains.md](capability-domains.md) | Active domains, planned domains, and promotion rules. | +| **Capability recipes** | [capability-recipes.md](capability-recipes.md) | Copyable manifest patterns for provider/requester/observer plugins. | | **Visualization contracts** | [plugin-visualization-contracts.md](plugin-visualization-contracts.md) | Building a highway renderer (setRenderer), an overlay layer, or a note-state provider. | | **Plugin styles** | [plugin-styles.md](plugin-styles.md) | Shipping a plugin-owned prebuilt stylesheet via `styles: "assets/plugin.css"`. | -| **Audio mixer faders** | [plugin-audio-mixer.md](plugin-audio-mixer.md) | Plugin produces audio outside the song `