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 c688a6f0abc Add breeze verify to list local checks for changed files 
(#73241)
c688a6f0abc is described below

commit c688a6f0abc79d7a54bb5a2ef4276cd49ba27b42
Author: Andrew Chang <[email protected]>
AuthorDate: Sun Oct 4 10:26:27 2026 +0800

    Add breeze verify to list local checks for changed files (#73241)
    
    * Add breeze verify to show CI-required local verification for changed files
    
    The self-review checklist in AGENTS.md (step 7) asks contributors to find
    the tests selective-checks would run and run them locally. The only tool
    for that is `breeze ci selective-check`, which looks at a single commit
    and prints GitHub Actions outputs, so a contributor still has to read the
    workflow files to turn those outputs into commands. This adds a
    developer-facing, dry-run command that feeds the working-tree diff to the
    same SelectiveChecks logic and prints the commands for the default CI
    matrix cell, with the rest explicitly left to CI.
    
    * Print the CI reproduction banner on stderr so breeze verify --json stays 
parseable
    
    In GitHub Actions breeze appends a "how to reproduce locally" banner after
    every command. It went to stdout, so any command whose stdout is a machine
    contract, like `breeze verify --json`, produced JSON followed by the banner.
    The banner is a log aid, so stderr is the right stream for it.
    
    * Let a breeze command opt out of the CI reproduction banner instead of 
moving it to stderr
    
    Moving the banner to stderr broke the build-info job: `breeze ci
    get-workflow-info` and `breeze ci selective-check` write their GitHub
    Actions outputs on stderr (`2>> $GITHUB_OUTPUT`), so the banner ended up
    in the outputs file. Neither stream is safe for every command. Keep the
    banner on stdout and let a command whose stdout is a machine contract,
    here `breeze verify --json`, switch it off through the click context.
    
    * List renamed files the way CI does when computing breeze verify changed 
files
    
    CI feeds selective checks the output of `git diff-tree`, which has no
    rename detection and reports a rename as the old path deleted plus the
    new path added. `git diff --name-only` detects renames by default and
    lists only the new path, so a rule matching the old path would not fire
    locally although it fires in CI.
    
    * Only list jobs triggered by the change in breeze verify
    
    Breeze's own unit tests, the shared distribution tests and the translation
    check run on every PR regardless of what changed, so listing them on every
    run adds rows nobody acts on and pushes the relevant ones down. Now every
    row comes from a SelectiveChecks output. The `required` column goes with
    them: with the translation check gone it was always true.
    
    * Make breeze verify list what to run by default and keep the CI expansion 
behind --full
    
    When a change touches CI tooling or dependency files, selective checks
    make CI run the whole suite. That is the right call for CI, but an agent
    that executes the list literally would run Go, Java and every provider
    for a breeze change. The default now applies the same selective-checks
    rules without that expansion, notes that CI will run more, and --full
    lists the whole default matrix cell.
    
    * Fold long commands in the breeze verify table instead of truncating them
    
    A truncated command cannot be copied, and the prek row with its SKIP list
    is longer than most terminals.
    
    * Print breeze verify commands one per line instead of in a table
    
    A table cell is not something you can copy: long commands got truncated,
    and folding them only spread one command over several lines. Each command
    is now on its own unwrapped line under a comment saying what kind of check
    it is and where it runs, so a triple-click selects the whole command.
    
    * Keep the table output in breeze verify
    
    Reverts the one-command-per-line output. The table is easier to read, and
    anyone who wants to copy commands has --json.
    
    * List the jobs CI runs on every PR under breeze verify --full
    
    --full means everything CI runs for the default matrix cell, so breeze's
    own unit tests and the shared distribution tests belong there even though
    no change triggers them. The default list still leaves them out. The
    translation check stays out of both: CI runs it with `|| true`.
    
    * Address review: guard hook names, use --from-ref by default, point agents 
at breeze verify
    
    The manual-stage hook names in the mapping are now checked against
    ci-amd.yml, so a renamed hook fails a test instead of going stale. The
    default prek row uses --from-ref, the scope AGENTS.md recommends, and
    --full keeps CI's --all-files form. Step 7 of the publish-changes skill,
    the AGENTS.md commands block and the selective-checks doc now point at
    breeze verify. All patches in the CLI tests use autospec.
    
    * Classify breeze verify flags from the workflow gates instead of the run_ 
prefix
    
    The classification test used to enumerate SelectiveChecks properties named
    run_* and assume those were the job gates, so a gate with another name
    would slip through. It now scans every `if:` in .github/workflows/ for
    the selective-checks outputs CI actually gates on, which also drops the
    hand-kept list of run_* outputs no job reads.
    
    * Scan complete multiline workflow conditions
    
    * Keep breeze verify classification focused on run flags
    
    * Regenerate breeze command images and skill-eval hash after rebase
    
    * Leave static checks to prek in breeze verify
    
    prek already picks the hooks to run for the changed files, so breeze
    verify no longer lists a prek row, in either mode. The mypy-providers
    and migration-round-trip jobs are manual-stage prek hooks too; their
    selective-checks flags are now classified in PREK_JOBS and not printed.
    The output, the --full help, the breeze docs, AGENTS.md and the
    publish-changes skill say static checks are not listed.
    
    Also classify run_agent_framework_tests, added to SelectiveChecks on
    main, with the breeze shell commands its CI job runs per framework.
    
    * Compare breeze verify with main on the apache/airflow remote by default
    
    The default --base-ref was the local main branch. In a fork it is often
    behind the main the branch has merged (a GitHub "Update branch" or a
    conflict fix), and then every merged-in commit counted as a change:
    3287 files instead of the 21 the PR changes.
    
    The default is now main on the remote whose URL points at apache/airflow,
    preferring upstream, which is the base GitHub compares the PR with.
    Without such a remote it falls back to the local main and says so.
    
    The local copy of that remote can be stale too, after "Update branch"
    without a fetch. breeze verify now warns when a merge on the branch
    brought in commits the base does not have. With --json the warnings go
    to stderr so stdout stays parseable.
    
    * Point breeze verify at the prek hooks a default install does not run
    
    Dropping the static-check rows also hid mypy-providers and 
migration-round-trip, which CI runs but a default prek install never triggers 
(pre-push and manual stage), so a migration change got no hint until CI failed. 
The CLI tests also ran real git against the developer's checkout and global 
config.
    
    Generated-by: Claude Opus 5
    
    ---------
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .agents/skills/airflow-publish-changes/SKILL.md    |   2 +-
 AGENTS.md                                          |   1 +
 contributing-docs/08_static_code_checks.rst        |   1 +
 dev/breeze/doc/03_developer_tasks.rst              |  55 ++++
 dev/breeze/doc/ci/04_selective_checks.md           |   5 +
 dev/breeze/doc/images/output-commands.svg          |  68 +++--
 .../output_setup_check-all-params-in-groups.svg    |   4 +-
 .../output_setup_check-all-params-in-groups.txt    |   2 +-
 .../output_setup_regenerate-command-images.svg     |   4 +-
 .../output_setup_regenerate-command-images.txt     |   2 +-
 dev/breeze/doc/images/output_verify.svg            | 128 ++++++++
 dev/breeze/doc/images/output_verify.txt            |   1 +
 dev/breeze/src/airflow_breeze/breeze.py            |   1 +
 .../commands/developer_commands_config.py          |  11 +
 .../src/airflow_breeze/commands/verify_commands.py | 180 ++++++++++++
 .../src/airflow_breeze/utils/reproduce_ci.py       |  10 +-
 .../src/airflow_breeze/utils/verification_plan.py  | 170 +++++++++++
 dev/breeze/tests/test_reproduce_ci.py              |  12 +
 dev/breeze/tests/test_verification_plan.py         | 244 +++++++++++++++
 dev/breeze/tests/test_verify_commands.py           | 327 +++++++++++++++++++++
 dev/skill-evals/last-eval-hash.txt                 |   2 +-
 21 files changed, 1188 insertions(+), 42 deletions(-)

diff --git a/.agents/skills/airflow-publish-changes/SKILL.md 
b/.agents/skills/airflow-publish-changes/SKILL.md
index cc4fe94beb7..8f115bea26f 100644
--- a/.agents/skills/airflow-publish-changes/SKILL.md
+++ b/.agents/skills/airflow-publish-changes/SKILL.md
@@ -115,7 +115,7 @@ code review checklist in 
[`.github/instructions/code-review.instructions.md`](..
    and fix any failures. The skipped hooks start long-running local servers or 
provision the
    opt-in Codex environment rather than run checks that complete.
 6. Run relevant individual tests and confirm they pass.
-7. Find which tests to run for the changes with selective-checks and run those 
tests in parallel to confirm they pass and check for CI-specific issues.
+7. Run `breeze verify` and execute every command it lists. The list comes from 
the same selective-checks logic CI uses. `breeze verify --full` shows 
everything CI runs for the default matrix cell, including the full suite CI 
adds when a change touches CI tooling or dependency files. Static checks are 
not listed: steps 4 and 5 cover them. Confirm the commands pass and check for 
CI-specific issues.
 8. Check for security issues — no secrets, no injection vulnerabilities, no 
unsafe patterns.
 
 Before pushing, always rebase your branch onto the latest target branch 
(usually `main`)
diff --git a/AGENTS.md b/AGENTS.md
index f7a6a9b5fb7..7685fb036af 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -54,6 +54,7 @@ Don't spell out **Directed Acyclic Graph** except for 
historical context.
 - **Run regular (fast) static checks:** `prek run --from-ref <target_branch> 
--stage pre-commit`
 - **Run manual (slower) checks:** `prek run --from-ref <target_branch> --stage 
manual --skip compile-ui-assets-dev --skip view-skill-eval --skip 
run-skill-eval-codex` (the skipped hooks start long-running local servers or 
provision the opt-in Codex environment rather than run checks that complete)
 - **Build docs:** `breeze build-docs`
+- **List the local verification for your changes:** `breeze verify` (add 
`--full` for everything CI runs for the default matrix cell except static 
checks, `--json` for machine-readable output). It uses the same 
selective-checks logic as CI.
 - **Determine which tests to run based on changed files:** `breeze ci 
selective-check --commit-ref <commit_with_squashed_changes>`
 <!-- END generated-commands, please keep comment here to allow auto update -->
 
diff --git a/contributing-docs/08_static_code_checks.rst 
b/contributing-docs/08_static_code_checks.rst
index 9bae6b9248f..9c34dfd16b1 100644
--- a/contributing-docs/08_static_code_checks.rst
+++ b/contributing-docs/08_static_code_checks.rst
@@ -236,6 +236,7 @@ To check other usage types of the pre-commit framework, see 
`Pre-commit website
      - "- **Run regular (fast) static checks:** `prek run --from-ref 
<target_branch> --stage pre-commit`"
      - "- **Run manual (slower) checks:** `prek run --from-ref <target_branch> 
--stage manual --skip compile-ui-assets-dev --skip view-skill-eval --skip 
run-skill-eval-codex` (the skipped hooks start long-running local servers or 
provision the opt-in Codex environment rather than run checks that complete)"
      - "- **Build docs:** `breeze build-docs`"
+     - "- **List the local verification for your changes:** `breeze verify` 
(add `--full` for everything CI runs for the default matrix cell except static 
checks, `--json` for machine-readable output). It uses the same 
selective-checks logic as CI."
      - "- **Determine which tests to run based on changed files:** `breeze ci 
selective-check --commit-ref <commit_with_squashed_changes>`"
 .. AGENT-SKILL-END
 
diff --git a/dev/breeze/doc/03_developer_tasks.rst 
b/dev/breeze/doc/03_developer_tasks.rst
index 56b8bbbb8ba..b94b4b01727 100644
--- a/dev/breeze/doc/03_developer_tasks.rst
+++ b/dev/breeze/doc/03_developer_tasks.rst
@@ -316,6 +316,61 @@ If the provider name is 
``apache-airflow-providers-cncf-kubernetes``, it will be
 Note: For building docs for apache-airflow-providers index, use 
``apache-airflow-providers``
 as the short hand operator.
 
+Finding out what CI will run for your change
+--------------------------------------------
+
+``breeze verify`` reads the files changed against the target branch (the 
merge-base of
+``--base-ref`` and ``HEAD``, plus untracked files), runs the same 
selective-checks logic CI uses
+and lists the commands to run locally for the change. Nothing is executed. The 
only non-zero exit
+is a usage error such as a base ref git cannot resolve.
+
+By default ``--base-ref`` is ``main`` on the git remote that points at 
apache/airflow (``upstream``
+if several do), the same base GitHub compares a PR with. A local ``main`` that 
is behind the
+``main`` your branch has merged would count every merged-in commit as your 
change. Without such a
+remote, ``breeze verify`` falls back to the local ``main`` branch and says so. 
It also warns when
+the branch has merged commits the base does not have, for example after 
GitHub's "Update branch"
+when the remote was not fetched since. With ``--json`` the warnings go to 
stderr.
+
+.. code-block:: bash
+
+     breeze verify
+     breeze verify --json
+     breeze verify --full
+     breeze verify --base-ref main
+
+Each row says what kind of check it is, whether it runs on the host or needs 
Docker and the
+CI image (``breeze``), and the exact command. Jobs CI runs on every PR 
regardless of the change
+(breeze's own unit tests, the shared distributions) only show up with 
``--full``. The translation
+check is never listed: CI runs it with ``|| true``, so it cannot fail a PR.
+Static checks are not listed either: ``prek`` already picks the hooks to run 
for the changed files,
+so run it as usual. The exception is the ``mypy-providers`` and 
``migration-round-trip`` hooks,
+which CI runs but a default ``prek install`` does not (they are ``pre-push`` 
or ``manual`` stage
+hooks). When the change triggers them, ``breeze verify`` prints the
+``prek run --stage manual <hook> --from-ref <base>`` command to run each one, 
and ``--json``
+lists them under ``manual_prek_hooks``.
+Use ``--json`` for machine-readable output with the same fields.
+
+When a change touches CI tooling or dependency files, selective checks make CI 
run the full suite.
+The default list leaves that expansion out and only shows what the changed 
files match themselves,
+with a note that CI will run more. ``--full`` lists everything CI runs for the 
default matrix cell
+(default Python, sqlite), apart from static checks.
+
+Even ``--full`` is not the full CI matrix. Other Python versions, Postgres and 
MySQL,
+lowest-dependency runs, Kubernetes, Helm, e2e suites, the provider 
compatibility matrix and ARM
+runners only run in CI. Two more caveats. Selective checks compare 
``pyproject.toml`` contents
+between ``HEAD`` and ``HEAD^`` only, so dependency changes that are 
uncommitted or in earlier
+commits of your branch are not detected as such (test selection is unaffected, 
only the
+dependency-bump checks are). Packaging steps CI runs around some tests 
(building and
+twine-checking the Task SDK and airflow-ctl wheels, regenerating the Python 
API client from its
+own repository) are not listed.
+
+These are all available flags of ``verify`` command:
+
+.. image:: ./images/output_verify.svg
+  :target: 
https://raw.githubusercontent.com/apache/airflow/main/dev/breeze/images/output_verify.svg
+  :width: 100%
+  :alt: Breeze verify
+
 Running static checks
 ---------------------
 
diff --git a/dev/breeze/doc/ci/04_selective_checks.md 
b/dev/breeze/doc/ci/04_selective_checks.md
index f4f0ce8badf..5d96ed6d910 100644
--- a/dev/breeze/doc/ci/04_selective_checks.md
+++ b/dev/breeze/doc/ci/04_selective_checks.md
@@ -50,6 +50,11 @@ contributors in case of simpler changes.
 > [`dev/breeze/src/airflow_breeze/utils/selective_checks.py`](../../src/airflow_breeze/utils/selective_checks.py).
 > When you change that file, **update this document in the same PR** so the 
 > behaviour and the
 > documentation stay in sync.
+>
+> `breeze verify` maps the `run_*` flags to local commands in
+> 
[`dev/breeze/src/airflow_breeze/utils/verification_plan.py`](../../src/airflow_breeze/utils/verification_plan.py).
+> Adding a `run_*` flag to `SelectiveChecks` also means classifying it there;
+> `dev/breeze/tests/test_verification_plan.py` fails until you do.
 
 ## Why selective checks exist (the optimisation goal)
 
diff --git a/dev/breeze/doc/images/output-commands.svg 
b/dev/breeze/doc/images/output-commands.svg
index 13418f6e0a6..e6405bcad55 100644
--- a/dev/breeze/doc/images/output-commands.svg
+++ b/dev/breeze/doc/images/output-commands.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 1482 2538.7999999999997" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 1482 2563.2" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -44,7 +44,7 @@
 
     <defs>
     <clipPath id="breeze-help-clip-terminal">
-      <rect x="0" y="0" width="1463.0" height="2487.7999999999997" />
+      <rect x="0" y="0" width="1463.0" height="2512.2" />
     </clipPath>
     <clipPath id="breeze-help-line-0">
     <rect x="0" y="1.5" width="1464" height="24.65"/>
@@ -349,9 +349,12 @@
 <clipPath id="breeze-help-line-100">
     <rect x="0" y="2441.5" width="1464" height="24.65"/>
             </clipPath>
+<clipPath id="breeze-help-line-101">
+    <rect x="0" y="2465.9" 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="2536.8" rx="8"/><text 
class="breeze-help-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Breeze&#160;commands</text>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="1480" height="2561.2" rx="8"/><text 
class="breeze-help-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Breeze&#160;commands</text>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
@@ -434,35 +437,36 @@
 </text><text class="breeze-help-r5" x="0" y="1728" textLength="12.2" 
clip-path="url(#breeze-help-line-70)">│</text><text class="breeze-help-r1" 
x="329.4" y="1728" textLength="1110.2" 
clip-path="url(#breeze-help-line-70)">images.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
 </text><text class="breeze-help-r5" x="0" y="1752.4" textLength="12.2" 
clip-path="url(#breeze-help-line-71)">│</text><text class="breeze-help-r4" 
x="24.4" y="1752.4" textLength="280.6" 
clip-path="url(#breeze-help-line-71)">generate-migration-file</text><text 
class="breeze-help-r1" x="329.4" y="1752.4" textLength="1110.2" 
clip-path="url(#breeze-help-line-71)">Autogenerate&#160;the&#160;alembic&#160;migration&#160;file&#160;for&#160;the&#160;ORM&#160;changes.&#160;&#160;&#160;&#160;&#160;&
 [...]
 </text><text class="breeze-help-r5" x="0" y="1776.8" textLength="12.2" 
clip-path="url(#breeze-help-line-72)">│</text><text class="breeze-help-r4" 
x="24.4" y="1776.8" textLength="280.6" 
clip-path="url(#breeze-help-line-72)">doctor&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="1776.8" textLength="1110.2" 
clip-path="url(#breeze-help-line-72)">Auto-healing&#160;of&#160;breeze&#160;&#160;&#
 [...]
-</text><text class="breeze-help-r5" x="0" y="1801.2" textLength="1464" 
clip-path="url(#breeze-help-line-73)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="1801.2" textLength="12.2" 
clip-path="url(#breeze-help-line-73)">
-</text><text class="breeze-help-r5" x="0" y="1825.6" textLength="24.4" 
clip-path="url(#breeze-help-line-74)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="1825.6" textLength="219.6" 
clip-path="url(#breeze-help-line-74)">&#160;Testing&#160;commands&#160;</text><text
 class="breeze-help-r5" x="244" y="1825.6" textLength="1195.6" 
clip-path="url(#breeze-help-line-74)">──────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="1850" textLength="12.2" 
clip-path="url(#breeze-help-line-75)">│</text><text class="breeze-help-r4" 
x="24.4" y="1850" textLength="183" 
clip-path="url(#breeze-help-line-75)">testing&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="231.8" y="1850" textLength="1207.8" 
clip-path="url(#breeze-help-line-75)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;run&#160;tests&#160;&#160;&#160;&#160;&#160;
 [...]
-</text><text class="breeze-help-r5" x="0" y="1874.4" textLength="12.2" 
clip-path="url(#breeze-help-line-76)">│</text><text class="breeze-help-r4" 
x="24.4" y="1874.4" textLength="183" 
clip-path="url(#breeze-help-line-76)">k8s&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="231.8" y="1874.4" textLength="1207.8" 
clip-path="url(#breeze-help-line-76)">Tools&#160;that&#160;developers&#160;use&#160;to&#160;run&#160;Kubernetes&#160;te
 [...]
-</text><text class="breeze-help-r5" x="0" y="1898.8" textLength="1464" 
clip-path="url(#breeze-help-line-77)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="1898.8" textLength="12.2" 
clip-path="url(#breeze-help-line-77)">
-</text><text class="breeze-help-r5" x="0" y="1923.2" textLength="24.4" 
clip-path="url(#breeze-help-line-78)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="1923.2" textLength="195.2" 
clip-path="url(#breeze-help-line-78)">&#160;Image&#160;commands&#160;</text><text
 class="breeze-help-r5" x="219.6" y="1923.2" textLength="1220" 
clip-path="url(#breeze-help-line-78)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="1947.6" textLength="12.2" 
clip-path="url(#breeze-help-line-79)">│</text><text class="breeze-help-r4" 
x="24.4" y="1947.6" textLength="207.4" 
clip-path="url(#breeze-help-line-79)">ci-image&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="256.2" y="1947.6" textLength="1183.4" 
clip-path="url(#breeze-help-line-79)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;manually&#160;manage&#160;CI&
 [...]
-</text><text class="breeze-help-r5" x="0" y="1972" textLength="12.2" 
clip-path="url(#breeze-help-line-80)">│</text><text class="breeze-help-r4" 
x="24.4" y="1972" textLength="207.4" 
clip-path="url(#breeze-help-line-80)">prod-image&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="256.2" y="1972" textLength="1183.4" 
clip-path="url(#breeze-help-line-80)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;manually&#160;manage&#160;PROD&#160;images&#1
 [...]
-</text><text class="breeze-help-r5" x="0" y="1996.4" textLength="1464" 
clip-path="url(#breeze-help-line-81)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="1996.4" textLength="12.2" 
clip-path="url(#breeze-help-line-81)">
-</text><text class="breeze-help-r5" x="0" y="2020.8" textLength="24.4" 
clip-path="url(#breeze-help-line-82)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2020.8" textLength="353.8" 
clip-path="url(#breeze-help-line-82)">&#160;Release&#160;management&#160;commands&#160;</text><text
 class="breeze-help-r5" x="378.2" y="2020.8" textLength="1061.4" 
clip-path="url(#breeze-help-line-82)">───────────────────────────────────────────────────────────────────────────────────────</text><text
 clas [...]
-</text><text class="breeze-help-r5" x="0" y="2045.2" textLength="12.2" 
clip-path="url(#breeze-help-line-83)">│</text><text class="breeze-help-r4" 
x="24.4" y="2045.2" textLength="280.6" 
clip-path="url(#breeze-help-line-83)">release-management&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2045.2" textLength="1110.2" 
clip-path="url(#breeze-help-line-83)">Tools&#160;that&#160;release&#160;managers&#160;can&#160;use&#160;to&#160;prepare&#160;and&#160;manage&#16
 [...]
-</text><text class="breeze-help-r5" x="0" y="2069.6" textLength="12.2" 
clip-path="url(#breeze-help-line-84)">│</text><text class="breeze-help-r4" 
x="24.4" y="2069.6" textLength="280.6" 
clip-path="url(#breeze-help-line-84)">sbom&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2069.6" textLength="1110.2" 
clip-path="url(#breeze-help-line-84)">Tools&#160;that&#160;release&#160;ma [...]
-</text><text class="breeze-help-r5" x="0" y="2094" textLength="12.2" 
clip-path="url(#breeze-help-line-85)">│</text><text class="breeze-help-r4" 
x="24.4" y="2094" textLength="280.6" 
clip-path="url(#breeze-help-line-85)">workflow-run&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2094" textLength="1110.2" 
clip-path="url(#breeze-help-line-85)">Tools&#160;to&#160;manage&#160;Airflow&#160;repository&#160;workflows&#160;&#160;&
 [...]
-</text><text class="breeze-help-r5" x="0" y="2118.4" textLength="1464" 
clip-path="url(#breeze-help-line-86)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2118.4" textLength="12.2" 
clip-path="url(#breeze-help-line-86)">
-</text><text class="breeze-help-r5" x="0" y="2142.8" textLength="24.4" 
clip-path="url(#breeze-help-line-87)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2142.8" textLength="158.6" 
clip-path="url(#breeze-help-line-87)">&#160;CI&#160;commands&#160;</text><text 
class="breeze-help-r5" x="183" y="2142.8" textLength="1256.6" 
clip-path="url(#breeze-help-line-87)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2167.2" textLength="12.2" 
clip-path="url(#breeze-help-line-88)">│</text><text class="breeze-help-r4" 
x="24.4" y="2167.2" textLength="61" 
clip-path="url(#breeze-help-line-88)">ci&#160;&#160;&#160;</text><text 
class="breeze-help-r1" x="109.8" y="2167.2" textLength="1329.8" 
clip-path="url(#breeze-help-line-88)">Tools&#160;that&#160;CI&#160;workflows&#160;use&#160;to&#160;cleanup/manage&#160;CI&#160;environment&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
-</text><text class="breeze-help-r5" x="0" y="2191.6" textLength="1464" 
clip-path="url(#breeze-help-line-89)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2191.6" textLength="12.2" 
clip-path="url(#breeze-help-line-89)">
-</text><text class="breeze-help-r5" x="0" y="2216" textLength="24.4" 
clip-path="url(#breeze-help-line-90)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2216" textLength="231.8" 
clip-path="url(#breeze-help-line-90)">&#160;Registry&#160;commands&#160;</text><text
 class="breeze-help-r5" x="256.2" y="2216" textLength="1183.4" 
clip-path="url(#breeze-help-line-90)">─────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="breeze-h [...]
-</text><text class="breeze-help-r5" x="0" y="2240.4" textLength="12.2" 
clip-path="url(#breeze-help-line-91)">│</text><text class="breeze-help-r4" 
x="24.4" y="2240.4" textLength="256.2" 
clip-path="url(#breeze-help-line-91)">registry&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="305" y="2240.4" textLength="1134.6" 
clip-path="url(#breeze-help-line-91)">Tools&#160;for&#160;the&#160;Airflow&#160;Provider&#160;Registry&#160;
 [...]
-</text><text class="breeze-help-r5" x="0" y="2264.8" textLength="1464" 
clip-path="url(#breeze-help-line-92)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2264.8" textLength="12.2" 
clip-path="url(#breeze-help-line-92)">
-</text><text class="breeze-help-r5" x="0" y="2289.2" textLength="24.4" 
clip-path="url(#breeze-help-line-93)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2289.2" textLength="158.6" 
clip-path="url(#breeze-help-line-93)">&#160;UI&#160;commands&#160;</text><text 
class="breeze-help-r5" x="183" y="2289.2" textLength="1256.6" 
clip-path="url(#breeze-help-line-93)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2313.6" textLength="12.2" 
clip-path="url(#breeze-help-line-94)">│</text><text class="breeze-help-r4" 
x="24.4" y="2313.6" textLength="85.4" 
clip-path="url(#breeze-help-line-94)">ui&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="134.2" y="2313.6" textLength="1305.4" 
clip-path="url(#breeze-help-line-94)">Tools&#160;for&#160;UI&#160;development&#160;and&#160;maintenance&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#16
 [...]
-</text><text class="breeze-help-r5" x="0" y="2338" textLength="1464" 
clip-path="url(#breeze-help-line-95)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2338" textLength="12.2" 
clip-path="url(#breeze-help-line-95)">
-</text><text class="breeze-help-r5" x="0" y="2362.4" textLength="24.4" 
clip-path="url(#breeze-help-line-96)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2362.4" textLength="207.4" 
clip-path="url(#breeze-help-line-96)">&#160;Issues&#160;commands&#160;</text><text
 class="breeze-help-r5" x="231.8" y="2362.4" textLength="1207.8" 
clip-path="url(#breeze-help-line-96)">───────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="br [...]
-</text><text class="breeze-help-r5" x="0" y="2386.8" textLength="12.2" 
clip-path="url(#breeze-help-line-97)">│</text><text class="breeze-help-r4" 
x="24.4" y="2386.8" textLength="231.8" 
clip-path="url(#breeze-help-line-97)">issues&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="280.6" y="2386.8" textLength="1159" 
clip-path="url(#breeze-help-line-97)">Tools&#160;for&#160;managing&#160;GitHub&#160;issues.&#160;&#160;&#160;&
 [...]
-</text><text class="breeze-help-r5" x="0" y="2411.2" textLength="1464" 
clip-path="url(#breeze-help-line-98)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2411.2" textLength="12.2" 
clip-path="url(#breeze-help-line-98)">
-</text><text class="breeze-help-r5" x="0" y="2435.6" textLength="24.4" 
clip-path="url(#breeze-help-line-99)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2435.6" textLength="195.2" 
clip-path="url(#breeze-help-line-99)">&#160;Setup&#160;commands&#160;</text><text
 class="breeze-help-r5" x="219.6" y="2435.6" textLength="1220" 
clip-path="url(#breeze-help-line-99)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
-</text><text class="breeze-help-r5" x="0" y="2460" textLength="12.2" 
clip-path="url(#breeze-help-line-100)">│</text><text class="breeze-help-r4" 
x="24.4" y="2460" textLength="146.4" 
clip-path="url(#breeze-help-line-100)">setup&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="195.2" y="2460" textLength="1244.4" 
clip-path="url(#breeze-help-line-100)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;configure&#160;Breeze&#160;&#160;&#160;&#160;&#
 [...]
-</text><text class="breeze-help-r5" x="0" y="2484.4" textLength="1464" 
clip-path="url(#breeze-help-line-101)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2484.4" textLength="12.2" 
clip-path="url(#breeze-help-line-101)">
+</text><text class="breeze-help-r5" x="0" y="1801.2" textLength="12.2" 
clip-path="url(#breeze-help-line-73)">│</text><text class="breeze-help-r4" 
x="24.4" y="1801.2" textLength="280.6" 
clip-path="url(#breeze-help-line-73)">verify&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="1801.2" textLength="1110.2" 
clip-path="url(#breeze-help-line-73)">List&#160;the&#160;local&#160;verification&#16
 [...]
+</text><text class="breeze-help-r5" x="0" y="1825.6" textLength="1464" 
clip-path="url(#breeze-help-line-74)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="1825.6" textLength="12.2" 
clip-path="url(#breeze-help-line-74)">
+</text><text class="breeze-help-r5" x="0" y="1850" textLength="24.4" 
clip-path="url(#breeze-help-line-75)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="1850" textLength="219.6" 
clip-path="url(#breeze-help-line-75)">&#160;Testing&#160;commands&#160;</text><text
 class="breeze-help-r5" x="244" y="1850" textLength="1195.6" 
clip-path="url(#breeze-help-line-75)">──────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="breeze-hel [...]
+</text><text class="breeze-help-r5" x="0" y="1874.4" textLength="12.2" 
clip-path="url(#breeze-help-line-76)">│</text><text class="breeze-help-r4" 
x="24.4" y="1874.4" textLength="183" 
clip-path="url(#breeze-help-line-76)">testing&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="231.8" y="1874.4" textLength="1207.8" 
clip-path="url(#breeze-help-line-76)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;run&#160;tests&#160;&#160;&#160;&#160;
 [...]
+</text><text class="breeze-help-r5" x="0" y="1898.8" textLength="12.2" 
clip-path="url(#breeze-help-line-77)">│</text><text class="breeze-help-r4" 
x="24.4" y="1898.8" textLength="183" 
clip-path="url(#breeze-help-line-77)">k8s&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="231.8" y="1898.8" textLength="1207.8" 
clip-path="url(#breeze-help-line-77)">Tools&#160;that&#160;developers&#160;use&#160;to&#160;run&#160;Kubernetes&#160;te
 [...]
+</text><text class="breeze-help-r5" x="0" y="1923.2" textLength="1464" 
clip-path="url(#breeze-help-line-78)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="1923.2" textLength="12.2" 
clip-path="url(#breeze-help-line-78)">
+</text><text class="breeze-help-r5" x="0" y="1947.6" textLength="24.4" 
clip-path="url(#breeze-help-line-79)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="1947.6" textLength="195.2" 
clip-path="url(#breeze-help-line-79)">&#160;Image&#160;commands&#160;</text><text
 class="breeze-help-r5" x="219.6" y="1947.6" textLength="1220" 
clip-path="url(#breeze-help-line-79)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="1972" textLength="12.2" 
clip-path="url(#breeze-help-line-80)">│</text><text class="breeze-help-r4" 
x="24.4" y="1972" textLength="207.4" 
clip-path="url(#breeze-help-line-80)">ci-image&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="256.2" y="1972" textLength="1183.4" 
clip-path="url(#breeze-help-line-80)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;manually&#160;manage&#160;CI&#160;i
 [...]
+</text><text class="breeze-help-r5" x="0" y="1996.4" textLength="12.2" 
clip-path="url(#breeze-help-line-81)">│</text><text class="breeze-help-r4" 
x="24.4" y="1996.4" textLength="207.4" 
clip-path="url(#breeze-help-line-81)">prod-image&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="256.2" y="1996.4" textLength="1183.4" 
clip-path="url(#breeze-help-line-81)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;manually&#160;manage&#160;PROD&#160;ima
 [...]
+</text><text class="breeze-help-r5" x="0" y="2020.8" textLength="1464" 
clip-path="url(#breeze-help-line-82)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2020.8" textLength="12.2" 
clip-path="url(#breeze-help-line-82)">
+</text><text class="breeze-help-r5" x="0" y="2045.2" textLength="24.4" 
clip-path="url(#breeze-help-line-83)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2045.2" textLength="353.8" 
clip-path="url(#breeze-help-line-83)">&#160;Release&#160;management&#160;commands&#160;</text><text
 class="breeze-help-r5" x="378.2" y="2045.2" textLength="1061.4" 
clip-path="url(#breeze-help-line-83)">───────────────────────────────────────────────────────────────────────────────────────</text><text
 clas [...]
+</text><text class="breeze-help-r5" x="0" y="2069.6" textLength="12.2" 
clip-path="url(#breeze-help-line-84)">│</text><text class="breeze-help-r4" 
x="24.4" y="2069.6" textLength="280.6" 
clip-path="url(#breeze-help-line-84)">release-management&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2069.6" textLength="1110.2" 
clip-path="url(#breeze-help-line-84)">Tools&#160;that&#160;release&#160;managers&#160;can&#160;use&#160;to&#160;prepare&#160;and&#160;manage&#16
 [...]
+</text><text class="breeze-help-r5" x="0" y="2094" textLength="12.2" 
clip-path="url(#breeze-help-line-85)">│</text><text class="breeze-help-r4" 
x="24.4" y="2094" textLength="280.6" 
clip-path="url(#breeze-help-line-85)">sbom&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2094" textLength="1110.2" 
clip-path="url(#breeze-help-line-85)">Tools&#160;that&#160;release&#160;managers
 [...]
+</text><text class="breeze-help-r5" x="0" y="2118.4" textLength="12.2" 
clip-path="url(#breeze-help-line-86)">│</text><text class="breeze-help-r4" 
x="24.4" y="2118.4" textLength="280.6" 
clip-path="url(#breeze-help-line-86)">workflow-run&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="329.4" y="2118.4" textLength="1110.2" 
clip-path="url(#breeze-help-line-86)">Tools&#160;to&#160;manage&#160;Airflow&#160;repository&#160;workflows&#160;&
 [...]
+</text><text class="breeze-help-r5" x="0" y="2142.8" textLength="1464" 
clip-path="url(#breeze-help-line-87)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2142.8" textLength="12.2" 
clip-path="url(#breeze-help-line-87)">
+</text><text class="breeze-help-r5" x="0" y="2167.2" textLength="24.4" 
clip-path="url(#breeze-help-line-88)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2167.2" textLength="158.6" 
clip-path="url(#breeze-help-line-88)">&#160;CI&#160;commands&#160;</text><text 
class="breeze-help-r5" x="183" y="2167.2" textLength="1256.6" 
clip-path="url(#breeze-help-line-88)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="2191.6" textLength="12.2" 
clip-path="url(#breeze-help-line-89)">│</text><text class="breeze-help-r4" 
x="24.4" y="2191.6" textLength="61" 
clip-path="url(#breeze-help-line-89)">ci&#160;&#160;&#160;</text><text 
class="breeze-help-r1" x="109.8" y="2191.6" textLength="1329.8" 
clip-path="url(#breeze-help-line-89)">Tools&#160;that&#160;CI&#160;workflows&#160;use&#160;to&#160;cleanup/manage&#160;CI&#160;environment&#160;&#160;&#160;&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-help-r5" x="0" y="2216" textLength="1464" 
clip-path="url(#breeze-help-line-90)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2216" textLength="12.2" 
clip-path="url(#breeze-help-line-90)">
+</text><text class="breeze-help-r5" x="0" y="2240.4" textLength="24.4" 
clip-path="url(#breeze-help-line-91)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2240.4" textLength="231.8" 
clip-path="url(#breeze-help-line-91)">&#160;Registry&#160;commands&#160;</text><text
 class="breeze-help-r5" x="256.2" y="2240.4" textLength="1183.4" 
clip-path="url(#breeze-help-line-91)">─────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="br [...]
+</text><text class="breeze-help-r5" x="0" y="2264.8" textLength="12.2" 
clip-path="url(#breeze-help-line-92)">│</text><text class="breeze-help-r4" 
x="24.4" y="2264.8" textLength="256.2" 
clip-path="url(#breeze-help-line-92)">registry&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="305" y="2264.8" textLength="1134.6" 
clip-path="url(#breeze-help-line-92)">Tools&#160;for&#160;the&#160;Airflow&#160;Provider&#160;Registry&#160;
 [...]
+</text><text class="breeze-help-r5" x="0" y="2289.2" textLength="1464" 
clip-path="url(#breeze-help-line-93)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2289.2" textLength="12.2" 
clip-path="url(#breeze-help-line-93)">
+</text><text class="breeze-help-r5" x="0" y="2313.6" textLength="24.4" 
clip-path="url(#breeze-help-line-94)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2313.6" textLength="158.6" 
clip-path="url(#breeze-help-line-94)">&#160;UI&#160;commands&#160;</text><text 
class="breeze-help-r5" x="183" y="2313.6" textLength="1256.6" 
clip-path="url(#breeze-help-line-94)">───────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="bree [...]
+</text><text class="breeze-help-r5" x="0" y="2338" textLength="12.2" 
clip-path="url(#breeze-help-line-95)">│</text><text class="breeze-help-r4" 
x="24.4" y="2338" textLength="85.4" 
clip-path="url(#breeze-help-line-95)">ui&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="134.2" y="2338" textLength="1305.4" 
clip-path="url(#breeze-help-line-95)">Tools&#160;for&#160;UI&#160;development&#160;and&#160;maintenance&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#16
 [...]
+</text><text class="breeze-help-r5" x="0" y="2362.4" textLength="1464" 
clip-path="url(#breeze-help-line-96)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2362.4" textLength="12.2" 
clip-path="url(#breeze-help-line-96)">
+</text><text class="breeze-help-r5" x="0" y="2386.8" textLength="24.4" 
clip-path="url(#breeze-help-line-97)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2386.8" textLength="207.4" 
clip-path="url(#breeze-help-line-97)">&#160;Issues&#160;commands&#160;</text><text
 class="breeze-help-r5" x="231.8" y="2386.8" textLength="1207.8" 
clip-path="url(#breeze-help-line-97)">───────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="br [...]
+</text><text class="breeze-help-r5" x="0" y="2411.2" textLength="12.2" 
clip-path="url(#breeze-help-line-98)">│</text><text class="breeze-help-r4" 
x="24.4" y="2411.2" textLength="231.8" 
clip-path="url(#breeze-help-line-98)">issues&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="280.6" y="2411.2" textLength="1159" 
clip-path="url(#breeze-help-line-98)">Tools&#160;for&#160;managing&#160;GitHub&#160;issues.&#160;&#160;&#160;&
 [...]
+</text><text class="breeze-help-r5" x="0" y="2435.6" textLength="1464" 
clip-path="url(#breeze-help-line-99)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2435.6" textLength="12.2" 
clip-path="url(#breeze-help-line-99)">
+</text><text class="breeze-help-r5" x="0" y="2460" textLength="24.4" 
clip-path="url(#breeze-help-line-100)">╭─</text><text class="breeze-help-r5" 
x="24.4" y="2460" textLength="195.2" 
clip-path="url(#breeze-help-line-100)">&#160;Setup&#160;commands&#160;</text><text
 class="breeze-help-r5" x="219.6" y="2460" textLength="1220" 
clip-path="url(#breeze-help-line-100)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="breeze- [...]
+</text><text class="breeze-help-r5" x="0" y="2484.4" textLength="12.2" 
clip-path="url(#breeze-help-line-101)">│</text><text class="breeze-help-r4" 
x="24.4" y="2484.4" textLength="146.4" 
clip-path="url(#breeze-help-line-101)">setup&#160;&#160;&#160;&#160;&#160;&#160;&#160;</text><text
 class="breeze-help-r1" x="195.2" y="2484.4" textLength="1244.4" 
clip-path="url(#breeze-help-line-101)">Tools&#160;that&#160;developers&#160;can&#160;use&#160;to&#160;configure&#160;Breeze&#160;&#160;&#160;&#
 [...]
+</text><text class="breeze-help-r5" x="0" y="2508.8" textLength="1464" 
clip-path="url(#breeze-help-line-102)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-help-r1" x="1464" y="2508.8" textLength="12.2" 
clip-path="url(#breeze-help-line-102)">
 </text>
     </g>
     </g>
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 102ac89521a..a3da27c3903 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
@@ -246,8 +246,8 @@
 </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="890.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-39)">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="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-r6" x="158.6" y="996" 
textLength="1195.6" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-40)">testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;testing:python-api-client-tests&#160;|&#160;</text><text
 class="breeze-setup- [...]
 </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-r6" x="158.6" y="1020.4" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-41)">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="br [...]
-</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-r6" x="158.6" y="1044.8" 
textLength="1281" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs&#160;|</
 [...]
-</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-r6" x="158.6" y="1069.2" 
textLength="841.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">workflow-run:release-constraints&#160;|&#160;workflow-run:sync-staging-to-main)</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="1451.8" y= [...]
+</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-r6" x="158.6" y="1044.8" 
textLength="1061.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-42)">|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;ui:compile-assets&#160;|&#160;verify&#160;|&#160;workflow-run&#160;|&#160;</text><text
  [...]
+</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-r6" x="158.6" y="1069.2" 
textLength="1183.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-43)">workflow-run:publish-docs&#160;|&#160;workflow-run:release-constraints&#160;|&#160;workflow-run:sync-staging-to-main)</text><text
 class="breeze-setup-check [...]
 </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="1118" 
textLength="24.4" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">╭─</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="24.4" y="1118" 
textLength="195.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-45)">&#160;Common&#160;options&#160;</text><text
 class="breeze-setup-check-all-params-in-groups-r5" x="219.6" y="1118" 
textLength="1220" clip-path="url(#breeze-setup [...]
 </text><text class="breeze-setup-check-all-params-in-groups-r5" x="0" 
y="1142.4" textLength="12.2" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-46)">│</text><text 
class="breeze-setup-check-all-params-in-groups-r4" x="24.4" y="1142.4" 
textLength="109.8" 
clip-path="url(#breeze-setup-check-all-params-in-groups-line-46)">--verbose</text><text
 class="breeze-setup-check-all-params-in-groups-r7" x="158.6" y="1142.4" 
textLength="24.4" clip-path="url(#breeze-setup-check-all-params [...]
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 79fb51c84ed..e906db0e840 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 @@
-466e12cd92f255b4a2ea75d3fe6d2c6c
+071569892714c5561156e19cdaa04041
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 9d2c56dd70a..8d98810ae3a 100644
--- a/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
+++ b/dev/breeze/doc/images/output_setup_regenerate-command-images.svg
@@ -264,8 +264,8 @@
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1044.8" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-42)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1044.8" 
textLength="1037" 
clip-path="url(#breeze-setup-regenerate-command-images-line-42)">testing:helm-tests&#160;|&#160;testing:providers-integration-tests&#160;|&#160;testing:providers-tests&#160;|&#160;</text><text
 class="breeze-setup-regenerate-co [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1069.2" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-43)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1069.2" 
textLength="1146.8" 
clip-path="url(#breeze-setup-regenerate-command-images-line-43)">testing:python-api-client-tests&#160;|&#160;testing:system-tests&#160;|&#160;testing:task-sdk-integration-tests&#160;|&#160;</text><text
 class="breeze-setup-re [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1093.6" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-44)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1093.6" 
textLength="1085.8" 
clip-path="url(#breeze-setup-regenerate-command-images-line-44)">testing:task-sdk-tests&#160;|&#160;testing:ui-e2e-tests&#160;|&#160;ui&#160;|&#160;ui:check-translation-completeness&#160;|&#160;</text><text
 class="breeze-set [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118" 
textLength="1195.6" 
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:compile-assets&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs&#160;|&#160;workflow-run:release-constraints&#160;|&#160;</text><text
 class="breez [...]
-</text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1142.4" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1142.4" 
textLength="414.8" 
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">workflow-run:sync-staging-to-main)</text><text
 class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1142.4" 
textLength="12.2" clip-path="url(#breeze- [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" y="1118" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1118" 
textLength="878.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-45)">ui:compile-assets&#160;|&#160;verify&#160;|&#160;workflow-run&#160;|&#160;workflow-run:publish-docs&#160;|&#160;</text><text
 class="breeze-setup-regenerate-command- [...]
+</text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1142.4" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">│</text><text 
class="breeze-setup-regenerate-command-images-r6" x="195.2" y="1142.4" 
textLength="841.8" 
clip-path="url(#breeze-setup-regenerate-command-images-line-46)">workflow-run:release-constraints&#160;|&#160;workflow-run:sync-staging-to-main)</text><text
 class="breeze-setup-regenerate-command-images-r5" x="1451.8" y="1142 [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1166.8" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-47)">│</text><text 
class="breeze-setup-regenerate-command-images-r4" x="24.4" y="1166.8" 
textLength="146.4" 
clip-path="url(#breeze-setup-regenerate-command-images-line-47)">--check-only</text><text
 class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1166.8" 
textLength="1244.4" clip-path="url(#breeze-setup-regenerate-comma [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1191.2" textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-48)">│</text><text 
class="breeze-setup-regenerate-command-images-r1" x="195.2" y="1191.2" 
textLength="170.8" 
clip-path="url(#breeze-setup-regenerate-command-images-line-48)">together&#160;with&#160;</text><text
 class="breeze-setup-regenerate-command-images-r4" x="366" y="1191.2" 
textLength="109.8" clip-path="url(#breeze-setup-regene [...]
 </text><text class="breeze-setup-regenerate-command-images-r5" x="0" 
y="1215.6" textLength="1464" 
clip-path="url(#breeze-setup-regenerate-command-images-line-49)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-setup-regenerate-command-images-r1" x="1464" y="1215.6" 
textLength="12.2" 
clip-path="url(#breeze-setup-regenerate-command-images-line-49)">
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 8c6c5112178..d5eb31a16b7 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 @@
-4463e56c4d0363f78d210bc76e8613bb
+57d31722815e46eeb1cdf1b92d5a87b1
diff --git a/dev/breeze/doc/images/output_verify.svg 
b/dev/breeze/doc/images/output_verify.svg
new file mode 100644
index 00000000000..4cecb2cbe93
--- /dev/null
+++ b/dev/breeze/doc/images/output_verify.svg
@@ -0,0 +1,128 @@
+<svg class="rich-terminal" viewBox="0 0 1482 464.79999999999995" 
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-verify-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .breeze-verify-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .breeze-verify-r1 { fill: #c5c8c6 }
+.breeze-verify-r2 { fill: #d0b344 }
+.breeze-verify-r3 { fill: #c5c8c6;font-weight: bold }
+.breeze-verify-r4 { fill: #68a0b3;font-weight: bold }
+.breeze-verify-r5 { fill: #868887 }
+.breeze-verify-r6 { fill: #8d7b39 }
+.breeze-verify-r7 { fill: #98a84b;font-weight: bold }
+    </style>
+
+    <defs>
+    <clipPath id="breeze-verify-clip-terminal">
+      <rect x="0" y="0" width="1463.0" height="413.79999999999995" />
+    </clipPath>
+    <clipPath id="breeze-verify-line-0">
+    <rect x="0" y="1.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-1">
+    <rect x="0" y="25.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-2">
+    <rect x="0" y="50.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-3">
+    <rect x="0" y="74.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-4">
+    <rect x="0" y="99.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-5">
+    <rect x="0" y="123.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-6">
+    <rect x="0" y="147.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-7">
+    <rect x="0" y="172.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-8">
+    <rect x="0" y="196.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-9">
+    <rect x="0" y="221.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-10">
+    <rect x="0" y="245.5" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-11">
+    <rect x="0" y="269.9" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-12">
+    <rect x="0" y="294.3" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-13">
+    <rect x="0" y="318.7" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-14">
+    <rect x="0" y="343.1" width="1464" height="24.65"/>
+            </clipPath>
+<clipPath id="breeze-verify-line-15">
+    <rect x="0" y="367.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="462.8" rx="8"/><text 
class="breeze-verify-title" fill="#c5c8c6" text-anchor="middle" x="740" 
y="27">Command:&#160;verify</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-verify-clip-terminal)">
+    
+    <g class="breeze-verify-matrix">
+    <text class="breeze-verify-r1" x="1464" y="20" textLength="12.2" 
clip-path="url(#breeze-verify-line-0)">
+</text><text class="breeze-verify-r2" x="12.2" y="44.4" textLength="73.2" 
clip-path="url(#breeze-verify-line-1)">Usage:</text><text 
class="breeze-verify-r3" x="97.6" y="44.4" textLength="158.6" 
clip-path="url(#breeze-verify-line-1)">breeze&#160;verify</text><text 
class="breeze-verify-r1" x="268.4" y="44.4" textLength="12.2" 
clip-path="url(#breeze-verify-line-1)">[</text><text class="breeze-verify-r4" 
x="280.6" y="44.4" textLength="85.4" 
clip-path="url(#breeze-verify-line-1)">OPTIONS</tex [...]
+</text><text class="breeze-verify-r1" x="1464" y="68.8" textLength="12.2" 
clip-path="url(#breeze-verify-line-2)">
+</text><text class="breeze-verify-r1" x="12.2" y="93.2" textLength="1098" 
clip-path="url(#breeze-verify-line-3)">List&#160;the&#160;local&#160;verification&#160;CI&#160;would&#160;require&#160;for&#160;the&#160;current&#160;changes.&#160;Nothing&#160;is&#160;executed.</text><text
 class="breeze-verify-r1" x="1464" y="93.2" textLength="12.2" 
clip-path="url(#breeze-verify-line-3)">
+</text><text class="breeze-verify-r1" x="1464" y="117.6" textLength="12.2" 
clip-path="url(#breeze-verify-line-4)">
+</text><text class="breeze-verify-r5" x="0" y="142" textLength="24.4" 
clip-path="url(#breeze-verify-line-5)">╭─</text><text class="breeze-verify-r5" 
x="24.4" y="142" textLength="170.8" 
clip-path="url(#breeze-verify-line-5)">&#160;Verify&#160;flags&#160;</text><text
 class="breeze-verify-r5" x="195.2" y="142" textLength="1244.4" 
clip-path="url(#breeze-verify-line-5)">──────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 class="br [...]
+</text><text class="breeze-verify-r5" x="0" y="166.4" textLength="12.2" 
clip-path="url(#breeze-verify-line-6)">│</text><text class="breeze-verify-r4" 
x="24.4" y="166.4" textLength="122" 
clip-path="url(#breeze-verify-line-6)">--base-ref</text><text 
class="breeze-verify-r1" x="170.8" y="166.4" textLength="1268.8" 
clip-path="url(#breeze-verify-line-6)">Git&#160;ref&#160;the&#160;change&#160;will&#160;be&#160;merged&#160;into.&#160;Changed&#160;files&#160;are&#160;computed&#160;from&#160;its
 [...]
+</text><text class="breeze-verify-r5" x="0" y="190.8" textLength="12.2" 
clip-path="url(#breeze-verify-line-7)">│</text><text class="breeze-verify-r1" 
x="170.8" y="190.8" textLength="1268.8" 
clip-path="url(#breeze-verify-line-7)">Defaults&#160;to&#160;main&#160;on&#160;the&#160;remote&#160;that&#160;points&#160;at&#160;apache/airflow,&#160;or&#160;the&#160;local&#160;main&#160;branch&#160;if&#160;there&#160;is&#160;no&#160;&#160;&#160;</text><text
 class="breeze-verify-r5" x="1451.8" y="19 [...]
+</text><text class="breeze-verify-r5" x="0" y="215.2" textLength="12.2" 
clip-path="url(#breeze-verify-line-8)">│</text><text class="breeze-verify-r1" 
x="170.8" y="215.2" textLength="158.6" 
clip-path="url(#breeze-verify-line-8)">such&#160;remote.&#160;</text><text 
class="breeze-verify-r6" x="329.4" y="215.2" textLength="73.2" 
clip-path="url(#breeze-verify-line-8)">(TEXT)</text><text 
class="breeze-verify-r5" x="1451.8" y="215.2" textLength="12.2" 
clip-path="url(#breeze-verify-line-8)">│</t [...]
+</text><text class="breeze-verify-r5" x="0" y="239.6" textLength="12.2" 
clip-path="url(#breeze-verify-line-9)">│</text><text class="breeze-verify-r4" 
x="24.4" y="239.6" textLength="122" 
clip-path="url(#breeze-verify-line-9)">--full&#160;&#160;&#160;&#160;</text><text
 class="breeze-verify-r1" x="170.8" y="239.6" textLength="1268.8" 
clip-path="url(#breeze-verify-line-9)">List&#160;everything&#160;CI&#160;runs&#160;for&#160;the&#160;default&#160;matrix&#160;cell&#160;except&#160;static&#160
 [...]
+</text><text class="breeze-verify-r5" x="0" y="264" textLength="12.2" 
clip-path="url(#breeze-verify-line-10)">│</text><text class="breeze-verify-r1" 
x="170.8" y="264" textLength="1268.8" 
clip-path="url(#breeze-verify-line-10)">adds&#160;when&#160;a&#160;change&#160;touches&#160;CI&#160;tooling&#160;or&#160;dependency&#160;files.&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;
 [...]
+</text><text class="breeze-verify-r5" x="0" y="288.4" textLength="12.2" 
clip-path="url(#breeze-verify-line-11)">│</text><text class="breeze-verify-r4" 
x="24.4" y="288.4" textLength="122" 
clip-path="url(#breeze-verify-line-11)">--json&#160;&#160;&#160;&#160;</text><text
 class="breeze-verify-r1" x="170.8" y="288.4" textLength="573.4" 
clip-path="url(#breeze-verify-line-11)">Print&#160;machine-readable&#160;JSON&#160;instead&#160;of&#160;a&#160;table.</text><text
 class="breeze-verify-r5" x=" [...]
+</text><text class="breeze-verify-r5" x="0" y="312.8" textLength="1464" 
clip-path="url(#breeze-verify-line-12)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-verify-r1" x="1464" y="312.8" textLength="12.2" 
clip-path="url(#breeze-verify-line-12)">
+</text><text class="breeze-verify-r5" x="0" y="337.2" textLength="24.4" 
clip-path="url(#breeze-verify-line-13)">╭─</text><text class="breeze-verify-r5" 
x="24.4" y="337.2" textLength="195.2" 
clip-path="url(#breeze-verify-line-13)">&#160;Common&#160;options&#160;</text><text
 class="breeze-verify-r5" x="219.6" y="337.2" textLength="1220" 
clip-path="url(#breeze-verify-line-13)">────────────────────────────────────────────────────────────────────────────────────────────────────</text><text
 cl [...]
+</text><text class="breeze-verify-r5" x="0" y="361.6" textLength="12.2" 
clip-path="url(#breeze-verify-line-14)">│</text><text class="breeze-verify-r4" 
x="24.4" y="361.6" textLength="109.8" 
clip-path="url(#breeze-verify-line-14)">--verbose</text><text 
class="breeze-verify-r7" x="158.6" y="361.6" textLength="24.4" 
clip-path="url(#breeze-verify-line-14)">-v</text><text class="breeze-verify-r1" 
x="207.4" y="361.6" textLength="585.6" 
clip-path="url(#breeze-verify-line-14)">Print&#160;verbose& [...]
+</text><text class="breeze-verify-r5" x="0" y="386" textLength="12.2" 
clip-path="url(#breeze-verify-line-15)">│</text><text class="breeze-verify-r4" 
x="24.4" y="386" textLength="109.8" 
clip-path="url(#breeze-verify-line-15)">--help&#160;&#160;&#160;</text><text 
class="breeze-verify-r7" x="158.6" y="386" textLength="24.4" 
clip-path="url(#breeze-verify-line-15)">-h</text><text class="breeze-verify-r1" 
x="207.4" y="386" textLength="329.4" 
clip-path="url(#breeze-verify-line-15)">Show&#160;th [...]
+</text><text class="breeze-verify-r5" x="0" y="410.4" textLength="1464" 
clip-path="url(#breeze-verify-line-16)">╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯</text><text
 class="breeze-verify-r1" x="1464" y="410.4" textLength="12.2" 
clip-path="url(#breeze-verify-line-16)">
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/dev/breeze/doc/images/output_verify.txt 
b/dev/breeze/doc/images/output_verify.txt
new file mode 100644
index 00000000000..24e0226a248
--- /dev/null
+++ b/dev/breeze/doc/images/output_verify.txt
@@ -0,0 +1 @@
+af28dee252e39588e825b2fef52146f0
diff --git a/dev/breeze/src/airflow_breeze/breeze.py 
b/dev/breeze/src/airflow_breeze/breeze.py
index be8b8bae1ae..34b4a8d5d65 100755
--- a/dev/breeze/src/airflow_breeze/breeze.py
+++ b/dev/breeze/src/airflow_breeze/breeze.py
@@ -46,6 +46,7 @@ from airflow_breeze.commands.sbom_commands import sbom_group  
# noqa: E402
 from airflow_breeze.commands.setup_commands import setup_group  # noqa: E402
 from airflow_breeze.commands.testing_commands import testing_group  # noqa: 
E402
 from airflow_breeze.commands.ui_commands import ui_group  # noqa: E402
+from airflow_breeze.commands import verify_commands  # noqa: E402, F401
 
 main.add_command(testing_group)
 main.add_command(kubernetes_group)
diff --git 
a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py 
b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
index 0c38fb7efa9..8d53feb5a5d 100644
--- a/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
+++ b/dev/breeze/src/airflow_breeze/commands/developer_commands_config.py
@@ -28,6 +28,7 @@ DEVELOPER_COMMANDS: dict[str, str | list[str]] = {
         "cleanup",
         "generate-migration-file",
         "doctor",
+        "verify",
     ],
 }
 DEVELOPER_PARAMETERS: dict[str, list[dict[str, str | list[str]]]] = {
@@ -400,6 +401,16 @@ DEVELOPER_PARAMETERS: dict[str, list[dict[str, str | 
list[str]]]] = {
             ],
         },
     ],
+    "breeze verify": [
+        {
+            "name": "Verify flags",
+            "options": [
+                "--base-ref",
+                "--full",
+                "--json",
+            ],
+        },
+    ],
     "breeze doctor": [
         {
             "name": "Auto-healing of breeze",
diff --git a/dev/breeze/src/airflow_breeze/commands/verify_commands.py 
b/dev/breeze/src/airflow_breeze/commands/verify_commands.py
new file mode 100644
index 00000000000..0392a600c6a
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/commands/verify_commands.py
@@ -0,0 +1,180 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import contextlib
+import io
+import json
+import re
+import sys
+from subprocess import CompletedProcess
+
+import click
+from rich.markup import escape
+from rich.table import Table
+
+from airflow_breeze.branch_defaults import AIRFLOW_BRANCH, 
DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH
+from airflow_breeze.commands.common_options import option_verbose
+from airflow_breeze.commands.main_command import main
+from airflow_breeze.global_constants import GithubEvents
+from airflow_breeze.utils.console import get_console, get_stderr_console
+from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
+from airflow_breeze.utils.reproduce_ci import SKIP_LOCAL_REPRODUCTION
+from airflow_breeze.utils.run_utils import run_command
+from airflow_breeze.utils.shared_options import get_verbose
+from airflow_breeze.utils.verification_plan import LeanSelectiveChecks, 
build_local_verification_plan
+
+APACHE_AIRFLOW_URL = re.compile(r"github\.com[:/]apache/airflow(\.git)?/?$")
+
+
+def _run_git(*args: str) -> CompletedProcess:
+    return run_command(["git", *args], capture_output=True, text=True, 
check=False, cwd=AIRFLOW_ROOT_PATH)
+
+
+def find_default_base_ref() -> str | None:
+    """The target branch on the remote that points at apache/airflow, 
preferring ``upstream``."""
+    remotes = []
+    for line in _run_git("config", "--get-regexp", 
r"^remote\..*\.url$").stdout.splitlines():
+        key, _, url = line.partition(" ")
+        if APACHE_AIRFLOW_URL.search(url.strip()):
+            remotes.append(key.removeprefix("remote.").removesuffix(".url"))
+    for remote in sorted(remotes, key=lambda name: (name != "upstream", name)):
+        ref = f"{remote}/{AIRFLOW_BRANCH}"
+        if _run_git("rev-parse", "--verify", "--quiet", 
f"refs/remotes/{ref}").returncode == 0:
+            return ref
+    return None
+
+
+def has_merged_commits_missing_from(base_ref: str) -> bool:
+    """Whether a merge on the branch brought in commits that ``base_ref`` does 
not have, as after
+    GitHub's "Update branch" when the local copy of the target branch was not 
fetched since."""
+    for line in _run_git("rev-list", "--merges", "--parents", 
f"{base_ref}..HEAD").stdout.splitlines():
+        for merged_parent in line.split()[2:]:
+            if _run_git("merge-base", "--is-ancestor", merged_parent, 
base_ref).returncode != 0:
+                return True
+    return False
+
+
+def get_changed_files_against(base_ref: str) -> tuple[str, ...]:
+    def _git(*args: str) -> list[str]:
+        result = _run_git(*args)
+        if result.returncode != 0:
+            raise click.ClickException(
+                f"git {args[0]} failed for base ref {base_ref!r}: 
{result.stderr.strip()}"
+            )
+        return result.stdout.splitlines()
+
+    merge_base = _git("merge-base", base_ref, "HEAD")[0]
+    # CI lists changed files with diff-tree, which reports a rename as a 
delete plus an add.
+    changed = _git("diff", "--name-only", "--no-renames", merge_base) + _git(
+        "ls-files", "--others", "--exclude-standard"
+    )
+    return tuple(sorted(set(changed)))
+
+
[email protected](
+    name="verify",
+    help="List the local verification CI would require for the current 
changes. Nothing is executed.",
+)
[email protected](
+    "--base-ref",
+    help=f"Git ref the change will be merged into. Changed files are computed 
from its merge-base with "
+    f"HEAD. Defaults to {AIRFLOW_BRANCH} on the remote that points at 
apache/airflow, or the local "
+    f"{AIRFLOW_BRANCH} branch if there is no such remote.",
+)
[email protected](
+    "--full",
+    is_flag=True,
+    help="List everything CI runs for the default matrix cell except static 
checks, including the full "
+    "suite CI adds when a change touches CI tooling or dependency files.",
+)
[email protected]("--json", "as_json", is_flag=True, help="Print machine-readable 
JSON instead of a table.")
+@option_verbose
[email protected]_context
+def verify(ctx: click.Context, base_ref: str | None, full: bool, as_json: 
bool):
+    from airflow_breeze.utils.selective_checks import SelectiveChecks
+
+    if as_json:
+        ctx.meta[SKIP_LOCAL_REPRODUCTION] = True
+    no_apache_remote = False
+    if base_ref is None:
+        base_ref = find_default_base_ref()
+        if base_ref is None:
+            base_ref, no_apache_remote = AIRFLOW_BRANCH, True
+    # SelectiveChecks narrates its decisions on stdout; keep stdout for the 
result only.
+    with contextlib.redirect_stdout(sys.stderr if get_verbose() else 
io.StringIO()):
+        changed_files = get_changed_files_against(base_ref)
+        kwargs = dict(
+            files=changed_files,
+            commit_ref="HEAD",
+            default_branch=AIRFLOW_BRANCH,
+            default_constraints_branch=DEFAULT_AIRFLOW_CONSTRAINTS_BRANCH,
+            github_event=GithubEvents.PULL_REQUEST,
+        )
+        ci = SelectiveChecks(**kwargs)
+        sc = ci if full else LeanSelectiveChecks(**kwargs)
+        result = build_local_verification_plan(
+            sc, changed_files, base_ref, 
full_tests_needed=ci.full_tests_needed, full=full
+        )
+    warnings = []
+    if no_apache_remote:
+        warnings.append(
+            f"No git remote points at apache/airflow, so changes are compared 
with the local "
+            f"{AIRFLOW_BRANCH} branch."
+        )
+    if has_merged_commits_missing_from(base_ref):
+        warnings.append(
+            f"Your branch has merged commits that are not in {base_ref}, so 
they are listed as your "
+            f"changes. Update {base_ref} (for example with git fetch) and run 
again."
+        )
+    if as_json:
+        print(json.dumps(result, indent=2))
+        for warning in warnings:
+            get_stderr_console().print(f"[warning]{warning}[/]")
+        return
+    console = get_console()
+    for warning in warnings:
+        console.print(f"[warning]{warning}[/]\n")
+    console.print(
+        f"[info]{len(changed_files)} changed file(s) against {base_ref}, "
+        f"CI default Python {result['default_python_version']}[/]\n"
+    )
+    table = Table(title="What CI runs for this change" if full else "What to 
run for this change")
+    table.add_column("kind")
+    table.add_column("runs_in")
+    table.add_column("command", overflow="fold")
+    for item in result["items"]:
+        table.add_row(item["kind"], item["runs_in"], escape(item["command"]))
+    console.print(table)
+    console.print(
+        "\n[info]Static checks are not listed. Run prek as usual. It picks the 
hooks for the changed files.[/]"
+    )
+    if result["manual_prek_hooks"]:
+        console.print(
+            "\n[warning]CI also runs these prek hooks, which a default prek 
install does not run:[/]"
+        )
+        for hook in result["manual_prek_hooks"]:
+            console.print(escape(f"prek run --stage manual {hook} --from-ref 
{base_ref}"), soft_wrap=True)
+    if result["full_tests_needed"] and not full:
+        console.print(
+            "\n[warning]CI also runs the full suite for this change because it 
touches CI tooling or "
+            "dependency files. Pass --full to list it.[/]"
+        )
+    console.print(
+        f"\n[warning]Covers Python {result['default_python_version']} on 
sqlite only; other Python versions, "
+        "Postgres, MySQL, the provider compatibility matrix and CI-only jobs 
run in CI.[/]"
+    )
diff --git a/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py 
b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
index 4a429f556ab..87dd17f484b 100644
--- a/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
+++ b/dev/breeze/src/airflow_breeze/utils/reproduce_ci.py
@@ -218,9 +218,15 @@ def print_local_reproduction(commands: 
list[ReproductionCommand]) -> None:
     console.print(f"[warning]{ruler}[/]\n")
 
 
+SKIP_LOCAL_REPRODUCTION = "skip_local_reproduction"
+
+
 def maybe_print_reproduction(ctx: click.Context) -> None:
-    """Called by BreezeCommand.invoke() — prints reproduction instructions in 
CI."""
-    if not should_print_local_reproduction():
+    """Called by BreezeCommand.invoke() — prints reproduction instructions in 
CI.
+
+    Commands whose stdout is a machine-readable contract set 
``ctx.meta[SKIP_LOCAL_REPRODUCTION]``.
+    """
+    if not should_print_local_reproduction() or 
ctx.meta.get(SKIP_LOCAL_REPRODUCTION):
         return
 
     github_repository = ctx.params.get("github_repository", 
APACHE_AIRFLOW_GITHUB_REPOSITORY)
diff --git a/dev/breeze/src/airflow_breeze/utils/verification_plan.py 
b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
new file mode 100644
index 00000000000..6e2e73fe69d
--- /dev/null
+++ b/dev/breeze/src/airflow_breeze/utils/verification_plan.py
@@ -0,0 +1,170 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import json
+from dataclasses import asdict, dataclass
+from typing import Any
+
+from airflow_breeze.utils.selective_checks import SelectiveChecks
+
+
+@dataclass(frozen=True)
+class VerificationItem:
+    kind: str
+    command: str
+    runs_in: str
+
+
+# SelectiveChecks flag -> (kind, command, runs_in) of the CI job(s) that flag 
gates.
+# Mirrors .github/workflows/ci-amd.yml and basic-tests.yml; "breeze" means the 
command needs
+# Docker and the CI image, "host" means only host tooling.
+FLAG_COMMANDS: dict[str, tuple[tuple[str, str, str], ...]] = {
+    "run_task_sdk_tests": (("unit", "breeze testing task-sdk-tests", 
"breeze"),),
+    "run_airflow_ctl_tests": (("unit", "breeze testing airflow-ctl-tests", 
"breeze"),),
+    "run_scripts_tests": (("unit", "uv run --project scripts pytest 
scripts/tests/", "host"),),
+    "run_ui_tests": (
+        ("ui", "cd airflow-core/src/airflow/ui && pnpm install 
--frozen-lockfile && pnpm test", "host"),
+        (
+            "ui",
+            "cd airflow-core/src/airflow/api_fastapi/auth/managers/simple/ui 
&& pnpm install --frozen-lockfile && pnpm test",
+            "host",
+        ),
+    ),
+    "run_api_codegen": (("schema", "breeze testing python-api-client-tests", 
"breeze"),),
+    "run_go_sdk_tests": (("unit", "cd go-sdk && go test ./...", "host"),),
+    "run_java_sdk_tests": (
+        ("unit", "cd java-sdk && ./gradlew test", "host"),
+        ("docs", "breeze build-docs --sdk-docs-only --sdk=java", "breeze"),
+    ),
+    "run_ts_sdk_docs": (("docs", "breeze build-docs --sdk-docs-only 
--sdk=typescript", "breeze"),),
+    "run_agent_framework_tests": (
+        (
+            "unit",
+            'breeze shell --backend none --skip-db-tests "bash 
/opt/airflow/scripts/in_container/run_agent_framework_tests.sh strands-agents"',
+            "breeze",
+        ),
+        (
+            "unit",
+            'breeze shell --backend none --skip-db-tests "bash 
/opt/airflow/scripts/in_container/run_agent_framework_tests.sh google-adk"',
+            "breeze",
+        ),
+    ),
+    "run_breeze_integration_tests": (
+        ("unit", "cd dev/breeze && uv run --locked pytest", "host"),
+        ("unit", "cd dev/breeze && uv run --locked pytest -m 
integration_tests", "host"),
+    ),
+}
+
+# Gated CI jobs that need a cluster, a prod image or an e2e stack; classified 
only, never printed.
+NOT_RUNNABLE_LOCALLY: frozenset[str] = frozenset(
+    {
+        "run_kubernetes_tests",
+        "run_helm_tests",
+        "run_kustomize_overlays_tests",
+        "run_ui_e2e_tests",
+        "run_task_sdk_integration_tests",
+        "run_airflow_ctl_integration_tests",
+        "run_system_tests",
+        "run_remote_logging_s3_e2e_tests",
+        "run_remote_logging_elasticsearch_e2e_tests",
+        "run_remote_logging_opensearch_e2e_tests",
+        "run_event_driven_e2e_tests",
+        "run_java_sdk_e2e_tests",
+        "run_go_sdk_e2e_tests",
+        "run_openlineage_e2e_tests",
+        "run_openlineage_e2e_compat_tests",
+        "run_ts_sdk_e2e_tests",
+    }
+)
+
+# Gated CI jobs that run a prek hook a default `prek install` never triggers 
(the hooks are
+# manual or pre-push stage), mapped to that hook. They are not plan items; the 
hooks are
+# reported separately so the user can run them with `--stage manual`.
+PREK_JOBS: dict[str, str] = {
+    "run_mypy_providers": "mypy-providers",
+    "has_migrations": "migration-round-trip",
+}
+
+
+def build_unit_test_items(group: str, test_types_json: str | None) -> 
list[VerificationItem]:
+    test_types = " ".join(chunk["test_types"] for chunk in 
json.loads(test_types_json or "null") or [])
+    if not test_types:
+        return []
+    prefix = f"breeze testing {group}-tests"
+    return [
+        VerificationItem(
+            "unit",
+            f'{prefix} --run-in-parallel --run-db-tests-only 
--parallel-test-types "{test_types}"',
+            "breeze",
+        ),
+        VerificationItem(
+            "unit",
+            f'{prefix} --use-xdist --skip-db-tests --no-db-cleanup --backend 
none --parallel-test-types "{test_types}"',
+            "breeze",
+        ),
+    ]
+
+
+class LeanSelectiveChecks(SelectiveChecks):
+    """The same selective-checks rules without the run-everything safety net 
CI applies to changes
+    that touch CI tooling or dependency files; only what the changed files 
match themselves."""
+
+    full_tests_needed = False
+
+
+def build_local_verification_plan(
+    sc: SelectiveChecks,
+    changed_files: tuple[str, ...],
+    base_ref: str,
+    *,
+    full_tests_needed: bool,
+    full: bool = False,
+) -> dict[str, Any]:
+    items: list[VerificationItem] = []
+    if sc.run_unit_tests:
+        items += build_unit_test_items("core", 
sc.core_test_types_list_as_strings_in_json)
+    if not sc.skip_providers_tests:
+        items += build_unit_test_items("providers", 
sc.providers_test_types_list_as_strings_in_json)
+    for flag, commands in FLAG_COMMANDS.items():
+        if getattr(sc, flag):
+            items += [VerificationItem(*command) for command in commands]
+    if sc.docs_build:
+        items.append(
+            VerificationItem("docs", f"breeze build-docs 
{sc.docs_list_as_string}".rstrip(), "breeze")
+        )
+    if full:
+        # Jobs CI runs on every PR regardless of the change.
+        breeze_tests = VerificationItem("unit", "cd dev/breeze && uv run 
--locked pytest", "host")
+        if breeze_tests not in items:
+            items.append(breeze_tests)
+        if dists := " 
".join(sorted(json.loads(sc.shared_distributions_as_json))):
+            items.append(
+                VerificationItem(
+                    "unit",
+                    f"for d in {dists}; do (cd shared/$d && uv run --group dev 
pytest) || exit 1; done",
+                    "host",
+                )
+            )
+    return {
+        "base_ref": base_ref,
+        "default_python_version": sc.default_python_version,
+        "full_tests_needed": full_tests_needed,
+        "changed_files": list(changed_files),
+        "items": [asdict(item) for item in items],
+        "manual_prek_hooks": [hook for flag, hook in PREK_JOBS.items() if 
getattr(sc, flag)],
+    }
diff --git a/dev/breeze/tests/test_reproduce_ci.py 
b/dev/breeze/tests/test_reproduce_ci.py
index 13d9c1b58e6..5b85b41d360 100644
--- a/dev/breeze/tests/test_reproduce_ci.py
+++ b/dev/breeze/tests/test_reproduce_ci.py
@@ -24,10 +24,12 @@ import pytest
 
 from airflow_breeze.utils.click_utils import BreezeGroup
 from airflow_breeze.utils.reproduce_ci import (
+    SKIP_LOCAL_REPRODUCTION,
     ReproductionCommand,
     build_checkout_reproduction_commands,
     build_ci_image_reproduction_command,
     build_reproduction_command_from_context,
+    maybe_print_reproduction,
     print_local_reproduction,
     should_print_local_reproduction,
 )
@@ -430,3 +432,13 @@ class TestBuildReproductionCommandFromContext:
         ctx = _invoke_and_get_context(cmd, ["-b", "postgres"])
         result = build_reproduction_command_from_context(ctx)
         assert result.argv == ["my-cmd", "--backend", "postgres"]
+
+
[email protected]("airflow_breeze.utils.reproduce_ci.print_local_reproduction", 
autospec=True)
+def test_commands_can_opt_out_of_reproduction_banner(mock_print, monkeypatch):
+    monkeypatch.setenv("CI", "true")
+    monkeypatch.setenv("GITHUB_ACTIONS", "true")
+    ctx = click.Context(click.Command("verify"))
+    ctx.meta[SKIP_LOCAL_REPRODUCTION] = True
+    maybe_print_reproduction(ctx)
+    mock_print.assert_not_called()
diff --git a/dev/breeze/tests/test_verification_plan.py 
b/dev/breeze/tests/test_verification_plan.py
new file mode 100644
index 00000000000..6a5d10250d3
--- /dev/null
+++ b/dev/breeze/tests/test_verification_plan.py
@@ -0,0 +1,244 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import inspect
+import json
+import shlex
+from functools import cached_property
+from unittest.mock import Mock
+
+import pytest
+import yaml
+
+from airflow_breeze.global_constants import 
DEFAULT_PYTHON_MAJOR_MINOR_VERSION, GithubEvents
+from airflow_breeze.utils.path_utils import AIRFLOW_ROOT_PATH
+from airflow_breeze.utils.selective_checks import SelectiveChecks
+from airflow_breeze.utils.verification_plan import (
+    FLAG_COMMANDS,
+    NOT_RUNNABLE_LOCALLY,
+    PREK_JOBS,
+    LeanSelectiveChecks,
+    build_local_verification_plan,
+    build_unit_test_items,
+)
+
+NEUTRAL_COMMIT = "938f0c1f3cc4cbe867123ee8aa9f290f9f18100a"
+# Exported by SelectiveChecks but no workflow job is gated on them (build-info 
pass-through only).
+NOT_A_CI_JOB = {
+    "run_amazon_tests",
+    "run_api_tests",
+    "run_ol_tests",
+    "run_python_scans",
+    "run_javascript_scans",
+}
+
+
+def _selective_checks(files: tuple[str, ...], default_branch: str = "main") -> 
SelectiveChecks:
+    return SelectiveChecks(
+        files=files,
+        commit_ref=NEUTRAL_COMMIT,
+        github_event=GithubEvents.PULL_REQUEST,
+        pr_labels=tuple(),
+        default_branch=default_branch,
+    )
+
+
+def _mock_selective_checks(**flags: object) -> Mock:
+    sc = Mock(spec=SelectiveChecks)
+    for flag in (*FLAG_COMMANDS, *PREK_JOBS, "run_unit_tests", "docs_build"):
+        setattr(sc, flag, False)
+    sc.skip_providers_tests = True
+    sc.shared_distributions_as_json = "[]"
+    sc.default_python_version = DEFAULT_PYTHON_MAJOR_MINOR_VERSION
+    for flag, value in flags.items():
+        setattr(sc, flag, value)
+    return sc
+
+
[email protected]("flag", sorted(FLAG_COMMANDS))
+def test_each_flag_maps_to_its_commands(flag: str):
+    result = build_local_verification_plan(
+        _mock_selective_checks(**{flag: True}), (), "main", 
full_tests_needed=False
+    )
+    commands = [item["command"] for item in result["items"]]
+    assert commands == [command for _, command, _ in FLAG_COMMANDS[flag]]
+
+
+def test_docs_only_change_mirrors_the_ci_cell():
+    files = ("airflow-core/docs/index.rst",)
+    sc = _selective_checks(files)
+    result = build_local_verification_plan(sc, files, "main", 
full_tests_needed=False)
+    assert [item["command"] for item in result["items"]] == ["breeze 
build-docs apache-airflow"]
+
+
+def test_core_change_splits_db_and_non_db_cells():
+    files = ("airflow-core/src/airflow/models/dag.py",)
+    sc = _selective_checks(files)
+    core_types = " ".join(c["test_types"] for c in 
json.loads(sc.core_test_types_list_as_strings_in_json))
+    result = build_local_verification_plan(sc, files, "main", 
full_tests_needed=False)
+    unit = [
+        item["command"] for item in result["items"] if 
item["command"].startswith("breeze testing core-tests")
+    ]
+    assert unit == [
+        f'breeze testing core-tests --run-in-parallel --run-db-tests-only 
--parallel-test-types "{core_types}"',
+        "breeze testing core-tests --use-xdist --skip-db-tests --no-db-cleanup 
--backend none "
+        f'--parallel-test-types "{core_types}"',
+    ]
+
+
+def test_every_selective_checks_run_flag_is_classified():
+    run_flags = {
+        name
+        for name, value in inspect.getmembers(SelectiveChecks)
+        if isinstance(value, cached_property)
+        and (name.startswith("run_") or name in ("docs_build", 
"has_migrations"))
+    }
+    classified = (
+        set(FLAG_COMMANDS)
+        | NOT_RUNNABLE_LOCALLY
+        | set(PREK_JOBS)
+        | {"run_unit_tests", "docs_build"}
+        | NOT_A_CI_JOB
+    )
+    assert run_flags - classified == set(), (
+        "new SelectiveChecks flag: add it to FLAG_COMMANDS, 
NOT_RUNNABLE_LOCALLY, PREK_JOBS or NOT_A_CI_JOB"
+    )
+    assert classified - run_flags == set(), "classified flag no longer exists 
on SelectiveChecks"
+
+
[email protected]("group", ["core", "providers"])
+def test_unit_test_commands_use_the_same_flags_as_the_ci_script(group: str):
+    ci_script = AIRFLOW_ROOT_PATH / "scripts" / "ci" / "testing" / 
"run_unit_tests.sh"
+    ci_flag_sets = {
+        frozenset(shlex.split(line)[3:])
+        for line in ci_script.read_text().splitlines()
+        if line.strip().startswith(f"breeze testing {group}-tests")
+    }
+    for item in build_unit_test_items(group, json.dumps([{"description": "x", 
"test_types": "Always"}])):
+        tokens = shlex.split(item.command)[3:]
+        cut = tokens.index("--parallel-test-types")
+        assert frozenset(tokens[:cut] + tokens[cut + 2 :]) in ci_flag_sets, 
item.command
+
+
+def test_empty_diff_lists_nothing():
+    plan = build_local_verification_plan(_selective_checks(()), (), "main", 
full_tests_needed=False)
+    assert plan["items"] == []
+    assert plan["manual_prek_hooks"] == []
+
+
[email protected]("full", [False, True])
+def test_prek_jobs_are_not_listed(full: bool):
+    files = (
+        "providers/amazon/src/airflow/providers/amazon/hooks/s3.py",
+        "airflow-core/src/airflow/migrations/versions/0001_example.py",
+    )
+    sc = _selective_checks(files)
+    assert all(getattr(sc, flag) for flag in PREK_JOBS)
+    plan = build_local_verification_plan(sc, files, "main", 
full_tests_needed=False, full=full)
+    assert not any("prek" in item["command"] for item in plan["items"])
+    assert plan["manual_prek_hooks"] == ["mypy-providers", 
"migration-round-trip"]
+
+
[email protected]("hook", sorted(PREK_JOBS.values()))
+def test_prek_job_hooks_are_not_installed_by_default(hook: str):
+    stages = [
+        entry.get("stages", ["pre-commit"])
+        for config in [
+            AIRFLOW_ROOT_PATH / ".pre-commit-config.yaml",
+            *AIRFLOW_ROOT_PATH.glob("*/.pre-commit-config.yaml"),
+        ]
+        for repo in yaml.safe_load(config.read_text()).get("repos", [])
+        for entry in repo.get("hooks", [])
+        if entry.get("id") == hook
+    ]
+    assert stages, f"{hook} is not defined in any .pre-commit-config.yaml"
+    assert all("pre-commit" not in hook_stages for hook_stages in stages)
+
+
+def test_full_plan_skips_shared_distributions_when_there_are_none():
+    plan = build_local_verification_plan(
+        _mock_selective_checks(), (), "main", full_tests_needed=False, 
full=True
+    )
+    assert [item["command"] for item in plan["items"]] == ["cd dev/breeze && 
uv run --locked pytest"]
+
+
+def test_release_branch_drops_providers_tests():
+    files = ("airflow-core/src/airflow/models/dag.py",)
+    result = build_local_verification_plan(
+        _selective_checks(files, default_branch="v3-1-test"), files, 
"v3-1-test", full_tests_needed=False
+    )
+    commands = [item["command"] for item in result["items"]]
+    assert any(command.startswith("breeze testing core-tests") for command in 
commands)
+    assert not any(command.startswith("breeze testing providers-tests") for 
command in commands)
+
+
+def test_lean_plan_skips_the_full_suite_expansion_for_ci_tooling_changes():
+    files = ("dev/breeze/src/airflow_breeze/breeze.py",)
+    ci = _selective_checks(files)
+    lean = LeanSelectiveChecks(
+        files=files, commit_ref=NEUTRAL_COMMIT, 
github_event=GithubEvents.PULL_REQUEST, default_branch="main"
+    )
+    assert ci.full_tests_needed is True
+    full_commands = [
+        i["command"]
+        for i in build_local_verification_plan(ci, files, "main", 
full_tests_needed=True)["items"]
+    ]
+    lean_commands = [
+        i["command"]
+        for i in build_local_verification_plan(lean, files, "main", 
full_tests_needed=True)["items"]
+    ]
+    assert any(c.startswith("breeze testing core-tests") for c in 
full_commands)
+    assert not any(c.startswith("breeze testing") for c in lean_commands)
+    assert "cd dev/breeze && uv run --locked pytest" in lean_commands
+
+
+def test_lean_and_full_plans_agree_when_the_change_does_not_expand():
+    files = ("airflow-core/src/airflow/models/dag.py",)
+    lean = LeanSelectiveChecks(
+        files=files, commit_ref=NEUTRAL_COMMIT, 
github_event=GithubEvents.PULL_REQUEST, default_branch="main"
+    )
+    assert (
+        build_local_verification_plan(lean, files, "main", 
full_tests_needed=False)["items"]
+        == (
+            build_local_verification_plan(_selective_checks(files), files, 
"main", full_tests_needed=False)[
+                "items"
+            ]
+        )
+    )
+
+
+def test_full_plan_adds_the_jobs_ci_runs_on_every_pr():
+    files = ("airflow-core/docs/index.rst",)
+    sc = _selective_checks(files)
+    lean = build_local_verification_plan(sc, files, "main", 
full_tests_needed=False)["items"]
+    full = build_local_verification_plan(sc, files, "main", 
full_tests_needed=False, full=True)["items"]
+    assert [i["command"] for i in full] == [
+        *(i["command"] for i in lean),
+        "cd dev/breeze && uv run --locked pytest",
+        "for d in "
+        + " ".join(sorted(json.loads(sc.shared_distributions_as_json)))
+        + "; do (cd shared/$d && uv run --group dev pytest) || exit 1; done",
+    ]
+
+
+def test_full_plan_does_not_duplicate_breeze_tests_for_a_breeze_change():
+    files = ("dev/breeze/src/airflow_breeze/breeze.py",)
+    full = build_local_verification_plan(
+        _selective_checks(files), files, "main", full_tests_needed=True, 
full=True
+    )
+    assert [i["command"] for i in full["items"]].count("cd dev/breeze && uv 
run --locked pytest") == 1
diff --git a/dev/breeze/tests/test_verify_commands.py 
b/dev/breeze/tests/test_verify_commands.py
new file mode 100644
index 00000000000..58b9a163631
--- /dev/null
+++ b/dev/breeze/tests/test_verify_commands.py
@@ -0,0 +1,327 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import json
+import os
+import re
+import subprocess
+from subprocess import CompletedProcess
+from unittest.mock import patch
+
+import pytest
+from click.testing import CliRunner
+
+from airflow_breeze.commands.verify_commands import (
+    find_default_base_ref,
+    get_changed_files_against,
+    has_merged_commits_missing_from,
+    verify,
+)
+
+ANSI = re.compile(r"\x1b\[[0-9;]*m")
+
+
+def _default_base_ref() -> str:
+    return "main"
+
+
+def _no_merged_commits_missing(base_ref: str) -> bool:
+    return False
+
+
[email protected](autouse=True)
+def _no_git_lookups_against_the_checkout():
+    """Keep CLI tests independent of the developer's remotes; tests that need 
these patch them again."""
+    # Plain functions rather than mocks, so tests can still patch these with 
autospec=True.
+    with (
+        patch("airflow_breeze.commands.verify_commands.find_default_base_ref", 
new=_default_base_ref),
+        patch(
+            
"airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+            new=_no_merged_commits_missing,
+        ),
+    ):
+        yield
+
+
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    return_value=("airflow-core/docs/index.rst",),
+)
+def test_json_output_keeps_stdout_parseable(mock_files, monkeypatch):
+    # GitHub Actions makes breeze print a "how to reproduce" banner after 
every command.
+    monkeypatch.setenv("CI", "true")
+    monkeypatch.setenv("GITHUB_ACTIONS", "true")
+    monkeypatch.setenv("GITHUB_SHA", "0" * 40)
+    result = CliRunner().invoke(verify, ["--json"], catch_exceptions=False)
+    assert result.exit_code == 0
+    payload = json.loads(result.stdout)
+    assert set(payload) == {
+        "base_ref",
+        "default_python_version",
+        "full_tests_needed",
+        "changed_files",
+        "items",
+        "manual_prek_hooks",
+    }
+    assert payload["changed_files"] == ["airflow-core/docs/index.rst"]
+    assert payload["items"] == [
+        {"kind": "docs", "command": "breeze build-docs apache-airflow", 
"runs_in": "breeze"}
+    ]
+
+
+@patch(
+    "airflow_breeze.commands.verify_commands.run_command",
+    autospec=True,
+    return_value=CompletedProcess(
+        args=[], returncode=128, stdout="", stderr="fatal: Not a valid object 
name"
+    ),
+)
+def test_unknown_base_ref_is_a_clean_usage_error(mock_run):
+    result = CliRunner().invoke(verify, ["--base-ref", "no-such-branch"])
+    assert result.exit_code == 1
+    rendered = " ".join(ANSI.sub("", result.output).replace("│", " ").split())
+    assert "git merge-base failed for base ref 'no-such-branch': fatal: Not a 
valid object name" in rendered
+
+
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    return_value=("airflow-core/docs/index.rst",),
+)
+def test_selective_checks_narration_is_hidden_unless_verbose(mock_files):
+    result = CliRunner().invoke(verify, [], catch_exceptions=False)
+    assert result.exit_code == 0
+    assert "FileGroupForCi" not in result.output
+    assert "FileGroupForCi" not in result.stderr
+
+
[email protected]("args", [[], ["--full"]])
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    
return_value=("providers/amazon/src/airflow/providers/amazon/hooks/s3.py",),
+)
+def test_static_checks_are_left_to_prek(mock_files, args: list[str]):
+    result = CliRunner().invoke(verify, args, catch_exceptions=False)
+    assert result.exit_code == 0
+    output = " ".join(result.output.split())
+    assert "Static checks are not listed. Run prek as usual." in output
+    assert "prek run --stage manual mypy-providers --from-ref main" in output
+    assert "migration-round-trip" not in output
+
+
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    return_value=("airflow-core/docs/index.rst",),
+)
+def test_no_manual_prek_hint_when_no_hook_is_triggered(mock_files):
+    result = CliRunner().invoke(verify, [], catch_exceptions=False)
+    assert result.exit_code == 0
+    assert "--stage manual" not in " ".join(result.output.split())
+
+
[email protected](
+    ("files", "expanded"),
+    [
+        (("airflow-core/docs/index.rst",), False),
+        (("dev/breeze/src/airflow_breeze/breeze.py",), True),
+    ],
+)
+def test_full_suite_expansion_is_explained(files: tuple[str, ...], expanded: 
bool):
+    with patch(
+        "airflow_breeze.commands.verify_commands.get_changed_files_against", 
autospec=True, return_value=files
+    ):
+        result = CliRunner().invoke(verify, [], catch_exceptions=False)
+        full = CliRunner().invoke(verify, ["--full"], catch_exceptions=False)
+    assert result.exit_code == 0
+    assert ("CI also runs the full suite" in " ".join(result.output.split())) 
is expanded
+    assert "CI also runs the full suite" not in " ".join(full.output.split())
+    assert ("breeze testing core-tests" in " ".join(full.output.split())) is 
expanded
+    assert "breeze testing core-tests" not in " ".join(result.output.split())
+
+
+@patch("airflow_breeze.commands.verify_commands.run_command", autospec=True)
+def test_changed_files_list_renames_like_ci_diff_tree(mock_run):
+    mock_run.side_effect = [
+        CompletedProcess(args=[], returncode=0, stdout="abc123\n", stderr=""),
+        CompletedProcess(args=[], returncode=0, stdout="new.py\nold.py\n", 
stderr=""),
+        CompletedProcess(args=[], returncode=0, stdout="untracked.py\n", 
stderr=""),
+    ]
+    assert get_changed_files_against("main") == ("new.py", "old.py", 
"untracked.py")
+    assert mock_run.call_args_list[1].args[0] == ["git", "diff", 
"--name-only", "--no-renames", "abc123"]
+
+
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    return_value=("airflow-core/src/airflow/models/dag.py",),
+)
+def test_long_commands_are_folded_not_truncated(mock_files):
+    result = CliRunner().invoke(verify, [], catch_exceptions=False)
+    assert result.exit_code == 0
+    assert "\u2026" not in result.output
+    compact = re.sub(r"[\u2502\s]", "", result.output)
+    assert 
"breezetestingcore-tests--use-xdist--skip-db-tests--no-db-cleanup--backendnone" 
in compact
+
+
+FORK = "https://github.com/someone/airflow.git";
+APACHE_HTTPS = "https://github.com/apache/airflow.git";
+APACHE_SSH = "[email protected]:apache/airflow.git"
+
+
[email protected](
+    ("remotes", "fetched", "expected"),
+    [
+        pytest.param(
+            {"origin": FORK, "upstream": APACHE_HTTPS},
+            {"origin", "upstream"},
+            "upstream/main",
+            id="convention",
+        ),
+        pytest.param(
+            {"origin": FORK, "apache": APACHE_SSH}, {"origin", "apache"}, 
"apache/main", id="other-name-ssh"
+        ),
+        pytest.param(
+            {"aaa": APACHE_HTTPS, "upstream": APACHE_HTTPS},
+            {"aaa", "upstream"},
+            "upstream/main",
+            id="prefers-upstream",
+        ),
+        pytest.param(
+            {"aaa": APACHE_HTTPS, "upstream": APACHE_HTTPS}, {"aaa"}, 
"aaa/main", id="upstream-not-fetched"
+        ),
+        pytest.param(
+            {"origin": "https://github.com/apache/airflow-site.git"}, 
{"origin"}, None, id="other-apache-repo"
+        ),
+        pytest.param({"origin": FORK}, {"origin"}, None, id="fork-only"),
+    ],
+)
+@patch("airflow_breeze.commands.verify_commands.run_command", autospec=True)
+def test_default_base_ref_is_main_on_the_apache_remote(mock_run, remotes, 
fetched, expected):
+    def fake_git(cmd, **kwargs):
+        if cmd[1] == "config":
+            stdout = "".join(f"remote.{name}.url {url}\n" for name, url in 
remotes.items())
+            return CompletedProcess(args=cmd, returncode=0, stdout=stdout, 
stderr="")
+        exists = cmd[-1].removeprefix("refs/remotes/").removesuffix("/main") 
in fetched
+        return CompletedProcess(args=cmd, returncode=0 if exists else 1, 
stdout="", stderr="")
+
+    mock_run.side_effect = fake_git
+    assert find_default_base_ref() == expected
+
+
[email protected](
+    ("found", "compared_with", "warned"),
+    [("upstream/main", "upstream/main", False), (None, "main", True)],
+)
+def test_verify_compares_with_the_default_base_ref(found, compared_with: str, 
warned: bool):
+    with (
+        patch(
+            "airflow_breeze.commands.verify_commands.find_default_base_ref", 
autospec=True, return_value=found
+        ),
+        patch(
+            
"airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+            autospec=True,
+            return_value=False,
+        ),
+        patch(
+            
"airflow_breeze.commands.verify_commands.get_changed_files_against",
+            autospec=True,
+            return_value=("airflow-core/docs/index.rst",),
+        ) as mock_files,
+    ):
+        result = CliRunner().invoke(verify, [], catch_exceptions=False)
+    assert result.exit_code == 0
+    mock_files.assert_called_once_with(compared_with)
+    output = " ".join(ANSI.sub("", result.output).split())
+    assert ("No git remote points at apache/airflow" in output) is warned
+    assert f"changed file(s) against {compared_with}," in output
+
+
+def _git(repo, *args: str) -> None:
+    subprocess.run(
+        [
+            "git",
+            "-c",
+            "user.name=t",
+            "-c",
+            "user.email=t@t",
+            "-c",
+            "commit.gpgsign=false",
+            "-c",
+            f"core.hooksPath={os.devnull}",
+            *args,
+        ],
+        cwd=repo,
+        check=True,
+        capture_output=True,
+    )
+
+
+def _commit(repo, name: str) -> None:
+    (repo / name).write_text(name)
+    _git(repo, "add", name)
+    _git(repo, "commit", "-m", name)
+
+
[email protected](
+    ("merge_main", "base", "expected"),
+    [
+        pytest.param(False, "stale-copy", False, id="never-merged"),
+        pytest.param(True, "stale-copy", True, id="merged-newer-than-base"),
+        pytest.param(True, "main", False, id="merged-and-base-fetched"),
+    ],
+)
+def test_detects_merged_commits_the_base_does_not_have(tmp_path, merge_main: 
bool, base: str, expected: bool):
+    _git(tmp_path, "init", "-b", "main")
+    _commit(tmp_path, "B")
+    _git(tmp_path, "branch", "stale-copy")
+    _git(tmp_path, "switch", "-c", "feature")
+    _commit(tmp_path, "X")
+    _git(tmp_path, "switch", "main")
+    _commit(tmp_path, "C")
+    _git(tmp_path, "switch", "feature")
+    if merge_main:
+        _git(tmp_path, "merge", "--no-edit", "main")
+    with patch("airflow_breeze.commands.verify_commands.AIRFLOW_ROOT_PATH", 
tmp_path):
+        assert has_merged_commits_missing_from(base) is expected
+
+
[email protected]("as_json", [False, True])
+@patch(
+    "airflow_breeze.commands.verify_commands.has_merged_commits_missing_from",
+    autospec=True,
+    return_value=True,
+)
+@patch(
+    "airflow_breeze.commands.verify_commands.get_changed_files_against",
+    autospec=True,
+    return_value=("airflow-core/docs/index.rst",),
+)
+def test_merged_commits_missing_from_the_base_are_warned_about(mock_files, 
mock_merged, as_json: bool):
+    result = CliRunner().invoke(
+        verify, ["--base-ref", "upstream/main", *(["--json"] if as_json else 
[])], catch_exceptions=False
+    )
+    assert result.exit_code == 0
+    mock_merged.assert_called_once_with("upstream/main")
+    warning = "Your branch has merged commits that are not in upstream/main"
+    stream = result.stderr if as_json else result.stdout
+    assert warning in " ".join(ANSI.sub("", stream).split())
+    if as_json:
+        assert json.loads(result.stdout)["base_ref"] == "upstream/main"
diff --git a/dev/skill-evals/last-eval-hash.txt 
b/dev/skill-evals/last-eval-hash.txt
index 6e5a9b0264e..451e4d1e3d4 100644
--- a/dev/skill-evals/last-eval-hash.txt
+++ b/dev/skill-evals/last-eval-hash.txt
@@ -1,3 +1,3 @@
 # Generated by dev/skill-evals/eval.py — do not edit or resolve conflicts by 
hand.
 # Run `prek run run-skill-eval --hook-stage manual --all-files` to regenerate.
-062e019ffb2c0045c5676dd0b286ccfd25879eaa674abd768abe891a8ff3f70e
+d0e8ed3ebe9973449495c65634f02bfe2fb1a41e38bb4a09aa7d0199502b3f72

Reply via email to