This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new cf6e458 feat(pr-management-triage): stale ready-for-review label
sweep (#148)
cf6e458 is described below
commit cf6e4581b12fe48b2db348abfa6fc7bb7865fbc3
Author: Yeonguk Choo <[email protected]>
AuthorDate: Thu May 14 18:54:19 2026 +0900
feat(pr-management-triage): stale ready-for-review label sweep (#148)
---
.claude/skills/pr-management-triage/SKILL.md | 12 ++-
.claude/skills/pr-management-triage/actions.md | 52 ++++++++++
.../pr-management-triage/comment-templates.md | 21 ++++
.../skills/pr-management-triage/fetch-and-batch.md | 7 ++
.../skills/pr-management-triage/stale-sweeps.md | 110 ++++++++++++++++++++-
5 files changed, 196 insertions(+), 6 deletions(-)
diff --git a/.claude/skills/pr-management-triage/SKILL.md
b/.claude/skills/pr-management-triage/SKILL.md
index 7adc468..afe15c8 100644
--- a/.claude/skills/pr-management-triage/SKILL.md
+++ b/.claude/skills/pr-management-triage/SKILL.md
@@ -384,7 +384,9 @@ the order:
maintainer reviews all label-add proposals back-to-back)
6. `passing` → `mark-ready`
7. Stale sweeps (`stale_draft` → `close`, `inactive_open` →
- `draft`, `stale_workflow_approval` → `draft`)
+ `draft`, `stale_workflow_approval` → `draft`,
+ `stale_ready_label` → `strip-ready-label`,
+ `stale_ready_label_unhealthy` → `close`)
For each group, present one screen worth of headline info
(PR number, title, author, 1-line reason, label chips) and
@@ -440,9 +442,17 @@ When the maintainer has worked through every interactive
group
- convert non-draft PRs with >4 weeks of no activity to draft
- convert workflow-approval PRs with >4 weeks of no activity
to draft
+- on PRs labeled `ready for maintainer review` whose author
+ has been silent ≥ 7 days after a maintainer comment,
+ strip the label (4a — branch healthy) or propose `close`
+ (4b — red CI or merge conflicts). See
+
[`stale-sweeps.md#sweep-4--stale-ready-for-review-label`](stale-sweeps.md#sweep-4--stale-ready-for-review-label).
Each sweep emits its own group in the interaction loop (Step 3),
so the maintainer still confirms before any PR is touched.
+Sweep 4 issues its own paged search (the default search
+excludes labeled PRs) — see
+[`fetch-and-batch.md#search-query-construction`](fetch-and-batch.md#search-query-construction).
---
diff --git a/.claude/skills/pr-management-triage/actions.md
b/.claude/skills/pr-management-triage/actions.md
index 0dedf26..23c4f1a 100644
--- a/.claude/skills/pr-management-triage/actions.md
+++ b/.claude/skills/pr-management-triage/actions.md
@@ -534,6 +534,57 @@ decision.
---
+## `strip-ready-label` — remove the ready-for-review label, no comment
+
+Used by [Sweep 4a — branch healthy → strip
label](stale-sweeps.md#4a--branch-healthy--strip-label).
+One mutation, no comment. (The rotted-branch sibling
+[Sweep 4b](stale-sweeps.md#4b--branch-rotted--propose-close)
+uses `close` instead, not this action.)
+
+```bash
+# Remove the now-stale ready-for-review label (idempotent —
+# a 422 "Label does not exist on this issue" is benign; log
+# and continue).
+gh pr edit <N> --repo <repo> --remove-label "ready for maintainer review"
+```
+
+The label string is read from
+[`<project-config>/pr-management-config.md →
ready_for_maintainer_review_label`](../../../projects/_template/pr-management-config.md);
+do not hard-code it. The same `gh` recipe is used by the
+"strip-on-downgrade" hook inside `draft` and `comment`
+(`actions.md` §[draft](#draft--convert-to-draft-and-post-violations-comment) /
+§[comment](#comment--post-violations--stale-review--ping-comment)),
+but those flows additionally convert to draft / post a
+comment. The `strip-ready-label` action is **only** the
+label-removal step — no other mutation, no comment.
+
+### Why no comment
+
+The PR already carries an unanswered maintainer comment (that
+is the trigger condition; see Sweep 4a). Posting a second
+contributor-facing comment would either duplicate the
+maintainer's existing ask or race the normal-queue re-triage
+that may run on the next sweep. Removing the label silently
+is the most conservative move; the maintainer can re-add the
+label in one click if the strip was unwarranted.
+
+### Failure handling
+
+- 422 "Label does not exist on this issue" — benign, log and
+ treat the action as successful (the desired end state is
+ already in place).
+- 404 / network error — surface to the maintainer with the PR
+ number, do not retry silently. The next sweep run will
+ re-evaluate.
+- Anything else — surface and stop the batch (consistent with
+ the `gh pr edit --remove-label` failure handling in `draft`).
+
+### Order-of-operations
+
+One step. No comment to sequence against.
+
+---
+
## Order-of-operations recap for destructive actions
For every action that includes a comment, post the comment
@@ -547,6 +598,7 @@ For every action that includes a comment, post the comment
| `flag-suspicious` | post comment → close → label *(per PR in the batch)* |
| `mark-ready` | label only |
| `mark-ready-with-ping` | post comment → label |
+| `strip-ready-label` | remove-label only (no comment) |
| `rerun` | rerun (no comment) |
| `rebase` | update-branch (no comment) |
| `ping` | post comment |
diff --git a/.claude/skills/pr-management-triage/comment-templates.md
b/.claude/skills/pr-management-triage/comment-templates.md
index 563fec4..b5adaa2 100644
--- a/.claude/skills/pr-management-triage/comment-templates.md
+++ b/.claude/skills/pr-management-triage/comment-templates.md
@@ -374,6 +374,27 @@ No label is added — the conversion itself is the signal.
---
+## Stale ready-label close
+
+*(`stale-ready-label-close` — close a label-flagged PR after author silence +
bitrot)*
+
+Used by [Sweep 4b](stale-sweeps.md#4b--branch-rotted--propose-close).
+
+```markdown
+@<author> This PR has had no author response for <days_since_maintainer> days
since the last maintainer comment, and the branch now has <bitrot_signal>.
Closing to keep the queue clean.
+
+When you're ready to resume, please rebase onto the current `<base>` branch,
address any failing checks, and either reopen this PR or open a fresh one.
There is no rush.
+
+<ai_attribution_footer>
+```
+
+`<bitrot_signal>` ∈ {`failing CI`,
+`merge conflicts with <base>`, `failing CI and merge conflicts with <base>`},
+keyed off `statusCheckRollup.state == FAILURE` and
+`mergeable == CONFLICTING`.
+
+---
+
## Stale workflow approval
*(`stale-workflow-approval` — convert stale WF-approval to draft)*
diff --git a/.claude/skills/pr-management-triage/fetch-and-batch.md
b/.claude/skills/pr-management-triage/fetch-and-batch.md
index 8f8735a..18cacb6 100644
--- a/.claude/skills/pr-management-triage/fetch-and-batch.md
+++ b/.claude/skills/pr-management-triage/fetch-and-batch.md
@@ -125,6 +125,13 @@ is:pr is:open repo:<upstream>
sort:updated-asc
```
+The [stale-ready-label
sweep](stale-sweeps.md#sweep-4--stale-ready-for-review-label)
+is the one exception that *includes* the label, using the same
+batch schema with `label:"ready for maintainer review"`
+(positive match) instead of the negative match above. All
+other selectors flow through the construction table in
+[`#search-query-construction`](#search-query-construction).
+
### Why this shape
Every field above is consumed by Step 2 (filter, classify, and
diff --git a/.claude/skills/pr-management-triage/stale-sweeps.md
b/.claude/skills/pr-management-triage/stale-sweeps.md
index 869b0ea..c41c0f0 100644
--- a/.claude/skills/pr-management-triage/stale-sweeps.md
+++ b/.claude/skills/pr-management-triage/stale-sweeps.md
@@ -29,19 +29,39 @@ entirely). Both paths go through the same rules below.
## Inputs
-Each stale sweep needs two timestamps per PR:
+Each stale sweep needs these timestamps per PR:
- `updated_at` — the PR's `updatedAt` field (already in the
batch query)
- `last_triage_comment_at` — the `createdAt` of the most
recent comment by the viewer containing the
`Pull Request quality criteria` marker, if any
-
-Both come from the same aliased query that drives
+- `last_author_activity_at` — the max of three timestamps,
+ all already in the batch query:
+ 1. the head commit's `committedDate` from `commits(last: 1)`
+ (catches pushes, including force-pushes — `committedDate`
+ advances on rebase even when no new code is added),
+ 2. the `createdAt` of the most recent **issue-level** comment
+ by the PR author in `comments(last: 10)`,
+ 3. the `createdAt` of the most recent **review-thread** reply
+ by the PR author across
+ `reviewThreads.nodes.comments(first: 5)` (filter
+ `author.login == pullRequest.author.login`).
+ Item 3 matters because line-level discussion is the most
+ common form of author response on substantive PRs; omitting
+ it would surface PRs as stale even while an active inline
+ conversation is in progress.
+- `last_maintainer_comment_at` — the `createdAt` of the most
+ recent comment in `comments(last: 10)` whose
+ `authorAssociation` is one of `COLLABORATOR`/`MEMBER`/`OWNER`,
+ excluding the viewer's own triage comments (the
+ `Pull Request quality criteria` marker disqualifies).
+
+All inputs come from the same aliased query that drives
classification — no extra fetches. If a PR hasn't been triaged
in the current session and also wasn't triaged in a prior one,
-`last_triage_comment_at` is null and the sweep uses `updated_at`
-alone.
+`last_triage_comment_at` is null and Sweeps 1a/1b fall back to
+`updated_at` alone.
`<now>` is the session start time (captured in UTC on entry).
Use a single reference moment for the whole sweep so edge
@@ -152,18 +172,98 @@ Same as Sweep 2 — simple `[A]ll`.
---
+## Sweep 4 — Stale ready-for-review label
+
+When a PR carries `ready for maintainer review` and the author
+has been silent for ≥ 7 days after a maintainer comment, branch
+health splits the disposition: 4a strips the label; 4b proposes
+`close`.
+
+### Why a separate sub-query
+
+The default fetch search (see
+[`fetch-and-batch.md#search-query-construction`](fetch-and-batch.md#search-query-construction))
+excludes `ready for maintainer review`, so candidates here are
+never in the default page. Sweep 4 issues its own paged search
+on the same enrichment schema — no new GraphQL surface:
+
+```text
+is:pr is:open repo:<upstream>
+label:"ready for maintainer review"
+sort:updated-asc
+```
+
+The label name comes from
+[`<project-config>/pr-management-config.md →
ready_for_maintainer_review_label`](../../../projects/_template/pr-management-config.md)
+— do not hard-code the string.
+
+### Common trigger (4a and 4b)
+
+- The PR carries the `ready for maintainer review` label.
+- `last_maintainer_comment_at` is not null and
+ `<now> - last_maintainer_comment_at >= 7 days`.
+- `last_author_activity_at` is null **or**
+ `last_author_activity_at <= last_maintainer_comment_at`.
+
+The last condition makes this sweep about *author silence*,
+not label age — a "still working on it" reply resets the clock.
+
+### 4a — Branch healthy → strip label
+
+**Extra trigger.** `mergeable != CONFLICTING` and
+`statusCheckRollup.state != FAILURE`.
+
+**Action.** `strip-ready-label`. See
+[`actions.md#strip-ready-label`](actions.md#strip-ready-label--remove-the-ready-for-review-label-no-comment).
+
+**Reason string.** *"Ready-for-review label stale — N days
+since maintainer comment, no author reply, branch healthy —
+strip label only"*.
+
+**Group behaviour.** Simple `[A]ll` — non-destructive, no
+per-PR confirm.
+
+### 4b — Branch rotted → propose close
+
+**Extra trigger.** `mergeable == CONFLICTING` **or**
+`statusCheckRollup.state == FAILURE`.
+
+**Action.** `close` with the
+[`stale-ready-label-close`](comment-templates.md#stale-ready-label-close)
+comment template; **skip the quality-violations label step**
+(close reason is bitrot, not policy violation). Otherwise
+[`actions.md#close`](actions.md#close--close-with-comment-and-quality-violations-label)
+unchanged.
+
+**Reason string.** *"Ready-for-review label stale — N days
+since maintainer comment, no author reply, branch has
+<bitrot_signal> — close"*. `<bitrot_signal>` ∈ {`failing CI`,
+`merge conflicts`, `failing CI + conflicts`}.
+
+**Group behaviour.** Per-PR confirm inside the batch
+(inherited from the `close`-group rule, SKILL.md Step 3).
+
+---
+
## Order of sweeps
1. Sweep 1a (triaged drafts, 7d)
2. Sweep 1b (untriaged drafts, 2w)
3. Sweep 2 (inactive open, 4w)
4. Sweep 3 (stale WF approval, 4w)
+5. Sweep 4a (stale ready-label, healthy, 7d) → strip
+6. Sweep 4b (stale ready-label, rotted, 7d) → close
Run 1a before 1b so a draft that's both "triaged 7d ago" and
"never-triaged 2w ago" (the triage comment is recent but the
overall PR is old) is categorised by the more precise trigger.
In practice that overlap is rare, but the order is defined.
+Sweep 4 operates on a disjoint candidate set (the labeled PRs
+the default search excluded), so there is no overlap with the
+earlier sweeps. 4a runs before 4b so the cheap label-tidying
+batch lands before the per-PR-confirm `close` group.
+
---
## What the sweeps do NOT do