.cursorrules
| 1 | # Muse — Agent Rules |
| 2 | |
| 3 | ## Identity |
| 4 | |
| 5 | Muse — domain-agnostic version control for multidimensional state. Version control is the abstraction; music is the first domain; genomics, scientific simulation, 3D spatial design, and spacetime simulation are next. |
| 6 | |
| 7 | - **Entry point:** `muse` CLI. No web server, no HTTP API, no database. |
| 8 | - **Stack:** Python 3.14, Typer, file-based content-addressed storage (`.muse/`), synchronous I/O throughout. |
| 9 | - **Plugin contract:** `MuseDomainPlugin` protocol in `muse/domain.py`. Every new domain is a new plugin — the core engine never changes. |
| 10 | - **Version control:** Muse and MuseHub exclusively. Git and GitHub are not used. |
| 11 | |
| 12 | ## No legacy. No deprecated. No exceptions. |
| 13 | |
| 14 | This is the single most repeated rule. It is a hard constraint enforced on every change. |
| 15 | |
| 16 | - **Delete on sight.** If you touch a file and find dead code, a deprecated API shape, a backward-compatibility shim, or a legacy fallback — delete it in the same commit. |
| 17 | - **No fallback paths for old shapes.** Remove every trace of the old way. |
| 18 | - **No "legacy" or "deprecated" comments.** If it's marked deprecated, delete it. |
| 19 | - **No dead constants, dead regexes, dead fields.** If it can never be reached, delete it. |
| 20 | |
| 21 | When you remove something, remove it completely: implementation, tests, docs, config. |
| 22 | |
| 23 | ## Version control — Muse only. Git and GitHub are not used. |
| 24 | |
| 25 | All code changes, branching, merging, and releases happen through Muse commands. Git and GitHub do not exist in this workflow. Never run `git`, `gh`, or reference GitHub. |
| 26 | |
| 27 | ### Starting work |
| 28 | |
| 29 | ``` |
| 30 | muse status # where am I, what's dirty |
| 31 | muse branch feat/my-thing # create branch |
| 32 | muse checkout feat/my-thing # switch to it |
| 33 | ``` |
| 34 | |
| 35 | ### While working |
| 36 | |
| 37 | ``` |
| 38 | muse status # constantly — like breathing |
| 39 | muse diff # what exactly changed, symbol-level |
| 40 | muse code add . # stage what you want in the next snapshot |
| 41 | muse commit -m "..." # typed event: Muse proposes MAJOR/MINOR/PATCH |
| 42 | ``` |
| 43 | |
| 44 | ### Before merging |
| 45 | |
| 46 | ``` |
| 47 | muse fetch origin # pull down what's changed upstream |
| 48 | muse status # shows if you're behind |
| 49 | muse merge --dry-run main # will this conflict? what's the semver impact? |
| 50 | ``` |
| 51 | |
| 52 | ### Merging |
| 53 | |
| 54 | ``` |
| 55 | muse merge main # if dry-run was clean |
| 56 | ``` |
| 57 | |
| 58 | ### Releasing |
| 59 | |
| 60 | ``` |
| 61 | # Create a local release at HEAD (--title and --body are required by convention) |
| 62 | muse release add <tag> --title "<title>" --body "<description>" |
| 63 | |
| 64 | # Optionally set channel (default is inferred from semver pre-release label) |
| 65 | muse release add <tag> --title "<title>" --body "<description>" --channel stable |
| 66 | |
| 67 | # Push to a remote |
| 68 | muse release push <tag> --remote local |
| 69 | |
| 70 | # Full delete-and-recreate cycle (e.g. after a DB migration or data fix): |
| 71 | muse release delete <tag> --remote local --yes |
| 72 | muse release add <tag> --title "<title>" --body "<description>" |
| 73 | muse release push <tag> --remote local |
| 74 | ``` |
| 75 | |
| 76 | ### The mental model |
| 77 | |
| 78 | Git tracks line changes in files. Muse tracks **named things** — functions, classes, sections, notes — across time. The file is the container; the symbol is the unit of meaning. |
| 79 | |
| 80 | - `muse diff` shows `Invoice.calculate()` was modified, not that lines 42–67 changed. |
| 81 | - `muse merge --dry-run` identifies conflicting symbol edits before a conflict marker is written. |
| 82 | - `muse status` surfaces untracked symbols and dead code the moment it is orphaned. |
| 83 | - `muse commit` is a **typed event** — Muse proposes MAJOR/MINOR/PATCH based on what changed structurally. |
| 84 | |
| 85 | ### Branch discipline |
| 86 | |
| 87 | **Never work directly on `main`.** |
| 88 | |
| 89 | Full task lifecycle: |
| 90 | 1. `muse status` — must be clean before branching. |
| 91 | 2. `muse branch feat/<description>` then `muse checkout feat/<description>` — always branch first. |
| 92 | 3. Do the work, commit on the branch. |
| 93 | 4. **Verify locally before merging — in this exact order:** |
| 94 | ``` |
| 95 | mypy muse/ # zero errors |
| 96 | python tools/typing_audit.py --dirs muse/ tests/ --max-any 0 # zero violations |
| 97 | pytest tests/ -v # all green |
| 98 | ``` |
| 99 | 5. `muse merge --dry-run main` — confirm clean. |
| 100 | 6. `muse checkout main && muse merge feat/<description>` |
| 101 | 7. `muse release add <tag> --title "<title>" --body "<description>"` then `muse release push <tag> --remote local`. |
| 102 | |
| 103 | ## MuseHub interactions |
| 104 | |
| 105 | MuseHub at `http://localhost:10003` is the remote repository server. The `user-github` MCP server can be used for **MuseHub issue tracking only** (not for code commits or releases — those go through Muse). |
| 106 | |
| 107 | | Operation | Tool | |
| 108 | |-----------|------| |
| 109 | | Browse releases | `http://localhost:10003/<owner>/<repo>/releases` | |
| 110 | | Push a release | `muse release push <tag> --remote local` | |
| 111 | | Delete a remote release | `muse release delete <tag> --remote local --yes` | |
| 112 | |
| 113 | ## Architecture (do not weaken) |
| 114 | |
| 115 | ``` |
| 116 | muse/ |
| 117 | domain.py → MuseDomainPlugin protocol + LiveState / StateSnapshot / StateDelta types |
| 118 | core/ |
| 119 | object_store.py → content-addressed blob storage (.muse/objects/) |
| 120 | snapshot.py → manifest hashing, workdir diff |
| 121 | store.py → file-based CRUD for commits, snapshots, tags (.muse/commits/ etc.) |
| 122 | merge_engine.py → three-way merge, merge-base, conflict detection |
| 123 | repo.py → require_repo() — locates .muse/ from cwd |
| 124 | errors.py → ExitCode enum |
| 125 | cli/ |
| 126 | app.py → Typer root — registers all commands |
| 127 | commands/ → one file per command (init, commit, log, status, …) |
| 128 | models.py → re-exports CommitRecord, SnapshotRecord, TagRecord |
| 129 | config.py → .muse/config.toml helpers |
| 130 | midi_parser.py → MIDI / MusicXML → NoteEvent (MIDI domain utility) |
| 131 | plugins/ |
| 132 | music/ |
| 133 | plugin.py → MidiPlugin — reference MuseDomainPlugin implementation |
| 134 | tools/ |
| 135 | typing_audit.py → typing violation scanner (ratchet: --max-any 0) |
| 136 | tests/ → pytest suite |
| 137 | ``` |
| 138 | |
| 139 | **Layer rules:** |
| 140 | - Commands are thin. No business logic in `cli/commands/` — delegate to `muse.core.*`. |
| 141 | - `muse.core.*` is domain-agnostic. It never imports from `muse.plugins.*`. |
| 142 | - `muse.plugins.music.plugin` is the only file that may import domain-specific logic. |
| 143 | - New domains are new directories under `muse/plugins/`. The core engine is never modified. |
| 144 | |
| 145 | ## Frontend separation of concerns — absolute rule (applies to MuseHub contributions) |
| 146 | |
| 147 | When working on any MuseHub template or static asset, every concern belongs in exactly one layer: |
| 148 | |
| 149 | | Layer | Where | What | |
| 150 | |-------|-------|------| |
| 151 | | **Structure** | `templates/musehub/pages/*.html`, `fragments/*.html` | Jinja2 markup only — no `<style>`, no `<script>` | |
| 152 | | **Behaviour** | `templates/musehub/static/js/*.js` | All JS / Alpine / HTMX | |
| 153 | | **Style** | `templates/musehub/static/scss/_*.scss` | All CSS — compiled to `app.css` via `app.scss` | |
| 154 | |
| 155 | **Never put `<style>` blocks or inline `style="..."` (beyond truly dynamic values) in a template.** If you find them, extract them to the matching SCSS partial in the same commit. |
| 156 | |
| 157 | ## Code standards |
| 158 | |
| 159 | - Type hints everywhere — 100% coverage, no untyped functions or parameters. |
| 160 | - `list[X]` / `dict[K, V]` style — never `List`, `Dict`, `Optional`. |
| 161 | - `X | None` — never `Optional[X]`. |
| 162 | - Synchronous I/O throughout — no `async`, no `await`, no `asyncio`. |
| 163 | - `logging.getLogger(__name__)` — never `print()`. |
| 164 | - Sparse logs. Docstrings on public modules, classes, and functions. "Why" over "what." |
| 165 | |
| 166 | ## Typing — zero-tolerance rules |
| 167 | |
| 168 | Strong types are the contract. There are no exceptions. |
| 169 | |
| 170 | - **No `Any`. Ever.** Use `TypedDict`, a `Protocol`, or a specific union. There is always a correct type. |
| 171 | - **No `object`. Ever.** It is `Any` with a different name. Express the actual shape. |
| 172 | - **No bare collections.** `list`, `dict`, `set`, `tuple` without type parameters are banned. Always `list[str]`, `dict[str, int]`, etc. |
| 173 | - **No `# type: ignore`.** Fix the root cause. If a third-party library forces the issue, write a typed adapter. |
| 174 | - **No `cast()`.** If you need a cast, the callee returns the wrong type — fix the callee. |
| 175 | - **No `Optional[X]`.** Write `X | None`. |
| 176 | - **No legacy typing imports.** `List`, `Dict`, `Set`, `Tuple` from `typing` are banned — use lowercase builtins. |
| 177 | |
| 178 | ### What to use instead |
| 179 | |
| 180 | | Banned | Use instead | |
| 181 | |--------|-------------| |
| 182 | | `Any` | `TypedDict`, `Protocol`, specific union | |
| 183 | | `object` | The actual type or a constrained union | |
| 184 | | `list` (bare) | `list[X]` | |
| 185 | | `dict` (bare) | `dict[K, V]` | |
| 186 | | `dict[str, Any]` with known keys | `TypedDict` — if you know the keys, name them | |
| 187 | | `cast(T, x)` | Fix the function producing `x` to return `T` | |
| 188 | | `# type: ignore` | Fix the underlying type error | |
| 189 | | `Optional[X]` | `X \| None` | |
| 190 | | `List[X]`, `Dict[K,V]` | `list[X]`, `dict[K, V]` | |
| 191 | |
| 192 | ## Verification checklist |
| 193 | |
| 194 | Run before every merge — in this exact order: |
| 195 | |
| 196 | - [ ] On a feature branch, not `main` |
| 197 | - [ ] `mypy muse/` — zero errors, strict mode |
| 198 | - [ ] `python tools/typing_audit.py --dirs muse/ tests/ --max-any 0` — zero violations |
| 199 | - [ ] `pytest tests/ -v` — all green |
| 200 | - [ ] No `Any`, bare collections, `cast()`, `# type: ignore`, `Optional[X]`, legacy `List`/`Dict` |
| 201 | - [ ] No dead code, no legacy patterns |
| 202 | - [ ] Affected docs updated in the same commit |
| 203 | - [ ] No secrets, no `print()`, no orphaned imports |
| 204 | |
| 205 | ## Anti-patterns (never do these) |
| 206 | |
| 207 | - Using `git`, `gh`, or GitHub for anything. Muse and MuseHub are the only VCS tools. |
| 208 | - Working directly on `main`. |
| 209 | - `Any`, `object`, bare collections, `cast()`, `# type: ignore` — absolute bans. |
| 210 | - `Optional[X]`, `List[X]`, `Dict[K,V]` — use modern syntax. |
| 211 | - `async`/`await` anywhere in `muse/` — the CLI is synchronous by design. |
| 212 | - Importing from `muse.plugins.*` inside `muse.core.*`. |
| 213 | - Adding `fastapi`, `sqlalchemy`, `pydantic`, `httpx`, `asyncpg` as dependencies — the whole point of v2 is that they are gone. |
| 214 | - Hardcoded paths or repo IDs outside `.muse/repo.json`. |
| 215 | - `print()` for diagnostics — use `logging`. |
| 216 | |
| 217 | ## Test efficiency — mandatory protocol |
| 218 | |
| 219 | 1. Run the full suite **once** to find all failures. |
| 220 | 2. Fix every failure found. |
| 221 | 3. Re-run **only the specific failing file(s)** to confirm the fix: `pytest tests/test_foo.py -v` |
| 222 | 4. Run the full suite only as the final pre-merge gate. |
| 223 | |
| 224 | ## Quick reference |
| 225 | |
| 226 | | Area | Module | Tests | |
| 227 | |------|--------|-------| |
| 228 | | Plugin contract | `muse/domain.py` | `tests/test_midi_plugin.py` | |
| 229 | | Object store | `muse/core/object_store.py` | `tests/test_core_snapshot.py` | |
| 230 | | File store | `muse/core/store.py` | `tests/test_core_store.py` | |
| 231 | | Merge engine | `muse/core/merge_engine.py` | `tests/test_core_merge_engine.py` | |
| 232 | | CLI commands | `muse/cli/commands/` | `tests/test_cli_workflow.py` | |
| 233 | | MIDI plugin | `muse/plugins/midi/plugin.py` | `tests/test_midi_plugin.py` | |
| 234 | | Typing audit | `tools/typing_audit.py` | run with `--max-any 0` | |