# Muse — Agent Rules ## Identity 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. - **Entry point:** `muse` CLI. No web server, no HTTP API, no database. - **Stack:** Python 3.14, Typer, file-based content-addressed storage (`.muse/`), synchronous I/O throughout. - **Plugin contract:** `MuseDomainPlugin` protocol in `muse/domain.py`. Every new domain is a new plugin — the core engine never changes. - **Version control:** Muse and MuseHub exclusively. Git and GitHub are not used. ## No legacy. No deprecated. No exceptions. This is the single most repeated rule. It is a hard constraint enforced on every change. - **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. - **No fallback paths for old shapes.** Remove every trace of the old way. - **No "legacy" or "deprecated" comments.** If it's marked deprecated, delete it. - **No dead constants, dead regexes, dead fields.** If it can never be reached, delete it. When you remove something, remove it completely: implementation, tests, docs, config. ## Version control — Muse only. Git and GitHub are not used. 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. ### Starting work ``` muse status # where am I, what's dirty muse branch feat/my-thing # create branch muse checkout feat/my-thing # switch to it ``` ### While working ``` muse status # constantly — like breathing muse diff # what exactly changed, symbol-level muse code add . # stage what you want in the next snapshot muse commit -m "..." # typed event: Muse proposes MAJOR/MINOR/PATCH ``` ### Before merging ``` muse fetch origin # pull down what's changed upstream muse status # shows if you're behind muse merge --dry-run main # will this conflict? what's the semver impact? ``` ### Merging ``` muse merge main # if dry-run was clean ``` ### Releasing ``` # Create a local release at HEAD (--title and --body are required by convention) muse release add --title "" --body "<description>" # Optionally set channel (default is inferred from semver pre-release label) muse release add <tag> --title "<title>" --body "<description>" --channel stable # Push to a remote muse release push <tag> --remote local # Full delete-and-recreate cycle (e.g. after a DB migration or data fix): muse release delete <tag> --remote local --yes muse release add <tag> --title "<title>" --body "<description>" muse release push <tag> --remote local ``` ### The mental model 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. - `muse diff` shows `Invoice.calculate()` was modified, not that lines 42–67 changed. - `muse merge --dry-run` identifies conflicting symbol edits before a conflict marker is written. - `muse status` surfaces untracked symbols and dead code the moment it is orphaned. - `muse commit` is a **typed event** — Muse proposes MAJOR/MINOR/PATCH based on what changed structurally. ### Branch discipline **Never work directly on `main`.** Full task lifecycle: 1. `muse status` — must be clean before branching. 2. `muse branch feat/<description>` then `muse checkout feat/<description>` — always branch first. 3. Do the work, commit on the branch. 4. **Verify locally before merging — in this exact order:** ``` mypy muse/ # zero errors python tools/typing_audit.py --dirs muse/ tests/ --max-any 0 # zero violations pytest tests/ -v # all green ``` 5. `muse merge --dry-run main` — confirm clean. 6. `muse checkout main && muse merge feat/<description>` 7. `muse release add <tag> --title "<title>" --body "<description>"` then `muse release push <tag> --remote local`. ## MuseHub interactions 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). | Operation | Tool | |-----------|------| | Browse releases | `http://localhost:10003/<owner>/<repo>/releases` | | Push a release | `muse release push <tag> --remote local` | | Delete a remote release | `muse release delete <tag> --remote local --yes` | ## Architecture (do not weaken) ``` muse/ domain.py → MuseDomainPlugin protocol + LiveState / StateSnapshot / StateDelta types core/ object_store.py → content-addressed blob storage (.muse/objects/) snapshot.py → manifest hashing, workdir diff store.py → file-based CRUD for commits, snapshots, tags (.muse/commits/ etc.) merge_engine.py → three-way merge, merge-base, conflict detection repo.py → require_repo() — locates .muse/ from cwd errors.py → ExitCode enum cli/ app.py → Typer root — registers all commands commands/ → one file per command (init, commit, log, status, …) models.py → re-exports CommitRecord, SnapshotRecord, TagRecord config.py → .muse/config.toml helpers midi_parser.py → MIDI / MusicXML → NoteEvent (MIDI domain utility) plugins/ music/ plugin.py → MidiPlugin — reference MuseDomainPlugin implementation tools/ typing_audit.py → typing violation scanner (ratchet: --max-any 0) tests/ → pytest suite ``` **Layer rules:** - Commands are thin. No business logic in `cli/commands/` — delegate to `muse.core.*`. - `muse.core.*` is domain-agnostic. It never imports from `muse.plugins.*`. - `muse.plugins.music.plugin` is the only file that may import domain-specific logic. - New domains are new directories under `muse/plugins/`. The core engine is never modified. ## Frontend separation of concerns — absolute rule (applies to MuseHub contributions) When working on any MuseHub template or static asset, every concern belongs in exactly one layer: | Layer | Where | What | |-------|-------|------| | **Structure** | `templates/musehub/pages/*.html`, `fragments/*.html` | Jinja2 markup only — no `<style>`, no `<script>` | | **Behaviour** | `templates/musehub/static/js/*.js` | All JS / Alpine / HTMX | | **Style** | `templates/musehub/static/scss/_*.scss` | All CSS — compiled to `app.css` via `app.scss` | **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. ## Code standards - Type hints everywhere — 100% coverage, no untyped functions or parameters. - `list[X]` / `dict[K, V]` style — never `List`, `Dict`, `Optional`. - `X | None` — never `Optional[X]`. - Synchronous I/O throughout — no `async`, no `await`, no `asyncio`. - `logging.getLogger(__name__)` — never `print()`. - Sparse logs. Docstrings on public modules, classes, and functions. "Why" over "what." ## Typing — zero-tolerance rules Strong types are the contract. There are no exceptions. - **No `Any`. Ever.** Use `TypedDict`, a `Protocol`, or a specific union. There is always a correct type. - **No `object`. Ever.** It is `Any` with a different name. Express the actual shape. - **No bare collections.** `list`, `dict`, `set`, `tuple` without type parameters are banned. Always `list[str]`, `dict[str, int]`, etc. - **No `# type: ignore`.** Fix the root cause. If a third-party library forces the issue, write a typed adapter. - **No `cast()`.** If you need a cast, the callee returns the wrong type — fix the callee. - **No `Optional[X]`.** Write `X | None`. - **No legacy typing imports.** `List`, `Dict`, `Set`, `Tuple` from `typing` are banned — use lowercase builtins. ### What to use instead | Banned | Use instead | |--------|-------------| | `Any` | `TypedDict`, `Protocol`, specific union | | `object` | The actual type or a constrained union | | `list` (bare) | `list[X]` | | `dict` (bare) | `dict[K, V]` | | `dict[str, Any]` with known keys | `TypedDict` — if you know the keys, name them | | `cast(T, x)` | Fix the function producing `x` to return `T` | | `# type: ignore` | Fix the underlying type error | | `Optional[X]` | `X \| None` | | `List[X]`, `Dict[K,V]` | `list[X]`, `dict[K, V]` | ## Verification checklist Run before every merge — in this exact order: - [ ] On a feature branch, not `main` - [ ] `mypy muse/` — zero errors, strict mode - [ ] `python tools/typing_audit.py --dirs muse/ tests/ --max-any 0` — zero violations - [ ] `pytest tests/ -v` — all green - [ ] No `Any`, bare collections, `cast()`, `# type: ignore`, `Optional[X]`, legacy `List`/`Dict` - [ ] No dead code, no legacy patterns - [ ] Affected docs updated in the same commit - [ ] No secrets, no `print()`, no orphaned imports ## Anti-patterns (never do these) - Using `git`, `gh`, or GitHub for anything. Muse and MuseHub are the only VCS tools. - Working directly on `main`. - `Any`, `object`, bare collections, `cast()`, `# type: ignore` — absolute bans. - `Optional[X]`, `List[X]`, `Dict[K,V]` — use modern syntax. - `async`/`await` anywhere in `muse/` — the CLI is synchronous by design. - Importing from `muse.plugins.*` inside `muse.core.*`. - Adding `fastapi`, `sqlalchemy`, `pydantic`, `httpx`, `asyncpg` as dependencies — the whole point of v2 is that they are gone. - Hardcoded paths or repo IDs outside `.muse/repo.json`. - `print()` for diagnostics — use `logging`. ## Test efficiency — mandatory protocol 1. Run the full suite **once** to find all failures. 2. Fix every failure found. 3. Re-run **only the specific failing file(s)** to confirm the fix: `pytest tests/test_foo.py -v` 4. Run the full suite only as the final pre-merge gate. ## Quick reference | Area | Module | Tests | |------|--------|-------| | Plugin contract | `muse/domain.py` | `tests/test_midi_plugin.py` | | Object store | `muse/core/object_store.py` | `tests/test_core_snapshot.py` | | File store | `muse/core/store.py` | `tests/test_core_store.py` | | Merge engine | `muse/core/merge_engine.py` | `tests/test_core_merge_engine.py` | | CLI commands | `muse/cli/commands/` | `tests/test_cli_workflow.py` | | MIDI plugin | `muse/plugins/midi/plugin.py` | `tests/test_midi_plugin.py` | | Typing audit | `tools/typing_audit.py` | run with `--max-any 0` |