labels: keep edited/reopened on issues; correct 0.3.0 adoption prose (#199 round)

Round fixes on #200.

codex-1 (blocking): the issues narrowing dropped `edited`/`reopened`, but both
carry a queue-state change an event uniquely carries — `edited` a body rewrite
of the `Blocked by #N` declaration the sweep parses
(issueflow-reconcile.sh:179), `reopened` a closed issue re-entering the queue.
Dropping them tripped #199's must-fail. Narrow to
`[opened, closed, edited, reopened]`, dropping only the churn/validation
actions labeled/unlabeled/assigned/unassigned. Trigger tests now pin
edited/reopened present and the four dropped; labels.test.sh exact-list updated.

kimi (blocking): the "supersedes unreleased #144" prose was false — #144's
edited/reopened shipped in 0.3.0. Dissolved: we now keep them. CONSUMERS prose
rewritten to the real version history (0.2.0 #32 / 0.3.0 #144 / #199 narrows),
and the #137 review-request line corrected from "unreleased" to shipped-in-0.3.0.

kimi (non-blocking): reusable labels.yml comment no longer cites */15.

codex-2 (AC1 after-measurement / closing) escalated to triage on #199 — held,
not guessed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-28 19:04:27 +00:00
parent 155828a069
commit 0a812c4b19
6 changed files with 79 additions and 56 deletions

View file

@ -10,11 +10,12 @@ name: labels
# and reconcile checks out the BASE branch only. Keep it that way. # and reconcile checks out the BASE branch only. Keep it that way.
# #
# There is no pull_request_review_target, so a review landing cannot wake this # There is no pull_request_review_target, so a review landing cannot wake this
# workflow directly — and the */15 cron is advisory: GitHub deprioritises # workflow directly — and the caller's cron is a backstop, not the wake (#199
# short intervals hard enough that a quiet repo goes hours between ticks. The # relaxed it from */15 to hourly): GitHub deprioritises short intervals anyway,
# handoff wakes the sweep itself: the author sets state:needs-human, and the # so a quiet repo goes a while between ticks. The handoff wakes the sweep
# caller's `labeled` event confirms or corrects that optimistic write within # itself: the author sets state:needs-human, and the caller's `labeled` event
# seconds. The cron stays as the last resort for a forgotten handoff. # confirms or corrects that optimistic write within seconds. The cron stays as
# the last resort for a forgotten handoff.
# #
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not # This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a # create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a

View file

@ -12,16 +12,20 @@ on:
# billing floor, 52% of one private consumer's runs for near-zero real work. # billing floor, 52% of one private consumer's runs for near-zero real work.
schedule: [{cron: "0 * * * *"}] schedule: [{cron: "0 * * * *"}]
workflow_dispatch: # bootstraps missing labels on a fresh repo workflow_dispatch: # bootstraps missing labels on a fresh repo
# Narrowed to the two actions with promptness-critical reconcile behavior # Narrowed (#199) to the actions that carry a queue-state change the hourly
# (#199): `opened` drives the mint→needs-triage check (issueflow's opened-only # cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
# path), `closed` drives the blocker-closes→ready self-heal (crew#96/#98). The # unassigned, which feed validation and the 48h claim clock (caught within one
# dropped actions — labeled/unlabeled/assigned/unassigned/edited/reopened — # cadence) and were the dominant issues-churn source. Kept: `opened` (the
# only feed validation and the 48h claim clock, which the hourly cron catches # mint→needs-triage check, issueflow's opened-only path), `closed` (the
# within one cadence; labeled/unlabeled were also the dominant issues-churn # blocker-closes→ready self-heal, crew#96/#98), `edited` (a body rewrite of the
# source. The PR handoff wake is pull_request_target:labeled, NOT issues, so # `Blocked by #N` declaration the sweep parses — issueflow-reconcile.sh:179),
# `reopened` (a closed issue re-entering the queue wearing labels derived when
# it closed). The must-fail in #199 is exactly "a queue-state transition waits
# on the schedule when an event could have carried it", so edited/reopened stay
# on events. The PR handoff wake is pull_request_target:labeled, NOT issues, so
# this does not touch the handoff. # this does not touch the handoff.
issues: issues:
types: [opened, closed] types: [opened, closed, edited, reopened]
pull_request_target: pull_request_target:
# Every PR arrives from a fork, so these carry the head/draft/review facts # Every PR arrives from a fork, so these carry the head/draft/review facts
# the sweep derives state:* from. labeled/unlabeled are the handoff wake — # the sweep derives state:* from. labeled/unlabeled are the handoff wake —

View file

@ -4,10 +4,11 @@
Events still carry every state change in seconds; the cron only backstops a Events still carry every state change in seconds; the cron only backstops a
forgotten handoff, so a full-board sweep is billed far less often without forgotten handoff, so a full-board sweep is billed far less often without
delaying any real transition past one cadence. delaying any real transition past one cadence.
- `labels` — the caller's `issues:` trigger narrows to `[opened, closed]` - `labels` — the caller's `issues:` trigger narrows to
(#199), the two actions with reconcile behavior the cron cannot wait a `[opened, closed, edited, reopened]` (#199), the actions that carry a
cadence for. The churn actions — labeled/unlabeled/assigned/unassigned/ queue-state change the cron cannot wait a cadence for. The churn/validation
edited/reopened — come off; the PR handoff wake is unaffected. actions — labeled/unlabeled/assigned/unassigned — come off; the PR handoff
wake is unaffected.
- `labels` — each caller trigger now carries a comment saying why it is - `labels` — each caller trigger now carries a comment saying why it is
subscribed, and reconcile keeps `cancel-in-progress: false` (#199) — subscribed, and reconcile keeps `cancel-in-progress: false` (#199) —
cancelling a sweep mid-board is the race that guard exists to prevent. cancelling a sweep mid-board is the race that guard exists to prevent.

View file

@ -306,20 +306,21 @@ on:
# Fork PRs; these carry the head/draft/review facts state:* derives from. # Fork PRs; these carry the head/draft/review facts state:* derives from.
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here); # labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
# synchronize re-derives on every push. review_requested/review_request_removed # synchronize re-derives on every push. review_requested/review_request_removed
# are unreleased — not in 0.2.0; add them with the pin bump to the first tag # (shipped in 0.3.0, ceremony#137) wake the sweep that clears
# carrying ceremony#137. They wake the sweep that clears blocker:unrequested # blocker:unrequested when the panel is asked.
# when the panel is asked.
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed] types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
# Available at 0.2.0 and later (the first tag carrying ceremony#32); a # Available at 0.2.0 and later (the first tag carrying ceremony#32); a
# consumer pinned to 0.1.0 omits this block. # consumer pinned to 0.1.0 omits this block.
issues: issues:
# Narrowed to the two promptness-critical actions (#199): opened → the # Narrowed (#199) to the actions carrying a queue-state change the hourly
# mint→needs-triage check, closed → the blocker-closes→ready self-heal. The # cron cannot wait one cadence for: opened → the mint→needs-triage check,
# rest (labeled/unlabeled/assigned/unassigned/edited/reopened) only feed # closed → the blocker-closes→ready self-heal, edited → a body rewrite of the
# validation and the 48h claim clock, which the hourly cron catches within # `Blocked by #N` declaration the sweep parses, reopened → a closed issue
# one cadence; labeled/unlabeled were the issues-churn source. The handoff # re-entering the queue. Dropped: labeled/unlabeled/assigned/unassigned —
# wake is pull_request_target:labeled, not issues, so this leaves it intact. # validation + the 48h claim clock, caught within one cadence, and
types: [opened, closed] # labeled/unlabeled were the issues-churn source. The handoff wake is
# pull_request_target:labeled, not issues, so this leaves it intact.
types: [opened, closed, edited, reopened]
permissions: permissions:
contents: read contents: read
checks: read # mergeability/check-rollup read for PR state checks: read # mergeability/check-rollup read for PR state
@ -340,29 +341,32 @@ needs all three explicit reads above; without them the failure appears as an emp
The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the
first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt
it only by bumping every ceremony reference to `0.2.0` or later; never mix it only by bumping every ceremony reference to `0.2.0` or later; never mix
refs to adopt it early. `0.2.0` shipped the broad type list refs to adopt it early. The type list has grown then narrowed across tags:
(`labeled`/`unlabeled`/`assigned`/`unassigned` alongside `opened`/`closed`); `0.2.0` (ceremony#32) shipped `[opened, labeled, unlabeled, assigned,
ceremony#199 narrows it to `[opened, closed]` and relaxes the cron to hourly, unassigned, closed]`; `0.3.0` (ceremony#144) added `edited` and `reopened`;
so a consumer picks up the smaller trigger surface at the pin bump to the ceremony#199 narrows it to `[opened, closed, edited, reopened]` and relaxes the
first tag carrying ceremony#199. The narrowing supersedes the unreleased cron to hourly, so a consumer picks up the smaller trigger surface at the pin
`edited`/`reopened` additions (#144) rather than shipping them — those actions bump to the first tag carrying ceremony#199. The narrowing drops
are gone from the caller, not deferred — while keeping the stub and ceremony's `labeled`/`unlabeled`/`assigned`/`unassigned` — validation and the 48h claim
own caller byte-for-byte parity that #144 established. Only two `issues` clock, which the hourly cron catches within one cadence, and `labeled`/
actions survive because only two carry reconcile behavior the cron cannot wait `unlabeled` were the issues-churn source — while **keeping** #144's `edited`/
one cadence for: `opened` drives the mint→`needs-triage` check, and `closed` `reopened`: those carry a queue-state change an event uniquely carries (a body
drives the blocker-closes→`ready` self-heal. rewrite of `Blocked by #N`, and a closed issue re-entering the queue), so the
must-fail in ceremony#199 keeps them on events. `opened` drives the
mint→`needs-triage` check and `closed` the blocker-closes→`ready` self-heal;
the stub and ceremony's own caller stay byte-for-byte identical, the parity
#144 established.
`pull_request_target` is intentional: fork PRs need the base repository's `pull_request_target` is intentional: fork PRs need the base repository's
token to write labels. The reusable workflow executes no PR code. It checks token to write labels. The reusable workflow executes no PR code. It checks
out only the consumer's base branch and the pinned ceremony implementation. out only the consumer's base branch and the pinned ceremony implementation.
The #52 ruling invariants ride exactly these triggers — but the caller above The #52 ruling invariants ride exactly these triggers — but the caller above
is no longer the #18 shape, so adopting current triggers is a stub edit, not is no longer the #18 shape, so adopting current triggers is a stub edit, not
a bare pin bump. The one pending `pull_request_target:` edit is a bare pin bump. `review_requested` and `review_request_removed` on
`review_requested` and `review_request_removed` (#137) — the wake that `pull_request_target:` shipped in `0.3.0` (ceremony#137) — the wake that
clears `blocker:unrequested` the moment the panel is asked, without which a clears `blocker:unrequested` the moment the panel is asked, without which a
quiet repo wears that flag until the advisory cron. Adopt it with the pin bump quiet repo wears that flag until the backstop cron; a consumer picks them up
to the first tag carrying ceremony#137 — never before it and never through by pinning `0.3.0` or later, never through mixed refs.
mixed refs.
`.github/labels.conf` has one mandatory panel setting, one mandatory `.github/labels.conf` has one mandatory panel setting, one mandatory
`triage-actors` setting, and then zero or more scope rows: `triage-actors` setting, and then zero or more scope rows:

View file

@ -64,12 +64,24 @@ check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
check "stub cron line no longer fires */15" 1 "" \ check "stub cron line no longer fires */15" 1 "" \
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$STUB" bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$STUB"
# ---- issues: is narrowed to the two promptness-critical actions (#199) ------- # ---- issues: is narrowed to the queue-state-changing actions (#199) ----------
# opened → mint→needs-triage; closed → blocker-closes→ready self-heal. The # Kept because each carries a queue-state change an event uniquely carries, so
# churn actions must not reappear on the issues surface without a fresh why. # dropping it would trip #199's must-fail (a transition waiting on the schedule
# (labels.test.sh owns the exact-list and caller<->stub parity assertions; here # when an event could have carried it): opened → mint→needs-triage; closed →
# we name each dropped action so its return produces a #199-specific failure.) # blocker-closes→ready self-heal; edited → a body rewrite of the `Blocked by #N`
for churn in labeled unlabeled assigned unassigned edited reopened; do # declaration the sweep parses; reopened → a closed issue re-entering the queue.
# (labels.test.sh owns the exact-list and caller<->stub parity assertions.)
for keep in opened closed edited reopened; do
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
check "self caller issues surface keeps '$keep'" 0 "" \
bash -c 'trigger_types() {
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$keep"
done
# The churn actions must not reappear on the issues surface without a fresh why.
# labeled/unlabeled were the dominant issues-churn source; assigned/unassigned
# only feed validation and the 48h claim clock, caught within one cadence.
for churn in labeled unlabeled assigned unassigned; do
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here # shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
check "self caller issues surface drops '$churn'" 1 "" \ check "self caller issues surface drops '$churn'" 1 "" \
bash -c 'trigger_types() { bash -c 'trigger_types() {

View file

@ -69,11 +69,12 @@ check "LABELS.md enumerates no repo's scope labels" 1 "0" \
# same lists. review_requested/review_request_removed are the wake that # same lists. review_requested/review_request_removed are the wake that
# clears blocker:unrequested — the label sat false for as long as a quiet # clears blocker:unrequested — the label sat false for as long as a quiet
# repo stayed quiet because the one event that falsifies it was never # repo stayed quiet because the one event that falsifies it was never
# listed (#137). The issues list narrowed to [opened, closed] (#199): those # listed (#137). The issues list narrowed to [opened, closed, edited, reopened]
# two carry reconcile behavior the hourly cron cannot wait one cadence for — # (#199): each carries a queue-state change the hourly cron cannot wait one
# opened drives mint→needs-triage, closed drives the blocker-closes→ready # cadence for — opened drives mint→needs-triage, closed the blocker-closes→ready
# self-heal — while the churn actions (labeled/unlabeled/assigned/unassigned/ # self-heal, edited a body rewrite of the `Blocked by #N` line the sweep parses,
# edited/reopened) came off. The stub is prose, so nothing but these rows # reopened a closed issue re-entering the queue — while the churn/validation
# actions (labeled/unlabeled/assigned/unassigned) came off. The stub is prose, so nothing but these rows
# keeps the lists from drifting: a type in one file only is a wake that fires # keeps the lists from drifting: a type in one file only is a wake that fires
# at home and nowhere in the fleet, or the reverse — the drift #144 caught. # at home and nowhere in the fleet, or the reverse — the drift #144 caught.
# The NF guard keeps `issues: write` under permissions: from matching the # The NF guard keeps `issues: write` under permissions: from matching the
@ -100,8 +101,8 @@ check "caller and stub pull_request_target lists are identical" 0 "" \
check "the caller lists both review-request wakes" 0 "" pr_has_both_review_wakes check "the caller lists both review-request wakes" 0 "" pr_has_both_review_wakes
check "caller and stub issues lists are identical" 0 "" \ check "caller and stub issues lists are identical" 0 "" \
types_in_sync issues "$CALLER" "$STUB" types_in_sync issues "$CALLER" "$STUB"
check "the caller lists exactly the two promptness-critical issue types" 0 \ check "the caller lists exactly the queue-state-changing issue types" 0 \
"types: [opened, closed]" event_types "$CALLER" issues "types: [opened, closed, edited, reopened]" event_types "$CALLER" issues
# the failing cases: drop a type from either file, or reorder one list only, # the failing cases: drop a type from either file, or reorder one list only,
# and the identity rows above go red — exercised here on mutated copies # and the identity rows above go red — exercised here on mutated copies
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md" mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"