diff --git a/.claude/agents/slopsmith-reviewer.md b/.claude/agents/slopsmith-reviewer.md index 10f0077..57c00ad 100644 --- a/.claude/agents/slopsmith-reviewer.md +++ b/.claude/agents/slopsmith-reviewer.md @@ -7,7 +7,7 @@ model: sonnet # slopsmith-reviewer -Plugin-aware reviewer for Slopsmith. Use when reviewing a plugin's code or manifest. Does **not** duplicate the built-in `peer-review` skill — focus is narrow: the plugin contract surface defined in [`docs/PLUGIN_AUTHORING.md`](../../docs/PLUGIN_AUTHORING.md) and enforced by [`schema/plugin.schema.json`](../../schema/plugin.schema.json) and `tests/test_plugin_schema.py`. +Plugin-aware reviewer for Slopsmith. Use when reviewing a plugin's code or manifest. Does **not** duplicate the built-in `peer-review` skill — focus is narrow: the plugin contract surface defined in [`docs/PLUGIN_AUTHORING.md`](../../docs/PLUGIN_AUTHORING.md) and enforced by [`schema/plugin.schema.json`](../../schema/plugin.schema.json), including `capability-pipelines.v1` metadata, and `tests/test_plugin_schema.py`. ## When to invoke diff --git a/.claude/rules/plugin-author.md b/.claude/rules/plugin-author.md index 679e833..573ddaf 100644 --- a/.claude/rules/plugin-author.md +++ b/.claude/rules/plugin-author.md @@ -12,6 +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. - **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). diff --git a/.claude/settings.json b/.claude/settings.json index d814957..2dc1123 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,7 +5,7 @@ "hooks": {}, "_commented_example_hooks": { - "_comment": "Copy the block below into the empty `hooks` object above if you want plugin.json edits to be auto-validated against schema/plugin.schema.json on save. Per-user opt-in; not enabled by default because hooks run in every Claude Code session that loads this repo.", + "_comment": "Copy the block below into the empty `hooks` object above if you want plugin.json edits to be auto-validated against schema/plugin.schema.json, including capability metadata, on save. Per-user opt-in; not enabled by default because hooks run in every Claude Code session that loads this repo.", "PostToolUse": [ { "matcher": "Write|Edit", diff --git a/.claude/skills/plugin-scaffold/SKILL.md b/.claude/skills/plugin-scaffold/SKILL.md index 822fa17..1445e7a 100644 --- a/.claude/skills/plugin-scaffold/SKILL.md +++ b/.claude/skills/plugin-scaffold/SKILL.md @@ -30,25 +30,31 @@ If the plugin slug or type is missing, ask once. **Common to all types:** -- `plugins//plugin.json` — minimum schema-valid manifest. Set `id`, `name`, `version: "0.1.0"`, and `license: "AGPL-3.0-only"` by default (ask if a different license is desired). +- `plugins//plugin.json` — schema-valid manifest. Set `id`, `name`, `version: "0.1.0"`, `license: "AGPL-3.0-only"`, and `standards: ["capability-pipelines.v1", "plugin-runtime-idempotent.v1"]` by default (ask if a different license is desired). **`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 } }` - `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 } }` +- 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` - `tests/browser/.spec.ts` — toggle on / off test **`type=settings-only`** — adds: - `"settings": { "html": "settings.html" }` to manifest +- `"settings_schema"` with a schema version and an empty `packable_keys` list +- a `"ui"` settings contribution with a stable id - `settings.html` — empty form skeleton with explanatory comments - `screen.js` reading/writing `localStorage` keys prefixed with `_` **`type=routes-only`** — adds: - `"routes": "routes.py"` to manifest +- a conservative `"capabilities"` declaration only when the route participates in a known domain such as `library`, `jobs`, or `privileged-capabilities`; otherwise leave capability participation out and ask what workflow the route owns - `routes.py` with `def setup(app, context):` that registers one example route and uses `context["log"].info("plugin ready")` (never `print()`) - `tests/test__routes.py` — FastAPI TestClient stub @@ -64,7 +70,7 @@ Then point the user at [`docs/PLUGIN_AUTHORING.md`](../../../docs/PLUGIN_AUTHORI ## Don'ts -- Don't add fields to the manifest that aren't in the schema. If the user wants something custom, ask whether it should become a real field — that's a `schema/plugin.schema.json` change, not a plugin-local convention. +- Don't invent plugin-local manifest conventions. If the user wants something custom, either express it through existing `capability-pipelines.v1` metadata or ask whether it should become a real schema field. - Don't scaffold a `requirements.txt` without confirming the deps. The plugin loader installs them on first load; an accidental dep slows everyone's startup. - Don't pre-fill `localStorage` keys without a prefix. Collisions across plugins are real. - Don't generate hidden side effects at import time (`screen.js` top level or `routes.py` top level). Keep all wiring inside the IIFE / `setup()`. diff --git a/.claude/skills/plugin-validate/SKILL.md b/.claude/skills/plugin-validate/SKILL.md index 821a636..e0f90f8 100644 --- a/.claude/skills/plugin-validate/SKILL.md +++ b/.claude/skills/plugin-validate/SKILL.md @@ -1,6 +1,6 @@ --- name: plugin-validate -description: Validate a Slopsmith plugin's plugin.json against the schema and run structural checks. USE WHEN the user asks to validate a plugin, check the manifest, plugin.json errors, lint plugin, verify plugin structure, plugin license check, or audit plugin contract. Runs JSON Schema validation, file-existence checks for declared script/routes/settings.html/tour paths, and license-allowlist check. +description: Validate a Slopsmith plugin's plugin.json against the schema and run structural checks. USE WHEN the user asks to validate a plugin, check the manifest, plugin.json errors, lint plugin, verify plugin structure, plugin license check, or audit plugin contract. Runs JSON Schema validation, capability metadata checks, file-existence checks for declared script/routes/settings.html/tour paths, and license-allowlist check. --- # plugin-validate @@ -32,7 +32,7 @@ for path in sorted(glob.glob('plugins/*/plugin.json')): plugin_id = plugin_dir.name m = json.load(open(path)) plugin_ok = True # per-iteration flag so we don't print OK after a later FAIL - # 1. Schema + # 1. Schema, including capability-pipelines.v1 metadata try: jsonschema.validate(m, schema) except jsonschema.ValidationError as e: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 201af83..5af73fb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,7 +2,7 @@ Thanks for the PR! A few quick reminders before you hit submit: - DCO sign-off on every commit (git commit -s; --amend -s to fix) - Plugin work? Check docs/PLUGIN_AUTHORING.md and validate plugin.json - against schema/plugin.schema.json + against schema/plugin.schema.json, including capability metadata - Touching the highway / player UI? Add or update a Playwright test under tests/browser/ --> @@ -23,7 +23,7 @@ Thanks for the PR! A few quick reminders before you hit submit: - [ ] `npm run test:js` passes locally - [ ] `npm test` (Playwright) passes locally — *or* CI will run it - [ ] Verified in `docker compose up` (live-reload working directory) -- [ ] Plugin manifest validates against `schema/plugin.schema.json` +- [ ] Plugin manifest validates against `schema/plugin.schema.json` (legacy fields + capability metadata) - [ ] Not applicable — explain below ## Screenshots / recordings diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9a20b30..6701d88 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,8 +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. - **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). +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. diff --git a/.github/workflows/validate-plugins.yml b/.github/workflows/validate-plugins.yml index 101cac3..9540098 100644 --- a/.github/workflows/validate-plugins.yml +++ b/.github/workflows/validate-plugins.yml @@ -34,7 +34,7 @@ jobs: python -m pip install --upgrade pip pip install -r requirements-test.txt - - name: Validate every in-tree plugin.json against schema + - name: Validate every in-tree plugin.json against capability-aware schema run: | python - <<'PY' import glob, json, sys @@ -66,7 +66,7 @@ jobs: sys.exit(1) PY - - name: Run schema sanity tests + - name: Run schema and capability contract tests # --noconftest skips tests/conftest.py, which imports structlog # (not in requirements-test.txt). The schema tests don't use # shared fixtures, so this is safe and avoids dragging the full diff --git a/AGENTS.md b/AGENTS.md index c9314c0..aab4eaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,12 +119,14 @@ Slopsmith supports two: ## 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). +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) @@ -169,6 +171,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. - **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/docs/PLUGIN_AUTHORING.md b/docs/PLUGIN_AUTHORING.md index b1017ce..250b498 100644 --- a/docs/PLUGIN_AUTHORING.md +++ b/docs/PLUGIN_AUTHORING.md @@ -26,12 +26,16 @@ 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. + ## Topics | Topic | Doc | When to read | |---|---|---| | **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`. | | **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 `