This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new 46a1f62  feat(security-issue-sync): gate RM hand-off on Vulnogram 
state REVIEW; add fill-fields comment for remediation developer (#255)
46a1f62 is described below

commit 46a1f623158400b15323e4260bbe1ad1b6f3c89e
Author: Jarek Potiuk <[email protected]>
AuthorDate: Mon May 25 07:00:12 2026 +0200

    feat(security-issue-sync): gate RM hand-off on Vulnogram state REVIEW; add 
fill-fields comment for remediation developer (#255)
    
    * feat(security-issue-sync): gate RM hand-off on Vulnogram state REVIEW
    
    Changes the `pr merged → fix released` hand-off so the release manager
    never receives a hand-off comment while the CVE record is still in
    `DRAFT` state. State-advance responsibility moves from the RM to sync.
    
    - New template 
`tools/vulnogram/remediation-developer-fill-fields-comment.md`
      — fires at Step 11 (pr merged) and Step 12 (fix released) when CVE
      body fields are incomplete OR the CVE record state is still DRAFT
      after sync's JSON push attempt. Tags the remediation developer;
      issue stays assigned to them until the gate clears.
    - Both release-manager-handoff-comment{,-oauth-pushed}.md rewritten:
      drop the DRAFT branch from Step 1, assert "you will never see this
      comment in DRAFT state", clearer step-by-step UI actions.
    - security-issue-sync/SKILL.md updated with the two-stage gate
      (body fields populated + state == REVIEW), Step 1d table rows
      describing both firing points of the fill-fields comment, and a
      new Step 5b.6 (post-push state verification).
    
    Follow-ups (not in this PR): vulnogram-api-record-fetch CLI;
    generate-cve-json --state flag; publication-ready template UX
    rewrite; tools/vulnogram/record.md state-machine section update.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    * feat(security-issue-sync): implement Task 1-4 follow-ups for state-gate
    
    Addresses the four follow-up TODOs called out in #255's PR description:
    
    Task 1 — vulnogram-api-record-fetch CLI
    - New file: tools/vulnogram/oauth-api/src/vulnogram_api/record_fetch.py
    - Read-only counterpart to vulnogram-api-record-{update,publish}
    - Supports --state-only for cheap shell-parsing of CNA_private.state
    - Defaults to one-line JSON output on stdout for jq piping
    - Wired into pyproject.toml as the `vulnogram-api-record-fetch` script
    
    Task 2 — generator already auto-promotes state on field-readiness
    - Discovery: generate-cve-json's compute_cna_private_state already
      emits CNA_private.state = "REVIEW" automatically when all required
      body fields are populated (CVE ID, title, description, affected
      versions, CWE, non-Unknown severity, at least one credit, at least
      one reference). No new --state flag is needed.
    - Updated SKILL.md to describe this correctly: sync just pushes the
      generator-emitted JSON; Vulnogram accepts the state field verbatim.
    
    Task 3 — publication-ready comment templates rewritten
    - Both manual-paste and oauth-pushed variants now reflect the new
      workflow where sync drives READY → PUBLIC + tracker close.
    - Templates are informational only; no RM action required at the
      publication-ready moment. The wrap-up comment (posted post-close)
      is the single RM-action surface for the board archive + milestone
      close.
    
    Task 4 — tools/vulnogram/record.md state-machine update
    - State table: DRAFT → REVIEW now set by sync (via generator);
      READY → PUBLIC now sync-driven via vulnogram-api-record-publish.
    - Release-manager checklist rewritten: RM-side write count is now
      zero in the common case (sync handles steps 1, 2, 6, 7, 8;
      RM only clicks REVIEW → READY in step 3 + previews + sends in
      steps 4-5).
    - Two-record-write-paths section updated to describe the new layered
      state-transition model.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    * fix(vulnogram): drop broken README anchor from publication-ready templates
    
    lychee CI on #255 caught two broken fragment links pointing at
    `README.md#for-release-managers--steps-1215` — that anchor does not
    exist in the framework README (no "For release managers" section
    there). Replace with a relative link to `tools/vulnogram/record.md`
    which already carries the full RM-side checklist.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    * fix(security-issue-sync): drop manual board-archive / milestone-close 
asks from RM-facing comments
    
    Per RM feedback on a live tracker — *"Same here for step 3 — not idiot
    safe (I fail to understand)"* — the original Step 3 of the hand-off
    comment (and the matching wrap-up comment) asked the release manager
    to manually archive the tracker from the board and close the
    milestone. Both actions are already sync-driven (the
    `archiveProjectV2Item` GraphQL mutation fires on every close per
    Step 4's apply mechanic; the milestone-close PATCH fires when the
    just-closed tracker was the last open sibling), so listing them as
    RM tasks created the same confusion class the new state-gated
    hand-off was designed to eliminate.
    
    Changes:
    
    - `release-manager-handoff-comment{,-oauth-pushed}.md`: Step 3
      rewritten. Now lists the auto-archive + auto-close-milestone as
      sync sub-steps alongside URL capture / JSON regen / state advance
      / label flip / close. Ends with *"You're done. The lifecycle is
      complete from your side at Step 2 (Send Email)."* — no
      implication that another comment will tag the RM with manual
      cleanups.
    - `release-manager-wrap-up-comment.md`: rewritten as **purely
      informational** ("✅ Lifecycle complete"). Lists what sync did
      (state advance + label flip + close + board archive + conditional
      milestone close) but solicits no further action. `MILESTONE_BULLET`
      conditional reads as a status note ("Milestone X closed
      automatically..."), not an ask ("Close milestone X").
    - `security-issue-sync/SKILL.md` Step 1d / Step 2b: updated to
      describe the auto-archive + auto-close-milestone as sync sub-steps
      inside the *Advisory archived on `<users-list>`* combined apply,
      and the wrap-up comment as informational-only.
    
    The state-gated invariant from the prior commit on this branch
    (handoff fires only when CVE state == REVIEW) plus this cleanup
    together give the RM a clean three-action flow with zero
    post-Send-Email touchpoints.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 .claude/skills/security-issue-sync/SKILL.md        | 244 +++++++++++++++++++--
 tools/vulnogram/oauth-api/pyproject.toml           |   1 +
 .../oauth-api/src/vulnogram_api/record_fetch.py    | 133 +++++++++++
 tools/vulnogram/record.md                          | 198 +++++++++--------
 ...release-manager-handoff-comment-oauth-pushed.md | 203 ++++++++---------
 tools/vulnogram/release-manager-handoff-comment.md | 239 ++++++++++----------
 ...ase-manager-publication-comment-oauth-pushed.md |  55 +++--
 .../release-manager-publication-comment.md         |  63 ++++--
 tools/vulnogram/release-manager-wrap-up-comment.md | 108 ++++-----
 .../remediation-developer-fill-fields-comment.md   | 128 +++++++++++
 10 files changed, 903 insertions(+), 469 deletions(-)

diff --git a/.claude/skills/security-issue-sync/SKILL.md 
b/.claude/skills/security-issue-sync/SKILL.md
index 0aba65b..ea4490f 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -696,17 +696,17 @@ update, label change, or next-step recommendation in Step 
2:
 | Reporter reply with a confirmed credit line (*"please credit me as …"*, 
*"use handle X"*, *"anonymous is fine"*) | Replace the `Reporter credited as` 
placeholder with the confirmed form; mark the credit question as resolved so 
the next status-update draft does not re-ask it. |
 | Reporter explicit opt-out of credit (*"do not credit me"*, *"anonymous"*) | 
Set the field to `anonymous` and flag the advisory to use that form. |
 | Release manager's `[RESULT][VOTE] Release Airflow <version>` on `<dev-list>` 
for a version that carries the fix | Record the release manager in the "Known 
release managers" subsection of [`AGENTS.md`](../../../AGENTS.md) if not 
already there; flag Step 13 (advisory) as assigned to that person. |
-| Advisory archived on `<users-list>` (the announcement message is now visible 
in `lists.apache.org/list.html?<users-list>` — scan the archive with the CVE ID 
when `fix released` is set and the *"Public advisory URL"* body field is empty) 
| This is the **post-advisory lifecycle close-out trigger**. Propose, in a 
single combined apply: (1) populate the *"Public advisory URL"* body field with 
the archive URL; (2) **extract the public-facing short summary from the 
advisory email body** (the [...]
+| Advisory archived on `<users-list>` (the announcement message is now visible 
in `lists.apache.org/list.html?<users-list>` — scan the archive with the CVE ID 
when `fix released` is set and the *"Public advisory URL"* body field is empty) 
| This is the **post-advisory lifecycle close-out trigger**. Propose, in a 
single combined apply: (1) populate the *"Public advisory URL"* body field with 
the archive URL; (2) **extract the public-facing short summary from the 
advisory email body** (the [...]
 | Advisory message sent to `[email protected]` / `<users-list>` but archive 
URL not yet visible | No-op transition; **do not** flip the `fix released → 
announced` labels here. The label flip is part of the combined "archive URL 
captured" apply above and only fires when the archive URL is confirmed live on 
`lists.apache.org` (this is the load-bearing real-world signal that the 
advisory actually shipped — a `[VOTE]/[ANNOUNCE]` mail thread in flight without 
an archived URL is ambiguous). |
 | Project-board column drifted from the issue's label-derived state (e.g. a 
tracker carries `pr merged` but is still in the `PR created` column on [Project 
2](<project-board-url>), or `announced` + *Public advisory URL* body field 
populated but the column is still `Fix released`) | Propose moving the project 
item to the correct column per the mapping table in Step 2b. The board is the 
primary security-team overview surface; a stale column hides ownership handoffs 
from the team at a glance. |
 | `announced` label set and CVE record on `cveprocess.apache.org` now reports 
state PUBLISHED (checked via `curl -s 
https://cveprocess.apache.org/cve5/<CVE-ID>.json` / the ASF CVE tool API, or an 
explicit release-manager comment on the issue stating the Vulnogram push is 
done) | Propose closing the issue. Do not update any labels. This is the 
terminal transition. |
 | CVE record has open **review comments / reviewer proposals** (detected via 
the Gmail-search path in Step 1e — reviewer-comment notifications from 
Vulnogram land on `<security-list>` with the CVE ID in the subject line; the 
`cveprocess.apache.org/cve5/<CVE-ID>.json` endpoint is behind ASF OAuth and is 
not readable from this skill's context, so Gmail is the load-bearing signal 
source). | Surface each open review comment in Step 2a with **clickable links** 
to the Gmail thread and to the C [...]
 | The referenced `<upstream>` PR has been opened but is still in `open` state 
| Propose `pr created` label; update the *"PR with the fix"* body field with 
the PR URL. |
-| The referenced `<upstream>` PR moved to `merged` | Propose swapping `pr 
created` → `pr merged`; update milestone to the shipping release if now known. |
+| The referenced `<upstream>` PR moved to `merged` | Propose swapping `pr 
created` → `pr merged`; update milestone to the shipping release if now known. 
**Also**: check whether all six mandatory CVE body fields are populated (*CWE*, 
*Affected versions*, *Severity*, *Reporter credited as*, *Short public summary 
for publish*, *PR with the fix*). If any is empty / `_No response_`, propose 
posting (or PATCH-updating) the *Remediation-developer fill-fields comment* per 
[the dedicated bullet i [...]
 | The *"PR with the fix"* body field has at least one PR URL **and** the 
*"Remediation developer"* body field is missing the PR author's name (or is 
`_No response_`) | Propose appending the PR author's display name (`gh pr view 
<N> --repo <upstream> --json author --jq '.author.name // .author.login'`) to 
the *"Remediation developer"* body field. **Append, never overwrite** — manual 
edits (co-authors added by the triager, name spelling corrections, "Anonymous" 
overrides) must survive subs [...]
 | The *"Affected versions"* body field is missing, holds a pre-convention 
shape, or carries the project's pre-release sentinel, and the tracker is 
**not** at `fix released` yet | Propose populating / refining *"Affected 
versions"* per the project's convention. The per-scope shape, the pre-release 
sentinel (if any), and the lifecycle live in 
[`<project-config>/scope-labels.md` — *Affected versions convention by 
scope*](../../../<project-config>/scope-labels.md#affected-versions-convention 
[...]
 | A tracker is transitioning to `fix released` (per the row below) and 
*"Affected versions"* still carries the project's pre-release sentinel | 
Propose replacing the sentinel with the concrete released version per the 
project's convention; see [`<project-config>/scope-labels.md` — *Affected 
versions convention by 
scope*](../../../<project-config>/scope-labels.md#affected-versions-convention-by-scope)
 for the recipe. After the body update, regenerate the CVE JSON attachment so 
`versions[] [...]
-| A release carrying the fix has shipped. Detection is **scope-dependent** — 
different scope labels on a project can ride different release trains, each 
with its own *"is it released?"* signal (which artifact registry to consult, 
what to query, how to map a tracker's milestone to that registry, 
partial-release edge cases). The per-scope detection recipe lives in 
[`<project-config>/scope-labels.md` — *Detecting that a fix release has 
shipped*](../../../<project-config>/scope-labels.md#det [...]
+| A release carrying the fix has shipped. Detection is **scope-dependent** — 
different scope labels on a project can ride different release trains, each 
with its own *"is it released?"* signal (which artifact registry to consult, 
what to query, how to map a tracker's milestone to that registry, 
partial-release edge cases). The per-scope detection recipe lives in 
[`<project-config>/scope-labels.md` — *Detecting that a fix release has 
shipped*](../../../<project-config>/scope-labels.md#det [...]
 | GHSA state transition (opened, accepted, published, rejected) in a 
GHSA-forwarded email | If the GHSA is closed as "not accepted" but the security 
team accepted the report on `security@`, flag the divergence in the status 
comment so it is not lost. |
 | Team member saying *"let's also backport to v3-2-test"* / *"please mark X 
for backport"* | Note the requested backport label on the public PR as an item 
for Step 9 of the `security-issue-fix` workflow. |
 | Reporter flagging a second distinct vulnerability on the same thread | 
Surface as an explicit question to the user — it may warrant a separate 
tracking issue. |
@@ -918,7 +918,7 @@ process the issue is currently at:
 | Release with the fix has shipped, advisory not sent yet (swap `pr merged` → 
`fix released`) | 12 |
 | `fix released` set, advisory not yet sent — release manager owns the 
advisory | 13 |
 | Advisory sent, no archive URL yet (no labels flipped; the `fix released → 
announced` label flip is deferred to the combined "archive URL captured" apply) 
| 13 → 14 |
-| **Archive URL captured** — sync's combined apply fires at this moment: 
writes the URL into the body, extracts the public short summary from the 
advisory and writes it into the body, flips `fix released → announced - emails 
sent + announced`, regenerates + re-pushes the JSON, moves the Vulnogram record 
`REVIEW → PUBLIC` via API, moves the board to `Announced`, closes the tracker, 
and posts the conditional wrap-up comment with the milestone URL when 
last-sibling on the milestone. See the [...]
+| **Archive URL captured** — sync's combined apply fires at this moment: 
writes the URL into the body, extracts the public short summary from the 
advisory and writes it into the body, flips `fix released → announced - emails 
sent + announced`, regenerates + re-pushes the JSON, moves the Vulnogram record 
`REVIEW → PUBLIC` via API, moves the board to `Announced`, closes the tracker, 
**archives the tracker from the board**, **closes the milestone if 
last-sibling**, and posts the purely-info [...]
 | **Closed**, `announced` set, cve.org check **not yet run** for this tracker 
since close | post-15 (cve.org publication check — see 
[1g](#1g-recently-closed-trackers--check-cveorg-publication-state)) |
 | Closed, credits missing | 16 |
 
@@ -1549,6 +1549,101 @@ will change and *why*. Group them by category:
   single preformatted block and hiding every link. Do not
   indent entries for "readability".
 
+- **Remediation-developer fill-fields comment** — when this sync
+  pass detects that mandatory CVE body fields are not yet
+  populated, propose posting (or PATCH-updating) a comment tagging
+  the **remediation developer** with the concrete list of missing
+  fields. The tracker stays assigned to the remediation developer;
+  the release-manager hand-off is **not** fired until the gate
+  clears.
+
+  **This is its own first-class comment, not a rollup entry**, for
+  the same reason as the RM hand-off — it carries a concrete
+  call-to-action that needs to be visible at-a-glance, not hidden
+  inside a `<details>` block.
+
+  **Trigger — two firing points**:
+
+  1. **At the `pr created` → `pr merged` transition (Step 11)** —
+     when sync proposes the `pr created` → `pr merged` label swap,
+     check whether all six mandatory body fields are populated
+     (*CWE*, *Affected versions*, *Severity*, *Reporter credited
+     as*, *Short public summary for publish*, *PR with the fix*).
+     If any field is empty / `_No response_`, propose the
+     fill-fields comment with that field list. Issue stays
+     assigned to the remediation developer (who is also the fix-PR
+     author and the current assignee in the common case). **Do
+     not propose any RM-related action at Step 11**; that belongs
+     to Step 12.
+  2. **At the `pr merged` → `fix released` transition (Step 12)** —
+     after sync's Step 5b push attempt, check the CVE record state
+     in Vulnogram. If the state is still `DRAFT` for any reason
+     (one of the body fields was still empty, the JSON push was
+     blocked, the API push happened but the state did not advance
+     because the JSON failed CNA-schema validation, etc.),
+     **re-fire** the fill-fields comment with the refreshed list
+     of what is still blocking. **Do not** fire the RM hand-off,
+     do not flip the label to `fix released`, do not swap the
+     assignee — those all gate on `state == REVIEW`. A subsequent
+     sync run that finds the state finally promoted to `REVIEW`
+     will clear the gate and fire the RM hand-off then.
+
+  **Idempotency + PATCH-in-place**. Same shape as the hand-off
+  comment: scan for the marker
+  ```html
+  <!-- apache-steward: remediation-developer-fill-fields v1 -->
+  ```
+  on line 1 of each comment. Three outcomes:
+
+  - **No marker found** — POST a fresh comment.
+  - **Marker found, current body matches the body the skill would
+    render this run** — no-op; surface as
+    *"fill-fields comment already posted on `<comment-url>` and
+    the missing-fields list is unchanged (skipping)"*.
+  - **Marker found, current body does NOT match** (typically: the
+    missing-fields list changed because the remediation developer
+    filled some — but not all — fields between sync runs) —
+    PATCH-edit the existing comment with the refreshed list.
+
+  **Body source.** 
`tools/<cve-tool>/remediation-developer-fill-fields-comment.md`
+  (for Vulnogram:
+  
[`tools/vulnogram/remediation-developer-fill-fields-comment.md`](../../../tools/vulnogram/remediation-developer-fill-fields-comment.md)).
+  This template carries no OAuth-pushed / manual-paste variants —
+  the remediation developer's job is to fill in body fields, and
+  the API-push state is invisible to them.
+
+  **Resolving placeholders.** Inherits the same resolution rules
+  as the hand-off comment for the placeholders it shares
+  (`CVE_ID`, `SOURCE_TAB_URL`, `TRACKER_URL`, `SECURITY_LIST`,
+  `SECURITY_LIST_DOMAIN`, `FRAMEWORK_README_URL`,
+  `FRAMEWORK_SYNC_SKILL_URL`). Plus two unique placeholders:
+
+  - `REMEDIATION_DEVELOPER_HANDLE` — read from the tracker's
+    *Remediation developer* body field. When the field carries a
+    `Full Name (@handle)` line, extract the `@handle` token. When
+    only the name is set, fall back to the fix-PR author's
+    `@`-handle (looked up via `gh pr view --json author --jq
+    .author.login`) and propose adding the `@handle` to the body
+    field on the same sync pass (so the next sync resolves
+    cleanly).
+  - `MISSING_FIELDS_LIST` — Markdown bullet list, one line per
+    empty mandatory field, of the shape
+    Markdown bullets shaped `- **<Field name>** — currently the
+    empty placeholder; <one-line hint on how to fill it>`. The hint
+    comes from the project's
+    *Issue-template fields* docs; for Vulnogram-based projects
+    the hint is the field's `description` from
+    `<project-config>/.github/ISSUE_TEMPLATE/issue_report.yml`.
+
+  **Apply mechanic.** See the *Remediation-developer fill-fields
+  comment* bullet in Step 4 below; POST vs PATCH decided by the
+  marker check above.
+
+  **Recap.** Surface the comment URL (new or PATCH-edited) in the
+  recap (Step 6) so the user can click through and verify the
+  list, plus a one-line note *"hand-off to RM blocked on N
+  field(s); fill-fields comment posted/refreshed"*.
+
 - **Release-manager hand-off comment** — when this sync pass
   proposes the `pr merged` → `fix released` label swap (Step 12),
   **also** propose posting a separate hand-off comment that walks
@@ -1563,13 +1658,22 @@ will change and *why*. Group them by category:
   comment. Folding it into the rollup would bury the call-to-action
   inside a `<details>` block.
 
-  **Trigger.** Fires *exactly once* per tracker, at the same sync
-  pass that proposes `pr merged` → `fix released`. Do not propose it
-  earlier — the tracker is not yet the release manager's
-  responsibility before that swap, and a hand-off comment posted at
-  `cve allocated` or `pr merged` would lose context by the time the
-  release actually ships. Do not propose it on subsequent runs once
-  it has already been posted (idempotency check below).
+  **Trigger — gated on `state == REVIEW`.** Fires *exactly once*
+  per tracker, at the sync pass that proposes
+  `pr merged` → `fix released` **AND** finds the CVE record state
+  in Vulnogram is `REVIEW` (verified by sync after Step 5b's push
+  attempt — the push includes the `body.CNA_private.state =
+  "REVIEW"` advance when all six mandatory body fields are
+  populated). When the CVE record is still in `DRAFT` after the
+  push attempt, **do not** fire this hand-off; fire the
+  *Remediation-developer fill-fields comment* instead and leave
+  the tracker assigned to the remediation developer. **The RM
+  must never receive this hand-off while the record is in
+  `DRAFT`** — that invariant is asserted in the template body
+  itself so the RM can recognise a misfire if it ever happens.
+  Do not propose it earlier than Step 12; do not propose it on
+  subsequent runs once it has already been posted (idempotency
+  check below).
 
   **Idempotency + variant edit-in-place.** Before proposing, scan
   the issue's existing comments for the marker
@@ -1961,25 +2065,33 @@ before moving on to the next item. Use:
   comment's *"the JSON has been regenerated to include the archive
   URL and pushed to the record"* claim is true at the moment the
   RM reads it.
-- **Wrap-up comment (post-close):** load
+- **Wrap-up comment (post-close, informational only):** load
   
[`tools/<cve-tool>/release-manager-wrap-up-comment.md`](../../../tools/vulnogram/release-manager-wrap-up-comment.md)
   and post it as the **last** action of the *Advisory archived on
-  `<users-list>`* combined apply, right after the tracker close
-  succeeds. The comment is the residual-manual-steps ping to the RM
-  (archive from the `Announced` column, and — conditionally —
-  close the milestone).
+  `<users-list>`* combined apply, right after sync has already
+  (a) archived the tracker from the project board via
+  `archiveProjectV2Item` and (b) closed the milestone if the
+  just-closed tracker was the last open sibling. **The comment is
+  purely informational** — a timeline-event marker confirming
+  what sync did, **not** a ping for residual manual actions. The
+  RM has zero remaining actions post-Send-Email; asking them to
+  do what sync already did creates the same confusion class the
+  state-gated hand-off was designed to eliminate (worked example:
+  RM feedback on the original wrap-up template — *"Same here for
+  step 3 - not idiot safe (I fail to understand)"*).
 
   Placeholders to substitute: `CVE_ID`, `RM_HANDLE` (from the
   release-manager identity resolved in Step 1f / `release-trains.md`),
-  `TRACKER_URL`, `BOARD_URL` (project-board URL with
-  `?filterQuery=status%3AAnnounced` appended),
   `PUBLISH_TIMESTAMP` (from the just-completed
   `vulnogram-api-record-publish` call), `ADVISORY_URL` (the
   archive URL captured in the same apply), and the conditional
   `MILESTONE_BULLET` — see below.
 
   **`MILESTONE_BULLET` is the only conditional in the template.**
-  Resolve via a sibling-state check right before substitution:
+  When sync's milestone-close action fired in the same apply
+  (i.e. the just-closed tracker was the last open sibling on its
+  milestone), substitute with a one-line *informational* note —
+  not an ask:
 
   ```bash
   ms=$(gh issue view <N> --repo <tracker> --json milestone \
@@ -1993,7 +2105,7 @@ before moving on to the next item. Use:
     if [ "$open" -eq 0 ]; then
       ms_url=$(gh api repos/<tracker>/milestones/$ms --jq '.html_url')
       ms_title=$(gh api repos/<tracker>/milestones/$ms --jq '.title')
-      bullet="Close the [\`$ms_title\`]($ms_url) milestone — every tracker on 
it is now closed too."
+      bullet="Milestone [\`$ms_title\`]($ms_url) closed automatically (every 
tracker on it is now done)."
     else
       bullet=""
     fi
@@ -2282,10 +2394,51 @@ per machine — see
 
[`tools/vulnogram/oauth-api/README.md`](../../../tools/vulnogram/oauth-api/README.md)),
 **sync pushes the JSON to the record directly** instead of leaving the
 paste step to the release manager. The push is mechanical and follows
-from the same JSON the user just approved as part of the body update;
-it does **not** advance the Vulnogram state machine
-(`DRAFT` → `REVIEW` → `READY` → `PUBLIC`) — those transitions stay
-with the RM because they include the CNA-feed dispatch trigger.
+from the same JSON the user just approved as part of the body update.
+
+**State auto-promote (DRAFT → REVIEW) — driven by the generator,
+not by sync.** The CVE JSON the generator produces already carries
+the correct `CNA_private.state` value based on the readiness of
+the tracker's body fields. The generator's logic (see
+`compute_cna_private_state` in
+[`tools/vulnogram/generate-cve-json`](../../../tools/vulnogram/generate-cve-json/src/generate_cve_json/cve_json.py)):
+
+- `DRAFT` — when any required field is missing (no title, no
+  description, no affected versions, no CWE, no non-Unknown
+  severity, no credit, no reference).
+- `REVIEW` — when every field a release manager needs to send
+  the advisory is present, **but** no public advisory URL has
+  been captured yet.
+- `PUBLIC` — when the CNA is review-ready AND at least one
+  `references[]` entry is tagged `vendor-advisory` (i.e. the
+  *Public advisory URL* body field is populated with the
+  archived users-list URL).
+
+Sync's role is therefore **just** to push the generated JSON and
+verify the saved state matches what the generator computed.
+Vulnogram accepts the state field verbatim from the pushed
+document; no separate state-flip call is needed for the
+`DRAFT` → `REVIEW` transition. This is the load-bearing gate for
+the release-manager hand-off (see Step 2b's *Two-stage gate*):
+the RM never receives the hand-off comment while the record is
+still in `DRAFT`.
+
+The remaining transitions stay separate:
+
+- `REVIEW` → `READY` is a **release-manager UI click** in
+  Vulnogram, done as Step 1 of the RM hand-off after any reviewer
+  comments on the record are resolved. (The generator does not
+  emit `READY` — it is intentionally a human decision that
+  reviewer feedback is closed.)
+- `READY` → `PUBLIC` is **sync-driven** via the
+  `vulnogram-api-record-publish` CLI (see Step 4 below), fired
+  when the advisory archive URL has been captured on
+  `lists.apache.org/list.html?<users-list>` — the CNA-feed
+  dispatch trigger has a real-world signal (the archived
+  advisory) so sync drives it.
+
+Step 6 below describes how to verify the state advance landed
+(and what to do if it did not).
 
 ### Decision flow
 
@@ -2351,6 +2504,49 @@ with the RM because they include the CNA-feed dispatch 
trigger.
    sync run that re-regenerated the JSON should re-push to keep
    the record byte-identical to the tracker body.
 
+6. **Verify the state advance landed (DRAFT → REVIEW gate).** When
+   step 4 above succeeded **and** the JSON pushed included
+   `body.CNA_private.state = "REVIEW"`, immediately fetch the
+   record to confirm the state actually advanced:
+
+   ```bash
+   uv run --project <framework>/tools/vulnogram/oauth-api 
vulnogram-api-record-fetch \
+     --cve-id <CVE-ID> --jq '.body.CNA_private.state'
+   ```
+
+   *(If `vulnogram-api-record-fetch` is not yet available on the
+   operator's machine — the CLI was added together with this
+   gate; see 
[`tools/vulnogram/oauth-api/README.md`](../../../tools/vulnogram/oauth-api/README.md)
+   — fall back to extracting the state from the
+   `record-update` call's response envelope, which already
+   includes the saved `CNA_private.state`.)*
+
+   Three outcomes:
+
+   - **`"REVIEW"` or any later state (`READY` / `PUBLIC`)** →
+     state-gate clear. Step 5c picks the OAuth-pushed hand-off
+     variant and Step 4 of the *Reconcile* flow posts /
+     PATCH-flips the RM hand-off comment. Step 6 recap notes
+     *"CVE record state auto-promoted to REVIEW at
+     `PUSH_TIMESTAMP`."*
+   - **`"DRAFT"`** → state-gate NOT cleared. Surface the
+     specific reason: the most common case is one of the body
+     fields was empty so the JSON did not include
+     `state = "REVIEW"` in the first place (Stage 1 of the
+     two-stage gate caught this); the other common case is that
+     a body field carried a value the CNA schema rejected
+     silently (the upsert saved fields it could parse but did not
+     advance the state). Either way, **do not post the RM
+     hand-off comment**. Fire the *Remediation-developer
+     fill-fields comment* instead per the dedicated Step 2b
+     bullet, and surface the state-gate-not-cleared blocker in
+     the Step 6 recap.
+   - **Fetch failed (transient HTTP error, session expired
+     between push and fetch)** → conservative fallback: surface
+     the fetch failure as a blocker, post nothing on the
+     RM-hand-off front this run, and retry the verification on
+     the next sync.
+
 ## Step 5c — Reconcile the release-manager hand-off comment
 
 The Step 12 (`pr merged` → `fix released`) **hand-off comment** and
diff --git a/tools/vulnogram/oauth-api/pyproject.toml 
b/tools/vulnogram/oauth-api/pyproject.toml
index 36e14a5..60ce98c 100644
--- a/tools/vulnogram/oauth-api/pyproject.toml
+++ b/tools/vulnogram/oauth-api/pyproject.toml
@@ -36,6 +36,7 @@ dependencies = []
 vulnogram-api-setup = "vulnogram_api.setup_session:main"
 vulnogram-api-record-update = "vulnogram_api.record_update:main"
 vulnogram-api-record-publish = "vulnogram_api.record_publish:main"
+vulnogram-api-record-fetch = "vulnogram_api.record_fetch:main"
 vulnogram-api-check = "vulnogram_api.check:main"
 
 [dependency-groups]
diff --git a/tools/vulnogram/oauth-api/src/vulnogram_api/record_fetch.py 
b/tools/vulnogram/oauth-api/src/vulnogram_api/record_fetch.py
new file mode 100644
index 0000000..3fb8cf6
--- /dev/null
+++ b/tools/vulnogram/oauth-api/src/vulnogram_api/record_fetch.py
@@ -0,0 +1,133 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Read a Vulnogram CVE record's stored JSON over the OAuth API.
+
+Used by `security-issue-sync` to verify the record state after a
+`vulnogram-api-record-update` push — the new `DRAFT` → `REVIEW`
+auto-promote gate in Step 5b.6 of the sync skill checks whether the
+state actually advanced before firing the release-manager hand-off
+comment. See ``.claude/skills/security-issue-sync/SKILL.md`` for the
+gate-decision flow.
+
+Outputs:
+
+- Default: writes the record's full stored JSON to stdout (one
+  compact line, suitable for piping into ``jq``).
+- ``--state-only``: writes just the ``CNA_private.state`` field
+  (e.g. ``DRAFT`` / ``REVIEW`` / ``READY`` / ``PUBLIC``) to stdout,
+  no JSON. Useful for shell-parsing in the sync skill without a
+  ``jq`` dependency.
+
+Read-only: this script never POSTs. The companion
+:mod:`vulnogram_api.record_update` writes; :mod:`vulnogram_api.record_publish`
+flips state.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+
+from vulnogram_api.client import (
+    SessionExpired,
+    VulnogramAPIError,
+    get_record,
+)
+from vulnogram_api.credentials import Session, locate_session
+
+CVE_ID_RE = re.compile(r"^CVE-\d{4}-\d{4,7}$")
+
+
+def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+    ap = argparse.ArgumentParser(
+        description=(__doc__ or "").split("\n\n", 1)[0],
+    )
+    ap.add_argument(
+        "--cve-id",
+        required=True,
+        help="The CVE ID, e.g. CVE-2026-12345.",
+    )
+    ap.add_argument(
+        "--credentials",
+        default=None,
+        help=(
+            "Path to the session JSON. Defaults to "
+            "$VULNOGRAM_SESSION, else "
+            "~/.config/apache-steward/vulnogram-session.json."
+        ),
+    )
+    ap.add_argument(
+        "--section",
+        default="cve5",
+        help="Vulnogram section path component. Default: cve5.",
+    )
+    ap.add_argument(
+        "--state-only",
+        action="store_true",
+        help=(
+            "Print only the CNA_private.state field (one word, no "
+            "JSON). Useful for shell-parsing without a jq dependency."
+        ),
+    )
+    return ap.parse_args(argv)
+
+
+def main(argv: list[str] | None = None) -> int:
+    args = parse_args(argv)
+
+    if not CVE_ID_RE.match(args.cve_id):
+        raise SystemExit(f"--cve-id {args.cve_id!r} does not match 
CVE-YYYY-NNNN form. Refusing to fetch.")
+
+    creds_path = locate_session(args.credentials)
+    session = Session.load(creds_path)
+
+    try:
+        document = get_record(session, args.cve_id, section=args.section)
+    except SessionExpired as e:
+        print(f"✗ {e}", file=sys.stderr)
+        return 2
+    except VulnogramAPIError as e:
+        print(f"✗ {e}", file=sys.stderr)
+        return 6
+
+    if args.state_only:
+        cna = document.get("CNA_private")
+        if not isinstance(cna, dict):
+            print(
+                f"✗ {args.cve_id} document's CNA_private field is not an 
object: {type(cna).__name__}.",
+                file=sys.stderr,
+            )
+            return 7
+        state = cna.get("state")
+        if not isinstance(state, str):
+            print(
+                f"✗ {args.cve_id} document's CNA_private.state is not a 
string: {state!r}.",
+                file=sys.stderr,
+            )
+            return 7
+        print(state)
+        return 0
+
+    json.dump(document, sys.stdout, separators=(",", ":"), ensure_ascii=False)
+    sys.stdout.write("\n")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/tools/vulnogram/record.md b/tools/vulnogram/record.md
index ad6349b..1792623 100644
--- a/tools/vulnogram/record.md
+++ b/tools/vulnogram/record.md
@@ -80,13 +80,24 @@ outcomes drive the proposal:
   with a one-line *"or skip setup and use the copy-paste flow
   below"* hint. **Do not** force setup on an unwilling operator.
 
-The state-transition clicks (`DRAFT` → `REVIEW`,
-`REVIEW` → `READY`, `READY` → `PUBLIC`) stay in the Vulnogram UI
-in **both** paths. The state lives in the document body's
-`CNA_private.state` field — pre-setting it before the API call
-works for `DRAFT` ↔ `REVIEW`, but the `READY` → `PUBLIC` push has
-out-of-band side effects (CNA-feed dispatch to `cve.org`) and we
-deliberately do not automate it.
+State transitions happen at three different layers depending on
+which arrow you're crossing:
+
+- **`DRAFT` ↔ `REVIEW`** — sync-driven via the generator. The
+  `generate-cve-json` tool emits the correct state in the JSON
+  based on body-field readiness; sync pushes via
+  `vulnogram-api-record-update`; Vulnogram accepts the state from
+  the document body. No separate state-flip call.
+- **`REVIEW` → `READY`** — release-manager UI click. The generator
+  cannot decide this from the tracker body alone (it depends on
+  whether reviewer comments are closed), so it stays a human
+  action.
+- **`READY` → `PUBLIC`** — sync-driven via the dedicated
+  `vulnogram-api-record-publish` CLI, fired when the advisory
+  archive URL has been captured. The CLI defaults to refusing
+  the publish unless the current state is `REVIEW` (widen with
+  `--allow-state` for the rare cases where the RM has already
+  moved to `READY` manually).
 
 ## `#source` paste flow
 
@@ -115,43 +126,49 @@ states the generic skills interact with:
 | State | Set by | What it means |
 |---|---|---|
 | `DRAFT` | Allocation (initial state post-allocation) | Record exists, ID is 
reserved, but content is still being filled in. Not visible on `cve.org`. |
-| `REVIEW` | Release manager once the content is complete | Ready for CNA 
review. ASF CNA reviewers may leave reviewer comments at this point (see 
*"Reviewer-comment signal"* below). Still not on `cve.org`. |
+| `REVIEW` | **Sync, via the generator** (post-2026 convention; see below) | 
Ready for CNA review. ASF CNA reviewers may leave reviewer comments at this 
point (see *"Reviewer-comment signal"* below). Still not on `cve.org`. |
 | `READY` | Release manager once review feedback is addressed (or immediately 
after `REVIEW` if no feedback arrived) | Content is final and the record is 
staged for the advisory-send step. The advisory emails are dispatched from 
Vulnogram while in `READY`. Still not on `cve.org`. |
-| `PUBLIC` | Release manager as the terminal step | Record pushed to 
`cve.org`. World-readable. The generic tracking-issue lifecycle terminates at 
the `close` action once this state has been reached. |
-
-**`DRAFT` → `REVIEW`** happens when all required fields are populated.
-The release manager moves the record to `REVIEW` after the first
-JSON paste, opening the window for CNA reviewers to leave comments.
-
-**`REVIEW` → `READY`** happens once any reviewer comments have been
-addressed (via the body-field round-trip described in the
-*Record-generator round trip* section below), or immediately after
-`REVIEW` when no comments arrive. `READY` is the state Vulnogram
-expects when the release manager triggers the advisory email send.
-
-**`READY` → `PUBLIC`** is a human release-manager click in Vulnogram
-after the advisory archive URL has been captured on the tracker. The
-generic `security-issue-sync` skill's Step 2b does not propose this
-transition — it is a Step 15 release-manager action. The
-publication-ready notification comment (see
-[*Release-manager checklist*](#release-manager-checklist) below)
-gives the RM the explicit go-ahead.
+| `PUBLIC` | **Sync, via `vulnogram-api-record-publish`**, when the advisory 
archive URL is captured on the tracker | Record pushed to `cve.org`. 
World-readable. The generic tracking-issue lifecycle terminates at the `close` 
action once this state has been reached. |
+
+**`DRAFT` → `REVIEW` — sync-driven via the generator.** The
+`generate-cve-json` tool decides `REVIEW` versus `DRAFT` automatically
+based on the readiness of the tracker's body fields — see the
+`_is_cna_ready_for_review` helper in `generate-cve-json/src/…/cve_json.py`.
+When all required fields (CVE ID, title, description, affected
+versions, CWE, non-`Unknown` severity, at least one credit, at
+least one reference) are populated, the generated JSON carries
+`CNA_private.state = "REVIEW"`; when any field is missing, it
+carries `"DRAFT"`. Sync pushes the generated JSON verbatim via
+`vulnogram-api-record-update`, and Vulnogram accepts the state
+field from the document body — no separate state-flip API call
+is needed. The release manager **never** has to click `DRAFT` →
+`REVIEW` manually; the `security-issue-sync` skill gates the RM
+hand-off on the post-push state being `REVIEW` (see
+[`security-issue-sync` Step 2b *Two-stage 
gate*](../../.claude/skills/security-issue-sync/SKILL.md)).
+
+**`REVIEW` → `READY` — release-manager UI click.** Happens once any
+reviewer comments have been addressed (via the body-field
+round-trip described in the *Record-generator round trip* section
+below), or immediately after `REVIEW` when no comments arrive.
+`READY` is the state Vulnogram expects when the release manager
+triggers the advisory email send. The generator does not emit
+`READY` directly because it cannot tell, from the tracker body
+alone, whether reviewer comments are still pending — that
+judgement stays with the RM.
+
+**`READY` → `PUBLIC` — sync-driven via `vulnogram-api-record-publish`**,
+fired when the advisory archive URL has been captured on
+`lists.apache.org/list.html?<users-list>` (the real-world signal
+the advisory has actually shipped). This transition was
+historically a manual RM click because it triggers the CNA-feed
+dispatch to `cve.org`; the post-2026 convention drives it from
+sync because the captured archive URL is the same signal a human
+would use to decide it is safe to flip.
 
 `PUBLISHED` is sometimes used as a synonym for `PUBLIC` in older
 Vulnogram documentation; the current action is literally labelled
 `PUBLIC` in the UI.
 
-The `generate-cve-json` tool decides `REVIEW` versus `DRAFT` for the
-generated `CNA_private.state` field by inspecting whether the tracker's
-*public-advisory-url* body field is populated and whether a
-`vendor-advisory` reference landed in the generated `references[]`;
-see the *`_is_cna_ready_for_review`* helper in
-`generate-cve-json/src/…/cve_json.py` for the exact predicate. The
-`READY` state itself is set by hand in Vulnogram after the
-post-`REVIEW` body-field stabilisation — the generator does not emit
-`READY` directly because it cannot tell, from the tracker body alone,
-whether reviewer comments are still pending.
-
 ## Reviewer-comment signal
 
 ASF CNA reviewers leave comments on `REVIEW`-state records. Those
@@ -215,36 +232,36 @@ or copy-paste (fallback). The instructions below show the 
API form
 inline; the copy-paste form is the *"open `#source`, paste, Save"*
 flow described in [*`#source` paste flow*](#source-paste-flow):
 
-1. **First write — `DRAFT` → `REVIEW`.** Push the CVE JSON
-   embedded in the tracking issue (regenerated by `generate-cve-json`
-   on every sync — see
-   [*Record-generator round trip*](#record-generator-round-trip)
-   above). Then move the record `DRAFT` → `REVIEW` via the Vulnogram
-   UI button.
-
-   ```bash
-   # API path (default). Save the embedded JSON to a file first if
-   # you only have it in the tracker body — `gh issue view <N>
-   # --jq .body` + an editor's selection-to-file works, or use
-   # `generate-cve-json --out <path>` to write the canonical artefact.
-   uv run --project <framework>/tools/vulnogram/oauth-api 
vulnogram-api-record-update \
-     --cve-id <CVE-ID> --json-file <path>
-   ```
-
-2. **(conditional) Re-write after reviewer comments.** ASF CNA
-   reviewers may leave comments while the record sits in `REVIEW`
-   (see [*Reviewer-comment signal*](#reviewer-comment-signal) above).
-   The `security-issue-sync` skill detects them automatically and
-   proposes matching body-field updates on the tracker; the security
-   team confirms and the embedded JSON regenerates. Once the body has
-   settled (no more pending reviewer-comment proposals), re-push the
-   regenerated JSON via the same `vulnogram-api-record-update` call
-   (or, fallback, re-paste into `#source` and **Save**). **Skip this
-   step if no reviewer comments arrived** (the common case for
-   well-formed records).
+1. **(pre-handoff, sync-driven) — record reaches `REVIEW` state.**
+   The release manager picks up the tracker with the record
+   *already* in `REVIEW` state. Sync pushes the
+   generator-emitted JSON via `vulnogram-api-record-update` and
+   the generator emits `CNA_private.state = "REVIEW"` when all
+   the required body fields are populated. The hand-off comment
+   the RM receives only fires once sync confirms the post-push
+   state is `REVIEW` — until then the tracker stays with the
+   remediation developer and a [*fill missing 
fields*](remediation-developer-fill-fields-comment.md)
+   comment names what's blocking. **The RM never performs the
+   `DRAFT` → `REVIEW` click.**
+
+2. **(conditional) Body-field round-trip after reviewer comments.**
+   ASF CNA reviewers may leave comments while the record sits in
+   `REVIEW` (see [*Reviewer-comment signal*](#reviewer-comment-signal)
+   above). The `security-issue-sync` skill detects them
+   automatically and proposes matching body-field updates on the
+   tracker; the security team confirms and the embedded JSON
+   regenerates. Sync re-pushes via the same
+   `vulnogram-api-record-update` call. **As the RM, you wait
+   for the security team's next sync** — the round-trip is fully
+   handled there; you only watch the `#email` tab for the
+   reviewer thread to close. **Skip this step if no reviewer
+   comments arrived** (the common case for well-formed records).
 
 3. **Set `READY`.** Vulnogram UI action — moves the record from
    `REVIEW` to `READY` and stages it for the advisory-send step.
+   This is the **first** state-flip the RM performs; the
+   generator cannot do it (it cannot tell from the tracker body
+   alone whether reviewer comments are closed).
 
 4. **Preview the advisory email** on the
    [`#email` tab](#record-urls). The preview renders the advisory
@@ -271,28 +288,31 @@ flow described in [*`#source` paste 
flow*](#source-paste-flow):
    
[`release-manager-publication-comment.md`](release-manager-publication-comment.md)).
    That comment is the explicit go-ahead for steps 7-8.
 
-7. **Second write — `READY` → `PUBLIC`.** *Only after the
-   publication-ready notification fires.* Push the now-final JSON
-   (carrying the archive URL in `references[]`) via the API path
-   (or, fallback, re-open `#source` and paste). Then move the record
-   `READY` → `PUBLIC` via the Vulnogram UI button — the
-   `READY` → `PUBLIC` transition is intentionally not automated
-   because it triggers the CNA-feed dispatch to `cve.org`.
-
-   ```bash
-   uv run --project <framework>/tools/vulnogram/oauth-api 
vulnogram-api-record-update \
-     --cve-id <CVE-ID> --json-file <path-to-final-json>
-   ```
-
-8. **Close the tracker.** Close as completed; do not update any
-   labels. The `security-issue-sync` skill's apply step archives the
-   project-board item afterwards (per the *archive-from-board* recipe
-   in [`../github/project-board.md`](../github/project-board.md))
-   so the closed tracker leaves the active board.
-
-The "twice write" framing in the hand-off comment maps to steps 1
-and 7 of the checklist above; step 2 is the rare conditional middle
-write. The hand-off comment (template at
+7. **(sync-driven) — record reaches `PUBLIC`.** *Fires
+   automatically once the publication-ready notification has
+   landed.* On its next pass, sync re-pushes the regenerated
+   JSON (now carrying the archive URL as a `vendor-advisory`
+   reference) and then moves the record `READY` → `PUBLIC` via
+   the dedicated `vulnogram-api-record-publish` CLI (which keys
+   off the captured archive URL — the real-world signal that
+   the advisory shipped). **The RM does not perform the
+   `READY` → `PUBLIC` click.**
+
+8. **Close the tracker.** Sync's apply step closes the tracker
+   as completed (do not update labels) and archives the
+   project-board item afterwards (per the *archive-from-board*
+   recipe in [`../github/project-board.md`](../github/project-board.md)).
+   The RM's last touchpoint is the wrap-up comment sync posts
+   on the tracker — archive from the `Announced` column on the
+   board and (conditionally) close the milestone if the just-
+   closed tracker was the last open issue on it.
+
+**RM-side write count** is **zero** in the common case (no
+reviewer comments): sync handles steps 1, 2, 6, 7, 8; the RM
+clicks `REVIEW` → `READY` (step 3), previews the advisory
+(step 4), and sends it (step 5). When reviewer comments arrive,
+the round-trip in step 2 is also handled by sync; the RM still
+performs only steps 3–5. The hand-off comment (template at
 [`release-manager-handoff-comment.md`](release-manager-handoff-comment.md))
-proposes the API one-liner inline and links the copy-paste fallback;
-keep both in lock-step when one changes.
+walks the RM through their three actions without invoking any
+`uv run` commands; keep both in lock-step when one changes.
diff --git a/tools/vulnogram/release-manager-handoff-comment-oauth-pushed.md 
b/tools/vulnogram/release-manager-handoff-comment-oauth-pushed.md
index 44e2399..c6e9ce0 100644
--- a/tools/vulnogram/release-manager-handoff-comment-oauth-pushed.md
+++ b/tools/vulnogram/release-manager-handoff-comment-oauth-pushed.md
@@ -2,8 +2,11 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
-- [🚀 Release-manager hand-off — `CVE_ID` (CVE JSON 
auto-pushed)](#-release-manager-hand-off--cve_id-cve-json-auto-pushed)
-  - [Step-by-step](#step-by-step)
+- [🚀 Release-manager hand-off — `CVE_ID`  *(CVE JSON 
auto-pushed)*](#-release-manager-hand-off--cve_id--cve-json-auto-pushed)
+  - [Step 1 of 3 — address reviewer feedback (if any), then promote to 
READY](#step-1-of-3--address-reviewer-feedback-if-any-then-promote-to-ready)
+  - [Step 2 of 3 — preview the advisory email, then send 
it](#step-2-of-3--preview-the-advisory-email-then-send-it)
+  - [Step 3 of 3 — sync closes out the rest (no further action from 
you)](#step-3-of-3--sync-closes-out-the-rest-no-further-action-from-you)
+  - [Reference links (only if you want 
them)](#reference-links-only-if-you-want-them)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -25,20 +28,30 @@
      `.claude/skills/security-issue-sync/SKILL.md` for the decision
      flow.
 
-     Idempotency: the marker on the line below is the **same** as the
-     manual-paste variant's. The skill's idempotency grep keys on the
-     marker only; the variant choice is detected by re-reading the
-     comment body and checking which template's signature it carries.
-     On a re-sync where the previous comment is the manual-paste
-     variant but the current push succeeded, the skill PATCH-edits the
-     comment body in place to the OAuth-pushed body (no second comment).
+     **Gate**: this comment is ONLY posted when the CVE record's
+     state in Vulnogram is `REVIEW` (verified by sync via
+     `vulnogram-api-record-fetch` after Step 5b's push attempt).
+     When the record is still `DRAFT` after the push attempt — for
+     any reason — sync posts the
+     `remediation-developer-fill-fields-comment.md` instead and the
+     tracker stays assigned to the remediation developer. The RM
+     never receives a hand-off while the record is in `DRAFT`.
+
+     Idempotency: the marker on the first line is the **same** as
+     the manual-paste variant's. The skill's idempotency grep keys
+     on the marker only; the variant choice is detected by
+     re-reading the comment body and checking which template's
+     signature it carries. On a re-sync where the previous comment
+     is the manual-paste variant but the current push succeeded,
+     the skill PATCH-edits the comment body in place to the
+     OAuth-pushed body (no second comment).
 
      The OAuth-pushed variant intentionally carries **no `uv run`
      invocations as RM-facing instructions** — the API push (and any
-     re-push triggered by a body change) is run by `security-issue-sync`
-     during sync, not by the release manager. The RM's surface is
-     restricted to Vulnogram UI clicks, reviewer-thread responses, and
-     the advisory send.
+     re-push triggered by a body change) is run by
+     `security-issue-sync` during sync, not by the release manager.
+     The RM's surface is restricted to Vulnogram UI clicks,
+     reviewer-thread responses, and the advisory send.
 
      Placeholders the skill substitutes:
 
@@ -52,12 +65,13 @@
        EMAIL_TAB_URL             <cve_tool_record_url_template>#email
        JSON_ANCHOR_URL           Tracker body deep-link to the embedded
                                  CVE JSON section
-       ARCHIVE_SCAN_URL          The PonyMail / archive search URL for
-                                 USERS_LIST (parameterised on CVE_ID)
-       MILESTONE_URL             Tracker-side URL of the milestone this
-                                 tracker belongs to (used in the
-                                 conditional close-milestone line of the
-                                 wrap-up comment in Step 6)
+       BOARD_URL                 Project-board URL with the `Status:
+                                 Announced` filter pre-applied
+       MILESTONE_URL             Tracker-side URL of the milestone
+                                 this tracker belongs to (used in
+                                 the conditional close-milestone
+                                 line of the wrap-up comment in
+                                 Step 3)
        FRAMEWORK_RECORD_MD_URL   Link to tools/vulnogram/record.md on
                                  the framework's GitHub
        FRAMEWORK_SYNC_SKILL_URL  Link to .claude/skills/security-issue-sync/
@@ -73,103 +87,68 @@
 -->
 <!-- apache-steward: release-manager-handoff v1 -->
 
-## 🚀 Release-manager hand-off — `CVE_ID` (CVE JSON auto-pushed)
-
-RM_HANDLE, the release containing the fix has shipped — this tracker
-now belongs to you. **The CVE JSON has already been pushed to
-[`#source`](SOURCE_TAB_URL) by the security team via the OAuth API at
-`PUSH_TIMESTAMP`**, and `security-issue-sync` keeps the record in
-lock-step with the tracker body on every subsequent sync run. Your
-remaining work is a small handful of Vulnogram-UI clicks plus the
-advisory send — no shell commands required.
-
-### Step-by-step
-
-1. **Confirm the record is in `REVIEW`.** Open
-   [`#source`](SOURCE_TAB_URL). If the record is still in `DRAFT`,
-   click the Vulnogram UI button to move `DRAFT` → `REVIEW`. (The
-   data is already in place — `PUSH_TIMESTAMP`.)
-
-2. **Respond to any pending reviewer asks** in the
-   [`#email` tab](EMAIL_TAB_URL). Reviewer comments arrive by email on
-   `SECURITY_LIST` with the CVE ID in the subject line —
-   [`security-issue-sync`](FRAMEWORK_SYNC_SKILL_URL) detects them
-   automatically and updates the tracker body if a field needs to
-   change. If the body changes, the JSON regenerates and re-pushes as
-   part of the next sync; you do not push manually.
-
-3. **Set `READY`** via the Vulnogram UI button when the reviewer
-   thread closes. The record is now ready for the advisory-send step.
-
-4. **Preview the advisory email** on the
-   [`#email` tab](EMAIL_TAB_URL). Inspect how the email will render:
-   subject, body, recipient list. The preview surfaces formatting
-   issues (truncation, broken markdown, missing patch links) that the
-   JSON view does not. If anything needs to change, edit the
-   corresponding tracker body field — the JSON regenerates and
-   re-pushes; re-preview before sending.
-
-5. **Send the advisory** from the Vulnogram form. The form sends to
-   `USERS_LIST` and `ANNOUNCE_LIST`. **Do not touch the tracker
-   labels** — sync handles the label flips automatically when it sees
-   the advisory on the users-list (see Step 6).
-
-6. **(fully automatic — sync skill drives the lifecycle close-out.)**
-   On the next sync run after the advisory lands in the
-   [users-list archive](ARCHIVE_SCAN_URL),
-   [`security-issue-sync`](FRAMEWORK_SYNC_SKILL_URL):
-   - Captures the published advisory URL into the *Public advisory
-     URL* body field.
-   - **Extracts the public-facing short summary** from the advisory
-     email body and writes it back to the *Short summary for the
-     publish* body field, so the tracker matches what actually
-     shipped.
-   - **Flips the tracker labels**: adds `announced - emails sent` and
-     `announced`; removes `fix released`. The `announced` label
-     triggers the project-board automation to move the item from the
-     `Fix released` column to the `Announced` column.
-   - Regenerates the embedded CVE JSON (now picking up the updated
-     short summary as `descriptions[].value` and the archive URL as a
-     `vendor-advisory` reference) and **re-pushes the JSON to the
-     Vulnogram record** over the OAuth API.
-   - **Moves the record `REVIEW → PUBLIC`** via the OAuth API. This
-     triggers the CNA-feed dispatch to `cve.org`. (Previously a
-     manual UI click; sync now drives it on the same trigger as the
-     archive-URL capture, since the archive URL is the real-world
-     signal that the advisory has actually shipped.)
-   - **Closes the tracker** as `completed`.
-   - **Posts a follow-up comment** tagging the RM with the wrap-up
-     checklist: archive the now-closed tracker from the project
-     board's `Announced` column, and — **if every sibling on the
-     tracker's milestone is also closed** at that moment — close the
-     milestone via the link in the comment ([`MILESTONE_URL`](MILESTONE_URL)).
-     If other siblings on the milestone are still open, the wrap-up
-     comment omits the close-milestone line; the close happens when
-     the *last* sibling tracker reaches the same Step 6.
-
-7. **Follow the wrap-up comment** posted by sync in Step 6. Archive
-   the closed tracker from the project board's `Announced` column
-   (it stays accessible via the *Archived items* filter). If the
-   comment also linked the milestone (last-sibling case), click
-   through and close it — that's the explicit *"everything destined
-   for this release has shipped and been announced"* signal.
+## 🚀 Release-manager hand-off — [`CVE_ID`](SOURCE_TAB_URL)  *(CVE JSON 
auto-pushed)*
+
+RM_HANDLE — the release with the fix has shipped, and the CVE record on 
Vulnogram is in **`REVIEW`** state with all mandatory content populated. The 
security team's last sync auto-pushed the CVE JSON over the OAuth API at 
`PUSH_TIMESTAMP` and the record state advanced to `REVIEW`, which is the 
precondition for this hand-off.
+
+This tracker is now yours to drive from **Steps 13 → 14 → 15** of the security 
process. Three actions, in order; each one is a single click in Vulnogram; **no 
shell commands required from you at any point**.
+
+> **You will never see this comment while the record is in `DRAFT`.** Sync 
gates the hand-off on the record reaching `REVIEW`. If you ever see this 
comment paired with a `DRAFT` state on the linked record, please ping @potiuk 
on this issue before clicking anything in Vulnogram — that combination is a bug 
we want to know about, not a state for you to resolve.
+>
+> *(The security team has already pushed the CVE JSON content into Vulnogram 
and filled every body field on this tracker that the public advisory needs. 
Your job is to: address any CVE-reviewer feedback that lands during `REVIEW`, 
move the record to `READY` when review closes, then send the advisory email. 
That's it.)*
+
+---
+
+### Step 1 of 3 — address reviewer feedback (if any), then promote to READY
+
+Open the record's [`#source` tab](SOURCE_TAB_URL) in your browser. **State** 
field at the top should read `REVIEW` — that is the precondition for this 
comment firing, so it should match. If it doesn't, stop and ping @potiuk; 
otherwise:
+
+1. **Click the [`#email` tab](EMAIL_TAB_URL)** on the same page. Scroll 
through any reviewer comments left by the ASF Security Team's CVE reviewers. 
**You do not need to act on reviewer comments yourself** — they arrive by email 
on `SECURITY_LIST` with the CVE ID in the subject, and sync detects them on the 
next run, opens corresponding body-field updates on this tracker, and re-pushes 
the JSON. If the comments tab is empty, or carries a closure note (*"OK, looks 
good"* / *"approved"*),  [...]
+
+2. **When the reviewer thread is clear** (no open comments, or all comments 
have an *"OK, looks good"*-style closer), use the **State** dropdown on 
`#source` to change `REVIEW` → `READY`. Click **Save**. *The record is now 
staged for advisory send.*
+
+> 💡 *How do you know the reviewer thread is clear?* Two signals: (a) no new 
reviewer email on `SECURITY_LIST` carrying the CVE ID for ~3 days, or (b) an 
explicit "looks good" reply from the reviewer. Most CVEs go through `REVIEW` 
with no reviewer comments at all — in that case, you can usually move `REVIEW → 
READY` immediately after Step 1.1's tab-check confirms there's nothing to 
address.
+
+---
+
+### Step 2 of 3 — preview the advisory email, then send it
+
+With the record in `READY`, click the [`#email` tab](EMAIL_TAB_URL) on the 
same record page. This shows you, in the exact format that goes out, what the 
advisory email will look like when sent to `USERS_LIST` and `ANNOUNCE_LIST`.
+
+**Check that:**
+- The subject line is `CVE_ID: <one-line description>` and the description 
matches what you'd want public.
+- The body's short-summary paragraph reads as instructions to a user (*"Users 
are advised to upgrade to version X"*), not just a technical description of the 
bug.
+- The *Affected versions* range is correct.
+- The reporter credit line is present and spelled correctly.
+
+**If anything looks wrong**: don't edit it in Vulnogram. Comment on this 
tracker (just `@potiuk: the X field needs Y`) and we'll fix the corresponding 
body field here, regenerate the JSON, and re-push within the next sync. 
Re-preview after that.
+
+**If everything looks right**: click the **Send Email** button on the `#email` 
tab. The advisory ships to `USERS_LIST` and `ANNOUNCE_LIST`. **That is the only 
manual send action you make for this CVE.**
+
+> ⚠️ **Do not touch the tracker labels yourself.** Sync flips `fix released` → 
`announced - emails sent` + `announced` automatically when it sees the advisory 
in the public archive (usually within the same day). If you flip them manually 
you race the automation.
 
 ---
 
-**If the OAuth push fails on a future sync** (session expired, schema
-rejection, transient HTTP error), the sync's recap surfaces the
-failure and PATCH-edits this comment back to the manual-paste variant
-([`release-manager-handoff-comment.md`](FRAMEWORK_RECORD_MD_URL)). The
-most common cause is a stale OAuth session cookie on the operator
-machine — the security team re-runs `vulnogram-api-setup`, the next
-sync resumes auto-pushing, and the comment flips back to this
-variant. **You as the RM are never asked to run shell commands** in
-this fallback path.
+### Step 3 of 3 — sync closes out the rest (no further action from you)
+
+Once the advisory archives on `lists.apache.org/list.html?USERS_LIST` 
(typically within minutes of sending), the next sync run does this for you, 
end-to-end:
+
+1. Captures the published advisory URL into this tracker's body.
+2. Regenerates the CVE JSON (now including the archive URL as a 
`vendor-advisory` reference) and re-pushes it to Vulnogram via the OAuth API.
+3. Moves the Vulnogram record `READY` → `PUBLIC` (this is the CNA-feed 
dispatch — once it lands, `cve.org` starts propagating).
+4. Flips this tracker's labels (`fix released` → `announced - emails sent` + 
`announced`).
+5. Closes this tracker as `completed`.
+6. **Archives this tracker** from the `Announced` column on the [Security 
issues board](BOARD_URL) (`archiveProjectV2Item` GraphQL mutation — sync, not 
you).
+7. *(Conditional)* **Closes the [`MILESTONE_TITLE`](MILESTONE_URL) milestone** 
if this tracker was the last open issue on it.
+
+The CVE will propagate to `cve.org` on its own within a few hours; sync will 
detect the publication on a subsequent run and post a courtesy *"CVE is live on 
cve.org"* note to the reporter on the original email thread.
+
+**You're done.** The lifecycle is complete from your side at Step 2 (Send 
Email). Everything above is sync's job — no further comments will tag you with 
manual cleanups.
 
 ---
 
-**References:**
+### Reference links (only if you want them)
 
-- Vulnogram state machine: 
[`tools/vulnogram/record.md`](FRAMEWORK_RECORD_MD_URL).
-- Reusable email wording (if you draft anything by hand): 
[`canned-responses.md`](CANNED_RESPONSES_URL).
-- Full lifecycle (Steps 12-15): 
[`README.md`](FRAMEWORK_README_URL#for-release-managers--steps-1215).
+- **The full lifecycle in one place** — [`README.md` Steps 
12–15](FRAMEWORK_README_URL#for-release-managers--steps-1215)
+- **Vulnogram-specific mechanics** (state machine, paste-flow details) — 
[`tools/vulnogram/record.md`](FRAMEWORK_RECORD_MD_URL)
+- **Reusable email wording for ad-hoc replies** — 
[`canned-responses.md`](CANNED_RESPONSES_URL)
diff --git a/tools/vulnogram/release-manager-handoff-comment.md 
b/tools/vulnogram/release-manager-handoff-comment.md
index aaac446..afda54f 100644
--- a/tools/vulnogram/release-manager-handoff-comment.md
+++ b/tools/vulnogram/release-manager-handoff-comment.md
@@ -3,7 +3,10 @@
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
 - [🚀 Release-manager hand-off — `CVE_ID`](#-release-manager-hand-off--cve_id)
-  - [Step-by-step](#step-by-step)
+  - [Step 1 of 3 — address reviewer feedback (if any), then promote to 
READY](#step-1-of-3--address-reviewer-feedback-if-any-then-promote-to-ready)
+  - [Step 2 of 3 — preview the advisory email, then send 
it](#step-2-of-3--preview-the-advisory-email-then-send-it)
+  - [Step 3 of 3 — sync closes out the rest (no further action from 
you)](#step-3-of-3--sync-closes-out-the-rest-no-further-action-from-you)
+  - [Reference links (only if you want 
them)](#reference-links-only-if-you-want-them)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -14,7 +17,8 @@
      Manual-paste variant of the release-manager hand-off comment
      posted by `security-issue-sync` at the `pr merged` → `fix released`
      transition (Step 12 of the lifecycle), when the OAuth API push
-     did not succeed (no OAuth credentials configured on the operator
+     of the CVE JSON did not succeed during the sync run that fires
+     this hand-off (no OAuth credentials configured on the operator
      machine, session expired, transient HTTP error, schema rejection).
 
      This template is the counterpart to
@@ -24,14 +28,25 @@
      `.claude/skills/security-issue-sync/SKILL.md` for the decision
      flow.
 
-     **No `uv run` invocations are RM-facing in this template either.**
-     When the OAuth push fails, the security team's next sync resolves
-     the issue (re-run `vulnogram-api-setup`, retry the push, etc.).
-     The RM's surface stays the same as in the OAuth-pushed variant
-     plus an explicit "you may need to paste the JSON into #source"
-     fallback if the security team cannot resolve the OAuth path. The
-     paste itself is a UI action — click Edit → paste → Save — not a
-     shell command.
+     **Gate**: this comment is ONLY posted when the CVE record's
+     state in Vulnogram is `REVIEW` (verified by sync via
+     `vulnogram-api-record-fetch` after Step 5b's push attempt).
+     When the record is still `DRAFT` after the push attempt — for
+     any reason — sync posts the
+     `remediation-developer-fill-fields-comment.md` instead and the
+     tracker stays assigned to the remediation developer. The RM
+     never receives a hand-off while the record is in `DRAFT`. The
+     "How to advance from `DRAFT` to `REVIEW`" lifecycle question is
+     scoped to the remediation-developer template, not this one.
+
+     **No `uv run` invocations are RM-facing in this template.**
+     When the OAuth push fails, the security team's next sync
+     resolves the issue (re-run `vulnogram-api-setup`, retry the
+     push). The RM's surface stays the same as in the OAuth-pushed
+     variant plus an explicit "you may need to paste the JSON into
+     #source" fallback if the security team cannot resolve the
+     OAuth path. The paste itself is a UI action — click Edit →
+     paste → Save — not a shell command.
 
      Placeholders the skill substitutes:
 
@@ -47,12 +62,15 @@
        JSON_ANCHOR_URL           Tracker body deep-link to the embedded
                                  CVE JSON section (e.g.
                                  
https://github.com/<tracker>/issues/<N>#cve-json--paste-ready-for-cve-yyyy-nnnn)
-       ARCHIVE_SCAN_URL          The PonyMail / archive search URL for
-                                 USERS_LIST (parameterised on CVE_ID)
-       MILESTONE_URL             Tracker-side URL of the milestone this
-                                 tracker belongs to (used in the
-                                 conditional close-milestone line of the
-                                 wrap-up comment in Step 6)
+       BOARD_URL                 Project-board URL with the `Status:
+                                 Announced` filter pre-applied (used
+                                 in Step 3's reference to the
+                                 wrap-up cleanup)
+       MILESTONE_URL             Tracker-side URL of the milestone
+                                 this tracker belongs to (used in
+                                 the conditional close-milestone
+                                 line of the wrap-up comment in
+                                 Step 3)
        FRAMEWORK_RECORD_MD_URL   Link to tools/vulnogram/record.md on
                                  the framework's GitHub
        FRAMEWORK_SYNC_SKILL_URL  Link to .claude/skills/security-issue-sync/
@@ -62,128 +80,93 @@
        CANNED_RESPONSES_URL      Link to <project-config>/canned-
                                  responses.md on the tracker's GitHub
 
-     The HTML marker on line 60 is load-bearing: the skill detects an
-     already-posted hand-off comment by grepping for this exact string
-     and skips the post on subsequent sync runs (idempotency).
+     The HTML marker on the first line is load-bearing: the skill
+     detects an already-posted hand-off comment by grepping for
+     this exact string and skips the post on subsequent sync runs
+     (idempotency).
 
-     Do not paraphrase the marker. Do not move it off line 60. Do not
-     add a `<!-- v2 -->` until the schema actually changes — the
-     skill's grep is anchored on `v1`.
+     Do not paraphrase the marker. Do not move it off line 1. Do
+     not add a `<!-- v2 -->` until the schema actually changes —
+     the skill's grep is anchored on `v1`.
 -->
 <!-- apache-steward: release-manager-handoff v1 -->
 
-## 🚀 Release-manager hand-off — `CVE_ID`
-
-RM_HANDLE, the release containing the fix has shipped — this tracker
-now belongs to you. The OAuth-API push of the CVE JSON did not
-succeed during the last sync (the security team will retry on the
-next pass); until that lands, the paste-ready CVE JSON in this
-tracker's [issue body](JSON_ANCHOR_URL) is the canonical version. If
-the OAuth path remains blocked when you start working through the
-checklist below, the manual-paste fallback at the bottom tells you
-what to copy where — **all via the Vulnogram UI, no shell commands
-required from you**.
-
-The Vulnogram-specific recipe lives in
-[`tools/vulnogram/record.md` — *Release-manager 
checklist*](FRAMEWORK_RECORD_MD_URL);
-the high-level numbered checklist below is here for at-a-glance
-reference without leaving this issue.
-
-### Step-by-step
-
-1. **Confirm the record content matches the tracker body.** Open
-   [`#source`](SOURCE_TAB_URL) and compare to the
-   [tracker body's embedded JSON](JSON_ANCHOR_URL). If they diverge
-   (the OAuth push remained blocked), follow the manual-paste
-   fallback at the bottom of this comment — open
-   [`#source`](SOURCE_TAB_URL), paste the embedded JSON, click
-   **Save**. Then click `DRAFT → REVIEW` via the Vulnogram UI button.
-
-2. **Respond to any pending reviewer asks** in the
-   [`#email` tab](EMAIL_TAB_URL). Reviewer comments arrive by email on
-   `SECURITY_LIST` with the CVE ID in the subject line —
-   [`security-issue-sync`](FRAMEWORK_SYNC_SKILL_URL) detects them
-   automatically and updates the tracker body if a field needs to
-   change. If the body changes and the OAuth push is healthy by then,
-   the JSON re-push runs in the next sync; if not, follow the
-   manual-paste fallback again to land the regenerated JSON.
-
-3. **Set `READY`** via the Vulnogram UI button when the reviewer
-   thread closes. The record is now ready for the advisory-send step.
-
-4. **Preview the advisory email** on the
-   [`#email` tab](EMAIL_TAB_URL). If anything needs to change, edit
-   the corresponding tracker body field; the JSON regenerates and (if
-   the OAuth push is healthy) re-pushes; re-preview before sending.
-
-5. **Send the advisory** from the Vulnogram form. The form sends to
-   `USERS_LIST` and `ANNOUNCE_LIST`. **Do not touch the tracker
-   labels** — sync handles the label flips automatically when it sees
-   the advisory on the users-list (see Step 6).
-
-6. **(fully automatic — sync skill drives the lifecycle close-out.)**
-   On the next sync run after the advisory lands in the
-   [users-list archive](ARCHIVE_SCAN_URL),
-   [`security-issue-sync`](FRAMEWORK_SYNC_SKILL_URL):
-   - Captures the published advisory URL into the *Public advisory
-     URL* body field.
-   - **Extracts the public-facing short summary** from the advisory
-     email body and writes it back to the *Short summary for the
-     publish* body field, so the tracker matches what actually
-     shipped.
-   - **Flips the tracker labels**: adds `announced - emails sent` and
-     `announced`; removes `fix released`. The `announced` label
-     triggers the project-board automation to move the item from the
-     `Fix released` column to the `Announced` column.
-   - Regenerates the embedded CVE JSON (now picking up the updated
-     short summary as `descriptions[].value` and the archive URL as a
-     `vendor-advisory` reference).
-   - **If the OAuth push is healthy**: re-pushes the regenerated JSON
-     and **moves the record `REVIEW → PUBLIC`** via the OAuth API.
-   - **If the OAuth push is still blocked**: the wrap-up comment from
-     this step names the `#source`-tab paste-and-Save + UI-click
-     `REVIEW → PUBLIC` as your manual fallback (single UI session).
-   - **Closes the tracker** as `completed`.
-   - **Posts a follow-up comment** tagging the RM with the wrap-up
-     checklist: archive the now-closed tracker from the project
-     board's `Announced` column, and — **if every sibling on the
-     tracker's milestone is also closed** at that moment — close the
-     milestone via the link in the comment ([`MILESTONE_URL`](MILESTONE_URL)).
-     If other siblings on the milestone are still open, the wrap-up
-     comment omits the close-milestone line; the close happens when
-     the *last* sibling tracker reaches the same Step 6.
-
-7. **Follow the wrap-up comment** posted by sync in Step 6. Archive
-   the closed tracker from the project board's `Announced` column
-   (it stays accessible via the *Archived items* filter). If the
-   comment also linked the milestone (last-sibling case), click
-   through and close it — that's the explicit *"everything destined
-   for this release has shipped and been announced"* signal.
+## 🚀 Release-manager hand-off — [`CVE_ID`](SOURCE_TAB_URL)
+
+RM_HANDLE — the release with the fix has shipped, and the CVE record on 
Vulnogram is in **`REVIEW`** state with all mandatory content populated. This 
tracker is now yours to drive from **Steps 13 → 14 → 15** of the security 
process. Three actions, in order; each one is a single click in Vulnogram; **no 
shell commands required from you at any point**.
+
+> **You will never see this comment while the record is in `DRAFT`.** Sync 
gates the hand-off on the record reaching `REVIEW`. If you ever see this 
comment paired with a `DRAFT` state on the linked record, please ping @potiuk 
on this issue before clicking anything in Vulnogram — that combination is a bug 
we want to know about, not a state for you to resolve.
+>
+> *(The security team has already pushed the CVE JSON content into Vulnogram 
and filled every body field on this tracker that the public advisory needs. 
Your job is to: address any CVE-reviewer feedback that lands during `REVIEW`, 
move the record to `READY` when review closes, then send the advisory email. 
That's it.)*
+
+---
+
+### Step 1 of 3 — address reviewer feedback (if any), then promote to READY
+
+Open the record's [`#source` tab](SOURCE_TAB_URL) in your browser. **State** 
field at the top should read `REVIEW` — that is the precondition for this 
comment firing, so it should match. If it doesn't, stop and ping @potiuk; 
otherwise:
+
+1. **Click the [`#email` tab](EMAIL_TAB_URL)** on the same page. Scroll 
through any reviewer comments left by the ASF Security Team's CVE reviewers. 
**You do not need to act on reviewer comments yourself** — they arrive by email 
on `SECURITY_LIST` with the CVE ID in the subject, and sync detects them on the 
next run, opens corresponding body-field updates on this tracker, and re-pushes 
the JSON. If the comments tab is empty, or carries a closure note (*"OK, looks 
good"* / *"approved"*),  [...]
+
+2. **When the reviewer thread is clear** (no open comments, or all comments 
have an *"OK, looks good"*-style closer), use the **State** dropdown on 
`#source` to change `REVIEW` → `READY`. Click **Save**. *The record is now 
staged for advisory send.*
+
+> 💡 *How do you know the reviewer thread is clear?* Two signals: (a) no new 
reviewer email on `SECURITY_LIST` carrying the CVE ID for ~3 days, or (b) an 
explicit "looks good" reply from the reviewer. Most CVEs go through `REVIEW` 
with no reviewer comments at all — in that case, you can usually move `REVIEW → 
READY` immediately after Step 1.1's tab-check confirms there's nothing to 
address.
+
+---
+
+### Step 2 of 3 — preview the advisory email, then send it
+
+With the record in `READY`, click the [`#email` tab](EMAIL_TAB_URL) on the 
same record page. This shows you, in the exact format that goes out, what the 
advisory email will look like when sent to `USERS_LIST` and `ANNOUNCE_LIST`.
+
+**Check that:**
+- The subject line is `CVE_ID: <one-line description>` and the description 
matches what you'd want public.
+- The body's short-summary paragraph reads as instructions to a user (*"Users 
are advised to upgrade to version X"*), not just a technical description of the 
bug.
+- The *Affected versions* range is correct.
+- The reporter credit line is present and spelled correctly.
+
+**If anything looks wrong**: don't edit it in Vulnogram. Comment on this 
tracker (just `@potiuk: the X field needs Y`) and we'll fix the corresponding 
body field here, regenerate the JSON, and re-push within the next sync. 
Re-preview after that.
+
+**If everything looks right**: click the **Send Email** button on the `#email` 
tab. The advisory ships to `USERS_LIST` and `ANNOUNCE_LIST`. **That is the only 
manual send action you make for this CVE.**
+
+> ⚠️ **Do not touch the tracker labels yourself.** Sync flips `fix released` → 
`announced - emails sent` + `announced` automatically when it sees the advisory 
in the public archive (usually within the same day). If you flip them manually 
you race the automation.
 
 ---
 
-**Manual-paste fallback** — only when the OAuth push remained blocked
-through to the advisory-send pass:
+### Step 3 of 3 — sync closes out the rest (no further action from you)
 
-- Open [`#source`](SOURCE_TAB_URL) for the CVE record.
-- Open the [tracker body's embedded JSON](JSON_ANCHOR_URL) section.
-- Copy the JSON block; paste it into Vulnogram's `#source` editor;
-  click **Save**.
-- Repeat after any subsequent body-field change on the tracker (the
-  embedded JSON regenerates automatically on each change; the paste
-  is the only step that does not happen automatically when the OAuth
-  push is blocked).
+Once the advisory archives on `lists.apache.org/list.html?USERS_LIST` 
(typically within minutes of sending), the next sync run does this for you, 
end-to-end:
 
-The security team's next sync run resolves the underlying OAuth
-issue (re-run `vulnogram-api-setup`, retry the push) and the comment
-PATCH-edits back to the OAuth-pushed variant. **You as the RM are
-never asked to run `vulnogram-api-*` shell commands** in this
-fallback path.
+1. Captures the published advisory URL into this tracker's body.
+2. Regenerates the CVE JSON (now including the archive URL as a 
`vendor-advisory` reference) and re-pushes it to Vulnogram.
+3. Moves the Vulnogram record `READY` → `PUBLIC` (this is the CNA-feed 
dispatch — once it lands, `cve.org` starts propagating).
+4. Flips this tracker's labels (`fix released` → `announced - emails sent` + 
`announced`).
+5. Closes this tracker as `completed`.
+6. **Archives this tracker** from the `Announced` column on the [Security 
issues board](BOARD_URL) (`archiveProjectV2Item` GraphQL mutation — sync, not 
you).
+7. *(Conditional)* **Closes the [`MILESTONE_TITLE`](MILESTONE_URL) milestone** 
if this tracker was the last open issue on it.
+
+The CVE will propagate to `cve.org` on its own within a few hours; sync will 
detect the publication on a subsequent run and post a courtesy *"CVE is live on 
cve.org"* note to the reporter on the original email thread.
+
+**You're done.** The lifecycle is complete from your side at Step 2 (Send 
Email). Everything above is sync's job — no further comments will tag you with 
manual cleanups.
 
 ---
 
-**References:**
+### Reference links (only if you want them)
+
+- **The full lifecycle in one place** — [`README.md` Steps 
12–15](FRAMEWORK_README_URL#for-release-managers--steps-1215)
+- **Vulnogram-specific mechanics** (state machine, paste-flow details) — 
[`tools/vulnogram/record.md`](FRAMEWORK_RECORD_MD_URL)
+- **Reusable email wording for ad-hoc replies** — 
[`canned-responses.md`](CANNED_RESPONSES_URL)
+
+---
+
+<details><summary>Manual-paste fallback — only if Step 1 found the JSON 
content on Vulnogram does not match this tracker's embedded JSON</summary>
+
+If Step 1 found the JSON on Vulnogram does **not** match this tracker's 
[embedded JSON](JSON_ANCHOR_URL) (i.e. the security team's automated push was 
blocked when this comment was posted), follow these one-time steps before 
continuing with Step 1:
+
+1. Open [the embedded JSON section](JSON_ANCHOR_URL) of this tracker's body. 
Click `Copy` on the JSON code block (or select and ⌘C / Ctrl-C).
+2. Open [`#source` tab](SOURCE_TAB_URL) on Vulnogram. The editor takes JSON as 
plain text.
+3. Select all (⌘A / Ctrl-A) in the editor and paste (⌘V / Ctrl-V).
+4. Click **Save**.
+5. Now go back to Step 1 — the record is loaded; verify reviewer comments (if 
any) and move `REVIEW` → `READY`.
+
+The security team will resolve the automated-push issue on their side (re-run 
`vulnogram-api-setup`); subsequent sync runs will keep the record in sync 
without you needing to repeat this paste.
 
-- Vulnogram state machine + paste flow: 
[`tools/vulnogram/record.md`](FRAMEWORK_RECORD_MD_URL).
-- Reusable email wording (if you draft anything by hand): 
[`canned-responses.md`](CANNED_RESPONSES_URL).
-- Full lifecycle (Steps 12-15): 
[`README.md`](FRAMEWORK_README_URL#for-release-managers--steps-1215).
+</details>
diff --git 
a/tools/vulnogram/release-manager-publication-comment-oauth-pushed.md 
b/tools/vulnogram/release-manager-publication-comment-oauth-pushed.md
index 5897a94..6c96acc 100644
--- a/tools/vulnogram/release-manager-publication-comment-oauth-pushed.md
+++ b/tools/vulnogram/release-manager-publication-comment-oauth-pushed.md
@@ -2,7 +2,9 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
-- [📰 Advisory archived — ready for `PUBLIC` (CVE JSON 
auto-pushed)](#-advisory-archived--ready-for-public-cve-json-auto-pushed)
+- [📰 Advisory archived — sync taking it from here  *(CVE JSON 
auto-pushed)*](#-advisory-archived--sync-taking-it-from-here--cve-json-auto-pushed)
+  - [What still happens automatically on the next 
sync](#what-still-happens-automatically-on-the-next-sync)
+  - [Where this fits in the lifecycle](#where-this-fits-in-the-lifecycle)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -18,6 +20,14 @@
      Vulnogram via the OAuth API on the same sync pass (Step 14 of
      the lifecycle).
 
+     **Informational only.** Sync drives every state change in the
+     post-2026 flow — the regenerated JSON push happened this run,
+     and on the next sync pass the `READY` → `PUBLIC` advance
+     (`vulnogram-api-record-publish`), the label flips, the tracker
+     close, and the board archive will all fire automatically. The
+     RM's only remaining actions land in the *wrap-up* comment sync
+     posts immediately after the close.
+
      This template is the counterpart to `release-manager-publication-
      comment.md` (the manual-paste variant). The sync skill picks
      between the two based on the outcome of `vulnogram-api-check` +
@@ -25,7 +35,7 @@
      `.claude/skills/security-issue-sync/SKILL.md` for the decision
      flow.
 
-     Idempotency: the marker on line below is the **same** as the
+     Idempotency: the marker on the first line is the **same** as the
      manual-paste variant's; the skill keys idempotency on the marker
      and detects the variant by re-reading the body. On a re-sync
      where the previous comment was manual-paste but the current push
@@ -47,40 +57,25 @@
 -->
 <!-- apache-steward: release-manager-publication-ready v1 -->
 
-## 📰 Advisory archived — ready for `PUBLIC` (CVE JSON auto-pushed)
+## 📰 Advisory archived — sync taking it from here  *(CVE JSON auto-pushed)*
+
+RM_HANDLE — the advisory you sent in Step 2 of the hand-off comment above has 
now been archived on the public users-list. **You do not need to do anything in 
Vulnogram in response to this comment.**
 
-RM_HANDLE, the advisory you sent in step 7 of the hand-off above has
-been archived on the public users-list. This sync pass made the
-following deterministic updates on this tracker — *and pushed the
-regenerated JSON to Vulnogram via the OAuth API at `PUSH_TIMESTAMP`*,
-so the record at [`#source`](SOURCE_TAB_URL) now carries the archive
-URL as a `vendor-advisory` reference:
+This sync pass made the following deterministic updates:
 
 - **Public advisory URL** body field populated: [ARCHIVE_URL](ARCHIVE_URL)
-- The embedded CVE JSON regenerated to include the archive URL.
-- The regenerated JSON pushed to the CVE record via
-  `vulnogram-api-record-update` (no manual paste needed).
+- The embedded CVE JSON regenerated to include the archive URL as a 
`vendor-advisory` reference.
+- The regenerated JSON auto-pushed to [`#source`](SOURCE_TAB_URL) via the 
Vulnogram OAuth API at `PUSH_TIMESTAMP` (no manual paste needed).
 - The `announced` label added.
 
-You can now do the final state move:
-
-1. **Verify the record content** at [`#source`](SOURCE_TAB_URL)
-   matches the [tracker body](JSON_ANCHOR_URL) — they should be
-   byte-identical. If they diverge, the push failed silently; fall
-   back to the manual paste below.
-
-2. **Move `REVIEW` → `PUBLIC`** via the Vulnogram UI. The record
-   propagates to [`cve.org`](CVE_ORG_URL) once the state lands.
+### What still happens automatically on the next sync
 
-3. **Close this tracker** — close as completed; do not update any
-   labels. The `security-issue-sync` skill archives the project-board
-   item afterwards.
+- ✅ The record is now staged for the `READY` → `PUBLIC` move (sync drives this 
via `vulnogram-api-record-publish` on its next pass).
+- ✅ This tracker will be closed as `completed`.
+- ✅ A **wrap-up comment** will then be posted on this tracker with the only 
remaining manual actions for you: archive this tracker from the `Announced` 
column on the [security project board](https://github.com/<tracker>/projects), 
and (conditionally) close the milestone if every sibling on it has shipped.
 
-That terminates the lifecycle. Thanks for driving this one.
+The CVE will propagate to [`cve.org`](CVE_ORG_URL) within a few hours of the 
`READY → PUBLIC` move. Once it does, sync posts a courtesy *"CVE is live on 
cve.org"* note to the reporter on the original email thread (no action from you 
needed).
 
----
+### Where this fits in the lifecycle
 
-**Manual paste fallback** (only if step 1's record content does not
-match): open [`#source`](SOURCE_TAB_URL), paste the JSON from this
-tracker's [issue body](JSON_ANCHOR_URL), click **Save**, then move
-`REVIEW` → `PUBLIC`.
+Step 14 (advisory archive captured) → Step 15 (record `PUBLIC` + tracker 
close) — see [`tools/vulnogram/record.md`](record.md) for the full 
Vulnogram-side checklist. The wrap-up comment that follows in the next sync 
pass is the explicit go-ahead for your final board + milestone cleanups.
diff --git a/tools/vulnogram/release-manager-publication-comment.md 
b/tools/vulnogram/release-manager-publication-comment.md
index aa3749a..ee6ad74 100644
--- a/tools/vulnogram/release-manager-publication-comment.md
+++ b/tools/vulnogram/release-manager-publication-comment.md
@@ -2,7 +2,9 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
-- [📰 Advisory archived — ready for 
`PUBLIC`](#-advisory-archived--ready-for-public)
+- [📰 Advisory archived — sync taking it from 
here](#-advisory-archived--sync-taking-it-from-here)
+  - [What still needs to happen (and who does 
it)](#what-still-needs-to-happen-and-who-does-it)
+  - [Where this fits in the lifecycle](#where-this-fits-in-the-lifecycle)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -10,17 +12,27 @@
      https://www.apache.org/licenses/LICENSE-2.0 -->
 
 <!--
-     Comment-template body for the publication-ready notification
+     Manual-paste variant of the publication-ready notification
      comment posted by `security-issue-sync` when the *Public advisory
      URL* body field has just been populated and the CVE JSON has been
      regenerated to carry the archive URL as a `vendor-advisory`
      reference (Step 14 of the lifecycle).
 
-     This comment is the explicit follow-up referenced in step 6 of
-     the release-manager hand-off comment (see
-     `release-manager-handoff-comment.md`); together they form a
-     two-comment narrative the RM can drive without consulting the
-     status rollup.
+     **Informational only.** Sync drives every state change in the
+     post-2026 flow — the regenerated JSON push (Step 5b), the
+     `REVIEW` → `PUBLIC` advance (`vulnogram-api-record-publish`),
+     the label flips, the tracker close, the board archive. The
+     RM's only remaining actions land in the *wrap-up* comment
+     sync posts immediately after the close (archive the tracker
+     from the `Announced` column, conditionally close the
+     milestone).
+
+     The manual-paste variant fires when sync could not auto-push
+     the JSON this run (no OAuth credentials, expired session,
+     transient HTTP error). In that case the post-archive JSON
+     push, the `READY` → `PUBLIC` move, and the tracker close are
+     all deferred until the security team's next sync resolves the
+     push issue — the body of this comment explains the deferral.
 
      Placeholders the skill substitutes:
 
@@ -36,29 +48,36 @@
        CVE_ORG_URL           https://www.cve.org/CVERecord?id=CVE_ID
                              (the public mirror, post-PUBLIC)
 
-     The HTML marker on line 1 is load-bearing: the skill detects an
-     already-posted publication-ready comment by grepping for this
-     exact string and skips the post on subsequent sync runs
+     The HTML marker on the first line is load-bearing: the skill
+     detects an already-posted publication-ready comment by grepping
+     for this exact string and skips the post on subsequent sync runs
      (idempotency).
 -->
 <!-- apache-steward: release-manager-publication-ready v1 -->
 
-## 📰 Advisory archived — ready for `PUBLIC`
+## 📰 Advisory archived — sync taking it from here
+
+RM_HANDLE — the advisory you sent in Step 2 of the hand-off comment above has 
now been archived on the public users-list. **You do not need to do anything in 
Vulnogram in response to this comment.**
 
-RM_HANDLE, the advisory you sent in step 5 of the hand-off above has
-been archived on the public users-list. This sync pass made the
-following deterministic updates on this tracker:
+This sync pass made the following deterministic updates on this tracker:
 
 - **Public advisory URL** body field populated: [ARCHIVE_URL](ARCHIVE_URL)
-- The embedded CVE JSON regenerated to include the archive URL as a
-  `vendor-advisory` reference in `references[]`.
+- The embedded CVE JSON regenerated to include the archive URL as a 
`vendor-advisory` reference in `references[]`.
 - The `announced` label added.
 
-You can now do the final paste + state move:
+### What still needs to happen (and who does it)
+
+The Vulnogram OAuth push was blocked on this sync (no credentials, expired 
session, or transient error). Until the security team's next sync resolves the 
push, the final transitions are **deferred**:
+
+- 🟡 Re-push the regenerated JSON to [`#source`](SOURCE_TAB_URL) (sync, next 
pass).
+- 🟡 Move the record `READY` → `PUBLIC` via `vulnogram-api-record-publish` 
(sync, next pass).
+- 🟡 Close this tracker as `completed` (sync, next pass).
+- 🟡 Post the wrap-up comment with your final board-archive + milestone-close 
cleanup (sync, next pass).
+
+**The release manager should not paste the JSON manually or move the state 
manually** unless an explicit "sync is stuck — please paste manually" follow-up 
arrives. Doing those manually races the automation and creates noisy timeline 
state.
+
+If sync hasn't picked this up within ~24h, ping @potiuk on this tracker and 
we'll resolve the push issue on our side.
 
-1. Open the [`#source` tab](SOURCE_TAB_URL) on the CVE record.
-2. Copy the regenerated JSON from this tracker's [issue body](JSON_ANCHOR_URL) 
and paste into the form. **Save**.
-3. Move the record `REVIEW` → `PUBLIC` via the Vulnogram UI. The record 
propagates to [`cve.org`](CVE_ORG_URL) once the state lands.
-4. **Close this tracker** — close as completed; do not update any labels. The 
`security-issue-sync` skill archives the project-board item afterwards.
+### Where this fits in the lifecycle
 
-That terminates the lifecycle. Thanks for driving this one.
+Step 14 (advisory archive captured) → Step 15 (record `PUBLIC` + tracker 
close) — see [`tools/vulnogram/record.md`](record.md) for the full 
Vulnogram-side checklist. The wrap-up comment that closes the loop is the 
explicit go-ahead for your board-archive + milestone-close actions.
diff --git a/tools/vulnogram/release-manager-wrap-up-comment.md 
b/tools/vulnogram/release-manager-wrap-up-comment.md
index a2c29d2..e67142e 100644
--- a/tools/vulnogram/release-manager-wrap-up-comment.md
+++ b/tools/vulnogram/release-manager-wrap-up-comment.md
@@ -2,7 +2,7 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
-- [✅ Wrap-up — `CVE_ID`](#-wrap-up--cve_id)
+- [✅ Lifecycle complete — `CVE_ID`](#-lifecycle-complete--cve_id)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -17,88 +17,68 @@
      `Advisory archived on <users-list>` row of Step 2b in
      `.claude/skills/security-issue-sync/SKILL.md`).
 
-     The combined apply that triggers this comment runs when the
-     advisory's archive URL is captured on `<users-list>` AND every
-     intermediate write (label flip, JSON re-push, REVIEW → PUBLIC
-     via `vulnogram-api-record-publish`) succeeded. By the time this
-     comment posts the tracker is already closed (`completed`) and
-     the `announced` label has moved the board item to the
-     `Announced` column.
-
-     Residual manual steps for the RM:
-
-       1. Archive the closed tracker from the project board's
-          `Announced` column.
-       2. (Conditional, last-sibling case only) Close the milestone
-          this tracker belonged to. The comment carries the
-          milestone URL as a clickable link ONLY when sync detected
-          that every milestone-sibling is also closed at this
-          moment. In the more common "other siblings still open"
-          case the comment omits the close-milestone line and the
-          milestone close happens when the *last* sibling tracker
-          reaches this same step.
-
-     Idempotency: the HTML marker on the line below is the skill's
+     **INFORMATIONAL ONLY.** The combined apply that triggers
+     this comment runs when the advisory's archive URL is captured
+     on `<users-list>` AND every intermediate write (label flip,
+     JSON re-push, REVIEW → PUBLIC via `vulnogram-api-record-publish`,
+     tracker close, project-board archive, conditional milestone
+     close) succeeded — all sync-driven. By the time this comment
+     posts:
+
+       * the tracker is already closed (`completed`);
+       * the `announced` label has moved the board item to the
+         `Announced` column AND sync has archived it from the
+         board (`archiveProjectV2Item` mutation);
+       * the milestone is closed (last-sibling case only — sync
+         skips the milestone close otherwise).
+
+     **No residual actions for the RM.** This template intentionally
+     carries no asks. The post-2026-05-24 design (see RM feedback
+     on airflow-s#415) is: when the auto-archive + auto-close-
+     milestone are sync-driven, asking the RM to do them anyway
+     creates a confusion class — "agent says it's done, but also
+     asks me to do it manually?". The wrap-up comment stays as a
+     timeline marker for audit-trail purposes but does not solicit
+     manual actions.
+
+     Idempotency: the HTML marker on the first line is the skill's
      idempotency anchor. On a re-sync where this comment already
-     exists, sync skips the post (the tracker is already closed,
-     this comment is informational only — re-posting would be
-     noise).
+     exists, sync skips the post.
 
      Placeholders the skill substitutes:
 
        CVE_ID                    e.g. CVE-2026-40690
        RM_HANDLE                 GitHub handle of the release manager
                                  (with leading `@`)
-       TRACKER_URL               Tracker issue URL
-       BOARD_URL                 Project-board URL with the
-                                 `Announced` column scrolled into
-                                 view (e.g.
-                                 
https://github.com/orgs/<org>/projects/<N>/views/<V>?filterQuery=status%3AAnnounced)
-       MILESTONE_URL             Optional. Set ONLY in the
-                                 last-sibling case. Sync omits the
-                                 close-milestone bullet entirely
-                                 when this placeholder is unset.
-       MILESTONE_TITLE           Optional. Set alongside
-                                 MILESTONE_URL — the human-readable
-                                 milestone title for the link text.
        PUBLISH_TIMESTAMP         ISO-8601 timestamp of the
                                  `vulnogram-api-record-publish` call
                                  that flipped REVIEW → PUBLIC.
        ADVISORY_URL              The captured `<users-list>` archive
                                  URL for the advisory.
+       MILESTONE_BULLET          Optional. Set when sync detected
+                                 every milestone-sibling was also
+                                 closed at this moment, and reads:
+                                 `Milestone [`MILESTONE_TITLE`](MILESTONE_URL)
+                                 closed automatically (every tracker on
+                                 it is now done).`
+                                 Unset otherwise; sync omits the line
+                                 entirely.
 -->
 <!-- apache-steward: release-manager-wrap-up v1 -->
 
-## ✅ Wrap-up — `CVE_ID`
+## ✅ Lifecycle complete — `CVE_ID`
 
-RM_HANDLE — the post-advisory close-out for [`CVE_ID`](ADVISORY_URL)
-ran cleanly. This tracker is now closed; the Vulnogram record moved
-`REVIEW → PUBLIC` at `PUBLISH_TIMESTAMP` (CNA-feed dispatch to
-`cve.org` triggered); the `announced` label has moved the board
-item to the [`Announced` column](BOARD_URL).
+RM_HANDLE — the post-advisory close-out for [`CVE_ID`](ADVISORY_URL) ran 
cleanly:
 
-**Two small residual actions for you:**
+- Vulnogram record moved `REVIEW → PUBLIC` at `PUBLISH_TIMESTAMP` (CNA-feed 
dispatch to `cve.org` triggered).
+- Tracker labels flipped `fix released → announced - emails sent + announced`.
+- Tracker closed as `completed`.
+- Board item archived from the `Announced` column.
+- MILESTONE_BULLET
 
-1. **Archive this tracker from the [`Announced` column](BOARD_URL)** on the 
project board. The closed tracker stays accessible via the *Archived items* 
filter; this just clears it from the active board view.
+CVE will propagate to [`cve.org`](https://www.cve.org/CVERecord?id=CVE_ID) 
within a few hours. On the next sync run after that, a courtesy *"CVE is live 
on cve.org"* note will go to the reporter on the original email thread.
 
-2. **MILESTONE_BULLET**
-
-<!--
-     The skill substitutes the MILESTONE_BULLET placeholder with
-     either an empty string (when MILESTONE_URL is unset — other
-     milestone-siblings still open) or with the literal:
-
-         Close the [`MILESTONE_TITLE`](MILESTONE_URL) milestone —
-         every tracker on it is now closed too.
-
-     This is the only conditional in the template. The numbering
-     stays "1." and "2." regardless; in the no-milestone case the
-     "2." item just reads as the empty bullet which is harmless
-     visual noise — preferable to dropping the numbering and
-     keeping the next-pass parse stable.
--->
-
-That's it — nothing else owed on this tracker. Thanks for shepherding `CVE_ID` 
through the release + advisory.
+**Nothing else is owed on your side.** Thanks for shepherding `CVE_ID` through 
the release + advisory.
 
 ---
 
diff --git a/tools/vulnogram/remediation-developer-fill-fields-comment.md 
b/tools/vulnogram/remediation-developer-fill-fields-comment.md
new file mode 100644
index 0000000..322103b
--- /dev/null
+++ b/tools/vulnogram/remediation-developer-fill-fields-comment.md
@@ -0,0 +1,128 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [📝 Fill in remaining CVE fields — 
REMEDIATION_DEVELOPER_HANDLE](#-fill-in-remaining-cve-fields--remediation_developer_handle)
+  - [Fields that still need values](#fields-that-still-need-values)
+  - [How to fill them](#how-to-fill-them)
+  - [What happens next (automatic)](#what-happens-next-automatic)
+  - [If any of the fields is unclear](#if-any-of-the-fields-is-unclear)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+<!--
+     Remediation-developer "fill remaining CVE fields" comment posted
+     by `security-issue-sync` when the CVE record is still in `DRAFT`
+     state because mandatory body fields on the tracking issue have
+     not yet been populated.
+
+     **Trigger.** Two firing points in the lifecycle:
+
+       (a) at the `pr created` → `pr merged` transition (Step 11),
+           when sync detects the fix PR has merged but the mandatory
+           CVE body fields (CWE, Severity, Short public summary,
+           Affected versions, Reporter credited as, PR with the fix)
+           are not all populated. The comment names the missing
+           fields and tags the remediation developer.
+
+       (b) at the `pr merged` → `fix released` transition (Step 12),
+           when sync has just attempted to push the CVE JSON to
+           Vulnogram (Step 5b) and the record state is **still
+           `DRAFT`** after the push — meaning either the JSON push
+           was blocked, or the body fields are still incomplete
+           (the push refuses to advance the state if the JSON does
+           not validate cleanly against the CNA schema). The comment
+           re-fires; the tracker stays assigned to the remediation
+           developer; **no hand-off to the release manager** happens
+           on this pass.
+
+     **The release-manager hand-off comment is gated on the CVE
+     record reaching the `REVIEW` state**, which can only happen
+     after the body fields are filled in and the JSON push advances
+     the state. This template is what keeps the work with the
+     remediation developer until that gate clears.
+
+     Placeholders the skill substitutes:
+
+       CVE_ID                       e.g. CVE-2026-40690
+       REMEDIATION_DEVELOPER_HANDLE GitHub handle of the remediation
+                                    developer, with leading `@`
+                                    (read from the *Remediation
+                                    developer* body field; falls
+                                    back to the fix-PR author
+                                    @-handle when the field is empty)
+       MISSING_FIELDS_LIST          Markdown bullet list of the
+                                    mandatory body fields that are
+                                    still empty / `_No response_`,
+                                    one per line (e.g.
+                                    "- **CWE** — currently
+                                    `_No response_`; pick from
+                                    https://cwe.mitre.org/data/";)
+       TRACKER_URL                  Full URL of the tracking issue
+                                    (the issue this comment is being
+                                    posted on)
+       SOURCE_TAB_URL               <cve_tool_record_url_template>#source
+                                    (read-only check link for the
+                                    remediation developer)
+       SECURITY_LIST                e.g. security@<project>.apache.org
+       SECURITY_LIST_DOMAIN         e.g. <project>.apache.org
+       FRAMEWORK_README_URL         Link to README.md on the
+                                    framework's GitHub
+       FRAMEWORK_SYNC_SKILL_URL     Link to .claude/skills/
+                                    security-issue-sync/SKILL.md on
+                                    the framework's GitHub
+
+     The HTML marker on the first line is load-bearing: the skill
+     detects an already-posted fill-fields comment by grepping for
+     this exact string. When the marker is found and a sync run
+     re-fires the trigger (e.g. fields still empty on the next
+     pass), the existing comment's body is PATCH-edited in place with
+     the refreshed missing-fields list, not duplicated with a fresh
+     POST — same PATCH-don't-post rule as the rollup and hand-off
+     comments. The first-line marker convention is documented in
+     the skill's Step 4 apply mechanic.
+
+     Do not paraphrase the marker. Do not move it off line 1. Do not
+     add a `<!-- v2 -->` until the schema actually changes — the
+     skill's grep is anchored on `v1`.
+-->
+<!-- apache-steward: remediation-developer-fill-fields v1 -->
+
+## 📝 Fill in remaining CVE fields — REMEDIATION_DEVELOPER_HANDLE
+
+The fix PR for [`CVE_ID`](SOURCE_TAB_URL) has merged. Before this tracker can 
be handed off to the release manager for advisory composition, the CVE record 
needs every mandatory field populated.
+
+> **Why this is in your court, not the RM's**: the CVE record at 
[`SOURCE_TAB_URL`](SOURCE_TAB_URL) is currently in `DRAFT` state. The hand-off 
to the release manager only fires once the record reaches `REVIEW`, and the 
record can only advance to `REVIEW` after the body fields below are filled in. 
As the person who wrote the fix, you also have the deepest context on these 
fields (CWE class, affected version range, short summary wording). **The RM 
will never receive a hand-off while the r [...]
+
+### Fields that still need values
+
+MISSING_FIELDS_LIST
+
+### How to fill them
+
+1. Open this tracker's body (the GitHub `…` menu → *Edit*).
+2. Update the listed sections — they are near the bottom under headings like 
`### CWE`, `### Severity`, `### Short public summary for publish`. Replace `_No 
response_` with the value.
+3. Click **Update issue** at the bottom of the edit form.
+
+### What happens next (automatic)
+
+The next sync run will:
+
+1. Detect that the previously-empty fields are now populated.
+2. Regenerate the embedded CVE JSON in this tracker's body.
+3. Push the updated JSON to [`SOURCE_TAB_URL`](SOURCE_TAB_URL) over the 
Vulnogram OAuth API. The push includes the state advance `DRAFT` → `REVIEW`.
+4. Verify the record state is now `REVIEW`.
+5. Hand the tracker off to the release manager via the regular 
[release-manager hand-off comment](FRAMEWORK_SYNC_SKILL_URL) — that comment 
names the RM, sets them as assignee, and walks them through Steps 13–15 
(advisory composition, send, publish).
+
+If the next sync still finds the record in `DRAFT` (e.g. one of the new field 
values failed CNA-schema validation, or the API push was blocked), **this 
comment re-fires** with the updated missing-fields list. The cycle ends when 
sync sees `REVIEW`.
+
+### If any of the fields is unclear
+
+Comment on this tracker with your question, or ping 
[`SECURITY_LIST`](mailto:SECURITY_LIST). For a field-by-field rubric (what kind 
of value goes in CWE / Severity / Short summary) see the security team's 
handling process at 
[`README.md`](FRAMEWORK_README_URL#for-remediation-developers).
+
+---
+
+**Reference**: [tracker body](TRACKER_URL) · [CVE record 
(read-only)](SOURCE_TAB_URL) · [handling process](FRAMEWORK_README_URL) · [sync 
skill spec](FRAMEWORK_SYNC_SKILL_URL)

Reply via email to