Appearance
Versioning a skills package
How to assign SemVer versions to a library of skills and tools, and how this repo automates it. SemVer was designed for code APIs, so "what is a breaking change in a pile of Markdown?" is the confusing part. The unlock: version the contract a consumer actually depends on, not the prose.
This guidance is reusable for any skills or content library, not just dev-skills.
Version the contract, not the prose
A consumer depends on three things, and only these form the public surface:
- The tool surface (machine contract): CLI command names, flags, output format, and exit codes that a consumer's scripts or CI invoke.
- Skill and topic file paths (reference contract): the directory and file names consumers link to, symlink, or read via
dev-skills path(e.g.git/git.md). - Check strictness (CI contract): how strict the lint, spec, and eval tools are, since a consumer's CI runs them and can start failing.
Guidance prose is advisory: an agent reads it, but it does not break a build. So most content edits ride at patch or minor, and the surfaces above are what need strict SemVer.
The rubric
- PATCH: no contract change. Typo and wording fixes, clarifications, formatting, internal script refactors with identical behavior.
- MINOR: additive and backward-compatible. A new skill or topic, a new subcommand or flag, expanded guidance, or a new check that only warns.
- MAJOR: anything that can break a consumer with no change on their side. Removing or renaming a skill, topic, or file path (breaks links, symlinks, and
dev-skills path); removing or renaming a CLI subcommand or flag, or changing output a script parses; or tightening a check so previously-passing CI fails.
Edge case worth a CHANGELOG note even when it is not a major: reversing an existing guideline ("always X" becomes "never X"). It does not break a build, but it changes how agents behave, so flag it.
How this repo automates the bump
Two constraints shape the automation: do not assume Conventional Commit messages, and stay fully automated (the only action is merging a change, never a separate release step). That rules out release-please and semantic-release (both infer the level from commit types) and changesets (needs an authored file per change) as normally used.
Instead, the level is decided in two stages on each merge to main:
A script computes a baseline from the structural diff. The mechanism is a committed snapshot of the public surface,
scripts/surface.json: the CLI command tree (dumped from the fractal command tree) plus every skill and topic.mdpath.scripts/version-bump.jsdiffs the live surface against the snapshot: a removed or renamed command or path is major, a new one is minor, and anything else (edits to existing files) is patch. This is deterministic and covers adds, removes, renames, and ordinary edits with zero judgment.A human raises the level in PR review for the cases a script cannot compute: a change inside an edited file that secretly raises severity, namely a check getting stricter (previously-passing CI would now fail) or a guideline reversed. Add a
Release-Level:trailer to the merge commit and write the matchingmigrations/<version>.mdso consumers adopt it with the work already done:Tighten the unused-export rule Release-Level: majorThe workflow takes the higher of script and trailer, so the trailer may only raise the level, never lower it.
scripts/release-level.shresolves this. The key and value both match in any case, surrounding whitespace is tolerated, and the last trailer wins. An unrecognized value fails the release rather than silently publishing the structural level, and internal whitespace is never repaired into a valid level.
Why this split: a removed path or renamed command is a fact, so a script decides it. "This lint rule got stricter" is not computable from a text diff, so judgment is unavoidable there. That judgment belongs to the reviewer who made the change and already knows it is breaking, not to an agent reconstructing it from a diff in CI.
.github/workflows/version-on-merge.yaml runs the script stage, bumps package.json, advances scripts/surface.json, writes CHANGELOG.md, publishes the package, and pushes the v<version> tag, then lands the same bump on main through an auto-merging release PR (opened with a GitHub App token so it clears the protected-branch Tests gate instead of pushing directly). Auto-publishing every merge produces more versions, but each consumer still gates adoption via its own weekly Dependabot PR, so it does not spam consumers.
Every major ships a migration
For consumers to adopt a major automatically, each breaking change must ship a migration that says how to adapt. Because every consumer already runs a coding agent, the form is agent-readable migration instructions: a per-major upgrade guide at migrations/<version>.md listing the rename map (old path to new path, old subcommand to new), the find-and-replace edits, and how to resolve any newly strict check in the consumer's own code. See migrations/README.md for the format.
The release process gains one rule: a breaking change must add migration instructions. migrations/migrations.spec.md asserts every MAJOR in CHANGELOG.md has a matching file, so a breaking change cannot ship without an upgrade path. The quality bar is that the instructions are clear enough for an agent to execute unattended.
Verifying the rules
The rubric splits by how enforceable each half is:
Structural contract (deterministically enforceable). "A skill or topic path, or a CLI command or flag, was removed or renamed" is computable. The surface snapshot and diff above do double duty: they compute the baseline level and gate the release (a removed or renamed surface forces at least a major). Because the bump level is derived from this same diff, a structural removal cannot be under-rated. This catches the breakages that actually hurt consumers: dead symlinks, broken
dev-skills path, broken script references.Behavioral contract (not deterministically enforceable). "A check quietly got stricter" or "a guideline flipped" moves no file and no command, so a structural diff cannot see it. That is exactly why stage two exists: the reviewer who made the change already knows it is breaking, and records that judgment as a
Release-Level:trailer the release honors. An optional LLM-judged*.spec.mdcan additionally warn that the final bump matches the rubric, but it asserts a judgment, not a proof.
packages-versioning.spec.md (co-located with this doc) is that advisory check: it asserts this doc, the rubric, and the automation stay in sync as they change.
See also
- Publish to GitHub Packages: the publish workflow this tags into.
- Use dev-skills in a repo: the consumer side that adopts these versions.