Table of Contents generated with DocToc
Every agent-authored status update on a <tracker> issue (the import receipt, each sync pass, CVE allocation, dedupe merges, fix-PR announcements, etc.) lands in one single rollup comment per tracker. Each pass appends a new entry to that comment instead of posting a fresh one. The result: scrolling a tracker's timeline shows one rollup comment plus the human discussion, not twenty bot comments drowning out the actual conversation.
This file is the canonical shape + upsert recipe. Skills (sync-*, import-*, allocate-*, deduplicate-*, fix-*) reference this file instead of re-specifying the shape.
One comment per tracker, identified by an opening HTML marker on the first line:
<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. --> <details><summary>YYYY-MM-DD · @user · <Action></summary> <entry body> </details> --- <details><summary>YYYY-MM-DD · @user · <Action></summary> <entry body> </details>
Rules (all load-bearing — breaking any of them breaks GitHub's Markdown rendering):
First line is the marker. <!-- airflow-s status rollup v1 — … --> identifies the comment as the rollup. Detection is anchored on airflow-s status rollup v so future v2 bumps remain findable.
Every entry is its own <details> block. Including the very first one (the import receipt). There is no always-visible preamble above the first <details>. The reason: every entry — current and historical — is collapsed by default per the user's rule; nothing is promoted above the fold.
Open tag is one line. Write <details><summary>…</summary> on a single line. Do not split <details> onto its own line and <summary> onto the next — that variant renders reliably on github.com too, but the one-line form is what the skills emit and detection passes match on.
Summary contains three fields, ·-separated, in this order: YYYY-MM-DD · @handle · <Action>. No trailing text, no parenthetical headlines, nothing else. The details-open arrow on github.com already consumes horizontal space; keep the summary scannable at a glance. Optional fourth field in parentheses is allowed only for disambiguation — see Summary — action labels below.
Exactly one blank line after <summary>…</summary>. Markdown inside a <details> needs a blank line after the open to render. Two blank lines is fine; zero blank lines silently suppresses all markdown inside.
Exactly one blank line before </details>. Same reason, other end.
No leading whitespace on any line inside the entry. A leading space or tab turns the line into a preformatted-code block and wrecks the rendering for every subsequent line. When a sync proposal pastes multi-line content into an entry, left-trim every line before writing.
Entries are separated by a bare --- on its own line, with one blank line on each side:
</details> --- <details><summary>…</summary>
The ruler is GitHub's <hr>. Skip the blank lines around it and the preceding </details> will stay attached.
Chronological order — newest at the bottom. New entries append; the comment grows downward. A reader opens the rollup and scrolls to the latest entry at the end.
Each skill emits one of the following <Action> strings so the summary line tells the reader at a glance what the entry represents:
| Emitting skill | <Action> value | Optional parenthetical |
|---|---|---|
import-security-issue | Import | class + reporter, e.g. Import (Report, Jane Doe) |
sync-security-issue (ordinary pass) | Sync | one-phrase headline, e.g. Sync (pr merged → fix released) |
sync-security-issue (escalation, Step 4) | Sync | Sync (Step 4 escalation) |
sync-security-issue (reformat-only, migrating legacy comments) | Reformat | Reformat (N legacy comments folded) |
allocate-cve | CVE allocated | the allocated ID, e.g. CVE allocated (CVE-2026-40913) |
deduplicate-security-issue, on the kept tracker | Merge (kept) | dropped side's number, e.g. Merge (kept) (from #305) |
deduplicate-security-issue, on the dropped tracker | Merge (dropped) | kept side's number, e.g. Merge (dropped) (into #244) |
fix-security-issue | Fix PR | upstream PR number, e.g. Fix PR (<upstream>#65346) |
The parenthetical is optional; include it when it adds information a scroller actually wants (the CVE ID, the dedupe counterpart, the PR number). Do not restate the same fact inside the entry body and in the parenthetical; the body carries the detail, the summary carries the headline.
Inside the <details> block, write what the skill used to write in its pre-collapse body — the bold headline, the **Next:** line, the reporter-notification line, the full rationale. The entire body is already inside <details>, so the “keep visible part under six lines” rule from the legacy status-comment shape no longer applies. Write what the auditor needs to reconstruct the decision; the scroller sees the summary only and only expands entries they care about.
That said — brevity still wins. Do not pad. Do not restate the previous entry. Each entry is incremental: what changed in this pass, what comes next, what the reporter now knows. Earlier state lives in earlier entries.
Required elements inside every entry body:
**Sync 2026-04-21 — pr merged → fix released.**. This is what a reader sees first when they expand the entry.**Next:** line — one sentence on what comes next. Omit only when the entry is terminal (e.g. dedupe on the dropped side, the “all triage continues on #” line replaces **Next:**).Outside that required frame, the content is free-form markdown. Clickable <tracker> references (the Linking tracker issues and PRs rule in AGENTS.md) apply everywhere, same as before.
Every skill that emits a status update runs this recipe. The steps assume the skill has already composed <new-entry> — the full <details>…</details> block for this pass, with no leading/trailing blank lines.
gh issue view <N> --repo <tracker> \ --json comments \ --jq '.comments[] | select(.body | startswith("<!-- airflow-s status rollup v")) | {id: .id, body: .body, url: .url}'
The matching comment is the rollup. If the query returns nothing, there is no rollup yet (expected on a fresh tracker where import-security-issue has not run, or on a legacy tracker that pre-dates this convention).
Use the first match chronologically if the query somehow returns more than one — two rollups is a bug; surface it to the user and let them pick which one to keep.
Construct the new body by concatenating the old body + a ruler + the new entry, with exactly one blank line on each side of the ruler:
<old body> --- <new entry>
Write the new body to a temp file and PATCH the comment:
python3 - <<'PY' > /tmp/rollup-body.md import pathlib, subprocess, json, textwrap old = subprocess.check_output( ["gh", "api", "repos/<tracker>/issues/comments/<comment-id>", "--jq", ".body"], text=True, ).rstrip("\n") new_entry = pathlib.Path("/tmp/new-entry.md").read_text().rstrip("\n") print(old + "\n\n---\n\n" + new_entry) PY jq -Rs '{body: .}' /tmp/rollup-body.md > /tmp/rollup-patch.json gh api -X PATCH repos/<tracker>/issues/comments/<comment-id> --input /tmp/rollup-patch.json
The -X PATCH repos/<tracker>/issues/comments/<id> form is the only reliable way; gh issue comment --edit-last does not target an arbitrary comment, and the --input flag is needed because --field body=@file URL-encodes the newlines in the body.
Only if Step 1 returned no existing rollup. Prepend the marker line and emit the new entry as the rollup's first entry:
<!-- airflow-s status rollup v1 — all bot-authored status updates fold into this single comment. --> <new entry>
Post as a regular comment via gh issue comment --body-file:
gh issue comment <N> --repo <tracker> --body-file /tmp/rollup-body.md
Capture the returned comment URL + ID so subsequent passes in the same run can append without re-searching.
Trackers created before this convention carry one or more bot-authored status comments as separate top-level comments. Every sync pass runs a fold-legacy sub-step: detect each legacy bot comment, move its content into the rollup as its own entry, and delete (or minimise) the original.
A comment is a candidate for folding when all of the following hold:
<!-- airflow-s status rollup v..comments[].author.login against the collaborator list (see operations.md) or the project's roster declared in <project-config>/release-trains.md. Human discussion from an external reporter is never folded; their content stays as a top-level comment.**Sync **Status update**Merged **Closing as duplicate**Split for scope clarity**Imported on **Process-step escalation**Allocated CVE / **CVE allocated / **Sync … — CVE**): Sync status (, Sync YYYY-MM-DD, Status updatesync-security-issue skill, re-triage, Reporter notification still pending, Outstanding — Step , a verbatim generate-cve-json embed block.A comment that matches 1 + 2 + 3 is foldable. A comment that matches only 1 + 2 (team-member comment with no bot-shape prefix) is regular human discussion — leave it alone.
For each foldable legacy comment, in chronological order:
Reconstruct the entry shape. Take the legacy body and wrap it in the rollup's <details> envelope:
<details><summary><createdAt date> · @<author.login> · <Action></summary> <legacy body — verbatim, left-trimmed> </details>
<createdAt date> is the first 10 chars of the legacy comment's createdAt (YYYY-MM-DD).<Action> is derived from the legacy body's prefix via the table in Summary — action labels above; when the prefix does not map cleanly, use Sync and tag the fold as Reformat (N legacy comments folded) on the overall rollup entry the sync is about to write.airflow-s#244's 2026-04-20 comment, which had on most lines); leaving that indentation inside a <details> turns the whole entry into a preformatted-code block.Append the reconstructed entry to the rollup, using the upsert recipe above (Step 2a). Preserve the original order by appending oldest-first.
Delete the legacy comment once the rollup PATCH succeeds:
gh api -X DELETE repos/<tracker>/issues/comments/<legacy-comment-id>
Only delete after the append lands — if the PATCH fails, the content is still on the tracker via the legacy comment and the fold can be retried on the next pass. Never delete first and hope the append works.
Like every other skill action, surface each proposed fold as a numbered item in the skill‘s Step 2 proposal. Show the legacy comment’s URL, its first ~3 lines, and the derived <Action> for the summary. The user may accept all, accept some, or reject (for example when a legacy comment has inline discussion from a reporter mixed into a status update — in that case leave it alone, it is not a pure bot comment).
The fold path still works: create the rollup (Step 2b) with the oldest foldable legacy comment as its first entry, then append the rest (Step 2a), then append the current pass's new entry last. The recap reports “created new rollup comment on #, folded N legacy comments into it” and lists the deleted comment IDs so the user can audit the change.
<details> block (with left-trim applied to fix indentation-induced rendering bugs, but nothing else). Paraphrasing a historical status update rewrites the audit trail.<details> block. A single stray indentation breaks rendering for every subsequent line of the entry. Compose entries with all lines flush-left.<!-- airflow-s status rollup v marker, stop and ask the user which to keep — the cheapest recovery is a manual merge, not a silent overwrite.AGENTS.md for the full rule, the why, and the grep-list self-check to run before posting. Summarise load-bearing cross-project context in de-identified form (“the reporter has filed similar reports with other ASF projects”) rather than naming the project..claude/skills/import-security-issue/SKILL.md — creates the rollup with the first entry..claude/skills/sync-security-issue/SKILL.md — appends per-sync entries and runs the fold-legacy sub-step..claude/skills/allocate-cve/SKILL.md — appends the CVE-allocation entry..claude/skills/deduplicate-security-issue/SKILL.md — appends the merge entry on both trackers..claude/skills/fix-security-issue/SKILL.md — appends the fix-PR entry.