Seven ADRs described code that no longer exists or behaviour the gates do
not have. Where the wrong text came from main it carries a dated
correction; where this branch introduced it, it is fixed in place, because
main never published it and there is no record to preserve.
Fixed in place, branch-introduced:
- ADR-0021's 2026-09-14 correction asserted apm audit --ci "was never a
drift gate at all". It is one: it replays the install and diffs. The
claim contradicted this branch's own AGENTS.md and gates.md.
- ADR-0015 said unconditionally that no pre-push hook needs the network.
The guarantee holds only once apm install has populated apm_modules/.
- ADR-0014's 2026-09-16 correction said restoring .pre-commit-hooks.yaml
would ship a hook that fails for every consumer, because their checkout
has no lib-boundary-resolver.sh. pre-commit clones the whole hook repo
and skill-size-check.sh resolves the library from BASH_SOURCE, so the
hook would work.
- ADR-0019's "twelve hooks pass under unshare -rn" matched neither HEAD
(8) nor main (14), and stated the offline guarantee unconditionally.
Corrected, inherited from main:
- ADR-0022 and ADR-0013 named skill-frontmatter's pre-commit hook as the
enforcer of mandatory metadata.version. That hook was deleted on this
branch; the check lives in skill-size-check.sh.
- ADR-0022 enumerated the tip rule's carve-outs as a closed list and
described a single merge-base. The gate also exempts a tree-identical
skill and intersects every base from merge-base --all, and emits a third
failure form. 8cfd54f said the documented behaviour did not change; it
did. The gate is correct and is unchanged -- the record was not.
- ADR-0020's Decision still routed description overflow to README.md, its
ADR-0025 amendment pointed the mirrored constants at validate.sh, which
holds none, and its Enforcement table still named the two deleted
validate.sh paths.
- ADR-0015's Status claimed every plugin's plugin.json is pack output;
none exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
203 lines
14 KiB
Markdown
203 lines
14 KiB
Markdown
# Every skill's `metadata.version` is mandatory, not a per-plugin option
|
|
|
|
**Status: accepted (2026-09-07).**
|
|
|
|
## Context
|
|
|
|
`metadata.version` is optional SKILL.md frontmatter (`create.md`'s "Optional frontmatter" list:
|
|
"uncomment and fill in, or remove entirely"). `skill-author`'s own bump logic was written
|
|
conditionally — "with `metadata.version` present, bump the minor version on create... and the
|
|
patch version on improve" — which only makes sense if presence is a real per-skill choice.
|
|
|
|
Adoption never followed a rule; it followed the plugin. Of 39 skills, 12 carry a version:
|
|
|
|
| Plugin | Has it | Total |
|
|
|---|---|---|
|
|
| `core` | 3 | 3 |
|
|
| `gitea` | 6 | 7 |
|
|
| `lint` | 2 | 2 |
|
|
| `git` | 1 | 9 |
|
|
| `bin` | 0 | 11 |
|
|
| `kyberforge` | 0 | 7 |
|
|
|
|
`core`, `gitea` and `lint` are consistent adopters (`gitea-files` the one gap); `bin` and
|
|
`kyberforge` are consistent non-adopters; `git` has one outlier (`git-commits`, versioned for no
|
|
plugin-specific reason found on inspection — no comment, no cross-reference, nothing distinguishing
|
|
it from its eight siblings). Issue #127 raised this as an undocumented split: two house norms
|
|
coexisting with no stated rule for which applies where, the same class of defect as an unstated
|
|
`rtk`/bare-`git` convention (#113) found in the same audit pass.
|
|
|
|
## Decision
|
|
|
|
**Every skill's frontmatter carries `metadata.version`.** It is no longer optional, and no longer a
|
|
per-plugin choice.
|
|
|
|
- **The 27 skills that never carried one are seeded at `1.0.0`**, not `0.1.0`. `0.1.0` is
|
|
`skill-author`'s existing new-skill starting point, chosen for a skill with no revision history to
|
|
its name yet. These 27 have all been through the ADR-0020 retrofit and repeated audit passes
|
|
without ever tracking a version; crediting them with `0.1.0` would understate that, and there is
|
|
no real history to justify seeding higher than a first stable release. `1.0.0` marks "versioned as
|
|
of this retrofit," `0.1.0` keeps meaning "created and never yet revised."
|
|
- **New skills still start at `0.1.0`.** `skill-author`'s create/improve bump convention is
|
|
unchanged; only the presence of the field stops being conditional.
|
|
- **The one outlier in the other direction, `git-commits`, keeps its existing value** (`0.1.3`) —
|
|
it already had real tracked history under the old conditional rule, and this decision does not
|
|
reset skills that were already compliant.
|
|
- **`bin/write-docs`'s top-level `version:` moves into `metadata:`, normalized to `1.0.0`.** It is
|
|
the one skill that carried a version outside the `metadata:` block, which is why the table above
|
|
counts `bin` as 0 — a top-level `version:` is not `metadata.version`, and nothing reads it. #127
|
|
raised it alongside the split because "does a skill carry a version" and "where does it live" are
|
|
the same question. Its value (`1.0`) is not semver and carries no more real history than the 27
|
|
unversioned skills, so it is relocated and reset to the same `1.0.0` seed rather than preserved
|
|
like `git-commits`'s tracked `0.1.3`.
|
|
- **`skill-frontmatter`'s pre-commit hook gains the check.** It already fails a SKILL.md missing
|
|
`name:` or `description:`; a missing `metadata.version` is now the same class of failure, not a
|
|
style nit an audit might or might not catch.
|
|
|
|
> **Correction (2026-09-20): that hook no longer exists.** `skill-frontmatter` was removed and its
|
|
> required-field checks folded into `skill-size-check`. The enforcer is now
|
|
> `scripts/skill-size-check.sh:324-335`, declared under the `skill-size-check` hook at
|
|
> `.pre-commit-config.yaml:237`. It checks presence and three-part-semver shape, on the same
|
|
> `SKILL.md` glob and at the same pre-commit stage, so the decision is unaffected — only the name
|
|
> of the hook that holds it. The same substitution applies to the Consequences section below.
|
|
|
|
## Considered options
|
|
|
|
**Leave it per-plugin, document the split.** This was the initial framing of #127 and is coherent —
|
|
`core`/`gitea`/`lint` keep it, `bin`/`kyberforge` don't, two outliers get normalized to match their
|
|
plugin. Rejected on reconsideration: a rule that says "some plugins track this and some don't" is
|
|
strictly harder to state, audit and onboard against than "every skill does," for a field whose entire
|
|
job is answering "did this change since I last read it" — a question with the same shape everywhere
|
|
it's asked, not one that varies by plugin domain.
|
|
|
|
**Drop the field corpus-wide.** Rejected: `skill-author` already depends on it to decide whether a
|
|
create/improve pass owes a bump, so the 12 skills carrying it are not tracking dead weight — removing
|
|
it discards real revision signal for no gain.
|
|
|
|
## Amendment (2026-09-16): the bump is enforced at push, not only required to exist
|
|
|
|
Making the field mandatory did not make it move. The only thing that bumped it was `skill-author`
|
|
Step 4, so every hand edit and every trim pass skipped the bump: on `docs/simplification-audit`, 17
|
|
of the 40 skill directories that changed against `main` carried the same `metadata.version` as
|
|
`main`, and `gitea` alone sat at six different values. Both validators —
|
|
`scripts/skill-size-check.sh` (the pre-commit hook) and `factory-audit`'s
|
|
`scripts/lib-checks-skill.sh` — checked presence and semver shape, never movement, so the field could not answer the question this ADR gives it — "did this
|
|
change since I last read it". (Simplification audit finding 33.)
|
|
|
|
`scripts/check-skill-version-bump.sh` now runs as a pre-push hook on every push, whatever the
|
|
target branch. It takes its baseline from the merge-base of the pushed commit with `origin/main`
|
|
(local `main` if `origin/main` does not resolve). For each skill directory under
|
|
`plugins/*/.apm/skills/` that differs between the pushed commit and that merge-base, ignoring
|
|
`tests/`, the pushed `metadata.version` must be strictly greater than the version the skill had at
|
|
the merge-base — not the version on `main`'s current tip. (The amendment below reverses that last
|
|
choice: the pushed version must now also exceed `main`'s tip.)
|
|
|
|
The baseline is the merge-base, not the previous commit. Readers only ever see `main` — installs
|
|
resolve against the default branch (ADR-0018) — so one bump per branch is what the field owes
|
|
them. A per-commit check would bump a skill once per commit and inflate the number past meaning.
|
|
The rule is "greater", not "exactly one patch higher", so a second `skill-author` pass on the same
|
|
branch that bumps again still passes. `tests/` is excluded because no agent loads it; a
|
|
fixture-only change does not change the skill. Skills absent from either side are exempt: new,
|
|
renamed and merged skills start fresh under the rules above, and deleted skills have nothing to
|
|
check. Every plugin is covered, `bin` included, and the gate is repo-local — it is not exported
|
|
through `.pre-commit-hooks.yaml`.
|
|
|
|
The gate fails closed rather than passing when it has no trustworthy baseline: when neither
|
|
`origin/main` nor `main` resolves, when the pushed commit shares no merge-base with it, and when
|
|
only local `main` resolves and the pushed commit is that merge-base, since a local `main` the
|
|
pushed commit already contains is no independent record of what shipped. It also fails closed when
|
|
the pushed ref does not resolve to a commit, and when a `SKILL.md` the tree names cannot be read by
|
|
`git show` or parsed by `python3` — a read failure is reported as such, never as a missing version.
|
|
It reads versions with `python3` and PyYAML and fails with a clear message if either is missing.
|
|
|
|
Three alternatives were rejected. Declaring the field advisory is the cheapest, but concedes the
|
|
field cannot do its job. Dropping the field was rejected by this ADR already, and costs more now.
|
|
Checking at commit time against `HEAD` was rejected for the inflation described above.
|
|
|
|
The 17 unbumped skills took a patch bump in the commit that added the gate. A typo fix in a skill
|
|
now costs a version bump; that is the rule working, not noise.
|
|
|
|
**Amended by ADR-0014 (2026-09-16).** `check-release-needed` is retired, so the comparison below
|
|
records the state when this ADR was written, not a hook that still runs. See
|
|
[ADR-0014's amendment](0014-vale-prefilter-ships-from-the-plugin.md#amendment-2026-09-16-the-external-hook-contract-is-retired).
|
|
|
|
The gate differs from `check-release-needed` in when it runs: that hook acts only when pre-commit
|
|
reports a push to `main`, so a manual `pre-commit run --hook-stage pre-push` skips it, while this
|
|
gate runs there too and checks `HEAD`. The two hooks share both known gaps. A merge made with
|
|
Gitea's merge button runs no local hooks, so it is not checked. And pre-commit's pre-push
|
|
integration checks only one ref of a multi-ref push (`git push origin a b`, `git push --all`).
|
|
In pre-commit 4.6.1, `_pre_push_ns` in `hook_impl.py` skips delete lines and returns on the first
|
|
remaining ref whose remote sha is non-zero and present locally; a ref whose remote sha is zero or
|
|
unknown locally is returned only if it has commits that no remote-tracking ref of that remote has.
|
|
Every later ref is pushed unchecked. When the returned ref's unpushed history reaches a root
|
|
commit, pre-commit sets no `PRE_COMMIT_TO_REF` at all, so the gate checks `HEAD`, which is the
|
|
pushed ref only if it is checked out.
|
|
|
|
## Amendment (2026-09-16): the pushed version must also exceed `main`'s tip
|
|
|
|
This reverses the choice above that the baseline is the merge-base "not the version on `main`'s
|
|
current tip". A changed skill's pushed `metadata.version` must now be strictly greater than **both**
|
|
its version at the merge-base and its version at the tip of `origin/main` (local `main` under the
|
|
same fallback, with the same fail-closed rules).
|
|
|
|
The merge-base alone lets two branches ship two different changes under one version. Branches A and
|
|
B both start from a skill at `1.0.0`, change it differently, and bump it to `1.0.1`. A merges. B's
|
|
merge-base is still the `1.0.0` commit, so B passes, and the two `1.0.1` bumps are the same line
|
|
change, so git merges B without a conflict. `main` then carries two different `1.0.1` contents, and
|
|
the field again fails to answer "did this change since I last read it". Checking against the tip as
|
|
well makes B fail until it bumps past `1.0.1`.
|
|
|
|
- **A skill absent at the tip** (deleted on `main` since the branch started) is held to the
|
|
merge-base rule alone. **A skill absent at both** is new and stays exempt.
|
|
- **When `main` has not moved since the merge-base**, the two baselines are the same commit and the
|
|
skill is checked once.
|
|
- **The failure names the baseline it missed**: `(not above merge-base)` or
|
|
`(not above origin/main tip)`, one line per baseline missed.
|
|
|
|
The cost is that a branch behind `main` may have to bump again after another branch lands a bump
|
|
on the same skill. That is the case the rule exists for, and rebasing onto or merging `main` first
|
|
shows the version to beat. The rule reads `origin/main` as last fetched, so a tip that moved since
|
|
the last fetch is not seen until the next one.
|
|
|
|
## Amendment (2026-09-20): two exemptions and a third failure form the rules above never stated
|
|
|
|
The carve-outs enumerated above read as a closed list, and the baseline above reads as a single
|
|
merge-base. `scripts/check-skill-version-bump.sh` as shipped has two further exemptions and emits a
|
|
third failure form. **The gate is right and is not changing; this ADR was behind it.** Its own header
|
|
comments (`:9-93`) have described all three correctly since it shipped.
|
|
|
|
- **The baseline is `git merge-base --all`, not one merge-base.** A criss-cross history — `main`
|
|
merges a branch while that branch merges a commit of `main` — has two merge bases, and which one
|
|
`git merge-base` prints is an implementation detail. The script takes every base (`:154-157`) and
|
|
**intersects** the changed-skill sets across them (`:203-217`): a skill matching any one base is
|
|
already shipped by that base and is exempt, and a skill that does reach the comparison must exceed
|
|
the version at every base it exists at (`:367-376`). Picking one base made the verdict a coin
|
|
flip — an already-merged bump failed the push it should have passed.
|
|
- **A skill whose directory tree object equals the tip's skips the tip comparison.**
|
|
`same_subtree()` (`:288-297`, applied at `:334-337`) compares tree object ids rather than diffing:
|
|
the same tree is the same content, whatever route the history took to it. A branch cut before a
|
|
fix landed on `main` and then cherry-picking that fix has one merge-base, predating the fix, so the
|
|
skill counts as changed against it and reaches the tip comparison carrying exactly the tip's
|
|
version — same content, same version. The merge-base intersection catches that only when some base
|
|
carries the content, which the criss-cross shape gives and a linear one does not. Without the skip
|
|
the only escapes are a spurious bump, leaving `main` carrying two versions of identical content,
|
|
or a rebase the push does not otherwise need.
|
|
- **A third failure form.** The amendment above lists `(not above merge-base)` and
|
|
`(not above origin/main tip)`. When there is more than one base, the merge-base line is
|
|
sha-suffixed — `(not above merge-base <sha>)` (`:372`, against the unsuffixed `:374`) — because
|
|
"which merge-base" is the one question a reader cannot answer from the branch alone.
|
|
|
|
`8cfd54f` recorded that "ADR-0022 is not amended: the documented behaviour does not change". That
|
|
was wrong for the tree-identical case: the `same_subtree` skip makes a push **pass** that this ADR as
|
|
written requires to **fail**, which is documented behaviour changing, not an implementation detail.
|
|
|
|
## Consequences
|
|
|
|
27 SKILL.md files gain `metadata.version: "1.0.0"`, and a 28th — `bin/write-docs` — reaches the same
|
|
value by relocating its top-level `version: "1.0"` into `metadata:`. `skill-author`'s `create.md`
|
|
moves the field from "Optional frontmatter" to the required list, citing this ADR. `skill-author`'s
|
|
own SKILL.md drops the "with `metadata.version` present" conditional in its bump-rule line, since
|
|
presence is no longer in question. `.pre-commit-config.yaml`'s `skill-frontmatter` hook is extended
|
|
to require the field, closing the gap #113 and #118 both named in the same audit pass: a stated rule
|
|
with nothing enforcing it drifts the same way an unstated one does.
|