# `muse tag` — Protocol-Native Version Tags **The complete developer guide with real command output.** Muse version tags are a first-class VCS primitive: local-first, semver-native, and push-aware. They are the lightweight equivalent of `git tag v1.2.3` — minus the footguns. This guide walks through every operation from scratch with actual command output so you know exactly what to expect. --- ## Why version tags? Muse has two annotation systems that serve different purposes: | Tool | Purpose | Hub required? | |------|---------|---------------| | `muse label add emotion:joyful` | Freeform namespace:value annotations — musical metadata, emotional tags, section markers | No | | `muse tag add v0.2.0-rc14` | Protocol-native semantic version stamps — release markers, CI gates, `describe` reference points | No | | `muse release add v1.0.0` | Full MuseHub release with changelog, asset attachments, draft flag | Yes | Version tags live in `.muse/version-tags/` as content-addressed JSON. They push and pull alongside branches. They are the reference points for `muse describe`. No hub connection needed to create them. --- ## Tagging a commit ``` $ muse tag add v0.2.0-rc14 --json { "exit_code": 0, "duration_ms": 0.6, "muse_version": "0.2.0rc14", "schema": 1, "timestamp": "2026-06-28T22:17:43.201Z", "warnings": [], "status": "tagged", "tag_id": "sha256:a3f2c901…", "commit_id": "sha256:7d84ef12…", "tag": "v0.2.0-rc14", "semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" }, "author": "gabriel", "created_at": "2026-06-28T22:17:43+00:00", "message": "", "dry_run": false } ``` Tags HEAD by default. To tag any other commit or branch: ``` $ muse tag add v0.1.0 abc123def # by commit ID prefix $ muse tag add v0.1.0 feat/audio # by branch name ``` Add an annotation message: ``` $ muse tag add v0.2.0 -m "stable: all E2E green, docs reviewed" ``` Validate before writing: ``` $ muse tag add v0.2.0-rc99 --dry-run --json { "status": "dry_run", "tag_id": null, "commit_id": "sha256:7d84ef12…", "tag": "v0.2.0-rc99", ... } ``` Nothing is written to disk. `tag_id` is `null`. --- ## What "tagged" means on disk Every tag is one JSON file in `.muse/version-tags/`: ``` .muse/version-tags/ v0.1.0.json v0.2.0-rc13.json v0.2.0-rc14.json ``` ``` $ cat .muse/version-tags/v0.2.0-rc14.json { "tag_id": "sha256:a3f2c901…", "repo_id": "sha256:5c8e2a0f…", "tag": "v0.2.0-rc14", "semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" }, "commit_id": "sha256:7d84ef12…", "created_at": "2026-06-28T22:17:43+00:00", "author": "gabriel", "message": "" } ``` Content-addressed: the `tag_id` is a deterministic SHA-256 of `repo_id + ":" + tag`. Tamper-evident. Survives bundle transfer and offline workflows. --- ## Idempotent tagging Re-tagging the same commit at the same version is a no-op: ``` $ muse tag add v0.2.0-rc14 --json { "status": "already_tagged", "tag_id": "sha256:a3f2c901…", ... } ``` To move an existing tag to a new commit (e.g. after an amendment): ``` $ muse tag add v0.2.0-rc14 --force ``` --- ## Listing tags ``` $ muse tag list --json { "exit_code": 0, "total": 2, "tags": [ { "tag": "v0.2.0-rc14", "semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" }, "commit_id": "sha256:7d84ef12…", "author": "gabriel", "created_at": "2026-06-28T22:17:43+00:00", "message": "" }, { "tag": "v0.1.0", "semver": { "major": 0, "minor": 1, "patch": 0, "pre": "", "build": "" }, "commit_id": "sha256:b2a91c03…", "author": "gabriel", "created_at": "2026-06-01T18:00:00+00:00", "message": "first stable release" } ] } ``` Without `--pre`, only stable tags are shown (`pre: ""`). This is intentional: your CI pipeline should list stable releases by default; pass `--pre` when you need the full picture including release candidates. ``` $ muse tag list --pre --json # includes v0.2.0-rc14, v0.1.0 $ muse tag list --json # only v0.1.0 (stable only) ``` Sort options: ``` $ muse tag list --sort created # newest created_at first $ muse tag list --sort commit # lexicographic by commit_id $ muse tag list --sort semver # semver precedence (default) ``` --- ## Semver precedence Muse implements full SemVer 2.0 ordering. From highest to lowest: ``` v2.0.0 stable — always above any pre-release of the same M.M.P v1.0.0 v0.2.0 stable — above v0.2.0-rc99 v0.2.0-rc14 rc beats beta; higher number beats lower v0.2.0-rc13 v0.2.0-beta.2 beta beats alpha v0.2.0-beta.1 v0.2.0-alpha.1 v0.1.0 ``` Supported pre-release forms: ``` rc v1.0.0-rc1, v1.0.0-rc14 alpha v1.0.0-alpha alpha. v1.0.0-alpha.1, v1.0.0-alpha.2 beta v1.0.0-beta beta. v1.0.0-beta.2 ``` Build metadata is ignored in ordering per SemVer §10: ``` v1.0.0+build.42 == v1.0.0 (ordering only; metadata stored) ``` --- ## Reading and deleting a tag ``` $ muse tag read v0.2.0-rc14 --json { "exit_code": 0, "tag": "v0.2.0-rc14", "semver": { "major": 0, "minor": 2, "patch": 0, "pre": "rc14", "build": "" }, "commit_id": "sha256:7d84ef12…", "author": "gabriel", "created_at": "2026-06-28T22:17:43+00:00", "message": "" } ``` Missing tag exits 1 with `"error": "version_tag_not_found"`. ``` $ muse tag delete v0.2.0-rc13 --json { "exit_code": 0, "deleted": true, "tag": "v0.2.0-rc13" } $ muse tag delete v0.2.0-rc13 --json # idempotent { "exit_code": 0, "deleted": false, "tag": "v0.2.0-rc13" } ``` --- ## Latest tag ``` $ muse tag latest --json { "exit_code": 0, "latest": { "tag": "v0.1.0", "commit_id": "sha256:b2a91c03…", ... } } $ muse tag latest --pre --json { "exit_code": 0, "latest": { "tag": "v0.2.0-rc14", "commit_id": "sha256:7d84ef12…", ... } } ``` When no matching tags exist, `"latest": null`, exit 0. --- ## Pushing a tag to a remote ``` $ muse tag push local v0.2.0-rc14 --json { "exit_code": 0, "status": "pushed", "remote": "local", "tag": "v0.2.0-rc14", "commit_id": "sha256:7d84ef12…" } ``` Already current on the remote: ``` $ muse tag push local v0.2.0-rc14 --json { "exit_code": 0, "status": "already_current", "remote": "local", "tag": "v0.2.0-rc14" } ``` Conflict — remote tag points to a different commit: ``` $ muse tag push local v0.2.0-rc14 --json { "exit_code": 1, "error": "remote_tag_conflict", "message": "Remote tag v0.2.0-rc14 exists at a different commit. Use --force to overwrite." } $ muse tag push local v0.2.0-rc14 --force # overwrite ``` --- ## Pushing tags with a branch The idiomatic workflow: push the branch and all tags in one command. ``` $ muse push local dev --tags --json { "exit_code": 0, "commits_sent": 3, "tags_pushed": 2, "tags_skipped": 0, ... } ``` `tags_skipped` counts tags already current on the remote. To make `--tags` the default for every push in this repo: ``` $ muse config set push.tags true $ muse config get push.tags --json { "key": "push.tags", "value": "true" } $ muse push local dev # now includes tags automatically ``` --- ## Fetching tags from a remote `muse pull` fetches version tags alongside commits: ``` $ muse pull local main ✔ 3 commits fetched ✔ 2 version tags fetched: v0.1.0, v0.2.0-rc14 ``` After pulling, `muse tag list --pre` shows the fetched tags locally. --- ## `muse describe` — position relative to the nearest tag `muse describe` walks backward from HEAD and finds the nearest version tag. It is the Muse equivalent of `git describe --tags`. **HEAD is exactly on a tag:** ``` $ muse describe --json { "exit_code": 0, "tag": "v0.1.0", "distance": 0, "short_sha": "b2a91c03", "description": "v0.1.0", "exact": true, "commit_id": "sha256:b2a91c03…", "branch": "main" } ``` **HEAD is 3 commits ahead of a tag:** ``` $ muse describe --json { "exit_code": 0, "tag": "v0.1.0", "distance": 3, "short_sha": "7d84ef12", "description": "v0.1.0-3-7d84ef12", "exact": false, "commit_id": "sha256:7d84ef12…", "branch": "dev" } ``` The description string `v0.1.0-3-7d84ef12` tells you: 3 commits past tag v0.1.0, at the commit starting with `7d84ef12`. **Without `--pre`, only stable tags are reference points:** ``` # Only RC tags exist: $ muse describe --json { "exit_code": 1, "tag": null, "distance": null, "description": null, ... } $ muse describe --pre --json { "exit_code": 0, "tag": "v0.2.0-rc14", "distance": 0, "description": "v0.2.0-rc14", ... } ``` **Describe from a specific branch or commit:** ``` $ muse describe feat/audio --json $ muse describe sha256:7d84ef12 --json ``` **Control the hex suffix length:** ``` $ muse describe --abbrev 12 --json { "short_sha": "7d84ef12a3b9", "description": "v0.1.0-3-7d84ef12a3b9" } ``` Default is 8. Range: 4–64. No `sha256:` prefix — raw hex only. --- ## Full round-trip workflow ```bash # Start new feature work muse checkout dev muse checkout -b task/audio-export --intent "implement audio export" --resumable # ... do your work, commit ... muse code add . muse commit -m "feat(audio): implement WAV export" --agent-id claude-code --model-id claude-sonnet-4-6 --sign # Tag the release candidate muse tag add v0.3.0-rc1 -m "RC1: WAV export, no MP3 yet" # Confirm it's there muse tag list --pre --json | jq '.tags[].tag' # "v0.3.0-rc1" # "v0.2.0-rc14" # "v0.1.0" # Describe HEAD position muse describe --pre --json # { "tag": "v0.3.0-rc1", "distance": 0, "description": "v0.3.0-rc1", "exact": true } # Merge to dev muse checkout dev muse merge task/audio-export muse branch -d task/audio-export # Push branch + all tags together muse push local dev --tags # Confirm tags arrived on the remote muse -C ~/repos/clone tag list --pre --json | jq '.tags[].tag' # "v0.3.0-rc1" # "v0.2.0-rc14" # "v0.1.0" ``` --- ## Label annotations vs version tags — when to use which Use **`muse label`** when the annotation is semantic metadata about content: ``` muse label add emotion:joyful muse label add section:chorus muse label add tempo:allegro ``` Use **`muse tag`** when you are marking a point in history with a version number: ``` muse tag add v0.2.0-rc14 muse tag add v1.0.0 ``` They live in different directories (`.muse/tags/` vs `.muse/version-tags/`), sort by different keys, and push through different channels. They do not interfere. --- ## Error cases **Invalid version tag name:** ``` $ muse tag add emotion:joyful --json { "exit_code": 1, "error": "invalid_version_tag_name", "message": "Version tag must start with 'v' (e.g. 'v0.2.0-rc14'), got: 'emotion:joyful'" } $ muse tag add v1.0 --json { "exit_code": 1, "error": "invalid_version_tag_name", "message": "Version 'v1.0' is not valid semver (expected vMAJOR.MINOR.PATCH[-pre][+build])." } ``` **Tag not found:** ``` $ muse tag read v9.9.9 --json { "exit_code": 1, "error": "version_tag_not_found", "tag": "v9.9.9" } ``` **Remote unreachable:** ``` $ muse tag push offline v0.1.0 --json { "exit_code": 1, "error": "remote_not_found", "remote": "offline" } ``` --- ## Quick reference ``` muse tag add v1.0.0 # tag HEAD muse tag add v1.0.0 feat/audio # tag branch tip muse tag add v1.0.0 -m "stable" # with annotation muse tag add v1.0.0 --dry-run # validate without writing muse tag add v1.0.0 --force # overwrite existing tag muse tag list # stable only, semver-descending muse tag list --pre # include pre-release muse tag list --sort created # newest created first muse tag read v1.0.0 # full record muse tag delete v1.0.0 # remove (idempotent) muse tag latest # highest stable tag muse tag latest --pre # highest overall tag muse tag push local v1.0.0 # push one tag muse tag push local v1.0.0 --force # force-overwrite remote tag muse push local dev --tags # push branch + all tags muse config set push.tags true # always include tags on push muse describe # stable tags only muse describe --pre # include RC tags muse describe # describe from branch or commit muse describe --abbrev 12 # 12-char hex suffix muse describe --json # machine-readable ```