This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/main by this push:
     new ec49a34  feat(security): config-driven lifts of 6 skills (PR2/5) (#386)
ec49a34 is described below

commit ec49a34910a59247ad49d2587ff658f194533e44
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat May 30 19:49:38 2026 +0200

    feat(security): config-driven lifts of 6 skills (PR2/5) (#386)
    
    Second of 5 PRs converting the security skill family from
    Airflow/ASF-coupled to a generic framework.
    
    PR1 (#381) landed the schema + adapter contracts with ASF
    defaults. This PR lifts 6 skills to read those knobs (and the
    existing sibling adopter-config files) instead of inlining
    ASF/Airflow values. Byte-equivalent for the airflow-s adopter:
    every value the skill currently inlines either becomes a
    reference to a config knob whose ASF default matches today's
    inlined value, OR keeps the inlined airflow-s value as a single
    named example in generic prose.
    
    Per-skill lifts:
    
    - security-tracker-stats-dashboard (+15/-6 SKILL.md, +14/-2
      default-config.yaml) — cross-reference `scope_detection.labels`
      and `tracker.labels` in default-config.yaml comments; no
      literal default values changed.
    
    - security-issue-deduplicate (+26/-15) — golden-rule scope cross-
      merge, scope-check, milestone shapes, and the CVE-record URL
      now reference `scope_detection.labels`, `<project-config>/milestones.md`,
      and `cve_authority.record_url_template`.
    
    - security-issue-import-from-md (+18/-5) — `<security-list>`
      placeholder uses, body-field heading mapping ->
      `tracker.body_fields`, label list -> `tracker.labels`, scope
      rule -> `scope_detection.labels`.
    
    - security-issue-fix (+78/-48) — toolchain commands (`uv`,
      `breeze`, `prek`) -> `<project-config>/fix-workflow.md`; package
      registries -> `release_process.artifact_registries`;
      `apache/airflow` -> `<upstream>`; `main` -> `<default-branch>`;
      local-clone probe paths kept as airflow-s examples in generic
      prose.
    
    - security-issue-triage (+48/-27) — scope-label triads ->
      `scope_detection.labels`; canned-response examples reframed
      as airflow-s named examples in generic guidance; `@`-handle
      routing -> roster references.
    
    - security-issue-import-from-pr (+85/-45) — biggest lift in this
      batch. Project-board node IDs de-inlined (already in
      project.md "GitHub project board" table). Scope cascade
      (`airflow|providers|chart` triad) -> `scope_detection.labels`
      with the airflow-s mapping kept as one named example.
      `Apache Airflow:` title-prefix -> `<vendor>: <product>:`
      derived from `project.md`.
    
    Aggregate: +280/-129 lines across 7 files. Validator clean
    (1 pre-existing soft warning on an unrelated skill). 218 tests
    green. No new placeholders introduced beyond those declared in
    PR1's schema.
    
    Out of scope: no ASF-default adapter (`tools/vulnogram/`,
    `tools/ponymail/`, `tools/gmail/asf-relay.md`) is touched — those
    are PR3/PR4. No skill outside the 6 above is touched. The
    deeper Vulnogram-state-machine refactor in `security-issue-sync`,
    `security-cve-allocate`, and `security-issue-invalidate` is PR4.
    The forwarder-relay sub-skill extract is PR3.
    
    Generated-by: Claude Code (Opus 4.7)
---
 .claude/skills/security-issue-deduplicate/SKILL.md |  41 ++++---
 .claude/skills/security-issue-fix/SKILL.md         | 126 +++++++++++++--------
 .../skills/security-issue-import-from-md/SKILL.md  |  23 +++-
 .../skills/security-issue-import-from-pr/SKILL.md  | 117 +++++++++++++------
 .claude/skills/security-issue-triage/SKILL.md      |  75 ++++++++----
 .../security-tracker-stats-dashboard/SKILL.md      |  15 ++-
 .../default-config.yaml                            |  16 ++-
 7 files changed, 282 insertions(+), 131 deletions(-)

diff --git a/.claude/skills/security-issue-deduplicate/SKILL.md 
b/.claude/skills/security-issue-deduplicate/SKILL.md
index f93d983..caee2f2 100644
--- a/.claude/skills/security-issue-deduplicate/SKILL.md
+++ b/.claude/skills/security-issue-deduplicate/SKILL.md
@@ -53,14 +53,19 @@ command, and shows all of them to the user. Nothing is 
applied
 until the user confirms. There is no fast-path.
 
 **Golden rule — never merge across scopes.** Two trackers with
-different **scope labels** (`airflow` vs. `providers`, `airflow`
-vs. `chart`, etc.) must not be merged. If an external reporter
-rediscovers the same bug in two different products' surfaces, that
-is a multi-scope report and the resolution is a
-**scope split** handled by the `security-issue-sync` skill, not a
-dedupe. This skill refuses to operate when the two candidate
-trackers have different scope labels, and the proposal says so
-explicitly.
+different **scope labels** must not be merged. The set of scope
+labels the project recognises comes from `scope_detection.labels`
+in 
[`<project-config>/project.md`](../../../<project-config>/project.md#scope-detection)
+(cross-referenced from 
[`<project-config>/scope-labels.md`](../../../<project-config>/scope-labels.md)).
+In the airflow-s adopter's case the scope labels are `airflow`,
+`providers`, and `chart`, so `airflow` vs. `providers` or
+`airflow` vs. `chart` are the typical mismatches; other adopters
+declare their own. If an external reporter rediscovers the same
+bug in two different products' surfaces, that is a multi-scope
+report and the resolution is a **scope split** handled by the
+`security-issue-sync` skill, not a dedupe. This skill refuses to
+operate when the two candidate trackers have different scope
+labels, and the proposal says so explicitly.
 
 **Golden rule — every `<tracker>` / `<upstream>` reference is
 clickable in the surface it lands on.** Whenever this skill emits
@@ -228,10 +233,13 @@ Verify:
 - Both trackers are in state `open` (merging into or out of a closed
   tracker is almost always a mistake; surface as a blocker if
   either side is already closed and ask the user to confirm).
-- Both have the **same scope label** — `airflow` vs. `airflow`,
-  or `providers` vs. `providers`, or `chart` vs. `chart`. If the
-  scope labels differ, refuse the merge and tell the user this is
-  a multi-scope report to be handled by `security-issue-sync`'s
+- Both have the **same scope label** — the recognised scope
+  labels come from `scope_detection.labels` in
+  
[`<project-config>/project.md`](../../../<project-config>/project.md#scope-detection).
+  In the airflow-s adopter that means matching one of `airflow`,
+  `providers`, or `chart` against itself. If the scope labels
+  differ, refuse the merge and tell the user this is a
+  multi-scope report to be handled by `security-issue-sync`'s
   scope-split flow instead.
 - Neither tracker is already labelled `duplicate` (that would
   indicate a partial-merge already happened and someone left it
@@ -262,8 +270,11 @@ Also capture:
 
 - Each tracker's **labels** (scope, `cve allocated`, `pr *`,
   `announced - emails sent`, etc.).
-- Each tracker's **milestone** (Airflow version / Providers wave /
-  Chart version).
+- Each tracker's **milestone** — per-scope milestone naming
+  conventions live in
+  [`<project-config>/milestones.md`](../../../<project-config>/milestones.md)
+  (the airflow-s adopter uses Airflow-version / Providers-wave /
+  Chart-version shapes, one per `scope_detection.labels` entry).
 - Each tracker's **assignees**.
 - Whether each tracker has a **CVE JSON attachment** comment (from
   `generate-cve-json --attach`) — only the kept side's attachment
@@ -440,7 +451,7 @@ before `</details>`.
 - Body: <keep.reporter>'s original report preserved; <drop.reporter>'s report 
appended as *"Second independent report"*.
 - Credits: **<keep credit>** + **<drop credit>**.
 - Mailing threads: both listed.
-- CVE: [<CVE-N>-<M>](https://cveprocess.apache.org/cve5/<CVE-N>-<M>) stays 
allocated here; [<tracker>#<drop>](...) being closed as duplicate.
+- CVE: [<CVE-N>-<M>](<cve-record-url>) stays allocated here; 
[<tracker>#<drop>](...) being closed as duplicate. The `<cve-record-url>` form 
is assembled from `cve_authority.record_url_template` in 
[`<project-config>/project.md`](../../../<project-config>/project.md#cve-authority)
 (the airflow-s adopter resolves to 
`https://cveprocess.apache.org/cve5/<CVE-ID>`).
 
 **Next:** <one-line next step — e.g. credit-preference confirmation for both, 
or Step 6 CVE refinement>.
 
diff --git a/.claude/skills/security-issue-fix/SKILL.md 
b/.claude/skills/security-issue-fix/SKILL.md
index 323edf2..c2e1b25 100644
--- a/.claude/skills/security-issue-fix/SKILL.md
+++ b/.claude/skills/security-issue-fix/SKILL.md
@@ -194,15 +194,19 @@ tracker only to discover you cannot push the branch.
   skill does **not** guess filesystem layouts — there is no
   hard-coded search path. The clone must:
   - have a remote pointing at your fork;
-  - be on a non-dirty `main` (or the appropriate base branch) —
-    the skill will create a new branch from that base;
-  - have the project's dev toolchain available — for the active
-    project see
+  - be on a non-dirty `<default-branch>` (or the appropriate base
+    branch) — the skill will create a new branch from that base;
+  - have the project's dev toolchain available — the list and
+    invocation form of those tools live in
     
[`<project-config>/fix-workflow.md`](../../../<project-config>/fix-workflow.md#toolchain)
-    (`uv`, Python 3.x, `breeze` when needed) and
+    (for the airflow-s adopter that's `uv`, Python, and `breeze`;
+    your project's toolchain is whatever `fix-workflow.md` declares)
+    and
     
[`<upstream>/contributing-docs`](https://github.com/<upstream>/blob/main/contributing-docs/README.md).
-- **Outbound HTTPS to `pypi.org` / `github.com`** for dependency
-  resolution and `gh` API calls.
+- **Outbound HTTPS** to the project's package registries (from
+  `release_process.artifact_registries` in
+  [`<project-config>/project.md`](../../../<project-config>/project.md))
+  and `github.com` for dependency resolution and `gh` API calls.
 
 See
 [Prerequisites for running the agent 
skills](../../../docs/prerequisites.md#prerequisites-for-running-the-agent-skills)
@@ -222,25 +226,32 @@ continue.
    on the first means no <tracker> access; on the second it is a
    quota/auth issue — both require user action, stop.
 2. **Fork exists and is pushable** —
-   `gh repo view <your-login>/airflow --json name --jq .name`
-   returns `airflow`. If there is no fork, tell the user to run
+   `gh repo view <your-login>/<upstream-repo-name> --json name --jq .name`
+   returns the bare repo name (the segment after the `/` in
+   `<upstream>`). If there is no fork, tell the user to run
    `gh repo fork <upstream> --clone=false` and re-invoke.
-3. **Local clone is found and clean** — probe the usual locations
-   (the input path if supplied, else `~/code/airflow`,
-   `~/src/airflow`, `~/airflow`, or a sibling of the current
-   working directory) for a directory whose `origin` remote
-   points at `<upstream>`. Then verify `git status
-   --porcelain` is empty. Uncommitted work would collide with the
-   branch the skill is about to create; stop and ask the user to
-   stash / commit / clean first.
+3. **Local clone is found and clean** — resolve the clone path
+   from
+   
[`.apache-steward-overrides/user.md`](../../../docs/setup/agentic-overrides.md)
+   → `environment.upstream_clone` (per
+   [`AGENTS.md` § Per-project and per-user 
configuration](../../../AGENTS.md#per-project-and-per-user-configuration)).
+   Verify that path resolves to a directory whose `origin` remote
+   points at `<upstream>`, then `git status --porcelain` is empty.
+   Uncommitted work would collide with the branch the skill is
+   about to create; stop and ask the user to stash / commit /
+   clean first. Do not probe hard-coded filesystem paths — layouts
+   vary per user.
 4. **Base branch is current** — `git fetch origin` and make sure
-   the base (default `main`, or the branch the user specified) is
-   a fast-forward of `origin/<base>`. Stale bases produce stale
-   PRs.
-5. **Toolchain probe** — `uv --version`, `python3 --version`. If
-   `breeze` is required for the area of the fix, also
-   `breeze --version`. Any missing tool stops the skill;
-   installing them mid-run is out of scope.
+   the base (default `<default-branch>`, or the branch the user
+   specified) is a fast-forward of `origin/<base>`. Stale bases
+   produce stale PRs.
+5. **Toolchain probe** — run the tool-version checks named in
+   
[`<project-config>/fix-workflow.md`](../../../<project-config>/fix-workflow.md#toolchain).
+   For the airflow-s adopter that is `uv --version`,
+   `python3 --version`, and `breeze --version` when `breeze` is
+   required for the area of the fix; your project's probe list is
+   whatever `fix-workflow.md` declares. Any missing tool stops the
+   skill; installing them mid-run is out of scope.
 6. **Privacy-LLM gate-check** passes:
 
    ```bash
@@ -465,21 +476,25 @@ touching any files:
    untracked or modified files the user did not opt in to).
    If it is dirty, stop and ask the user how to proceed.
 
-4. Check that `prek` is installed and hooks are enabled per
-   `<upstream>/AGENTS.md` — `uv tool install prek` and
-   `prek install` if not.
+4. Check that any project-required pre-commit hook tool is
+   installed and hooks are enabled per `<upstream>/AGENTS.md` and
+   
[`<project-config>/fix-workflow.md`](../../../<project-config>/fix-workflow.md#toolchain).
+   For the airflow-s adopter that's `uv tool install prek` and
+   `prek install`; your project may use plain `pre-commit` or a
+   different hook runner.
 
 5. Fast-forward the base branch to the latest upstream. For a typical
-   fix, that is `main`:
+   fix, that is `<default-branch>`:
 
    ```bash
-   git checkout main
-   git fetch <upstream-remote> main
-   git reset --hard <upstream-remote>/main
+   git checkout <default-branch>
+   git fetch <upstream-remote> <default-branch>
+   git reset --hard <upstream-remote>/<default-branch>
    ```
 
    Do not run this destructive command without the user's explicit
-   confirmation if `main` is ahead of the upstream for any reason.
+   confirmation if `<default-branch>` is ahead of the upstream for
+   any reason.
 
 ---
 
@@ -491,7 +506,7 @@ verbatim.**
 
 ### 5a. Branch and base
 
-- **Base:** `main` (or the specific release branch if agreed).
+- **Base:** `<default-branch>` (or the specific release branch if agreed).
 - **Branch name:** Use a descriptive, non-security slug. For example:
   - good: `fix-extra-links-xcom-deserialization`
   - good: `tighten-assets-graph-dag-permission-check`
@@ -555,31 +570,44 @@ List:
 - existing tests that the change must continue to pass,
 - new tests to be added that exercise the fix (required unless the
   change is a pure rename / typo fix),
-- the exact commands the skill will run locally before pushing, taken
-  from `<upstream>/AGENTS.md`:
+- the exact commands the skill will run locally before pushing,
+  taken from `<upstream>/AGENTS.md` and the toolchain block of
+  
[`<project-config>/fix-workflow.md`](../../../<project-config>/fix-workflow.md#toolchain).
+  In this example we use the airflow-s adopter's commands (the
+  `<upstream>` toolchain is `uv` / `prek` / `mypy`); your project's
+  invocation forms come from `fix-workflow.md`:
 
   - `uv run --project airflow-core pytest 
path/to/test.py::TestClass::test_method -xvs` (unit test),
-  - `prek run --from-ref main --stage pre-commit` (fast static checks),
-  - `prek run --from-ref main --stage manual` (slow static checks),
+  - `prek run --from-ref <default-branch> --stage pre-commit` (fast static 
checks),
+  - `prek run --from-ref <default-branch> --stage manual` (slow static checks),
   - and a type-check (`uv run --project <project> --with 
"apache-airflow-devel-common[mypy]" mypy <path>`) where applicable.
 
 ### 5e. Backport label
 
-If the `<tracker>` issue's milestone indicates a release branch that
-has not yet been cut (e.g. `3.1.9`, `3.2.1`), note which
-`backport-to-vX-Y-test` label the PR should carry so that the fix
-lands on the intended patch release. If no backport is needed (the
-milestone is the next `main`-branch release), say so explicitly.
+If the `<tracker>` issue's milestone indicates a release branch
+that has not yet been cut, note which backport label the PR should
+carry so that the fix lands on the intended patch release. The
+label vocabulary and the active release branches live in
+[`<project-config>/fix-workflow.md`](../../../<project-config>/fix-workflow.md#backport-labels)
+and
+[`<project-config>/release-trains.md`](../../../<project-config>/release-trains.md).
+If no backport is needed (the milestone is the next
+`<default-branch>`-branch release), say so explicitly.
 
 ### 5f. Newsfragment
 
-Per `<upstream>/AGENTS.md`, newsfragments are only added for
-major or breaking user-visible changes, and usually coordinated
-during review. For a security-adjacent bug fix, default to **not**
-adding a newsfragment in the initial PR — reviewers will ask for one
-if needed. Never add a newsfragment that describes the change as a
-security fix, because that reveals the security nature and defeats
-the whole point of the private tracking workflow.
+Per `<upstream>/AGENTS.md` and
+`release_process.newsfragments` in
+[`<project-config>/project.md`](../../../<project-config>/project.md),
+where the project ships a newsfragment / changelog-fragment tool,
+fragments are typically only added for major or breaking
+user-visible changes and usually coordinated during review. For a
+security-adjacent bug fix, default to **not** adding a fragment in
+the initial PR — reviewers will ask for one if needed. Never add a
+fragment that describes the change as a security fix, because that
+reveals the security nature and defeats the whole point of the
+private tracking workflow. Skip this section entirely for projects
+whose `release_process.newsfragments.enabled` is `false`.
 
 ### 5g. PR body draft
 
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md 
b/.claude/skills/security-issue-import-from-md/SKILL.md
index 1ff15ee..ac92a18 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -387,14 +387,15 @@ Map markdown sections to the standard `<tracker>` 
issue-template
 body fields (per
 [`tools/github/issue-template.md`](../../../tools/github/issue-template.md);
 the role → concrete-name mapping comes from
-[`<project-config>/project.md`](../../../<project-config>/project.md#issue-template-fields)):
+[`<project-config>/project.md`](../../../<project-config>/project.md#issue-template-fields),
+with the heading literals declared under `tracker.body_fields`):
 
 | Markdown source | Tracker body field | Shape |
 |---|---|---|
 | `## Details` + `## Impact` + `## Reproduction steps` | `The issue 
description` | Verbatim, in that order, separated by blank lines and a 
`**Impact**`/`**Reproduction steps**` sub-heading line. |
 | (auto) | `Short public summary for publish` | `_No response_` (the public 
summary is sanitised separately at Step 13). |
 | `**Repository:**` + `**Branch:**` | `Affected versions` | Literal text 
*"`<owner>/<repo>` @ `<branch>` — versions to be confirmed during triage."* The 
release-train mapping happens at allocation. |
-| (auto) | `Security mailing list thread` | `N/A — imported from markdown file 
<basename>; no security@ thread.` |
+| (auto) | `Security mailing list thread` (the concrete heading name comes 
from `tracker.body_fields.mailing_thread` in `<project-config>/project.md`) | 
`N/A — imported from markdown file <basename>; no <security-list> thread.` |
 | (auto) | `Public advisory URL` | `_No response_`. |
 | (auto) | `Reporter credited as` | `_No response_`. The credit decision 
happens at triage; if the file is AI-generated, there is typically no human 
finder to credit. If the markdown carries a `**Reporter:**` / `**Finder:**` / 
`**Discovered by:**` metadata line naming a specific handle, **apply the 
[bot/AI credit policy](../../../tools/vulnogram/bot-credits-policy.md)** before 
lifting it into the field — when the policy fires (e.g. the markdown was 
generated by an LLM scan and names the  [...]
 | `## Location` URL (when it points at a `<upstream>` PR) | `PR with the fix` 
| The URL. Otherwise `_No response_` — the location commonly references a 
vulnerable file, not a fix. |
@@ -411,7 +412,10 @@ out of the per-field surgery the other skills perform.
 
 ### 3c — Labels
 
-Apply at creation:
+Apply at creation (the concrete label names come from
+`tracker.labels` in `<project-config>/project.md` —
+`needs_triage` and `security_marker`; literals below are the
+ASF/airflow-s defaults):
 
 - **`needs triage`** — every finding from this skill enters the
   standard validity-assessment flow.
@@ -421,6 +425,10 @@ Apply at creation:
 
 Do **not** apply a scope label. Scope labels are assigned at
 Step 5 of the handling process, after the validity assessment.
+The project's scope-label vocabulary lives in
+[`scope-labels.md`](../../../<project-config>/scope-labels.md)
+and is enumerated under `scope_detection.labels` in
+[`<project-config>/project.md`](../../../<project-config>/project.md#scope-detection).
 
 ### 3d — Project board
 
@@ -521,7 +529,7 @@ Write the body to a temp file (per finding):
 cat > /tmp/import-md-<basename>-<index>-body.md <<'EOF'
 ### The issue description
 
-> **Imported from markdown file `<basename>` (finding <K>/<N>)** — there is no 
inbound `security@` report; the markdown sections below are the verbatim source.
+> **Imported from markdown file `<basename>` (finding <K>/<N>)** — there is no 
inbound `<security-list>` report; the markdown sections below are the verbatim 
source.
 
 **Details:**
 
@@ -545,7 +553,7 @@ _No response_
 
 ### Security mailing list thread
 
-N/A — imported from markdown file `<basename>`; no security@ thread.
+N/A — imported from markdown file `<basename>`; no <security-list> thread.
 
 ### Public advisory URL
 
@@ -762,6 +770,11 @@ the validity discussion produces signal.
 
 ### Example 1 — A six-finding AI-scan output
 
+In this example we use the airflow-s adopter's filename
+convention (`<reporter>-<project>-<date>`) — your project's
+file-naming convention is irrelevant to the skill; the basename
+just gets carried into the rollup comment verbatim.
+
 ```text
 import findings from /tmp/scan-michaelwinser-airflow-2026-04-28.md
 ```
diff --git a/.claude/skills/security-issue-import-from-pr/SKILL.md 
b/.claude/skills/security-issue-import-from-pr/SKILL.md
index ac40351..4dcd9ee 100644
--- a/.claude/skills/security-issue-import-from-pr/SKILL.md
+++ b/.claude/skills/security-issue-import-from-pr/SKILL.md
@@ -254,17 +254,30 @@ release train, the milestone format, the CVE container, 
and the
 *Affected versions* shape (see
 
[`<project-config>/scope-labels.md`](../../../<project-config>/scope-labels.md)).
 
-Map `pr.files[].path` to scope:
-
-| Path prefix | Scope | Notes |
+The scope label set and the `path_prefix` → scope mapping come
+from `scope_detection.labels` in
+[`<project-config>/project.md`](../../../<project-config>/project.md#scope-detection).
+Each entry there declares a `path_prefix` regex; the skill matches
+`pr.files[].path` against these regexes and the matching label
+becomes the tracker's scope.
+
+In this example we use the airflow-s adopter's scope labels
+(`airflow`, `providers`, `chart`) — your project's scope labels
+come from `scope_detection.labels`:
+
+| `path_prefix` match | Scope (airflow-s default) | Notes |
 |---|---|---|
-| `providers/<name>/…` | `providers` | Capture `<name>` (e.g. `amazon`, 
`cncf-kubernetes`, `smtp`) — used for the package name and the *Affected 
versions* field. |
-| `chart/…` or `helm-chart/…` | `chart` | Helm-chart-only changes. |
-| Anything else (e.g. `airflow-core/…`, top-level `airflow/…`, `task-sdk/…` 
through 3.2.x) | `airflow` | Core / shared. |
+| `^providers/` (with `<name>` segment, e.g. `providers/amazon/`) | 
`providers` | Capture `<name>` — used for the `packageName` substitution in 
`scope_detection.labels.providers.packageName` and the *Affected versions* 
field. |
+| `^chart/` | `chart` | Helm-chart-only changes. |
+| `^(airflow-core/|airflow/(?!providers/)|airflow-ctl/)` (or whatever the 
project's `airflow`-equivalent label declares) | `airflow` | Core / shared. |
+
+When `scope_detection.enabled` is `false`, every PR maps to the
+single product declared in the `product` block of `project.md` —
+skip the matching step and apply the default scope label (if any).
 
-**Mixed-scope guard.** If `pr.files[]` touches more than one
-scope (e.g. one file in `providers/amazon/` and one in
-`airflow-core/`), **stop** and surface a blocker:
+**Mixed-scope guard.** If `pr.files[]` matches more than one
+scope's `path_prefix` (e.g. one file under `^providers/` and one
+under `^airflow-core/`), **stop** and surface a blocker:
 
 > PR <N> changes files across more than one scope (`<scope-A>`,
 > `<scope-B>`). One tracker maps to one CVE container. Either
@@ -277,13 +290,14 @@ The same convention exists in
 *"if a report affects more than one scope, the security team
 splits the report into per-scope trackers before allocation."*
 
-**Multiple providers.** A PR that touches
-`providers/amazon/` only is a single-package `providers`
-tracker. A PR that touches `providers/amazon/` **and**
-`providers/google/` is still a single `providers`-scope tracker
-(scope is one), but the *Affected versions* body field carries
-**one line per affected package** — propose both lines in
-Step 5.
+**Multiple sub-packages within one scope.** When a scope's
+`packageName` template contains a `<…>` substitution (the
+airflow-s `providers` label uses
+`apache-airflow-providers-<provider>`), a PR that touches more
+than one sub-package within that scope (e.g. `providers/amazon/`
+**and** `providers/google/`) is still a single tracker (scope is
+one), but the *Affected versions* body field carries **one line
+per affected sub-package** — propose both lines in Step 5.
 
 **Test-only changes** (`*/tests/**`) do **not** count toward
 scope detection — they ride wherever the production code rides.
@@ -293,8 +307,13 @@ Strip them before applying the scope mapping.
 
 ## Step 3 — Propose milestone
 
-Milestone shape is scope-dependent (see
-[`milestones.md`](../../../<project-config>/milestones.md)):
+Milestone shape is scope-dependent. The per-scope milestone
+formats and "which scopes ride the PR's own milestone vs which
+ride a separate release-train wave" mapping live in
+[`<project-config>/milestones.md`](../../../<project-config>/milestones.md)
+and 
[`<project-config>/release-trains.md`](../../../<project-config>/release-trains.md).
+
+On the airflow-s adopter, the cascade is:
 
 - **`airflow` / `chart` scope** — propose the PR's own
   milestone (e.g. `Airflow 3.2.2`). If the PR has no milestone,
@@ -308,6 +327,10 @@ Milestone shape is scope-dependent (see
   If the PR is already merged and the next wave's date is
   unclear, surface the question and let the user pick.
 
+Other projects' scope-to-milestone mapping comes from their
+`milestones.md`; the skill applies the same "consult per-scope
+mapping; fall back to user pick on ambiguity" pattern.
+
 Validate the proposed milestone exists on `<tracker>`:
 
 ```bash
@@ -367,11 +390,13 @@ Start from `pr.title`. Strip:
 - `[skip ci]`, `[ci-skip]`, `[skip-ci]` markers.
 - Trailing `(#NNNN)` and `[#NNNN]`.
 
-Do **not** add `<vendor>: <product>:` (e.g. `Apache Airflow:`) prefix — that 
lives in the CVE
-title, not the tracker title (the
-[`security-cve-allocate`](../security-cve-allocate/SKILL.md) skill normalises 
for
-the CVE record). Tracker titles in `<tracker>` are
-plain-language summaries.
+Do **not** add a `<vendor>: <product>:` prefix (e.g. the
+airflow-s adopter would otherwise prepend `<vendor>: <product>:` —
+derived from `project.md`'s `vendor` / `product.name` fields) —
+that prefix lives in the CVE title, not the tracker title (the
+[`security-cve-allocate`](../security-cve-allocate/SKILL.md)
+skill normalises for the CVE record). Tracker titles in
+`<tracker>` are plain-language summaries.
 
 If the cleaned title is shorter than ~25 characters or vague
 (e.g. just `fix bug in secrets backend`), propose a longer
@@ -388,7 +413,7 @@ has nine fields. Fill them as follows:
 |---|---|
 | **The issue description** | Two paragraphs: (1) a one-line note `> 
**Imported from public PR <upstream>#<N>** — there is no inbound \`security@\` 
report; the PR description below is the public statement of the vulnerability.` 
(2) the PR body verbatim, fenced if it is heavily templated. |
 | **Short public summary for publish** | `_No response_` (the team writes this 
when drafting the advisory; not derivable from the PR). |
-| **Affected versions** | Per the scope's *Affected versions* convention from 
[`scope-labels.md`](../../../<project-config>/scope-labels.md). For 
`providers`: one line per affected package, `<package-name> < NEXT VERSION`. 
For `airflow` / `chart`: `< X.Y.Z` from the milestone. |
+| **Affected versions** | Per the scope's *Affected versions* convention from 
[`scope-labels.md`](../../../<project-config>/scope-labels.md). The 
`packageName` shape comes from `scope_detection.labels.<scope>.packageName` in 
[`<project-config>/project.md`](../../../<project-config>/project.md#scope-detection).
 On the airflow-s adopter: for `providers`, one line per affected sub-package, 
`<package-name> < NEXT VERSION`; for `airflow` / `chart`, `< X.Y.Z` from the 
milestone. |
 | **Security mailing list thread** | Sentinel: `N/A — opened from public PR 
<upstream>#<N>; no security@ thread`. The field is `required: true` in the form 
— the skill creates the issue via `gh api` (Step 7), which bypasses 
form-required-field enforcement, but the sentinel is still set so future 
`security-issue-sync` runs do not flag the field as missing. |
 | **Public advisory URL** | `_No response_`. |
 | **Reporter credited as** | `_No response_`. **The PR author is *not* 
credited as the CVE reporter for this kind of import.** A public PR is not a 
responsible disclosure — the contributor went straight to the public fix 
without giving the security team a chance to coordinate the announcement, so 
the security team neither owes a finder credit nor wants to incentivise the 
practice. The user can populate the field manually if there is a 
project-specific reason to credit a different individ [...]
@@ -445,21 +470,43 @@ learns about the CVE — if at all — when the public 
advisory ships.
 
 ### 5c — Labels
 
-Apply at creation:
+Apply at creation. Concrete label names come from `tracker.labels`
+in 
[`<project-config>/project.md`](../../../<project-config>/project.md#tracker)
+— the skill speaks in roles, the project binds role → literal:
+
+- **Scope label**: one of `scope_detection.labels` (airflow-s
+  values: `airflow`, `providers`, `chart`).
+- **PR-state label**: `tracker.labels.pr_open` if
+  `pr.state == OPEN`, `tracker.labels.pr_merged` if
+  `pr.state == MERGED` (airflow-s values: `pr created` /
+  `pr merged`).
+- **`security issue`** — required for the `<tracker>` *Auto-add
+  to project* workflow filter (`is:issue label:"security
+  issue"`); without it the issue will not appear on the board.
+  This is the airflow-s security-marker label; non-ASF adopters
+  whose marker label differs use whichever literal their auto-add
+  filter requires (declared in `tracker.labels.security_marker`).
+
+Do **not** apply the `tracker.labels.needs_triage` label (airflow-s
+value: `needs triage`) — this skill's deliberate-import contract
+is that the validity assessment has already happened.
 
-- **Scope label**: `<scope>` (`airflow`, `providers`, or `chart`).
-- **PR-state label**: `pr created` if `pr.state == OPEN`, `pr merged` if 
`pr.state == MERGED`.
-- **`security issue`** — required for the `<tracker>` *Auto-add to project* 
workflow filter (`is:issue label:"security issue"`); without it the issue will 
not appear on the board.
+### 5d — Project board
 
-Do **not** apply `needs triage` — this skill's deliberate-import
-contract is that the validity assessment has already happened.
+Target column: `Assessed`. The board's `project_board_node_id`,
+`status_field_node_id`, and the per-column option IDs all live in
+[`<project-config>/project.md`](../../../<project-config>/project.md#github-project-board);
+the skill reads the `Assessed` option ID from that table at run
+time (re-fetch via the introspection query in
+[`tools/github/project-board.md`](../../../tools/github/project-board.md)
+if a write returns `not found`).
 
-### 5d — Project board
+When `tracker.project_board_enabled` is `false` in
+[`<project-config>/project.md`](../../../<project-config>/project.md#tracker),
+this step is a no-op — skills skip column transitions on projects
+that don't run a board.
 
-Target column: `Assessed` (option ID
-`ce6377ce` — see
-[`<project-config>/project.md`](../../../<project-config>/project.md#github-project-board)).
-Validates the *Label + body state → Status* mapping:
+This validates the *Label + body state → Status* mapping:
 
 > Scope label applied, no CVE yet → `Assessed`.
 
diff --git a/.claude/skills/security-issue-triage/SKILL.md 
b/.claude/skills/security-issue-triage/SKILL.md
index 5198f30..f76a88a 100644
--- a/.claude/skills/security-issue-triage/SKILL.md
+++ b/.claude/skills/security-issue-triage/SKILL.md
@@ -246,7 +246,7 @@ in `docs/prerequisites.md` for the overall setup.
 |---|---|
 | `triage` (default) | every open issue carrying `needs triage` |
 | `triage #NNN`, `triage 212`, `triage #NNN, #MMM`, `triage #NNN-#MMM` | 
specific issues by number (verbatim — no resolution) |
-| `triage scope:<label>` (e.g. `triage scope:airflow`) | subset by scope 
label, when set; useful when scoped-batch triage is split across triagers |
+| `triage scope:<label>` (e.g. `triage scope:airflow` for the airflow-s 
adopter; the project's scope labels come from `scope_detection.labels` in 
[`<project-config>/project.md`](../../../<project-config>/project.md)) | subset 
by scope label, when set; useful when scoped-batch triage is split across 
triagers |
 | `triage CVE-YYYY-NNNNN` | the tracker for that allocated CVE — used together 
with `--retriage` (below) when a passed-triage decision needs re-litigating |
 | `--retriage` (flag) | force-include trackers that already had `needs triage` 
removed but where new comment activity warrants a fresh proposal (e.g. a 
reporter follow-up landed a substantive update; a sibling-vector report changed 
the team's read on a prior `INVALID` close). Combine with one of the selectors 
above; bare `--retriage` without a selector is a hard error — the skill refuses 
to re-triage everything ever. |
 
@@ -362,10 +362,14 @@ the inputs the classifier needs. Each tracker gets:
    bodies before passing them to the classifier.
 
 2. **Scope label** — extract from the `labels` field; classify as
-   one of `airflow`, `providers`, `chart`, `<missing>` (or the
-   project's analogous scope labels per
-   
[`<project-config>/scope-labels.md`](../../../<project-config>/scope-labels.md)).
-   The scope drives the `@`-mention routing in Step 4.
+   one of the project's scope labels declared in
+   `scope_detection.labels` in
+   [`<project-config>/project.md`](../../../<project-config>/project.md)
+   (see also
+   
[`<project-config>/scope-labels.md`](../../../<project-config>/scope-labels.md)),
+   or `<missing>` when no scope label is set yet. For the airflow-s
+   adopter this resolves to `airflow`, `providers`, `chart`. The
+   scope drives the `@`-mention routing in Step 4.
 
 3. **Linked-PR state** — same `gh search prs` calls as
    [`security-issue-sync`](../security-issue-sync/SKILL.md) Step
@@ -416,12 +420,16 @@ the inputs the classifier needs. Each tracker gets:
 5. **Canned-response precedent check** — scan
    
[`<project-config>/canned-responses.md`](../../../<project-config>/canned-responses.md)
    for headings whose name matches the tracker's report shape.
-   A hit on *"When someone claims Dag author-provided 'user
-   input' is dangerous"* (or analogous) is a strong signal for
-   `INVALID`; a hit on *"Image scan results"* / *"DoS/RCE
-   via Connection configuration"* signals `INFO-ONLY` or
-   `INVALID`. Surface the matching canned-response name
-   in the proposal so the team can confirm-with-template.
+   A hit on a *"misframed user-input"-shaped* template (e.g. the
+   airflow-s adopter's *"When someone claims Dag author-provided
+   'user input' is dangerous"*) is a strong signal for `INVALID`;
+   a hit on a *"scanner output"-shaped* or *"misconfiguration"-shaped*
+   template (e.g. airflow-s's *"Image scan results"* or *"DoS/RCE
+   via Connection configuration"*) signals `INFO-ONLY` or `INVALID`.
+   Project-specific heading names come from
+   
[`<project-config>/canned-responses.md`](../../../<project-config>/canned-responses.md);
+   surface the matching canned-response name in the proposal so the
+   team can confirm-with-template.
 
 6. **Cross-reference search** — for `PROBABLE-DUP` detection,
    run the same three-key fuzzy match
@@ -463,14 +471,25 @@ explain how this tracker maps to (or escapes) that 
wording.
 ### Trust-boundary cheat-sheet
 
 Apply mechanically before VALID / DEFENSE-IN-DEPTH /
-INVALID:
+INVALID. The table below is the **airflow-s adopter's worked
+example** — each row maps an actor-and-effect pair to a Security
+Model section the project considers authoritative. Adopters
+maintain their own per-project trust-boundary cheat-sheet at the
+top of
+[`<project-config>/security-model.md`](../../../<project-config>/security-model.md);
+the section names quoted in the *Default class* column are the
+literal `§` anchors declared there (the airflow-s table cites the
+airflow-s Security Model anchors). The *positive precedent* search
+in Step 2.6 reads the precedent-tracker label name from
+`tracker.labels.cve_allocated` in
+[`<project-config>/project.md`](../../../<project-config>/project.md).
 
 | If the attacker is… | …and the target / effect is… | Default class |
 |---|---|---|
 | DAG author | code execution in worker / DAG processor / Triggerer | INVALID 
(cite §"DAG Authors executing arbitrary code") |
 | DAG author | cross-DAG effect within shared parser / triggerer / worker pool 
| INVALID (cite §"Limiting DAG Author access to subset of Dags") |
 | Worker holding Execution JWT | read or write of another task's data via 
Execution API | INVALID (cite the *"Cross-DAG access via the Task Execution API 
or Task SDK"* canned: `ti:self` is mutation-only, not per-DAG access control) |
-| Authenticated UI / REST user with restricted DAG-scoped perms | reads other 
DAGs' data via UI / REST | **VALID** (precedent: prior CVEs on this shape — 
search closed `cve allocated` trackers in Step 2.6) |
+| Authenticated UI / REST user with restricted DAG-scoped perms | reads other 
DAGs' data via UI / REST | **VALID** (precedent: prior CVEs on this shape — 
search closed `tracker.labels.cve_allocated` trackers in Step 2.6) |
 | Operator / Deployment Manager | misconfigures something with side-effects | 
INVALID (cite §"Connection configuration users" / operator-trust framing) |
 | Authenticated user | DoS or self-XSS | INVALID (cite §"DoS by authenticated 
users" / §"Self-XSS by authenticated users") |
 | External actor (email sender, request poster) | exploit via parser on 
attacker-controlled input that reaches a supported platform | **VALID** |
@@ -504,11 +523,20 @@ Step 2's fuzzy-dup search looks for open-tracker 
duplicates. This
 step adds **rejection-precedent search** — same fuzzy keys (GHSA
 IDs, code pointers, subject keywords from Step 2a of
 
[`security-issue-import`](../security-issue-import/SKILL.md#step-2a--search-for-related-potentially-duplicate-existing-trackers)),
-but against **closed trackers labelled `invalid` /
-`not CVE worthy` / `duplicate`**:
+but against **closed trackers labelled with the project's closing-
+disposition labels** — resolve the literal label names from
+`tracker.labels.not_cve_worthy` and the generic closing-disposition
+labels (`invalid`, `duplicate`) declared in
+[`<project-config>/project.md`](../../../<project-config>/project.md)
+and
+[`<project-config>/scope-labels.md`](../../../<project-config>/scope-labels.md)
+(*Closing dispositions* section). For the airflow-s adopter these
+resolve to `invalid`, `not CVE worthy`, `duplicate`:
 
 ```bash
-# Per orthogonal key (code pointer, GHSA, subject keyword):
+# Per orthogonal key (code pointer, GHSA, subject keyword) — example
+# labels are the airflow-s defaults; substitute from the adopter's
+# tracker.labels and scope-labels.md closing dispositions:
 gh search issues "<key>" --repo <tracker> --state closed \
   --label "invalid" --json number,title,closedAt --jq '.[]'
 gh search issues "<key>" --repo <tracker> --state closed \
@@ -526,7 +554,10 @@ VALID → INVALID. Include the citation in the proposal:
 > (closed YYYY-MM-DD as INVALID, same shape: <one-line>).
 
 Also search for **positive precedents** — CVE-allocated trackers
-with similar shape — via:
+with similar shape — via (substitute the literal label from
+`tracker.labels.cve_allocated` in
+[`<project-config>/project.md`](../../../<project-config>/project.md);
+the airflow-s default is `cve allocated`):
 
 ```bash
 gh search issues "<key>" --repo <tracker> --state all \
@@ -617,10 +648,12 @@ Propose when **all** of:
 - The behaviour does **not** violate any documented rule, and a
   canned-response template in
   
[`<project-config>/canned-responses.md`](../../../<project-config>/canned-responses.md)
-  already covers the shape (e.g. *"Not an issue, please submit
-  it"*, *"DoS/RCE via Connection configuration"*, *"Image scan
-  results"*, *"When someone claims Dag author-provided 'user
-  input' is dangerous"*).
+  already covers the shape (the airflow-s adopter ships templates
+  named *"Not an issue, please submit it"*, *"DoS/RCE via Connection
+  configuration"*, *"Image scan results"*, and *"When someone claims
+  Dag author-provided 'user input' is dangerous"*; project-specific
+  heading names come from
+  
[`<project-config>/canned-responses.md`](../../../<project-config>/canned-responses.md)).
 
 `INFO-ONLY` is distinct from `INVALID`: the latter is
 typically a *misframing* the team has to explain (and may
diff --git a/.claude/skills/security-tracker-stats-dashboard/SKILL.md 
b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
index a685537..c97a43d 100644
--- a/.claude/skills/security-tracker-stats-dashboard/SKILL.md
+++ b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
@@ -204,15 +204,20 @@ The most-overridden knobs by adopters tend to be:
   changes the dashboard should highlight (skill adoption, team
   handover, policy update). Set to `[]` to remove them.
 - **`scope_labels:`** — the project's primary "what does this
-  affect" axis. Defaults to `[airflow, providers, chart]`;
-  adopters use whatever scope-label set
-  
[`<project-config>/scope-labels.md`](../../../projects/_template/scope-labels.md)
-  declares.
+  affect" axis. Resolved from `scope_detection.labels` in
+  [`<project-config>/project.md`](../../../projects/_template/project.md)
+  (and the matching rows of
+  
[`<project-config>/scope-labels.md`](../../../projects/_template/scope-labels.md)).
+  ASF/Airflow default is `[airflow, providers, chart]` —
+  non-ASF adopters re-state this list in their overlay to
+  match their own scope set.
 - **`categories:`** — the lifecycle-band classification rules.
   Defaults match the airflow-s reference implementation
   byte-for-byte; adopters with different label conventions
   (e.g. `triaged` instead of *no `needs triage`*) re-state the
-  whole list.
+  whole list. The label literals used in predicates come from
+  `tracker.labels` in
+  [`<project-config>/project.md`](../../../projects/_template/project.md).
 - **`triage.keywords:`** / **`triage.bot_prefixes:`** — the
   time-to-triage signal. Adopters whose security team uses
   different phrasing in triage-proposal comments override these.
diff --git a/tools/security-tracker-stats-dashboard/default-config.yaml 
b/tools/security-tracker-stats-dashboard/default-config.yaml
index 12edaad..b98e88b 100644
--- a/tools/security-tracker-stats-dashboard/default-config.yaml
+++ b/tools/security-tracker-stats-dashboard/default-config.yaml
@@ -7,16 +7,30 @@
 # (`TRACKER_STATS_BUCKETS`, `TRACKER_STATS_START`, 
`TRACKER_STATS_UPSTREAM_REPO`).
 #
 # Defaults below match the reference `airflow-s/airflow-s` dashboard
-# byte-for-byte.
+# byte-for-byte; the ASF adopter inherits these without an overlay.
+# Non-ASF adopters override via the YAML overlay pointed at by
+# `tracker_stats_config:` in <project-config>/security-tracker-stats.md.
+# The `upstream_repo`, `scope_labels`, and `categories` knobs below
+# mirror values declared in <project-config>/project.md (Repositories
+# block and `scope_detection.labels`) — keep them in sync with that
+# manifest when overriding.
 
 buckets: monthly                # monthly | quarterly
 start: null                     # null = first tracker createdAt; else 
"YYYY-MM" (monthly) or "YYYY-Qn" (quarterly)
 upstream_repo: apache/airflow   # null -> skip c_prc/c_prm/c_rel charts and 
the back-fill rule
+                                # Mirrors <project-config>/project.md -> 
`upstream_repo`; ASF default is the airflow-s adopter's
+                                # value. Override in the overlay if the 
renderer should chart a different repo (or null).
 
 milestones:
   - date: 2026-04-20
     label: skill adoption
 
+# Tracker scope labels — the project's "what does this affect" axis.
+# Mirrors the keys of `scope_detection.labels` in
+# <project-config>/project.md (and the rows of
+# <project-config>/scope-labels.md). ASF/Airflow default below matches
+# the three airflow-s scope labels; non-ASF adopters override in the
+# overlay to match their own scope label set.
 scope_labels: [airflow, providers, chart]
 
 # Categories - evaluated top-to-bottom, FIRST MATCH WINS. Multiple rules


Reply via email to