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.git


The following commit(s) were added to refs/heads/main by this push:
     new 8c4c9908b8c Add `breeze pr stats` command for open PR statistics by 
area (#64667)
8c4c9908b8c is described below

commit 8c4c9908b8ca7c9fac0faf2b1b94bf58ae389576
Author: Jarek Potiuk <[email protected]>
AuthorDate: Fri Apr 3 23:25:45 2026 +0200

    Add `breeze pr stats` command for open PR statistics by area (#64667)
    
    Add a new `breeze pr stats` command that produces aggregate statistics
    of open PRs in the Airflow repo, split by area label. The command uses
    GraphQL to fetch all open PRs and displays two tables:
    
    1. **Triaged PRs — Final State**: Shows triaged PRs that were closed or
       merged since 2026-03-11, with counts and percentages for closed,
       merged, and author-responded.
    
    2. **Triaged PRs — Still Open**: Shows all open PRs grouped by area with
       draft/non-draft counts, contributor counts, triage status, response
       rates, ready-for-review status, time since triager drafted the PR,
       and time since the author's last interaction.
    
    Features:
    - Cached interaction data (keyed by updated_at) to avoid re-fetching
      unchanged PRs on subsequent runs
    - `--clear-cache` flag to force fresh data
    - Detects triager draft conversions via GraphQL timeline events
    - Tracks closed vs merged triaged PRs with area breakdown
    - Footer header rows on both tables for readability
---
 dev/breeze/doc/13_pr_tasks.rst                     |  10 +
 dev/breeze/doc/images/output_pr.svg                |  12 +-
 dev/breeze/doc/images/output_pr.txt                |   2 +-
 dev/breeze/doc/images/output_pr_stats.svg          | 116 ++++
 dev/breeze/doc/images/output_pr_stats.txt          |   1 +
 .../output_setup_check-all-params-in-groups.svg    |  74 ++-
 .../output_setup_check-all-params-in-groups.txt    |   2 +-
 .../output_setup_regenerate-command-images.svg     |   6 +-
 .../output_setup_regenerate-command-images.txt     |   2 +-
 .../src/airflow_breeze/commands/pr_commands.py     | 706 +++++++++++++++++++++
 .../airflow_breeze/commands/pr_commands_config.py  |  13 +-
 dev/breeze/src/airflow_breeze/utils/pr_cache.py    |   1 +
 uv.lock                                            |  14 +-
 13 files changed, 909 insertions(+), 50 deletions(-)

diff --git a/dev/breeze/doc/13_pr_tasks.rst b/dev/breeze/doc/13_pr_tasks.rst
index 4084bee61b8..d0f411f0703 100644
--- a/dev/breeze/doc/13_pr_tasks.rst
+++ b/dev/breeze/doc/13_pr_tasks.rst
@@ -440,6 +440,16 @@ Example usage
      # Verbose mode — show individual skip reasons during filtering
      breeze pr auto-triage --verbose
 
+PR statistics
+"""""""""""""
+
+The ``breeze pr stats`` command produces aggregate statistics of open PRs 
grouped by area label.
+
+.. image:: ./images/output_pr_stats.svg
+  :target: 
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/doc/images/output_pr_stats.svg
+  :width: 100%
+  :alt: Breeze PR stats
+
 -----
 
 Next step: Follow the `Advanced breeze topics 
<14_advanced_breeze_topics.rst>`__ instructions to learn how to manage GitHub
diff --git a/dev/breeze/doc/images/output_pr.svg 
b/dev/breeze/doc/images/output_pr.svg
index 97cc4d5efe9..1051b307b09 100644
--- a/dev/breeze/doc/images/output_pr.svg
+++ b/dev/breeze/doc/images/output_pr.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 318.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 342.79999999999995" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -42,7 +42,7 @@
 
     <defs>
     <clipPath id="breeze-pr-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="267.4" />
+      <rect x="0" y="0" width="1463.0" height="291.79999999999995" />
     </clipPath>
     <clipPath id="breeze-pr-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -74,9 +74,12 @@
 <clipPath id="breeze-pr-line-9">
     <rect x="0" y="221.1" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-pr-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="316.4" rx="8"/><text class="breeze-pr-title" 
fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;pr</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="340.8" rx="8"/><text class="breeze-pr-title" 
fill="#c5c8c6" text-anchor="middle" x="740" y="27">Command:&#160;pr</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -96,7 +99,8 @@
 </text><text class="breeze-pr-r5" x="0" y="190.8" textLength="1464" 
clip-path="url(#breeze-pr-line-7)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-pr-r1" x="1464" y="190.8" textLength="12.2" 
clip-path="url(#breeze-pr-line-7)">
 </text><text class="breeze-pr-r5" x="0" y="215.2" textLength="24.4" 
clip-path="url(#breeze-pr-line-8)">╭─</text><text class="breeze-pr-r5" x="24.4" 
y="215.2" textLength="158.6" 
clip-path="url(#breeze-pr-line-8)">&#160;PR&#160;commands&#160;</text><text 
class="breeze-pr-r5" x="183" y="215.2" textLength="1256.6" 
clip-path="url(#breeze-pr-line-8)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="breeze-pr-r5" x="1439. [...]
 </text><text class="breeze-pr-r5" x="0" y="239.6" textLength="12.2" 
clip-path="url(#breeze-pr-line-9)">│</text><text class="breeze-pr-r4" x="24.4" 
y="239.6" textLength="158.6" 
clip-path="url(#breeze-pr-line-9)">auto-triage&#160;&#160;</text><text 
class="breeze-pr-r1" x="207.4" y="239.6" textLength="1232.2" 
clip-path="url(#breeze-pr-line-9)">Find&#160;open&#160;PRs&#160;from&#160;non-collaborators&#160;that&#160;don&#x27;t&#160;meet&#160;quality&#160;criteria&#160;and&#160;convert&#160;to
 [...]
-</text><text class="breeze-pr-r5" x="0" y="264" textLength="1464" 
clip-path="url(#breeze-pr-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-pr-r1" x="1464" y="264" textLength="12.2" 
clip-path="url(#breeze-pr-line-10)">
+</text><text class="breeze-pr-r5" x="0" y="264" textLength="12.2" 
clip-path="url(#breeze-pr-line-10)">│</text><text class="breeze-pr-r4" x="24.4" 
y="264" textLength="158.6" 
clip-path="url(#breeze-pr-line-10)">stats&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-pr-r1" x="207.4" y="264" textLength="1232.2" 
clip-path="url(#breeze-pr-line-10)">Show&#160;statistics&#160;of&#160;open&#160;PRs&#160;grouped&#160;by&#160;area&#160;label.&#160;&#160;&#160;&#160;&#160;&#
 [...]
+</text><text class="breeze-pr-r5" x="0" y="288.4" textLength="1464" 
clip-path="url(#breeze-pr-line-11)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-pr-r1" x="1464" y="288.4" textLength="12.2" 
clip-path="url(#breeze-pr-line-11)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_pr.txt 
b/dev/breeze/doc/images/output_pr.txt
index 070bb74360c..37a9b1ce0a6 100644
--- a/dev/breeze/doc/images/output_pr.txt
+++ b/dev/breeze/doc/images/output_pr.txt
@@ -1 +1 @@
-e48502d9b2f145e867ce6608e8cd8c9d
+38d9d08de73f3c9bedb3ed7d3739b2c2
diff --git a/dev/breeze/doc/images/output_pr_stats.svg 
b/dev/breeze/doc/images/output_pr_stats.svg
new file mode 100644
index 00000000000..3a6f8e38ade
--- /dev/null
+++ b/dev/breeze/doc/images/output_pr_stats.svg
@@ -0,0 +1,116 @@
+<svg class="rich-terminal" viewBox="0 0 1482 391.59999999999997" 
xmlns="http://www.w3.org/2000/svg";>
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff";)
 format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff";)
 format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .breeze-pr-stats-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .breeze-pr-stats-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .breeze-pr-stats-r1 { fill: #c5c8c6 }
+.breeze-pr-stats-r2 { fill: #d0b344 }
+.breeze-pr-stats-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-pr-stats-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-pr-stats-r5 { fill: #868887 }
+.breeze-pr-stats-r6 { fill: #8d7b39 }
+.breeze-pr-stats-r7 { fill: #98a84b;font-weight: bold }
+    </style>
+
+    <defs>
+    <clipPath id="breeze-pr-stats-clip-terminal">
+      <rect x="0" y="0" width="1463.0" height="340.59999999999997" />
+    </clipPath>
+    <clipPath id="breeze-pr-stats-line-0">
+    <rect x="0" y="1.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-1">
+    <rect x="0" y="25.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-2">
+    <rect x="0" y="50.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-3">
+    <rect x="0" y="74.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-4">
+    <rect x="0" y="99.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-5">
+    <rect x="0" y="123.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-6">
+    <rect x="0" y="147.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-7">
+    <rect x="0" y="172.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-8">
+    <rect x="0" y="196.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-9">
+    <rect x="0" y="221.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-11">
+    <rect x="0" y="269.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-pr-stats-line-12">
+    <rect x="0" y="294.3" width="1464" height="24.65"/>
+            </clipPath>
+    </defs>
+
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="389.6" rx="8"/><text 
class="breeze-pr-stats-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;pr&#160;stats</text>
+            <g transform="translate(26,22)">
+            <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
+            <circle cx="22" cy="0" r="7" fill="#febc2e"/>
+            <circle cx="44" cy="0" r="7" fill="#28c840"/>
+            </g>
+        
+    <g transform="translate(9, 41)" 
clip-path="url(#breeze-pr-stats-clip-terminal)">
+    
+    <g class="breeze-pr-stats-matrix">
+    <text class="breeze-pr-stats-r1" x="1464" y="20" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-0)">
+</text><text class="breeze-pr-stats-r2" x="12.2" y="44.4" textLength="73.2" 
clip-path="url(#breeze-pr-stats-line-1)">Usage:</text><text 
class="breeze-pr-stats-r3" x="97.6" y="44.4" textLength="183" 
clip-path="url(#breeze-pr-stats-line-1)">breeze&#160;pr&#160;stats</text><text 
class="breeze-pr-stats-r1" x="292.8" y="44.4" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-1)">[</text><text 
class="breeze-pr-stats-r4" x="305" y="44.4" textLength="85.4" 
clip-path="url(#breeze-pr-stats-li [...]
+</text><text class="breeze-pr-stats-r1" x="1464" y="68.8" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-2)">
+</text><text class="breeze-pr-stats-r1" x="12.2" y="93.2" textLength="610" 
clip-path="url(#breeze-pr-stats-line-3)">Show&#160;statistics&#160;of&#160;open&#160;PRs&#160;grouped&#160;by&#160;area&#160;label.</text><text
 class="breeze-pr-stats-r1" x="1464" y="93.2" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-3)">
+</text><text class="breeze-pr-stats-r1" x="1464" y="117.6" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-4)">
+</text><text class="breeze-pr-stats-r5" x="0" y="142" textLength="24.4" 
clip-path="url(#breeze-pr-stats-line-5)">╭─</text><text 
class="breeze-pr-stats-r5" x="24.4" y="142" textLength="109.8" 
clip-path="url(#breeze-pr-stats-line-5)">&#160;Options&#160;</text><text 
class="breeze-pr-stats-r5" x="134.2" y="142" textLength="1305.4" 
clip-path="url(#breeze-pr-stats-line-5)">───────────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 cl [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="166.4" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-6)">│</text><text 
class="breeze-pr-stats-r4" x="24.4" y="166.4" textLength="231.8" 
clip-path="url(#breeze-pr-stats-line-6)">--batch-size&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-pr-stats-r1" x="329.4" y="166.4" textLength="500.2" 
clip-path="url(#breeze-pr-stats-line-6)">Number&#160;of&#160;PRs&#160;to&#160;fetch&#160;per&#160;GraphQL&#160;page.&#160;</
 [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="190.8" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-7)">│</text><text 
class="breeze-pr-stats-r4" x="24.4" y="190.8" textLength="231.8" 
clip-path="url(#breeze-pr-stats-line-7)">--clear-cache&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-pr-stats-r1" x="329.4" y="190.8" textLength="561.2" 
clip-path="url(#breeze-pr-stats-line-7)">Clear&#160;cached&#160;interaction&#160;data&#160;before&#160;fetching.</text><text
 clas [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="215.2" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-8)">│</text><text 
class="breeze-pr-stats-r4" x="24.4" y="215.2" textLength="231.8" 
clip-path="url(#breeze-pr-stats-line-8)">--github-token&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-pr-stats-r1" x="329.4" y="215.2" textLength="512.4" 
clip-path="url(#breeze-pr-stats-line-8)">The&#160;token&#160;used&#160;to&#160;authenticate&#160;to&#160;GitHub.&#160;</text><text
 cla [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="239.6" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-9)">│</text><text 
class="breeze-pr-stats-r4" x="24.4" y="239.6" textLength="231.8" 
clip-path="url(#breeze-pr-stats-line-9)">--github-repository</text><text 
class="breeze-pr-stats-r7" x="280.6" y="239.6" textLength="24.4" 
clip-path="url(#breeze-pr-stats-line-9)">-g</text><text 
class="breeze-pr-stats-r1" x="329.4" y="239.6" textLength="597.8" 
clip-path="url(#breeze-pr-stats-line-9 [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="264" textLength="1464" 
clip-path="url(#breeze-pr-stats-line-10)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-pr-stats-r1" x="1464" y="264" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-10)">
+</text><text class="breeze-pr-stats-r5" x="0" y="288.4" textLength="24.4" 
clip-path="url(#breeze-pr-stats-line-11)">╭─</text><text 
class="breeze-pr-stats-r5" x="24.4" y="288.4" textLength="195.2" 
clip-path="url(#breeze-pr-stats-line-11)">&#160;Common&#160;options&#160;</text><text
 class="breeze-pr-stats-r5" x="219.6" y="288.4" textLength="1220" 
clip-path="url(#breeze-pr-stats-line-11)">────────────────────────────────────────────────────────────────────────────────────────────────────</t
 [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="312.8" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-12)">│</text><text 
class="breeze-pr-stats-r4" x="24.4" y="312.8" textLength="73.2" 
clip-path="url(#breeze-pr-stats-line-12)">--help</text><text 
class="breeze-pr-stats-r7" x="122" y="312.8" textLength="24.4" 
clip-path="url(#breeze-pr-stats-line-12)">-h</text><text 
class="breeze-pr-stats-r1" x="170.8" y="312.8" textLength="329.4" 
clip-path="url(#breeze-pr-stats-line-12)">Show&#160 [...]
+</text><text class="breeze-pr-stats-r5" x="0" y="337.2" textLength="1464" 
clip-path="url(#breeze-pr-stats-line-13)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-pr-stats-r1" x="1464" y="337.2" textLength="12.2" 
clip-path="url(#breeze-pr-stats-line-13)">
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_pr_stats.txt 
b/dev/breeze/doc/images/output_pr_stats.txt
new file mode 100644
index 00000000000..8d6aa1f2ce1
--- /dev/null
+++ b/dev/breeze/doc/images/output_pr_stats.txt
@@ -0,0 +1 @@
+1bcfe84fbd67e3e013325b945b3a8d8b
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg 
b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
index b33ae7f03e5..b43dd779612 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 1148.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 1172.3999999999999" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -43,7 +43,7 @@
 
     <defs>
     <clipPath id="breeze-setup-check-all-params-in-groups-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="1097.0" />
+      <rect x="0" y="0" width="1463.0" height="1121.3999999999999" />
     </clipPath>
     <clipPath id="breeze-setup-check-all-params-in-groups-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -177,9 +177,12 @@
 <clipPath id="breeze-setup-check-all-params-in-groups-line-43">
     <rect x="0" y="1050.7" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-setup-check-all-params-in-groups-line-44">
+    <rect x="0" y="1075.1" width="1464" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1146" rx="8"/><text 
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;setup&#160;check-all-params-in-groups</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="1170.4" rx="8"/><text 
class="breeze-setup-check-all-params-in-groups-title" fill="#c5c8c6" 
text-anchor="middle" x="740" 
y="27">Command:&#160;setup&#160;check-all-params-in-groups</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -202,38 +205,39 @@
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="264" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-10)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="264" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-10)">|&#160;generate-migration-file&#160;|&#160;issues&#160;|&#160;issues:unassign&#160;|&#160;k8s&#160;|&#160;k8s:build-k8s-image&#160;|&#160;k8s:configure-cluster&#16
 [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="288.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-11)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="288.4" 
textLength="1171.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-11)">|&#160;k8s:create-cluster&#160;|&#160;k8s:delete-cluster&#160;|&#160;k8s:deploy-airflow&#160;|&#160;k8s:dev&#160;|&#160;k8s:k9s&#160;|&#160;k8s:logs&#160;|&#
 [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="312.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-12)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="312.8" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-12)">k8s:run-complete-tests&#160;|&#160;k8s:setup-env&#160;|&#160;k8s:shell&#160;|&#160;k8s:status&#160;|&#160;k8s:tests&#160;|&#160;k8s:upload-k8s-image&#160;|&#16
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="337.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="337.2" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">pr:auto-triage&#160;|&#160;prod-image&#160;|&#160;prod-image:build&#160;|&#160;prod-image:load&#160;|&#160;prod-image:pull&#160;|&#160;prod-image:save&#160;|
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="361.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="361.6" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">prod-image:verify&#160;|&#160;registry&#160;|&#160;registry:backfill&#160;|&#160;registry:extract-data&#160;|&#160;registry:publish-versions&#160;|&#160;</te
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="386" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">release-management&#160;|&#160;release-management:add-back-references&#160;|&#160;release-management:check-release-files&#160;|&#160;</text><text
 class="breeze-s [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="410.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="410.4" 
textLength="1183.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">release-management:clean-old-provider-artifacts&#160;|&#160;release-management:constraints-version-check&#160;|&#160;</text><text
 class="breeze-setup-check-a [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="434.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="434.8" 
textLength="1012.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">release-management:create-minor-branch&#160;|&#160;release-management:generate-constraints&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-g [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="459.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="459.2" 
textLength="1268.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">release-management:generate-issue-content-core&#160;|&#160;release-management:generate-issue-content-helm-chart&#160;|&#160;</text><text
 class="breeze-setup- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="483.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="483.6" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">release-management:generate-issue-content-providers&#160;|&#160;release-management:generate-providers-metadata&#160;|&#160;</text><text
 class="breeze-setup-c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="508" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="508" 
textLength="1110.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">release-management:install-provider-distributions&#160;|&#160;release-management:merge-prod-images&#160;|&#160;</text><text
 class="breeze-setup-check-all-params- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="532.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:prepare-airflow-ctl-distributions&#160;|&#160;release-management:prepare-airflow-distributions&#160;|</text><text
 class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="556.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8" 
textLength="1171.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:prepare-helm-chart-package&#160;|&#160;release-management:prepare-helm-chart-tarball&#160;|&#160;</text><text
 class="breeze-setup-check-al [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="581.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2" 
textLength="1268.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:prepare-provider-distributions&#160;|&#160;release-management:prepare-provider-documentation&#160;|&#160;</text><text
 class="breeze-setup- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="605.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" 
textLength="976" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-python-client&#160;|&#160;release-management:prepare-tarball&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-groups- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" 
textLength="1049.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-task-sdk-distributions&#160;|&#160;release-management:publish-docs&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-gr [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="654.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" 
textLength="988.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:publish-docs-to-s3&#160;|&#160;release-management:release-prod-images&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-grou [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="678.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:start-rc-process&#160;|&#160;release-management:start-release&#160;|&#160;release-management:tag-providers</text><text
 class="breeze-setup-c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="703.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" 
textLength="1134.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">|&#160;release-management:update-constraints&#160;|&#160;release-management:update-providers-next-version&#160;|&#160;</text><text
 class="breeze-setup-check- [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="727.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" 
textLength="1244.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">release-management:verify-provider-distributions&#160;|&#160;release-management:verify-rc-by-pmc&#160;|&#160;run&#160;|&#160;sbom&#160;|&#160;</text><text
 cl [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">sbom:build-all-airflow-images&#160;|&#160;sbom:export-dependency-information&#160;|&#160;sbom:generate-providers-requirements</text><text
 class="breeze-setup-check [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="776.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" 
textLength="1183.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">|&#160;sbom:update-sbom-information&#160;|&#160;setup&#160;|&#160;setup:autocomplete&#160;|&#160;setup:check-all-params-in-groups&#160;|&#160;</text><text
 cl [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="800.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">setup:config&#160;|&#160;setup:regenerate-command-images&#160;|&#160;setup:self-upgrade&#160;|&#160;setup:synchronize-local-mounts&#160;|&#160;</text><text
 c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="825.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" 
textLength="1098" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;testing:airflow-ctl-integration-tests&#160;|&#160;</text><text
 class= [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="849.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6" 
textLength="1085.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">testing:airflow-ctl-tests&#160;|&#160;testing:airflow-e2e-tests&#160;|&#160;testing:core-integration-tests&#160;|&#160;</text><text
 class="breeze-setup-check [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874" 
textLength="890.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-groups [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="898.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4" 
textLength="1195.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;</text><text
 class="breeze-se [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="922.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="922.8" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests</text><text
 class="bree [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="947.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="947.2" 
textLength="1268.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs)</text><
 [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="971.6" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="971.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996" 
textLength="24.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">╭─</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="996" 
textLength="195.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">&#160;Common&#160;options&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="996" 
textLength="1220" clip-path="url(#breeze-setup-ch [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1020.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1020.4" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">--verbose</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1020.4" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1044.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1044.8" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--dry-run</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1069.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1069.2" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">--help&#160;&#160;&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1069.2" 
textLength="24.4" clip-path="url(#breeze-setup-c [...]
-</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1093.6" textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1093.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="337.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="337.2" 
textLength="1171.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-13)">pr:auto-triage&#160;|&#160;pr:stats&#160;|&#160;prod-image&#160;|&#160;prod-image:build&#160;|&#160;prod-image:load&#160;|&#160;prod-image:pull&#160;|&#160;<
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="361.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="361.6" 
textLength="1134.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-14)">prod-image:save&#160;|&#160;prod-image:verify&#160;|&#160;registry&#160;|&#160;registry:backfill&#160;|&#160;registry:extract-data&#160;|&#160;</text><text
 c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="386" 
textLength="1098" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-15)">registry:publish-versions&#160;|&#160;release-management&#160;|&#160;release-management:add-back-references&#160;|&#160;</text><text
 class="breeze-setup-check-all- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="410.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="410.4" 
textLength="1110.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-16)">release-management:check-release-files&#160;|&#160;release-management:clean-old-provider-artifacts&#160;|&#160;</text><text
 class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="434.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="434.8" 
textLength="1073.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-17)">release-management:constraints-version-check&#160;|&#160;release-management:create-minor-branch&#160;|&#160;</text><text
 class="breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="459.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="459.2" 
textLength="1110.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-18)">release-management:generate-constraints&#160;|&#160;release-management:generate-issue-content-core&#160;|&#160;</text><text
 class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="483.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="483.6" 
textLength="671" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-19)">release-management:generate-issue-content-helm-chart&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y="483.6" 
textLength [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="508" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="508" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-20)">release-management:generate-issue-content-providers&#160;|&#160;release-management:generate-providers-metadata&#160;|&#160;</text><text
 class="breeze-setup-check [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="532.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="532.4" 
textLength="1110.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-21)">release-management:install-provider-distributions&#160;|&#160;release-management:merge-prod-images&#160;|&#160;</text><text
 class="breeze-setup-check-all-par [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="556.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="556.8" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-22)">release-management:prepare-airflow-ctl-distributions&#160;|&#160;release-management:prepare-airflow-distributions&#160;|</text><text
 class="breeze-setup-check- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="581.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="581.2" 
textLength="1171.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-23)">release-management:prepare-helm-chart-package&#160;|&#160;release-management:prepare-helm-chart-tarball&#160;|&#160;</text><text
 class="breeze-setup-check-al [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="605.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="605.6" 
textLength="1268.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-24)">release-management:prepare-provider-distributions&#160;|&#160;release-management:prepare-provider-documentation&#160;|&#160;</text><text
 class="breeze-setup- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="630" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="630" 
textLength="976" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-25)">release-management:prepare-python-client&#160;|&#160;release-management:prepare-tarball&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5"  [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="654.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="654.4" 
textLength="1049.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-26)">release-management:prepare-task-sdk-distributions&#160;|&#160;release-management:publish-docs&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-i [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="678.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="678.8" 
textLength="988.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-27)">release-management:publish-docs-to-s3&#160;|&#160;release-management:release-prod-images&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-grou [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="703.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="703.2" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-28)">release-management:start-rc-process&#160;|&#160;release-management:start-release&#160;|&#160;release-management:tag-providers</text><text
 class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="727.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="727.6" 
textLength="1134.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-29)">|&#160;release-management:update-constraints&#160;|&#160;release-management:update-providers-next-version&#160;|&#160;</text><text
 class="breeze-setup-check- [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="752" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="752" 
textLength="1244.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-30)">release-management:verify-provider-distributions&#160;|&#160;release-management:verify-rc-by-pmc&#160;|&#160;run&#160;|&#160;sbom&#160;|&#160;</text><text
 class= [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="776.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="776.4" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-31)">sbom:build-all-airflow-images&#160;|&#160;sbom:export-dependency-information&#160;|&#160;sbom:generate-providers-requirements</text><text
 class="breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="800.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="800.8" 
textLength="1183.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-32)">|&#160;sbom:update-sbom-information&#160;|&#160;setup&#160;|&#160;setup:autocomplete&#160;|&#160;setup:check-all-params-in-groups&#160;|&#160;</text><text
 cl [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="825.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="825.2" 
textLength="1256.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-33)">setup:config&#160;|&#160;setup:regenerate-command-images&#160;|&#160;setup:self-upgrade&#160;|&#160;setup:synchronize-local-mounts&#160;|&#160;</text><text
 c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="849.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="849.6" 
textLength="1098" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-34)">setup:version&#160;|&#160;shell&#160;|&#160;start-airflow&#160;|&#160;testing&#160;|&#160;testing:airflow-ctl-integration-tests&#160;|&#160;</text><text
 class= [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="874" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="874" 
textLength="1085.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-35)">testing:airflow-ctl-tests&#160;|&#160;testing:airflow-e2e-tests&#160;|&#160;testing:core-integration-tests&#160;|&#160;</text><text
 class="breeze-setup-check-all [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="898.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="898.4" 
textLength="890.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-36)">testing:core-tests&#160;|&#160;testing:docker-compose-tests&#160;|&#160;testing:helm-tests&#160;|&#160;</text><text
 class="breeze-setup-check-all-params-in-gr [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="922.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="922.8" 
textLength="1195.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-37)">testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;</text><text
 class="breeze-se [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="947.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="947.2" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-38)">testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests</text><text
 class="bree [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="971.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r6" x="158.6" y="971.6" 
textLength="1268.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs)</text><
 [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="996" 
textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="996" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1020.4" textLength="24.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">╭─</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="1020.4" 
textLength="195.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">&#160;Common&#160;options&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="1020.4" 
textLength="1220" clip-path="url(#breeze [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1044.8" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1044.8" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">--verbose</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1044.8" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1069.2" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1069.2" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">--dry-run</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1069.2" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1093.6" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1093.6" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-44)">--help&#160;&#160;&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1093.6" 
textLength="24.4" clip-path="url(#breeze-setup-c [...]
+</text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" y="1118" 
textLength="1464" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-check-all-params-in-groups-r1" x="1464" y="1118" 
textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">
 </text>
     </g>
     </g>
diff --git a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt 
b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
index 4b4b042063c..bd0ef796d4b 100644
--- a/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
+++ b/dev/breeze/doc/images/output_setup_check-all-params-in-groups.txt
@@ -1 +1 @@
-acfe23e5b4622df765994caf52f455d2
+eba55480dd7f88affd08c6f609ad8c40
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg 
b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
index d44be5b47ad..2d6b2bae76c 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -221,9 +221,9 @@
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="288.4" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-11)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="288.4" 
textLength="1122.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-11)">cleanup&#160;|&#160;doctor&#160;|&#160;down&#160;|&#160;exec&#160;|&#160;generate-migration-file&#160;|&#160;issues&#160;|&#160;issues:unassign&#160;|&#160;k8s&#
 [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="312.8" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-12)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="312.8" 
textLength="1073.6" 
clip-path="url(#breeze-setup-regenerate-command-images-line-12)">k8s:build-k8s-image&#160;|&#160;k8s:configure-cluster&#160;|&#160;k8s:create-cluster&#160;|&#160;k8s:delete-cluster&#160;|&#160;</text><text
 class="breeze-setup- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="337.2" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-13)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="337.2" 
textLength="1244.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-13)">k8s:deploy-airflow&#160;|&#160;k8s:dev&#160;|&#160;k8s:k9s&#160;|&#160;k8s:logs&#160;|&#160;k8s:run-complete-tests&#160;|&#160;k8s:setup-env&#160;|&#160;k8s:shel
 [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="361.6" 
textLength="1244.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">|&#160;k8s:status&#160;|&#160;k8s:tests&#160;|&#160;k8s:upload-k8s-image&#160;|&#160;pr&#160;|&#160;pr:auto-triage&#160;|&#160;prod-image&#160;|&#160;prod-image:
 [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="386" 
textLength="1061.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">|&#160;prod-image:load&#160;|&#160;prod-image:pull&#160;|&#160;prod-image:save&#160;|&#160;prod-image:verify&#160;|&#160;registry&#160;|&#160;</text><text
 class="bre [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="410.4" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="410.4" 
textLength="1134.6" 
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">registry:backfill&#160;|&#160;registry:extract-data&#160;|&#160;registry:publish-versions&#160;|&#160;release-management&#160;|&#160;</text><text
 class="breeze-s [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="361.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="361.6" 
textLength="1171.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-14)">|&#160;k8s:status&#160;|&#160;k8s:tests&#160;|&#160;k8s:upload-k8s-image&#160;|&#160;pr&#160;|&#160;pr:auto-triage&#160;|&#160;pr:stats&#160;|&#160;prod-image&#1
 [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="386" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="386" 
textLength="1244.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-15)">prod-image:build&#160;|&#160;prod-image:load&#160;|&#160;prod-image:pull&#160;|&#160;prod-image:save&#160;|&#160;prod-image:verify&#160;|&#160;registry&#160;</text><
 [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="410.4" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="410.4" 
textLength="1159" 
clip-path="url(#breeze-setup-regenerate-command-images-line-16)">|&#160;registry:backfill&#160;|&#160;registry:extract-data&#160;|&#160;registry:publish-versions&#160;|&#160;release-management&#160;|&#160;</text><text
 class="bre [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="434.8" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-17)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="434.8" 
textLength="1000.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-17)">release-management:add-back-references&#160;|&#160;release-management:check-release-files&#160;|&#160;</text><text
 class="breeze-setup-regenerate-command-images- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="459.2" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-18)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="459.2" 
textLength="1183.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-18)">release-management:clean-old-provider-artifacts&#160;|&#160;release-management:constraints-version-check&#160;|&#160;</text><text
 class="breeze-setup-regenerate- [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="483.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-19)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="483.6" 
textLength="1012.6" 
clip-path="url(#breeze-setup-regenerate-command-images-line-19)">release-management:create-minor-branch&#160;|&#160;release-management:generate-constraints&#160;|&#160;</text><text
 class="breeze-setup-regenerate-command-images [...]
diff --git a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt 
b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
index 5cb537dfca9..45cb2a115ab 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.txt
@@ -1 +1 @@
-cd2b1818493fedb67afad21a6a46f98d
+3b18680cf71a6fe2ac14af44b029a537
diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands.py 
b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
index 104f4e9b062..bdf810ab3e0 100644
--- a/dev/breeze/src/airflow_breeze/commands/pr_commands.py
+++ b/dev/breeze/src/airflow_breeze/commands/pr_commands.py
@@ -10951,3 +10951,709 @@ def auto_triage(
     import os as _os
 
     _os._exit(0)
+
+
+# ---------------------------------------------------------------------------
+# breeze pr stats — aggregate statistics of open PRs by area
+# ---------------------------------------------------------------------------
+
+_AGE_BUCKET_LABELS = ["<1w", "1-2w", "2-4w", ">1m"]
+
+_AGE_BUCKET_THRESHOLDS: list[tuple[int, str]] = [
+    (7, "<1w"),
+    (14, "1-2w"),
+    (28, "2-4w"),
+]
+
+
+_DRAFT_AGE_BUCKET_LABELS = ["<1w", "1-2w", "2-4w", ">1m"]
+
+
+@dataclass
+class _AreaStats:
+    """Aggregated statistics for a single area label."""
+
+    total: int = 0
+    drafts: int = 0
+    non_drafts: int = 0
+    contributors: int = 0  # non-collaborator authors
+    triaged_waiting: int = 0
+    triaged_responded: int = 0
+    ready_for_review: int = 0
+    triager_drafted: int = 0
+    draft_age_buckets: dict[str, int] = field(
+        default_factory=lambda: {b: 0 for b in _DRAFT_AGE_BUCKET_LABELS}
+    )
+    age_buckets: dict[str, int] = field(default_factory=lambda: {b: 0 for b in 
_AGE_BUCKET_LABELS})
+
+
+def _compute_age_bucket(last_interaction_iso: str) -> str:
+    """Map an ISO-8601 datetime string to an age bucket label."""
+    from datetime import datetime, timezone
+
+    try:
+        dt = datetime.fromisoformat(last_interaction_iso.replace("Z", 
"+00:00"))
+    except (ValueError, AttributeError):
+        return ">1m"
+    delta = datetime.now(tz=timezone.utc) - dt
+    days = delta.total_seconds() / 86400
+    for threshold_days, label in _AGE_BUCKET_THRESHOLDS:
+        if days < threshold_days:
+            return label
+    return ">1m"
+
+
+def _fetch_all_open_prs(token: str, github_repository: str, batch_size: int = 
100) -> list[PRData]:
+    """Fetch all open PRs from the repository, paginating through all 
results."""
+    console = get_console()
+    all_prs: list[PRData] = []
+    after_cursor: str | None = None
+    page = 0
+
+    while True:
+        page += 1
+        prs, has_next_page, end_cursor, total_count = _fetch_prs_graphql(
+            token,
+            github_repository,
+            labels=(),
+            exclude_labels=(),
+            filter_user=None,
+            sort="created-desc",
+            batch_size=batch_size,
+            after_cursor=after_cursor,
+            quiet=page > 1,
+        )
+        all_prs.extend(prs)
+        if page == 1:
+            console.print(f"[info]Total open PRs: {total_count}[/]")
+        else:
+            console.print(f"[info]  Fetched page {page}: 
{len(all_prs)}/{total_count} PRs[/]")
+
+        if not has_next_page:
+            break
+        after_cursor = end_cursor
+
+    return all_prs
+
+
+@dataclass
+class _PRInteractionData:
+    """Interaction data for a single PR."""
+
+    last_interaction: str  # ISO-8601 datetime of last author interaction
+    drafted_by_triager_at: str  # ISO-8601 datetime when triager converted to 
draft, or ""
+
+
+def _fetch_pr_interaction_data(
+    token: str, github_repository: str, prs: list[PRData], viewer_login: str
+) -> dict[int, _PRInteractionData]:
+    """Fetch interaction data for each PR: last author activity and triager 
draft conversion.
+
+    Uses a file-based cache keyed by PR number and ``updated_at`` — if a PR 
hasn't
+    been updated since the last run, the cached data is reused.
+    """
+    from airflow_breeze.utils.pr_cache import stats_interaction_cache
+
+    if not prs:
+        return {}
+
+    owner, repo = github_repository.split("/", 1)
+    result: dict[int, _PRInteractionData] = {}
+
+    # Check cache — only fetch PRs whose updated_at changed since last cache 
write
+    uncached_prs: list[PRData] = []
+    for pr in prs:
+        cached = stats_interaction_cache.get(
+            github_repository, f"pr_{pr.number}", match={"updated_at": 
pr.updated_at}
+        )
+        if cached and "last_interaction" in cached:
+            result[pr.number] = _PRInteractionData(
+                last_interaction=cached["last_interaction"],
+                drafted_by_triager_at=cached.get("drafted_by_triager_at", ""),
+            )
+        else:
+            uncached_prs.append(pr)
+
+    console = get_console()
+    cache_hits = len(prs) - len(uncached_prs)
+    console.print(
+        f"[info]Fetching PR interaction data: {cache_hits}/{len(prs)} cached, "
+        f"{len(uncached_prs)} to fetch...[/]"
+    )
+
+    if not uncached_prs:
+        return result
+
+    chunk_size = _COMMITS_BEHIND_BATCH_SIZE  # reuse existing batch constant 
(20)
+
+    for chunk_start in range(0, len(uncached_prs), chunk_size):
+        chunk = uncached_prs[chunk_start : chunk_start + chunk_size]
+
+        pr_fields = []
+        for pr in chunk:
+            alias = f"pr{pr.number}"
+            pr_fields.append(
+                f"    {alias}: pullRequest(number: {pr.number}) {{\n"
+                f"      comments(last: 10) {{\n"
+                f"        nodes {{ author {{ login }} createdAt }}\n"
+                f"      }}\n"
+                f"      commits(last: 1) {{\n"
+                f"        nodes {{ commit {{ committedDate }} }}\n"
+                f"      }}\n"
+                f"      timelineItems(last: 10, itemTypes: 
CONVERT_TO_DRAFT_EVENT) {{\n"
+                f"        nodes {{\n"
+                f"          ... on ConvertToDraftEvent {{\n"
+                f"            createdAt\n"
+                f"            actor {{ login }}\n"
+                f"          }}\n"
+                f"        }}\n"
+                f"      }}\n"
+                f"    }}"
+            )
+
+        query = (
+            f'query {{\n  repository(owner: "{owner}", name: "{repo}") {{\n'
+            + "\n".join(pr_fields)
+            + "\n  }\n}"
+        )
+
+        try:
+            data = _graphql_request(token, query, {})
+        except SystemExit:
+            # On API failure, fall back to updated_at for this chunk
+            for pr in chunk:
+                result[pr.number] = _PRInteractionData(
+                    last_interaction=pr.updated_at or pr.created_at,
+                    drafted_by_triager_at="",
+                )
+            continue
+
+        repo_data = data.get("repository", {})
+        for pr in chunk:
+            alias = f"pr{pr.number}"
+            pr_data = repo_data.get(alias) or {}
+
+            # Start with PR creation as the baseline
+            latest = pr.created_at
+
+            # Check last commit date
+            commits = pr_data.get("commits", {}).get("nodes", [])
+            if commits:
+                commit_date = commits[0].get("commit", 
{}).get("committedDate", "")
+                if commit_date and commit_date > latest:
+                    latest = commit_date
+
+            # Check last comment from the PR author
+            comments = pr_data.get("comments", {}).get("nodes", [])
+            for comment in reversed(comments):
+                comment_author = (comment.get("author") or {}).get("login", "")
+                if comment_author == pr.author_login:
+                    comment_date = comment.get("createdAt", "")
+                    if comment_date and comment_date > latest:
+                        latest = comment_date
+                    break
+
+            # Check if the triager (viewer) converted this PR to draft
+            drafted_by_triager_at = ""
+            timeline_nodes = pr_data.get("timelineItems", {}).get("nodes", [])
+            for event in reversed(timeline_nodes):
+                actor = (event.get("actor") or {}).get("login", "")
+                if actor == viewer_login:
+                    drafted_by_triager_at = event.get("createdAt", "")
+                    break
+
+            result[pr.number] = _PRInteractionData(
+                last_interaction=latest,
+                drafted_by_triager_at=drafted_by_triager_at,
+            )
+            # Cache the result keyed by updated_at so it invalidates on any PR 
change
+            stats_interaction_cache.save(
+                github_repository,
+                f"pr_{pr.number}",
+                {
+                    "updated_at": pr.updated_at,
+                    "last_interaction": latest,
+                    "drafted_by_triager_at": drafted_by_triager_at,
+                },
+            )
+
+        if chunk_start + chunk_size < len(uncached_prs):
+            console.print(
+                f"[info]  Fetched interactions: "
+                f"{min(chunk_start + chunk_size, 
len(uncached_prs))}/{len(uncached_prs)}[/]"
+            )
+
+    return result
+
+
+def _aggregate_stats_by_area(
+    prs: list[PRData],
+    triage_classification: dict[str, set[int]],
+    interaction_data: dict[int, _PRInteractionData],
+) -> dict[str, _AreaStats]:
+    """Aggregate PR statistics grouped by area: labels."""
+    waiting = triage_classification.get("waiting", set())
+    responded = triage_classification.get("responded", set())
+    area_stats: dict[str, _AreaStats] = {}
+
+    for pr in prs:
+        # Extract area labels
+        areas = [label.removeprefix("area:") for label in pr.labels if 
label.startswith("area:")]
+        if not areas:
+            areas = ["(no area)"]
+
+        pr_data = interaction_data.get(pr.number)
+        last_interaction = pr_data.last_interaction if pr_data else 
pr.created_at
+        drafted_at = pr_data.drafted_by_triager_at if pr_data else ""
+
+        age_bucket = _compute_age_bucket(last_interaction)
+        is_ready = _READY_FOR_REVIEW_LABEL in pr.labels
+        is_triaged_waiting = pr.number in waiting
+        is_triaged_responded = pr.number in responded
+
+        is_contributor = pr.author_association not in 
_COLLABORATOR_ASSOCIATIONS
+
+        for area in areas:
+            stats = area_stats.setdefault(area, _AreaStats())
+            stats.total += 1
+            if pr.is_draft:
+                stats.drafts += 1
+            else:
+                stats.non_drafts += 1
+            if is_contributor:
+                stats.contributors += 1
+            if is_triaged_waiting:
+                stats.triaged_waiting += 1
+            if is_triaged_responded:
+                stats.triaged_responded += 1
+            if is_ready:
+                stats.ready_for_review += 1
+            stats.age_buckets[age_bucket] += 1
+            if drafted_at:
+                stats.triager_drafted += 1
+                draft_bucket = _compute_age_bucket(drafted_at)
+                stats.draft_age_buckets[draft_bucket] += 1
+
+    return area_stats
+
+
+def _compute_totals(
+    prs: list[PRData],
+    triage_classification: dict[str, set[int]],
+    interaction_data: dict[int, _PRInteractionData],
+) -> _AreaStats:
+    """Compute totals across all PRs (unique counts, not sum of per-area)."""
+    waiting = triage_classification.get("waiting", set())
+    responded = triage_classification.get("responded", set())
+    totals = _AreaStats()
+    for pr in prs:
+        totals.total += 1
+        if pr.is_draft:
+            totals.drafts += 1
+        else:
+            totals.non_drafts += 1
+        if pr.author_association not in _COLLABORATOR_ASSOCIATIONS:
+            totals.contributors += 1
+        if pr.number in waiting:
+            totals.triaged_waiting += 1
+        if pr.number in responded:
+            totals.triaged_responded += 1
+        if _READY_FOR_REVIEW_LABEL in pr.labels:
+            totals.ready_for_review += 1
+        pr_data = interaction_data.get(pr.number)
+        last_interaction = pr_data.last_interaction if pr_data else 
pr.created_at
+        drafted_at = pr_data.drafted_by_triager_at if pr_data else ""
+        totals.age_buckets[_compute_age_bucket(last_interaction)] += 1
+        if drafted_at:
+            totals.triager_drafted += 1
+            totals.draft_age_buckets[_compute_age_bucket(drafted_at)] += 1
+    return totals
+
+
+def _pct(numerator: int, denominator: int) -> str:
+    """Format a percentage, returning '-' when the denominator is zero."""
+    return f"{100 * numerator / denominator:.0f}%" if denominator else "-"
+
+
+def _render_stats_tables(
+    area_stats: dict[str, _AreaStats],
+    totals: _AreaStats,
+    github_repository: str,
+    closed_stats: dict[str, _ClosedTriagedAreaStats],
+    closed_since_date: str,
+) -> None:
+    """Render two tables: triaged final-state and triaged still-open."""
+    console = get_console()
+    closed_totals = closed_stats.get("__total__", _ClosedTriagedAreaStats())
+
+    # Collect all area names across both open and closed stats
+    all_areas = set(area_stats.keys()) | {k for k in closed_stats if k != 
"__total__"}
+    sorted_area_names = sorted(
+        all_areas, key=lambda a: (a == "(no area)", -(area_stats.get(a, 
_AreaStats()).total))
+    )
+
+    # ── Table 1: Triaged PRs — Final State (closed/merged) ──────────
+    t1 = Table(
+        title=f"Triaged PRs — Final State since {closed_since_date} 
({github_repository})",
+        title_style="bold",
+        show_lines=True,
+        show_footer=True,
+    )
+    t1.add_column("Area", style="bold cyan", min_width=12, footer="Area")
+    t1.add_column("Triaged\nTotal", justify="right", style="yellow", 
footer="Triaged\nTotal")
+    t1.add_column("Closed", justify="right", style="red", footer="Closed")
+    t1.add_column("%Closed", justify="right", footer="%Closed")
+    t1.add_column("Merged", justify="right", style="green", footer="Merged")
+    t1.add_column("%Merged", justify="right", footer="%Merged")
+    t1.add_column("Responded", justify="right", footer="Responded")
+    t1.add_column("%Responded", justify="right", footer="%Responded")
+
+    for area in sorted_area_names:
+        cs = closed_stats.get(area, _ClosedTriagedAreaStats())
+        if cs.total == 0:
+            continue
+        t1.add_row(
+            area,
+            str(cs.total),
+            str(cs.closed),
+            cs.pct_closed,
+            str(cs.merged),
+            cs.pct_merged,
+            str(cs.responded_before_close),
+            cs.pct_responded,
+        )
+
+    t1.add_row(
+        "[bold white]TOTAL[/]",
+        f"[bold white]{closed_totals.total}[/]",
+        f"[bold white]{closed_totals.closed}[/]",
+        f"[bold white]{closed_totals.pct_closed}[/]",
+        f"[bold white]{closed_totals.merged}[/]",
+        f"[bold white]{closed_totals.pct_merged}[/]",
+        f"[bold white]{closed_totals.responded_before_close}[/]",
+        f"[bold white]{closed_totals.pct_responded}[/]",
+        style="on grey7",
+        end_section=True,
+    )
+
+    console.print()
+    console.print(t1)
+
+    # ── Table 2: Triaged PRs — Still Open ────────────────────────────
+    t2 = Table(
+        title=f"Triaged PRs — Still Open ({github_repository})",
+        title_style="bold",
+        show_lines=True,
+        show_footer=True,
+    )
+    t2.add_column("Area", style="bold cyan", min_width=12, footer="Area")
+    t2.add_column("Total", justify="right", footer="Total")
+    t2.add_column("Draft", justify="right", footer="Draft")
+    t2.add_column("%Draft", justify="right", footer="%Draft")
+    t2.add_column("Non-Draft", justify="right", footer="Non-Draft")
+    t2.add_column("Contrib.", justify="right", footer="Contrib.")
+    t2.add_column("%Contrib.", justify="right", footer="%Contrib.")
+    t2.add_column("Triaged", justify="right", style="yellow", footer="Triaged")
+    t2.add_column("Responded", justify="right", style="green", 
footer="Responded")
+    t2.add_column("%Responded", justify="right", footer="%Responded")
+    t2.add_column("Ready", justify="right", style="bold green", footer="Ready")
+    t2.add_column("%Ready", justify="right", footer="%Ready")
+    t2.add_column("Drafted\nby triager", justify="right", style="magenta", 
footer="Drafted\nby triager")
+    for bucket in _DRAFT_AGE_BUCKET_LABELS:
+        t2.add_column(f"Drafted\n{bucket}", justify="right", style="magenta 
dim", footer=f"Drafted\n{bucket}")
+    for bucket in _AGE_BUCKET_LABELS:
+        t2.add_column(f"Author resp\n{bucket}", justify="right", style="dim", 
footer=f"Author resp\n{bucket}")
+
+    for area in sorted_area_names:
+        s = area_stats.get(area)
+        if not s or s.total == 0:
+            continue
+        triaged = s.triaged_waiting + s.triaged_responded
+        t2.add_row(
+            area,
+            str(s.total),
+            str(s.drafts),
+            _pct(s.drafts, s.total),
+            str(s.non_drafts),
+            str(s.contributors),
+            _pct(s.contributors, s.total),
+            str(triaged),
+            str(s.triaged_responded),
+            _pct(s.triaged_responded, triaged),
+            str(s.ready_for_review),
+            _pct(s.ready_for_review, s.total),
+            str(s.triager_drafted),
+            *[str(s.draft_age_buckets.get(b, 0)) for b in 
_DRAFT_AGE_BUCKET_LABELS],
+            *[str(s.age_buckets.get(b, 0)) for b in _AGE_BUCKET_LABELS],
+        )
+
+    total_triaged = totals.triaged_waiting + totals.triaged_responded
+    t2.add_row(
+        "[bold white]TOTAL[/]",
+        f"[bold white]{totals.total}[/]",
+        f"[bold white]{totals.drafts}[/]",
+        f"[bold white]{_pct(totals.drafts, totals.total)}[/]",
+        f"[bold white]{totals.non_drafts}[/]",
+        f"[bold white]{totals.contributors}[/]",
+        f"[bold white]{_pct(totals.contributors, totals.total)}[/]",
+        f"[bold white]{total_triaged}[/]",
+        f"[bold white]{totals.triaged_responded}[/]",
+        f"[bold white]{_pct(totals.triaged_responded, total_triaged)}[/]",
+        f"[bold white]{totals.ready_for_review}[/]",
+        f"[bold white]{_pct(totals.ready_for_review, totals.total)}[/]",
+        f"[bold white]{totals.triager_drafted}[/]",
+        *[f"[bold white]{totals.draft_age_buckets.get(b, 0)}[/]" for b in 
_DRAFT_AGE_BUCKET_LABELS],
+        *[f"[bold white]{totals.age_buckets.get(b, 0)}[/]" for b in 
_AGE_BUCKET_LABELS],
+        style="on grey7",
+        end_section=True,
+    )
+
+    console.print()
+    console.print(t2)
+
+    # ── Legend ────────────────────────────────────────────────────────
+    legend_lines = [
+        "[bold]Column legend:[/]",
+        "  [bold]Contrib.[/]      = PRs by non-collaborator contributors",
+        "  [yellow]Triaged[/]       = PRs where a triage comment was posted",
+        "  [green]Responded[/]     = author replied after the triage comment",
+        "  [bold green]Ready[/]         = PRs with the [bold]'ready for 
maintainer review'[/] label",
+        "  [magenta]Drafted by triager[/] = PRs converted to draft by the 
triager",
+        "",
+        "[bold]Author resp[/] columns show time since the PR author's last 
interaction "
+        "(comment, commit, or PR creation).",
+        "[bold magenta]Drafted[/] columns show time since the triager 
converted the PR to draft.",
+    ]
+    console.print(Panel("\n".join(legend_lines), border_style="dim", 
expand=False))
+    console.print()
+
+
+_CLOSED_TRIAGED_SINCE = "2026-03-11"
+
+_CLOSED_PRS_SEARCH_QUERY = """
+query($query: String!, $first: Int!, $after: String) {
+  search(query: $query, type: ISSUE, first: $first, after: $after) {
+    issueCount
+    pageInfo {
+      hasNextPage
+      endCursor
+    }
+    nodes {
+      ... on PullRequest {
+        number
+        author { login }
+        authorAssociation
+        state
+        mergedAt
+        closedAt
+        labels(first: 20) {
+          nodes { name }
+        }
+        comments(last: 20) {
+          nodes { author { login } body createdAt }
+        }
+      }
+    }
+  }
+}
+"""
+
+
+@dataclass
+class _ClosedTriagedAreaStats:
+    """Per-area stats for triaged PRs that reached a final state."""
+
+    closed: int = 0
+    merged: int = 0
+    responded_before_close: int = 0
+    contributors: int = 0  # non-collaborator authors
+
+    @property
+    def total(self) -> int:
+        return self.closed + self.merged
+
+    @property
+    def pct_closed(self) -> str:
+        return f"{100 * self.closed / self.total:.0f}%" if self.total else "-"
+
+    @property
+    def pct_merged(self) -> str:
+        return f"{100 * self.merged / self.total:.0f}%" if self.total else "-"
+
+    @property
+    def pct_responded(self) -> str:
+        return f"{100 * self.responded_before_close / self.total:.0f}%" if 
self.total else "-"
+
+    @property
+    def pct_contributors(self) -> str:
+        return f"{100 * self.contributors / self.total:.0f}%" if self.total 
else "-"
+
+
+def _fetch_closed_triaged_prs(
+    token: str, github_repository: str, viewer_login: str, since: str
+) -> dict[str, _ClosedTriagedAreaStats]:
+    """Fetch closed/merged PRs since *since* that had a triage comment.
+
+    Returns a dict mapping area label (or "(no area)") to stats.
+    Also includes a "__total__" key with aggregate counts.
+    """
+    console = get_console()
+    console.print(f"[info]Fetching closed/merged triaged PRs since 
{since}...[/]")
+
+    search_query = (
+        f"repo:{github_repository} type:pr is:closed closed:>={since} "
+        f"commenter:{viewer_login} sort:updated-desc"
+    )
+
+    area_stats: dict[str, _ClosedTriagedAreaStats] = {}
+    total_stats = _ClosedTriagedAreaStats()
+    after_cursor: str | None = None
+
+    while True:
+        variables: dict = {"query": search_query, "first": 100}
+        if after_cursor:
+            variables["after"] = after_cursor
+
+        try:
+            data = _graphql_request(token, _CLOSED_PRS_SEARCH_QUERY, variables)
+        except SystemExit:
+            break
+
+        search_data = data["search"]
+        for node in search_data.get("nodes") or []:
+            if not node:
+                continue
+            comments = (node.get("comments") or {}).get("nodes") or []
+            author_login = (node.get("author") or {}).get("login", "")
+
+            # Check if any comment from viewer contains the triage marker
+            triage_comment_date = ""
+            for comment in reversed(comments):
+                commenter = (comment.get("author") or {}).get("login", "")
+                body = comment.get("body", "")
+                if commenter == viewer_login and _TRIAGE_COMMENT_MARKER in 
body:
+                    triage_comment_date = comment.get("createdAt", "")
+                    break
+
+            if not triage_comment_date:
+                continue
+
+            # Determine state: MERGED or CLOSED
+            is_merged = node.get("state") == "MERGED" or 
bool(node.get("mergedAt"))
+            is_contributor = node.get("authorAssociation", "") not in 
_COLLABORATOR_ASSOCIATIONS
+
+            # Extract area labels
+            labels = [n["name"] for n in (node.get("labels") or 
{}).get("nodes") or []]
+            areas = [lbl.removeprefix("area:") for lbl in labels if 
lbl.startswith("area:")]
+            if not areas:
+                areas = ["(no area)"]
+
+            # Check if author responded after triage
+            author_responded = False
+            for comment in comments:
+                commenter = (comment.get("author") or {}).get("login", "")
+                comment_date = comment.get("createdAt", "")
+                if commenter == author_login and comment_date > 
triage_comment_date:
+                    author_responded = True
+                    break
+
+            # Update per-area stats
+            for area in areas:
+                stats = area_stats.setdefault(area, _ClosedTriagedAreaStats())
+                if is_merged:
+                    stats.merged += 1
+                else:
+                    stats.closed += 1
+                if author_responded:
+                    stats.responded_before_close += 1
+                if is_contributor:
+                    stats.contributors += 1
+
+            # Update totals
+            if is_merged:
+                total_stats.merged += 1
+            else:
+                total_stats.closed += 1
+            if author_responded:
+                total_stats.responded_before_close += 1
+            if is_contributor:
+                total_stats.contributors += 1
+
+        page_info = search_data.get("pageInfo", {})
+        if not page_info.get("hasNextPage", False):
+            break
+        after_cursor = page_info.get("endCursor")
+
+    area_stats["__total__"] = total_stats
+    console.print(
+        f"[info]Found {total_stats.total} triaged PRs in final state "
+        f"({total_stats.closed} closed, {total_stats.merged} merged) since 
{since}.[/]"
+    )
+    return area_stats
+
+
+@pr_group.command(name="stats", help="Show statistics of open PRs grouped by 
area label.")
+@option_github_token
+@option_github_repository
[email protected](
+    "--batch-size",
+    type=int,
+    default=100,
+    show_default=True,
+    help="Number of PRs to fetch per GraphQL page.",
+)
[email protected](
+    "--clear-cache",
+    is_flag=True,
+    default=False,
+    help="Clear cached interaction data before fetching.",
+)
+def stats(github_token: str | None, github_repository: str, batch_size: int, 
clear_cache: bool) -> None:
+    """Produce aggregate statistics of open PRs, split by area."""
+    from airflow_breeze.utils.pr_cache import stats_interaction_cache
+
+    token = _resolve_github_token(github_token)
+    if not token:
+        console_print("[error]GitHub token is required. Use --github-token or 
set GITHUB_TOKEN.[/]")
+        sys.exit(1)
+
+    console = get_console()
+
+    if clear_cache:
+        import shutil
+
+        cache_dir = stats_interaction_cache.cache_dir(github_repository)
+        shutil.rmtree(cache_dir, ignore_errors=True)
+        console.print("[info]Cleared interaction cache.[/]")
+
+    # Step 1: Fetch all open PRs
+    all_prs = _fetch_all_open_prs(token, github_repository, batch_size)
+    if not all_prs:
+        console.print("[warning]No open PRs found.[/]")
+        return
+
+    # Step 2: Resolve viewer login and classify triage status
+    viewer_login = _resolve_viewer_login(token)
+    console.print(f"[info]Classifying triage status (viewer: 
{viewer_login})...[/]")
+    triage_classification = _classify_already_triaged_prs(token, 
github_repository, all_prs, viewer_login)
+
+    # Step 3: Fetch interaction data (last author activity + triager draft 
conversion)
+    interaction_data = _fetch_pr_interaction_data(token, github_repository, 
all_prs, viewer_login)
+
+    # Step 4: Aggregate by area
+    area_stats = _aggregate_stats_by_area(all_prs, triage_classification, 
interaction_data)
+
+    # Step 5: Compute totals (unique PR counts)
+    totals = _compute_totals(all_prs, triage_classification, interaction_data)
+
+    # Step 6: Fetch closed/merged triaged PRs since March 11, 2026
+    closed_stats = _fetch_closed_triaged_prs(token, github_repository, 
viewer_login, _CLOSED_TRIAGED_SINCE)
+
+    # Step 7: Render
+    _render_stats_tables(
+        area_stats,
+        totals,
+        github_repository,
+        closed_stats=closed_stats,
+        closed_since_date=_CLOSED_TRIAGED_SINCE,
+    )
diff --git a/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
index 237d42f2653..8311a29d26a 100644
--- a/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/pr_commands_config.py
@@ -18,7 +18,7 @@ from __future__ import annotations
 
 PR_COMMANDS: dict[str, str | list[str]] = {
     "name": "PR commands",
-    "commands": ["auto-triage"],
+    "commands": ["auto-triage", "stats"],
 }
 
 PR_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
@@ -76,4 +76,15 @@ PR_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = 
{
             ],
         },
     ],
+    "breeze pr stats": [
+        {
+            "name": "Options",
+            "options": [
+                "--batch-size",
+                "--clear-cache",
+                "--github-token",
+                "--github-repository",
+            ],
+        },
+    ],
 }
diff --git a/dev/breeze/src/airflow_breeze/utils/pr_cache.py 
b/dev/breeze/src/airflow_breeze/utils/pr_cache.py
index abe8b652f67..acd842e4e81 100644
--- a/dev/breeze/src/airflow_breeze/utils/pr_cache.py
+++ b/dev/breeze/src/airflow_breeze/utils/pr_cache.py
@@ -76,6 +76,7 @@ review_cache = CacheStore("review_cache")
 classification_cache = CacheStore("classification_cache")
 triage_cache = CacheStore("triage_cache")
 status_cache = CacheStore("status_cache", ttl_seconds=4 * 3600)
+stats_interaction_cache = CacheStore("stats_interaction_cache")
 
 
 # Convenience functions for common cache operations
diff --git a/uv.lock b/uv.lock
index f9a4ab162eb..ffccaa368a2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -12,7 +12,7 @@ resolution-markers = [
 ]
 
 [options]
-exclude-newer = "2026-03-27T19:04:41.304277104Z"
+exclude-newer = "2026-03-30T10:47:15.148565Z"
 exclude-newer-span = "P4D"
 
 [manifest]
@@ -1774,7 +1774,7 @@ requires-dist = [
     { name = "rich-argparse", specifier = ">=1.0.0" },
     { name = "setproctitle", specifier = ">=1.3.3" },
     { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.48" },
-    { name = "starlette", specifier = ">=0.45.0,<1" },
+    { name = "starlette", specifier = ">=0.45.0" },
     { name = "statsd", marker = "extra == 'statsd'", specifier = ">=3.3.0" },
     { name = "structlog", specifier = ">=25.4.0" },
     { name = "svcs", specifier = ">=25.1.0" },
@@ -2645,7 +2645,7 @@ docs = [
 
 [package.metadata]
 requires-dist = [
-    { name = "aiobotocore", extras = ["boto3"], marker = "extra == 
'aiobotocore'", specifier = ">=2.26.0" },
+    { name = "aiobotocore", marker = "extra == 'aiobotocore'", specifier = 
">=2.26.0" },
     { name = "apache-airflow", editable = "." },
     { name = "apache-airflow-providers-apache-hive", marker = "extra == 
'apache-hive'", editable = "providers/apache/hive" },
     { name = "apache-airflow-providers-cncf-kubernetes", marker = "extra == 
'cncf-kubernetes'", editable = "providers/cncf/kubernetes" },
@@ -4581,6 +4581,9 @@ dependencies = [
 kerberos = [
     { name = "kerberos" },
 ]
+oauth = [
+    { name = "authlib" },
+]
 
 [package.dev-dependencies]
 dev = [
@@ -4588,6 +4591,7 @@ dev = [
     { name = "apache-airflow-devel-common" },
     { name = "apache-airflow-providers-common-compat" },
     { name = "apache-airflow-task-sdk" },
+    { name = "authlib" },
     { name = "kerberos" },
     { name = "requests-kerberos" },
 ]
@@ -4599,6 +4603,7 @@ docs = [
 requires-dist = [
     { name = "apache-airflow", editable = "." },
     { name = "apache-airflow-providers-common-compat", editable = 
"providers/common/compat" },
+    { name = "authlib", marker = "extra == 'oauth'", specifier = ">=1.0.0" },
     { name = "blinker", specifier = ">=1.6.2" },
     { name = "cachetools", specifier = ">=6.0" },
     { name = "flask", specifier = ">=2.2.1" },
@@ -4618,7 +4623,7 @@ requires-dist = [
     { name = "werkzeug", marker = "python_full_version >= '3.14'", specifier = 
">=3.1.6" },
     { name = "wtforms", specifier = ">=3.0" },
 ]
-provides-extras = ["kerberos"]
+provides-extras = ["kerberos", "oauth"]
 
 [package.metadata.requires-dev]
 dev = [
@@ -4626,6 +4631,7 @@ dev = [
     { name = "apache-airflow-devel-common", editable = "devel-common" },
     { name = "apache-airflow-providers-common-compat", editable = 
"providers/common/compat" },
     { name = "apache-airflow-task-sdk", editable = "task-sdk" },
+    { name = "authlib", specifier = ">=1.0.0" },
     { name = "kerberos", specifier = ">=1.3.0" },
     { name = "requests-kerberos", specifier = ">=0.14.0" },
 ]


Reply via email to