docs: correct the version-bump gate and branch-refresh documentation

ADR-0022 and gates.md now state the merge-base baseline, the fail-closed
cases, the PyYAML requirement and the multi-ref push gap (shared with
check-release-needed); the new hook gets its own gates.md group. Both
ADR additions follow each file's amendment format. README and AGENTS.md
now say to discard a refreshed apm.lock.yaml on a feature branch, and
that an .apm/ edit is live only once it is on the remote's main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 10:33:34 +00:00
parent 1ce596cdbe
commit dd0b9233e6
5 changed files with 110 additions and 62 deletions

View File

@@ -77,38 +77,49 @@ presence is no longer in question. `.pre-commit-config.yaml`'s `skill-frontmatte
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.
## Amendment (2026-09-16) — the bump is enforced at push, not only required to exist
## Amendment (2026-09-16): the bump is enforced at push, not only required to exist
**Context.** 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 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.)
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 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.)
**Decision.** `scripts/check-skill-version-bump.sh` runs as a pre-push hook on every push. For each
skill directory under `plugins/*/.apm/skills/` that differs between the pushed ref and its
merge-base with `main`, ignoring `tests/`, the pushed `metadata.version` must be strictly greater
than `main`'s.
`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.
- **Baseline is the merge-base with `main`, 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.
- **"Greater", not "exactly one patch higher".** A second `skill-author` pass on the same branch
that bumps again still passes.
- **`tests/` is excluded.** 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; 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 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`.
**Considered options.** *Declare the field advisory* — cheapest, but concedes the field cannot do
its job. *Drop the field* — rejected by this ADR already, and costlier now. *Check at commit
time against `HEAD`* — rejected for the inflation above.
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 reads versions with
`python3` and PyYAML and fails with a clear message if either is missing.
**Consequences.** The 17 unbumped skills took a patch bump in the commit that added the gate.
Like `check-release-needed`, the gate only fires on a local `git push` through pre-commit: a merge
made with Gitea's merge button runs no local hooks and is not checked. A typo fix in a skill now
costs a version bump; that is the rule working, not noise.
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.
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 the first ref with new commits in a multi-ref push (`git push origin a b`,
`git push --all`): `_pre_push_ns` in pre-commit's `hook_impl.py` returns after that ref, so the
other refs are pushed unchecked.