# Tagsmith > Tagsmith is a Git tag and SemVer release-tag manager for single-target repositories and monorepos. It resolves SemVer versions, creates annotated Git tags, optionally pushes them, and validates existing tags for CI. It does not run deployments, fetch automatically, or read `package.json` to decide versions. This document is the agent-facing manual for Tagsmith. It supports `configVersion: 1` and is intended to be read in full before you advise the user on anything. ## How to use this document Your job is to help the user adopt and operate Tagsmith in their repository. Work through the playbook in order. Stop and ask the user when a real decision is required — never invent target names, paths, channel names, remote names, or branch names from your own assumptions. ### Operating rules 1. Inspect the repo before proposing anything. At minimum: `git rev-parse --show-toplevel`, `git remote`, `git branch --show-current`, `git tag --list`, the contents of `package.json` or `pnpm-workspace.yaml` or `turbo.json` or similar, the contents of `.github/workflows/`, and whether `.tagsmith.jsonc` already exists. 2. Invoke Tagsmith via `npx tagsmith@latest ` so the user does not need a global install. 3. Supply complete flag sets. Pass `--target` and `--channel` explicitly even when they could be inferred from a single-target config. 4. Use `--json` for any read operation so the parsed output is unambiguous. 5. Preview every mutation with `--dry-run --json` first. Show the user the resolved tag, base version, channel, and commit before running the real command. 6. When the user wants to drive the workflow themselves from a terminal, recommend bare `tagsmith` for the interactive action menu or `tagsmith tag` without required flags for the guided planning and review flow. Tagsmith renders the equivalent non-interactive command at the end of an interactive review — reconstruct that into the user's automation if asked. 7. After every meaningful change, summarize what was decided, what was changed, and the next command the user should run. ### Document map - Playbook — onboarding a new repo, choosing channel shape, wiring CI. - Daily operations — cutting stable, cutting and promoting prereleases, recovering from a failed push. - Configuration reference — every field, every constraint, tag pattern grammar. - Command reference — `init`, `targets`, `tag`, `validate` — flags, exit conditions, output shape. - Preconditions and invariants — the contracts the CLI enforces and the agent should uphold. - Error catalogue — error string, cause, fix. - Worked examples — 21 concrete scenarios in detection-cues → questions → config diff → commands → expected output → applicable errors form. --- ## Playbook ### When to use Tagsmith Tagsmith fits when the user wants: - Deterministic, validated Git tags for releases — `v1.2.3` or `app@1.2.3` style. - Channel-aware prereleases — `app@1.2.3-rc.1`, `app@1.2.4-beta.1` — with explicit promotion gates. - A single CLI that works the same in a developer's terminal and in CI. - Per-target version lines in a monorepo so each releasable unit advances independently. Tagsmith does not replace `npm publish`, `cargo publish`, Docker pushes, or cloud deploys. It validates that a tag is what it claims to be, then hands you canonical release facts you can feed into anything downstream. ### Step 1 — repository inspection Before touching anything, gather facts and report them back to the user: - Is this a single-package repo or a monorepo? Look for `package.json` at the root, `pnpm-workspace.yaml`, `turbo.json`, `lerna.json`, `nx.json`, `apps/`, `packages/`, `services/`, `crates/`, `cmd/`, or other multi-target layouts. - What is the default remote and base branch? `git remote -v`, `git symbolic-ref refs/remotes/origin/HEAD` (or fall back to asking). - What tags already exist? `git tag --list` and group them by detected pattern (`v1.2.3`, `pkg@1.2.3`, `release/1.2.3`, etc.). - Are there existing release workflows in `.github/workflows/`? Look for any file that triggers on `push: tags` or runs `npm publish`, `pnpm publish`, `cargo publish`, `docker push`, `gh release create`, or similar. - Does `.tagsmith.jsonc` already exist? If so, read it before proposing changes. Summarize what you found in three or four lines and then ask the user to confirm the shape before generating config. ### Step 2 — channel shape decision Ask the user: - Do you ship preview builds that internal or external users consume — alpha, beta, release candidates — or only stable cuts? - For a monorepo: do all targets release on the same channel cadence, or do some need a heavier ladder than others? Two canonical shapes: **Minimal — stable only with an `rc` gate.** Suitable when previews are short-lived and stable is the only consumer-visible cut. ```jsonc "channels": [ { "name": "rc", "strategy": "prerelease" }, { "name": "stable", "strategy": "stable", "dependsOn": ["rc"] } ] ``` **Full ladder — alpha → beta → rc → stable.** Suitable when the user runs preview channels with their own consumers (internal dogfood, public beta, customer RCs). ```jsonc "channels": [ { "name": "alpha", "strategy": "prerelease" }, { "name": "beta", "strategy": "prerelease", "dependsOn": ["alpha"] }, { "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] }, { "name": "stable", "strategy": "stable", "dependsOn": ["rc"] } ] ``` A channel's `dependsOn` requires that the same base version exists as a tag on the named channel, locally and remotely, before a tag on this channel can be cut. So `stable` with `dependsOn: ["rc"]` forces every stable release to have already passed through `rc`. Mixing is allowed per target. One target can run the full ladder while another sticks with `rc → stable`. ### Step 3 — generate config Run `npx tagsmith@latest init`. The CLI writes `.tagsmith.jsonc` at the repo root with the full `alpha → beta → rc → stable` ladder across three example targets (`web`, `api`, `auth`). Walk the user through pruning channels they do not need and renaming targets to match their actual code layout. If the user prefers, run `npx tagsmith@latest init --dry-run` first to print the template to stdout without writing anything. If `.tagsmith.jsonc` already exists, ask before overwriting. `--force` overwrites; without it the CLI fails fast. After editing the config: ```sh npx tagsmith@latest targets --json ``` `targets --json` validates the parsed config and emits the resolved target structure. If this fails, fix the config before any tag operation. ### Step 4 — first dry-run Pick the lowest-risk target and channel and run a dry-run with full flags: ```sh npx tagsmith@latest tag \ --target \ --channel stable \ --bump patch \ --dry-run \ --json ``` The CLI runs the full preflight (clean working tree, `HEAD == /`, tag does not already exist, dependency gates satisfied) without creating or pushing anything. Show the user the resolved JSON facts. If preflight fails, fix the underlying state. Do not work around it. ### Step 5 — real tag When the user is ready, drop `--dry-run` and add `--push` only if they want to publish immediately: ```sh # local tag only npx tagsmith@latest tag --target --channel stable --bump patch # local tag + push npx tagsmith@latest tag --target --channel stable --bump patch --push ``` Local tag creation is the default mutation. `--push` is required to push. ### Step 6 — CI wiring Once tags are produced reliably, set up GitHub Actions to validate them before any release side effect runs. The standard split is: - A `validate` job that runs `tagsmith validate --tag "$GITHUB_REF_NAME" --github-output` and exposes the parsed facts as job outputs. - A `release` job that depends on `validate`, reads from `needs.validate.outputs.*`, and runs the actual publish, deploy, or upload steps. The release job should never see an invalid tag because Tagsmith fails the validate job first. A complete reference workflow is in the README "GitHub Actions publish workflow" section. Always: - `actions/checkout@v6` with `fetch-depth: 0`. - An explicit `git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*'` step before validation, because Tagsmith never fetches automatically. - Tag trigger glob that matches the project's `tagPattern` (`v*`, `*@*`, etc.). --- ## Daily operations ### Cut a stable release ```sh npx tagsmith@latest tag --target app --channel stable --bump patch --dry-run --json npx tagsmith@latest tag --target app --channel stable --bump patch --push ``` `--bump patch` reads the latest stable for the target and bumps the patch component. `--bump minor` and `--bump major` work the same way. `--version 1.4.0` sets an explicit canonical SemVer instead. ### Cut a release candidate ```sh # start a new rc line from the latest stable + minor bump → 1.5.0-rc.1 npx tagsmith@latest tag --target app --channel rc --bump minor --dry-run --json npx tagsmith@latest tag --target app --channel rc --bump minor # advance the existing rc line → 1.5.0-rc.2 npx tagsmith@latest tag --target app --channel rc --bump prerelease --dry-run --json npx tagsmith@latest tag --target app --channel rc --bump prerelease ``` `--bump prerelease` requires an existing prerelease tag on the same target and channel — it increments the counter on the highest matching line. To start a new prerelease line, use `--bump patch|minor|major` on the prerelease channel; Tagsmith bumps the latest stable (or `initialVersion`) and appends `-.1`. ### Promote a release candidate to stable The stable channel typically depends on `rc`. Tagsmith enforces that the same base version exists as a peer rc tag before allowing the stable cut. ```sh # 1.5.0-rc.3 exists locally and remotely; cut 1.5.0 npx tagsmith@latest tag --target app --channel stable --version 1.5.0 --dry-run --json npx tagsmith@latest tag --target app --channel stable --version 1.5.0 --push ``` Pass `--version` explicitly when promoting to make the intended base version unambiguous. ### Recover from a failed push If `tagsmith tag --push` creates the local tag but the push or post-push verification fails, the local tag is intentionally not rolled back. Two recovery paths: - Investigate the push failure (auth, refspec, branch protection, remote tag race), then push manually: `git push refs/tags/`. Re-run `tagsmith validate --tag ` to confirm the recovered state. - If the tag is wrong, delete it locally (`git tag -d `) and let the user re-run `tagsmith tag` from scratch. Tagsmith never overwrites existing tags. If a malformed tag is on the remote, it must be deleted before a fresh cut on the same version. ### Run validate locally ```sh npx tagsmith@latest validate --tag app@1.5.0 --target app --channel stable --json ``` Validation requires the tag to exist locally and remotely as an annotated tag, peel to the same commit, be reachable from the remote-read base branch tip, and satisfy direct `dependsOn` checks. `--target` and `--channel` are optional assertions; when omitted Tagsmith infers them from the tag name and the config. --- ## Configuration reference The config file is JSONC: comments and trailing commas are allowed. Reserved keys (`__proto__`) and duplicate keys are rejected. Unknown keys at any level are rejected. The default config path is `/.tagsmith.jsonc`. `--config ` selects a different file; relative paths resolve from the repo root, not the current directory. ### Top level | Field | Type | Required | Notes | | --- | --- | --- | --- | | `$schema` | string | optional | Should point at `https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.json` for editor autocomplete. | | `configVersion` | const `1` | yes | Only `1` is accepted. | | `git` | object | yes | Repository-wide Git policy. | | `defaults` | object | yes | Inherited tag pattern, message, and initial version for every target. | | `targets` | object | yes | Map of target name → target config. Must have at least one entry. | ### `git` | Field | Type | Constraint | | --- | --- | --- | | `remote` | string | Configured Git remote name. Safe Git ref characters only; no slashes, whitespace, control chars, or `.lock` suffix. Typically `origin`. | | `baseBranch` | string | Unqualified branch name. Not `HEAD`, not `origin/...`, not `refs/...`. Typically `main` or `release/1.x`. | ### `defaults` | Field | Type | Constraint | | --- | --- | --- | | `tagPattern` | string | See "Tag pattern grammar" below. | | `tagMessage` | string | See "Tag message grammar" below. | | `initialVersion` | stable SemVer | E.g. `0.0.0` or `1.0.0`. No build metadata, no leading `v`. Acts as both bump baseline and minimum managed version. | ### `targets.` Target names match `^[a-z][a-z0-9-]*$` (lowercase, hyphenated, must start with a letter). | Field | Type | Notes | | --- | --- | --- | | `path` | string | Path to the target's source. Resolves from the repo root if relative. Must exist as a directory, stay inside the repo realpath, and have a realpath distinct from every other target. | | `channels` | array | At least one entry. Exactly one channel must have `strategy: "stable"`. | | `tagPattern` | string | Optional override for this target. | | `tagMessage` | string | Optional override for this target. | | `initialVersion` | stable SemVer | Optional override for this target. | ### `channels[]` | Field | Type | Notes | | --- | --- | --- | | `name` | string | Matches `^[a-z][a-z0-9-]*$`. Unique within the target. | | `strategy` | `"stable"` or `"prerelease"` | Exactly one stable channel per target. | | `dependsOn` | string[] | Optional. References sibling channel names in the same target. Self-references and cycles are rejected. | ### Tag pattern grammar `tagPattern` supports `{target}` (zero or one occurrence) and `{version}` (exactly one). Literal characters allowed: lowercase letters, digits, `.`, `_`, `-`, `@`. Rejected: `/`, whitespace, uppercase letters, unsupported placeholders. Rendered tags must also be safe Git tag names: no leading `.` or `-`, no trailing `.`, no `..`, no `.lock` suffix. Recommended patterns: - Single target: `v{version}` → `v1.2.3`. - Monorepo: `{target}@{version}` → `app@1.2.3`. Channel names are encoded in prerelease versions (`1.2.3-rc.1`); there is no `{channel}` placeholder. Patterns where `{version}` touches an alphanumeric or `_` character (other than the recommended `v{version}`) emit a warning in human mode. Multi-target effective patterns must be statically unambiguous — Tagsmith rejects configs where two targets could produce the same rendered tag for different versions. ### Tag message grammar `tagMessage` supports `{target}`, `{version}`, and `{tag}`. Must be printable single-line text. Each target's effective rendered message must contain non-whitespace text. ### SemVer values Stable: `1.2.3`, `0.0.0`, `10.20.30`. Maximum component value is `Number.MAX_SAFE_INTEGER` (9007199254740991). Prerelease: `1.2.3-rc.1`, `1.2.4-pre-prod.1`, `0.1.0-alpha.7`. Prerelease shape is exactly `[, ]` for managed tags. Rejected: `v1.2.3` (put the `v` in `tagPattern` instead), `1.2.3+build.5`, `1.2.4-rc` (no counter), `1.2.4-rc.0` (counter must be ≥ 1), `01.2.3` (leading zero). --- ## Command reference All commands accept the global flags: | Flag | Notes | | --- | --- | | `--config ` | Defaults to `/.tagsmith.jsonc`. Relative paths resolve from the repo root. | | `--verbose` | Human-mode diagnostics only. Incompatible with `--json` and `--github-output`. | | `--help`, `-h` | Show help for the command. | | `--version`, `-v` | Show Tagsmith version. | Attached values like `--target=app` are rejected. Use space-separated values (`--target app`). Only `-h` and `-v` have shorthand aliases. Bare `tagsmith` in a TTY opens an interactive action menu. In non-TTY contexts (CI, scripts, agents) it prints help and exits. ### `tagsmith init` Creates `.tagsmith.jsonc`. | Flag | Notes | | --- | --- | | `--force` | Overwrite an existing config file. | | `--dry-run` | Print the template bytes to stdout, write nothing, skip overwrite checks. Still requires a Git repository. | Exit status: non-zero if the destination exists without `--force`, if the parent directory is missing, or if the write fails. ### `tagsmith targets` Validates the config and target paths, then lists configured targets. | Flag | Notes | | --- | --- | | `--json` | Emit the parsed config shape on stdout (includes `git`, `defaults`, and `targets`). | Does not inspect Git tags, remotes, or base branch refs. ### `tagsmith tag` Resolves a version and creates an annotated local tag at the current `HEAD`. | Flag | Notes | | --- | --- | | `--target ` | Required when the config has multiple targets. | | `--channel ` | Required. | | `--bump ` | One of `major`, `minor`, `patch`, `prerelease`. Mutually exclusive with `--version`. | | `--version ` | Canonical SemVer override. Mutually exclusive with `--bump`. | | `--dry-run` | Resolve and run preflight; skip create and push. | | `--push` | Push the created tag to `git.remote`. | | `--json` | Machine-readable output. | Preflight before mutation: config valid; target paths exist; working tree clean; local tags read; remote tags read; remote base branch tip read; `HEAD == /`; no malformed managed tags; tag does not already exist; version policy holds; direct same-base dependencies satisfied. `--dry-run` runs the same preflight and skips only create and push. `tag --json` output keys: ```json { "target": "app", "channel": "stable", "strategy": "stable", "version": "1.2.3", "baseVersion": "1.2.3", "tag": "app@1.2.3", "tagMessage": "Release app 1.2.3", "commit": "0123456789abcdef0123456789abcdef01234567", "created": true, "pushed": false, "dryRun": false } ``` ### `tagsmith validate` Strictly validates an existing managed tag. | Flag | Notes | | --- | --- | | `--tag ` | Required. The Git tag name to validate. | | `--target ` | Optional assertion. Tagsmith infers the target from the tag name when unambiguous. | | `--channel ` | Optional assertion. Tagsmith infers the channel from the version shape. | | `--json` | Machine-readable output. | | `--github-output` | Write validation facts to `$GITHUB_OUTPUT`. Requires `GITHUB_OUTPUT` to be set. Writes only after full validation succeeds. | Validation: tag exists locally and remotely; both peel to the same commit; commit is reachable from the remote-read base branch tip; `dependsOn` gates satisfied; managed tag namespace is clean (no malformed peers). `validate --json` output keys: ```json { "target": "app", "channel": "stable", "strategy": "stable", "version": "1.2.3", "baseVersion": "1.2.3", "tag": "app@1.2.3", "tagMessage": "Release app 1.2.3", "commit": "0123456789abcdef0123456789abcdef01234567", "remote": "origin", "baseBranch": "main", "valid": true } ``` `validate` does not emit `created`, `pushed`, or `dryRun` — those keys belong to `tag` output only. `validate --github-output` writes single-line records: ``` target=app channel=stable strategy=stable version=1.2.3 baseVersion=1.2.3 tag=app@1.2.3 tagMessage=Release app 1.2.3 commit=0123456789abcdef0123456789abcdef01234567 remote=origin baseBranch=main valid=true ``` For `validate`, `tagMessage` is rendered from the current config. Tagsmith does not read or compare the existing annotated tag's message. --- ## Preconditions and invariants State each of these to the user when they apply to the current operation. - **Annotated tags only.** Tagsmith creates and accepts annotated Git tags. A lightweight tag in the managed namespace fails validation. - **`HEAD` equals `/`.** Before `tag`, the commit at `HEAD` must equal the commit Tagsmith reads from the remote base branch tip. The CLI never fetches; if the local view is stale, the user fetches first. - **Working tree clean.** No staged, unstaged, or untracked changes before `tag`. - **Tag commit reachable from base branch.** `validate` requires the tag's peeled commit to be an ancestor of the remote base branch tip. CI must check out enough history. - **Canonical SemVer.** No leading `v`, no build metadata. Put the `v` in `tagPattern` if needed. - **Prerelease shape.** Managed prerelease versions are `[, ]`. `1.2.3-rc.1` is valid; `1.2.3-rc` and `1.2.3-rc.0` are not. - **`--bump prerelease` continues an existing line.** Start a new prerelease line with `--bump patch|minor|major`; Tagsmith creates `-.1` from the latest stable or `initialVersion`. - **Monotonic versions.** Resolved versions must be greater than the latest tag on the same channel and base. Base versions of prereleases must be greater than the latest stable. - **Annotation parity.** When `--push` runs, Tagsmith re-reads the remote tag and verifies the remote peeled commit matches the local one. If verification fails, the local tag remains in place. - **Channel `dependsOn` gates.** A tag on a channel with `dependsOn: [X]` requires the same base version to exist as a tag on channel `X`, locally and remotely, peeling to the same commit. - **Realpath uniqueness.** Two targets cannot point at the same real directory. - **Pattern disjointness.** Two targets' effective tag patterns must be statically unambiguous. --- ## Error catalogue Group: error string → cause → fix. Strings may include format placeholders; treat them as substring matches. ### Config parse and schema | Error | Cause | Fix | | --- | --- | --- | | `reserved key __proto__ at ` | A `__proto__` key in JSONC. | Rename the key. | | `duplicate key at ` | Same key twice in a single object. | Remove the duplicate. | | `malformed JSONC ()` | JSONC parser error. | Fix syntax — usually a missing comma, bracket, or stray comment marker. | | `: : ` | Zod schema violation. | Fix the field to match the expected shape. The message identifies the constraint. | ### Config validation | Error | Cause | Fix | | --- | --- | --- | | `targets must contain at least one target` | Empty `targets` object. | Add at least one target. | | `git.remote must be a safe configured remote name without whitespace or slash` | Invalid remote string. | Use a configured remote name like `origin`. | | `git.baseBranch must be an unqualified branch name` | Branch is `HEAD`, prefixed with `origin/` or `refs/`, or has invalid chars. | Use the bare branch name (`main`, `release/1.x`). | | `targets. must match \`^[a-z][a-z0-9-]*$\`` | Target name has uppercase, underscores, or starts with a digit. | Rename. | | `targets..channels must contain exactly one stable channel` | Zero or multiple `strategy: "stable"`. | Pick one stable channel per target. | | `targets..channels contains duplicate channel ` | Two channels share a name. | Rename. | | `targets..channels..dependsOn may not depend on self` | `dependsOn` lists own name. | Remove the self-reference. | | `targets..channels..dependsOn references missing channel ` | `dependsOn` names a channel that does not exist on this target. | Add the channel or remove the dependency. | | `targets..channels dependency cycle is invalid` | Channel `dependsOn` chain forms a cycle. | Break the cycle. | | `targets..tagPattern renders an unsafe Git tag name` | Rendered pattern violates Git refname rules. | Adjust the pattern. | | `targets..tagMessage must be non-empty after interpolation` | Rendered message is empty or whitespace only. | Provide non-whitespace text. | | `targets and have ambiguous effective tagPattern ` | Two targets could produce the same rendered tag. | Differentiate the patterns or add target-specific overrides. | | ` must be canonical stable SemVer without build metadata or leading v` | `initialVersion` is malformed. | Use a pure stable SemVer like `0.0.0`. | | ` contains unsupported placeholder` | `tagPattern` or `tagMessage` uses an unsupported `{...}` placeholder. | Use only the documented placeholders. | | ` requires exactly one {version}` | `tagPattern` is missing `{version}` or has more than one. | Include exactly one. | | ` may contain {target} at most once` | Multiple `{target}` placeholders. | Reduce to at most one. | | ` tagPattern contains unsafe characters` | Pattern has `/`, whitespace, uppercase, or other rejected characters. | Use only `[a-z0-9._@-]`. | | ` must be printable single-line text` | `tagMessage` has control chars or line separators. | Use plain text. | ### Target paths | Error | Fix | | --- | --- | | `targets..path must exist` | Create the directory or correct the path. | | `targets..path must be a directory` | Replace with a directory. | | `targets..path must resolve inside the Git repository` | Move the path inside the repo or remove the target. | | `targets..path resolves to the same real directory as targets..path` | Give the target a distinct path or remove the duplicate. | ### Git operations | Error | Cause | Fix | | --- | --- | --- | | `Git repository not found from ` | Tagsmith was invoked outside a Git repo. | Run from inside the repo. | | `git.remote is not configured in ` | The configured remote is missing. | `git remote add ` or change `git.remote` in the config. | | `working tree must be clean before tagging` | Staged, unstaged, or untracked changes present. | Commit, stash, or clean before retrying. | | `failed to read current HEAD` | `git rev-parse HEAD` failed. | Investigate repo state — usually an unborn branch or corrupted ref. | | `failed to read remote base branch /` | `git ls-remote` failed. | Check network, auth, and that the branch exists on the remote. | | `failed to read local tags` / `failed to read remote tags from ` | Git ref read failed. | Investigate per the underlying Git error. | | `cannot prove tag commit is reachable from / with local history. Fetch enough history and retry: git fetch --tags` | Reachability check failed because local history is shallow or missing. | Run the suggested `git fetch` and retry. | | `failed to create annotated local tag ` | `git tag -a` failed. | Inspect the underlying Git error; usually permission or ref collision. | | `failed to push tag to ` | Push rejected. | Investigate refspec, auth, branch protection, or remote race. | | `HEAD must equal / () before tagging` | Local `HEAD` differs from the remote base branch tip. | Fetch and align. Do not force. | ### Release logic | Error | Cause | Fix | | --- | --- | --- | | `tag requires exactly one of --bump or --version` | Both or neither supplied. | Pass one. | | `tag requires --channel` | Missing `--channel`. | Pass it. | | `tag requires --target when config has multiple targets` | Ambiguous target. | Pass `--target`. | | `unknown target ` | `--target` not in config. | Use a configured target name. | | `unknown channel for target ` | `--channel` not in this target. | Use a configured channel name. | | `invalid --bump ; expected major, minor, patch, or prerelease` | Bad bump value. | Use one of the four. | | ` must be canonical SemVer without build metadata or leading v` | `--version` is malformed. | Pass a pure SemVer like `1.2.3` or `1.2.3-rc.1`. | | ` must be a stable SemVer for channel ` | `--version` has prerelease on a stable channel. | Pass a stable SemVer. | | ` must match channel ` | Prerelease channel name does not match the version's prerelease. | Pass a version like `1.2.3-.N`. | | `stable channel rejects --bump prerelease` | `--bump prerelease` on a stable channel. | Use `major`, `minor`, `patch`, or `--version`. | | `Cannot bump prerelease for : no existing prerelease tag found. Use --bump major, --bump minor, --bump patch, or --version to start a prerelease line.` | First prerelease on a channel cannot be a `--bump prerelease`. | Use a base bump to start the line. | | `tag already exists locally or remotely` | Duplicate. | Pick a different version. | | `malformed managed tag : lightweight tag is not allowed` | A lightweight tag matches the pattern. | Recreate as annotated, or delete. | | `malformed managed tag : is invalid` | Managed namespace contains a tag that fails parsing. | Repair or delete the offending tag. | | `malformed managed tag : version is below initialVersion ` | Existing tag is below `initialVersion`. | Either bump `initialVersion` to cover the existing tag or delete the tag. | | `tag does not match target ` | Asserted target does not match the tag's pattern. | Drop or correct `--target`. | | `tag must contain canonical SemVer without build metadata` | Validation found a malformed version inside the managed tag. | Repair the tag. | | `tag does not match any configured target` | Tag does not match any target's effective pattern. | Drop or correct `--tag`. | | `tag matches multiple targets` | Tag matches more than one target's effective pattern; the namespace is ambiguous. | Pass `--target` to disambiguate, or adjust `tagPattern` so targets are statically disjoint. | | `tag must exist locally` / `must exist remotely` / `must exist locally and remotely` | Validation found the tag missing on one side. | Push or fetch as appropriate. | | `--channel does not match inferred channel ` | `--channel` assertion mismatches the version shape. | Drop the assertion or correct it. | | `resolved requires dependency tag for at ` | `dependsOn` gate unsatisfied. | Cut the dependency tag first. | | `dependency tag must exist locally and remotely` | Dependency tag missing on one side. | Push or fetch. | | `dependency tag must peel to ` | Dependency tag points at a different commit than the resolved tag. | Realign or cut a fresh dependency tag. | ### Init | Error | Fix | | --- | --- | | `destination parent directory is not a directory: ` | Move config to a valid parent or fix the path. | | `destination parent directory does not exist: ` | Create the directory or fix the path. | | `destination already exists: ` | Use `--force` to overwrite, or write to a different path. | ### GitHub output | Error | Fix | | --- | --- | | `GitHub output key must be an identifier.` | Internal contract; should not appear in normal use. | | `GitHub output value for must be single-line printable text.` | Internal contract; same. | ### Push verification | Error | Cause | Fix | | --- | --- | --- | | `local tag exists but was not pushed: ` | Push failed after local tag was created. | Inspect underlying push error; manual `git push refs/tags/` may recover. | | `push verification failed for : . Local tag remains.` | Post-push remote re-read failed. | Investigate remote state. | | `push verification failed for : remote tag does not peel to . Local tag remains.` | Remote tag points at a different commit (race or remote rewrite). | Investigate and reconcile. | ### CLI parsing | Error | Fix | | --- | --- | | `unexpected argument ` | Remove the extraneous token. | | `unknown option ` | Use a documented flag. | | `option requires a value` | Supply the value as the next token. | | ` is incompatible with ` | Pick one of `--json` or `--github-output`. | | `--verbose is incompatible with ` | Drop `--verbose` when emitting machine output. | | `option does not support attached values. Use .` | Use space-separated values. | ### Command requirements | Error | Fix | | --- | --- | | `validate requires --tag` | Pass `--tag`. | | `validate --github-output requires GITHUB_OUTPUT` | Run inside GitHub Actions, or set `GITHUB_OUTPUT` to a writable path. | --- ## Worked examples Each example uses the same six-part shape: 1. **Detection cues** — how to recognize the scenario. 2. **Questions for the user** — decisions the user must make. 3. **Config diff or fragment** — relevant slice of `.tagsmith.jsonc`. 4. **Commands** — what to run, with full flag sets. 5. **Expected output** — JSON or `--github-output` keys the user should see. 6. **Applicable errors** — entries from the catalogue that may fire. ### Setup #### E1 — Single-package greenfield, stable only 1. **Detection cues:** one `package.json` at repo root; no `apps/`, `packages/`, `services/` layout; no `.tagsmith.jsonc`; existing tags either absent or in `vX.Y.Z` form. 2. **Questions:** Use `v{version}` tags? Stable channel only, or add an `rc` gate? Default branch is `main` and remote is `origin`? 3. **Config:** ```jsonc { "$schema": "https://raw.githubusercontent.com/sadiksaifi/tagsmith/refs/heads/main/schema/v1.json", "configVersion": 1, "git": { "remote": "origin", "baseBranch": "main" }, "defaults": { "tagPattern": "v{version}", "tagMessage": "Release {version}", "initialVersion": "0.0.0" }, "targets": { "app": { "path": ".", "channels": [ { "name": "rc", "strategy": "prerelease" }, { "name": "stable", "strategy": "stable", "dependsOn": ["rc"] } ] } } } ``` 4. **Commands:** ```sh npx tagsmith@latest targets --json npx tagsmith@latest tag --target app --channel rc --bump minor --dry-run --json npx tagsmith@latest tag --target app --channel rc --bump minor ``` 5. **Expected output:** `targets --json` returns the parsed config; `tag --dry-run --json` returns `version: "0.1.0-rc.1"`, `tag: "v0.1.0-rc.1"`, `created: false`, `dryRun: true`. 6. **Applicable errors:** `targets..path must exist`, `tag already exists locally or remotely`, `HEAD must equal /`. #### E2 — Single-package greenfield, full preview ladder 1. **Detection cues:** same as E1 but user explicitly wants alpha/beta/rc preview channels for partner builds or internal dogfood. 2. **Questions:** Confirm consumers of each preview channel exist. Default branch and remote? 3. **Config:** as E1 but channels: ```jsonc "channels": [ { "name": "alpha", "strategy": "prerelease" }, { "name": "beta", "strategy": "prerelease", "dependsOn": ["alpha"] }, { "name": "rc", "strategy": "prerelease", "dependsOn": ["beta"] }, { "name": "stable", "strategy": "stable", "dependsOn": ["rc"] } ] ``` 4. **Commands:** ```sh npx tagsmith@latest tag --target app --channel alpha --bump minor --dry-run --json npx tagsmith@latest tag --target app --channel alpha --bump minor ``` Promotion through the ladder uses `--bump prerelease` on the same channel for further iterations, then a fresh `--bump` on the next channel up. 5. **Expected output:** first run yields `version: "0.1.0-alpha.1"`. 6. **Applicable errors:** `Cannot bump prerelease for : no existing prerelease tag found` if the user tries `--bump prerelease` on a fresh channel; `resolved requires dependency tag for ` when skipping ladder rungs. #### E3 — Greenfield monorepo 1. **Detection cues:** `apps/`, `packages/`, `services/`, `crates/`, or workspace declarations in `package.json`, `pnpm-workspace.yaml`, `turbo.json`, `Cargo.toml`, etc. 2. **Questions:** Which directories are independently releasable? Do all targets share the same channel ladder? 3. **Config:** ```jsonc "defaults": { "tagPattern": "{target}@{version}", "tagMessage": "Release {target} {version}", "initialVersion": "0.0.0" }, "targets": { "web": { "path": "apps/web", "channels": [/* ladder */] }, "api": { "path": "apps/api", "channels": [/* ladder */] }, "auth": { "path": "packages/auth", "channels": [/* ladder */] } } ``` 4. **Commands:** ```sh npx tagsmith@latest targets --json npx tagsmith@latest tag --target web --channel stable --bump patch --dry-run --json ``` 5. **Expected output:** `targets --json` shows three targets; `tag` resolves to `web@`. 6. **Applicable errors:** `targets and have ambiguous effective tagPattern`, `targets..path resolves to the same real directory as targets..path`. #### E4 — Existing repo with manual `v1.2.3` tags 1. **Detection cues:** `git tag --list` shows tags like `v1.0.0`, `v1.1.0`, `v1.2.0` and no `.tagsmith.jsonc`. 2. **Questions:** Should the existing `vX.Y.Z` series be the managed namespace, or should Tagsmith start a fresh pattern? What is the highest existing version? 3. **Config:** as E1; set `initialVersion` to the highest existing managed version so older manual tags are not flagged as malformed managed peers. 4. **Commands:** ```sh # confirm Tagsmith parses the existing tags correctly npx tagsmith@latest tag --target app --channel stable --bump patch --dry-run --json ``` 5. **Expected output:** `baseVersion` reflects the existing latest stable; `version` is the next patch. 6. **Applicable errors:** `malformed managed tag : lightweight tag is not allowed` if old tags are lightweight; `malformed managed tag : version is below initialVersion` if `initialVersion` is set above some tags. #### E5 — Existing monorepo with mixed tag schemes 1. **Detection cues:** tags like `web-1.0.0`, `api@2.1.0`, `release/auth-0.5.0` — inconsistent across packages. 2. **Questions:** Pick one canonical pattern (recommend `{target}@{version}`). Decide how each old tag maps to a target. Identify highest existing version per target. 3. **Config:** monorepo as E3. Optionally set per-target `tagPattern` overrides if a single uniform pattern would orphan too many old tags; prefer to align everything to the canonical form going forward. 4. **Commands:** ```sh npx tagsmith@latest targets --json # for each target npx tagsmith@latest tag --target --channel stable --bump patch --dry-run --json ``` 5. **Expected output:** dry-runs succeed for every target; resolved tags follow the canonical pattern. 6. **Applicable errors:** `targets and have ambiguous effective tagPattern` if per-target overrides overlap; `malformed managed tag` on legacy tags that match the new pattern but fail SemVer rules. ### Daily operations #### E6 — Cut first stable release on a fresh channel 1. **Detection cues:** no stable tag exists for the target. 2. **Questions:** Confirm starting version. Default is `0.1.0` from `--bump minor` against `initialVersion: 0.0.0`, or `0.0.1` from `--bump patch`. 3. **Config diff:** none — uses existing config. 4. **Commands:** ```sh npx tagsmith@latest tag --target app --channel stable --bump minor --dry-run --json npx tagsmith@latest tag --target app --channel stable --bump minor --push ``` 5. **Expected output:** `version: "0.1.0"`, `tag: "app@0.1.0"`, `created: true`, `pushed: true`. 6. **Applicable errors:** `resolved requires dependency tag for at ` if the stable channel has `dependsOn` and no peer exists yet — fix by cutting the dependency channel first. #### E7 — Cut rc.1 → rc.2 → promote to stable 1. **Detection cues:** user wants to release `1.5.0` through an rc gate. 2. **Questions:** Confirm minor vs patch base bump. Confirm rc number expectations. 3. **Config diff:** none. 4. **Commands:** ```sh # rc.1 npx tagsmith@latest tag --target app --channel rc --bump minor --dry-run --json npx tagsmith@latest tag --target app --channel rc --bump minor --push # rc.2 after fixes npx tagsmith@latest tag --target app --channel rc --bump prerelease --dry-run --json npx tagsmith@latest tag --target app --channel rc --bump prerelease --push # promote npx tagsmith@latest tag --target app --channel stable --version 1.5.0 --dry-run --json npx tagsmith@latest tag --target app --channel stable --version 1.5.0 --push ``` 5. **Expected output:** `1.5.0-rc.1` → `1.5.0-rc.2` → `1.5.0`. Each `--dry-run` shows the resolved tag and commit. 6. **Applicable errors:** ` must be greater than latest ` on rc bumps if version policy slips; `resolved requires dependency tag for stable at 1.5.0` if the rc on `1.5.0` does not exist. #### E8 — Hotfix patch on stable while an rc line is open 1. **Detection cues:** latest stable is `1.4.2`, latest rc is `1.5.0-rc.3`; user needs `1.4.3` urgently. 2. **Questions:** Confirm the hotfix branch already aligns with `/` and the open rc work is on a different commit. 3. **Config diff:** none. 4. **Commands:** ```sh npx tagsmith@latest tag --target app --channel stable --version 1.4.3 --dry-run --json npx tagsmith@latest tag --target app --channel stable --version 1.4.3 --push ``` 5. **Expected output:** `version: "1.4.3"`, `baseVersion: "1.4.3"`, `tag: "app@1.4.3"`. 6. **Applicable errors:** `resolved requires dependency tag for rc at 1.4.3` if the stable channel has `dependsOn: ["rc"]` and no `1.4.3-rc.*` exists — the user must cut an rc on the hotfix base first, or drop the `dependsOn` for hotfixes. #### E9 — Recover from a failed push 1. **Detection cues:** previous `tag --push` exited non-zero; local tag is present, remote tag absent or mismatched. 2. **Questions:** What did the push failure say? Auth, branch protection, refspec, race? 3. **Config diff:** none. 4. **Commands:** ```sh # confirm local tag git show # retry the push directly git push refs/tags/ # validate npx tagsmith@latest validate --tag --target --channel --json ``` If the local tag is wrong: `git tag -d ` and re-run `tagsmith tag` from scratch. 5. **Expected output:** `validate --json` returns `valid: true` once the remote is repaired. 6. **Applicable errors:** `local tag exists but was not pushed`, `push verification failed for : remote tag does not peel to `, `tag must exist remotely`. ### CI integration #### E10 — Build `publish.yml` from scratch 1. **Detection cues:** `.github/workflows/` has no release workflow yet. 2. **Questions:** Trigger glob for tags? What runs after validation — npm publish, Docker push, GitHub release? 3. **Config diff:** none in `.tagsmith.jsonc`; the change is in `.github/workflows/publish.yml`. 4. **Commands:** none locally; CI runs on tag push. 5. **Expected output:** `validate` job emits the documented `--github-output` keys; downstream job reads `needs.validate.outputs.*`. 6. **Applicable errors:** `cannot prove tag commit is reachable from /` (missing `fetch-depth: 0` or missing explicit fetch step); `validate requires --tag` (workflow forgot `--tag "$GITHUB_REF_NAME"`). Reference workflow shape: ```yaml name: Publish on: push: tags: ["*"] permissions: { contents: read } concurrency: group: publish-${{ github.ref }} cancel-in-progress: false jobs: validate: runs-on: ubuntu-24.04 outputs: tag: ${{ steps.tagsmith.outputs.tag }} commit: ${{ steps.tagsmith.outputs.commit }} target: ${{ steps.tagsmith.outputs.target }} channel: ${{ steps.tagsmith.outputs.channel }} version: ${{ steps.tagsmith.outputs.version }} steps: - uses: actions/checkout@v6 with: { fetch-depth: 0 } - run: git fetch --force --tags origin '+refs/heads/*:refs/remotes/origin/*' - id: tagsmith run: npx tagsmith@latest validate --tag "$GITHUB_REF_NAME" --github-output release: needs: validate runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 with: ref: ${{ needs.validate.outputs.commit }} fetch-depth: 0 - run: echo "release ${{ needs.validate.outputs.tag }}" ``` #### E11 — Audit and fix an existing `publish.yml` 1. **Detection cues:** existing workflow uses Tagsmith but is failing or producing wrong outputs. 2. **Questions:** What error does the workflow log show? 3. **Config diff:** none. 4. **Commands:** none locally; edit the YAML. 5. **Common breakage modes and fixes:** - Missing `fetch-depth: 0` → add to `actions/checkout`. - No explicit `git fetch` for tags and branches → add the fetch step. - Release side effects in the same job as `validate` → split into two jobs; release `needs: validate`. - Reading `steps.tagsmith.outputs.*` from a downstream job → use `needs.validate.outputs.*` instead. - Missing `outputs:` declaration on the `validate` job → add the keys you want to expose. - Trigger glob mismatch with `tagPattern` → align the glob. 6. **Applicable errors:** `cannot prove tag commit is reachable`, `validate --github-output requires GITHUB_OUTPUT`. #### E12 — Migrate a tag-driven npm publish onto `tagsmith validate` 1. **Detection cues:** existing `publish.yml` runs `npm publish` directly on tag push with no validation gate. 2. **Questions:** Is the existing tag scheme compatible with Tagsmith's `tagPattern`? Are tags currently lightweight or annotated? 3. **Config diff:** add `.tagsmith.jsonc` per E1 or E3. 4. **Commands locally:** ```sh npx tagsmith@latest validate --tag --json ``` to confirm Tagsmith parses existing tags. 5. **Expected output:** validation passes for healthy tags; surface failures for lightweight or malformed ones. 6. **Applicable errors:** `malformed managed tag : lightweight tag is not allowed` — convert to annotated by deleting and recreating; `tag does not match any configured target` — tag does not match any configured `tagPattern`, adjust pattern or rename. ### Error-driven recovery #### E13 — `tag requires --target when config has multiple targets` 1. **Detection cues:** monorepo config; agent omitted `--target`. 2. **Questions:** Which target is this tag for? 3. **Fix:** add `--target `. 4. **Commands:** ```sh npx tagsmith@latest tag --target --channel stable --bump patch --dry-run --json ``` 5. **Note:** in non-TTY contexts (CI, agents) Tagsmith fails fast with this error. In TTY contexts the CLI prompts for a target; the agent should still pass it explicitly so the operation is reproducible. #### E14 — `HEAD must equal / () before tagging` 1. **Detection cues:** preflight failure before `tag` mutates anything. 2. **Questions:** Is the user on a feature branch, ahead of `/`, or are remote refs stale? 3. **Fix:** ```sh git fetch git switch git reset --hard / # only if user explicitly wants to drop local commits # or rebase / merge as appropriate, then retry npx tagsmith@latest tag --target --channel --bump patch --dry-run --json ``` 4. **Note:** never recommend `git push --force` to resolve this. The whole point of the check is that the tag commit is what is on the canonical branch tip. #### E15 — `Cannot bump prerelease for : no existing prerelease tag found` 1. **Detection cues:** `--bump prerelease` on a channel that has no prior tag. 2. **Fix:** use a base bump to start the line. 3. **Commands:** ```sh npx tagsmith@latest tag --target app --channel rc --bump minor --dry-run --json ``` #### E16 — `cannot prove tag commit is reachable from /` 1. **Detection cues:** CI failure during `validate`; local clone too shallow. 2. **Fix in CI:** - `actions/checkout@v6` with `fetch-depth: 0`. - Explicit step: `git fetch --force --tags '+refs/heads/*:refs/remotes//*'` before `validate`. 3. **Local recovery:** ```sh git fetch --tags ``` #### E17 — `targets and have ambiguous effective tagPattern ` 1. **Detection cues:** config validation fails on `targets`, `tag`, or `validate`. 2. **Fix:** disambiguate the patterns. Common options: - Distinct fixed prefixes per target (e.g. `web@{version}` and `api@{version}` — disjoint because the prefix differs). - Distinct fixed suffixes per target. - Different separator characters. 3. **Note:** two targets without `{target}` and the same literal pattern are always ambiguous. Either add `{target}` to the pattern or give each target an override that differs in fixed text. #### E18 — `malformed managed tag : lightweight tag is not allowed` 1. **Detection cues:** a tag matching the managed pattern exists but is lightweight. 2. **Fix:** ```sh git tag -d git push :refs/tags/ # recreate as annotated via tagsmith npx tagsmith@latest tag --target --channel --version --push ``` 3. **Note:** Tagsmith creates only annotated tags. Any lightweight tag in the managed namespace must be replaced before further operations succeed. #### E19 — `destination already exists` on `init` 1. **Detection cues:** `.tagsmith.jsonc` is already present. 2. **Questions:** Does the user want to overwrite the existing config, or merge changes manually? 3. **Fix:** - To overwrite: `npx tagsmith@latest init --force`. - To preview the canonical template without writing: `npx tagsmith@latest init --dry-run` and diff manually. ### User-driven interactive paths #### E20 — User wants to drive setup themselves in a terminal 1. **Detection cues:** user prefers to confirm each prompt visually, or is learning Tagsmith hands-on. 2. **Recommend:** - `tagsmith` (bare) for the action menu: pick `init`, `targets`, `tag`, or `validate` from a Clack-style selector. - `tagsmith init` and follow the create/overwrite review. - `tagsmith tag` without flags: the CLI prompts for target, channel, and version, runs preflight, shows a review screen with all release facts, asks for confirmation, then optionally pushes. 3. **Agent role:** describe what each prompt is asking before the user runs the command; stand by to interpret review output or errors. 4. **Note:** the interactive flow exists only in a TTY. In non-TTY contexts (CI, scripts, the agent itself) Tagsmith requires complete flag sets and never prompts. #### E21 — Reconstruct the equivalent non-interactive command from an interactive review 1. **Detection cues:** the user describes what their terminal showed during an interactive `tag` review and wants to scripts the same operation. 2. **Recognition:** at the bottom of the interactive review, Tagsmith prints `Equivalent command: tagsmith tag --target --channel --version [--push]`. The user can also copy the rendered shell command directly. 3. **Agent role:** take the captured equivalent command, parameterize any literal values that should be variables in CI, and place it inside the user's workflow or release script. Verify by running with `--dry-run --json` first. 4. **Note:** the equivalent command is deterministic and reproduces the exact mutation the user confirmed. --- ## Document version - Document: Tagsmith llms.txt, revision 1. - Supports: Tagsmith `configVersion: 1`. - Source of truth: .