code muse-tag-guide.md markdown gabriel
# `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

```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 <ref>                 # describe from branch or commit
muse describe --abbrev 12           # 12-char hex suffix
muse describe --json                # machine-readable
```