From 458caf6e33d28672750e6d5262f518b726450cbb Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 07:21:25 +0200 Subject: [PATCH 1/6] 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 --- ...26-08-08-release-vote-mail-skill-design.md | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md 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 0000000000..d4efd6b2a2 --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md @@ -0,0 +1,244 @@ +# `creating-release-vote-mail` skill — design + +**Date:** 2026-08-08 +**Status:** approved, ready for implementation + +## Problem + +Opening a Struts release vote means sending `[VOTE] Apache Struts X.Y.Z` to `dev@`. +The mail is mostly frozen ASF boilerplate wrapped around a plain-text rendering of the +release's Version Notes page, but it is currently written by hand each time. Hand-writing +it produces drift — the 7.2.1 and 6.10.0 mails disagree on their sign-off — and it re-states, +in a public archived list, content whose canonical form already exists on the cwiki. + +## Scope + +**In scope:** composing and drafting the vote-opening mail. + +**Out of scope:** tallying the vote, the `Re: [CLOSED] [VOTE] …` reply, and the `[ANN]` +announcement. Those are later stages and may become their own skill if wanted. + +## Position in the release flow + +The skill is a sibling of `creating-version-notes` and runs immediately after it: + +``` +Version Notes page ─┐ +GitHub release ├─ creating-version-notes ─→ [TEST] mail ─→ creating-release-vote-mail ─→ [VOTE] mail +staged artifacts ─┘ +``` + +`creating-version-notes` ends at the `[TEST]` announcement; this skill begins there. Their +frontmatter descriptions therefore do not compete for the same trigger. + +## 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. + +## Core principle + +**The mail is a rendering of the Version Notes page, not a second account of the release.** + +Everything below the opening sentence is a plain-text transform of a published cwiki section. +Nothing is re-authored, so the mail cannot assert something the page does not. + +### The Iron Rule + +``` +THE VERSION NOTES PAGE IS THE ONLY SOURCE FOR THE BODY. +NEVER RETYPE THE ISSUE LIST, AND NEVER CLONE THE PREVIOUS VOTE MAIL. +``` + +Cloning is the same failure `creating-version-notes` bans for the same reason: the number +gets updated and the surrounding text does not. + +## Preconditions + +The mail is four links wrapped in boilerplate. All four must resolve *before* drafting — +a vote opened on a 404 burns the 72-hour window before anyone can test. + +| Link | Produced by | Check | +|---|---|---| +| `Version+Notes+X.Y.Z` on cwiki | `creating-version-notes` | fetch it — it is also the body source | +| `releases/tag/STRUTS_X_Y_Z` | `creating-version-notes` | `gh release view`, must still be `--prerelease` | +| `dist/dev/struts/X.Y.Z/` | release build | HTTP check; artifacts and signatures present | +| Nexus `content/repositories/staging/` | `mvn release` | staging repo open, not dropped | + +## Recipients + +``` +To: dev@struts.apache.org +Bcc: private@struts.apache.org +``` + +**`user@` must not appear.** The `[TEST]` mail one step earlier goes to both `dev@` and +`user@`; the `[VOTE]` mail goes to `dev@` alone, because a vote invitation on the user list +solicits votes from people whose votes are not binding. Both sampled mails got this right; +the skill records *why* so it stays right. + +Subject is exactly `[VOTE] Apache Struts X.Y.Z` — no "test build", no RC suffix. + +## The opening sentence — the only authored prose + +Two sentences: the fixed `The Apache Struts X.Y.Z test build is available.` plus one +describing the *shape* of the issue list, never its individual contents. + +| Page has | Second sentence | +|---|---| +| no Breaking changes | `With this release the following issues were addressed:` (as in 6.10.0) | +| Breaking changes | `This release contains . Also a lot of dependencies have been updated:` (as in 7.2.1) | + +## Transform rules, per section + +- **Breaking changes** — the page's items verbatim, `- ` prefixed, ticket references as bare + `[WW-XXXX]` text (plain-text mail carries no links). Verbatim copying is what holds them at + the page's one-sentence form; re-authoring is how the 7.2.1 items grew to three clauses. +- **Deprecations** — included when the page has them. A deprecation tells a voter what to + check in their own application, so it belongs in front of the people testing. +- **Rejected requests** — **not** included. A `Won't Do` ticket has nothing to test; it is + release documentation, and the Release notes link carries it. +- **Issue-type sections** — page order (Bug → New Feature → Improvement → Task → Dependency), + heading bare on its own line, entries `[WW-XXXX] - `, blank line between sections. + Omit any type the page omits. +- **Security-truncated summaries carry through exactly as truncated.** Where the page stopped + a summary at a clause boundary because its bulletin is unpublished, the mail stops there too. + Re-expanding it publishes to `dev@` — a public archived list — what the page deliberately + withheld. Cross-references `creating-security-bulletins`. +- **Hard-wrap at 72 columns**, continuation lines unindented, matching both sampled mails. + This keeps the list legible in the ASF archives and in quoted replies. + +### A ticket/page mismatch is not automatically an error + +A Dependency entry may name a lower version than the Breaking changes prose: 7.2.1 lists +`[WW-5536] - Bump ognl:ognl from 3.3.5 to 3.4.8` while its Breaking changes says OGNL went to +3.4.11. Both are correct — the entry reproduces the ticket summary verbatim, the prose states +what shipped. `creating-version-notes` mandates exactly this. Do not "fix" it in the mail. + +## The template + +`vote-mail-template.md` holds the headers, the slots, and the frozen tail: + +``` +Subject: [VOTE] Apache Struts +To: dev@struts.apache.org +Bcc: private@struts.apache.org + +The Apache Struts test build is available. + + + + + +Release notes: +* https://cwiki.apache.org/confluence/display/WW/Version+Notes+ + +Github release +* https://github.com/apache/struts/releases/tag/STRUTS_ + +Distribution: +* https://dist.apache.org/repos/dist/dev/struts// + +Maven 2 staging repository: +* https://repository.apache.org/content/repositories/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 +``` + +Two deliberate choices frozen here: + +- **Sign-off** is `On behalf of the Apache Struts project` (7.2.1's form, not 6.10.0's + `Kind regards`) — it reads as the PMC opening a formal vote rather than a personal note. +- **Staging URL** is `content/repositories/staging/`, as both sampled vote mails used. It + differs from the `[TEST]` mail's `content/groups/staging/`; that difference is accepted, + not a defect to reconcile. + +`Github release` keeps its missing colon and the boilerplate keeps its exact wording. The +template records what ships; it does not improve it. + +### All four checkboxes ship empty + +The release manager's own vote is a separate reply (`+1 (binding)`), as both sampled threads +show. A call that arrives with a quality level already ticked reads as a decision announced +rather than a vote opened. + +## Verification before creating the draft + +1. All four links resolve; the GitHub release is still flagged pre-release. +2. Ticket sets match — `diff` the mail's `WW-` ids against the page's, excluding the page's + `Rejected requests` section, which the mail deliberately omits. Empty output, or the mail + is not a rendering. +3. Boilerplate byte-identical to the template. +4. `To`/`Bcc` correct, `user@` absent, subject exactly `[VOTE] Apache Struts X.Y.Z`. +5. All four checkboxes empty. + +## Output + +The skill composes the mail and calls Gmail `create_draft` with To, Bcc, Subject and body set. + +**The skill drafts; it never sends.** Sending opens a binding project vote, which stays the +release manager's keystroke. + +## Failure modes recorded in the skill + +`SKILL.md` closes with the two tables the sibling skills use. + +**Red Flags — STOP:** + +- Cloning the previous release's vote mail +- Retyping the issue list instead of rendering the page +- Re-expanding a summary the page truncated +- Adding `user@struts.apache.org` to the recipients +- Pre-ticking a quality level +- Drafting before the page, tag or dist path exist +- Putting a severity, CVE or S2-XXX reference in the mail +- Editing the boilerplate wording +- "Correcting" a Dependency entry to match the Breaking changes prose + +**Common Mistakes** pairs each with its reality, e.g.: + +| Mistake | Reality | +|---|---| +| "Last release's vote mail is the fastest start" | It is how the sign-off drifted between 6.10.0 and 7.2.1. Start from the template. | +| "The [TEST] mail went to user@, so this should too" | A vote invitation on the user list solicits non-binding votes. `dev@` only. | +| "The ticket says 3.4.8 but we shipped 3.4.11" | Both are right. The entry is the ticket summary verbatim; the prose is what shipped. | +| "The fix is public, so I can describe it" | A public ticket does not publish the advisory. `dev@` is archived. Neutral framing until the bulletin ships. | +| "The page is up, so I can draft" | Check the tag, the dist path and the staging repo too. A vote on a 404 wastes 72 hours. | + +## Tests + +Ships without tests, matching `creating-version-notes`. A failing baseline under +`writing-skills` requires subagents, which are not spawned unprompted. Tests can be added on +request. From b7a9dfa10e4005507ab040b79c591d3af6ea96ac Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 07:28:15 +0200 Subject: [PATCH 2/6] docs: implementation plan for creating-release-vote-mail Co-Authored-By: Claude Opus 5 --- ...-08-08-creating-release-vote-mail-skill.md | 669 ++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-08-creating-release-vote-mail-skill.md 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 0000000000..a55718a9e6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-08-creating-release-vote-mail-skill.md @@ -0,0 +1,669 @@ +# `creating-release-vote-mail` Skill Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a `creating-release-vote-mail` skill that drafts the `[VOTE] Apache Struts X.Y.Z` mail as a plain-text rendering of the already-published Version Notes page. + +**Architecture:** Two files under `.claude/skills/creating-release-vote-mail/`, matching the shape of the three sibling skills in the same directory — `SKILL.md` carries judgement (preconditions, recipients, transform rules, failure modes) and `vote-mail-template.md` carries the frozen artifact (headers, slots, ASF vote boilerplate). A one-line handoff is added to `creating-version-notes` so the two chain. + +**Tech Stack:** Markdown skill files with YAML frontmatter; Gmail MCP (`create_draft`); `gh` CLI and `curl` for precondition checks. + +**Spec:** [`docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md`](../specs/2026-08-08-release-vote-mail-skill-design.md) + +## Global Constraints + +- Skill directory: `.claude/skills/creating-release-vote-mail/`, sibling to `creating-version-notes`, `creating-security-bulletins`, `triaging-security-reports`. +- Skill name is gerund-form kebab-case, matching every existing skill in that directory. +- Frontmatter is exactly two keys, `name` and `description`, matching the sibling skills. +- The skill **drafts, never sends**. Gmail `create_draft` only. +- Recipients are `To: dev@struts.apache.org`, `Bcc: private@struts.apache.org`. `user@struts.apache.org` must appear nowhere in either file except as a prohibition. +- Sign-off is exactly `On behalf of the Apache Struts project`. +- Staging URL is exactly `https://repository.apache.org/content/repositories/staging/`. +- The ASF vote boilerplate is reproduced **byte-identical** to the archived mails, including `Github release` without its colon. +- No tests ship with this skill, matching `creating-version-notes`. +- Commits use the `docs:` prefix with no Jira ticket — this is process documentation, not framework code. +- All work lands on branch `vote-mail-skill` (already created) and finishes as a PR. Never push to `main`. +- Never `git add -A` in this repo; stage explicit paths and check `git diff --cached --name-only`. + +## File Structure + +| File | Responsibility | +|---|---| +| `.claude/skills/creating-release-vote-mail/vote-mail-template.md` | The artifact: field table, mail skeleton with slots, frozen boilerplate, pre-send checklist. Changes when the mail's wording changes. | +| `.claude/skills/creating-release-vote-mail/SKILL.md` | The judgement: when to use, preconditions, recipient rule, transform rules, verification, Red Flags, Common Mistakes. Changes when a new failure mode is learned. | +| `.claude/skills/creating-version-notes/SKILL.md` | Modified: one handoff line at the end of "The test-build announcement". | + +The split follows the standing decision recorded for `creating-version-notes`: mechanical reference material goes in the template file, every rule requiring judgement stays in `SKILL.md`, because a rule in an unloaded file is a rule that will not be followed. + +--- + +### Task 1: The template file + +**Files:** +- Create: `.claude/skills/creating-release-vote-mail/vote-mail-template.md` +- Reference (read only, do not modify): `.claude/skills/creating-version-notes/version-notes-template.md`, `.claude/skills/creating-security-bulletins/bulletin-template.md` + +**Interfaces:** +- Produces: the slot names ``, ``, `` and the section-block names `Breaking changes`, `Deprecations`, `Issue list`. Task 2's `SKILL.md` refers to these by exactly these names. + +- [ ] **Step 1: Read the two sibling templates for house shape** + +Read `.claude/skills/creating-version-notes/version-notes-template.md` and +`.claude/skills/creating-security-bulletins/bulletin-template.md`. Both open with a +"Companion to `SKILL.md`" preamble, then a `## Fields` table, then the skeleton. Match that +order — do not invent a new structure. + +- [ ] **Step 2: Write the template file** + +Create `.claude/skills/creating-release-vote-mail/vote-mail-template.md` with exactly this content: + +````markdown +# Release Vote Mail Template + +The canonical skeleton for the `[VOTE] Apache Struts X.Y.Z` mail that opens a release vote +on `dev@struts.apache.org`. Companion to [`SKILL.md`](SKILL.md), which covers *how* to +establish what goes in the slots; this file covers *what the mail contains*. + +**This file is the source of truth.** Start every vote mail from the skeleton below, never +from a copy of the previous release's mail — see the Iron Rule in `SKILL.md`. + +## Fields + +| Slot | What goes in it | +|---|---| +| `` | The release being voted on, dotted, e.g. `7.3.0`. Appears in the subject, the opening sentence, the Version Notes URL and the dist path. | +| `` | The same version underscored, e.g. `7_3_0`, for the `STRUTS_` git tag only. | +| `` | One sentence describing the *shape* of the issue list, never its individual tickets. See `SKILL.md`. | +| Breaking changes | Optional block. The Version Notes page's items verbatim, `- ` prefixed. Omit the block when the page has none. | +| Deprecations | Optional block. Same shape. Omit when the page has none. | +| Issue list | The page's issue-type sections in page order, rendered as plain text. Never retyped from JIRA. | + +`Rejected requests` is **not** a slot. The page may carry that section; the mail never does. + +## Skeleton + +``` +Subject: [VOTE] Apache Struts +To: dev@struts.apache.org +Bcc: private@struts.apache.org + +The Apache Struts test build is available. + +Breaking changes + +- +- + +Deprecations + +- + +Bug +[WW-XXXX] - +[WW-XXXX] - + +New Feature +[WW-XXXX] - + +Improvement +[WW-XXXX] - + +Task +[WW-XXXX] - + +Dependency +[WW-XXXX] - + +Release notes: +* https://cwiki.apache.org/confluence/display/WW/Version+Notes+ + +Github release +* https://github.com/apache/struts/releases/tag/STRUTS_ + +Distribution: +* https://dist.apache.org/repos/dist/dev/struts// + +Maven 2 staging repository: +* https://repository.apache.org/content/repositories/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 +``` + +## Frozen text — do not edit + +Everything from `Once you have had a chance to review the test build` to the sign-off is +**byte-frozen**. It is the ASF vote call: the quality options voters tick, the binding-vote +threshold, the 72-hour minimum, and the obligation a binding vote carries. Rewording any of +it changes what the project is asking for. + +Three details that look like typos and are kept deliberately: + +| Detail | Why it stays | +|---|---| +| `Github release` has no trailing colon, unlike the other three link labels | Every archived Struts vote mail reads this way. The template records what ships. | +| The staging URL is `content/repositories/staging/`, while the `[TEST]` mail uses `content/groups/staging/` | Both are valid Nexus endpoints. The vote mail's form is what past votes used; the difference is accepted, not a defect to reconcile. | +| All four quality checkboxes are empty | The release manager's own `+1 (binding)` is a separate reply. A call arriving with GA pre-ticked reads as a decision announced, not a vote opened. | + +## Pre-send checklist + +- [ ] All four links resolve, and the GitHub release is still flagged pre-release +- [ ] Ticket ids in the mail match the page's, excluding the page's `Rejected requests` +- [ ] Boilerplate byte-identical to the frozen text above +- [ ] `To: dev@` only — `user@` absent — and `Bcc: private@` present +- [ ] Subject is exactly `[VOTE] Apache Struts X.Y.Z` +- [ ] All four checkboxes empty +- [ ] Body hard-wrapped at 72 columns, continuation lines unindented +```` + +- [ ] **Step 3: Verify the boilerplate is byte-identical to the archived mail** + +Write the archived 6.10.0 boilerplate to a scratchpad file and diff the template's copy +against it. Both mails carry identical boilerplate, so either is a valid reference. + +```bash +SCRATCH=/private/tmp/claude-501/-Users-lukaszlenart-Projects-Apache-struts/322a9b9b-d830-4ab7-a7e8-2e30a7682260/scratchpad +cat > "$SCRATCH/boilerplate-reference.txt" <<'EOF' +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. +EOF + +sed -n '/^Once you have had a chance/,/^to help do the work\.$/p' \ + .claude/skills/creating-release-vote-mail/vote-mail-template.md \ + > "$SCRATCH/boilerplate-template.txt" + +diff "$SCRATCH/boilerplate-reference.txt" "$SCRATCH/boilerplate-template.txt" +``` + +Expected: no output. Any diff means the boilerplate was retyped rather than copied — fix the +template, do not adjust the reference. + +- [ ] **Step 4: Verify the constrained strings** + +```bash +grep -n 'user@struts.apache.org' .claude/skills/creating-release-vote-mail/vote-mail-template.md +grep -c 'content/repositories/staging/' .claude/skills/creating-release-vote-mail/vote-mail-template.md +grep -n 'On behalf of the Apache Struts project' .claude/skills/creating-release-vote-mail/vote-mail-template.md +``` + +Expected: the first prints nothing (exit 1); the second prints `2` (skeleton plus the +frozen-text table row); the third prints one line. + +- [ ] **Step 5: Commit** + +```bash +git add .claude/skills/creating-release-vote-mail/vote-mail-template.md +git diff --cached --name-only +git commit -m "docs: add the release vote mail template + +Freezes the [VOTE] Apache Struts X.Y.Z skeleton: slots, link block, and +the ASF vote boilerplate reproduced byte-identical to the archived 6.10.0 +and 7.2.1 mails. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 2: The skill file + +**Files:** +- Create: `.claude/skills/creating-release-vote-mail/SKILL.md` +- Reference (read only): `.claude/skills/creating-version-notes/SKILL.md` + +**Interfaces:** +- Consumes: from Task 1, the slot names ``, ``, `` and the block names `Breaking changes`, `Deprecations`, `Issue list`; the pre-send checklist, which `SKILL.md` points at rather than duplicating. +- Produces: the skill `name: creating-release-vote-mail`, which Task 3's handoff line names. + +- [ ] **Step 1: Confirm the description does not collide with the sibling** + +```bash +grep -n '^description:' .claude/skills/*/SKILL.md +``` + +Expected: `creating-version-notes` ends its description at "the test-build announcement mail". +The new description must begin where that ends and must not contain the words "Version Notes +page", "Migration Guide" or "GitHub release notes" as things it *produces* — those are its +inputs. Two skills claiming the same trigger is the failure this step prevents. + +- [ ] **Step 2: Write `SKILL.md`** + +Create `.claude/skills/creating-release-vote-mail/SKILL.md` with this content: + +````markdown +--- +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. Everything below the opening sentence is a transform of a published cwiki section, +so the mail cannot assert something the page does not. + +**This is the step after [`creating-version-notes`](../creating-version-notes/SKILL.md).** +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. + +## The Iron Rule + +``` +THE VERSION NOTES PAGE IS THE ONLY SOURCE FOR THE BODY. +NEVER RETYPE THE ISSUE LIST, AND NEVER CLONE THE PREVIOUS VOTE MAIL. +``` + +Cloning fails the same way it fails for Version Notes pages: the version number gets updated +and the surrounding text does not. The 7.2.1 and 6.10.0 mails already disagree on their +sign-off, which is what cloning drift looks like before anyone notices it. + +[`vote-mail-template.md`](vote-mail-template.md) is the source of truth for the artifact — +slots, link block, frozen boilerplate, and the pre-send checklist. + +## Check every precondition before drafting + +The mail is four links. A vote opened on a 404 burns the 72-hour window before anyone can +test, and the mail cannot be recalled from a public archive. + +| Link | Produced by | How to check | +|---|---|---| +| `Version+Notes+X.Y.Z` on cwiki | `creating-version-notes` | Fetch it — it is also the body source | +| `releases/tag/STRUTS_X_Y_Z` | `creating-version-notes` | `gh release view STRUTS_X_Y_Z --json isPrerelease,url` — must still be a prerelease | +| `dist/dev/struts/X.Y.Z/` | The release build | `curl -sI` the directory; artifacts and their `.asc`/`.sha512` present | +| Nexus staging | `mvn release` | The staging repository is open, not dropped or already released | + +A GitHub release that is no longer a prerelease means someone promoted it before the vote +closed. Stop and resolve that before drafting. + +## Recipients + +``` +To: dev@struts.apache.org +Bcc: private@struts.apache.org +``` + +**`user@` must not appear.** The `[TEST]` mail one step earlier goes to `dev@` *and* `user@`, +which is correct for it — it asks people to test. This mail asks people to *vote*, and a vote +invitation on the user list solicits votes from people whose votes are not binding. + +The subject is exactly `[VOTE] Apache Struts X.Y.Z`. No "test build", no RC suffix, no +release-quality word. + +## The opening sentence is the only prose you author + +Two sentences. The first is fixed: `The Apache Struts X.Y.Z test build is available.` The +second describes the *shape* of the issue list. + +| The page has | Second sentence | +|---|---| +| No Breaking changes | `With this release the following issues were addressed:` | +| Breaking changes | `This release contains . Also a lot of dependencies have been updated:` | + +Fill `` from what the list actually holds — `a few minor breaking changes plus some bug +fixes` for 7.2.1. **Never preview individual tickets here.** The list below it is the preview; +a ticket named in the opener is a ticket you have decided matters more than its neighbours, +which is an editorial judgement the vote mail has no business making. + +## Transform rules, per section + +| Page section | In the mail | +|---|---| +| Breaking changes | Verbatim, `- ` prefixed, ticket refs as bare `[WW-XXXX]` text | +| Deprecations | Verbatim, same shape | +| Rejected requests | **Omitted** | +| Bug / New Feature / Improvement / Task / Dependency | Page order, heading bare on its own line, entries `[WW-XXXX] - ` | + +**Copy Breaking changes verbatim.** Verbatim is what holds each item at the page's +one-sentence form. Re-authoring them from the tickets is how the 7.2.1 items grew to three +clauses each, which is longer than the rule the page itself follows. + +**Deprecations are in; Rejected requests are out.** A deprecation tells a voter what to check +in their own application, so it belongs in front of the people testing. A `Won't Do` ticket +has nothing to test — it is release documentation, and the Release notes link carries it. + +**Plain text carries no links.** A ticket reference is the bare string `[WW-XXXX]`, and the +only URLs in the mail are the four link lines. + +**Hard-wrap at 72 columns**, continuation lines unindented. This is what keeps the issue list +legible in the ASF archives and in the quoted replies voters send back. + +## A security summary the page truncated stays truncated + +Where the Version Notes page stopped a ticket summary at a clause boundary because its +bulletin is unpublished, the mail stops at the same boundary. + +`dev@struts.apache.org` is a **public, permanently archived** list. Re-expanding a truncated +summary there publishes what the page deliberately withheld, to a wider audience than the +page, and no correction removes it from the archive. + +Nothing else about the release's security posture reaches this mail either: no severity, no +attack description, no S2-XXX or CVE that has not been published. + +**REQUIRED BACKGROUND:** where the wording of a security-relevant entry is in question, +`creating-security-bulletins` governs what may be said and when. + +## A ticket/prose version mismatch is not an error + +A Dependency entry may name a lower version than the Breaking changes prose. 7.2.1 lists +`[WW-5536] - Bump ognl:ognl from 3.3.5 to 3.4.8` while its Breaking changes says OGNL went to +3.4.11. Both are correct: the entry reproduces the ticket summary verbatim, the prose states +what shipped, and patch bumps land after a ticket is titled. + +**Do not reconcile it in the mail.** Changing either one makes the mail disagree with the +page, which is the one thing a rendering may never do. + +## Verify, then draft + +Run the pre-send checklist in [`vote-mail-template.md`](vote-mail-template.md). The one worth +scripting is the ticket-set comparison — sort the `WW-` ids in your draft against the page's, +excluding the page's `Rejected requests` section. Empty output, or the mail is not a rendering +and you should find out which direction the difference runs before sending. + +## Draft it, do not send it + +Create a Gmail draft with `To`, `Bcc`, `Subject` and body set. **Never send.** + +Sending opens a binding project vote on a public list, starts the 72-hour clock, and commits +the PMC to the artifacts as staged. That is the release manager's keystroke, not yours. + +## Red Flags — STOP + +- Starting from a copy of the previous release's vote mail +- Retyping the issue list from JIRA instead of rendering the page +- Re-expanding a summary the page truncated +- Adding `user@struts.apache.org` to the recipients +- Pre-ticking a quality level in the checkbox block +- Drafting before the page, tag, dist path or staging repo exist +- A severity, CVE or S2-XXX reference anywhere in the mail +- Editing the boilerplate wording +- "Correcting" a Dependency entry so it matches the Breaking changes prose +- Naming individual tickets in the opening sentence +- Sending, rather than drafting + +## Common Mistakes + +| Mistake | Reality | +|---|---| +| "Last release's vote mail is the fastest start" | It is how the sign-off drifted between 6.10.0 and 7.2.1. Start from the template. | +| "The `[TEST]` mail went to `user@`, so this should too" | That mail asks people to test. This one asks people to vote, and user-list votes are not binding. `dev@` only. | +| "The ticket says 3.4.8 but we shipped 3.4.11" | Both are right. The entry is the ticket summary verbatim; the prose is what shipped. Leave it. | +| "The ticket is public, so I can describe the vulnerability" | A public ticket does not publish the advisory, and `dev@` is archived forever. Neutral framing until the bulletin ships. | +| "The page is up, so I can draft" | Check the tag, the dist path and the staging repo too. A vote on a 404 wastes 72 hours. | +| "I'll tick GA since that's what we want" | The release manager's `+1` is a separate reply. A pre-ticked call announces a decision instead of opening a vote. | +| "Rejected requests are part of the release, so list them" | Voters test artifacts. A `Won't Do` ticket has nothing to test; the page link carries it. | +| "It only needs the ticket numbers, I'll type them out" | Typing is the failure mode. Render the page. | +```` + +- [ ] **Step 3: Verify frontmatter and cross-references** + +```bash +head -4 .claude/skills/creating-release-vote-mail/SKILL.md +grep -c 'vote-mail-template.md' .claude/skills/creating-release-vote-mail/SKILL.md +grep -n 'user@struts.apache.org' .claude/skills/creating-release-vote-mail/SKILL.md +``` + +Expected: frontmatter is `---`, `name:`, `description:`, `---` and nothing else; the template +is referenced at least 3 times; `user@` appears only in the prohibition and the two mistake +rows that explain it — read each hit and confirm none of them is a recipient instruction. + +- [ ] **Step 4: Commit** + +```bash +git add .claude/skills/creating-release-vote-mail/SKILL.md +git diff --cached --name-only +git commit -m "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. Records the dev@-only recipient rule, the +truncated-security-summary carry-through, and the draft-never-send +boundary. + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 3: Chain it to `creating-version-notes` + +**Files:** +- Modify: `.claude/skills/creating-version-notes/SKILL.md` — end of the `## The test-build announcement` section, immediately before `## Re-read the page immediately before you write to it` + +**Interfaces:** +- Consumes: the skill name `creating-release-vote-mail` from Task 2. + +- [ ] **Step 1: Locate the insertion point** + +```bash +grep -n '^## ' .claude/skills/creating-version-notes/SKILL.md +``` + +The handoff goes at the **end** of `## The test-build announcement`, after the paragraph +beginning `Keep the security posture of the pages`. + +- [ ] **Step 2: Add the handoff line** + +Append this paragraph to that section: + +```markdown +**The vote is the next step, and it is a different mail.** Once the `[TEST]` build has been +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 differ from the announcement above. +``` + +- [ ] **Step 3: Verify nothing else moved** + +```bash +git diff --stat .claude/skills/creating-version-notes/SKILL.md +git diff .claude/skills/creating-version-notes/SKILL.md +``` + +Expected: `1 file changed, N insertions(+)` with **no deletions**. Any deletion means a +neighbouring section was disturbed — this skill's own rules warn about swallowing an adjacent +heading, and the same care applies to editing it. + +- [ ] **Step 4: Commit** + +```bash +git add .claude/skills/creating-version-notes/SKILL.md +git diff --cached --name-only +git commit -m "docs: point creating-version-notes at the vote mail step + +Co-Authored-By: Claude Opus 5 " +``` + +--- + +### Task 4: Dry run against the archived 6.10.0 vote + +This is the closest thing the skill has to a test: render a mail the project already sent, and +compare. 6.10.0 is the better subject than 7.2.1 — its issue list is three tickets, so a diff +is readable, and it has no Breaking changes block, which exercises the omission path. + +**Files:** +- No repository files change. Working files go in the scratchpad. + +**Interfaces:** +- Consumes: `SKILL.md` and `vote-mail-template.md` from Tasks 1 and 2. + +- [ ] **Step 1: Capture the archived mail as the expected output** + +Fetch the sent `[VOTE] Apache Struts 6.10.0` mail (Gmail thread `19e5fc8e0a444f92`, message +`19e5fcd6bf35b65b`) and save its plaintext body to `$SCRATCH/expected-6.10.0.txt`. + +- [ ] **Step 2: Render a mail from the published page, following the skill** + +Follow `SKILL.md` end to end for version `6.10.0`, sourcing the body from +`https://cwiki.apache.org/confluence/display/WW/Version+Notes+6.10.0`. Write the result to +`$SCRATCH/rendered-6.10.0.txt` instead of creating a draft. + +Skip the precondition checks that cannot pass for a shipped release — the 6.10.0 GitHub +release is no longer a prerelease and its staging repo is long closed. Note which checks you +skipped; that list is itself a finding if it is longer than those two. + +- [ ] **Step 3: Diff and classify every difference** + +```bash +diff -u "$SCRATCH/expected-6.10.0.txt" "$SCRATCH/rendered-6.10.0.txt" +``` + +Expected: differences only in the sign-off (`Kind regards` → `On behalf of the Apache Struts +project`, a deliberate normalisation from the spec) and in wrapping of the authored opener. + +Classify each remaining difference into exactly one bucket: + +| Bucket | Action | +|---|---| +| Deliberate normalisation named in the spec | None — expected | +| The skill produced something wrong | Fix `SKILL.md` or the template, re-run from Step 2 | +| The archived mail was wrong | Add a Common Mistakes row so the skill prevents it next time | + +- [ ] **Step 4: Verify the ticket-set check actually works** + +```bash +grep -o 'WW-[0-9]*' "$SCRATCH/rendered-6.10.0.txt" | sort -u +``` + +Expected: exactly `WW-5623`, `WW-5628`, `WW-5629` — matching the page's three tickets. If the +rendering dropped or invented one, the transform rules in Task 2 need fixing. + +- [ ] **Step 5: Commit any corrections** + +If Steps 3 or 4 required changes: + +```bash +git add .claude/skills/creating-release-vote-mail/ +git diff --cached --name-only +git commit -m "docs: correct the vote mail skill from the 6.10.0 dry run + +Co-Authored-By: Claude Opus 5 " +``` + +If nothing changed, record the dry-run outcome in the PR description instead and move on. + +--- + +### Task 5: Open the PR + +**Files:** +- No file changes. + +- [ ] **Step 1: Review the whole branch before pushing** + +```bash +git log --oneline main..vote-mail-skill +git diff main...vote-mail-skill +``` + +Read the full diff. Every change on this branch gets the same review gate, including the +corrections Task 4 may have added. + +- [ ] **Step 2: Confirm this is not a security patch** + +This branch adds process documentation and touches no framework code. Confirm with: + +```bash +git diff --name-only main...vote-mail-skill +``` + +Expected: only paths under `.claude/skills/` and `docs/superpowers/`. Anything under `core/` +or `plugins/` means something unintended was staged — stop and investigate before pushing. + +- [ ] **Step 3: Push and open the PR** + +```bash +git push -u origin vote-mail-skill +gh pr create --title "docs: add creating-release-vote-mail skill" --body "$(cat <<'EOF' +Adds a `creating-release-vote-mail` skill that drafts the `[VOTE] Apache Struts X.Y.Z` mail +as a plain-text rendering of the already-published Version Notes page. + +It is the step after `creating-version-notes`: that skill ends at the `[TEST]` announcement, +this one opens the vote. + +- `vote-mail-template.md` freezes the skeleton and the ASF vote boilerplate, reproduced + byte-identical to the archived 6.10.0 and 7.2.1 mails. +- `SKILL.md` records the judgement: `dev@`-only recipients, Deprecations in and Rejected + requests out, truncated security summaries carrying through unchanged, and drafting rather + than sending. +- Validated by re-rendering the archived 6.10.0 vote mail from its published page. + +Design: `docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md` + +No Jira ticket — process documentation, consistent with the `creating-version-notes` and +`creating-security-bulletins` skill commits. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +## Self-Review + +**Spec coverage.** Every spec section maps to a task: identity and frontmatter → Task 2 Step 2; +preconditions table → Task 2; recipients → Tasks 1 and 2, verified in Task 1 Step 4 and Task 2 +Step 3; opening sentence → Task 2; transform rules including Deprecations-in/Rejected-out → +Task 2; security truncation → Task 2; template with frozen sign-off and staging URL → Task 1; +empty checkboxes → Task 1; verification checklist → Task 1 Step 2, exercised in Task 4; +Gmail-draft-never-send → Task 2; Red Flags and Common Mistakes → Task 2; "no tests" → Global +Constraints, with Task 4 standing in as a dry run. The spec's position-in-flow diagram is what +Task 3 implements. + +**Placeholders.** None. Every file's full content is given inline; every command is runnable; +every "Expected:" states a concrete result. + +**Name consistency.** `creating-release-vote-mail` (skill name, directory, frontmatter, Task 3 +handoff, PR title), `vote-mail-template.md` (Task 1 creates it, Task 2 links it three times), +and the slot names `` / `` / `` are used identically in Tasks 1 +and 2. Branch `vote-mail-skill` is the same in Global Constraints and Task 5. From 30cc46a757798b526b7a323e3f0814addd902bb0 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 07:48:31 +0200 Subject: [PATCH 3/6] 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 --- ...26-08-08-release-vote-mail-skill-design.md | 288 +++++++----------- 1 file changed, 112 insertions(+), 176 deletions(-) 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 index d4efd6b2a2..8d8df704c2 100644 --- 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 @@ -1,26 +1,24 @@ # `creating-release-vote-mail` skill — design **Date:** 2026-08-08 -**Status:** approved, ready for implementation +**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 mostly frozen ASF boilerplate wrapped around a plain-text rendering of the -release's Version Notes page, but it is currently written by hand each time. Hand-writing -it produces drift — the 7.2.1 and 6.10.0 mails disagree on their sign-off — and it re-states, -in a public archived list, content whose canonical form already exists on the cwiki. +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, and the `[ANN]` -announcement. Those are later stages and may become their own skill if wanted. +**Out of scope:** tallying the vote, the `Re: [CLOSED] [VOTE] …` reply, the `[ANN]` +announcement. ## Position in the release flow -The skill is a sibling of `creating-version-notes` and runs immediately after it: +Sibling of `creating-version-notes`, running immediately after it: ``` Version Notes page ─┐ @@ -28,8 +26,41 @@ GitHub release ├─ creating-version-notes ─→ [TEST] mail ─→ crea staged artifacts ─┘ ``` -`creating-version-notes` ends at the `[TEST]` announcement; this skill begins there. Their -frontmatter descriptions therefore do not compete for the same trigger. +## 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 @@ -43,202 +74,107 @@ frontmatter descriptions therefore do not compete for the same trigger. > 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. -## Core principle - -**The mail is a rendering of the Version Notes page, not a second account of the release.** - -Everything below the opening sentence is a plain-text transform of a published cwiki section. -Nothing is re-authored, so the mail cannot assert something the page does not. +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. -### The Iron Rule +## What the skill contains -``` -THE VERSION NOTES PAGE IS THE ONLY SOURCE FOR THE BODY. -NEVER RETYPE THE ISSUE LIST, AND NEVER CLONE THE PREVIOUS VOTE MAIL. -``` +### 1. The body recipe (addresses the bloat failure) -Cloning is the same failure `creating-version-notes` bans for the same reason: the number -gets updated and the surrounding text does not. +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: -## Preconditions +> 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. -The mail is four links wrapped in boilerplate. All four must resolve *before* drafting — -a vote opened on a 404 burns the 72-hour window before anyone can test. - -| Link | Produced by | Check | -|---|---|---| -| `Version+Notes+X.Y.Z` on cwiki | `creating-version-notes` | fetch it — it is also the body source | -| `releases/tag/STRUTS_X_Y_Z` | `creating-version-notes` | `gh release view`, must still be `--prerelease` | -| `dist/dev/struts/X.Y.Z/` | release build | HTTP check; artifacts and signatures present | -| Nexus `content/repositories/staging/` | `mvn release` | staging repo open, not dropped | +A part not on that list is not in the mail. Stated as a shape, there is nothing to negotiate. -## Recipients +### 2. Recipients (addresses the `user@` failure) ``` To: dev@struts.apache.org Bcc: private@struts.apache.org ``` -**`user@` must not appear.** The `[TEST]` mail one step earlier goes to both `dev@` and -`user@`; the `[VOTE]` mail goes to `dev@` alone, because a vote invitation on the user list -solicits votes from people whose votes are not binding. Both sampled mails got this right; -the skill records *why* so it stays right. - -Subject is exactly `[VOTE] Apache Struts X.Y.Z` — no "test build", no RC suffix. - -## The opening sentence — the only authored prose - -Two sentences: the fixed `The Apache Struts X.Y.Z test build is available.` plus one -describing the *shape* of the issue list, never its individual contents. - -| Page has | Second sentence | -|---|---| -| no Breaking changes | `With this release the following issues were addressed:` (as in 6.10.0) | -| Breaking changes | `This release contains . Also a lot of dependencies have been updated:` (as in 7.2.1) | - -## Transform rules, per section - -- **Breaking changes** — the page's items verbatim, `- ` prefixed, ticket references as bare - `[WW-XXXX]` text (plain-text mail carries no links). Verbatim copying is what holds them at - the page's one-sentence form; re-authoring is how the 7.2.1 items grew to three clauses. -- **Deprecations** — included when the page has them. A deprecation tells a voter what to - check in their own application, so it belongs in front of the people testing. -- **Rejected requests** — **not** included. A `Won't Do` ticket has nothing to test; it is - release documentation, and the Release notes link carries it. -- **Issue-type sections** — page order (Bug → New Feature → Improvement → Task → Dependency), - heading bare on its own line, entries `[WW-XXXX] - `, blank line between sections. - Omit any type the page omits. -- **Security-truncated summaries carry through exactly as truncated.** Where the page stopped - a summary at a clause boundary because its bulletin is unpublished, the mail stops there too. - Re-expanding it publishes to `dev@` — a public archived list — what the page deliberately - withheld. Cross-references `creating-security-bulletins`. -- **Hard-wrap at 72 columns**, continuation lines unindented, matching both sampled mails. - This keeps the list legible in the ASF archives and in quoted replies. - -### A ticket/page mismatch is not automatically an error - -A Dependency entry may name a lower version than the Breaking changes prose: 7.2.1 lists -`[WW-5536] - Bump ognl:ognl from 3.3.5 to 3.4.8` while its Breaking changes says OGNL went to -3.4.11. Both are correct — the entry reproduces the ticket summary verbatim, the prose states -what shipped. `creating-version-notes` mandates exactly this. Do not "fix" it in the mail. +`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". -## The template - -`vote-mail-template.md` holds the headers, the slots, and the frozen tail: +Bcc rather than Cc for `private@` has a reason worth recording: 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. -``` -Subject: [VOTE] Apache Struts -To: dev@struts.apache.org -Bcc: private@struts.apache.org +### 3. Draft, never send (addresses the send failure) -The Apache Struts test build is available. +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) -Release notes: -* https://cwiki.apache.org/confluence/display/WW/Version+Notes+ +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. -Github release -* https://github.com/apache/struts/releases/tag/STRUTS_ +### 5. The vote carries no security information, on any channel -Distribution: -* https://dist.apache.org/repos/dist/dev/struts// +**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. -Maven 2 staging repository: -* https://repository.apache.org/content/repositories/staging/ +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. -Once you have had a chance to review the test build, please respond -with a vote on its quality: +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. -[ ] Leave at test build -[ ] Alpha -[ ] Beta -[ ] General Availability (GA) +Neutral ticket summaries carried over from the page are not security information and stay, +truncated exactly as the page truncates them. -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. +### 6. Cross-references, not restatements -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. +For everything the baselines already got right, the skill points at the skill that taught it +rather than repeating it: -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. +- `creating-version-notes` — the page, the release, and what belongs on them +- `creating-security-bulletins` — what may be said about an unpublished advisory -On behalf of the Apache Struts project -Łukasz -``` - -Two deliberate choices frozen here: - -- **Sign-off** is `On behalf of the Apache Struts project` (7.2.1's form, not 6.10.0's - `Kind regards`) — it reads as the PMC opening a formal vote rather than a personal note. -- **Staging URL** is `content/repositories/staging/`, as both sampled vote mails used. It - differs from the `[TEST]` mail's `content/groups/staging/`; that difference is accepted, - not a defect to reconcile. - -`Github release` keeps its missing colon and the boilerplate keeps its exact wording. The -template records what ships; it does not improve it. - -### All four checkboxes ship empty - -The release manager's own vote is a separate reply (`+1 (binding)`), as both sampled threads -show. A call that arrives with a quality level already ticked reads as a decision announced -rather than a vote opened. +## The template -## Verification before creating the draft +`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. -1. All four links resolve; the GitHub release is still flagged pre-release. -2. Ticket sets match — `diff` the mail's `WW-` ids against the page's, excluding the page's - `Rejected requests` section, which the mail deliberately omits. Empty output, or the mail - is not a rendering. -3. Boilerplate byte-identical to the template. -4. `To`/`Bcc` correct, `user@` absent, subject exactly `[VOTE] Apache Struts X.Y.Z`. -5. All four checkboxes empty. +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 -The skill composes the mail and calls Gmail `create_draft` with To, Bcc, Subject and body set. - -**The skill drafts; it never sends.** Sending opens a binding project vote, which stays the -release manager's keystroke. - -## Failure modes recorded in the skill - -`SKILL.md` closes with the two tables the sibling skills use. - -**Red Flags — STOP:** - -- Cloning the previous release's vote mail -- Retyping the issue list instead of rendering the page -- Re-expanding a summary the page truncated -- Adding `user@struts.apache.org` to the recipients -- Pre-ticking a quality level -- Drafting before the page, tag or dist path exist -- Putting a severity, CVE or S2-XXX reference in the mail -- Editing the boilerplate wording -- "Correcting" a Dependency entry to match the Breaking changes prose - -**Common Mistakes** pairs each with its reality, e.g.: +Gmail `create_draft` with To, Bcc, Subject and body set. Never send. -| Mistake | Reality | -|---|---| -| "Last release's vote mail is the fastest start" | It is how the sign-off drifted between 6.10.0 and 7.2.1. Start from the template. | -| "The [TEST] mail went to user@, so this should too" | A vote invitation on the user list solicits non-binding votes. `dev@` only. | -| "The ticket says 3.4.8 but we shipped 3.4.11" | Both are right. The entry is the ticket summary verbatim; the prose is what shipped. | -| "The fix is public, so I can describe it" | A public ticket does not publish the advisory. `dev@` is archived. Neutral framing until the bulletin ships. | -| "The page is up, so I can draft" | Check the tag, the dist path and the staging repo too. A vote on a 404 wastes 72 hours. | +## Testing -## Tests +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. -Ships without tests, matching `creating-version-notes`. A failing baseline under -`writing-skills` requires subagents, which are not spawned unprompted. Tests can be added on -request. +Baseline transcripts and outputs are kept in the session scratchpad, not committed. From 3a40b458845dd6624e51baa6cc5f1a21b47630bd Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 07:50:17 +0200 Subject: [PATCH 4/6] 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 --- .../creating-release-vote-mail/SKILL.md | 131 +++++++++++++++++ .../vote-mail-template.md | 139 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 .claude/skills/creating-release-vote-mail/SKILL.md create mode 100644 .claude/skills/creating-release-vote-mail/vote-mail-template.md 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 0000000000..6429f87163 --- /dev/null +++ b/.claude/skills/creating-release-vote-mail/SKILL.md @@ -0,0 +1,131 @@ +--- +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: dev@struts.apache.org +Bcc: private@struts.apache.org +``` + +**`user@struts.apache.org` 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@` 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. + +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 +- `user@struts.apache.org` 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. | +| "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 0000000000..f6a5b84393 --- /dev/null +++ b/.claude/skills/creating-release-vote-mail/vote-mail-template.md @@ -0,0 +1,139 @@ +# 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 | +|---|---| +| `` | The release being voted on, dotted — subject, opening sentence, Version Notes URL, dist path | +| `` | The same version underscored, for the `STRUTS_` git tag only | +| `` | 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. | + +## 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 :` | + +**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 +To: dev@struts.apache.org +Bcc: private@struts.apache.org + +The Apache Struts test build is available. + +Breaking changes + +- + +Deprecations + +- + +Rejected requests + +[WW-XXXX] - + +Bug +[WW-XXXX] - + +New Feature +[WW-XXXX] - + +Improvement +[WW-XXXX] - + +Task +[WW-XXXX] - + +Dependency +[WW-XXXX] - + +Release notes: +* https://cwiki.apache.org/confluence/display/WW/Version+Notes+ + +Github release +* https://github.com/apache/struts/releases/tag/STRUTS_ + +Distribution: +* https://dist.apache.org/repos/dist/dev/struts// + +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 From 6192128388ce92609543c63a64db48084364fd60 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 07:56:41 +0200 Subject: [PATCH 5/6] 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 --- .../vote-mail-template.md | 6 + .../skills/creating-version-notes/SKILL.md | 2 + ...-08-08-creating-release-vote-mail-skill.md | 764 +++--------------- 3 files changed, 108 insertions(+), 664 deletions(-) diff --git a/.claude/skills/creating-release-vote-mail/vote-mail-template.md b/.claude/skills/creating-release-vote-mail/vote-mail-template.md index f6a5b84393..c25cc9083f 100644 --- a/.claude/skills/creating-release-vote-mail/vote-mail-template.md +++ b/.claude/skills/creating-release-vote-mail/vote-mail-template.md @@ -15,6 +15,12 @@ Companion to [`SKILL.md`](SKILL.md), which covers *how* to fill the slots; this | `` | 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 diff --git a/.claude/skills/creating-version-notes/SKILL.md b/.claude/skills/creating-version-notes/SKILL.md index b15aa5cb3b..e52ca187d5 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 index a55718a9e6..1d3bab84d1 100644 --- 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 @@ -1,669 +1,105 @@ -# `creating-release-vote-mail` Skill Implementation Plan +# `creating-release-vote-mail` — implementation record -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +**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) -**Goal:** Add a `creating-release-vote-mail` skill that drafts the `[VOTE] Apache Struts X.Y.Z` mail as a plain-text rendering of the already-published Version Notes page. +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. -**Architecture:** Two files under `.claude/skills/creating-release-vote-mail/`, matching the shape of the three sibling skills in the same directory — `SKILL.md` carries judgement (preconditions, recipients, transform rules, failure modes) and `vote-mail-template.md` carries the frozen artifact (headers, slots, ASF vote boilerplate). A one-line handoff is added to `creating-version-notes` so the two chain. +## What was built -**Tech Stack:** Markdown skill files with YAML frontmatter; Gmail MCP (`create_draft`); `gh` CLI and `curl` for precondition checks. - -**Spec:** [`docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md`](../specs/2026-08-08-release-vote-mail-skill-design.md) - -## Global Constraints - -- Skill directory: `.claude/skills/creating-release-vote-mail/`, sibling to `creating-version-notes`, `creating-security-bulletins`, `triaging-security-reports`. -- Skill name is gerund-form kebab-case, matching every existing skill in that directory. -- Frontmatter is exactly two keys, `name` and `description`, matching the sibling skills. -- The skill **drafts, never sends**. Gmail `create_draft` only. -- Recipients are `To: dev@struts.apache.org`, `Bcc: private@struts.apache.org`. `user@struts.apache.org` must appear nowhere in either file except as a prohibition. -- Sign-off is exactly `On behalf of the Apache Struts project`. -- Staging URL is exactly `https://repository.apache.org/content/repositories/staging/`. -- The ASF vote boilerplate is reproduced **byte-identical** to the archived mails, including `Github release` without its colon. -- No tests ship with this skill, matching `creating-version-notes`. -- Commits use the `docs:` prefix with no Jira ticket — this is process documentation, not framework code. -- All work lands on branch `vote-mail-skill` (already created) and finishes as a PR. Never push to `main`. -- Never `git add -A` in this repo; stage explicit paths and check `git diff --cached --name-only`. - -## File Structure - -| File | Responsibility | +| File | Purpose | |---|---| -| `.claude/skills/creating-release-vote-mail/vote-mail-template.md` | The artifact: field table, mail skeleton with slots, frozen boilerplate, pre-send checklist. Changes when the mail's wording changes. | -| `.claude/skills/creating-release-vote-mail/SKILL.md` | The judgement: when to use, preconditions, recipient rule, transform rules, verification, Red Flags, Common Mistakes. Changes when a new failure mode is learned. | -| `.claude/skills/creating-version-notes/SKILL.md` | Modified: one handoff line at the end of "The test-build announcement". | - -The split follows the standing decision recorded for `creating-version-notes`: mechanical reference material goes in the template file, every rule requiring judgement stays in `SKILL.md`, because a rule in an unloaded file is a rule that will not be followed. - ---- - -### Task 1: The template file - -**Files:** -- Create: `.claude/skills/creating-release-vote-mail/vote-mail-template.md` -- Reference (read only, do not modify): `.claude/skills/creating-version-notes/version-notes-template.md`, `.claude/skills/creating-security-bulletins/bulletin-template.md` - -**Interfaces:** -- Produces: the slot names ``, ``, `` and the section-block names `Breaking changes`, `Deprecations`, `Issue list`. Task 2's `SKILL.md` refers to these by exactly these names. - -- [ ] **Step 1: Read the two sibling templates for house shape** - -Read `.claude/skills/creating-version-notes/version-notes-template.md` and -`.claude/skills/creating-security-bulletins/bulletin-template.md`. Both open with a -"Companion to `SKILL.md`" preamble, then a `## Fields` table, then the skeleton. Match that -order — do not invent a new structure. - -- [ ] **Step 2: Write the template file** - -Create `.claude/skills/creating-release-vote-mail/vote-mail-template.md` with exactly this content: - -````markdown -# Release Vote Mail Template - -The canonical skeleton for the `[VOTE] Apache Struts X.Y.Z` mail that opens a release vote -on `dev@struts.apache.org`. Companion to [`SKILL.md`](SKILL.md), which covers *how* to -establish what goes in the slots; this file covers *what the mail contains*. - -**This file is the source of truth.** Start every vote mail from the skeleton below, never -from a copy of the previous release's mail — see the Iron Rule in `SKILL.md`. - -## Fields - -| Slot | What goes in it | -|---|---| -| `` | The release being voted on, dotted, e.g. `7.3.0`. Appears in the subject, the opening sentence, the Version Notes URL and the dist path. | -| `` | The same version underscored, e.g. `7_3_0`, for the `STRUTS_` git tag only. | -| `` | One sentence describing the *shape* of the issue list, never its individual tickets. See `SKILL.md`. | -| Breaking changes | Optional block. The Version Notes page's items verbatim, `- ` prefixed. Omit the block when the page has none. | -| Deprecations | Optional block. Same shape. Omit when the page has none. | -| Issue list | The page's issue-type sections in page order, rendered as plain text. Never retyped from JIRA. | - -`Rejected requests` is **not** a slot. The page may carry that section; the mail never does. - -## Skeleton - -``` -Subject: [VOTE] Apache Struts -To: dev@struts.apache.org -Bcc: private@struts.apache.org - -The Apache Struts test build is available. - -Breaking changes - -- -- - -Deprecations - -- - -Bug -[WW-XXXX] - -[WW-XXXX] - - -New Feature -[WW-XXXX] - - -Improvement -[WW-XXXX] - - -Task -[WW-XXXX] - - -Dependency -[WW-XXXX] - - -Release notes: -* https://cwiki.apache.org/confluence/display/WW/Version+Notes+ - -Github release -* https://github.com/apache/struts/releases/tag/STRUTS_ - -Distribution: -* https://dist.apache.org/repos/dist/dev/struts// - -Maven 2 staging repository: -* https://repository.apache.org/content/repositories/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 -``` - -## Frozen text — do not edit - -Everything from `Once you have had a chance to review the test build` to the sign-off is -**byte-frozen**. It is the ASF vote call: the quality options voters tick, the binding-vote -threshold, the 72-hour minimum, and the obligation a binding vote carries. Rewording any of -it changes what the project is asking for. - -Three details that look like typos and are kept deliberately: - -| Detail | Why it stays | -|---|---| -| `Github release` has no trailing colon, unlike the other three link labels | Every archived Struts vote mail reads this way. The template records what ships. | -| The staging URL is `content/repositories/staging/`, while the `[TEST]` mail uses `content/groups/staging/` | Both are valid Nexus endpoints. The vote mail's form is what past votes used; the difference is accepted, not a defect to reconcile. | -| All four quality checkboxes are empty | The release manager's own `+1 (binding)` is a separate reply. A call arriving with GA pre-ticked reads as a decision announced, not a vote opened. | - -## Pre-send checklist - -- [ ] All four links resolve, and the GitHub release is still flagged pre-release -- [ ] Ticket ids in the mail match the page's, excluding the page's `Rejected requests` -- [ ] Boilerplate byte-identical to the frozen text above -- [ ] `To: dev@` only — `user@` absent — and `Bcc: private@` present -- [ ] Subject is exactly `[VOTE] Apache Struts X.Y.Z` -- [ ] All four checkboxes empty -- [ ] Body hard-wrapped at 72 columns, continuation lines unindented -```` - -- [ ] **Step 3: Verify the boilerplate is byte-identical to the archived mail** - -Write the archived 6.10.0 boilerplate to a scratchpad file and diff the template's copy -against it. Both mails carry identical boilerplate, so either is a valid reference. - -```bash -SCRATCH=/private/tmp/claude-501/-Users-lukaszlenart-Projects-Apache-struts/322a9b9b-d830-4ab7-a7e8-2e30a7682260/scratchpad -cat > "$SCRATCH/boilerplate-reference.txt" <<'EOF' -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. -EOF - -sed -n '/^Once you have had a chance/,/^to help do the work\.$/p' \ - .claude/skills/creating-release-vote-mail/vote-mail-template.md \ - > "$SCRATCH/boilerplate-template.txt" - -diff "$SCRATCH/boilerplate-reference.txt" "$SCRATCH/boilerplate-template.txt" -``` - -Expected: no output. Any diff means the boilerplate was retyped rather than copied — fix the -template, do not adjust the reference. - -- [ ] **Step 4: Verify the constrained strings** - -```bash -grep -n 'user@struts.apache.org' .claude/skills/creating-release-vote-mail/vote-mail-template.md -grep -c 'content/repositories/staging/' .claude/skills/creating-release-vote-mail/vote-mail-template.md -grep -n 'On behalf of the Apache Struts project' .claude/skills/creating-release-vote-mail/vote-mail-template.md -``` - -Expected: the first prints nothing (exit 1); the second prints `2` (skeleton plus the -frozen-text table row); the third prints one line. - -- [ ] **Step 5: Commit** - -```bash -git add .claude/skills/creating-release-vote-mail/vote-mail-template.md -git diff --cached --name-only -git commit -m "docs: add the release vote mail template - -Freezes the [VOTE] Apache Struts X.Y.Z skeleton: slots, link block, and -the ASF vote boilerplate reproduced byte-identical to the archived 6.10.0 -and 7.2.1 mails. - -Co-Authored-By: Claude Opus 5 " -``` - ---- - -### Task 2: The skill file - -**Files:** -- Create: `.claude/skills/creating-release-vote-mail/SKILL.md` -- Reference (read only): `.claude/skills/creating-version-notes/SKILL.md` - -**Interfaces:** -- Consumes: from Task 1, the slot names ``, ``, `` and the block names `Breaking changes`, `Deprecations`, `Issue list`; the pre-send checklist, which `SKILL.md` points at rather than duplicating. -- Produces: the skill `name: creating-release-vote-mail`, which Task 3's handoff line names. - -- [ ] **Step 1: Confirm the description does not collide with the sibling** - -```bash -grep -n '^description:' .claude/skills/*/SKILL.md -``` - -Expected: `creating-version-notes` ends its description at "the test-build announcement mail". -The new description must begin where that ends and must not contain the words "Version Notes -page", "Migration Guide" or "GitHub release notes" as things it *produces* — those are its -inputs. Two skills claiming the same trigger is the failure this step prevents. - -- [ ] **Step 2: Write `SKILL.md`** - -Create `.claude/skills/creating-release-vote-mail/SKILL.md` with this content: - -````markdown ---- -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. Everything below the opening sentence is a transform of a published cwiki section, -so the mail cannot assert something the page does not. - -**This is the step after [`creating-version-notes`](../creating-version-notes/SKILL.md).** -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. - -## The Iron Rule - -``` -THE VERSION NOTES PAGE IS THE ONLY SOURCE FOR THE BODY. -NEVER RETYPE THE ISSUE LIST, AND NEVER CLONE THE PREVIOUS VOTE MAIL. -``` - -Cloning fails the same way it fails for Version Notes pages: the version number gets updated -and the surrounding text does not. The 7.2.1 and 6.10.0 mails already disagree on their -sign-off, which is what cloning drift looks like before anyone notices it. - -[`vote-mail-template.md`](vote-mail-template.md) is the source of truth for the artifact — -slots, link block, frozen boilerplate, and the pre-send checklist. - -## Check every precondition before drafting - -The mail is four links. A vote opened on a 404 burns the 72-hour window before anyone can -test, and the mail cannot be recalled from a public archive. - -| Link | Produced by | How to check | -|---|---|---| -| `Version+Notes+X.Y.Z` on cwiki | `creating-version-notes` | Fetch it — it is also the body source | -| `releases/tag/STRUTS_X_Y_Z` | `creating-version-notes` | `gh release view STRUTS_X_Y_Z --json isPrerelease,url` — must still be a prerelease | -| `dist/dev/struts/X.Y.Z/` | The release build | `curl -sI` the directory; artifacts and their `.asc`/`.sha512` present | -| Nexus staging | `mvn release` | The staging repository is open, not dropped or already released | - -A GitHub release that is no longer a prerelease means someone promoted it before the vote -closed. Stop and resolve that before drafting. - -## Recipients - -``` -To: dev@struts.apache.org -Bcc: private@struts.apache.org -``` - -**`user@` must not appear.** The `[TEST]` mail one step earlier goes to `dev@` *and* `user@`, -which is correct for it — it asks people to test. This mail asks people to *vote*, and a vote -invitation on the user list solicits votes from people whose votes are not binding. - -The subject is exactly `[VOTE] Apache Struts X.Y.Z`. No "test build", no RC suffix, no -release-quality word. - -## The opening sentence is the only prose you author - -Two sentences. The first is fixed: `The Apache Struts X.Y.Z test build is available.` The -second describes the *shape* of the issue list. - -| The page has | Second sentence | -|---|---| -| No Breaking changes | `With this release the following issues were addressed:` | -| Breaking changes | `This release contains . Also a lot of dependencies have been updated:` | - -Fill `` from what the list actually holds — `a few minor breaking changes plus some bug -fixes` for 7.2.1. **Never preview individual tickets here.** The list below it is the preview; -a ticket named in the opener is a ticket you have decided matters more than its neighbours, -which is an editorial judgement the vote mail has no business making. - -## Transform rules, per section - -| Page section | In the mail | -|---|---| -| Breaking changes | Verbatim, `- ` prefixed, ticket refs as bare `[WW-XXXX]` text | -| Deprecations | Verbatim, same shape | -| Rejected requests | **Omitted** | -| Bug / New Feature / Improvement / Task / Dependency | Page order, heading bare on its own line, entries `[WW-XXXX] - ` | - -**Copy Breaking changes verbatim.** Verbatim is what holds each item at the page's -one-sentence form. Re-authoring them from the tickets is how the 7.2.1 items grew to three -clauses each, which is longer than the rule the page itself follows. - -**Deprecations are in; Rejected requests are out.** A deprecation tells a voter what to check -in their own application, so it belongs in front of the people testing. A `Won't Do` ticket -has nothing to test — it is release documentation, and the Release notes link carries it. - -**Plain text carries no links.** A ticket reference is the bare string `[WW-XXXX]`, and the -only URLs in the mail are the four link lines. - -**Hard-wrap at 72 columns**, continuation lines unindented. This is what keeps the issue list -legible in the ASF archives and in the quoted replies voters send back. - -## A security summary the page truncated stays truncated - -Where the Version Notes page stopped a ticket summary at a clause boundary because its -bulletin is unpublished, the mail stops at the same boundary. - -`dev@struts.apache.org` is a **public, permanently archived** list. Re-expanding a truncated -summary there publishes what the page deliberately withheld, to a wider audience than the -page, and no correction removes it from the archive. - -Nothing else about the release's security posture reaches this mail either: no severity, no -attack description, no S2-XXX or CVE that has not been published. - -**REQUIRED BACKGROUND:** where the wording of a security-relevant entry is in question, -`creating-security-bulletins` governs what may be said and when. - -## A ticket/prose version mismatch is not an error - -A Dependency entry may name a lower version than the Breaking changes prose. 7.2.1 lists -`[WW-5536] - Bump ognl:ognl from 3.3.5 to 3.4.8` while its Breaking changes says OGNL went to -3.4.11. Both are correct: the entry reproduces the ticket summary verbatim, the prose states -what shipped, and patch bumps land after a ticket is titled. - -**Do not reconcile it in the mail.** Changing either one makes the mail disagree with the -page, which is the one thing a rendering may never do. - -## Verify, then draft - -Run the pre-send checklist in [`vote-mail-template.md`](vote-mail-template.md). The one worth -scripting is the ticket-set comparison — sort the `WW-` ids in your draft against the page's, -excluding the page's `Rejected requests` section. Empty output, or the mail is not a rendering -and you should find out which direction the difference runs before sending. - -## Draft it, do not send it - -Create a Gmail draft with `To`, `Bcc`, `Subject` and body set. **Never send.** - -Sending opens a binding project vote on a public list, starts the 72-hour clock, and commits -the PMC to the artifacts as staged. That is the release manager's keystroke, not yours. - -## Red Flags — STOP - -- Starting from a copy of the previous release's vote mail -- Retyping the issue list from JIRA instead of rendering the page -- Re-expanding a summary the page truncated -- Adding `user@struts.apache.org` to the recipients -- Pre-ticking a quality level in the checkbox block -- Drafting before the page, tag, dist path or staging repo exist -- A severity, CVE or S2-XXX reference anywhere in the mail -- Editing the boilerplate wording -- "Correcting" a Dependency entry so it matches the Breaking changes prose -- Naming individual tickets in the opening sentence -- Sending, rather than drafting - -## Common Mistakes - -| Mistake | Reality | -|---|---| -| "Last release's vote mail is the fastest start" | It is how the sign-off drifted between 6.10.0 and 7.2.1. Start from the template. | -| "The `[TEST]` mail went to `user@`, so this should too" | That mail asks people to test. This one asks people to vote, and user-list votes are not binding. `dev@` only. | -| "The ticket says 3.4.8 but we shipped 3.4.11" | Both are right. The entry is the ticket summary verbatim; the prose is what shipped. Leave it. | -| "The ticket is public, so I can describe the vulnerability" | A public ticket does not publish the advisory, and `dev@` is archived forever. Neutral framing until the bulletin ships. | -| "The page is up, so I can draft" | Check the tag, the dist path and the staging repo too. A vote on a 404 wastes 72 hours. | -| "I'll tick GA since that's what we want" | The release manager's `+1` is a separate reply. A pre-ticked call announces a decision instead of opening a vote. | -| "Rejected requests are part of the release, so list them" | Voters test artifacts. A `Won't Do` ticket has nothing to test; the page link carries it. | -| "It only needs the ticket numbers, I'll type them out" | Typing is the failure mode. Render the page. | -```` - -- [ ] **Step 3: Verify frontmatter and cross-references** - -```bash -head -4 .claude/skills/creating-release-vote-mail/SKILL.md -grep -c 'vote-mail-template.md' .claude/skills/creating-release-vote-mail/SKILL.md -grep -n 'user@struts.apache.org' .claude/skills/creating-release-vote-mail/SKILL.md -``` - -Expected: frontmatter is `---`, `name:`, `description:`, `---` and nothing else; the template -is referenced at least 3 times; `user@` appears only in the prohibition and the two mistake -rows that explain it — read each hit and confirm none of them is a recipient instruction. - -- [ ] **Step 4: Commit** - -```bash -git add .claude/skills/creating-release-vote-mail/SKILL.md -git diff --cached --name-only -git commit -m "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. Records the dev@-only recipient rule, the -truncated-security-summary carry-through, and the draft-never-send -boundary. - -Co-Authored-By: Claude Opus 5 " -``` - ---- - -### Task 3: Chain it to `creating-version-notes` - -**Files:** -- Modify: `.claude/skills/creating-version-notes/SKILL.md` — end of the `## The test-build announcement` section, immediately before `## Re-read the page immediately before you write to it` - -**Interfaces:** -- Consumes: the skill name `creating-release-vote-mail` from Task 2. - -- [ ] **Step 1: Locate the insertion point** - -```bash -grep -n '^## ' .claude/skills/creating-version-notes/SKILL.md -``` - -The handoff goes at the **end** of `## The test-build announcement`, after the paragraph -beginning `Keep the security posture of the pages`. - -- [ ] **Step 2: Add the handoff line** - -Append this paragraph to that section: - -```markdown -**The vote is the next step, and it is a different mail.** Once the `[TEST]` build has been -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 differ from the announcement above. -``` - -- [ ] **Step 3: Verify nothing else moved** - -```bash -git diff --stat .claude/skills/creating-version-notes/SKILL.md -git diff .claude/skills/creating-version-notes/SKILL.md -``` - -Expected: `1 file changed, N insertions(+)` with **no deletions**. Any deletion means a -neighbouring section was disturbed — this skill's own rules warn about swallowing an adjacent -heading, and the same care applies to editing it. - -- [ ] **Step 4: Commit** - -```bash -git add .claude/skills/creating-version-notes/SKILL.md -git diff --cached --name-only -git commit -m "docs: point creating-version-notes at the vote mail step - -Co-Authored-By: Claude Opus 5 " -``` - ---- - -### Task 4: Dry run against the archived 6.10.0 vote - -This is the closest thing the skill has to a test: render a mail the project already sent, and -compare. 6.10.0 is the better subject than 7.2.1 — its issue list is three tickets, so a diff -is readable, and it has no Breaking changes block, which exercises the omission path. - -**Files:** -- No repository files change. Working files go in the scratchpad. - -**Interfaces:** -- Consumes: `SKILL.md` and `vote-mail-template.md` from Tasks 1 and 2. - -- [ ] **Step 1: Capture the archived mail as the expected output** - -Fetch the sent `[VOTE] Apache Struts 6.10.0` mail (Gmail thread `19e5fc8e0a444f92`, message -`19e5fcd6bf35b65b`) and save its plaintext body to `$SCRATCH/expected-6.10.0.txt`. - -- [ ] **Step 2: Render a mail from the published page, following the skill** - -Follow `SKILL.md` end to end for version `6.10.0`, sourcing the body from -`https://cwiki.apache.org/confluence/display/WW/Version+Notes+6.10.0`. Write the result to -`$SCRATCH/rendered-6.10.0.txt` instead of creating a draft. - -Skip the precondition checks that cannot pass for a shipped release — the 6.10.0 GitHub -release is no longer a prerelease and its staging repo is long closed. Note which checks you -skipped; that list is itself a finding if it is longer than those two. - -- [ ] **Step 3: Diff and classify every difference** - -```bash -diff -u "$SCRATCH/expected-6.10.0.txt" "$SCRATCH/rendered-6.10.0.txt" -``` - -Expected: differences only in the sign-off (`Kind regards` → `On behalf of the Apache Struts -project`, a deliberate normalisation from the spec) and in wrapping of the authored opener. - -Classify each remaining difference into exactly one bucket: - -| Bucket | Action | -|---|---| -| Deliberate normalisation named in the spec | None — expected | -| The skill produced something wrong | Fix `SKILL.md` or the template, re-run from Step 2 | -| The archived mail was wrong | Add a Common Mistakes row so the skill prevents it next time | - -- [ ] **Step 4: Verify the ticket-set check actually works** - -```bash -grep -o 'WW-[0-9]*' "$SCRATCH/rendered-6.10.0.txt" | sort -u -``` - -Expected: exactly `WW-5623`, `WW-5628`, `WW-5629` — matching the page's three tickets. If the -rendering dropped or invented one, the transform rules in Task 2 need fixing. - -- [ ] **Step 5: Commit any corrections** - -If Steps 3 or 4 required changes: - -```bash -git add .claude/skills/creating-release-vote-mail/ -git diff --cached --name-only -git commit -m "docs: correct the vote mail skill from the 6.10.0 dry run - -Co-Authored-By: Claude Opus 5 " -``` - -If nothing changed, record the dry-run outcome in the PR description instead and move on. - ---- - -### Task 5: Open the PR - -**Files:** -- No file changes. - -- [ ] **Step 1: Review the whole branch before pushing** - -```bash -git log --oneline main..vote-mail-skill -git diff main...vote-mail-skill -``` - -Read the full diff. Every change on this branch gets the same review gate, including the -corrections Task 4 may have added. - -- [ ] **Step 2: Confirm this is not a security patch** - -This branch adds process documentation and touches no framework code. Confirm with: - -```bash -git diff --name-only main...vote-mail-skill -``` - -Expected: only paths under `.claude/skills/` and `docs/superpowers/`. Anything under `core/` -or `plugins/` means something unintended was staged — stop and investigate before pushing. - -- [ ] **Step 3: Push and open the PR** - -```bash -git push -u origin vote-mail-skill -gh pr create --title "docs: add creating-release-vote-mail skill" --body "$(cat <<'EOF' -Adds a `creating-release-vote-mail` skill that drafts the `[VOTE] Apache Struts X.Y.Z` mail -as a plain-text rendering of the already-published Version Notes page. - -It is the step after `creating-version-notes`: that skill ends at the `[TEST]` announcement, -this one opens the vote. - -- `vote-mail-template.md` freezes the skeleton and the ASF vote boilerplate, reproduced - byte-identical to the archived 6.10.0 and 7.2.1 mails. -- `SKILL.md` records the judgement: `dev@`-only recipients, Deprecations in and Rejected - requests out, truncated security summaries carrying through unchanged, and drafting rather - than sending. -- Validated by re-rendering the archived 6.10.0 vote mail from its published page. - -Design: `docs/superpowers/specs/2026-08-08-release-vote-mail-skill-design.md` - -No Jira ticket — process documentation, consistent with the `creating-version-notes` and -`creating-security-bulletins` skill commits. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - ---- - -## Self-Review - -**Spec coverage.** Every spec section maps to a task: identity and frontmatter → Task 2 Step 2; -preconditions table → Task 2; recipients → Tasks 1 and 2, verified in Task 1 Step 4 and Task 2 -Step 3; opening sentence → Task 2; transform rules including Deprecations-in/Rejected-out → -Task 2; security truncation → Task 2; template with frozen sign-off and staging URL → Task 1; -empty checkboxes → Task 1; verification checklist → Task 1 Step 2, exercised in Task 4; -Gmail-draft-never-send → Task 2; Red Flags and Common Mistakes → Task 2; "no tests" → Global -Constraints, with Task 4 standing in as a dry run. The spec's position-in-flow diagram is what -Task 3 implements. - -**Placeholders.** None. Every file's full content is given inline; every command is runnable; -every "Expected:" states a concrete result. - -**Name consistency.** `creating-release-vote-mail` (skill name, directory, frontmatter, Task 3 -handoff, PR title), `vote-mail-template.md` (Task 1 creates it, Task 2 links it three times), -and the slot names `` / `` / `` are used identically in Tasks 1 -and 2. Branch `vote-mail-skill` is the same in Global Constraints and Task 5. +| `.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. From f7844d2e773d36e1178f305535680f969dc2887c Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 8 Aug 2026 08:03:35 +0200 Subject: [PATCH 6/6] 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 --- .claude/skills/creating-release-vote-mail/SKILL.md | 11 +++++++++-- .../2026-08-08-release-vote-mail-skill-design.md | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.claude/skills/creating-release-vote-mail/SKILL.md b/.claude/skills/creating-release-vote-mail/SKILL.md index 6429f87163..d8cb99fe19 100644 --- a/.claude/skills/creating-release-vote-mail/SKILL.md +++ b/.claude/skills/creating-release-vote-mail/SKILL.md @@ -66,8 +66,14 @@ 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@` 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. +**`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`. @@ -124,6 +130,7 @@ A vote opened on a 404 burns the window before anyone can test. |---|---| | "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. | 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 index 8d8df704c2..a9486d9dd7 100644 --- 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 @@ -104,8 +104,15 @@ Bcc: private@struts.apache.org 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". -Bcc rather than Cc for `private@` has a reason worth recording: 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. +**`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)