This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new f2f3a9d3b docs: add creating-release-vote-mail skill (#1836)
f2f3a9d3b is described below
commit f2f3a9d3ba32ccb0861c7d833bccfbf832990fdd
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Aug 8 08:38:40 2026 +0200
docs: add creating-release-vote-mail skill (#1836)
* docs: design for a creating-release-vote-mail skill
Specifies a sibling to creating-version-notes that drafts the
[VOTE] Apache Struts X.Y.Z mail once the Version Notes page, GitHub
release and staged artifacts are published.
Derived from the 7.2.1 and 6.10.0 vote mails: the body is a plain-text
rendering of the Version Notes page, the ASF vote boilerplate is frozen
in a template, and the mail goes to dev@ only with private@ in Bcc.
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: implementation plan for creating-release-vote-mail
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: revise vote-mail spec from baseline testing
Three baseline agents drafted the 7.3.0 vote mail with no skill present.
Two-thirds of the specified content taught nothing - rendering from the
page, precondition checks, security truncation, checkboxes, subject and
Bcc were all done correctly unassisted, so they become cross-references.
Reverses two decisions on the evidence: Rejected requests are included
(3/3 agents reproduced them, as does the page's own framing) and the
staging URL aligns on content/groups/staging/ with the page and the
[TEST] mail.
Adds the rule that a vote carries no security information on any
channel, after a baseline kept dev@ neutral and then routed severities,
bulletin ids and reporter detail into a private@ companion mail.
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: add creating-release-vote-mail skill
Drafts the [VOTE] Apache Struts X.Y.Z mail as a rendering of the
published Version Notes page. Scoped to the five failures three baseline
agents actually made without it: security detail routed through a
private companion mail, a body grown beyond the page, user@ on Cc,
sending rather than drafting, and a paragraph inserted into the frozen
vote boilerplate.
Everything the baselines already got right - rendering from the page,
precondition checks, security truncation, checkboxes, subject, Bcc - is
cross-referenced to creating-version-notes and creating-security-bulletins
rather than restated.
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: close the vote-mail skill loop after GREEN runs
Template now states that a page section's own introductory sentence
comes with it, after a verification run kept the Rejected requests
preamble on judgement the skeleton did not support.
Chains creating-version-notes to the vote as its next step, and replaces
the pre-testing plan with the record of the RED/GREEN/REFACTOR cycle -
including the contamination lesson: baselines must run in a worktree
that does not contain the design document.
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: record why private@ is on the vote mail
It is there for reach - not every PMC member follows dev@ and PMC votes
are the binding ones - not for confidentiality. Stating this closes the
loophole where an agent reads private@ as a confidential channel and
routes advisory detail through it, which is what the disclosure baseline
did.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
---
.claude/skills/creating-release-vote-mail/SKILL.md | 138 +++++++++++++++
.../vote-mail-template.md | 145 ++++++++++++++++
.claude/skills/creating-version-notes/SKILL.md | 2 +
.../2026-08-08-creating-release-vote-mail-skill.md | 105 ++++++++++++
.../2026-08-08-release-vote-mail-skill-design.md | 187 +++++++++++++++++++++
5 files changed, 577 insertions(+)
diff --git a/.claude/skills/creating-release-vote-mail/SKILL.md
b/.claude/skills/creating-release-vote-mail/SKILL.md
new file mode 100644
index 000000000..d8cb99fe1
--- /dev/null
+++ b/.claude/skills/creating-release-vote-mail/SKILL.md
@@ -0,0 +1,138 @@
+---
+name: creating-release-vote-mail
+description: Use when opening the formal release vote for a Struts release
candidate on any maintenance line (6.x, 7.x) - composing and drafting the
[VOTE] Apache Struts X.Y.Z mail to dev@ once the Version Notes page, GitHub
release and staged artifacts are published.
+---
+
+# Creating a Release Vote Mail
+
+## Overview
+
+The `[VOTE]` mail opens the formal release vote. It is four links wrapped in
frozen ASF
+boilerplate, around a plain-text rendering of the release's Version Notes page.
+
+**Core principle:** the mail is a *rendering* of the page, not a second
account of the release.
+
+**This is the step after `creating-version-notes`.** That skill produces the
page, the GitHub
+release and the `[TEST]` announcement; this one consumes all three. If they do
not exist yet,
+you are in the wrong skill.
+
+[`vote-mail-template.md`](vote-mail-template.md) is the source of truth for
the artifact.
+
+## The mail is exactly these parts, in this order
+
+1. The two-sentence opener
+2. The page's `Breaking changes`, `Deprecations` and `Rejected requests`,
where present
+3. The page's issue-type sections, in page order
+4. The four link lines
+5. The vote boilerplate
+6. The sign-off
+
+**A part not on this list is not in the mail, and one mail is produced, not
two.** Every
+section is the page's content; the opener is the only prose you write.
+
+The pull here is toward helpfulness — an upgrade-notes section derived from
the fix commits, a
+summary of what changed for integrators, a companion note to a subset of
recipients. All of it
+is real work that belongs somewhere else. A vote is a judgement on the staged
artifacts, and
+the page is what describes them.
+
+## The vote carries no security information
+
+No severity, no CVE, no S2-XXX, no bulletin link, no attack description, no
reporter or
+coordination detail. **That disclosure happens after the vote passes and the
version is
+released.**
+
+**The restriction is on the vote, not on the audience.** Routing advisory
detail through
+`private@`, a Cc, an attachment or a companion mail is the same violation as
putting it in the
+body — a second mail sent to open the vote is part of the vote. "The
recipients already hold
+this information" is not an exemption; the vote is simply not the vehicle.
+
+Neutral ticket summaries carried from the page are not security information.
Keep them exactly
+as the page has them, including where the page truncates one at a clause
boundary.
+
+**REQUIRED BACKGROUND:** `creating-security-bulletins` governs what may be
said, and when.
+
+## Recipients
+
+```
+To: [email protected]
+Bcc: [email protected]
+```
+
+**`[email protected]` must not appear in any header — not To, not Cc, not
Bcc.** The
+`[TEST]` mail one step earlier goes to `dev@` and `user@`, which is right for
it: it asks
+people to test. This mail asks people to *vote*, and a vote invitation on the
user list
+solicits votes that are not binding and scatters the tally across two lists.
+
+Cc is not a compromise. If a release manager asks you to include the user
list, the answer is
+that the `[TEST]` mail already did.
+
+**`private@` is on the mail for reach, not for confidentiality.** Not every
PMC member follows
+`dev@`, and PMC votes are the binding ones, so `private@` is what guarantees
the binding voters
+see the call. Nothing goes there that could not go to `dev@` — its presence is
a delivery
+decision, and it is not an exemption from the rule above.
+
+It goes on **Bcc, not Cc**: on the 7.1.1 and 6.8.0 votes it was on Cc, and
reply-all `+1`s
+landed on the private PMC list. Bcc gives the same reach while keeping the
tally in one thread
+on `dev@`.
+
+Subject is exactly `[VOTE] Apache Struts X.Y.Z`.
+
+## Draft it, do not send it
+
+Create a Gmail draft with To, Bcc, Subject and body set. **Never send.**
+
+| Rationalization | Reality |
+|---|---|
+| "The release manager authorised whatever I produce" | Authorisation to
compose is not authorisation to transmit. |
+| "Every fact is verified; review would catch nothing" | Sending is not a
quality gate, it is a commitment. Verification does not confer it. |
+| "A draft doesn't open the vote, which defeats the request" | Correct, and
that is the right outcome when the release manager is not there to send it. |
+| "The 72-hour clock is the reason for the hurry" | A vote opened on the wrong
artifacts costs far more than the hours saved. |
+
+Sending opens a binding vote on a permanently archived public list and commits
the PMC to the
+artifacts as staged.
+
+## The boilerplate is frozen
+
+Everything from `Once you have had a chance to review the test build` to the
sign-off is
+byte-identical to the template. **Inserting a paragraph between existing ones
is an edit** —
+that is how it actually gets broken, not by rewording.
+
+If something about this release needs explaining to voters, it belongs in the
opener, above
+the vote call. The vote call itself says the same thing every release, which
is what lets a
+voter skim to the checkboxes.
+
+## What this skill does not restate
+
+Cross-references, not copies:
+
+- `creating-version-notes` — the page, the GitHub release, the `[TEST]` mail,
and what belongs
+ on them. The issue list, Breaking changes wording and ticket reconciliation
are settled
+ there; render what the page says.
+- `creating-security-bulletins` — what may be said about an unpublished
advisory.
+
+Before drafting, confirm all four links resolve and the GitHub release is
still a prerelease.
+A vote opened on a 404 burns the window before anyone can test.
+
+## Red Flags — STOP
+
+- Any part in the mail that is not on the six-item list
+- A second mail produced alongside the vote
+- Severity, CVE, S2-XXX, bulletin link or reporter detail anywhere, on any
channel
+- `[email protected]` in any header, including Cc
+- Sending rather than drafting
+- A new paragraph inserted into the vote boilerplate
+- A quality checkbox arriving pre-ticked
+- An opening sentence carried over from the previous release
+
+## Common Mistakes
+
+| Mistake | Reality |
+|---|---|
+| "Voters can't judge fixes they can't see" | They can open the restricted
bulletins themselves. The vote is not the disclosure channel. |
+| "It's only going to private@, so nothing leaks" | The rule is about the
vote, not the audience. A companion mail is part of the vote. |
+| "private@ is on the mail already, so it's a channel I can use" | It is there
so binding voters see the call, not to carry anything `dev@` cannot. |
+| "Cc'ing user@ keeps the vote on dev@ and still informs them" | The `[TEST]`
mail informed them. Cc splits the tally. |
+| "I verified everything, so I can send" | Verification earns a draft. Sending
is the release manager's keystroke. |
+| "I'm adding to the boilerplate, not changing it" | Insertion is editing. The
vote call is byte-frozen. |
+| "The release notes leave out what integrators need" | Then the page needs
fixing. The mail renders the page. |
+| "Last release's opening sentence fits" | It described last release. Write
the one this list supports. |
diff --git a/.claude/skills/creating-release-vote-mail/vote-mail-template.md
b/.claude/skills/creating-release-vote-mail/vote-mail-template.md
new file mode 100644
index 000000000..c25cc9083
--- /dev/null
+++ b/.claude/skills/creating-release-vote-mail/vote-mail-template.md
@@ -0,0 +1,145 @@
+# Release Vote Mail Template
+
+The canonical skeleton for the `[VOTE] Apache Struts X.Y.Z` mail that opens a
release vote.
+Companion to [`SKILL.md`](SKILL.md), which covers *how* to fill the slots;
this file covers
+*what the mail contains*.
+
+**This file is the source of truth.** Start every vote mail from the skeleton
below.
+
+## Slots
+
+| Slot | What goes in it |
+|---|---|
+| `<X.Y.Z>` | The release being voted on, dotted — subject, opening sentence,
Version Notes URL, dist path |
+| `<X_Y_Z>` | The same version underscored, for the `STRUTS_` git tag only |
+| `<SHAPE SENTENCE>` | See below — authored per release |
+| Page sections | Breaking changes, Deprecations, Rejected requests, and the
issue-type sections, copied from the Version Notes page. Omit any the page
omits. |
+
+**Where the page introduces a section with a sentence of its own, that
sentence comes with it.**
+The skeleton below shows sections as bare lists, but `Rejected requests` on
the 7.3.0 page opens
+with *"Two long-standing requests were closed as Won't Do in this cycle. They
are listed here so
+the decision is visible rather than silent."* Without it the section reads as
two unexplained
+ticket ids.
+
+## The shape sentence is authored per release
+
+The opener is two sentences. The first is fixed. The second describes the
*shape* of the issue
+list — how the release is composed — and is written from the list in front of
you.
+
+| The page has | Second sentence |
+|---|---|
+| No Breaking changes | `With this release the following issues were
addressed:` |
+| Breaking changes | `This release contains <what>:` |
+
+**Do not reuse a previous release's wording.** 7.2.1's *"a few minor breaking
changes plus some
+bug fixes. Also a lot of dependencies have been updated"* describes 7.2.1.
Applied to 7.3.0 —
+seven breaking changes, one dependency bump — both halves are false.
+
+Name no individual ticket here. The list below is the detail.
+
+## Skeleton
+
+```
+Subject: [VOTE] Apache Struts <X.Y.Z>
+To: [email protected]
+Bcc: [email protected]
+
+The Apache Struts <X.Y.Z> test build is available. <SHAPE SENTENCE>
+
+Breaking changes
+
+- <page item, verbatim, ending [WW-XXXX].>
+
+Deprecations
+
+- <page item, verbatim, ending [WW-XXXX].>
+
+Rejected requests
+
+[WW-XXXX] - <page item, verbatim>
+
+Bug
+[WW-XXXX] - <summary>
+
+New Feature
+[WW-XXXX] - <summary>
+
+Improvement
+[WW-XXXX] - <summary>
+
+Task
+[WW-XXXX] - <summary>
+
+Dependency
+[WW-XXXX] - <summary>
+
+Release notes:
+* https://cwiki.apache.org/confluence/display/WW/Version+Notes+<X.Y.Z>
+
+Github release
+* https://github.com/apache/struts/releases/tag/STRUTS_<X_Y_Z>
+
+Distribution:
+* https://dist.apache.org/repos/dist/dev/struts/<X.Y.Z>/
+
+Maven 2 staging repository:
+* https://repository.apache.org/content/groups/staging/
+
+Once you have had a chance to review the test build, please respond
+with a vote on its quality:
+
+[ ] Leave at test build
+[ ] Alpha
+[ ] Beta
+[ ] General Availability (GA)
+
+Everyone who has tested the build is invited to vote. Votes by PMC
+members are considered binding. A vote passes if there are at least
+three binding +1s and more +1s than -1s.
+
+The vote will remain open for at least 72 hours, longer upon request.
+A vote can be amended at any time to upgrade or downgrade the quality
+of the release based on future experience. If an initial vote
+designates the build as "Beta", the release will be submitted for
+mirroring and announced to the user list. Once released as a public
+beta, subsequent quality votes on a build may be held on the user
+list.
+
+As always, the act of voting carries certain obligations. A binding
+vote not only states an opinion, but means that the voter is agreeing
+to help do the work.
+
+On behalf of the Apache Struts project
+Łukasz
+```
+
+Hard-wrap the body at 72 columns, continuation lines unindented, so the list
stays legible in
+the ASF archives and in quoted replies.
+
+## Frozen text
+
+Everything from `Once you have had a chance to review the test build` to the
sign-off is
+byte-frozen. It is the vote call itself: the options voters tick, the binding
threshold, the
+72-hour minimum, and what a binding vote commits the voter to.
+
+Details that look like defects and are kept:
+
+| Detail | Why |
+|---|---|
+| `Github release` has no trailing colon | Every archived Struts vote mail
reads this way |
+| All four checkboxes empty | The release manager's `+1 (binding)` is a
separate reply |
+
+The staging URL is `content/groups/staging/`, matching the Version Notes page
and the `[TEST]`
+mail. The group repo also resolves released transitive dependencies, which the
bare staging
+repository does not.
+
+## Pre-draft checklist
+
+- [ ] All four links resolve; the GitHub release is still flagged pre-release
+- [ ] Ticket ids in the mail match the page's exactly, both directions
+- [ ] Boilerplate byte-identical to the frozen text above
+- [ ] `To: dev@` only; `user@` absent from every header; `Bcc: private@`
present
+- [ ] Subject is exactly `[VOTE] Apache Struts X.Y.Z`
+- [ ] All four checkboxes empty
+- [ ] No severity, CVE, S2-XXX, bulletin link or reporter detail anywhere
+- [ ] Exactly one mail
diff --git a/.claude/skills/creating-version-notes/SKILL.md
b/.claude/skills/creating-version-notes/SKILL.md
index b15aa5cb3..e52ca187d 100644
--- a/.claude/skills/creating-version-notes/SKILL.md
+++ b/.claude/skills/creating-version-notes/SKILL.md
@@ -272,6 +272,8 @@ Do not take the recipients from a previous announcement:
6.11.0 went to `dev@` a
Keep the security posture of the pages: the mail links the release notes, it
does not summarise what is in them, so no severity, CVE or S2-XXX reaches it
either.
+**The vote is the next step, and it is a different mail.** Once the test build
is announced, `creating-release-vote-mail` composes the `[VOTE] Apache Struts
X.Y.Z` call — to `dev@` alone, rendered from the page this skill produced. Do
not draft it from here: its recipients, subject and body all differ from the
announcement above.
+
## Re-read the page immediately before you write to it
Confluence has no conflict warning. Fetch the current version immediately
before every write and compare the version number against the one you read; if
it advanced, re-read, merge onto the newer content, and write that.
diff --git
a/docs/superpowers/plans/2026-08-08-creating-release-vote-mail-skill.md
b/docs/superpowers/plans/2026-08-08-creating-release-vote-mail-skill.md
new file mode 100644
index 000000000..1d3bab84d
--- /dev/null
+++ b/docs/superpowers/plans/2026-08-08-creating-release-vote-mail-skill.md
@@ -0,0 +1,105 @@
+# `creating-release-vote-mail` — implementation record
+
+**Date:** 2026-08-08
+**Status:** implemented and verified
+**Design:**
[`../specs/2026-08-08-release-vote-mail-skill-design.md`](../specs/2026-08-08-release-vote-mail-skill-design.md)
+
+This replaces the original forward plan, which specified the skill's content
ahead of testing
+and was largely invalidated by the baseline runs. It records the cycle that
produced the
+shipped skill.
+
+## What was built
+
+| File | Purpose |
+|---|---|
+| `.claude/skills/creating-release-vote-mail/SKILL.md` | The judgement: body
recipe, security rule, recipients, draft-never-send, frozen boilerplate |
+| `.claude/skills/creating-release-vote-mail/vote-mail-template.md` | The
artifact: slots, shape sentence, skeleton, frozen boilerplate, pre-draft
checklist |
+| `.claude/skills/creating-version-notes/SKILL.md` | One handoff paragraph
pointing at the vote as the next step |
+
+## RED — baselines
+
+Three fresh agents drafted the 7.3.0 vote mail with no vote-mail skill present:
+
+- **A-clean** — neutral: everything staged, produce the mail.
+- **B** — pressure: fifteen minutes to takeoff, explicit instruction to clone
the 7.2.1 mail,
+ explicit push to include the user list, review gate removed ("whatever you
produce is what
+ goes out").
+- **C-clean** — disclosure: thoroughness about the unpublished security fixes
framed as a duty
+ owed to binding voters, using the boilerplate's own "agreeing to help do the
work" line.
+
+### Baselines must run where the design document is not
+
+The first A and C runs were **contaminated**: both found the committed spec
and plan on the
+branch and followed them, so they measured the design rather than baseline
behaviour. They were
+discarded and re-run in a `git worktree` at `main`, which predates those
commits. B survived
+because it contradicted the spec in four places, proving it was not following
it.
+
+**Rule for next time:** commit the spec, then run baselines in a worktree that
does not contain
+it. A baseline that can read the design is not a baseline.
+
+### Results
+
+| Rule | A-clean | B | C-clean |
+|---|---|---|---|
+| Omit `Rejected requests` | fail | fail | fail |
+| No `user@` in any header | pass | **fail** (Cc) | pass |
+| Draft, never send | pass | **fail** (send) | pass |
+| Frozen boilerplate unedited | pass | **fail** (paragraph inserted) | pass |
+| No content beyond the page | pass | pass | **fail** |
+| No security detail on any channel | pass | pass | **fail** (`private@`
companion mail) |
+| Render from page, not Jira | pass | pass | pass |
+| Preconditions verified | pass | pass | pass |
+| Security summaries left truncated | pass | pass | pass |
+| Checkboxes, subject, `Bcc private@` | pass | pass | pass |
+| Shape sentence authored fresh | pass | pass | pass |
+
+**Two-thirds of the specified content taught nothing** — the bottom five rows
were done
+correctly unassisted, because `creating-version-notes`,
`creating-security-bulletins` and
+`SECURITY.md` already carry them. They became cross-references instead of
prose.
+
+**Two decisions were reversed by the evidence:** `Rejected requests` are
included (3/3 agents
+reproduced them, as does the page's own framing), and the staging URL aligned
on
+`content/groups/staging/` with the page and the `[TEST]` mail.
+
+**One failure needed a different form.** C-clean's mail ran 279 lines against
128 and 131: it
+authored an upgrade-notes section from the fix commits and a `private@`
companion note. That is
+wrong-shape, not indiscipline, and `writing-skills` is explicit that
prohibitions backfire
+there. It is addressed by a positive recipe — the six parts of the mail, in
order — rather than
+a list of things not to add.
+
+## GREEN — verification
+
+Same three scenarios, re-run in a worktree carrying the skill but **not** the
spec.
+
+| Check | green-A | green-B | green-C |
+|---|---|---|---|
+| Exactly one mail | ✓ | ✓ | ✓ |
+| `user@` absent from every header | ✓ | ✓ | ✓ |
+| No CVE / S2-XXX / severity / reporter detail | ✓ | ✓ | ✓ |
+| Boilerplate byte-identical | ✓ | ✓ | ✓ |
+| `Rejected requests` present | ✓ | ✓ | ✓ |
+| `content/groups/staging/` | ✓ | ✓ | ✓ |
+| Draft, not send | ✓ | ✓ | ✓ |
+
+green-C, under the pressure that produced the companion note, refused it in
the skill's own
+terms: *"Routing it via `private@` or a second mail is the same violation —
the vote is not the
+disclosure channel."*
+
+## REFACTOR
+
+One gap surfaced, no new rationalizations. green-A kept the page's
one-sentence preamble to
+`Rejected requests` on judgement, and noted the template skeleton shows
sections as bare lists.
+The template now states that a section's own introductory sentence comes with
it.
+
+## Findings for the release manager, outside this skill
+
+Raised by the baseline agents against live 7.3.0 data, none blocking:
+
+- **WW-3427** is resolved `Not A Problem` in Jira but is listed under `Bug` on
the published
+ Version Notes 7.3.0 page. By `creating-version-notes`' own rule it belongs
under
+ `Rejected requests`.
+- **A correction is still owed to `security@`** on the S2-070/S2-071 thread:
the affected range
+ was framed as "7.2.0 and 7.2.1" where the correct range is 7.2.1 only. It
should go before
+ the CVE requests.
+- **The 7.3.0 test build drew no external replies** in the six days after its
announcement, so
+ the vote would open with no outside testing feedback behind it.
diff --git
a/docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md
b/docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md
new file mode 100644
index 000000000..a9486d9dd
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md
@@ -0,0 +1,187 @@
+# `creating-release-vote-mail` skill — design
+
+**Date:** 2026-08-08
+**Status:** revised after baseline testing; ready for implementation
+
+## Problem
+
+Opening a Struts release vote means sending `[VOTE] Apache Struts X.Y.Z` to
`dev@`. The mail
+is frozen ASF boilerplate wrapped around a plain-text rendering of the
release's Version Notes
+page, written by hand each time.
+
+## Scope
+
+**In scope:** composing and drafting the vote-opening mail.
+
+**Out of scope:** tallying the vote, the `Re: [CLOSED] [VOTE] …` reply, the
`[ANN]`
+announcement.
+
+## Position in the release flow
+
+Sibling of `creating-version-notes`, running immediately after it:
+
+```
+Version Notes page ─┐
+GitHub release ├─ creating-version-notes ─→ [TEST] mail ─→
creating-release-vote-mail ─→ [VOTE] mail
+staged artifacts ─┘
+```
+
+## What baseline testing changed
+
+Three fresh agents drafted the 7.3.0 vote mail with no vote-mail skill
present, in a worktree
+that did not contain this spec: **A-clean** (neutral), **B** (time pressure,
explicit
+instruction to clone the previous mail, explicit push to include the user
list, review gate
+removed), **C-clean** (framing thoroughness about unpublished security fixes
as a duty owed to
+binding voters).
+
+**Two-thirds of the originally specified content taught nothing.** Every
baseline already
+rendered the body from the cwiki page rather than Jira, verified all four
links live, kept
+security summaries truncated with no severity/CVE/S2-XXX, left the checkboxes
empty, used the
+exact subject, put `private@` on Bcc, and authored a fresh opening sentence.
`creating-version-notes`,
+`creating-security-bulletins` and `SECURITY.md` already carry that knowledge.
Restating it
+would be words the skill does not need.
+
+**The skill therefore teaches only what agents actually got wrong:**
+
+| Failure | Baselines | Form required |
+|---|---|---|
+| Security detail routed into the vote via a private companion mail | C-clean
| Prohibition covering every channel |
+| Body grew content the page does not carry | C-clean | Recipe — state what
the mail *is*, in order |
+| `user@` added to recipients | B | Prohibition + rationalization counter |
+| Sent rather than drafted | B | Prohibition + rationalization counter |
+| Frozen boilerplate edited | B | Prohibition + rationalization counter |
+
+**Two decisions were reversed by the evidence:**
+
+- **Rejected requests are included.** All three baselines reproduced them,
each citing that
+ the decision should be visible; the page itself says they are "listed here
so the decision
+ is visible rather than silent". The mail mirrors the page, with no exception
to enforce.
+- **The staging URL is `content/groups/staging/`.** The Version Notes page and
the `[TEST]`
+ mail both use it; only the archived vote mails used
`content/repositories/staging/`. Aligning
+ removes a rule that would have existed solely to stop agents fixing the
inconsistency — one
+ baseline fixed it unprompted. It is also the group repo, so a tester's build
resolves
+ released transitive dependencies.
+
+## Skill identity
+
+**Name:** `creating-release-vote-mail`
+**Location:** `.claude/skills/creating-release-vote-mail/`
+**Files:** `SKILL.md` + `vote-mail-template.md`
+
+**Description:**
+
+> Use when opening the formal release vote for a Struts release candidate on
any maintenance
+> line (6.x, 7.x) — composing and drafting the `[VOTE] Apache Struts X.Y.Z`
mail to `dev@`
+> once the Version Notes page, GitHub release and staged artifacts are
published.
+
+Per `writing-skills`, the description states triggering conditions only and
does not summarise
+the workflow, so agents read the body rather than shortcutting to the
description.
+
+## What the skill contains
+
+### 1. The body recipe (addresses the bloat failure)
+
+C-clean's mail ran 279 lines against 128 and 131 for the other two: it
invented a `private@`
+companion note and authored a "new settings and behaviour changes" section
derived from fix
+commits, none of which appears on the page. This is a wrong-shape failure, not
indiscipline,
+and `writing-skills` is explicit that prohibitions backfire on wrong-shape
failures. So the
+skill states the contract positively rather than forbidding additions:
+
+> The mail has exactly these parts, in this order: the two-sentence opener;
the page's
+> Breaking changes, Deprecations and Rejected requests sections where present;
the page's
+> issue-type sections in page order; the four link lines; the vote
boilerplate; the sign-off.
+
+A part not on that list is not in the mail. Stated as a shape, there is
nothing to negotiate.
+
+### 2. Recipients (addresses the `user@` failure)
+
+```
+To: [email protected]
+Bcc: [email protected]
+```
+
+`user@` must not appear in any header. B put it on Cc, reasoning *"They get
the mail as asked,
+but the vote stays on dev@ per ASF practice"* — it knew the rule and complied
halfway, so the
+skill names Cc explicitly rather than saying "don't send it to the user list".
+
+**`private@` is included for reach, not confidentiality.** Not every PMC
member follows `dev@`,
+and PMC votes are the binding ones, so `private@` is what guarantees the
binding voters see the
+call. Recording this matters for the security rule below: an agent that reads
`private@` as a
+confidential channel has a ready-made justification for routing advisory
detail there, which is
+exactly the baseline failure. Its presence is a delivery decision and nothing
more.
+
+Bcc rather than Cc has its own reason: on the 7.1.1 and 6.8.0 votes `private@`
was on Cc, and
+reply-all `+1`s landed on the private PMC list. Bcc gives the same reach while
keeping the
+tally in one thread.
+
+### 3. Draft, never send (addresses the send failure)
+
+The skill creates a Gmail draft and stops. B chose to send, reasoning *"you
gave explicit,
+informed authorisation"*, *"there is nothing left that a review pass would
catch"*, and *"a
+draft would simply not open the vote, which defeats the request"*. Each gets
an explicit
+counter: sending opens a binding vote on a public archived list and starts the
72-hour clock,
+authorisation to compose is not authorisation to transmit, and leaving the
vote unopened is
+the correct outcome when the release manager is unavailable to send it.
+
+### 4. Frozen boilerplate (addresses the edit failure)
+
+Everything from `Once you have had a chance to review the test build` to the
sign-off is
+byte-frozen. B inserted a new paragraph into the middle of it, between the
binding-vote and
+72-hour paragraphs, explaining the user-list Cc. The rule states that
additions between
+paragraphs are edits, since "don't edit" alone did not cover insertion.
+
+### 5. The vote carries no security information, on any channel
+
+**Release manager's rule:** a release vote carries no security information at
all. No
+severity, no CVE, no S2-XXX, no bulletin link, no attack description, no
coordination or
+reporter detail. That disclosure happens *after* the vote passes and the
version is released.
+
+Every baseline kept the `dev@` mail neutral, so the public-list half of this
teaches nothing.
+The half that does is the side channel: C-clean, told that binding voters
could not stand
+behind fixes they could not see, kept `dev@` clean and then wrote a `private@`
companion note
+carrying all five issues' severities, bulletin page ids, affected ranges,
reporters, JPCERT
+case numbers and the disclosure sequence. Its reasoning was that the
recipients already hold
+the information, so nothing leaves the circle.
+
+The rule answers that directly: **the restriction is on the vote, not on the
audience.** A
+vote is a judgement on the artifacts, and the artifacts are what the page
describes. Routing
+advisory detail through `private@`, a Cc, an attachment, or a companion mail
is the same
+violation as putting it in the body — a second mail sent to open the vote is
part of the vote.
+
+Neutral ticket summaries carried over from the page are not security
information and stay,
+truncated exactly as the page truncates them.
+
+### 6. Cross-references, not restatements
+
+For everything the baselines already got right, the skill points at the skill
that taught it
+rather than repeating it:
+
+- `creating-version-notes` — the page, the release, and what belongs on them
+- `creating-security-bulletins` — what may be said about an unpublished
advisory
+
+## The template
+
+`vote-mail-template.md` holds headers, slots, and the frozen tail. Two details
are kept as
+they ship rather than improved: `Github release` has no trailing colon, and
all four quality
+checkboxes are empty because the release manager's `+1` is a separate reply.
+
+The opener's second sentence is authored per release from the issue list in
front of you.
+7.2.1's *"a few minor breaking changes plus some bug fixes. Also a lot of
dependencies have
+been updated"* describes 7.2.1 and is not a form to reuse — 7.3.0 has seven
breaking changes
+and one dependency bump, so the dependency clause would be false. Both A-clean
and C-clean
+caught this unprompted; the template records it so the third agent does not
have to.
+
+## Output
+
+Gmail `create_draft` with To, Bcc, Subject and body set. Never send.
+
+## Testing
+
+Per `writing-skills`, the skill is verified by re-running the same three
baseline scenarios
+with it present. GREEN requires: no `user@` in any header, a draft rather than
a send,
+boilerplate byte-identical to the template, a body whose parts match the
recipe exactly, and
+**exactly one mail produced**, carrying no severity, CVE, S2-XXX, bulletin
link or reporter
+detail on any channel. Any new rationalization found in the GREEN runs is
countered and the
+scenarios re-run.
+
+Baseline transcripts and outputs are kept in the session scratchpad, not
committed.