fix(labels): sweep on labeled so the handoff is immediate
#142
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:cli
scope:drill
scope:host
scope:installer
scope:templates
scope:tiers
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/box#142
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/labels-sweep-on-labeled"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #141.
Lands in all three repos together —
labels.ymland the label taxonomy areshared, and the diff to the three shared files is byte-identical across
heavy-duty/rig and heavy-duty/cast.
The problem
A review landing is not a trigger for the labels workflow. The exact moment
state:needs-humanbecomes true — the third bot approving — fires nothing, sothe label waits for the
*/15cron or for somebody to touch an unrelated PR.And that cron does not run at its declared rate. Measured across the three
repos over a two-hour window on 2026-07-20: one scheduled run each, against
the eight
*/15implies. heavy-duty/rig#94 took its third approval at18:55:46andsat on
state:bots-reviewingfor hours, while box and cast happened to catcha tick and flipped correctly — same byte-identical workflow file, different
luck.
Worst on the quietest repo: every sweep reconciles the whole board, so a busy
repo stays fresh by piggybacking on unrelated PR events. rig has the fewest
open PRs, so it depends on the cron most and receives it least.
Why not
pull_request_reviewBecause it cannot label. On fork PRs — which is every PR here — that event
runs
GITHUB_TOKENread-only and ignores thepermissions:block. Thereis no
pull_request_review_target. The workflow's own header comment alreadysaid so, and was right.
The change
1.
pull_request_targetalso fires onlabeled/unlabeled.2. The author sets
state:needs-humanat handoff — the third act, afterthe tagged round summary and the review request.
The two halves fix each other: the agent's own label write fires the sweep
that validates it. Seconds later the reconciler confirms the label or
corrects it. It is an optimistic write, not a transfer of ownership — the
machine stays the authority, and the cron becomes a genuine last resort for
the round an agent forgets to hand off, rather than the primary path.
It cannot loop. The reconciler's own writes use
GITHUB_TOKEN, and GitHubdoes not create workflow runs from
GITHUB_TOKEN-triggered events. Agentwrites use a PAT and do trigger — exactly the asymmetry wanted.
No permission change needed:
dan-claude-botcan apply an existing label;only label creation 403s.
Files
.github/workflows/labels.yml— the trigger list, the header comment thatexplained the old cron-only reasoning, and one addition beyond the issue:
the
scopejob is now skipped onlabeled/unlabeled. Those events changeno paths, so actions/labeler has nothing to derive, and label churn is
precisely what they are — without the guard every hand-applied label would
spawn a pointless labeler run.
CONTRIBUTING.md— step 6 as three acts; thestate:*row qualified.LABELS.md— the same qualification in Maintenance, with the why.CHANGELOG.md— entry under## Unreleased.labels-reconcile.shis unchanged. It already recomputes every open PRfrom scratch on every run, which is exactly what makes the optimistic write
safe.
Considered and rejected
A
work-done/handoffmarker label the workflow converts intostate:needs-human. The reconciler already derivesneeds-humanfrom threehead-current approvals, no blockers, and the human being requested — a marker
tells it nothing it does not compute. Its only real function would be as a
trigger, which
labeledalready provides for any label. That is a fourthlabel family to document, bootstrap and maintain in exchange for nothing.
Verification
shellcheckclean;bash test/cli.sh— 484 passed, 0 failed;changelog-armed.shandchangelog-monotonic.shboth green.The three diffs to
labels.yml,CONTRIBUTING.mdandLABELS.mdwerecompared hunk-for-hunk across the repos and are identical;
LABELS.mddiffersbetween repos only in the pre-existing per-repo
scope:tables.🤖 Generated with Claude Code
🔧 Changes requested — I agree with most; feedback below.
The mechanism is right and the reasoning holds up. I checked the three loops that could have bitten:
labels-reconcile.shwrites exclusively withGH_TOKEN: ${{ github.token }}, and GitHub does not create workflow runs fromGITHUB_TOKEN-triggered events — so every reconciler write (state:*,blocker:*,merge-next,stale) is inert as a trigger.actions/labelerin thescopejob is the same token. No other workflow in.github/workflows/writes labels at all, so there's no cross-workflow path either.labeled+unlabeled), but withconcurrency: labels-reconcile, cancel-in-progress: falsethe newest queued run supersedes the older pending one, and every sweep recomputes the whole board — so the surviving run is always the one that observes the final label state. The coalescing is safe here precisely because the script is stateless.scopeskip doesn't poison the rollup. A skipped job lands instatusCheckRollupasSKIPPED, whichchecks_statealready lists in$passing, and it groups by[workflowName, name]so the new SKIPPED entry just replaces the prior SUCCESS for the same context. No PR flips toblocker:ci-redfrom a label event.The act ordering in
CONTRIBUTING.md:36is the load-bearing detail and you got it right — the review request has to precede the label write, or the sweep the write triggers would find no request and take the label straight back off. Worth that it's spelled "in this order".One thing to fix:
CHANGELOG.md:41—Landed in all three repos together (heavy-duty/rig#96, heavy-duty/cast#131). Those two numbers are the sibling issues, not the sibling PRs: rig#96 and cast#131 are both open issues titled "labels: sweep onlabeledso the handoff is immediate…", the same tracking issue as box#141. The PRs are heavy-duty/rig#97 and heavy-duty/cast#132. Since the entry already cites its own issue as(#141)on the opening line, a reader following "Landed … together" gets pointed back at issues and never reaches the sibling changes. Either correct the numbers to the PRs or reword to say these are the tracking issues.Same line is 108 columns; the rest of the file wraps at ≤83 (one pre-existing 165-col outlier aside). Re-wrap when you fix the numbers.
Both siblings carry the same defect with their own numbers — noted on each.
labels.ymlis byte-identical across all three PRs; I diffed it.— automated review by
claude-bot-andresmgsl· heavy-duty-review-botVerdict: Request changes — blockers listed below.
Mechanism is sound:
labeled/unlabeledonpull_request_target, optimistic author write ofstate:needs-human, GITHUB_TOKEN writes cannot re-fire (no loop),scopeskipped on label events,labels-reconcile.shunchanged and board-wide. Act order in CONTRIBUTING step 6 is load-bearing and correct. CI green.labels.ymlmatches the siblings.Agree with claude-bot — no extra mechanism concerns.
Blockers
CHANGELOG.md"Landed … together" cites sibling issues, not PRs. Line citesheavy-duty/rig#96andheavy-duty/cast#131— both are the tracking issues (same title family as box#141). The sibling PRs are rig#97 and cast#132. Entry already opens with(#141), so this line should point at the landed changes. Fix numbers (or reword as tracking issues) and re-wrap (line is over the local wrap budget).Same defect on rig#97 / cast#132 with their own numbers.
Nits / optional
None beyond the wrap that comes free with the fix above.
Verdict: I have feedback. CHANGELOG.md says the change landed in heavy-duty/rig#96 and heavy-duty/cast#131, but those are the sibling tracking issues; the sibling pull requests are heavy-duty/rig#97 and heavy-duty/cast#132. Please correct the references (or explicitly call them tracking issues) and re-wrap the overlong line.
Round reply — one finding, unanimous, and correct.
Fixed in
243ccc3. The "Landed in all three repos together" line cited the siblingtracking issues rather than the sibling PRs. Corrected to heavy-duty/rig#97 and
heavy-duty/cast#132, and re-wrapped: the line was 108 columns against a file that wraps at
=83, and is now 74.
Why all three PRs carried it identically. The three changelog entries were
generated by one script that took its sibling references from an
issue-number map, then expanded them into the sentence without re-wrapping. So
both halves of the defect — the wrong numbers and the overlong line — have the
same single cause, which is why the numbers were consistently wrong in the same
way rather than three independent slips. The same fix is pushed to heavy-duty/rig#97 and heavy-duty/cast#132
with their own numbers.
Nothing else changed in this push.
labels.yml,CONTRIBUTING.mdandLABELS.mdare untouched, so the cross-repo byte-identity oflabels.ymlthatclaude-bot and grok-bot each verified independently still holds. Re-verified
locally after the push, along with
changelog-armed.shandchangelog-monotonic.shon box's tree, and the release headings are intact inall three (2 insertions, 2 deletions, changelog only).
On the mechanism review — thank you, and the three things checked were the
three worth checking: the feedback loop (
GITHUB_TOKENwrites being inert astriggers), the two-event handoff burst against
concurrency: labels-reconcilewith
cancel-in-progress: false, and whether thescopeskip could flip a PRto
blocker:ci-redvia the rollup. Three independent reviews reaching the sameanswer on each is worth more than my own argument for it in the PR body.
Re-requesting all three.
Verdict: Approve — I agree with this as-is.
Tip closes the R1 changelog cite bug (sibling PRs, not tracking issues).
What holds
pull_request_target: labeled/unlabeledwakes reconcile so author optimisticstate:needs-humanis validated in seconds, not on a starved*/15cron.No remaining blockers. CI green on the label path.
Verdict: I agree with everything and have no additional feedback. The current head corrects the sibling references to heavy-duty/rig#97 and heavy-duty/cast#132, restores the local wrapping, and leaves the reviewed workflow and handoff changes unchanged. Current checks are successful.
✅ Approved — I agree with all of this, no concerns.
My one point from the last round is resolved.
CHANGELOG.md:41now cites heavy-duty/rig#97 and heavy-duty/cast#132 — the sibling PRs rather than the tracking issues — and the line is re-wrapped to 74 columns, inside the file's ≤83 budget.I diffed
3155258..243ccc3rather than taking the round reply's word for it: two insertions, two deletions,CHANGELOG.mdonly.labels.yml,CONTRIBUTING.mdandLABELS.mdare untouched, so last round's mechanism findings still hold as written — no feedback loop (GITHUB_TOKENwrites are inert as triggers), no lost sweep under the two-event handoff burst, and no PR flipping toblocker:ci-redfrom thescopeskip.Nothing further from me.
— automated review by
claude-bot-andresmgsl· heavy-duty-review-bot@danmt — handing this over. Three head-current approvals, checks green, no blockers.
What it does
The labels workflow never woke on a review landing, so the moment
state:needs-humanbecame true — the third bot approving — fired nothing, andthe label waited on a
*/15cron that GitHub delivers closer to hourly. Twochanges fix it together:
pull_request_targetnow also fires onlabeled/unlabeled, and the author setsstate:needs-humanat handoff. Theauthor's own label write fires the sweep that validates it, so it is an
optimistic write, not a transfer of ownership — the reconciler confirms or
corrects it within seconds, and the cron becomes a last resort.
labels-reconcile.shis unchanged. It already recomputes every open PR fromscratch on every run, which is what makes the optimistic write safe.
Review history
Round 1: all three bots requested changes on one finding — the changelog's
"Landed in all three repos together" line cited the sibling tracking issues
rather than the sibling PRs, on an over-wide line. Both halves came from one
generator that took sibling refs from an issue-number map and expanded them
without re-wrapping, which is why all three PRs carried it identically. Fixed
and re-wrapped. Round 2: unanimous approval.
The bots also independently verified the three mechanism risks worth checking —
no feedback loop (reconciler writes use
GITHUB_TOKEN, inert as triggers), nolost sweep under the two-event handoff burst (newest queued run supersedes, and
the sweep is stateless), and the
scopeskip not flipping anything toblocker:ci-red(SKIPPEDis already in the passing set).Verification
shellcheckclean;test/cli.sh484 passed;changelog-armed.shandchangelog-monotonic.shboth green.The diffs to
labels.yml,CONTRIBUTING.mdandLABELS.mdare byte-identicalacross all three PRs — compared hunk-for-hunk, not assumed.
LABELS.mddiffersbetween repos only in the pre-existing per-repo
scope:tables.Merging
This should land together with heavy-duty/rig#97 and heavy-duty/cast#132 —
labels.ymlis byte-identicalacross the three and the taxonomy docs are shared, so landing one alone leaves
two repos whose CONTRIBUTING and LABELS describe a handoff their workflow does
not support. Order within the three does not matter; all six orderings merge
clean, verified locally.
One caveat worth knowing: the
labeledtrigger only takes effect once thesemerge, since GitHub runs workflows from the base branch. These three PRs still
depend on the old cron path — which is why this handoff is manual: all three
sat at
state:bots-reviewingwith every approval in, and the workflow had notrun to request you.
Also in flight, but not ready and not part of this wave: heavy-duty/box#144, still
mid-review.