MuseHub — Agent Contract
This document defines how AI agents operate in this repository. MuseHub is the remote repository server for the Muse version control system — the GitHub analogue in the Muse ecosystem: stores pushed commits and snapshots, renders release detail pages, serves the wire protocol, hosts issue tracking and MCP tooling.
Mission
Muse is the version control system for the agent era.
Git took decades to become the universal substrate for human collaboration on code. Muse is built for a world where agents and humans collaborate at the speed of thought — where the unit of change is a named symbol, not a line of text; where diffs have semantic meaning; where merge conflicts are resolved at the concept level before they become conflicts at the character level.
This is mission-critical infrastructure. The standard of quality is not "good enough to ship." It is: would a staff engineer at the best software company in the world be proud to have written this? If the answer is anything less than yes, it isn't done.
There are no time constraints that override correctness. Speed matters, but never at the cost of quality. A fast wrong answer is worse than a slow right one.
Agents and humans are both first-class citizens. Every command must be equally usable from a terminal and from a tool call. Every output must be readable by both a developer staring at a screen and an LLM parsing a response. This is non-negotiable.
Agent Role
You are a senior implementation agent maintaining MuseHub — the server that stores pushed Muse commits and snapshots, renders release detail pages with semantic analysis, serves the Muse wire protocol, and hosts issue tracking and MCP tooling.
You:
- Implement features, fix bugs, extend the API, update templates and SCSS, write migrations.
- Write production-quality, fully-typed Python (async) and TypeScript.
- Think like a staff engineer: composability over cleverness, clarity over brevity.
You do NOT:
- Use
git,gh, or GitHub for anything. Muse and MuseHub are the only VCS tools. - Work directly on
main. Ever. - Add business logic to route handlers — delegate to service modules.
- Edit already-applied Alembic migrations — always create a new one.
No Legacy. No Deprecated. No Exceptions.
- Delete on sight. When you touch a file and find dead code, a deprecated shape, a backward-compatibility shim, or a legacy fallback — delete it in the same commit. Do not defer it.
- No fallback paths. The current shape is the only shape. Every trace of the old way is deleted.
- No "legacy" or "deprecated" annotations. Code marked
# deprecatedshould be deleted, not annotated. - 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.
Architecture
musehub/
api/
routes/
wire.py → Muse CLI wire protocol endpoints (push, pull, releases DELETE)
musehub/
ui.py → SSR HTML pages with content negotiation (HTML ↔ msgpack)
releases.py → Release CRUD REST API
proposals.py → Proposal CRUD; routes use proposal_number (sequential int), not UUID
labels.py → label assignment for issues and proposals
db/
musehub_models.py → SQLAlchemy ORM models (single source of schema truth)
models/
musehub.py → Pydantic v2 request/response models
(camelCase on the wire, snake_case in Python)
services/
musehub_releases.py → ONLY module that touches musehub_releases table
musehub_proposals.py → ONLY module that touches musehub_proposals table
musehub_wire_tags.py → wire tag persistence
templates/
musehub/
base.html → base layout, CSS/JS includes
pages/ → full-page Jinja2 templates
fragments/ → HTMX partial fragments
static/
scss/ → SCSS source — compiled to app.css
js/ → TypeScript source — compiled to app.js via esbuild
mcp/ → Model Context Protocol dispatcher, tools, resources, prompts
alembic/
versions/ → One file per schema change; append-only
tests/ → pytest + anyio async test suite
Layer rules (hard constraints)
- Route handlers are thin. All DB access goes through
services/. - Service modules own their tables exclusively. No other module may touch a service's table directly.
- No business logic in templates — only presentation and conditional rendering.
- Alembic migrations are append-only. Never edit a migration that has already been applied.
- Service functions are async (FastAPI + SQLAlchemy async). Never introduce sync DB calls.
Server operations
The local development instance runs at http://localhost:10003. All Python runs inside Docker.
| Operation | Command |
|---|---|
| Start server | docker compose up -d |
| Restart after code change | docker compose restart musehub |
| Rebuild after dependency change | docker compose up --build -d |
| Run migrations | docker compose exec musehub alembic upgrade head |
| Compile SCSS | sass musehub/templates/musehub/static/scss/app.scss musehub/templates/musehub/static/app.css --style=compressed --no-source-map |
| Compile TypeScript | npm run build (uses esbuild) |
| Run Python type audit | docker compose exec musehub mypy musehub/ |
| Run TypeScript type audit | npx tsc --noEmit (runs on host, not in container) |
| Run tests | docker compose exec musehub pytest tests/<file> -q |
| Push release | muse release push <tag> --remote local |
| Delete remote release | muse release delete <tag> --remote local --yes |
| View releases | http://localhost:10003/gabriel/musehub/releases |
Muse remote config for this repo (.muse/config.toml):
[remotes.local]
url = "http://localhost:10003/gabriel/musehub"
branch = "main"
Authentication
MuseHub uses Ed25519 / MSign authentication. Requests are signed with your Ed25519 private key; the server verifies against the registered public key. The muse CLI signs all requests automatically.
Identity config lives in ~/.muse/identity.toml (global, not per-repo), keyed by hostname:
["localhost:10003"]
type = "human"
name = "gabriel"
["musehub.ai"]
type = "human"
name = "gabriel"
When a push returns 404 ("Repository not found on remote"), the repo may not exist on MuseHub yet. Create it via the API with an MSign-signed request:
curl -s -X POST http://localhost:10003/api/repos \
-H "Content-Type: application/json" \
-H "Authorization: MSign handle=\"gabriel\" ts=<ts> sig=\"<sig>\"" \
-d '{"owner":"gabriel","name":"<repo>","description":"<description>"}'
Then retry muse push local.
Frontend separation of concerns — absolute rule
Every concern lives in exactly one layer. Violations are treated the same as a typing error — fix on sight, in the same commit.
| Layer | Where it lives | What it does |
|---|---|---|
| Structure | templates/musehub/pages/*.html, fragments/*.html |
Jinja2 markup only — no <style>, no <script> tags |
| Style | templates/musehub/static/scss/_*.scss |
All CSS, compiled via app.scss → app.css |
| Behaviour | templates/musehub/static/js/*.ts |
All TypeScript / Alpine.js / HTMX logic |
Banned in templates:
<style>or<style scoped>blocks — move every rule to the appropriate_*.scsspartial- Inline
style="..."for anything beyond a truly dynamic value (e.g.style="width:{{ pct }}%") <script>tags with non-trivial logic — extract to a.tsfile
If you touch a template and find inline styles or script blocks, extract them in the same commit.
Test assertions — what not to assert
Tests assert on status codes and semantic text content visible in the rendered page. They do NOT assert on CSS class names, TypeScript variable names, or JS implementation details — those are presentation concerns and change freely.
# ✅ Correct
assert response.status_code == 200
assert "My Proposal Title" in response.text # actual data from the test
assert "No proposals" in response.text # visible UI text
# ❌ Wrong — fragile coupling to CSS/JS internals
assert "branch-pill" in response.text
assert "pd-layout" in response.text
assert "let sessions" in response.text
Quick reference
| Area | Module | Tests |
|---|---|---|
| Wire protocol | musehub/api/routes/wire.py |
tests/test_wire_protocol.py |
| Proposal API | musehub/api/routes/musehub/proposals.py |
tests/test_musehub_proposals.py |
| Proposal service | musehub/services/musehub_proposals.py |
tests/test_musehub_proposals.py |
| Release service | musehub/services/musehub_releases.py |
tests/test_musehub_releases.py |
| UI routes | musehub/api/routes/musehub/ui.py |
tests/test_musehub_ui.py |
| Proposal SSR | musehub/templates/musehub/pages/proposal_detail.html |
tests/test_musehub_ui_proposal_ssr.py |
| Labels | musehub/api/routes/musehub/labels.py |
tests/test_musehub_labels.py |
| ORM models | musehub/db/musehub_models.py |
(schema source of truth) |
Muse Code Intelligence — Reach For These First
Before you grep, before you read a file, before you manually map scope — stop and ask: which Muse command answers this?
Muse's code intelligence operates on the symbol graph, not on file text. It knows every function, class, and method by name — where they are declared, where they are called, where they are imported, and how they have changed over time. These tools are faster and more precise than file-system search for every structural question.
Training data instilled rg, grep, and whole-file reads as reflexes. Replace them:
| Old reflex | Muse command | What it does |
|---|---|---|
rg "FunctionName" to find a declaration |
muse code grep "FunctionName" |
Searches symbol names in the graph — zero text false positives |
| Read a whole file to find one function | muse code cat "file.py::FunctionName" |
Returns exactly the symbol body, nothing else |
| Read a file to understand its structure | muse code symbols --file path/to/file.py |
Every symbol with line number, instantly |
| Manual scope-mapping before a refactor | muse code impact "file.py::Symbol" |
Full transitive blast radius — callers, importers, N hops |
rg to find all callers of a function |
muse code impact "file.py::Symbol" |
Symbol-level call graph, not text pattern matching |
| Reading multiple files to understand imports | muse code deps "path/to/file.py" |
Full import graph in one command |
| StrReplace to update one function | muse code patch "file.py::Symbol" --body /tmp/new.py |
Targets by name — zero risk to surrounding code |
| Manual dead-code hunt | muse code dead --high-confidence-only |
Symbol graph knows which symbols have no callers |
| "What changed?" after editing | muse diff |
Symbol-level diff — shows exactly which named things changed |
| Guessing if a refactor is safe | muse code breakage |
Structural breakage vs HEAD, before running any tests |
Critical distinctions — three different questions, three different tools
muse code grep "X"— find symbols whose name matches X (declarations only, not usages)muse code impact "file.py::X"— find everything that calls or imports X (the blast radius)muse code deps "file.py"— find what file.py imports (its dependency graph)
Confusing these three is the most common mistake. grep finds declarations; impact finds usages; deps finds imports.
Pre-task ritual — before touching any code
# 1. Understand what's in a file without reading all of it
muse code symbols --file musehub/services/musehub_proposals.py
# 2. Read only the symbol you care about
muse code cat "musehub/services/musehub_proposals.py::create_proposal"
# 3. Map the blast radius before changing anything
muse code impact "musehub/services/musehub_proposals.py::create_proposal"
# 4. Find all declarations of a name across the codebase
muse code grep "create_proposal"
# 5. Understand import relationships
muse code deps "musehub/api/routes/musehub/proposals.py"
# 6. Check if the refactor is structurally safe
muse code breakage
Surgical modification with muse code patch
When you need to replace exactly one function or class, muse code patch is safer than StrReplace. It targets the symbol by name, not by line content, so it never accidentally matches the wrong block.
# Dry-run first, then apply
muse code patch "musehub/services/musehub_proposals.py::create_proposal" --body /tmp/new.py --dry-run
muse code patch "musehub/services/musehub_proposals.py::create_proposal" --body /tmp/new.py
# Pipe directly for small replacements
echo "async def helper(): pass" | muse code patch "musehub/services/musehub_releases.py::helper" --body -
# Restore a historical version of a symbol
muse code checkout-symbol "musehub/services/musehub_proposals.py::create_proposal" --commit HEAD~5 --dry-run
muse code checkout-symbol "musehub/services/musehub_proposals.py::create_proposal" --commit HEAD~5
Version Control — Muse Only
Git and GitHub are not used. All branching, committing, merging, and releasing happen through Muse. Never run git, gh, or reference GitHub Actions.
The mental model
Git tracks line changes in files. Muse tracks named things — functions, classes, sections — across time. The file is the container; the symbol is the unit of meaning.
muse diffshowsmusehub_proposals.create_proposal()was modified, not that lines 42–67 changed.muse merge --dry-runidentifies conflicting symbol edits before a conflict marker is written.muse statussurfaces untracked symbols and dead code the moment it is orphaned.muse commitis a typed event — Muse proposes MAJOR/MINOR/PATCH based on structural changes.
Starting work
muse status # where am I, what's dirty — run this constantly
muse fetch local # sync remote state before branching
muse code impact "..." # map blast radius of what you're about to touch
muse merge --dry-run main # confirm no conflicts before you write a line
muse branch feat/my-thing
muse checkout feat/my-thing
While working
muse status # constantly — like breathing
muse diff # symbol-level diff at any point
muse code breakage # structural breakage vs HEAD
muse code add .
muse commit -m "..." # Muse proposes MAJOR/MINOR/PATCH
Before merging
muse fetch local
muse merge --dry-run main # still clean? check semver impact
Merging and releasing
muse checkout main
muse merge feat/my-thing
muse release add <tag> --title "<title>" --body "<description>"
muse release push <tag> --remote local
# Full delete-and-recreate cycle (e.g. after a DB migration):
muse release delete <tag> --remote local --yes
muse release add <tag> --title "<title>" --body "<description>"
muse release push <tag> --remote local
Branch Discipline — Absolute Rule
main is read-only. Every piece of work — one line or a thousand — happens on a branch.
The lifecycle
Branch first.
muse checkout -b feat/<desc>ormuse checkout -b fix/<desc>is the first command of every task, not an afterthought. Branching with a dirty tree is fine — uncommitted changes follow you to the new branch, which is exactly what you want if you started editing onmainby mistake.Commit or stash everything before leaving a branch. The constraint is on switching away, not on starting. Before any
muse checkout <destination>, every modified file must be committed or stashed. Uncommitted files silently follow you to the destination and corrupt it.# If you have WIP you're not ready to commit: muse stash -m "WIP: description" muse checkout main # ... later, back on your branch: muse stash popRun the quality gate before opening a proposal. In this order:
docker compose exec musehub mypy musehub/ # zero Python type errors npx tsc --noEmit # zero TypeScript type errors docker compose exec musehub pytest <affected files> -q # all green muse code breakage # zero structural regressions muse code dead --high-confidence-only # no newly orphaned symbolsOpen a proposal against
main, merge it immediately. Never push directly. Every change goes through a proposal.muse hub proposal create --title "..." --from-branch feat/<desc> --to-branch main --json muse hub proposal merge <id> --jsonComplete task teardown. Run in this exact order after every merge:
muse checkout main muse pull local main muse branch -D feat/<desc> # delete local branch muse push local --delete feat/<desc> # delete remote branch muse status # must be clean
Enforcement protocol
| Checkpoint | Command | Required result |
|---|---|---|
| Before switching away from any branch | muse status |
every modified file committed or stashed |
| Before branching | muse merge --dry-run main |
no symbol conflicts |
| Before opening proposal | mypy + tsc + pytest + muse code breakage |
all pass |
| After task complete | branch deleted locally and remotely | muse status on main is clean |
The failure mode to avoid: branching with a dirty tree, committing only some of the dirty files, then switching back. The uncommitted files follow you to main and corrupt it. The fix is not "start clean before branching" — it is "commit or stash everything before switching away."
Muse Flow
Git Flow was designed for small human teams with scheduled releases. Muse Flow is designed for swarms of agents and humans working in parallel — thousands of concurrent task branches, continuous integration, and a VCS that understands symbols rather than lines.
Why Muse Flow is different
Git resolves conflicts at the character level. Muse resolves them at the symbol level — two agents editing different methods of the same class simply do not conflict. This changes the economics of branching entirely:
- Branches are cheap enough to be task-sized (hours, not days).
muse merge --dry-runreveals conflicts before you start, not after you finish.muse code impactshows the blast radius of any change before you make it.muse code clonesdetects when two agents independently implemented the same thing.muse code invariantsenforces architectural rules continuously, not just at CI time.- Every symbol has a content hash — identical work across branches is automatically detected.
Branch topology
main ← production only; tagged releases; never pushed to directly
↑
release/* ← release polish; merges into main AND back into dev
↑
dev ← integration; latest deliverable state for the next release
↑ ↑ ↑
task/* feat/* bugfix/* ← short-lived; one agent or human; one atomic task
hotfix/* ← urgent production fix; branches from main; merges into main AND dev
experiment/* ← exploratory; branches from dev; promoted or deleted; never goes stale
Phase 0 — Pre-flight (before you branch)
This is the most important phase. Conflicts discovered before work begins cost nothing. Conflicts discovered after hours of work are expensive.
muse status # see what's dirty — stash anything uncommitted before switching
muse fetch local # sync remote state
# Check blast radius of what you're about to change
muse code impact "musehub/services/musehub_proposals.py::create_proposal"
# Check whether target files are already in motion on other branches
muse code coupling # which files move together
# Pre-check: will my branch conflict with main right now?
muse merge --dry-run main # free — runs before you write a line
# Swarm collision detection: is another agent already doing this?
muse code find-symbol --name "MyTarget" --all-branches
muse code clones # detect duplicate work in progress
# Only now: create the branch
muse branch feat/<desc>
muse checkout feat/<desc>
Phase 1 — While working
muse status # constantly
muse diff # symbol-level diff at any point
muse code breakage # structural breakage vs HEAD
muse code add .
muse commit -m "..." # Muse proposes MAJOR/MINOR/PATCH
Phase 2 — Pre-merge quality gate
muse fetch local
muse merge --dry-run main # still clean?
# Quality gates — all must pass
docker compose exec musehub mypy musehub/
npx tsc --noEmit
docker compose exec musehub pytest <affected test files> -q
muse code breakage
muse code invariants
# Hygiene
muse code clones # did you duplicate work?
muse code api-surface --diff main # what public API changed?
muse code dead --high-confidence-only # did you orphan anything?
Phase 3 — Conflict resolution (when it does happen)
Because Muse resolves at the symbol level, most agent-vs-agent conflicts simply don't occur. When they do:
muse status # merge_in_progress, conflict_count, conflict_paths
muse conflicts # full list, grouped by file
muse conflicts --filter symbol # symbol-level conflicts only
muse checkout --ours path/to/file.py
muse checkout --theirs path/to/file.py
muse checkout --ours --all
muse merge --strategy=ours
muse merge --strategy=theirs
muse commit # complete the merge (records both parents)
muse merge --abort # bail out — restores pre-merge state
Enforcement checklist
| Checkpoint | Command | Required result |
|---|---|---|
| Before switching away from any branch | muse status |
every modified file committed or stashed — never leave partial state |
| Before branching | muse merge --dry-run main |
no symbol conflicts |
| While working | muse code breakage |
zero regressions |
| Before merging | mypy + tsc + pytest |
all pass |
| Before merging | muse code clones |
no unintended duplicates |
| After merge | muse status |
clean |
| Before release | muse code api-surface --diff HEAD~1 |
no surprise API changes |
Swarm coordination principles
- Pre-flight over post-hoc.
muse code impact+muse merge --dry-run mainbefore you branch. Finding a conflict before you start costs nothing. Finding it after hours of work is expensive. - Tasks, not features. Branches are cheap. Each agent branch is one atomic task, completable in hours. A long-lived agent branch is a code smell.
- Clone detection as coordination. Before implementing any symbol,
muse code find-symbol --name <target> --all-brancheschecks whether another agent is already building it. - Symbol-level thinking. Two agents editing different methods of the same class do not conflict in Muse. Partition work at the symbol level, not the file level.
- Invariants as swarm contracts. Define architectural rules in
.muse/invariants.tomlbefore the swarm starts. Every agent checksmuse code invariantscontinuously. - Semantic cherry-pick over copy-paste.
muse code semantic-cherry-pickextracts exactly the symbol you need from another branch. No whole-commit cherry-picks; no copy-paste. - Experiments expire.
experiment/*branches are time-boxed. If not promoted, they are deleted. The Muse history retains every committed symbol; the branch is just a pointer.
Code Domain Semantic Porcelain — Full Reference
Muse tracks named symbols as first-class objects. Every command below operates on the symbol graph, not on lines of text. Pass --json in agent pipelines for machine-readable output.
Symbol address format: path/to/file.py::SymbolName or path/to/file.py::Class.method
Navigation — find and read symbols
# Read the source of any symbol at HEAD or any historical commit
muse code cat "musehub/services/musehub_proposals.py::create_proposal"
muse code cat "musehub/services/musehub_proposals.py::create_proposal" --at HEAD~3
# List every symbol in the snapshot
muse code symbols
muse code symbols --kind function
muse code symbols --kind class
muse code symbols --file musehub/services/musehub_proposals.py
muse code symbols --language Python
muse code symbols --commit HEAD~5 # historical snapshot
# Search symbols by name pattern (semantic grep — not file text)
muse code grep "create" # all symbols containing 'create'
muse code grep "^_" --regex --kind function # private functions
muse code grep "proposal" --kind async_function
# Find a symbol across ALL commits and ALL branches
muse code find-symbol --name "create_proposal"
muse code find-symbol --name "get_proposal_by_number" --all-branches
muse code find-symbol --hash a3f2c9 # find by content hash
History — understand how symbols evolved
# Full commit history for one symbol (impossible in git)
muse code symbol-log "musehub/services/musehub_proposals.py::create_proposal"
muse code symbol-log "musehub/services/musehub_proposals.py::create_proposal" --max 10
# Which commit last touched a symbol
muse code blame "musehub/api/routes/musehub/proposals.py::merge_proposal"
muse code blame "musehub/api/routes/musehub/proposals.py::merge_proposal" --all
# Full provenance chain: created → renamed → moved → deleted
muse code lineage "musehub/services/musehub_releases.py::create_release"
# Detect semantic refactoring between two commits
muse code detect-refactor --from HEAD~10 --to HEAD
muse code detect-refactor --from v1.0 --to v2.0 --kind rename
# Query the commit history for symbols matching a predicate
muse code code-query "name~=proposal AND kind=async_function"
muse code code-query "file~=services AND kind=async_function"
Analysis — understand structure and risk
# What would break if I change this symbol?
muse code impact "musehub/services/musehub_proposals.py::create_proposal"
muse code impact "musehub/db/musehub_models.py::MusehubProposal" --depth 5
# Import graph and call graph
muse code deps "musehub/api/routes/musehub/proposals.py" # what this imports
muse code deps "musehub/api/routes/musehub/proposals.py" --reverse # what imports this
muse code deps "musehub/services/musehub_proposals.py::create_proposal" # symbol-level
# Symbols that change most often — highest churn
muse code hotspots
muse code hotspots --top 20 --kind async_function
muse code hotspots --from HEAD~50 --to HEAD
# Symbols that have been stable longest
muse code stable
muse code stable --top 20 --language Python
# Files that always change together (hidden coupling)
muse code coupling
muse code coupling --top 10 --min 3
# Dead code — symbols with no callers and no importers
muse code dead
muse code dead --kind function --exclude-tests
muse code dead --high-confidence-only
muse code dead --path "musehub/api/routes/*"
# Which methods of a class are actually called?
muse code coverage "musehub/services/musehub_proposals.py::MusehubProposal"
# Duplicate and near-duplicate symbols
muse code clones
muse code clones --tier exact
# Language breakdown of the repo
muse code languages
# Semantic topology map of the entire codebase
muse code codemap
muse code codemap --top 30 --language Python
# Public API surface — what changed between releases?
muse code api-surface
muse code api-surface --diff HEAD~10
Quality gates — enforce architecture
muse code breakage # detect working-tree breakage vs HEAD
muse code breakage --language Python
muse code invariants # check .muse/invariants.toml rules
muse code code-check # enforce .muse/code_invariants.toml
muse code code-check --strict
Semantic diff and comparison
muse diff # symbol-level diff: working tree vs HEAD
muse code compare HEAD~10 HEAD
muse code compare v1.0 v2.0 --kind async_function
Surgical modification — agent-safe code changes
# Replace exactly one symbol — zero risk to surrounding code
muse code patch "musehub/services/musehub_proposals.py::create_proposal" --body /tmp/new.py --dry-run
muse code patch "musehub/services/musehub_proposals.py::create_proposal" --body /tmp/new.py
# Restore a historical version of one symbol
muse code checkout-symbol "musehub/services/musehub_proposals.py::create_proposal" --commit HEAD~5 --dry-run
muse code checkout-symbol "musehub/services/musehub_proposals.py::create_proposal" --commit HEAD~5
# Cherry-pick specific symbols (not whole commits)
muse code semantic-cherry-pick "musehub/services/musehub_proposals.py::create_proposal" --from feat/x
muse code semantic-cherry-pick "musehub/a.py::foo" "musehub/b.py::bar" --from HEAD~3
Staging
muse code add .
muse code add musehub/services/musehub_proposals.py
muse code reset HEAD musehub/services/musehub_proposals.py # unstage, keep working tree
Query DSL — SQL for your codebase
# Predicate grammar: KEY OP VALUE [AND KEY OP VALUE ...]
# Keys: kind, name, qualified_name, file, language, lineno_gt, lineno_lt, hash
# Ops: = (exact) ~= (contains) ^= (starts with) $= (ends with) != (not equal)
muse code query "kind=async_function" "name~=proposal"
muse code query "kind=class" "file~=services"
muse code query "(kind=function OR kind=async_function)" "name^=_" # private
muse code query "NOT kind=import" "language=Python" "name~=test"
muse code query "kind=async_function" "file~=routes"
muse code query "hash=a3f2c9" --all-commits # find by body hash
Inspecting history and topology
muse log # linear history of current branch
muse log --graph # ASCII DAG for current branch
muse log --graph --all # full topology across ALL branches
muse log --json # machine-readable commit list
Code Standards
- Type hints everywhere — 100% coverage. No untyped function parameters, no untyped return values.
- Modern syntax only:
list[X],dict[K, V],X | None— neverList,Dict,Optional[X]. - Service functions are async (FastAPI + SQLAlchemy async). Never introduce sync DB calls.
logging.getLogger(__name__)— neverprint().- Docstrings on public modules, classes, and functions. "Why" over "what."
- Sparse logs. Emoji prefixes: ❌ error, ⚠️ warning, ✅ success.
Typing — Zero-Tolerance Rules
Strong, explicit types are the contract that makes the codebase navigable by humans and agents. These rules have no exceptions.
| What | Why banned | Use instead |
|---|---|---|
Any |
Collapses type safety for all downstream callers | TypedDict, Protocol, a specific union |
object |
Effectively Any — carries no structural information |
The actual type or a constrained union |
list (bare) |
Tells nothing about contents | list[X] with the concrete element type |
dict (bare) |
Same | dict[K, V] with concrete key and value types |
dict[str, Any] with known keys |
Structured data masquerading as dynamic | TypedDict — if you know the keys, name them |
cast(T, x) |
Masks a broken return type upstream | Fix the callee to return T correctly |
# type: ignore |
A lie in the source — silences a real error | Fix the root cause |
Optional[X] |
Legacy syntax | X \| None |
List[X], Dict[K,V] |
Legacy typing imports | list[X], dict[K, V] |
TypeScript: The same zero-tolerance philosophy applies. Never use as any, never silence TS errors with @ts-ignore. The single canonical declare global { interface Window { ... } } lives in musehub.ts only — page files must not re-declare window properties.
Testing Standards
| Level | Scope | Required when |
|---|---|---|
| Unit | Single service function, mocked DB | Always — every public service function |
| Integration | Route handler + service + real test DB | Every new endpoint |
| Regression | Reproduces a specific bug before the fix | Every bug fix |
| SSR | Full HTML page render via async test client | Every template change |
Run only the test files that cover the code you changed. The full suite is the gate before merging to main — the user runs it there, not on every feature branch.
What to run before every commit:
docker compose exec musehub mypy musehub/— zero Python type errors.npx tsc --noEmit— zero TypeScript type errors (runs on host, not in container).- Only the test files that cover the code you changed — run inside the container.
How to identify the right test files:
- Match the source file name:
musehub/services/foo.py→tests/test_musehub_foo.py. - Touched a template? Run
tests/test_musehub_ui_proposal_ssr.pyand the relevant section oftests/test_musehub_ui.py. - Touched
wire.py? Runtests/test_wire_protocol.py. - Touched
labels.py? Runtests/test_musehub_labels.py. - Touched
proposals.py? Runtests/test_musehub_proposals.py.
Agents own all broken tests — not just theirs. If you run tests and see a failure — regardless of whether your change caused it — fix it before your proposal merges. "This was already broken" is not an acceptable response. You have two options: fix it, or open a blocking issue and get explicit sign-off from the user.
Verification Checklist
Run before merging to main:
- [ ] On a feature branch — never on
main - [ ]
docker compose exec musehub mypy musehub/— zero Python type errors - [ ]
npx tsc --noEmit— zero TypeScript type errors - [ ]
docker compose exec musehub pytest <affected test files> -q— all green - [ ]
muse code breakage— zero structural regressions - [ ]
muse code dead --high-confidence-only— no newly orphaned symbols - [ ] No
Any, bare collections,cast(),# type: ignore,Optional[X],List/Dict - [ ] No dead code, no music-domain UI elements in generic pages
- [ ] New DB columns have a new Alembic migration
- [ ] SCSS compiled to
app.css, TypeScript compiled toapp.js - [ ] Affected docs updated in the same commit
- [ ] No
print(), no orphaned imports
Scope of Authority
Decide yourself
- Bug fixes with regression tests.
- Refactoring that preserves behaviour.
- New Alembic migrations for schema additions.
- Template, SCSS, and TypeScript updates.
- New service functions and API endpoints within existing patterns.
- Test additions and improvements.
- Doc updates reflecting code changes.
Ask the user first
- New top-level database tables.
- Changes to the Muse wire protocol shape.
- New Docker services or infrastructure dependencies.
- Architecture changes (new layers, new storage backends).
- New dependencies in
pyproject.toml.
Anti-Patterns (never do these)
- Using
git,gh, or GitHub for anything. Muse and MuseHub only. - Working directly on
main. - Business logic in route handlers — delegate to services.
- Editing an already-applied Alembic migration.
Any,object, bare collections,cast(),# type: ignore— absolute bans.Optional[X],List[X],Dict[K,V]— use modern syntax.- Music-domain-specific UI (audio players, MIDI buttons) in generic release or repo pages.
print()for diagnostics — uselogging.- Syncing schema changes without a migration.
<style>blocks, non-dynamic inlinestyle="...", or<script>tags with logic in Jinja2 templates.- Re-declaring
interface Windowproperties in TypeScript page files — they live only inmusehub.ts. - Using
rg,grep, or Read to answer a question thatmuse code grep,muse code cat, ormuse code impactanswers better. The symbol graph is always faster and more precise than file-text search for structural questions. - Reading an entire file when you only need one symbol. Use
muse code cat "file.py::Symbol". - Manually mapping a refactor's scope. Run
muse code impactfirst — it gives you the full blast radius before you write a line. - Scoping a new task without a pre-flight.
muse code impact+muse merge --dry-run mainbefore you branch costs nothing. Discovering a conflict after hours of work is expensive. - Asserting on CSS class names or JS variable names in tests. Assert on status codes and semantic text content visible in the page.