Measured tonight on PR #10698 (the #10490 aggregator-roster card). Unlike the near-miss in #10686, this one actually dropped the label — there is an unlabeled event, and the seat's write was the correct additive POST, not a whole-set write.
The specimen
Every label event on PR #10698, from the timeline API:
| time | event | label | actor |
|---|
| 09:05:29Z | labeled | skip-changeset | claude[bot] (seat, additive POST, HTTP 200) |
| 09:05:30Z | unlabeled | skip-changeset | github-actions[bot] |
| 09:05:30Z | labeled | size/l | github-actions[bot] |
| 09:05:42Z | labeled | ci/cd | github-actions[bot] |
| 09:06:03Z | labeled | skip-changeset | claude[bot] (re-applied after read-back) |
The seat did everything the brief asks. The additive POST /issues/10698/labels returned HTTP 200 and the read-back in the same call showed ['skip-changeset']. One second later the size labeler's whole-set PUT erased it, having snapshotted the label set before the seat's POST landed.
Caught only because the brief also says to read the labels back after the bots settle — the read-back at ~40s showed ['ci/cd', 'size/l'], with skip-changeset gone.
Why there is no open card for this
.github/workflows/pr-automation.yml documents the mechanism exactly, at its head, and states plainly that this half was left open:
NOT closed by either change, and deliberately recorded rather than implied: a writer OUTSIDE this workflow — an agent or a human labelling the PR seconds after gh pr create, i.e. exactly while these jobs run — can still land inside a PUT window and be erased. […] Closing that half needs the writes themselves to become additive, not merely better ordered; it is the open half of #5649 and no configuration here can stand in for it.
But #5649 is closed, and #5533 is closed. So the only tracker for a live, recurring defect is a prose paragraph inside a workflow file — findable only by someone who already knows to look there. That is the gap this card exists to fill, and it is why the recurrence went unnoticed until a read-back happened to catch it.
#10686 is a different direction and does not cover this: it is about the permission classifier pushing agents toward the unsafe write, and its own timeline records "No unlabeled event — nothing was dropped."
Why it matters beyond one label
skip-changeset is the exemption for a PR that publishes nothing. When the labeler erases it, changeset-check enforces a changeset against a PR that legitimately has none. The workflow does mitigate the read side — it re-reads labels live and has a "Settle the skip-changeset window" step — so the erasure is usually survivable if the seat notices and re-applies. A seat that applies the label, sees HTTP 200, and moves on (which is what the brief's happy path looks like) leaves a PR whose exemption silently does not exist.
The window is wide: bots wrote at +1s, +1s and +13s after the PR opened here.
Directions, not a prescription
- Make the writes additive.
codelytv/pr-size-labeler (src/github.sh:68-91) and actions/labeler@v7 (src/api/set-labels.ts) both PUT the whole set and expose no additive input; sync-labels is not that input. Replacing them with a small additive step is the only fix that closes the window rather than narrowing it — this is the remedy the workflow header already names. - Make the loss loud. A step that re-reads the labels after both writers finish and re-applies a seat-applied exemption it finds missing would turn a silent drop into a self-healing one.
- At minimum, re-open the tracking. The defect currently lives only as prose in a closed card's workflow comment.
Filed unassigned, no labels — PM triage. Found while working #10490; not fixed there, out of scope for a gate-tooling PR.
Generated by Claude Code
Generated by Claude Code
Measured tonight on PR #10698 (the #10490 aggregator-roster card). Unlike the near-miss in #10686, this one actually dropped the label — there is an
unlabeledevent, and the seat's write was the correct additivePOST, not a whole-set write.The specimen
Every label event on PR #10698, from the timeline API:
skip-changesetclaude[bot](seat, additive POST, HTTP 200)skip-changesetgithub-actions[bot]size/lgithub-actions[bot]ci/cdgithub-actions[bot]skip-changesetclaude[bot](re-applied after read-back)The seat did everything the brief asks. The additive
POST /issues/10698/labelsreturned HTTP 200 and the read-back in the same call showed['skip-changeset']. One second later the size labeler's whole-setPUTerased it, having snapshotted the label set before the seat's POST landed.Caught only because the brief also says to read the labels back after the bots settle — the read-back at ~40s showed
['ci/cd', 'size/l'], withskip-changesetgone.Why there is no open card for this
.github/workflows/pr-automation.ymldocuments the mechanism exactly, at its head, and states plainly that this half was left open:But #5649 is closed, and #5533 is closed. So the only tracker for a live, recurring defect is a prose paragraph inside a workflow file — findable only by someone who already knows to look there. That is the gap this card exists to fill, and it is why the recurrence went unnoticed until a read-back happened to catch it.
#10686 is a different direction and does not cover this: it is about the permission classifier pushing agents toward the unsafe write, and its own timeline records "No
unlabeledevent — nothing was dropped."Why it matters beyond one label
skip-changesetis the exemption for a PR that publishes nothing. When the labeler erases it,changeset-checkenforces a changeset against a PR that legitimately has none. The workflow does mitigate the read side — it re-reads labels live and has a "Settle the skip-changeset window" step — so the erasure is usually survivable if the seat notices and re-applies. A seat that applies the label, sees HTTP 200, and moves on (which is what the brief's happy path looks like) leaves a PR whose exemption silently does not exist.The window is wide: bots wrote at +1s, +1s and +13s after the PR opened here.
Directions, not a prescription
codelytv/pr-size-labeler(src/github.sh:68-91) andactions/labeler@v7(src/api/set-labels.ts) bothPUTthe whole set and expose no additive input;sync-labelsis not that input. Replacing them with a small additive step is the only fix that closes the window rather than narrowing it — this is the remedy the workflow header already names.Filed unassigned, no labels — PM triage. Found while working #10490; not fixed there, out of scope for a gate-tooling PR.
Generated by Claude Code
Generated by Claude Code