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 3df0451 pr-management-stats: fix doc anchors and split Action/Detail
columns (#106)
3df0451 is described below
commit 3df0451ef4d0bf6c2a390069c843b892116f040e
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sun May 10 20:13:02 2026 +0200
pr-management-stats: fix doc anchors and split Action/Detail columns (#106)
Two doc-quality fixes for the pr-management-stats skill, mirroring the
review feedback from @choo121600 on the corresponding apache/airflow PR
(apache/airflow#66464):
1. **Anchor links drop the fragment in the URL.**
Cross-references like `[`render.md#recommendation-rules`](render.md)`
show an anchor in the visible text but the URL omits it, so GFM never
scrolls to the target section. Mechanically copied each visible
anchor into the URL across 26 references in 5 files. Three target
headings were tweaked so the GFM-generated anchors actually exist:
- `aggregate.md#counters` → `#counters-per-area` (matches heading
"Counters (per area)")
- `fetch.md`: "Known limitation — GitHub search-index lag for
closed-since counts" renamed to "Known limitation"
- `fetch.md`: "Closed / merged triaged PRs (since cutoff)" renamed
to "Closed-merged-triaged PRs"
2. **Some `action` templates aren't paste-clean.**
The recommendation-rules table in `render.md` conflated paste-ready
slash commands with parentheticals, prose, and unicode arrows in a
single "Action template" column — directly contradicting the rule
on the same page that says "the action template must be a literal
slash-command the maintainer can paste". Split into two columns:
`Action` (slash command, or `—` when no command applies) and
`Detail` (prose explanation that goes in the card body). Card
rendering description and SKILL.md schema description updated to
match.
---
.claude/skills/pr-management-stats/SKILL.md | 30 +++++++++---------
.claude/skills/pr-management-stats/aggregate.md | 6 ++--
.claude/skills/pr-management-stats/classify.md | 4 +--
.claude/skills/pr-management-stats/fetch.md | 6 ++--
.claude/skills/pr-management-stats/render.md | 42 +++++++++++++------------
5 files changed, 45 insertions(+), 43 deletions(-)
diff --git a/.claude/skills/pr-management-stats/SKILL.md
b/.claude/skills/pr-management-stats/SKILL.md
index bf500ee..7af81c1 100644
--- a/.claude/skills/pr-management-stats/SKILL.md
+++ b/.claude/skills/pr-management-stats/SKILL.md
@@ -118,7 +118,7 @@ read-only and inherits everything from
`pr-management-triage`'s contract.
**Golden rule 5 — state the input scope up front.** Before rendering, print
one line summarising what the stats cover: repo name, total open PR count,
closed-since cutoff date, and viewer login. The numbers only make sense in
context.
-**Golden rule 6 — recommendations are deterministic, not opinions.** Every
action surfaced in the "What needs attention" panel comes from a fixed rule in
[`render.md#recommendation-rules`](render.md). The skill never editorialises
("queue is doing well", "you should focus on X") — it surfaces the rule's
trigger and the suggested next-step command. The maintainer reads the trigger
and decides; the skill never decides for them. New rules are added by editing
the rules table, not by adding [...]
+**Golden rule 6 — recommendations are deterministic, not opinions.** Every
action surfaced in the "What needs attention" panel comes from a fixed rule in
[`render.md#recommendation-rules`](render.md#recommendation-rules). The skill
never editorialises ("queue is doing well", "you should focus on X") — it
surfaces the rule's trigger and the suggested next-step command. The maintainer
reads the trigger and decides; the skill never decides for them. New rules are
added by editing the rules [...]
**Golden rule 7 — actions link to other skills, never mutate.** Every
recommendation's `action` field is the *exact* slash-command the maintainer can
paste to do the work — almost always `/pr-management-triage`,
`/maintainer-review`, or a focused variant with a label/PR-number filter. The
stats skill itself remains pure-read (Golden rule 1); the dashboard makes
downstream skills *one paste away* from running.
@@ -143,7 +143,7 @@ No per-PR drill-in — this skill is aggregate-only.
1. `gh auth status` must succeed; capture the viewer login (needed for the
triage-marker check in step 2).
2. Run one GraphQL query that asks both for `viewer { login }` and for
`repository(owner, name) { name }` to confirm the repo is reachable.
`viewerPermission` is NOT required (this skill doesn't mutate) — skip the
write-check that `pr-management-triage` does.
-3. Read or initialise the scratch cache at
`/tmp/pr-management-stats-cache-<repo-slug>.json` (see
[`aggregate.md#cache`](aggregate.md)). The cache stores the viewer login and a
map of `pr_number → (head_sha, triage_status)` so a re-run inside the same
session skips the per-PR enrichment.
+3. Read or initialise the scratch cache at
`/tmp/pr-management-stats-cache-<repo-slug>.json` (see
[`aggregate.md#cache`](aggregate.md#cache)). The cache stores the viewer login
and a map of `pr_number → (head_sha, triage_status)` so a re-run inside the
same session skips the per-PR enrichment.
A failure at step 1 is a **stop**. Steps 2 and 3 degrade with warnings.
@@ -151,7 +151,7 @@ A failure at step 1 is a **stop**. Steps 2 and 3 degrade
with warnings.
## Step 1 — Fetch open PRs
-Use the query template in [`fetch.md#open-prs`](fetch.md) to get every open PR
with the fields needed for classification (labels, `isDraft`,
`authorAssociation`, `createdAt`, last commit `committedDate`, last 10 comments
for the triage-marker scan).
+Use the query template in [`fetch.md#open-prs`](fetch.md#open-prs) to get
every open PR with the fields needed for classification (labels, `isDraft`,
`authorAssociation`, `createdAt`, last commit `committedDate`, last 10 comments
for the triage-marker scan).
Paginate until `pageInfo.hasNextPage == false`. Batch size of 50 is safe (the
open-PR selection set is lighter than `pr-management-triage`'s — no
`statusCheckRollup`, no `reviewThreads`, no `latestReviews`). For a 300-PR
backlog that's six GraphQL calls.
@@ -163,7 +163,7 @@ For each open PR, determine:
- `is_triaged_waiting` — viewer's (or any collaborator's) comment contains the
`Pull Request quality criteria` marker, the comment post-dates the PR's last
commit, AND the author has NOT commented after it.
- `is_triaged_responded` — same marker found, but the author HAS commented
after it.
-- `is_drafted_by_triager` — the PR was converted to draft by the viewer at or
after the triage comment (from the `ConvertToDraftEvent` timeline, optional —
see [`classify.md#drafted-by-triager`](classify.md) for the cheaper heuristic).
+- `is_drafted_by_triager` — the PR was converted to draft by the viewer at or
after the triage comment (from the `ConvertToDraftEvent` timeline, optional —
see [`classify.md#drafted-by-triager`](classify.md#drafted-by-triager) for the
cheaper heuristic).
- `last_author_interaction_at` — most recent `commit.committedDate` OR author
comment `createdAt`, whichever is later.
Cache these per `(pr_number, head_sha)` so a subsequent run skips the scan.
@@ -172,7 +172,7 @@ Cache these per `(pr_number, head_sha)` so a subsequent run
skips the scan.
## Step 3 — Fetch closed / merged triaged PRs since cutoff
-The second table is a separate search. Fetch closed or merged PRs whose
comment history contains the triage marker since the configured cutoff date.
Use the template in [`fetch.md#closed-merged-triaged`](fetch.md).
+The second table is a separate search. Fetch closed or merged PRs whose
comment history contains the triage marker since the configured cutoff date.
Use the template in
[`fetch.md#closed-merged-triaged-prs`](fetch.md#closed-merged-triaged-prs).
Cutoff defaults to `today - 6 weeks`. The cutoff should be configurable
because a maintainer asking "how did last week's sweep do" wants
`since:today-7d`, while a monthly report wants `since:today-30d`.
@@ -182,7 +182,7 @@ Cutoff defaults to `today - 6 weeks`. The cutoff should be
configurable because
Group each PR by every `area:*` label it carries. A PR with `area:UI` and
`area:scheduler` contributes to both groups. A PR with no `area:*` labels lands
in a pseudo-area `(no area)`.
-Per area, compute the counters in [`aggregate.md#counters`](aggregate.md):
total, drafts, non-drafts, contributors, triaged-waiting, triaged-responded,
ready-for-review, drafted-by-triager, plus age-bucket histograms.
+Per area, compute the counters in
[`aggregate.md#counters-per-area`](aggregate.md#counters-per-area): total,
drafts, non-drafts, contributors, triaged-waiting, triaged-responded,
ready-for-review, drafted-by-triager, plus age-bucket histograms.
Also compute a `TOTAL` row where each PR is counted exactly once (NOT the sum
of per-area counters — PRs with multiple `area:*` labels would double-count).
@@ -192,8 +192,8 @@ Also compute a `TOTAL` row where each PR is counted exactly
once (NOT the sum of
Pure function of the classified open-PR set. No network.
-1. Apply the **health rating** thresholds from
[`aggregate.md#health-rating`](aggregate.md): each fired threshold is a "issue
point". Map total points → `✅ Healthy` / `⚠️ Needs attention` / `🔥 Action
needed`.
-2. Walk the **recommendation rules** from
[`render.md#recommendation-rules`](render.md) in declared order. Each rule that
fires produces one entry with `priority`, `icon`, `title`, `detail`, `action`
(the exact slash command), and a count.
+1. Apply the **health rating** thresholds from
[`aggregate.md#health-rating`](aggregate.md#health-rating): each fired
threshold is a "issue point". Map total points → `✅ Healthy` / `⚠️ Needs
attention` / `🔥 Action needed`.
+2. Walk the **recommendation rules** from
[`render.md#recommendation-rules`](render.md#recommendation-rules) in declared
order. Each rule that fires produces one entry with `priority`, `icon`,
`title`, `detail`, `action` (an exact slash command, or `—` when no paste-clean
command applies), and a count. `action` and `detail` are kept in separate
columns so prose / parentheticals stay out of the slash command.
3. The recommendation list is the input to the dashboard's "What needs
attention" panel. If zero rules fire, surface the explicit "no urgent actions
detected" panel — never leave the section empty.
---
@@ -204,7 +204,7 @@ Pure function of the closed/merged-since-cutoff PR set.
For each of the last 6 weeks (rolling, anchored on the fetch-start `<now>`),
bucket PRs by `closedAt` and count `merged` and `closed` separately. Also count
the triaged-then-merged / triaged-then-closed / triaged-then-responded subsets
— those are what feed the trend mini-stats below the velocity bars.
-See [`aggregate.md#weekly-velocity`](aggregate.md) for the exact bucket
boundaries and the avg/peak summary computation.
+See [`aggregate.md#weekly-velocity`](aggregate.md#weekly-velocity) for the
exact bucket boundaries and the avg/peak summary computation.
---
@@ -218,17 +218,17 @@ For each of the same six rolling weekly windows, compute:
- `closed_total` — PR was closed/merged in the window (reuses the velocity
buckets from Step 5b)
- `net_delta = opened - closed_total`
-These per-week numbers feed the dashboard's "Opened vs closed momentum" line
chart and the two-line "Net delta" summary below it. See
[`aggregate.md#opened-vs-closed-weekly-buckets`](aggregate.md) for the exact
spec.
+These per-week numbers feed the dashboard's "Opened vs closed momentum" line
chart and the two-line "Net delta" summary below it. See
[`aggregate.md#opened-vs-closed-weekly-buckets`](aggregate.md#opened-vs-closed-weekly-buckets)
for the exact spec.
---
## Step 5d — Compute ready-for-review trend by top areas
-Needs one extra fetch (per [`fetch.md#ready-label-timeline`](fetch.md)): for
each currently-`ready for maintainer review` PR, the timestamp of its most
recent `LabeledEvent` adding that label. Aliased GraphQL, ~30 PRs per call.
+Needs one extra fetch (per
[`fetch.md#ready-label-timeline`](fetch.md#ready-label-timeline)): for each
currently-`ready for maintainer review` PR, the timestamp of its most recent
`LabeledEvent` adding that label. Aliased GraphQL, ~30 PRs per call.
Then for each top-pressure area (top 5 by Step 5f's score, filtered to areas
with ≥ 3 currently-ready PRs), compute a 6-bucket cumulative count:
`ready_count[a][w] = count of currently-ready PRs in area a where labeled_at <=
w.end`.
-Feeds the dashboard's "Ready-for-review trend" multi-line chart. See
[`aggregate.md#ready-for-review-trend-by-top-areas`](aggregate.md) for the
exact spec and rendering rules.
+Feeds the dashboard's "Ready-for-review trend" multi-line chart. See
[`aggregate.md#ready-for-review-trend-by-top-areas`](aggregate.md#ready-for-review-trend-by-top-areas)
for the exact spec and rendering rules.
---
@@ -238,7 +238,7 @@ Pure function of the closed/merged-since-cutoff PR set
(Step 3) — reuses the e
For each weekly bucket, classify each closed PR into exactly one of four
categories: `merged`, `closed-after-responded`,
`closed-after-triage-no-response`, `closed-no-triage`. Sum per category per
week.
-Feeds the dashboard's "Closed-by-triage-reason per week" stacked bar chart.
See [`aggregate.md#closed-by-triage-reason-per-week`](aggregate.md) for the
category definitions, colour map, and summary line.
+Feeds the dashboard's "Closed-by-triage-reason per week" stacked bar chart.
See
[`aggregate.md#closed-by-triage-reason-per-week`](aggregate.md#closed-by-triage-reason-per-week)
for the category definitions, colour map, and summary line.
---
@@ -246,7 +246,7 @@ Feeds the dashboard's "Closed-by-triage-reason per week"
stacked bar chart. See
Pure function of the classified open-PR set.
-Per area, compute a **pressure score** = weighted sum of urgent PR conditions.
The weights are defined in [`aggregate.md#pressure-score`](aggregate.md):
+Per area, compute a **pressure score** = weighted sum of urgent PR conditions.
The weights are defined in
[`aggregate.md#pressure-score`](aggregate.md#pressure-score):
- untriaged non-draft, > 4 weeks old → 5 pts
- untriaged non-draft, 1–4 weeks old → 3 pts
@@ -261,7 +261,7 @@ Sort areas by score descending; render the top 8 (filtering
areas with < 3 contr
## Step 6 — Render dashboard
-Render the maintainer dashboard per the layout in
[`render.md#dashboard-layout`](render.md):
+Render the maintainer dashboard per the layout in
[`render.md#dashboard-layout`](render.md#dashboard-layout):
1. **Context line** — repo, open count, cutoff, viewer, timestamp.
2. **Hero cards (4)** — health rating, total open, ready count,
untriaged-non-draft count.
diff --git a/.claude/skills/pr-management-stats/aggregate.md
b/.claude/skills/pr-management-stats/aggregate.md
index 3d4aafc..1f729d2 100644
--- a/.claude/skills/pr-management-stats/aggregate.md
+++ b/.claude/skills/pr-management-stats/aggregate.md
@@ -126,7 +126,7 @@ The score is also used to bucket the area into a severity
colour for the dashboa
| 15–29 | medium | amber |
| < 15 | low | grey |
-Tune the weights here in lockstep with the recommendation rules in
[`render.md#recommendation-rules`](render.md) — they share the same notion of
"what's urgent" and drift between the two would produce contradictory dashboard
sections.
+Tune the weights here in lockstep with the recommendation rules in
[`render.md#recommendation-rules`](render.md#recommendation-rules) — they share
the same notion of "what's urgent" and drift between the two would produce
contradictory dashboard sections.
---
@@ -149,7 +149,7 @@ Per window, count three metrics:
|---|---|
| `merged` | PR has `merged == true` AND `closedAt` falls in the window |
| `closed` | PR has `merged == false` AND `closedAt` falls in the window |
-| `triaged_then_responded` | PR is triaged (per
[`classify.md#triage-marker`](classify.md)) AND `closedAt` falls in the window
AND the author commented after the triage comment |
+| `triaged_then_responded` | PR is triaged (per
[`classify.md#triage-marker`](classify.md#triage-marker)) AND `closedAt` falls
in the window AND the author commented after the triage comment |
Render the bars oldest → newest (so the eye sweeps left-to-right matching
natural time order). Each bar is a stacked `merged` (green) + `closed` (grey)
segment, normalised to the maximum total in the 6-week window.
@@ -198,7 +198,7 @@ Net alone hides activity. A week with 100 opened and 100
closed has the same net
The dashboard's "Ready-for-review trend" panel shows the cumulative count of
currently-`ready for maintainer review` PRs over the last 6 weeks, broken down
by the top-N highest-pressure areas (default N = 5; areas with fewer than 3
currently-ready PRs are excluded as noise).
-For each PR currently carrying the label, the **labeled-at timestamp** is the
`createdAt` of the most recent `LabeledEvent` where `label.name == "ready for
maintainer review"` from the PR's timeline (see
[`fetch.md#ready-label-timeline`](fetch.md)).
+For each PR currently carrying the label, the **labeled-at timestamp** is the
`createdAt` of the most recent `LabeledEvent` where `label.name == "ready for
maintainer review"` from the PR's timeline (see
[`fetch.md#ready-label-timeline`](fetch.md#ready-label-timeline)).
For each top area `a` and each weekly bucket `w` in `0..5`:
diff --git a/.claude/skills/pr-management-stats/classify.md
b/.claude/skills/pr-management-stats/classify.md
index 556b7b9..20361f4 100644
--- a/.claude/skills/pr-management-stats/classify.md
+++ b/.claude/skills/pr-management-stats/classify.md
@@ -150,7 +150,7 @@ Count the PR as responded if it has the marker AND an
author comment between tri
## Pressure weight
-Per-PR helper used by [`aggregate.md#pressure-score`](aggregate.md). Returns
the integer weight a single contributor PR contributes to its area's pressure
score. Pure function of fields already populated above; no extra fetches.
+Per-PR helper used by
[`aggregate.md#pressure-score`](aggregate.md#pressure-score). Returns the
integer weight a single contributor PR contributes to its area's pressure
score. Pure function of fields already populated above; no extra fetches.
```text
def pressure_weight(pr) -> int:
@@ -169,7 +169,7 @@ def pressure_weight(pr) -> int:
return 1
```text
-The first-match-wins ordering matters: a ready-for-review PR that's also a
stale triaged draft scores 1 (ready takes precedence — once it has the label,
the maintainer is the gate, not the author). Keep this function in lockstep
with the table in [`aggregate.md#pressure-score`](aggregate.md).
+The first-match-wins ordering matters: a ready-for-review PR that's also a
stale triaged draft scores 1 (ready takes precedence — once it has the label,
the maintainer is the gate, not the author). Keep this function in lockstep
with the table in [`aggregate.md#pressure-score`](aggregate.md#pressure-score).
---
diff --git a/.claude/skills/pr-management-stats/fetch.md
b/.claude/skills/pr-management-stats/fetch.md
index bbe49d9..bc727aa 100644
--- a/.claude/skills/pr-management-stats/fetch.md
+++ b/.claude/skills/pr-management-stats/fetch.md
@@ -77,7 +77,7 @@ gh api graphql \
---
-## Closed / merged triaged PRs (since cutoff)
+## Closed-merged-triaged PRs
Table 1 needs PRs that were closed or merged since the cutoff date AND had a
triage comment posted at some point in their lifetime. Use GitHub's search with
an `-is:open` filter plus a `closed:>=<cutoff>` date predicate, then
client-side scan the `comments` subfield for the `Pull Request quality
criteria` marker.
@@ -145,7 +145,7 @@ In this case the visible body contains no "Pull Request
quality criteria" text a
Raw bodies are slightly noisier (Markdown formatting characters) but the
marker string is distinctive enough that false positives are not a concern on
`<upstream>`.
-### Known limitation — GitHub search-index lag for closed-since counts
+### Known limitation
Two GitHub-search behaviours conspire to make Table 1 hard to get right:
@@ -286,7 +286,7 @@ sequence (e.g. `\z`, `\e`), even `strict=False` fails with
## Ready-label timeline
-Needed for the dashboard's "Ready-for-review trend" chart (see
[`aggregate.md#ready-for-review-trend-by-top-areas`](aggregate.md)). The
open-PRs query above tells us *which* PRs currently carry the `ready for
maintainer review` label, but not *when* the label was added. Without that
timestamp the trend chart can't show growth.
+Needed for the dashboard's "Ready-for-review trend" chart (see
[`aggregate.md#ready-for-review-trend-by-top-areas`](aggregate.md#ready-for-review-trend-by-top-areas)).
The open-PRs query above tells us *which* PRs currently carry the `ready for
maintainer review` label, but not *when* the label was added. Without that
timestamp the trend chart can't show growth.
Run an aliased GraphQL query, **30 PRs per call**, that fetches each PR's
`LabeledEvent` timeline filtered to the relevant label:
diff --git a/.claude/skills/pr-management-stats/render.md
b/.claude/skills/pr-management-stats/render.md
index e9981fb..3a5ecb2 100644
--- a/.claude/skills/pr-management-stats/render.md
+++ b/.claude/skills/pr-management-stats/render.md
@@ -39,7 +39,7 @@ Four equally-sized cards, each one big number with a
sub-label:
| Card | Big number | Sub-label | Colour rule |
|---|---|---|---|
-| **Repo Health** | the rating label (`✅ Healthy` / `⚠️ Needs attention` / `🔥
Action needed`) | "based on triage backlog + queue size" | green / amber / red,
per [`aggregate.md#health-rating`](aggregate.md) |
+| **Repo Health** | the rating label (`✅ Healthy` / `⚠️ Needs attention` / `🔥
Action needed`) | "based on triage backlog + queue size" | green / amber / red,
per [`aggregate.md#health-rating`](aggregate.md#health-rating) |
| **Open PRs (non-bot)** | total open count | `<contrib_count> from
contributors · <collab_count> collaborator-authored` | blue (informational) |
| **Ready for review** | `len(ready_open)` | `<pct>% of contributor queue` |
green |
| **Untriaged non-drafts** | `len(untriaged_nondraft)` | `<X> are >4 weeks
old` | red if >0 are >4w, amber if total > 30, green otherwise |
@@ -48,7 +48,7 @@ Card layout is responsive: 4-column on wide screens, 2-column
on narrow, 1-colum
### 3. What needs attention (action panel)
-A vertical list of action cards built from the recommendation rules in
[`#recommendation-rules`](#recommendation-rules) below. Each card has a
coloured left border (red = high, amber = medium, grey = low), an icon, a
one-line title, a 1–2-line detail explanation, and a monospace `code` block
holding the exact slash-command the maintainer can paste.
+A vertical list of action cards built from the recommendation rules in
[`#recommendation-rules`](#recommendation-rules) below. Each card has a
coloured left border (red = high, amber = medium, grey = low), an icon, a
one-line title, a 1–2-line detail explanation, and (when applicable) a
monospace `code` block holding the exact slash-command the maintainer can
paste. When a rule's `action` is `—` (no paste-clean command applies), the card
omits the code block and shows title + detail only.
If zero rules fire, render a single low-priority card with a `✨` icon and the
body "No urgent actions detected. Queue is in healthy shape — periodic
/pr-management-triage when convenient." Never leave the section visually empty.
@@ -99,7 +99,7 @@ Title: **Ready-for-review trend (last 6 weeks, top areas)**
An inline SVG line chart with one line per top-pressure area (default: top 5,
filtered to areas with ≥ 3 currently-ready PRs). Each line is **cumulative**:
at week W it shows the count of PRs that *currently* carry the `ready for
maintainer review` label and were labelled on or before W.
-Each area's line uses its pressure-band colour (red, amber, or grey per
[`aggregate.md#pressure-score`](aggregate.md)). Same SVG dimensions as the
opened-vs-closed chart (720×220px). The chart legend in the top-left lists each
area name with its line colour swatch.
+Each area's line uses its pressure-band colour (red, amber, or grey per
[`aggregate.md#pressure-score`](aggregate.md#pressure-score)). Same SVG
dimensions as the opened-vs-closed chart (720×220px). The chart legend in the
top-left lists each area name with its line colour swatch.
Below the chart, a per-area summary list:
@@ -138,7 +138,7 @@ A healthy week is mostly green with thin amber/red
segments. A red-dominated wee
Title: **Pressure by area** (with a sub-line *"Pressure score = weighted sum
of untriaged-old PRs per area. Higher score = more maintainer attention
needed."*)
-Up to 8 rows, sorted by pressure score descending (filtering areas with < 3
contributor PRs). Each row is a horizontally-laid-out card with a coloured left
border (red / amber / grey, severity bands per
[`aggregate.md#pressure-score`](aggregate.md)) containing:
+Up to 8 rows, sorted by pressure score descending (filtering areas with < 3
contributor PRs). Each row is a horizontally-laid-out card with a coloured left
border (red / amber / grey, severity bands per
[`aggregate.md#pressure-score`](aggregate.md#pressure-score)) containing:
- area name (cyan, bold, e.g. `providers`)
- one-line stat: `<contrib_total> contributor PRs · <red>untriaged_4w</red>
>4w · <amber>untriaged_1_4w</amber> 1-4w · <grey>untriaged_recent</grey> recent
· <green>ready_pending</green> ready for review`
@@ -179,26 +179,28 @@ A bordered panel at the bottom explaining all the
colours, columns, and computed
The "What needs attention" panel is built from this fixed rule set, evaluated
in order. Each rule that fires produces one entry in the panel.
-| # | Trigger | Priority | Icon | Title template | Action template |
-|---|---|---|---|---|---|
-| 1 | `len(untriaged_old) > 0` (any contributor non-draft >4w) | high | 🔥 |
`Triage <N> non-draft contributor PRs older than 4 weeks` |
`/pr-management-triage all PR issues (focus on >4w bucket)` |
-| 2 | `len(untriaged_old) == 0 AND len(untriaged_med) > 0` (1-4w bucket
non-empty) | medium | 👀 | `Triage <N> non-draft PRs aged 1-4 weeks` |
`/pr-management-triage all PR issues` |
-| 3 | `len(stale_triaged_drafts) > 0` (drafts triaged ≥ 7d ago, no reply) |
medium | 🗑️ | `Close <N> stale-triaged drafts (≥7d, no response)` |
`/pr-management-triage stale → sweep 1a` |
-| 4 | `len(ready_open) >= 50` | high | 📥 | `<N> PRs labeled "ready for
maintainer review"` | `/maintainer-review (filter: ready for maintainer
review)` |
-| 5 | `20 <= len(ready_open) < 50` | medium | 📥 | `<N> PRs in "ready for
maintainer review" queue` | same as rule 4 |
-| 6 | `len(responded_no_ready) > 0` (triaged + responded but not
ready-for-review) | medium | 🔄 | `<N> triaged PRs have author responses
awaiting re-triage` | `/pr-management-triage all PR issues (will surface as
mark-ready-with-ping)` |
-| 7 | top area's `untriaged_4w + untriaged_1_4w >= 5` | medium | 📍 | `Area
"<area>" has <total> contributor PRs (<X> untriaged >4w)` |
`/pr-management-triage label:area:<area>` |
-| 8 | `velocity_drop > 30` (last_wk total - this_wk total) | low | 📉 | `PR
closure velocity dropped <N> this week` | `No immediate action — check next
week.` |
-| 9 | top ready-trend area's growth in last 7d ≥ 10 PRs | low | 📈 |
`Ready-for-review queue in "<area>" grew by <N> this week` |
`/maintainer-review label:area:<area>` |
-| 10 | weekly closed-by-reason `closed_no_response > merged` for 2+ recent
weeks | medium | 🧹 | `Stale-sweep is dominating closures (last 2 weeks: <N>
sweep-close vs <M> merged)` | review `/pr-management-triage stale` cadence —
too many PRs reaching the sweep |
+`Action` and `Detail` are separate columns by design: `Action` is a literal
paste-clean slash-command the maintainer runs (or `—` when no command applies);
`Detail` is the prose explanation that goes in the card body. Mixing prose into
`Action` would make the slash-command non-paste-clean and re-introduce the
editorialising the skill is supposed to avoid.
+
+| # | Trigger | Priority | Icon | Title template | Detail template | Action |
+|---|---|---|---|---|---|---|
+| 1 | `len(untriaged_old) > 0` (any contributor non-draft >4w) | high | 🔥 |
`Triage <N> non-draft contributor PRs older than 4 weeks` | Focus on the >4w
bucket — those are the ones rotting longest. | `/pr-management-triage all PR
issues` |
+| 2 | `len(untriaged_old) == 0 AND len(untriaged_med) > 0` (1-4w bucket
non-empty) | medium | 👀 | `Triage <N> non-draft PRs aged 1-4 weeks` | The 1–4w
bucket is the queue's leading edge; staying on top of it stops PRs from rolling
into >4w. | `/pr-management-triage all PR issues` |
+| 3 | `len(stale_triaged_drafts) > 0` (drafts triaged ≥ 7d ago, no reply) |
medium | 🗑️ | `Close <N> stale-triaged drafts (≥7d, no response)` | Closure
path lives under the `stale` flow (sweep step 1a). | `/pr-management-triage
stale` |
+| 4 | `len(ready_open) >= 50` | high | 📥 | `<N> PRs labeled "ready for
maintainer review"` | The `ready for maintainer review` queue is past the
triage stage; it needs maintainer review attention, not triage. |
`/pr-management-code-review ready` |
+| 5 | `20 <= len(ready_open) < 50` | medium | 📥 | `<N> PRs in "ready for
maintainer review" queue` | Same trigger family as rule 4 — banded by queue
size so the priority drops once the queue is comfortable. |
`/pr-management-code-review ready` |
+| 6 | `len(responded_no_ready) > 0` (triaged + responded but not
ready-for-review) | medium | 🔄 | `<N> triaged PRs have author responses
awaiting re-triage` | These will surface as mark-ready-with-ping inside the
regular triage sweep. | `/pr-management-triage all PR issues` |
+| 7 | top area's `untriaged_4w + untriaged_1_4w >= 5` | medium | 📍 | `Area
"<area>" has <total> contributor PRs (<X> untriaged >4w)` | One area is
dominating the untriaged queue; scoping a triage pass to it clears the bulk of
the load. | `/pr-management-triage label:area:<area>` |
+| 8 | `velocity_drop > 30` (last_wk total - this_wk total) | low | 📉 | `PR
closure velocity dropped <N> this week` | No immediate action — re-check next
week to see if the drop persists or was a one-off. | — |
+| 9 | top ready-trend area's growth in last 7d ≥ 10 PRs | low | 📈 |
`Ready-for-review queue in "<area>" grew by <N> this week` | Growth
concentrated in one area suggests it'd benefit from a focused review pass. |
`/pr-management-code-review label:area:<area>` |
+| 10 | weekly closed-by-reason `closed_no_response > merged` for 2+ recent
weeks | medium | 🧹 | `Stale-sweep is dominating closures (last 2 weeks: <N>
sweep-close vs <M> merged)` | Too many PRs are reaching the stale sweep —
review the `/pr-management-triage stale` cadence and whether earlier-stage
interventions (mark-ready, ping) are firing. | — |
Rules 1 and 2 are **mutually exclusive** (only one fires depending on whether
any >4w PRs exist). Rules 4 and 5 are **mutually exclusive** (banding on
`ready_open` count). All other rules can fire independently.
When adding a new rule:
- Prefer count-based triggers over percentage-based ones (counts are easier to
reason about for the maintainer).
-- The action template must be a literal slash-command the maintainer can paste
— never instructions like "consider running …".
-- Detail string should explain *why* the rule fired in one sentence, plus what
command will help.
+- The `Action` cell must be a literal slash-command the maintainer can paste,
with no parentheticals, prose, or unicode arrows. If the rule has no
paste-clean command (e.g. "wait and re-check"), set `Action` to `—` and put the
explanation in `Detail`.
+- The `Detail` cell explains *why* the rule fired and any context the
maintainer needs to act on it; this is where parentheticals and
cross-references live.
---
@@ -264,7 +266,7 @@ Plain text, before the first hero card:
Structure: `<repo> — <open_count> open PRs (non-bot) · closed/merged since
<cutoff> · viewer @<login> · <now>`.
-If the closed-since counts came from the lagging search index (see
[`fetch.md#known-limitation`](fetch.md)), prepend a one-line caveat before the
hero cards:
+If the closed-since counts came from the lagging search index (see
[`fetch.md#known-limitation`](fetch.md#known-limitation)), prepend a one-line
caveat before the hero cards:
```text
⚠ Closed-PR table built from GitHub's free-text search of the quality-criteria
marker. The index lags — older triaged+merged PRs are likely undercounted. Pass
accurate-closed for the hybrid REST + GraphQL path.
@@ -297,7 +299,7 @@ Lives inside the second collapsed `<details>` block. Title:
`Triaged PRs — Sti
One row per area where `total > 0`, sorted by `total` descending. `(no area)`
last. Append a bold **TOTAL** row.
-`Total` is a **reference-only** column — it counts every open PR in the area
(collaborator + contributor alike). Every other numeric column is
**contributor-only** (see [`aggregate.md#counters`](aggregate.md)). This keeps
draft-rate, triage-rate, and response-rate percentages meaningful: collaborator
PRs bypass the triage funnel, so including them in the denominators would
systematically understate how much of the contributor queue is ready, drafted,
responded, etc.
+`Total` is a **reference-only** column — it counts every open PR in the area
(collaborator + contributor alike). Every other numeric column is
**contributor-only** (see
[`aggregate.md#counters-per-area`](aggregate.md#counters-per-area)). This keeps
draft-rate, triage-rate, and response-rate percentages meaningful: collaborator
PRs bypass the triage funnel, so including them in the denominators would
systematically understate how much of the contributor queue is ready, drafted,
responded, etc.
| Column | Source | Denominator | Colour |
|---|---|---|---|