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 290ecd5e feat(pr-management-stats): always publish the dashboard as a 
secret HTML gist + add ready-queue-split and per-person-attribution panels 
(#486)
290ecd5e is described below

commit 290ecd5e417826ce9f3812d8a4ee32b0d89d13c8
Author: Jarek Potiuk <[email protected]>
AuthorDate: Thu Jun 11 01:40:11 2026 +0200

    feat(pr-management-stats): always publish the dashboard as a secret HTML 
gist + add ready-queue-split and per-person-attribution panels (#486)
    
    Make the stats skill always export its dashboard: every run publishes a
    self-contained HTML dashboard to a secret GitHub gist and returns the
    gistpreview.github.io URL (stable per-repo gist id, in-place PATCH
    updates, dry-run / no-gist-scope fallbacks). This replaces the prior
    "render inline only" default so a maintainer's dashboards are directly
    comparable across days at one URL.
    
    Adds two required analytic panels learned from real use:
    - Ready-for-review queue split (by why-waiting): the ready queue broken
      into never-reviewed / discussed-no-decision / changes-requested /
      approved, as coloured hero cards + an age timeline (oldest bucket on
      the left). Separates the first-review gap from the decision/merge gap.
    - Drafts & closes attribution by person: who does the draft-conversions
      and closes, triage-action (actor != author) vs author-self, with
      per-maintainer shares. Counted from CONVERT_TO_DRAFT / CLOSED timeline
      events (not comment text), bots and backport PRs excluded.
    
    Also mandates the 1000-result Search-cap data-integrity caveats and adds
    a reusable inline-SVG line-chart helper plus a worked reference dashboard.
    
    Generated-by: Claude Code (Opus 4.8 1M context)
---
 skills/pr-management-stats/SKILL.md                |  29 ++
 skills/pr-management-stats/export.md               | 145 ++++++++
 skills/pr-management-stats/render.md               |  69 ++++
 .../examples/reference-dashboard.html              | 382 +++++++++++++++++++++
 tools/pr-management-stats/gen_charts.sh            |  72 ++++
 5 files changed, 697 insertions(+)

diff --git a/skills/pr-management-stats/SKILL.md 
b/skills/pr-management-stats/SKILL.md
index 2d07f16a..701aa32d 100644
--- a/skills/pr-management-stats/SKILL.md
+++ b/skills/pr-management-stats/SKILL.md
@@ -333,6 +333,35 @@ Render the maintainer dashboard per the layout in 
[`render.md#dashboard-layout`]
 
 The dashboard is **HTML by default** so the colour-coded hero cards, action 
priority bars, and velocity bars render correctly. A Markdown fallback (and a 
Rich terminal-tables variant for the detailed-tables section only) is produced 
when the maintainer passes `markdown` or `tables-only`. See 
[`render.md`](render.md) for the full layout, the colour scheme, and the 
recommendation rule definitions.
 
+Two analytic panels are **required** in addition to the eleven above and
+are specified in [`render.md`](render.md):
+
+- **Ready-for-review queue split (by why-waiting)** — the `ready` queue
+  broken into never-reviewed / discussed-no-decision / changes-requested /
+  approved, as 4 coloured hero cards plus an age timeline (oldest bucket on
+  the left). See 
[`render.md#ready-for-review-queue-split-by-why-waiting`](render.md#ready-for-review-queue-split-by-why-waiting).
+- **Drafts & closes attribution by person** — who does the
+  draft-conversions and closes, triage-action (actor ≠ author) vs
+  author-self, with per-maintainer shares; counted from timeline events,
+  bots/backports excluded. See 
[`render.md#drafts--closes-attribution-by-person`](render.md#drafts--closes-attribution-by-person).
+
+---
+
+## Step 7 — Publish the dashboard (always)
+
+Every stats run ends by publishing the HTML dashboard to a **secret
+GitHub gist** and returning the `gistpreview.github.io` URL. This is not
+optional and not behind a flag — see [`export.md`](export.md) for the
+full contract (stable per-repo gist id, in-place `PATCH` updates, the
+`dry-run` / no-`gist`-scope fallbacks, and the mandatory data-integrity
+caveats for the 1000-result Search cap).
+
+The published dashboard is the single canonical export format; it
+replaces any earlier "render inline only" behaviour so a maintainer's
+dashboards are directly comparable across days at a stable URL. The
+inline terminal/markdown render is still emitted for the in-session read;
+the gist is the durable, shareable artefact.
+
 ---
 
 ## What this skill does NOT do
diff --git a/skills/pr-management-stats/export.md 
b/skills/pr-management-stats/export.md
new file mode 100644
index 00000000..af6cdf63
--- /dev/null
+++ b/skills/pr-management-stats/export.md
@@ -0,0 +1,145 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# export — always publish the dashboard as a self-contained HTML gist
+
+`pr-management-stats` **always** produces a self-contained HTML
+dashboard and publishes it to a **secret GitHub gist**, then returns
+the `gistpreview.github.io` URL. This is not optional and not gated
+behind a flag: every stats run ends with a published, link-shareable
+dashboard. The inline terminal/markdown rendering (per
+[`render.md`](render.md)) is still emitted for the maintainer reading
+in-session; the gist is the durable, colour-rendered artefact they can
+open in a browser and share.
+
+This replaces any earlier "render inline only" behaviour — there is now
+exactly one canonical export format (the dashboard described here and in
+[`render.md`](render.md)), published the same way on every run, so a
+maintainer's dashboards are directly comparable across days.
+
+## Golden rule — one export, always, same shape
+
+- **Always.** Every `pr-management-stats` invocation publishes the gist.
+  No `--export` flag, no "ask first". The only exceptions are `dry-run`
+  (compute + render inline, skip the publish) and a `gh auth status`
+  token without `gist` scope (warn once, fall back to writing the HTML
+  to `/tmp/pr-management-stats-<repo-slug>.html` and print the path).
+- **Secret gist.** Use a secret gist (`gh gist create` defaults to
+  secret) — URL-shareable but not publicly listed. The dashboard
+  carries only aggregate public-PR metadata, but secret-by-default is
+  the conservative choice for a maintainer's personal account.
+- **Stable identity.** Reuse one gist per repo across runs (store its id
+  in the session-state file `<adopter-repo>/.apache-magpie.session-state.json`
+  under `stats_gist_id`, gitignored) so the same URL updates each run
+  rather than littering the account with one gist per day. `PATCH` the
+  existing gist's `dashboard.html` file in place; only create a new gist
+  the first time.
+
+## Publish recipe
+
+```bash
+# 1. write the self-contained HTML (see render.md + the template below)
+#    to a local file
+OUT=/tmp/pr-management-stats-<repo-slug>.html
+# ... agent writes the dashboard HTML to $OUT ...
+
+# 2. publish / update the secret gist (in place, same id across runs)
+GID="$(read stats_gist_id from .apache-magpie.session-state.json, if any)"
+if [ -z "$GID" ]; then
+  URL=$(gh gist create "$OUT" --desc "<Project> — PR Backlog Dashboard 
(<date>)")
+  GID=$(printf '%s' "$URL" | grep -oE '[0-9a-f]{20,}$')
+  # persist GID into .apache-magpie.session-state.json -> stats_gist_id
+else
+  # update in place — keeps the URL stable
+  jq -n --rawfile c "$OUT" '{files:{"dashboard.html":{content:$c}}}' \
+    | gh api -X PATCH "gists/$GID" --input -
+fi
+
+# 3. return BOTH links to the maintainer
+echo "Rendered (browser): https://gistpreview.github.io/?$GID";
+echo "Raw gist:           https://gist.github.com/<viewer>/$GID"
+```
+
+`gistpreview.github.io/?<id>` renders the gist's `dashboard.html` as a
+live page (it fetches the gist via the GitHub API, so it works for
+secret gists too). Always return the `gistpreview` URL first — that is
+the one the maintainer clicks.
+
+**Confirmation:** publishing a gist writes to the maintainer's account.
+Per the framework's "assistant proposes, user fires" norm the agent may
+publish without a per-run prompt **because the maintainer invoked the
+stats skill** (the export is the documented, expected output of that
+invocation) — but it must surface the gist URL in the result, never
+publish silently. If the project's agent-instructions require explicit
+confirmation before any account write, honour that and print the local
+HTML path instead, offering to publish on confirmation.
+
+## The canonical dashboard (the "exact export")
+
+The published HTML is the full panel set from [`render.md`](render.md),
+self-contained (inline `<style>`, inline SVG charts — no external JS/CSS
+so `gistpreview` renders it offline-of-CDN). A worked reference rendering
+is checked in at
+[`tools/pr-management-stats/examples/reference-dashboard.html`](../../tools/pr-management-stats/examples/reference-dashboard.html);
+the inline SVG line charts are produced by
+[`tools/pr-management-stats/gen_charts.sh`](../../tools/pr-management-stats/gen_charts.sh).
+
+Required panels, in order (every one always rendered — see
+[`SKILL.md` Golden rule 8](SKILL.md#golden-rules)):
+
+1. **Context line** — repo, open count, cutoff, viewer, refresh timestamp.
+2. **Hero cards (4)** — `Total open` · `Non-maintainer` (contributor-authored)
+   · `Ready for review (non-maintainer)` · `Drafts (author's court)`. 
Maintainer-authored
+   PRs are split out because triage/review skip collaborator PRs (they 
self-manage).
+3. **Hero legend** — a boxed "What the numbers above mean" panel defining
+   every hero number and every coloured split card in plain language.
+   Dense dashboards are unreadable without it; this panel is **required**,
+   not optional.
+4. **Ready-for-review split (4 coloured cards)** — the ready queue broken
+   down by *what each PR is actually waiting for* (see
+   [`render.md` § Ready-for-review queue 
split](render.md#ready-for-review-queue-split-by-why-waiting)):
+   <span style="red">never reviewed</span> / <span style="blue">discussed,
+   no decision</span> / <span style="amber">changes requested</span> /
+   <span style="green">approved, awaiting merge</span>. Same colours as the
+   timeline chart in panel 7.
+5. **Triage funnel (5 mutually-exclusive states)** — Ready · Responded ·
+   Waiting (author silent) · Waiting for human maintainer comment · Drafts —
+   with a prose block explaining **every** cell (whose court it is in).
+6. **Trends over time** — inline SVG line charts: PRs opened/week by author
+   class, merged/week, open backlog, and **drafts-vs-closes per week by
+   triage attribution** (per-person; bot/backport excluded — see
+   [`render.md` § Drafts & closes by 
person](render.md#drafts--closes-attribution-by-person)).
+7. **Ready-for-review timeline** — the multi-line "why each ready PR is
+   waiting" chart, **x-axis = age, oldest on the left → newest on the right**
+   (a timeline reads past→present), same colours as panel 4. This is the
+   panel that exposes the first-review gap (never-reviewed rising toward the
+   newest bucket).
+8. **Pressure by area**, **Closure velocity**, **Detailed tables**,
+   **Methodology / legend** — as in [`render.md`](render.md).
+
+## Mandatory data-integrity caveats
+
+The GitHub Search API caps results at **1000**. Any series derived from
+the closed/merged-since-cutoff fetch (velocity, momentum, backlog,
+drafts-vs-closes) is therefore **truncated in the oldest weeks** once a
+busy repo exceeds 1000 closes in the window. The dashboard MUST:
+
+- annotate the affected weeks (mark them "cap-truncated" or omit them and
+  say so) rather than drawing a misleading ramp, and
+- state which weeks are authoritative (typically the most recent 3–4), and
+- recommend, in the methodology panel, a **daily snapshot job** (a tiny
+  scheduled Action writing `open_count / ready_count / per-area` to a CSV)
+  as the only reliable way to get true uncapped historical trends.
+
+Never present a cap-distorted absolute series as if it were real history.
+
+## Cross-references
+
+- [`render.md`](render.md) — panel-by-panel layout, colour scheme, the two
+  new analytic panels (ready-for-review split; drafts/closes by person).
+- [`SKILL.md` Step 7](SKILL.md#step-7--publish-the-dashboard-always) — the
+  always-publish step in the skill flow.
+- 
[`pr-management-triage/session-history.md`](../pr-management-triage/session-history.md)
+  — the *separate* session-history gist (triage calibration log); the stats
+  dashboard gist is its own artefact and reuses the same secret-gist +
+  stable-id mechanics.
diff --git a/skills/pr-management-stats/render.md 
b/skills/pr-management-stats/render.md
index 90c87fa7..4533f0f1 100644
--- a/skills/pr-management-stats/render.md
+++ b/skills/pr-management-stats/render.md
@@ -314,6 +314,75 @@ A bordered panel at the bottom explaining all the colours, 
columns, and computed
 
 ---
 
+## Ready-for-review queue split (by why-waiting)
+
+A required analytic panel (rendered as 4 coloured hero cards **and** a
+multi-line age timeline — panels 4 and 7 in [`export.md`](export.md)).
+It answers the question the bare "Ready for review = N" number cannot:
+**of the PRs that cleared triage, why is each still sitting there?**
+
+Scope the panel to the **non-maintainer** ready PRs only (maintainer-
+authored PRs carry the `ready` label too but are out of triage scope —
+they self-manage; exclude the handful and say so). For each ready PR,
+classify into exactly one sub-state from its `reviewDecision` plus
+maintainer engagement:
+
+| Sub-state | Colour | Rule |
+|---|---|---|
+| **Never reviewed** | red `#da3633` | no formal maintainer review **and** no 
non-triage maintainer comment — nobody has looked yet |
+| **Discussed, no decision** | blue `#388bfd` | `reviewDecision = 
REVIEW_REQUIRED`/none but a maintainer left a `COMMENTED` review or a real 
(non-triage-marker) comment |
+| **Changes requested** | amber `#d29922` | `reviewDecision = 
CHANGES_REQUESTED` — really the author's court; mislabeled `ready` |
+| **Approved (awaiting merge)** | green `#2ea043` | `reviewDecision = 
APPROVED` — review done, just needs a merge |
+
+Render the cards in that order; render the timeline with **x = age,
+oldest bucket on the LEFT** (`>12w … 0-2w` left→right, so it reads
+past→present). The signal to surface: if the red "never reviewed" line
+**climbs toward the newest bucket**, the `ready` label is being applied
+faster than anyone reviews — a *first-review gap* distinct from the
+*decision/merge gap* (discussed + approved). Quantify both in the
+caption.
+
+This split is the highest-value reframe in the dashboard: "review-bound"
+is rarely one problem — it is usually a large first-review backlog plus a
+smaller decision/merge tail, and the two need different responses.
+
+## Drafts & closes attribution by person
+
+A required panel showing **who actually does the draft-conversions and
+the closes**, over the cutoff window, split by whether the action was a
+**triage action** (actor ≠ the PR author — a maintainer acting on
+someone else's PR) or an **author self-action**, and naming each
+maintainer's share.
+
+Hard requirements for correctness (these are the traps that produce
+wrong numbers — all learned the hard way):
+
+- **Count from timeline events, not comment text.** Draft-conversions
+  come from `CONVERT_TO_DRAFT_EVENT` timeline items; closes from
+  `CLOSED_EVENT` actors. Detecting them by parsing comment bodies (e.g.
+  "Converting to draft") undercounts badly — the `comments(last:N)`
+  window drops older markers, and stale-close templates don't carry the
+  triage marker at all.
+- **Exclude bot-authored and backport PRs** before attributing. Bot
+  authors (`*bot`, `github-actions`, `dependabot`) and backport PRs
+  (`baseRefName ≠ main`, or a `[v*-test]` / `[*-stable]` title) are
+  release-branch housekeeping, not contributor triage — including them
+  inflates "maintainer closes" with bot/backport cleanup. State how many
+  were removed.
+- **Triage vs author = actor ≠ author.** Define "triage process" as an
+  action taken by someone other than the PR author; "author" as a
+  self-action. Derive the maintainer set from who comments as
+  `OWNER`/`MEMBER`/`COLLABORATOR` in the fetched data — do not hardcode a
+  committer list.
+
+Surface per-person shares (e.g. "@X did 72% of triage drafts",
+"@Y closed 60% of contributor PRs"). Single-maintainer concentration is
+the actionable finding: a deterministic action concentrated on one
+person (drafting) is a prime automation candidate; a judgment action
+concentrated on one person (closing) is a bus-factor to spread.
+
+---
+
 ## Recommendation rules
 
 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.
diff --git a/tools/pr-management-stats/examples/reference-dashboard.html 
b/tools/pr-management-stats/examples/reference-dashboard.html
new file mode 100644
index 00000000..c840b258
--- /dev/null
+++ b/tools/pr-management-stats/examples/reference-dashboard.html
@@ -0,0 +1,382 @@
+<\!-- SPDX-License-Identifier: Apache-2.0
+     Reference rendering of the pr-management-stats dashboard (see 
../../../skills/pr-management-stats/export.md).
+     Worked example over apache/airflow, 2026-06-09. Numbers are illustrative; 
the panel set + layout are the spec. -->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Apache Airflow — PR Backlog Dashboard</title>
+<style>
+  :root{ --bg:#0d1117; --card:#161b22; --bd:#30363d; --fg:#e6edf3; 
--mut:#8b949e;
+         --grn:#2ea043; --ylw:#d29922; --red:#da3633; --blu:#388bfd; 
--pur:#a371f7; }
+  *{box-sizing:border-box}
+  body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 
-apple-system,Segoe 
UI,Roboto,Helvetica,Arial,sans-serif;padding:24px;max-width:1100px;margin:0 
auto}
+  h1{font-size:24px;margin:0 0 4px} h2{font-size:18px;margin:30px 0 
12px;border-bottom:1px solid var(--bd);padding-bottom:6px}
+  h3{font-size:14px;margin:16px 0 6px}
+  .ctx{color:var(--mut);font-size:13px;margin-bottom:6px}
+  
.caveat{color:var(--ylw);font-size:12px;background:rgba(210,153,34,.1);border:1px
 solid var(--ylw);border-radius:6px;padding:8px 12px;margin:10px 0}
+  
.good{color:var(--grn);font-size:12px;background:rgba(46,160,67,.1);border:1px 
solid var(--grn);border-radius:6px;padding:8px 12px;margin:10px 0}
+  
.heroes{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0}
+  .hero{background:var(--card);border:1px solid 
var(--bd);border-radius:10px;padding:16px}
+  .hero 
.lbl{color:var(--mut);font-size:12px;text-transform:uppercase;letter-spacing:.5px}
+  .hero .big{font-size:30px;font-weight:700;margin-top:6px}
+  .hero .sub{font-size:12px;color:var(--mut);margin-top:4px}
+  .warn{color:var(--ylw)} .bad{color:var(--red)} .ok{color:var(--grn)}
+  table{border-collapse:collapse;width:100%;margin:8px 0;font-size:13.5px}
+  th,td{border:1px solid var(--bd);padding:6px 10px;text-align:left}
+  th{background:#1c2128;color:var(--mut);font-weight:600}
+  tr:nth-child(even) td{background:#11151b}
+  .pri-HIGH{color:var(--red);font-weight:700} 
.pri-MED{color:var(--ylw);font-weight:700} .pri-LOW{color:var(--blu)} 
.pri-INFO{color:var(--mut)}
+  code{background:#1c2128;border:1px solid 
var(--bd);border-radius:4px;padding:1px 5px;font-size:12px}
+  
.bar{display:inline-block;height:12px;background:var(--grn);border-radius:2px;vertical-align:middle}
+  
.funnel{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin:10px 0}
+  .fcell{background:var(--card);border:1px solid 
var(--bd);border-radius:8px;padding:12px;text-align:center}
+  .fcell .n{font-size:24px;font-weight:700} .fcell 
.l{font-size:11px;color:var(--mut);margin-top:4px}
+  .yes{color:#3fb950;font-weight:600} .no{color:#f85149;font-weight:600} 
.part{color:#d29922;font-weight:600}
+  .foot{color:var(--mut);font-size:12px;margin-top:30px;border-top:1px solid 
var(--bd);padding-top:12px}
+</style>
+</head>
+<body>
+<h1>📊 Apache Airflow — PR Backlog Dashboard</h1>
+<div class="ctx">Scope: <b>apache/airflow</b> · <b>500</b> open PRs · 
closed/merged since 2026-04-28 (6 wk) · viewer <code>@potiuk</code> · 
<b>refreshed 2026-06-09 (post-cleanup)</b></div>
+<div class="good">✓ Effect of this session's cleanup: ready queue <b>236 → 
217</b> (−19: 5 merged + 13 changes-requested stripped + 1 flipped), 
stale-ready (&gt;4wk) <b>105 → 88</b> (−17).</div>
+<div class="caveat">⚠ The closed/merged velocity series hit GitHub's 
1000-result Search cap — weeks 0–3 are authoritative, 4–5 truncated.</div>
+
+<div class="heroes">
+  <div class="hero"><div class="lbl">Total open</div><div 
class="big">500</div><div class="sub">405 non-draft · 95 draft</div></div>
+  <div class="hero"><div class="lbl">Non-maintainer</div><div 
class="big">373</div><div class="sub">contributor-authored (127 are 
maintainers' own)</div></div>
+  <div class="hero"><div class="lbl">Ready for review (non-maint.)</div><div 
class="big warn">211</div><div class="sub bad">of 217 ready; 88 are &gt;4 wk 
old</div></div>
+  <div class="hero"><div class="lbl">Drafts (author's court)</div><div 
class="big">95</div><div class="sub">57 non-maintainer · 19% of open</div></div>
+</div>
+<div class="ctx" style="margin:6px 0 2px"><b>Ready-for-review queue (211) 
split by review state</b> — same colors as the "why waiting" chart below:</div>
+<div class="funnel" style="grid-template-columns:repeat(4,1fr)">
+  <div class="fcell"><div class="n" style="color:#da3633">108</div><div 
class="l">Never reviewed<br>(no maintainer look yet)</div></div>
+  <div class="fcell"><div class="n" style="color:#388bfd">67</div><div 
class="l">Discussed,<br>no decision</div></div>
+  <div class="fcell"><div class="n" style="color:#d29922">13</div><div 
class="l">Changes requested<br>(author's court)</div></div>
+  <div class="fcell"><div class="n" style="color:#2ea043">23</div><div 
class="l">Approved<br>(awaiting merge)</div></div>
+</div>
+<div class="ctx" style="background:#11151b;border:1px solid 
#30363d;border-radius:6px;padding:10px 12px">
+<b>What the numbers above mean</b> &nbsp;(all counts are of 
currently-<i>open</i> PRs):
+<br>• <b>Total open — 500:</b> every open PR on apache/airflow (drafts + 
non-drafts, contributors + maintainers).
+<br>• <b>Non-maintainer — 373:</b> of those, the ones authored by 
<b>contributors</b> (not committers). The other 127 are maintainers' own PRs, 
which triage/review treat separately. This 373 is the work the triage + review 
pipeline actually serves.
+<br>• <b>Ready for review (non-maint.) — 211:</b> contributor PRs carrying the 
<code>ready for maintainer review</code> label — i.e. they cleared triage and 
are <b>waiting on a maintainer to review</b>. (6 maintainer-authored PRs also 
carry the label but are excluded — maintainers self-manage.)
+<br>• <b>Drafts (author's court) — 95:</b> open PRs in GitHub <i>draft</i> 
state — work-in-progress the author still owns; not awaiting maintainer action.
+<br><b>The 4 coloured cards</b> break the 211 ready PRs down by <b>what each 
is actually waiting for</b> (same colours + the line chart further below): 
<span style="color:#da3633"><b>108 never reviewed</b></span> (no maintainer has 
looked yet — the real backlog), <span style="color:#388bfd"><b>67 discussed but 
no decision</b></span> (a maintainer engaged but neither approved nor requested 
changes), <span style="color:#d29922"><b>13 changes requested</b></span> 
(really the author's court  [...]
+</div>
+
+<h2>🚦 Triage funnel <span class="ctx" style="font-weight:400">(mutually 
exclusive · 494 triage-scope PRs; the 6 maintainer-authored <code>ready</code> 
PRs are excluded — triage skips collaborator PRs)</span></h2>
+<div class="funnel">
+  <div class="fcell"><div class="n warn">211</div><div class="l">Ready (queue, 
non-maint.)</div></div>
+  <div class="fcell"><div class="n">15</div><div 
class="l">Responded</div></div>
+  <div class="fcell"><div class="n">34</div><div class="l">Waiting (author 
silent)</div></div>
+  <div class="fcell"><div class="n">139</div><div class="l">Waiting for human 
maintainer comment</div></div>
+  <div class="fcell"><div class="n">95</div><div class="l">Drafts (author's 
court)</div></div>
+</div>
+<div class="ctx">Every open non-maintainer PR sits in exactly one of these 
five mutually-exclusive states (left → right ≈ closest-to-merge → furthest):
+<br>• <b>Ready (queue) — 211:</b> carries the <code>ready for maintainer 
review</code> label → awaiting a maintainer's review. This is the bottleneck 
(see the split above).
+<br>• <b>Responded — 15:</b> a maintainer left review feedback and the author 
has <i>since replied / pushed</i> — back in the maintainer's court for a 
re-look.
+<br>• <b>Waiting (author silent) — 34:</b> a maintainer left feedback and the 
author has <i>not</i> responded for &gt;7 days — the author's court; stale-ping 
candidates.
+<br>• <b>Waiting for human maintainer comment — 139:</b> open non-draft PRs 
with no <code>ready</code> label and no maintainer triage marker — <i>no human 
maintainer has engaged yet</i>. <b>Not</b> all neglected contributor work: 
<b>79 of 139 (57%) are maintainers' own PRs</b> (triage skips collaborator PRs 
— they self-manage); only <b>60 are contributor PRs genuinely awaiting a first 
triage pass</b>, most brand-new (76 &lt;1 wk; 25 &gt;4 wk). Real "needs first 
triage" ≈ 60, not 139.
+<br>• <b>Drafts (author's court) — 95:</b> PRs in GitHub <i>draft</i> state — 
work-in-progress the <b>author</b> owns, <b>not awaiting maintainer action</b> 
(either opened as draft, or triage converted them for failing CI / conflicts / 
staleness). They re-enter the funnel only when the author re-marks "Ready for 
review."</div>
+
+<h2>🔎 Why the ready-for-review queue (211) is actually waiting <span 
class="ctx" style="font-weight:400">— non-maintainer PRs only (the 6 
maintainer-authored ready PRs are excluded — triage skips collaborator PRs); by 
age</span></h2>
+<svg viewBox="0 0 640 215" width="100%" 
style="max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px">
+<line x1="55" y1="185.0" x2="585" y2="185.0" stroke="#21262d"/>
+<text x="48" y="189" fill="#8b949e" font-size="10" text-anchor="end">0</text>
+<line x1="55" y1="145.0" x2="585" y2="145.0" stroke="#21262d"/>
+<text x="48" y="149" fill="#8b949e" font-size="10" text-anchor="end">10</text>
+<line x1="55" y1="105.0" x2="585" y2="105.0" stroke="#21262d"/>
+<text x="48" y="109" fill="#8b949e" font-size="10" text-anchor="end">20</text>
+<line x1="55" y1="65.0" x2="585" y2="65.0" stroke="#21262d"/>
+<text x="48" y="69" fill="#8b949e" font-size="10" text-anchor="end">30</text>
+<line x1="55" y1="25.0" x2="585" y2="25.0" stroke="#21262d"/>
+<text x="48" y="29" fill="#8b949e" font-size="10" text-anchor="end">40</text>
+<text x="55" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">>12w</text>
+<text x="188" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">8-12w</text>
+<text x="320" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">4-8w</text>
+<text x="452" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">2-4w</text>
+<text x="585" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">0-2w</text>
+<text x="55" y="16" fill="#e6edf3" font-size="11" font-weight="600">211 ready 
PRs by why they wait (oldest left -> newest right)</text>
+<polyline points=" 55,173.0 188,169.0 320,61.0 452,49.0 585,41.0" fill="none" 
stroke="#da3633" stroke-width="2.5"/>
+<circle cx="55" cy="173.0" r="3" fill="#da3633"/>
+<text x="55" y="166" fill="#da3633" font-size="9" text-anchor="middle">3</text>
+<circle cx="188" cy="169.0" r="3" fill="#da3633"/>
+<text x="188" y="162" fill="#da3633" font-size="9" 
text-anchor="middle">4</text>
+<circle cx="320" cy="61.0" r="3" fill="#da3633"/>
+<text x="320" y="54" fill="#da3633" font-size="9" 
text-anchor="middle">31</text>
+<circle cx="452" cy="49.0" r="3" fill="#da3633"/>
+<text x="452" y="42" fill="#da3633" font-size="9" 
text-anchor="middle">34</text>
+<circle cx="585" cy="41.0" r="3" fill="#da3633"/>
+<text x="585" y="34" fill="#da3633" font-size="9" 
text-anchor="middle">36</text>
+<polyline points=" 55,121.0 188,145.0 320,121.0 452,121.0 585,149.0" 
fill="none" stroke="#388bfd" stroke-width="2.5"/>
+<circle cx="55" cy="121.0" r="3" fill="#388bfd"/>
+<text x="55" y="114" fill="#388bfd" font-size="9" 
text-anchor="middle">16</text>
+<circle cx="188" cy="145.0" r="3" fill="#388bfd"/>
+<text x="188" y="138" fill="#388bfd" font-size="9" 
text-anchor="middle">10</text>
+<circle cx="320" cy="121.0" r="3" fill="#388bfd"/>
+<text x="320" y="114" fill="#388bfd" font-size="9" 
text-anchor="middle">16</text>
+<circle cx="452" cy="121.0" r="3" fill="#388bfd"/>
+<text x="452" y="114" fill="#388bfd" font-size="9" 
text-anchor="middle">16</text>
+<circle cx="585" cy="149.0" r="3" fill="#388bfd"/>
+<text x="585" y="142" fill="#388bfd" font-size="9" 
text-anchor="middle">9</text>
+<polyline points=" 55,181.0 188,181.0 320,165.0 452,161.0 585,185.0" 
fill="none" stroke="#d29922" stroke-width="2.5"/>
+<circle cx="55" cy="181.0" r="3" fill="#d29922"/>
+<text x="55" y="174" fill="#d29922" font-size="9" text-anchor="middle">1</text>
+<circle cx="188" cy="181.0" r="3" fill="#d29922"/>
+<text x="188" y="174" fill="#d29922" font-size="9" 
text-anchor="middle">1</text>
+<circle cx="320" cy="165.0" r="3" fill="#d29922"/>
+<text x="320" y="158" fill="#d29922" font-size="9" 
text-anchor="middle">5</text>
+<circle cx="452" cy="161.0" r="3" fill="#d29922"/>
+<text x="452" y="154" fill="#d29922" font-size="9" 
text-anchor="middle">6</text>
+<circle cx="585" cy="185.0" r="3" fill="#d29922"/>
+<text x="585" y="178" fill="#d29922" font-size="9" 
text-anchor="middle">0</text>
+<polyline points=" 55,173.0 188,173.0 320,161.0 452,157.0 585,169.0" 
fill="none" stroke="#2ea043" stroke-width="2.5"/>
+<circle cx="55" cy="173.0" r="3" fill="#2ea043"/>
+<text x="55" y="166" fill="#2ea043" font-size="9" text-anchor="middle">3</text>
+<circle cx="188" cy="173.0" r="3" fill="#2ea043"/>
+<text x="188" y="166" fill="#2ea043" font-size="9" 
text-anchor="middle">3</text>
+<circle cx="320" cy="161.0" r="3" fill="#2ea043"/>
+<text x="320" y="154" fill="#2ea043" font-size="9" 
text-anchor="middle">6</text>
+<circle cx="452" cy="157.0" r="3" fill="#2ea043"/>
+<text x="452" y="150" fill="#2ea043" font-size="9" 
text-anchor="middle">7</text>
+<circle cx="585" cy="169.0" r="3" fill="#2ea043"/>
+<text x="585" y="162" fill="#2ea043" font-size="9" 
text-anchor="middle">4</text>
+</svg>
+<div class="ctx"><span style="color:#da3633">■</span> never reviewed (no 
formal review, no non-triage maintainer comment) &nbsp; <span 
style="color:#388bfd">■</span> discussed, no decision &nbsp; <span 
style="color:#d29922">■</span> changes requested &nbsp; <span 
style="color:#2ea043">■</span> approved (awaiting merge)</div>
+<div class="caveat">🔎 <b>The headline finding:</b> <b>108 of 211 
non-maintainer ready PRs (51%) have never had a real maintainer look</b> — and 
the red "never reviewed" line <b>climbs steeply toward the right</b> (newer 
PRs) — 36 + 34 + 31 of them are &lt;8 wk old, while only 3–4 are &gt;8 wk — 
i.e. the <code>ready</code> label is being applied faster than anyone reviews, 
so fresh PRs pile up unreviewed. Breakdown: never-reviewed <b>108</b> · 
discussed-no-decision <b>67</b> · approved-aw [...]
+
+<h2>🤖 Workflow automation potential</h2>
+<p>Categorising every decision-point this session by whether the rule is 
<b>deterministic</b> (a GitHub Action could reproduce it exactly) or needs 
<b>judgment</b> (LLM/human):</p>
+<table>
+<tr><th>Workflow 
step</th><th>Vol.</th><th>Determinism</th><th>CI-automatable?</th></tr>
+<tr><td>Mark-ready gate (real-CI-green + mergeable + 0 unresolved + not 
changes-req)</td><td>53</td><td>Deterministic</td><td class="yes">YES — caught 
13/53 false-positives by rule</td></tr>
+<tr><td>CI-failing → draft + violations comment (check→category 
map)</td><td>49</td><td>Deterministic</td><td class="yes">YES</td></tr>
+<tr><td>Conflict → rebase comment 
(<code>mergeable_state=dirty</code>)</td><td>4</td><td>Deterministic</td><td 
class="yes">YES</td></tr>
+<tr><td>Changes-requested → strip <code>ready</code> + nudge 
(<code>reviewDecision</code>)</td><td>13</td><td>Deterministic</td><td 
class="yes">YES</td></tr>
+<tr><td>Stale-draft <i>detection</i> (idle 
thresholds)</td><td>5</td><td>Deterministic</td><td class="yes">YES (flag 
only)</td></tr>
+<tr><td>Workflow-approval safety scan (no workflow-file edits, no exfil 
patterns)</td><td>9</td><td>Semi</td><td class="part">PARTIAL — scan auto, 
approve = judgment</td></tr>
+<tr><td>Unresolved-thread engagement split (replied/pushed after 
marker)</td><td>10</td><td>Semi</td><td class="part">PARTIAL</td></tr>
+<tr><td>Stale-draft <i>close decision</i></td><td>5</td><td>Judgment</td><td 
class="no">NO — 3/5 kept (dependency / AIP / active first-timer)</td></tr>
+<tr><td>Code-review correctness</td><td>6</td><td>Judgment</td><td 
class="no">NO — caught a real "doesn't fix the bug" defect</td></tr>
+<tr><td>Mentor reply</td><td>1</td><td>Judgment</td><td class="no">NO</td></tr>
+</table>
+<div class="funnel" style="grid-template-columns:repeat(3,1fr)">
+  <div class="fcell"><div class="n yes">~80%</div><div class="l">Fully 
deterministic<br>(124 of ~155 decision-points)</div></div>
+  <div class="fcell"><div class="n part">~12%</div><div 
class="l">Semi-deterministic<br>(detection auto, action judgment)</div></div>
+  <div class="fcell"><div class="n no">~8%</div><div 
class="l">Judgment-required<br>(review, mentor, close)</div></div>
+</div>
+<h3 class="ok">Highest-value CI automations (in order)</h3>
+<table>
+<tr><th>#</th><th>GitHub Action</th><th>Why it wins</th></tr>
+<tr><td>1</td><td><b>ready-label bot</b> — auto add/remove <code>ready for 
maintainer review</code> on every push from: real CI green (not just bot 
checks) + mergeable + 0 unresolved threads + <code>reviewDecision ≠ 
CHANGES_REQUESTED</code></td><td>The mark-ready gate is 100% deterministic yet 
humans get it wrong <b>25%</b> of the time (the 
rollup-SUCCESS-but-real-CI-never-ran trap). A bot runs it continuously and 
never gets fooled — would have prevented all 13 false-positives + kept the [...]
+<tr><td>2</td><td><b>red-CI drafter</b> — convert to draft + post the 
quality-violations comment when required checks fail</td><td>49 PRs this 
session; pure check-name→category mapping.</td></tr>
+<tr><td>3</td><td><b>conflict / changes-requested sweeper</b> — comment + 
relabel on <code>dirty</code> or <code>CHANGES_REQUESTED</code></td><td>17 PRs; 
trivially rule-based, keeps the queue honest about author-vs-maintainer 
court.</td></tr>
+<tr><td>4</td><td><b>stale flagger</b> — label (not close) drafts idle &gt; 
threshold</td><td>Detection is safe to automate; the <i>close</i> stays human 
(3/5 were wrongly closeable).</td></tr>
+</table>
+<p class="ctx"><b>Keep human/LLM:</b> code-review correctness, mentor replies, 
and close-vs-keep decisions — these are where the session's judgment actually 
changed outcomes (caught a non-fixing "fix", kept 3 PRs that rules would have 
closed, unblocked a stuck first-timer).</p>
+
+<h3>Drafts vs closes per week — <span class="warn">contributor PRs only</span> 
<span class="ctx">(bot &amp; backport PRs excluded; wk 0–3 reliable)</span></h3>
+<svg viewBox="0 0 640 215" width="100%" 
style="max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px">
+<line x1="55" y1="185.0" x2="585" y2="185.0" stroke="#21262d"/>
+<text x="48" y="189" fill="#8b949e" font-size="10" text-anchor="end">0</text>
+<line x1="55" y1="145.0" x2="585" y2="145.0" stroke="#21262d"/>
+<text x="48" y="149" fill="#8b949e" font-size="10" text-anchor="end">13</text>
+<line x1="55" y1="105.0" x2="585" y2="105.0" stroke="#21262d"/>
+<text x="48" y="109" fill="#8b949e" font-size="10" text-anchor="end">27</text>
+<line x1="55" y1="65.0" x2="585" y2="65.0" stroke="#21262d"/>
+<text x="48" y="69" fill="#8b949e" font-size="10" text-anchor="end">41</text>
+<line x1="55" y1="25.0" x2="585" y2="25.0" stroke="#21262d"/>
+<text x="48" y="29" fill="#8b949e" font-size="10" text-anchor="end">55</text>
+<text x="55" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-3</text>
+<text x="232" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-2</text>
+<text x="408" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-1</text>
+<text x="585" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">now</text>
+<text x="55" y="16" fill="#e6edf3" font-size="11" font-weight="600">Drafts vs 
closes per week (contributor PRs only, wk 0-3 reliable)</text>
+<polyline points=" 55,121.0 232,153.0 408,161.7 585,54.1" fill="none" 
stroke="#d29922" stroke-width="2.5"/>
+<circle cx="55" cy="121.0" r="3" fill="#d29922"/><text x="55" y="114" 
fill="#d29922" font-size="9" text-anchor="middle">22</text>
+<circle cx="232" cy="153.0" r="3" fill="#d29922"/><text x="232" y="146" 
fill="#d29922" font-size="9" text-anchor="middle">11</text>
+<circle cx="408" cy="161.7" r="3" fill="#d29922"/><text x="408" y="154.7" 
fill="#d29922" font-size="9" text-anchor="middle">8</text>
+<circle cx="585" cy="54.1" r="3" fill="#d29922"/><text x="585" y="47.1" 
fill="#d29922" font-size="9" text-anchor="middle">45</text>
+<polyline points=" 55,135.5 232,33.7 408,80.3 585,123.9" fill="none" 
stroke="#da3633" stroke-width="2.5"/>
+<circle cx="55" cy="135.5" r="3" fill="#da3633"/><text x="55" y="128.5" 
fill="#da3633" font-size="9" text-anchor="middle">17</text>
+<circle cx="232" cy="33.7" r="3" fill="#da3633"/><text x="232" y="26.7" 
fill="#da3633" font-size="9" text-anchor="middle">52</text>
+<circle cx="408" cy="80.3" r="3" fill="#da3633"/><text x="408" y="73.3" 
fill="#da3633" font-size="9" text-anchor="middle">36</text>
+<circle cx="585" cy="123.9" r="3" fill="#da3633"/><text x="585" y="116.9" 
fill="#da3633" font-size="9" text-anchor="middle">21</text>
+</svg>
+<div class="ctx"><span style="color:#d29922">■</span> draft-conversions 
(<code>ConvertToDraft</code> events) &nbsp; <span 
style="color:#da3633">■</span> closed-unmerged. <b>Bot-authored and backport 
PRs (base ≠ <code>main</code>) are removed</b> — that stripped <b>76 of 203 
closes</b> (51 bot + 57 backport = release-branch housekeeping, not contributor 
triage) and 4 of 127 drafted PRs. wk-0 draft bump is partly this session; 
closes wk 4–5 truncated by the Search cap.</div>
+
+<h3>Who does it — triage process vs author, and @potiuk's share <span 
class="ctx">(contributor PRs, 6-wk window)</span></h3>
+<table>
+<tr><th>Action</th><th>Total</th><th>By triage (maintainer acted)</th><th>By 
author (self)</th><th>@potiuk</th></tr>
+<tr><td><b>Draft-conversions</b></td><td>173</td><td class="warn">125 
&nbsp;(72%)</td><td>48 &nbsp;(28%)</td><td><b>90</b> — 52% of all · <b>72% of 
triage</b></td></tr>
+<tr><td><b>Closes</b></td><td>127</td><td>67 &nbsp;(53%)</td><td>59 
&nbsp;(46%)</td><td><b>7</b> — 6% of all · 10% of triage</td></tr>
+</table>
+<div class="funnel" style="grid-template-columns:repeat(2,1fr)">
+  <div class="fcell"><div class="n warn">72%</div><div class="l">of triage 
<b>drafts</b> by @potiuk<br>(one-maintainer-heavy → prime automation 
target)</div></div>
+  <div class="fcell"><div class="n ok">10%</div><div class="l">of triage 
<b>closes</b> by @potiuk<br>(closing is @kaxil-heavy, see below)</div></div>
+</div>
+
+<h3>Closing stats per person <span class="ctx">(contributor PRs closed by a 
maintainer ≠ author; n = 67)</span></h3>
+<table>
+<tr><th>Maintainer</th><th>Contributor PRs 
closed</th><th>Share</th><th></th></tr>
+<tr><td><b>@kaxil</b></td><td>40</td><td>60%</td><td><span class="bar" 
style="width:240px"></span></td></tr>
+<tr><td>@potiuk</td><td>7</td><td>10%</td><td><span class="bar" 
style="width:42px"></span></td></tr>
+<tr><td>@pierrejeambrun</td><td>6</td><td>9%</td><td><span class="bar" 
style="width:36px"></span></td></tr>
+<tr><td>@bbovenzi</td><td>5</td><td>7%</td><td><span class="bar" 
style="width:30px"></span></td></tr>
+<tr><td>@choo121600</td><td>3</td><td>4%</td><td><span class="bar" 
style="width:18px"></span></td></tr>
+<tr><td>@shahar1</td><td>2</td><td>3%</td><td><span class="bar" 
style="width:12px"></span></td></tr>
+<tr><td>@Lee-W / @jason810496 / @eladkal / @ashb</td><td>1 
each</td><td>~1%</td><td><span class="bar" style="width:6px"></span></td></tr>
+</table>
+<div class="ctx"><b>Both triage actions are single-maintainer-concentrated, 
but different people:</b> @potiuk does <b>72%</b> of triage <i>drafting</i>, 
@kaxil does <b>60%</b> of contributor-PR <i>closing</i>. Drafting is fully 
deterministic → automate it (red-CI drafter). Closing needs judgment (3/5 close 
candidates this session were wrongly closeable) → keep human, but the @kaxil 
concentration is a bus-factor worth spreading. Definitions: <b>triage</b> = 
actor ≠ author; <b>author</b> = [...]
+
+
+<h2>🔥 Pressure by area (refreshed)</h2>
+<table>
+<tr><th>#</th><th>Area</th><th>Score</th><th>Open PRs</th></tr>
+<tr><td>1</td><td><code>area:task-sdk</code></td><td>173</td><td>106</td></tr>
+<tr><td>2</td><td><code>area:providers</code></td><td>159</td><td>134</td></tr>
+<tr><td>3</td><td><code>area:API</code></td><td>112</td><td>72</td></tr>
+<tr><td>4</td><td><code>(no area)</code></td><td>67</td><td>63</td></tr>
+<tr><td>5</td><td><code>area:dev-tools</code></td><td>63</td><td>47</td></tr>
+<tr><td>6</td><td><code>area:UI</code></td><td>59</td><td>42</td></tr>
+<tr><td>7</td><td><code>area:DAG-processing</code></td><td>56</td><td>40</td></tr>
+<tr><td>8</td><td><code>area:Scheduler</code></td><td>51</td><td>45</td></tr>
+</table>
+
+<h2>📈 Closure velocity (wk 0–3, from prior snapshot)</h2>
+<table>
+<tr><th>Week ago</th><th>Merged</th><th>Closed</th><th>Bar (merged)</th></tr>
+<tr><td>0</td><td>235</td><td>55</td><td><span class="bar" 
style="width:235px"></span></td></tr>
+<tr><td>1</td><td>202</td><td>64</td><td><span class="bar" 
style="width:202px"></span></td></tr>
+<tr><td>2</td><td>161</td><td>60</td><td><span class="bar" 
style="width:161px"></span></td></tr>
+<tr><td>3</td><td>171</td><td>24</td><td><span class="bar" 
style="width:171px"></span></td></tr>
+</table>
+<div class="ctx">Avg 192 merged/wk · ~314 opened/wk. Inflow runs ahead of 
closes — but the exact net is cap-limited (see Trends caveat).</div>
+
+<h2>📉 Trends over time (last 6 weeks)</h2>
+<div class="caveat">⚠ <b>Data limit:</b> GitHub's Search API caps results at 
1000, so closes older than ~4 weeks are truncated. That makes the <i>absolute 
backlog</i> and <i>net-flow</i> for weeks 4–5 unreliable (they read 
artificially low). Weeks <b>0–3</b> below are authoritative. <b>True historical 
trends need an uncapped daily snapshot job</b> — itself a good automation (a 
tiny scheduled Action writing <code>open_count / ready_count / per-area</code> 
to a CSV would give exact week-ov [...]
+
+<h3>PRs opened per week, by author class <span class="ctx">(reliable wk 
0–3)</span></h3>
+<svg viewBox="0 0 640 215" width="100%" 
style="max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px">
+<line x1="55" y1="185.0" x2="585" y2="185.0" stroke="#21262d"/>
+<text x="48" y="189" fill="#8b949e" font-size="10" text-anchor="end">0</text>
+<line x1="55" y1="145.0" x2="585" y2="145.0" stroke="#21262d"/>
+<text x="48" y="149" fill="#8b949e" font-size="10" text-anchor="end">45</text>
+<line x1="55" y1="105.0" x2="585" y2="105.0" stroke="#21262d"/>
+<text x="48" y="109" fill="#8b949e" font-size="10" text-anchor="end">90</text>
+<line x1="55" y1="65.0" x2="585" y2="65.0" stroke="#21262d"/>
+<text x="48" y="69" fill="#8b949e" font-size="10" text-anchor="end">135</text>
+<line x1="55" y1="25.0" x2="585" y2="25.0" stroke="#21262d"/>
+<text x="48" y="29" fill="#8b949e" font-size="10" text-anchor="end">180</text>
+<text x="55" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-3</text>
+<text x="232" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-2</text>
+<text x="408" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-1</text>
+<text x="585" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">now</text>
+<text x="55" y="16" fill="#e6edf3" font-size="11" font-weight="600">PRs opened 
/ week by author class</text>
+<polyline points=" 55,169.9 232,147.7 408,165.4 585,155.7" fill="none" 
stroke="#a371f7" stroke-width="2.5"/>
+<circle cx="55" cy="169.9" r="3" fill="#a371f7"/>
+<text x="55" y="162.9" fill="#a371f7" font-size="9" 
text-anchor="middle">17</text>
+<circle cx="232" cy="147.7" r="3" fill="#a371f7"/>
+<text x="232" y="140.7" fill="#a371f7" font-size="9" 
text-anchor="middle">42</text>
+<circle cx="408" cy="165.4" r="3" fill="#a371f7"/>
+<text x="408" y="158.4" fill="#a371f7" font-size="9" 
text-anchor="middle">22</text>
+<circle cx="585" cy="155.7" r="3" fill="#a371f7"/>
+<text x="585" y="148.7" fill="#a371f7" font-size="9" 
text-anchor="middle">33</text>
+<polyline points=" 55,33.9 232,68.6 408,68.6 585,58.8" fill="none" 
stroke="#388bfd" stroke-width="2.5"/>
+<circle cx="55" cy="33.9" r="3" fill="#388bfd"/>
+<text x="55" y="26.9" fill="#388bfd" font-size="9" 
text-anchor="middle">170</text>
+<circle cx="232" cy="68.6" r="3" fill="#388bfd"/>
+<text x="232" y="61.6" fill="#388bfd" font-size="9" 
text-anchor="middle">131</text>
+<circle cx="408" cy="68.6" r="3" fill="#388bfd"/>
+<text x="408" y="61.6" fill="#388bfd" font-size="9" 
text-anchor="middle">131</text>
+<circle cx="585" cy="58.8" r="3" fill="#388bfd"/>
+<text x="585" y="51.8" fill="#388bfd" font-size="9" 
text-anchor="middle">142</text>
+<polyline points=" 55,70.3 232,66.8 408,49.9 585,48.1" fill="none" 
stroke="#2ea043" stroke-width="2.5"/>
+<circle cx="55" cy="70.3" r="3" fill="#2ea043"/>
+<text x="55" y="63.3" fill="#2ea043" font-size="9" 
text-anchor="middle">129</text>
+<circle cx="232" cy="66.8" r="3" fill="#2ea043"/>
+<text x="232" y="59.8" fill="#2ea043" font-size="9" 
text-anchor="middle">133</text>
+<circle cx="408" cy="49.9" r="3" fill="#2ea043"/>
+<text x="408" y="42.9" fill="#2ea043" font-size="9" 
text-anchor="middle">152</text>
+<circle cx="585" cy="48.1" r="3" fill="#2ea043"/>
+<text x="585" y="41.1" fill="#2ea043" font-size="9" 
text-anchor="middle">154</text>
+</svg>
+<div class="ctx"><span style="color:#a371f7">■</span> first-time &nbsp; <span 
style="color:#388bfd">■</span> contributor &nbsp; <span 
style="color:#2ea043">■</span> maintainer. Roughly <b>~48% maintainer-authored, 
~43% contributor, ~9% first-timer</b> — steady week over week. Contributor + 
first-timer inflow (~50%) is exactly the volume the triage automation 
serves.</div>
+
+<h3>Merged per week <span class="ctx">(reliable wk 0–2; wk 3 
partial)</span></h3>
+<svg viewBox="0 0 640 215" width="100%" 
style="max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px">
+<line x1="55" y1="185.0" x2="585" y2="185.0" stroke="#21262d"/>
+<text x="48" y="189" fill="#8b949e" font-size="10" text-anchor="end">0</text>
+<line x1="55" y1="145.0" x2="585" y2="145.0" stroke="#21262d"/>
+<text x="48" y="149" fill="#8b949e" font-size="10" text-anchor="end">62</text>
+<line x1="55" y1="105.0" x2="585" y2="105.0" stroke="#21262d"/>
+<text x="48" y="109" fill="#8b949e" font-size="10" text-anchor="end">125</text>
+<line x1="55" y1="65.0" x2="585" y2="65.0" stroke="#21262d"/>
+<text x="48" y="69" fill="#8b949e" font-size="10" text-anchor="end">187</text>
+<line x1="55" y1="25.0" x2="585" y2="25.0" stroke="#21262d"/>
+<text x="48" y="29" fill="#8b949e" font-size="10" text-anchor="end">250</text>
+<text x="55" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-3</text>
+<text x="232" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-2</text>
+<text x="408" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-1</text>
+<text x="585" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">now</text>
+<text x="55" y="16" fill="#e6edf3" font-size="11" font-weight="600">Merged / 
week</text>
+<polyline points=" 55,75.6 232,82.0 408,55.7 585,34.6" fill="none" 
stroke="#2ea043" stroke-width="2.5"/>
+<circle cx="55" cy="75.6" r="3" fill="#2ea043"/>
+<text x="55" y="68.6" fill="#2ea043" font-size="9" 
text-anchor="middle">171</text>
+<circle cx="232" cy="82.0" r="3" fill="#2ea043"/>
+<text x="232" y="75" fill="#2ea043" font-size="9" 
text-anchor="middle">161</text>
+<circle cx="408" cy="55.7" r="3" fill="#2ea043"/>
+<text x="408" y="48.7" fill="#2ea043" font-size="9" 
text-anchor="middle">202</text>
+<circle cx="585" cy="34.6" r="3" fill="#2ea043"/>
+<text x="585" y="27.6" fill="#2ea043" font-size="9" 
text-anchor="middle">235</text>
+</svg>
+
+<h3>Open backlog at week-end <span class="ctx">(wk 0–3 only — 4–5 
cap-distorted, omitted)</span></h3>
+<svg viewBox="0 0 640 215" width="100%" 
style="max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px">
+<line x1="55" y1="185.0" x2="585" y2="185.0" stroke="#21262d"/>
+<text x="48" y="189" fill="#8b949e" font-size="10" text-anchor="end">0</text>
+<line x1="55" y1="145.0" x2="585" y2="145.0" stroke="#21262d"/>
+<text x="48" y="149" fill="#8b949e" font-size="10" text-anchor="end">137</text>
+<line x1="55" y1="105.0" x2="585" y2="105.0" stroke="#21262d"/>
+<text x="48" y="109" fill="#8b949e" font-size="10" text-anchor="end">275</text>
+<line x1="55" y1="65.0" x2="585" y2="65.0" stroke="#21262d"/>
+<text x="48" y="69" fill="#8b949e" font-size="10" text-anchor="end">412</text>
+<line x1="55" y1="25.0" x2="585" y2="25.0" stroke="#21262d"/>
+<text x="48" y="29" fill="#8b949e" font-size="10" text-anchor="end">550</text>
+<text x="55" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-3</text>
+<text x="232" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-2</text>
+<text x="408" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">wk-1</text>
+<text x="585" y="203" fill="#8b949e" font-size="10" 
text-anchor="middle">now</text>
+<text x="55" y="16" fill="#e6edf3" font-size="11" font-weight="600">Open 
backlog at week-end</text>
+<polyline points=" 55,89.0 232,62.8 408,52.1 585,39.5" fill="none" 
stroke="#d29922" stroke-width="2.5"/>
+<circle cx="55" cy="89.0" r="3" fill="#d29922"/>
+<text x="55" y="82" fill="#d29922" font-size="9" 
text-anchor="middle">330</text>
+<circle cx="232" cy="62.8" r="3" fill="#d29922"/>
+<text x="232" y="55.8" fill="#d29922" font-size="9" 
text-anchor="middle">420</text>
+<circle cx="408" cy="52.1" r="3" fill="#d29922"/>
+<text x="408" y="45.1" fill="#d29922" font-size="9" 
text-anchor="middle">457</text>
+<circle cx="585" cy="39.5" r="3" fill="#d29922"/>
+<text x="585" y="32.5" fill="#d29922" font-size="9" 
text-anchor="middle">500</text>
+</svg>
+<div class="ctx">Even within the reliable window the backlog is climbing (330 
→ 500 across the cap-clean weeks). Note this slope is partly inflated by the 
same truncation (older closes missing); the honest read is "backlog is growing, 
magnitude needs a snapshot job to pin down." This is the strongest argument for 
the daily-snapshot Action above.</div>
+
+<h2>📋 Detailed — top areas (Total / Ready / Waiting / Untriaged / Draft)</h2>
+<table>
+<tr><th>Area</th><th>Total</th><th>Ready</th><th>Waiting</th><th>Untriaged</th><th>Draft</th></tr>
+<tr><td><code>area:providers</code></td><td>134</td><td>50</td><td>11</td><td>35</td><td>30</td></tr>
+<tr><td><code>area:task-sdk</code></td><td>106</td><td>41</td><td>9</td><td>38</td><td>18</td></tr>
+<tr><td><code>area:API</code></td><td>72</td><td>27</td><td>8</td><td>23</td><td>14</td></tr>
+<tr><td><code>(no 
area)</code></td><td>63</td><td>40</td><td>0</td><td>15</td><td>8</td></tr>
+<tr><td><code>area:dev-tools</code></td><td>47</td><td>11</td><td>1</td><td>22</td><td>13</td></tr>
+<tr><td><code>area:Scheduler</code></td><td>45</td><td>20</td><td>1</td><td>11</td><td>11</td></tr>
+<tr><td><code>area:UI</code></td><td>42</td><td>11</td><td>1</td><td>22</td><td>8</td></tr>
+<tr><td><code>area:DAG-processing</code></td><td>40</td><td>16</td><td>3</td><td>12</td><td>8</td></tr>
+</table>
+
+<div class="foot">
+<b>Bottom line:</b> the queue is review-bound, not triage-bound (211 
contributor PRs ready, 88 &gt;4 wk; inflow ~+72/wk). The cleanup measurably 
shrank the ready queue by 19. <b>~80% of the triage workflow is deterministic 
and CI-automatable</b> — the single highest-leverage move is a <b>ready-label 
GitHub Action</b> that runs the mark-ready gate continuously (humans mis-call 
it 25% of the time). Reserve human/LLM effort for code-review correctness, 
mentoring, and close-vs-keep — the ~8% [...]
+Read-only stats — no mutations. Triage detection via the <code>Pull Request 
quality criteria</code> marker. Generated by Claude Code (Opus 4.8) for @potiuk.
+</div>
+</body>
+</html>
diff --git a/tools/pr-management-stats/gen_charts.sh 
b/tools/pr-management-stats/gen_charts.sh
new file mode 100755
index 00000000..57611281
--- /dev/null
+++ b/tools/pr-management-stats/gen_charts.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: Apache-2.0
+#
+# gen_charts.sh — emit self-contained inline-SVG line charts for the
+# pr-management-stats HTML dashboard (see 
../../skills/pr-management-stats/export.md).
+#
+# No external JS/CSS — the SVG is embedded directly in dashboard.html so
+# gistpreview.github.io renders it offline-of-CDN. GitHub-dark palette,
+# gridlines, axis labels, data-point markers + value annotations.
+#
+# Usage:
+#   chart <ymax> "<title>" "<name>:<color>:<v_oldest>,...,<v_newest>" [more 
series...]
+#
+#   - X positions are evenly spaced; pass the x-axis labels in $XLABELS
+#     (oldest on the LEFT — a timeline reads past -> present).
+#   - Colours: red #da3633  amber #d29922  blue #388bfd  green #2ea043  purple 
#a371f7
+#
+# Example (4 age buckets, 2 series):
+#   XLABELS=(">12w" "8-12w" "4-8w" "2-4w" "0-2w")
+#   chart 40 "Ready PRs by why they wait (oldest left -> newest right)" \
+#     "never:#da3633:3,4,31,34,36" "approved:#2ea043:3,3,6,7,4"
+set -u
+
+# plot box
+X0=55; X1=585; YT=25; YB=185
+# default x positions for up to 5 points (override XS / XLABELS as needed)
+XS=(${XS[@]:-55 188 320 452 585})
+XLABELS=(${XLABELS[@]:-})
+
+chart() {
+  local ymax="$1" title="$2"; shift 2
+  local n=${#XS[@]}
+  echo "<svg viewBox=\"0 0 640 215\" width=\"100%\" 
style=\"max-width:640px;background:#0d1117;border:1px solid 
#30363d;border-radius:8px\">"
+  # y gridlines + labels (5 lines)
+  local i yv yp
+  for i in 0 1 2 3 4; do
+    yv=$(awk -v m="$ymax" -v i="$i" 'BEGIN{printf "%d", m*i/4}')
+    yp=$(awk -v t="$YT" -v b="$YB" -v i="$i" 'BEGIN{printf "%.1f", 
b-(b-t)*i/4}')
+    echo "<line x1=\"$X0\" y1=\"$yp\" x2=\"$X1\" y2=\"$yp\" 
stroke=\"#21262d\"/>"
+    echo "<text x=\"48\" y=\"$(awk -v y=$yp 'BEGIN{print y+4}')\" 
fill=\"#8b949e\" font-size=\"10\" text-anchor=\"end\">$yv</text>"
+  done
+  # x labels
+  for i in $(seq 0 $((n-1))); do
+    [ -n "${XLABELS[$i]:-}" ] && echo "<text x=\"${XS[$i]}\" y=\"203\" 
fill=\"#8b949e\" font-size=\"10\" text-anchor=\"middle\">${XLABELS[$i]}</text>"
+  done
+  echo "<text x=\"$X0\" y=\"16\" fill=\"#e6edf3\" font-size=\"11\" 
font-weight=\"600\">$title</text>"
+  # series
+  local s color vals va pts
+  for s in "$@"; do
+    color="${s#*:}"; color="${color%%:*}"; vals="${s##*:}"
+    IFS=',' read -ra va <<< "$vals"; pts=""
+    for i in $(seq 0 $((n-1))); do
+      yp=$(awk -v t="$YT" -v b="$YB" -v m="$ymax" -v v="${va[$i]}" 
'BEGIN{printf "%.1f", b-(b-t)*v/m}')
+      pts="$pts ${XS[$i]},$yp"
+    done
+    echo "<polyline points=\"$pts\" fill=\"none\" stroke=\"$color\" 
stroke-width=\"2.5\"/>"
+    for i in $(seq 0 $((n-1))); do
+      yp=$(awk -v t="$YT" -v b="$YB" -v m="$ymax" -v v="${va[$i]}" 
'BEGIN{printf "%.1f", b-(b-t)*v/m}')
+      echo "<circle cx=\"${XS[$i]}\" cy=\"$yp\" r=\"3\" fill=\"$color\"/>"
+      echo "<text x=\"${XS[$i]}\" y=\"$(awk -v y=$yp 'BEGIN{print y-7}')\" 
fill=\"$color\" font-size=\"9\" text-anchor=\"middle\">${va[$i]}</text>"
+    done
+  done
+  echo "</svg>"
+}
+
+# If sourced, callers use chart(); if run directly, emit a tiny demo.
+if [ "${BASH_SOURCE[0]:-}" = "${0}" ]; then
+  XLABELS=(">12w" "8-12w" "4-8w" "2-4w" "0-2w")
+  chart 40 "demo: ready PRs by why they wait (oldest left -> newest right)" \
+    "never:#da3633:3,4,31,34,36" "discussed:#388bfd:16,10,16,16,9" \
+    "changes:#d29922:1,1,5,6,0" "approved:#2ea043:3,3,6,7,4"
+fi

Reply via email to