Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .claude/skills/pr-management-stats/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ read-only and inherits everything from `pr-management-triage`'s contract.

**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`, `/pr-management-code-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.

**Golden rule 8 β€” render ALL sections, never silently skip.** The dashboard layout in [`render.md`](render.md) declares 11 sections (Title context, Hero cards, Recommendations, Trends-over-time line charts, Closure velocity, Opened-vs-closed momentum, Ready-for-review trend by top areas, Closed-by-triage-reason, Pressure by area, CODEOWNERS responsibility, Triage funnel, Triager activity, Detailed tables, Legend). The agent MUST render every section. If a section's data is genuinely unavailable (e.g. no `.github/CODEOWNERS` present), render a stub with a one-line explanation of why β€” never omit a section silently. A "compact" rendering that drops line charts or the CODEOWNERS table is **not** an acceptable simplification β€” the maintainer asked for the dashboard, the dashboard is the full set of panels. The reference implementation in [`tools/pr-management-stats/reference.py`](../../../tools/pr-management-stats/reference.py) encodes the canonical fetch + classify contract; the agent's render MUST be consistent with what that script produces.

**Golden rule 9 β€” `is_engaged` requires the FULL engagement schema.** The open-PRs GraphQL query MUST include `reviewThreads`, `latestReviews`, and `timelineItems` (for `LABELED_EVENT`/`READY_FOR_REVIEW_EVENT`/`CONVERT_TO_DRAFT_EVENT`). The `is_engaged` predicate in [`classify.md`](classify.md) counts ALL of these as maintainer engagement; omitting any of them under-counts engagement and over-counts untriaged β€” concretely, a maintainer who left only a line-level review comment (no submitted review, no issue comment) would otherwise show as "no engagement" and that PR would be misclassified as untriaged. The implication: don't trim the open-PRs query to "save complexity points" β€” the missing fields are load-bearing. (Earlier iterations of [`fetch.md`](fetch.md) suggested `reviewThreads` was not needed for stats; that was a spec bug and has been corrected.)

---

## Inputs
Expand Down
70 changes: 65 additions & 5 deletions .claude/skills/pr-management-stats/fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,47 @@ query(
}
}
}
comments(last: 10) {
comments(last: 25) {
nodes {
author { login }
authorAssociation
createdAt
body
}
}
latestReviews(last: 10) {
nodes { author { login } state submittedAt }
}
reviewThreads(first: 30) {
nodes {
isResolved
comments(first: 3) {
nodes { author { login } authorAssociation createdAt body }
}
}
}
timelineItems(last: 50, itemTypes: [LABELED_EVENT, READY_FOR_REVIEW_EVENT, CONVERT_TO_DRAFT_EVENT]) {
nodes {
... on LabeledEvent { createdAt actor { login } label { name } }
... on ReadyForReviewEvent { createdAt actor { login } }
... on ConvertToDraftEvent { createdAt actor { login } }
}
}
}
}
}
}
```text

**These engagement fields are not optional.** `latestReviews`,
`reviewThreads`, and `timelineItems` are required by the `is_engaged`
predicate in [`classify.md`](classify.md). Dropping any of them
under-counts engagement and over-counts untriaged PRs β€” see
[Why no `statusCheckRollup` / `mergeable` β€” and why `reviewThreads`
IS required](#why-no-statuscheckrollup--mergeable--and-why-reviewthreads-is-required)
below for the rationale. `comments(last: N)` uses **25** here (not 10)
so the marker scan reliably finds the QC-marker comment on chatty PRs.

### `searchQuery`

```text
Expand All @@ -73,7 +100,12 @@ gh api graphql \

### Batch size

50 is the default. Empirically the open-PR selection set (no rollup, no review threads) stays well under GraphQL's complexity ceiling at 50. If a rare response returns `"errors": [{"type": "MAX_NODE_LIMIT_EXCEEDED", ...}]`, drop to 25 and retry β€” but that's a fallback, not a default.
**30** is the default. The open-PR selection set now includes the four
engagement signals (`comments(last:25)`, `latestReviews`, `reviewThreads`,
`timelineItems`) the `is_engaged` predicate needs β€” that costs ~11
complexity points per 30 PRs, well under the budget. Empirically `50`
also works but is borderline; if a response returns
`"errors": [{"type": "MAX_NODE_LIMIT_EXCEEDED", ...}]`, drop to 20.

---

Expand Down Expand Up @@ -359,9 +391,37 @@ Parse line-by-line: a non-comment, non-blank line is `<pattern> <owner1> <owner2

---

## Why no `statusCheckRollup` / `mergeable` / `reviewThreads`

`pr-management-triage` needs all three for classification; `pr-management-stats` does not. Dropping them keeps the query complexity well below GitHub's per-page ceiling, which is how we can safely run `batchSize=50` here versus `20` in `pr-management-triage`. If a future stats column ever needs one of those fields, raise only that query's complexity β€” don't pull them into the default shape "just in case".
## Why no `statusCheckRollup` / `mergeable` β€” and why `reviewThreads` IS required

`statusCheckRollup` and `mergeable` are not needed for stats β€” those drive
per-PR classification in `pr-management-triage` but aggregate counts don't use
them. Dropping them keeps the query lighter, which is how we can run a larger
`batchSize` here (30–50) than in `pr-management-triage` (20).

`reviewThreads`, `latestReviews`, and `timelineItems` (for
`LABELED_EVENT`/`READY_FOR_REVIEW_EVENT`/`CONVERT_TO_DRAFT_EVENT`), **on the
other hand, ARE required** for stats β€” the `is_engaged` predicate in
[`classify.md`](classify.md) counts maintainer engagement across:

- issue comments (`comments`) β€” already included
- submitted reviews (`latestReviews`) β€” required
- line-level review comments (`reviewThreads`) β€” required
- label adds + draft conversions by maintainers (`timelineItems`) β€” required

A maintainer who left only a line-level review comment (no issue comment, no
submitted review) would otherwise look like "no engagement" and the PR would
be misclassified as untriaged. On a large queue the under-count is material β€”
on `<upstream>` (~530 open PRs at the time of writing) it inflates the untriaged count by ~10Γ—.

(An earlier iteration of this doc claimed `reviewThreads` was not needed for
stats; that was a documentation bug. The current schema in the OPEN_PRS_QUERY
template above includes all four engagement signals. The same fix is encoded
in the reference implementation at
[`tools/pr-management-stats/reference.py`](../../../tools/pr-management-stats/reference.py).)

If a future stats column ever needs `statusCheckRollup` or `mergeable`, raise
only that query's complexity β€” don't pull them into the default shape "just
in case".

---

Expand Down
31 changes: 18 additions & 13 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,33 @@
# specific language governing permissions and limitations
# under the License.
#
# Every ecosystem update below uses a 7-day cooldown (all four
# semver buckets set to 7) so a just-released version has a week to
# settle (retags, withdrawals, upstream incident reports) before
# Dependabot proposes bumping to it. This mirrors the same window
# applied locally via `[tool.uv] exclude-newer = "7 days"` in the
# root pyproject.toml and `exclude-newer-span = "P7D"` baked into
# every tool's uv.lock.
# Every ecosystem update below uses a 7-day cooldown so a just-
# released version has a week to settle (retags, withdrawals,
# upstream incident reports) before Dependabot proposes bumping to
# it. This mirrors the same window applied locally via
# `[tool.uv] exclude-newer = "7 days"` in the root pyproject.toml
# and `exclude-newer-span = "P7D"` baked into every tool's uv.lock.
#
# `uv` ecosystems use the full cooldown form (default-days plus the
# four semver-* buckets). `github-actions` and `pre-commit` only
# support `default-days` β€” adding the semver-* keys there causes
# dependabot to reject the whole config (see comment on the
# github-actions block).
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
# github-actions and pre-commit ecosystems do not support the
# semver-{major,minor,patch}-days cooldown keys β€” dependabot
# rejects the whole config block when they are present, which
# is why this ecosystem produced zero PRs between adoption on
# 2026-04-29 and the fix on 2026-05-25. Only `default-days` is
# honoured here.
cooldown:
default-days: 7
semver-major-days: 7
semver-minor-days: 7
semver-patch-days: 7
groups:
github-actions:
patterns:
Expand All @@ -45,9 +53,6 @@ updates:
interval: "weekly"
cooldown:
default-days: 7
semver-major-days: 7
semver-minor-days: 7
semver-patch-days: 7
groups:
pre-commit-hooks:
patterns:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: ${{ matrix.language }}
# Neither the Python tools (stdlib-only / single OAuth dep, no
Expand All @@ -68,6 +68,6 @@ jobs:
queries: security-and-quality

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:${{ matrix.language }}"
2 changes: 1 addition & 1 deletion .github/workflows/link-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
# Restore the lychee result cache so external URL checks reuse
# results across runs (config sets `max_cache_age = "7d"`).
- name: Restore lychee cache
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: .lycheecache
key: cache-lychee-${{ github.sha }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
# - the `uv tool install prek` step below.
# Minimum uv version is pinned in the root `pyproject.toml`
# (`[tool.uv] required-version`).
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
# Install prek via uv (rather than via the `j178/prek-action`
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sandbox-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
# `--project` (not `--directory`) so the linter runs from the
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ jobs:
# uv brings its own Python and reads each project's
# `pyproject.toml` + `uv.lock`. Minimum uv version is enforced
# by the root `pyproject.toml`'s `[tool.uv] required-version`.
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true
- name: Run pytest
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
with:
persist-credentials: false
- name: "Run zizmor"
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
with:
advanced-security: false
config: .zizmor.yml
94 changes: 94 additions & 0 deletions tools/pr-management-stats/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [pr-management-stats reference implementation](#pr-management-stats-reference-implementation)
- [Layout](#layout)
- [Invocation](#invocation)
- [Contract for the agent](#contract-for-the-agent)
- [Parity implementations](#parity-implementations)
- [Cross-references](#cross-references)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->

# pr-management-stats reference implementation

Deterministic reference implementation of the data-fetch +
classification contract that backs the
[`pr-management-stats`](../../.claude/skills/pr-management-stats/SKILL.md) skill.

The skill's agent-emitted render is the **default** β€” this script
exists for two reasons:

1. **Anti-skip insurance.** Agents under context pressure can be
tempted to omit panels from the dashboard (line charts, CODEOWNERS
table, triager-activity table). The skill specifies all 11 panels;
agents must render them all. This script encodes the canonical
data-fetch shape so the agent has a single source of truth to read
the fields from β€” there is no "the skill says X, the agent guessed
Y" drift.

2. **CI-renderable artefact.** Adopters who want a daily dashboard
rendered by CI (rather than an interactive agent session) can run
this script on a schedule, extend it with the full render per
[`render.md`](../../.claude/skills/pr-management-stats/render.md),
and publish the HTML as a build artefact or gist.

## Layout

```text
tools/pr-management-stats/
β”œβ”€β”€ README.md (this file)
└── reference.py (Python implementation: fetch + classify + emit intermediates)
```

## Invocation

```bash
python3 tools/pr-management-stats/reference.py \
--repo <upstream> \
--viewer <maintainer-handle> \
--since 2026-04-12 \
--out /tmp/dashboard.html
```

The script:

1. Fetches all open PRs with the **full engagement schema**
(`comments`, `latestReviews`, `reviewThreads`, `timelineItems`
with `LABELED_EVENT`/`READY_FOR_REVIEW_EVENT`/`CONVERT_TO_DRAFT_EVENT`).
2. Fetches closed/merged PRs since the cutoff.
3. Fetches `.github/CODEOWNERS` + changed-file paths for each
currently-ready PR.
4. Classifies each PR per
[`classify.md`](../../.claude/skills/pr-management-stats/classify.md) β€”
`is_engaged` requires ANY maintainer touch (issue comment, review,
review-thread comment, label add, draft conversion).
5. Writes a JSON sidecar with all the counts that feed the dashboard.

## Contract for the agent

When the agent invokes the skill, it MUST:

- Use the GraphQL templates from [`fetch.md`](../../.claude/skills/pr-management-stats/fetch.md) verbatim. **In particular, the open-PRs query MUST include `reviewThreads` and `latestReviews` and `timelineItems`** β€” without those, the `is_engaged` predicate is undercounted and untriaged numbers blow up artificially. (Earlier iterations of `fetch.md` claimed those fields were not needed for stats; that was a documentation bug and has been corrected.)
- Implement ALL 11 sections per [`render.md`](../../.claude/skills/pr-management-stats/render.md). Skipping panels (e.g. dropping the line charts, CODEOWNERS table, triager-activity table) is **not** an acceptable simplification.
- If panel data is unavailable, the panel renders a stub with a one-line explanation of WHY the data is missing β€” never omit a section silently.

## Parity implementations

This script is a fetch + classify reference. The full render lives
in the agent-emitted version per `render.md`. Adopters who want a
deterministic CI-runnable equivalent should extend this script with
the aggregation + HTML emission directly; we welcome PRs.

## Cross-references

- [`pr-management-stats/SKILL.md`](../../.claude/skills/pr-management-stats/SKILL.md) β€” skill entry point.
- [`pr-management-stats/classify.md`](../../.claude/skills/pr-management-stats/classify.md) β€” `is_engaged` / `is_triaged` / `is_untriaged` predicates.
- [`pr-management-stats/fetch.md`](../../.claude/skills/pr-management-stats/fetch.md) β€” GraphQL templates.
- [`pr-management-stats/aggregate.md`](../../.claude/skills/pr-management-stats/aggregate.md) β€” per-panel computations.
- [`pr-management-stats/render.md`](../../.claude/skills/pr-management-stats/render.md) β€” dashboard layout, recommendation rules.
- [`tools/dashboard-generator/`](../dashboard-generator/) β€” sibling reference implementation for `issue-reassess-stats`.
Loading
Loading