gabriel / muse public
muse-tag-guide.md markdown
567 lines 12.2 KB
Raw
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901 docs: architectural plan for unified-store snapshot-content… Sonnet 5 9 days ago

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<N>        v1.0.0-rc1, v1.0.0-rc14
alpha        v1.0.0-alpha
alpha.<N>    v1.0.0-alpha.1, v1.0.0-alpha.2
beta         v1.0.0-beta
beta.<N>     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

# 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 <ref>                 # describe from branch or commit
muse describe --abbrev 12           # 12-char hex suffix
muse describe --json                # machine-readable
File History 1 commit
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901 docs: architectural plan for unified-store snapshot-content… Sonnet 5 9 days ago