The release lane composes an annotated tag message from the CHANGELOG section and writes it with git tag -a <tag> -F <message-file>. Git's default cleanup mode for -F is strip, which treats every #-prefixed line as a comment and deletes it.
Every Markdown heading in a composed tag message is therefore silently dropped: the ## [X.Y.Z] — DATE section heading and every ### Added / ### Fixed / ### Performance grouping.
This has already happened, and cannot be undone
Verified against this repository's real, published v2.8.13:
$ git tag -l -n999 v2.8.13
v2.8.13 Host-aware runtime vocabulary, plus dual-host audit-trail hardening.
- Audit-log lines in `gate-events.log` now record the writing host
...
No ## [2.8.13] heading. No ### Added. The bullet bodies survived; every heading that gave them structure did not.
Under the immutable-tag doctrine (ADR-0024, SKILL.md's "Recovering from a bad release"), published tags are not retargeted or rewritten. The already-published tags stay as they are. This issue is about making future tags correct, not repairing history.
Why nothing caught it
The release skill's own guard is notes_heading_matches, which checks that the notes file's first heading matches the tag. It runs against the composed file before git tag consumes it, so it validates the input and never observes that git discarded the heading on the way in. The guard is not wrong; it is simply upstream of the loss.
Found by the consumer-portability lane driver (#563), which executes the skill's invocation strings as the prose literally spells them rather than calling the library directly. A direct-import test could not have seen this: the defect lives in the gap between what the prose says to run and what that command actually does.
Options
--cleanup=verbatim — preserves the message exactly. Simplest and most faithful to "the tag carries the changelog section". Also preserves trailing whitespace and blank lines, which is a cosmetic cost.
--cleanup=whitespace — strips leading/trailing blank lines and trailing whitespace, but keeps # lines. Probably the right balance.
- Reformat the composed message to avoid
# at line start (e.g. underline-style headings). Rejected on sight: it makes the tag message diverge from the CHANGELOG section it is supposed to reproduce verbatim, which is the property the release skill explicitly requires.
Option 2 looks correct, but the choice belongs to whoever owns the release surface.
Also worth deciding
Whether a guard should assert the property after the fact — read the tag back with git tag -l -n999 or git cat-file tag and confirm the message still contains its heading. The skill already has a read-back discipline for the published GitHub Release ("verify publication, never assume it") on exactly the reasoning that a step can partially succeed. This is the same shape one layer down.
Acceptance
The release lane composes an annotated tag message from the CHANGELOG section and writes it with
git tag -a <tag> -F <message-file>. Git's default cleanup mode for-Fisstrip, which treats every#-prefixed line as a comment and deletes it.Every Markdown heading in a composed tag message is therefore silently dropped: the
## [X.Y.Z] — DATEsection heading and every### Added/### Fixed/### Performancegrouping.This has already happened, and cannot be undone
Verified against this repository's real, published
v2.8.13:No
## [2.8.13]heading. No### Added. The bullet bodies survived; every heading that gave them structure did not.Under the immutable-tag doctrine (ADR-0024,
SKILL.md's "Recovering from a bad release"), published tags are not retargeted or rewritten. The already-published tags stay as they are. This issue is about making future tags correct, not repairing history.Why nothing caught it
The release skill's own guard is
notes_heading_matches, which checks that the notes file's first heading matches the tag. It runs against the composed file beforegit tagconsumes it, so it validates the input and never observes that git discarded the heading on the way in. The guard is not wrong; it is simply upstream of the loss.Found by the consumer-portability lane driver (#563), which executes the skill's invocation strings as the prose literally spells them rather than calling the library directly. A direct-import test could not have seen this: the defect lives in the gap between what the prose says to run and what that command actually does.
Options
--cleanup=verbatim— preserves the message exactly. Simplest and most faithful to "the tag carries the changelog section". Also preserves trailing whitespace and blank lines, which is a cosmetic cost.--cleanup=whitespace— strips leading/trailing blank lines and trailing whitespace, but keeps#lines. Probably the right balance.#at line start (e.g. underline-style headings). Rejected on sight: it makes the tag message diverge from the CHANGELOG section it is supposed to reproduce verbatim, which is the property the release skill explicitly requires.Option 2 looks correct, but the choice belongs to whoever owns the release surface.
Also worth deciding
Whether a guard should assert the property after the fact — read the tag back with
git tag -l -n999orgit cat-file tagand confirm the message still contains its heading. The skill already has a read-back discipline for the published GitHub Release ("verify publication, never assume it") on exactly the reasoning that a step can partially succeed. This is the same shape one layer down.Acceptance
##and###headingsgit tag -a -F, not just in the composed file