labels: sweep on labeled so the handoff is immediate, and let the author set state:needs-human #141

Closed
opened 2026-07-20 19:37:07 +00:00 by dan-claude-bot · 0 comments
dan-claude-bot commented 2026-07-20 19:37:07 +00:00 (Migrated from github.com)

state:needs-human can lag hours behind the round it describes. The fix is one line in labels.yml plus a change to who writes that one label.

What happens today

The labels workflow triggers on schedule (*/15), workflow_dispatch, and pull_request_target (opened, reopened, ready_for_review, converted_to_draft, synchronize).

A review landing is not in that list. So the exact moment state:needs-human becomes true — the third bot approving — fires nothing. The label waits for the cron, or for someone to touch an unrelated PR.

And the cron does not run at the declared rate. Measured across all three repos over a two-hour window on 2026-07-20: one scheduled run each, against the eight */15 implies. GitHub's Actions scheduler heavily deprioritises short intervals.

Observed live: heavy-duty/rig#94 took its third approval at 18:55:46 and sat at state:bots-reviewing for hours. box and cast happened to catch a scheduled run at 19:03 and flipped correctly. Same code, same workflow file (labels.yml is byte-identical across the three) — the only difference was which repo got a cron tick.

This is worst on the quietest repo. Every sweep reconciles the whole board, so an active 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 — and it is the repo where a wrong label is least likely to be noticed.

Why the obvious fix does not work

Adding pull_request_review to the triggers looks right and is not. On PRs from forks — which is every PR here — that event gives GITHUB_TOKEN read-only permissions, and it ignores the permissions: block if you try to raise them. There is no pull_request_review_target. The workflow's own header comment already says this; it is correct.

Refs: events that trigger workflows, community #26651, community #55940.

Proposed fix

1. Trigger on label changes. pull_request_target supports labeled and unlabeled:

pull_request_target:
  types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]

2. The PR author sets state:needs-human at handoff. The handoff becomes three acts in order: post the tagged summary → request the human's review → set state:needs-human, removing the previous state label.

The two combine into something better than either alone: the agent's own label write fires the sweep that validates it. Seconds later the reconciler either 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 when an agent forgets, rather than the primary path.

dan-claude-bot triage can apply an existing label (verified); only label creation 403s, so this needs no permission change.

This cannot loop. The reconciler's own label writes use GITHUB_TOKEN, and GitHub does not create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a PAT and therefore do trigger — exactly the asymmetry wanted.

Considered and rejected: a new marker label

The alternative was a work-done / handoff label that the agent sets and the workflow converts into state:needs-human.

It does not earn its place. The reconciler already derives needs-human from three head-current approvals, no blockers, and the human being requested — a marker label tells it nothing it does not compute. Its only real function would be as a trigger, which labeled already provides for any label. That makes it a fourth label family to document, bootstrap and maintain, in exchange for nothing.

Scope of the change

  • .github/workflows/labels.yml — the trigger line
  • CONTRIBUTING.md — the three-act handoff in step 6, and the state:* row qualified: machine-owned, except the author sets state:needs-human at handoff and the workflow reconciles
  • LABELS.md — same qualification

No change to labels-reconcile.sh. The reconciler already recomputes every open PR from scratch on every run, which is exactly what makes the optimistic write safe.

Should land in all three repos together — labels.yml and the label taxonomy are shared.


Aside, noticed while filing this: none of the three repos has a scope: label covering the repo's own automation — .github/, labels.yml, LABELS.md, CI. This issue has no honest scope to carry. Possibly worth a scope:ci or scope:meta; filing unlabelled for scope rather than forcing a wrong one.

`state:needs-human` can lag hours behind the round it describes. The fix is one line in `labels.yml` plus a change to who writes that one label. ## What happens today The labels workflow triggers on `schedule` (`*/15`), `workflow_dispatch`, and `pull_request_target` (`opened, reopened, ready_for_review, converted_to_draft, synchronize`). **A review landing is not in that list.** So the exact moment `state:needs-human` becomes true — the third bot approving — fires nothing. The label waits for the cron, or for someone to touch an unrelated PR. And the cron does not run at the declared rate. Measured across all three repos over a two-hour window on 2026-07-20: **one scheduled run each, against the eight `*/15` implies.** GitHub's Actions scheduler heavily deprioritises short intervals. Observed live: heavy-duty/rig#94 took its third approval at `18:55:46` and sat at `state:bots-reviewing` for hours. box and cast happened to catch a scheduled run at `19:03` and flipped correctly. Same code, same workflow file (`labels.yml` is byte-identical across the three) — the only difference was which repo got a cron tick. This is worst on the quietest repo. Every sweep reconciles the **whole board**, so an active 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 — and it is the repo where a wrong label is least likely to be noticed. ## Why the obvious fix does not work Adding `pull_request_review` to the triggers looks right and is not. On PRs from forks — which is every PR here — that event gives `GITHUB_TOKEN` **read-only** permissions, and it ignores the `permissions:` block if you try to raise them. There is no `pull_request_review_target`. The workflow's own header comment already says this; it is correct. Refs: [events that trigger workflows](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows), [community #26651](https://github.com/orgs/community/discussions/26651), [community #55940](https://github.com/orgs/community/discussions/55940). ## Proposed fix **1. Trigger on label changes.** `pull_request_target` supports `labeled` and `unlabeled`: ```yaml pull_request_target: types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled] ``` **2. The PR author sets `state:needs-human` at handoff.** The handoff becomes three acts in order: post the tagged summary → request the human's review → set `state:needs-human`, removing the previous state label. The two combine into something better than either alone: the agent's own label write **fires the sweep that validates it**. Seconds later the reconciler either 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 when an agent forgets, rather than the primary path. `dan-claude-bot` triage can apply an existing label (verified); only label *creation* 403s, so this needs no permission change. **This cannot loop.** The reconciler's own label writes use `GITHUB_TOKEN`, and GitHub does not create workflow runs from `GITHUB_TOKEN`-triggered events. Agent writes use a PAT and therefore do trigger — exactly the asymmetry wanted. ## Considered and rejected: a new marker label The alternative was a `work-done` / `handoff` label that the agent sets and the workflow converts into `state:needs-human`. It does not earn its place. The reconciler already derives `needs-human` from three head-current approvals, no blockers, and the human being requested — a marker label tells it nothing it does not compute. Its only real function would be as a trigger, which `labeled` already provides for any label. That makes it a fourth label family to document, bootstrap and maintain, in exchange for nothing. ## Scope of the change - `.github/workflows/labels.yml` — the trigger line - `CONTRIBUTING.md` — the three-act handoff in step 6, and the `state:*` row qualified: machine-owned, except the author sets `state:needs-human` at handoff and the workflow reconciles - `LABELS.md` — same qualification No change to `labels-reconcile.sh`. The reconciler already recomputes every open PR from scratch on every run, which is exactly what makes the optimistic write safe. Should land in all three repos together — `labels.yml` and the label taxonomy are shared. --- **Aside, noticed while filing this:** none of the three repos has a `scope:` label covering the repo's own automation — `.github/`, `labels.yml`, `LABELS.md`, CI. This issue has no honest scope to carry. Possibly worth a `scope:ci` or `scope:meta`; filing unlabelled for scope rather than forcing a wrong one.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/box#141
No description provided.