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 24ddd89  feat(labels): capability taxonomy + validator enforcement + 
sync check (#340)
24ddd89 is described below

commit 24ddd89cdc3959288a6a5460edd4684b5f494957
Author: Jarek Potiuk <[email protected]>
AuthorDate: Wed May 27 21:45:38 2026 +0200

    feat(labels): capability taxonomy + validator enforcement + sync check 
(#340)
    
    Introduce a canonical capability taxonomy for the framework, enforce
    it in the validator, and add a sync-check that keeps the docs and
    the live source aligned.
    
    == Taxonomy ==
    
    Nine `capability:*` buckets orthogonal to the existing `area:*`
    labels: triage, review, fix, intake, reconciliation (new — covers
    and tools may carry more than one capability when they genuinely
    span lifecycle phases — `security-issue-fix` is fix+resolve,
    `setup-isolated-setup-doctor` is setup+reassess, `cve-org` is
    resolve+intake, etc.
    
    docs/labels-and-capabilities.md is the canonical reference: label-
    dimension definitions, capability bucket definitions, per-skill map
    for all 30 skills, per-tool map for all 18 tools.
    
    == Rule ==
    
    AGENTS.md states the rule: every issue, PR, new tool, new skill,
    and (where applicable) new doc declares its capabilities.
    
    - Issues / PRs: `area:*` + every applicable `capability:*` label.
    - New tools: `**Capability:** capability:NAME` (or
      `capability:NAME + capability:NAME` for multi-value) in the
      README first paragraph.
    - New skills: `capability:` in SKILL.md frontmatter — single string
      or YAML list form.
    
    `setup-override-upstream` now picks both labels before opening a
    framework PR. `write-skill` requires the new frontmatter field on
    every new-skill scaffold.
    
    == Backfill ==
    
    - All 30 existing skills got `capability:` added to their
      frontmatter via a one-shot script aligned with the per-skill map.
    - 10 existing tool READMEs got a `**Capability:**` line.
    - 8 tools without a README got a minimal stub README declaring
      capability + pointing at existing internal docs.
    
    == Validator (renamed skill-validator -> skill-and-tool-validator) ==
    
    The validator outgrew its name. Renamed the directory, the Python
    module, the CLI entry point (`skill-validate` ->
    `skill-and-tool-validate`), and updated every cross-reference
    (.asf.yaml, .pre-commit-config.yaml, dependabot.yml, tests.yml,
    CONTRIBUTING.md, write-skill, init_skill.py).
    
    New checks:
    
    - `capability` is now in REQUIRED_FRONTMATTER_KEYS. Validates both
      single-string and YAML-list forms; rejects values outside the
      9-bucket taxonomy.
    - `validate_tools()` — every `tools/<name>/` must have a README
      declaring its capabilities. Both "missing README" and "missing
      capability line" are HARD violations.
    - `validate_capability_sync()` — compares the two tables in
      docs/labels-and-capabilities.md against the live frontmatter +
      tool README declarations, bidirectionally. Drift in either
      direction is a HARD violation. Italic-parenthetical future-state
      notes (`*(+ capability:X once #N lands)*`) are stripped before
      comparison so the doc can flag planned capabilities without
      tripping the check.
    
    Prek hook trigger expanded so the sync check fires on
    `tools/*/README.md` and `docs/labels-and-capabilities.md` changes,
    not just skill files.
    
    == Tests ==
    
    12 new tests across 3 classes — single + list + missing + invalid
    + list-with-invalid for the frontmatter check; valid + missing-
    readme + missing-cap + invalid + multi-value + regex regression
    guard for the tool check; aligned + skill-doc-no-live + live-skill-
    no-doc + skill-mismatch + tool-doc-no-live + live-tool-no-doc +
    italic-parens for the sync check. All 218 tests green.
    
    Generated-by: Claude Code (Opus 4.7)
---
 .claude/skills/contributor-nomination/SKILL.md     |   1 +
 .claude/skills/issue-fix-workflow/SKILL.md         |   1 +
 .claude/skills/issue-reassess-stats/SKILL.md       |   1 +
 .claude/skills/issue-reassess/SKILL.md             |   1 +
 .claude/skills/issue-reproducer/SKILL.md           |   1 +
 .claude/skills/issue-triage/SKILL.md               |   1 +
 .claude/skills/list-steward-skills/SKILL.md        |   1 +
 .claude/skills/pairing-self-review/SKILL.md        |   1 +
 .claude/skills/pr-management-code-review/SKILL.md  |   1 +
 .claude/skills/pr-management-mentor/SKILL.md       |   1 +
 .claude/skills/pr-management-stats/SKILL.md        |   1 +
 .claude/skills/pr-management-triage/SKILL.md       |   1 +
 .claude/skills/security-cve-allocate/SKILL.md      |   1 +
 .claude/skills/security-issue-deduplicate/SKILL.md |   1 +
 .claude/skills/security-issue-fix/SKILL.md         |   3 +
 .../skills/security-issue-import-from-md/SKILL.md  |   1 +
 .../skills/security-issue-import-from-pr/SKILL.md  |   1 +
 .claude/skills/security-issue-import/SKILL.md      |   1 +
 .claude/skills/security-issue-invalidate/SKILL.md  |   1 +
 .claude/skills/security-issue-sync/SKILL.md        |   1 +
 .claude/skills/security-issue-triage/SKILL.md      |   1 +
 .../security-tracker-stats-dashboard/SKILL.md      |   1 +
 .../skills/setup-isolated-setup-doctor/SKILL.md    |   3 +
 .../skills/setup-isolated-setup-install/SKILL.md   |   1 +
 .../skills/setup-isolated-setup-update/SKILL.md    |   1 +
 .../skills/setup-isolated-setup-verify/SKILL.md    |   1 +
 .claude/skills/setup-override-upstream/SKILL.md    |  23 +-
 .claude/skills/setup-shared-config-sync/SKILL.md   |   3 +
 .claude/skills/setup-steward/SKILL.md              |   1 +
 .claude/skills/write-skill/SKILL.md                |  35 +-
 .claude/skills/write-skill/scripts/init_skill.py   |   2 +-
 .claude/skills/write-skill/security-checklist.md   |   2 +-
 .github/dependabot.yml                             |   4 +-
 .github/workflows/tests.yml                        |   4 +-
 .pre-commit-config.yaml                            |  47 +--
 AGENTS.md                                          |  61 ++++
 CONTRIBUTING.md                                    |  16 +-
 docs/labels-and-capabilities.md                    | 279 +++++++++++++++
 tools/agent-isolation/README.md                    |   2 +
 tools/cve-org/README.md                            |  16 +
 tools/dashboard-generator/README.md                |   2 +
 tools/dev/README.md                                |  16 +
 tools/github/README.md                             |  16 +
 tools/gmail/README.md                              |  16 +
 tools/jira/README.md                               |   2 +
 tools/mail-source/README.md                        |  16 +
 tools/ponymail/README.md                           |  16 +
 tools/pr-management-stats/README.md                |   2 +
 tools/privacy-llm/README.md                        |  16 +
 tools/probe-templates/README.md                    |   2 +
 tools/sandbox-lint/README.md                       |   2 +
 tools/security-tracker-stats-dashboard/README.md   |   2 +
 .../README.md                                      |  10 +-
 .../pyproject.toml                                 |   6 +-
 .../src/skill_and_tool_validator}/__init__.py      | 344 ++++++++++++++++++-
 .../tests/test_validator.py                        | 381 +++++++++++++++++++--
 .../uv.lock                                        |   2 +-
 tools/skill-evals/README.md                        |   2 +
 tools/spec-loop/README.md                          |   2 +
 tools/spec-status-index/README.md                  |   2 +
 tools/vulnogram/README.md                          |  16 +
 61 files changed, 1312 insertions(+), 86 deletions(-)

diff --git a/.claude/skills/contributor-nomination/SKILL.md 
b/.claude/skills/contributor-nomination/SKILL.md
index bed219f..11bbc78 100644
--- a/.claude/skills/contributor-nomination/SKILL.md
+++ b/.claude/skills/contributor-nomination/SKILL.md
@@ -17,6 +17,7 @@ when_to_use: |
   provided and the user has not indicated they want to assess
   a contributor.
 argument-hint: "<github-handle> [window:Nm] [target:committer|pmc]"
+capability: capability:stats
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/issue-fix-workflow/SKILL.md 
b/.claude/skills/issue-fix-workflow/SKILL.md
index 2895510..1cc4258 100644
--- a/.claude/skills/issue-fix-workflow/SKILL.md
+++ b/.claude/skills/issue-fix-workflow/SKILL.md
@@ -16,6 +16,7 @@ when_to_use: |
   to `issue-triage` for issues classified BUG or
   FEATURE-REQUEST. Skip when the fix is non-trivial enough to
   need design discussion — those go through an RFC first.
+capability: capability:fix
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/issue-reassess-stats/SKILL.md 
b/.claude/skills/issue-reassess-stats/SKILL.md
index 2fff08c..d440cc0 100644
--- a/.claude/skills/issue-reassess-stats/SKILL.md
+++ b/.claude/skills/issue-reassess-stats/SKILL.md
@@ -13,6 +13,7 @@ when_to_use: |
   "which issues still fail across pool runs". Also as a
   pre-release check on whether the EOL pool has dropped, and
   as a periodic health-of-the-backlog view.
+capability: capability:stats
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/issue-reassess/SKILL.md 
b/.claude/skills/issue-reassess/SKILL.md
index e2d36a8..e5d270f 100644
--- a/.claude/skills/issue-reassess/SKILL.md
+++ b/.claude/skills/issue-reassess/SKILL.md
@@ -17,6 +17,7 @@ when_to_use: |
   audit before releases or after a major version cut. Skip
   when the goal is per-PR triage — that is `pr-management-triage`
   — or when the issues are still in active triage flow.
+capability: capability:reassess
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/issue-reproducer/SKILL.md 
b/.claude/skills/issue-reproducer/SKILL.md
index c404d0c..b97515d 100644
--- a/.claude/skills/issue-reproducer/SKILL.md
+++ b/.claude/skills/issue-reproducer/SKILL.md
@@ -16,6 +16,7 @@ when_to_use: |
   issue in its candidate set. Skip when the issue does not
   carry runnable example code — use `issue-triage` to assess
   instead.
+capability: capability:reassess
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/issue-triage/SKILL.md 
b/.claude/skills/issue-triage/SKILL.md
index 0fb4cf4..ce9ca75 100644
--- a/.claude/skills/issue-triage/SKILL.md
+++ b/.claude/skills/issue-triage/SKILL.md
@@ -16,6 +16,7 @@ when_to_use: |
   Skip when team consensus has landed — invoke
   `/issue-fix-workflow` for confirmed bugs or the appropriate
   closure flow directly.
+capability: capability:triage
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/list-steward-skills/SKILL.md 
b/.claude/skills/list-steward-skills/SKILL.md
index 8398e7b..032b2e4 100644
--- a/.claude/skills/list-steward-skills/SKILL.md
+++ b/.claude/skills/list-steward-skills/SKILL.md
@@ -15,6 +15,7 @@ when_to_use: |
   repository — agents route via the live frontmatter
   `description` field directly and do not need this index to
   choose a skill.
+capability: capability:stats
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/pairing-self-review/SKILL.md 
b/.claude/skills/pairing-self-review/SKILL.md
index dd91ed5..980c0fd 100644
--- a/.claude/skills/pairing-self-review/SKILL.md
+++ b/.claude/skills/pairing-self-review/SKILL.md
@@ -15,6 +15,7 @@ when_to_use: |
   whether their branch is ready before requesting a human maintainer review.
   Skip when a PR is already open — use `pr-management-code-review` for that.
 argument-hint: "[base:<ref>] [staged] [path:<glob>]"
+capability: capability:review
 license: Apache-2.0
 ---
 <!-- SPDX-License-Identifier: Apache-2.0
diff --git a/.claude/skills/pr-management-code-review/SKILL.md 
b/.claude/skills/pr-management-code-review/SKILL.md
index 5f171f6..615cfc4 100644
--- a/.claude/skills/pr-management-code-review/SKILL.md
+++ b/.claude/skills/pr-management-code-review/SKILL.md
@@ -13,6 +13,7 @@ when_to_use: |
   ready-for-maintainer-review queue". Use after `pr-management-triage` has 
produced reviewable PRs; skip when triage
   has not yet engaged the PR.
 argument-hint: "[pr:N] [area:LBL] [collab:true|false] [team:NAME] [ready] 
[dry-run]"
+capability: capability:review
 license: Apache-2.0
 ---
 <!-- SPDX-License-Identifier: Apache-2.0
diff --git a/.claude/skills/pr-management-mentor/SKILL.md 
b/.claude/skills/pr-management-mentor/SKILL.md
index 9851cd4..1b25641 100644
--- a/.claude/skills/pr-management-mentor/SKILL.md
+++ b/.claude/skills/pr-management-mentor/SKILL.md
@@ -21,6 +21,7 @@ when_to_use: |
   thread is security-sensitive, or when the maintainer has
   *deliberately* not replied yet — ask before invoking.
 argument-hint: "[issue-or-pr-number]"
+capability: capability:review
 license: Apache-2.0
 ---
 <!-- SPDX-License-Identifier: Apache-2.0
diff --git a/.claude/skills/pr-management-stats/SKILL.md 
b/.claude/skills/pr-management-stats/SKILL.md
index dea1fa5..e4ab9a7 100644
--- a/.claude/skills/pr-management-stats/SKILL.md
+++ b/.claude/skills/pr-management-stats/SKILL.md
@@ -12,6 +12,7 @@ when_to_use: |
   health check, before or after a triage sweep, or as an input to a planning
   session.
 argument-hint: "[repo:owner/name] [since:date] [clear-cache]"
+capability: capability:stats
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/pr-management-triage/SKILL.md 
b/.claude/skills/pr-management-triage/SKILL.md
index ace3afe..125faf2 100644
--- a/.claude/skills/pr-management-triage/SKILL.md
+++ b/.claude/skills/pr-management-triage/SKILL.md
@@ -20,6 +20,7 @@ when_to_use: |
   skill is a no-op when every candidate is already triaged or
   inside its grace window.
 argument-hint: "[pr:N] [label:LBL] [author:LOGIN] [review-for-me] [stale] 
[repo:owner/name]"
+capability: capability:triage
 license: Apache-2.0
 ---
 <!-- SPDX-License-Identifier: Apache-2.0
diff --git a/.claude/skills/security-cve-allocate/SKILL.md 
b/.claude/skills/security-cve-allocate/SKILL.md
index 91085ad..2c3fe3a 100644
--- a/.claude/skills/security-cve-allocate/SKILL.md
+++ b/.claude/skills/security-cve-allocate/SKILL.md
@@ -17,6 +17,7 @@ when_to_use: |
   valid/invalid decision has landed, or for trackers that
   already carry a CVE ID in their *CVE tool link* body field.
 argument-hint: "[issue-number] [CVE-YYYY-NNNNN]"
+capability: capability:resolve
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-deduplicate/SKILL.md 
b/.claude/skills/security-issue-deduplicate/SKILL.md
index 6f77510..2564411 100644
--- a/.claude/skills/security-issue-deduplicate/SKILL.md
+++ b/.claude/skills/security-issue-deduplicate/SKILL.md
@@ -16,6 +16,7 @@ when_to_use: |
   appropriate as a periodic cleanup action when a triager spots two
   open trackers describing the same bug from different angles.
 argument-hint: "[kept-issue] [duplicate-issue]"
+capability: capability:resolve
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-fix/SKILL.md 
b/.claude/skills/security-issue-fix/SKILL.md
index 94ef59e..41b412b 100644
--- a/.claude/skills/security-issue-fix/SKILL.md
+++ b/.claude/skills/security-issue-fix/SKILL.md
@@ -20,6 +20,9 @@ when_to_use: |
   classified as valid vulnerabilities, or changes that require
   the private-PR fallback path.
 argument-hint: "[issue-number]"
+capability:
+  - capability:fix
+  - capability:resolve
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md 
b/.claude/skills/security-issue-import-from-md/SKILL.md
index f249cfd..623e914 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -18,6 +18,7 @@ when_to_use: |
   (`security-issue-import`) or when there is a public PR to
   anchor the import on (`security-issue-import-from-pr`).
 argument-hint: "[path-to-markdown-file]"
+capability: capability:intake
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-import-from-pr/SKILL.md 
b/.claude/skills/security-issue-import-from-pr/SKILL.md
index b539833..490bd71 100644
--- a/.claude/skills/security-issue-import-from-pr/SKILL.md
+++ b/.claude/skills/security-issue-import-from-pr/SKILL.md
@@ -20,6 +20,7 @@ when_to_use: |
   does not host a validity discussion. For reports that arrive on
   `<security-list>`, use `security-issue-import`.
 argument-hint: "[pr-number] [repo:owner/name]"
+capability: capability:intake
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-import/SKILL.md 
b/.claude/skills/security-issue-import/SKILL.md
index 149a65e..b10142d 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -21,6 +21,7 @@ when_to_use: |
   answered-and-closed on-thread. Use `import last 30d` / `import all`
   (= 90d) for a wider backlog sweep when genuinely warranted.
 argument-hint: "[import] [last Nd|all] [skip threadId]"
+capability: capability:intake
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-invalidate/SKILL.md 
b/.claude/skills/security-issue-invalidate/SKILL.md
index 8869901..5f2f1ed 100644
--- a/.claude/skills/security-issue-invalidate/SKILL.md
+++ b/.claude/skills/security-issue-invalidate/SKILL.md
@@ -21,6 +21,7 @@ when_to_use: |
   already shipped — closing as invalid then is a retraction with
   public consequences and warrants explicit team escalation.
 argument-hint: "[issue-number]"
+capability: capability:resolve
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-sync/SKILL.md 
b/.claude/skills/security-issue-sync/SKILL.md
index cbdf068..b345045 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -16,6 +16,7 @@ when_to_use: |
   where the team member wants to reconcile a batch of open issues with the
   current state of the world.
 argument-hint: "[issue-number]"
+capability: capability:intake
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-issue-triage/SKILL.md 
b/.claude/skills/security-issue-triage/SKILL.md
index 9d622ed..38f0d36 100644
--- a/.claude/skills/security-issue-triage/SKILL.md
+++ b/.claude/skills/security-issue-triage/SKILL.md
@@ -23,6 +23,7 @@ when_to_use: |
   `/security-cve-allocate` (VALID),
   `/security-issue-invalidate` (INFO-ONLY / INVALID), or
   `/security-issue-deduplicate` (PROBABLE-DUP) directly.
+capability: capability:triage
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/security-tracker-stats-dashboard/SKILL.md 
b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
index f7b4eee..a685537 100644
--- a/.claude/skills/security-tracker-stats-dashboard/SKILL.md
+++ b/.claude/skills/security-tracker-stats-dashboard/SKILL.md
@@ -7,6 +7,7 @@ when_to_use: |
   variations. Also when an existing dashboard at the configured output
   path is stale (older than ~24 h) and the user is reviewing tracker
   health. Read-only — the skill never modifies any tracker state.
+capability: capability:stats
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-isolated-setup-doctor/SKILL.md 
b/.claude/skills/setup-isolated-setup-doctor/SKILL.md
index 453d9ed..971deb6 100644
--- a/.claude/skills/setup-isolated-setup-doctor/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-doctor/SKILL.md
@@ -18,6 +18,9 @@ when_to_use: |
   permission errors). Also a good periodic check after every
   Claude Code upgrade — the sandbox profile evolves and a
   previously-working call may have moved into deny.
+capability:
+  - capability:setup
+  - capability:reassess
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-isolated-setup-install/SKILL.md 
b/.claude/skills/setup-isolated-setup-install/SKILL.md
index 09c5148..a2a0cb0 100644
--- a/.claude/skills/setup-isolated-setup-install/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-install/SKILL.md
@@ -17,6 +17,7 @@ when_to_use: |
   already in place — use `setup-isolated-setup-verify` (to
   confirm completeness) or `setup-isolated-setup-update` (to
   refresh against the framework's latest) instead.
+capability: capability:setup
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-isolated-setup-update/SKILL.md 
b/.claude/skills/setup-isolated-setup-update/SKILL.md
index 14d55b0..632d690 100644
--- a/.claude/skills/setup-isolated-setup-update/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-update/SKILL.md
@@ -14,6 +14,7 @@ when_to_use: |
   blocked Bash call now appears to succeed. Recommended cadence
   per the doc: once per Claude Code upgrade or once a month,
   whichever comes first. Cheap to re-run; never destructive.
+capability: capability:setup
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-isolated-setup-verify/SKILL.md 
b/.claude/skills/setup-isolated-setup-verify/SKILL.md
index 2ccbb6b..7937352 100644
--- a/.claude/skills/setup-isolated-setup-verify/SKILL.md
+++ b/.claude/skills/setup-isolated-setup-verify/SKILL.md
@@ -17,6 +17,7 @@ when_to_use: |
   time a previously-blocked Bash call appears to have succeeded
   (the "did a denial silently turn into an allow?" canary). Cheap
   to re-run; never destructive.
+capability: capability:setup
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-override-upstream/SKILL.md 
b/.claude/skills/setup-override-upstream/SKILL.md
index 76759e8..3873d67 100644
--- a/.claude/skills/setup-override-upstream/SKILL.md
+++ b/.claude/skills/setup-override-upstream/SKILL.md
@@ -15,6 +15,7 @@ when_to_use: |
   override locally for a while and deciding the change is
   worth contributing back.
 argument-hint: "[skill-name]"
+capability: capability:setup
 license: Apache-2.0
 ---
 
@@ -276,11 +277,29 @@ In `<framework-clone>`:
 3. **Confirm with the user before posting**. Show the
    exact title + body. Wait for "OK to post" / "yes" /
    "send" / similar before running `gh pr create`.
-4. Write the PR body to a tempfile first, then create the PR:
+4. **Pick the labels.** Every framework PR carries at least one
+   `area:*` and one `capability:*` label per
+   
[`docs/labels-and-capabilities.md`](../../../docs/labels-and-capabilities.md).
+   The override is upstreaming a change to skill `<skill>`, so:
+   - `area:*` — follow the skill's family
+     (`area:pr-management` for `pr-management-*`, `area:security`
+     for `security-*`, `area:setup` for `setup-*`, `area:issue`
+     for `issue-*`, etc.).
+   - `capability:*` — the capability the change is *implementing*,
+     not the file paths touched. Look up the skill's capability in
+     the skill-to-capability map at
+     
[`docs/labels-and-capabilities.md#capability-to-skill-map`](../../../docs/labels-and-capabilities.md#capability-to-skill-map).
+   - Add `kind:*` and `mode:*` when they apply per the same doc.
+
+   Surface the chosen labels in the confirmation preview alongside
+   the PR title and body, so the user sees them before posting.
+
+5. Write the PR body to a tempfile first, then create the PR:
    ```bash
    # Write tool: file_path: /tmp/override-pr-body.md, content: <PR body>
    gh pr create --repo apache/airflow-steward --base main \
-     --head <user>:<branch> --title "..." --body-file /tmp/override-pr-body.md
+     --head <user>:<branch> --title "..." --body-file /tmp/override-pr-body.md 
\
+     --label "area:<area>" --label "capability:<capability>"
    ```
 
 ### Step 7 — Post-PR cleanup pointer
diff --git a/.claude/skills/setup-shared-config-sync/SKILL.md 
b/.claude/skills/setup-shared-config-sync/SKILL.md
index 6be05fe..f5c7ba9 100644
--- a/.claude/skills/setup-shared-config-sync/SKILL.md
+++ b/.claude/skills/setup-shared-config-sync/SKILL.md
@@ -18,6 +18,9 @@ when_to_use: |
   `setup-isolated-setup-update` surfaces drift on a script the
   user keeps in `~/.claude-config/` and wants propagated to
   other machines.
+capability:
+  - capability:intake
+  - capability:setup
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/setup-steward/SKILL.md 
b/.claude/skills/setup-steward/SKILL.md
index 301803f..b091821 100644
--- a/.claude/skills/setup-steward/SKILL.md
+++ b/.claude/skills/setup-steward/SKILL.md
@@ -30,6 +30,7 @@ when_to_use: |
   maintenance: "upgrade steward", "verify steward setup",
   "check steward drift", "the snapshot is stale".
 argument-hint: "[adopt|upgrade|worktree-init|verify|override 
skill-name|unadopt]"
+capability: capability:setup
 license: Apache-2.0
 ---
 
diff --git a/.claude/skills/write-skill/SKILL.md 
b/.claude/skills/write-skill/SKILL.md
index b3a118f..11c1dd3 100644
--- a/.claude/skills/write-skill/SKILL.md
+++ b/.claude/skills/write-skill/SKILL.md
@@ -6,7 +6,7 @@ description: |
   shape (frontmatter, resources, placeholder convention,
   prompt-injection defences, Privacy-LLM gate-check) and
   validates via the framework's existing
-  [`tools/skill-validator`](../../../tools/skill-validator/).
+  [`tools/skill-and-tool-validator`](../../../tools/skill-and-tool-validator/).
   Scaffolds new skills via `init_skill.py`.
 when_to_use: |
   Invoke when the user says "write a skill", "create a new skill",
@@ -14,6 +14,7 @@ when_to_use: |
   variations thereof. Also when refactoring or expanding an
   existing skill that should pick up the framework's current
   conventions (e.g. the prompt-injection-defence patterns).
+capability: capability:setup
 license: Apache-2.0
 ---
 
@@ -67,7 +68,7 @@ will recognise the workflow shape:
   [`docs/setup/install-recipes.md`](../../../docs/setup/install-recipes.md),
   not as zip artefacts. The upstream's `package_skill.py` is not
   included; **validation** is performed by the existing
-  [`tools/skill-validator`](../../../tools/skill-validator/),
+  [`tools/skill-and-tool-validator`](../../../tools/skill-and-tool-validator/),
   which is the framework's superset of the upstream's
   `quick_validate.py`.
 - **New Step 5 (security checklist)** added — a hard
@@ -108,6 +109,12 @@ skill bundles:
 │   │   ├── name (required, kebab-case, must equal directory name)
 │   │   ├── description (required, third-person)
 │   │   ├── when_to_use (required, third-person trigger phrases)
+│   │   ├── capability (required, one OR a YAML list of values from:
+│   │   │   `capability:triage`, `capability:review`, `capability:fix`,
+│   │   │   `capability:intake`, `capability:reconciliation`,
+│   │   │   `capability:resolve`, `capability:reassess`,
+│   │   │   `capability:stats`, `capability:setup` — see
+│   │   │   
[`docs/labels-and-capabilities.md`](../../../docs/labels-and-capabilities.md))
 │   │   └── license: Apache-2.0 (required, exact string)
 │   ├── SPDX header comment + placeholder-convention comment
 │   ├── # <skill-name> heading
@@ -301,7 +308,7 @@ no external content / no private content).
 Run the framework's existing skill validator:
 
 ```bash
-uv run --directory tools/skill-validator skill-validator \
+uv run --directory tools/skill-and-tool-validator skill-and-tool-validator \
   .claude/skills/<skill-name>/SKILL.md
 ```
 
@@ -359,6 +366,21 @@ for the override → upstream loop.
   expansion at the wrong layer.
 - **Always set `license: Apache-2.0` in the frontmatter.** The
   validator enforces this; the prek run will fail otherwise.
+- **Always declare a `capability:`** in the frontmatter, picking
+  one or more buckets from
+  
[`docs/labels-and-capabilities.md`](../../../docs/labels-and-capabilities.md).
+  Most skills fit a single bucket; when a skill genuinely spans
+  lifecycle phases (e.g. `security-issue-fix` does
+  `capability:fix` + `capability:resolve`,
+  `setup-isolated-setup-doctor` does
+  `capability:setup` + `capability:reassess`), use the YAML list
+  form and list **all** that apply — do not collapse to one to be
+  neat. If the skill doesn't fit any of the nine buckets at all,
+  treat that as a design signal worth pausing for — either the
+  bucket set needs a new entry (raise an issue against
+  
[`docs/labels-and-capabilities.md`](../../../docs/labels-and-capabilities.md))
+  or the skill's scope is straddling too many phases and should be
+  split. Do not invent ad-hoc capability values.
 - **Always credit upstream content in `NOTICE`.** When adapting
   third-party skills (as this skill itself was adapted from
   `JuliusBrussee/awesome-claude-skills`), the project root
@@ -376,12 +398,17 @@ for the override → upstream loop.
 - [`AGENTS.md`](../../../AGENTS.md) — the framework's authoring
   conventions, placeholder convention, prompt-injection
   absolute rule.
+- [`docs/labels-and-capabilities.md`](../../../docs/labels-and-capabilities.md)
+  — the label taxonomy: `area:*` + `capability:*` dimensions, the
+  nine capability buckets, the skill / tool → capability map, and
+  the rule that every framework issue / PR / tool / skill / doc
+  declares its capability.
 - [`docs/setup/agentic-overrides.md`](../../../docs/setup/agentic-overrides.md)
   — the `Adopter overrides` contract every skill consults.
 - [`docs/setup/install-recipes.md`](../../../docs/setup/install-recipes.md)
   — the snapshot model that distributes skills (no zip
   packaging — Step 5 of the upstream's flow is dropped).
-- [`tools/skill-validator/`](../../../tools/skill-validator/) —
+- 
[`tools/skill-and-tool-validator/`](../../../tools/skill-and-tool-validator/) —
   the framework's frontmatter / placeholder / link validator.
 - [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md)
   — the Privacy-LLM gate-check boilerplate Step 5 references.
diff --git a/.claude/skills/write-skill/scripts/init_skill.py 
b/.claude/skills/write-skill/scripts/init_skill.py
index c6fdc4b..c345801 100755
--- a/.claude/skills/write-skill/scripts/init_skill.py
+++ b/.claude/skills/write-skill/scripts/init_skill.py
@@ -49,7 +49,7 @@ The script creates the skill directory with:
   skill reads private content.
 
 The skill is *not* validated by this script. Run
-``tools/skill-validator/`` separately after editing.
+``tools/skill-and-tool-validator/`` separately after editing.
 """
 
 from __future__ import annotations
diff --git a/.claude/skills/write-skill/security-checklist.md 
b/.claude/skills/write-skill/security-checklist.md
index 234c736..7b81806 100644
--- a/.claude/skills/write-skill/security-checklist.md
+++ b/.claude/skills/write-skill/security-checklist.md
@@ -231,7 +231,7 @@ backstops:
 1. **`init_skill.py`** scaffolds a SKILL.md skeleton with
    placeholders for the injection-guard callout (Pattern 4) and
    the Privacy-LLM gate-check (Pattern 6).
-2. **`tools/skill-validator`** validates frontmatter shape and
+2. **`tools/skill-and-tool-validator`** validates frontmatter shape and
    placeholder usage — it does not check for the patterns above.
 3. **`prek` hooks** (`check-placeholders`, `markdownlint`,
    `typos`) catch common mistakes but not pattern violations.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 34d005b..9d5f0b1 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -132,7 +132,7 @@ updates:
           - "*"
 
   - package-ecosystem: "uv"
-    directory: "/tools/skill-validator"
+    directory: "/tools/skill-and-tool-validator"
     schedule:
       interval: "weekly"
     cooldown:
@@ -141,7 +141,7 @@ updates:
       semver-minor-days: 7
       semver-patch-days: 7
     groups:
-      skill-validator-deps:
+      skill-and-tool-validator-deps:
         patterns:
           - "*"
 
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index b3a2ca4..25020bb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -46,8 +46,8 @@ jobs:
             path: tools/gmail/oauth-draft
           - name: generate-cve-json
             path: tools/vulnogram/generate-cve-json
-          - name: skill-validator
-            path: tools/skill-validator
+          - name: skill-and-tool-validator
+            path: tools/skill-and-tool-validator
           - name: privacy-llm-checker
             path: tools/privacy-llm/checker
           - name: privacy-llm-redactor
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 589361b..0d71ad1 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -278,39 +278,42 @@ repos:
 
   - repo: local
     hooks:
-      - id: skill-validator-ruff-check
-        name: ruff check (skill-validator)
+      - id: skill-and-tool-validator-ruff-check
+        name: ruff check (skill-and-tool-validator)
         language: system
-        entry: uv run --directory tools/skill-validator ruff check
-        files: ^tools/skill-validator/(src|tests|pyproject\.toml)
+        entry: uv run --directory tools/skill-and-tool-validator ruff check
+        files: ^tools/skill-and-tool-validator/(src|tests|pyproject\.toml)
         pass_filenames: false
-      - id: skill-validator-ruff-format
-        name: ruff format (skill-validator)
+      - id: skill-and-tool-validator-ruff-format
+        name: ruff format (skill-and-tool-validator)
         language: system
-        entry: uv run --directory tools/skill-validator ruff format --check
-        files: ^tools/skill-validator/(src|tests|pyproject\.toml)
+        entry: uv run --directory tools/skill-and-tool-validator ruff format 
--check
+        files: ^tools/skill-and-tool-validator/(src|tests|pyproject\.toml)
         pass_filenames: false
-      - id: skill-validator-mypy
-        name: mypy (skill-validator)
+      - id: skill-and-tool-validator-mypy
+        name: mypy (skill-and-tool-validator)
         language: system
-        entry: uv run --directory tools/skill-validator mypy
-        files: ^tools/skill-validator/(src|tests|pyproject\.toml)
+        entry: uv run --directory tools/skill-and-tool-validator mypy
+        files: ^tools/skill-and-tool-validator/(src|tests|pyproject\.toml)
         pass_filenames: false
-      - id: skill-validator-pytest
-        name: pytest (skill-validator)
+      - id: skill-and-tool-validator-pytest
+        name: pytest (skill-and-tool-validator)
         language: system
-        entry: uv run --directory tools/skill-validator pytest
-        files: ^tools/skill-validator/(src|tests|pyproject\.toml)
+        entry: uv run --directory tools/skill-and-tool-validator pytest
+        files: ^tools/skill-and-tool-validator/(src|tests|pyproject\.toml)
         pass_filenames: false
 
-  # Validate `.claude/skills/**` via the `skill-validate` CLI. Re-fires
-  # on validator-source changes so rule updates get re-applied.
+  # Validate `.claude/skills/**`, every `tools/<name>/README.md`, and the
+  # `docs/labels-and-capabilities.md` taxonomy via the
+  # `skill-and-tool-validate` CLI. Re-fires on validator-source changes so
+  # rule updates get re-applied. The doc and tool-README triggers exist so
+  # the capability-sync check catches drift between docs and live source.
   # `--project` (not `--directory`) so CWD stays at repo root.
   - repo: local
     hooks:
-      - id: skill-validate
-        name: skill-validate (.claude/skills/**)
+      - id: skill-and-tool-validate
+        name: skill-and-tool-validate (skills, tools, capability sync)
         language: system
-        entry: uv run --project tools/skill-validator skill-validate
-        files: 
^(\.claude/skills/.*\.md|tools/skill-validator/(src|pyproject\.toml))
+        entry: uv run --project tools/skill-and-tool-validator 
skill-and-tool-validate
+        files: 
^(\.claude/skills/.*\.md|tools/[^/]+/README\.md|docs/labels-and-capabilities\.md|tools/skill-and-tool-validator/(src|pyproject\.toml))
         pass_filenames: false
diff --git a/AGENTS.md b/AGENTS.md
index af00e3f..ef759bb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,6 +10,7 @@
     - [Placeholder convention used in skill 
files](#placeholder-convention-used-in-skill-files)
   - [Local setup](#local-setup)
   - [Commit and PR conventions](#commit-and-pr-conventions)
+  - [Labeling issues, PRs, tools, and 
documentation](#labeling-issues-prs-tools-and-documentation)
   - [Confidentiality of the tracker 
repository](#confidentiality-of-the-tracker-repository)
     - [Sharing a tracker URL with someone who cannot access 
it](#sharing-a-tracker-url-with-someone-who-cannot-access-it)
     - [What public surfaces still must not 
contain](#what-public-surfaces-still-must-not-contain)
@@ -495,6 +496,66 @@ there — never leave it in place "because it's already 
there".
 - Keep the commit message focused on the user-visible change, not the 
mechanics of how the edit
   was made.
 
+## Labeling issues, PRs, tools, and documentation
+
+This repository uses an orthogonal label taxonomy with two required
+dimensions on every issue and PR:
+
+- **`area:*`** — *what part of the framework does this touch?* (e.g.
+  `area:pr-management`, `area:security`, `area:setup`, `area:issue`,
+  `area:tools`, `area:ci`, `area:docs`).
+- **`capability:*`** — *what does the tool / change actually do?* (e.g.
+  `capability:triage`, `capability:review`, `capability:fix`,
+  `capability:intake`, `capability:reconciliation`,
+  `capability:resolve`, `capability:reassess`, `capability:stats`,
+  `capability:setup`).
+
+The full taxonomy — every label dimension, every capability bucket,
+the skill-to-capability and tool-to-capability maps — lives in
+[`docs/labels-and-capabilities.md`](docs/labels-and-capabilities.md).
+Read that page once; treat it as the source of truth.
+
+**Rules:**
+
+- When opening an **issue** on this repository, apply at least one
+  `area:*` and one `capability:*` label. Apply every capability the
+  issue spans — do not collapse to a single "primary" if the issue
+  genuinely covers multiple lifecycle phases.
+- When opening a **pull request**, same: `area:*` + every applicable
+  `capability:*`. Match the capabilities the change is *implementing*,
+  not the file paths it touches.
+- When adding a new **tool** under `tools/`, declare its capabilities
+  in the first paragraph of the tool's README using
+  `**Capability:** capability:NAME` (or `capability:NAME + capability:NAME`
+  when two apply). A tool is pure substrate by default
+  (`capability:setup`); if it grows to encode a specific lifecycle
+  phase as a first-class feature, add that capability too and explain
+  the dual role in the README.
+- When adding a new **skill** under `.claude/skills/`, declare the
+  capability in the skill's frontmatter — a single string for
+  single-capability skills, a YAML list for multi-capability skills:
+
+  ```yaml
+  capability: capability:triage
+  # or
+  capability:
+    - capability:intake
+    - capability:reconciliation
+  ```
+
+  The [`write-skill`](.claude/skills/write-skill/SKILL.md) skill
+  prompts for this on every new-skill scaffold.
+- When adding a new **doc** under `docs/`, link to
+  [`docs/labels-and-capabilities.md`](docs/labels-and-capabilities.md)
+  and name the capability the doc is about in its first paragraph
+  *if* the doc is capability-specific. Cross-cutting docs
+  (`MISSION.md`, top-level READMEs) need no capability marker.
+
+The taxonomy applies to *this framework repository*. Skills that create
+issues or PRs on an **adopter's tracker** (e.g. `security-issue-import`,
+`security-issue-fix`, `issue-fix-workflow`) use the adopter's own label
+scheme — adopters may mirror this taxonomy but are not required to.
+
 ## Confidentiality of the tracker repository
 
 The tracker repository (`<tracker>`) is private — only security-team
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 01cd3ea..915c8bf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -225,7 +225,7 @@ every piece of context it needs from some combination of 
the four
 │   ├── privacy-llm/      # Redactor + checker (per RFC-AI-0004 §6)
 │   ├── agent-isolation/  # Bubblewrap sandbox + network allowlist
 │   ├── sandbox-lint/     # Lint settings.json for sandbox correctness
-│   ├── skill-validator/  # Validate SKILL.md frontmatter + links + 
placeholders
+│   ├── skill-and-tool-validator/  # Validate SKILL.md frontmatter + links + 
placeholders
 │   ├── skill-evals/      # Behavioral eval harness for skill steps
 │   ├── dashboard-generator/  # HTML dashboard reference impl (Groovy + Python)
 │   ├── pr-management-stats/  # Maintainer dashboard data layer
@@ -265,7 +265,7 @@ is loaded only after the decision.
 Each skill's frontmatter `description` is the agent-router contract.
 Be precise — vague descriptions cause the router to load the wrong
 skill or miss the right one. The
-[`skill-validator`](tools/skill-validator/README.md) catches the
+[`skill-and-tool-validator`](tools/skill-and-tool-validator/README.md) catches 
the
 common shapes of bad description (action-inventory, distinct-from-
 sibling-skill, chain-handoff narrative).
 
@@ -302,7 +302,7 @@ needs to fill in.
 | Mail | `gmail/`, `ponymail/`, `mail-source/imap/`, `mail-source/mbox/` | 
Gmail (full); PonyMail archive (read-only); IMAP + mbox stubs |
 | CVE workflow | `vulnogram/`, `cve-org/` | ASF Vulnogram (CVE allocation + 
JSON generation); MITRE CVE Services v2 |
 | Runtime / safety | `agent-isolation/`, `privacy-llm/`, `sandbox-lint/` | 
Bubblewrap + network-allowlist sandbox; redactor + checker for privacy-LLM 
gating; settings.json linter |
-| Dev loop | `skill-validator/`, `skill-evals/`, `dev/` | SKILL.md validation; 
behavioral eval harness; local placeholder + pre-commit checkers |
+| Dev loop | `skill-and-tool-validator/`, `skill-evals/`, `dev/` | SKILL.md 
validation; behavioral eval harness; local placeholder + pre-commit checkers |
 | Reporting | `dashboard-generator/`, `pr-management-stats/`, 
`security-tracker-stats-dashboard/` | HTML dashboards for maintainer + security 
review |
 | Authoring | `probe-templates/` | Boilerplate scaffold for sandbox probes |
 
@@ -413,7 +413,7 @@ repo root pins versions across all packages.
 | [`tools/vulnogram/generate-cve-json/`](tools/vulnogram/generate-cve-json/) | 
Emits paste-ready CVE 5.x JSON from a tracker body. Invoked by 
`security-issue-sync` and `security-cve-allocate`. |
 | [`tools/vulnogram/oauth-api/`](tools/vulnogram/oauth-api/) | OAuth helper 
for Vulnogram API authentication. |
 | [`tools/gmail/oauth-draft/`](tools/gmail/oauth-draft/) | Gmail OAuth helper 
for the drafts-only mail-source flow. |
-| [`tools/skill-validator/`](tools/skill-validator/) | Validates `SKILL.md` 
frontmatter, internal links, and placeholder discipline. |
+| [`tools/skill-and-tool-validator/`](tools/skill-and-tool-validator/) | 
Validates `SKILL.md` frontmatter, internal links, and placeholder discipline. |
 | [`tools/skill-evals/`](tools/skill-evals/) | Behavioral eval harness for 
skill steps. Pure-stdlib runner; no third-party deps. |
 | [`tools/sandbox-lint/`](tools/sandbox-lint/) | Lints `settings.json` for 
sandbox-correctness regressions. |
 | [`tools/privacy-llm/checker/`](tools/privacy-llm/checker/) | Verifies that 
data destined for an LLM matches the privacy-LLM policy. |
@@ -733,7 +733,7 @@ points for the two most common authoring tasks:
   The meta-skill walks you through the framework's skill shape
   (frontmatter, resources, placeholder convention, prompt-injection
   defences, privacy-LLM gate-check), scaffolds the directory, and
-  validates the result via [`skill-validator`](tools/skill-validator/).
+  validates the result via 
[`skill-and-tool-validator`](tools/skill-and-tool-validator/).
 - **Modify an existing skill** — open the conversation with the
   skill's `SKILL.md` in context. State what behaviour should change
   and why; the agent will propose the diff plus the eval-case
@@ -803,7 +803,7 @@ Separate GitHub workflows:
 To run a single Python package's tests directly:
 
 ```bash
-cd tools/skill-validator
+cd tools/skill-and-tool-validator
 uv run pytest
 ```
 
@@ -902,7 +902,7 @@ Good entry points, in rough order of ramp-up cost:
    The meta-skill walks you through the framework's skill shape
    (frontmatter, resources, placeholder convention, prompt-injection
    defences, privacy-LLM gate-check) and validates via
-   [`skill-validator`](tools/skill-validator/). The scaffold puts
+   [`skill-and-tool-validator`](tools/skill-and-tool-validator/). The scaffold 
puts
    you in the right shape from the start.
 
 5. **A tool-bridge implementation** (the hardest of these but the
@@ -966,5 +966,5 @@ layer-specific doc wins. Re-read it first:
 - [`tools/<name>/`](tools/) — per-tool adapter contracts.
 - [`tools/skill-evals/README.md`](tools/skill-evals/README.md) —
   the eval harness and fixture format.
-- [`tools/skill-validator/README.md`](tools/skill-validator/README.md) —
+- 
[`tools/skill-and-tool-validator/README.md`](tools/skill-and-tool-validator/README.md)
 —
   the SKILL.md validation contract.
diff --git a/docs/labels-and-capabilities.md b/docs/labels-and-capabilities.md
new file mode 100644
index 0000000..346c7cc
--- /dev/null
+++ b/docs/labels-and-capabilities.md
@@ -0,0 +1,279 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Labels and capabilities](#labels-and-capabilities)
+  - [Label dimensions](#label-dimensions)
+    - [1. `area:*` — subject](#1-area--subject)
+    - [2. `capability:*` — what the tool 
does](#2-capability--what-the-tool-does)
+    - [3. `kind:*` — change type 
(pre-existing)](#3-kind--change-type-pre-existing)
+    - [4. `mode:*` — handling mode 
(pre-existing)](#4-mode--handling-mode-pre-existing)
+    - [Standalone labels](#standalone-labels)
+  - [Capability to skill map](#capability-to-skill-map)
+  - [Capability to tool map](#capability-to-tool-map)
+  - [The rule](#the-rule)
+    - [A GitHub issue](#a-github-issue)
+    - [A pull request](#a-pull-request)
+    - [A new tool under `tools/`](#a-new-tool-under-tools)
+    - [A new skill under `.claude/skills/`](#a-new-skill-under-claudeskills)
+    - [A new doc under `docs/`](#a-new-doc-under-docs)
+  - [Why this exists](#why-this-exists)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# Labels and capabilities
+
+This page is the canonical reference for the label taxonomy used on
+issues and pull requests in this framework repository
+(`apache/airflow-steward`). It also defines the **capability** model
+that classifies what each skill or tool in the framework actually
+*does*, independent of which subject area it sits under.
+
+Every issue and pull request opened against this repository should
+carry at least one **`area:*`** label and at least one
+**`capability:*`** label. New tools and new skills must declare their
+capability up front (see [The rule](#the-rule)).
+
+> **Scope caveat.** This taxonomy applies to *this framework
+> repository*. Skills that create issues or PRs on an **adopter's
+> tracker** (e.g. `security-issue-import`, `security-issue-fix`,
+> `issue-fix-workflow`) use the adopter's own label scheme — adopters
+> are free to mirror this taxonomy in their own repo but are not
+> required to.
+
+---
+
+## Label dimensions
+
+The repository's labels fall into four orthogonal dimensions. An issue
+or PR typically carries one label from each dimension that applies.
+
+### 1. `area:*` — subject
+
+What part of the framework does this touch?
+
+| Label | Covers |
+|---|---|
+| `area:pr-management` | `pr-management-*` skills |
+| `area:security` | `security-*` skills, `security-tracker-stats-dashboard` |
+| `area:setup` | `setup-*` skills, framework adoption, agent-sandbox setup |
+| `area:issue` | `issue-*` skills (`issue-triage`, `issue-fix-workflow`, 
`issue-reassess`, `issue-reassess-stats`, `issue-reproducer`) |
+| `area:tools` | Substrate tools under `tools/*` (CLI bridges, agent-runtime 
adapters, mail-source backends) |
+| `area:ci` | `.github/` workflows, prek, validators |
+| `area:docs` | `docs/`, `MISSION.md`, READMEs |
+
+### 2. `capability:*` — what the tool does
+
+Nine buckets. A tool or skill carries **one or more** `capability:*`
+labels. Most map cleanly to a single bucket; dual-capability cases
+are real and explicitly enumerated below. Issues and PRs follow the
+same rule — apply every capability the change is implementing.
+
+When a skill or tool spans multiple capabilities, list **all** of
+them in its frontmatter / README. Do not pick a single "primary"
+to be neat; that loses information the label system exists to
+surface.
+
+| Label | Definition |
+|---|---|
+| `capability:triage` | Sweep a queue, classify candidates, propose 
dispositions for human confirmation. |
+| `capability:review` | Deep per-item code review of a PR or local diff; also 
contributor mentoring (single-item teaching intervention). |
+| `capability:fix` | Implement a code change against an upstream repo to 
resolve a triaged issue. |
+| `capability:intake` | Import external signal (mailing list, scan report, 
public PR) into a tracker entry, or keep an existing entry reconciled with one 
of those sources. |
+| `capability:reconciliation` | Compare tracker state against an external 
inventory (e.g. ASF security dashboard, organization-wide issue registry); 
surface drift; propose corrections. Does **not** write to either source. |
+| `capability:resolve` | Close-out actions: invalidate, dedupe, CVE-allocate, 
post-announcement housekeeping. |
+| `capability:reassess` | Re-run resolved or end-of-life issues against 
current code to verify still-fixed / still-broken. |
+| `capability:stats` | Read-only dashboards, metrics, governance evidence, 
contributor nomination briefs. |
+| `capability:setup` | Framework / agent / substrate infrastructure: install, 
verify, update, doctor, override-upstream, write-skill, plus new tools under 
`tools/*`. |
+
+The `capability:*` dimension is **orthogonal** to `area:*`. A single
+query can answer "how is our triage stack doing across PR + issue +
+security?" by filtering on `capability:triage` alone, without
+enumerating per-area queries.
+
+### 3. `kind:*` — change type (pre-existing)
+
+| Label | Covers |
+|---|---|
+| `kind:dx` | Maintainer dev-loop / CLI UX |
+| `kind:policy` | Rule changes (eligibility, thresholds, behaviour switches) |
+| `kind:perf` | Token / latency / API-call budget |
+| `kind:adopter-config` | Per-adopter knob |
+
+### 4. `mode:*` — handling mode (pre-existing)
+
+| Label | Covers |
+|---|---|
+| `mode:A` | Mode A — triage |
+| `mode:B` | Mode B — mentoring |
+| `mode:C` | Mode C — agent-authored fix with human review |
+| `mode:D` | Mode D — narrowly-scoped auto-merge (off until A/B/C run 2 
quarters) |
+| `mode:cross-cutting` | Spans multiple modes |
+| `mode:platform` | Substrate / infra — not a mode (sandbox, CI, validators) |
+
+### Standalone labels
+
+`marketing` (branding artefacts), `dependencies` (dependency-update
+PRs), `python:uv` (Python uv-managed code), plus the default GitHub
+labels (`bug`, `enhancement`, `documentation`, `good first issue`,
+etc.).
+
+---
+
+## Capability to skill map
+
+Capabilities for every skill currently in
+[`.claude/skills/`](../.claude/skills/). Skills with two values
+(separated by `+`) carry both labels.
+
+| Skill | Capability / capabilities |
+|---|---|
+| `pr-management-triage` | `capability:triage` |
+| `issue-triage` | `capability:triage` |
+| `security-issue-triage` | `capability:triage` |
+| `pr-management-code-review` | `capability:review` |
+| `pairing-self-review` | `capability:review` |
+| `pr-management-mentor` | `capability:review` |
+| `issue-fix-workflow` | `capability:fix` |
+| `security-issue-fix` | `capability:fix` + `capability:resolve` *(opens the 
PR that closes the tracker — both phases)* |
+| `security-issue-import` | `capability:intake` |
+| `security-issue-import-from-md` | `capability:intake` |
+| `security-issue-import-from-pr` | `capability:intake` |
+| `security-issue-sync` | `capability:intake` *(+ `capability:reconciliation` 
once [#337](https://github.com/apache/airflow-steward/issues/337) lands the 
ASF-dashboard step)* |
+| `setup-shared-config-sync` | `capability:intake` + `capability:setup` 
*(reconciles user-scope config to a sync repo; the act is intake, the subject 
is setup)* |
+| `security-cve-allocate` | `capability:resolve` |
+| `security-issue-invalidate` | `capability:resolve` |
+| `security-issue-deduplicate` | `capability:resolve` |
+| `issue-reassess` | `capability:reassess` |
+| `issue-reproducer` | `capability:reassess` |
+| `pr-management-stats` | `capability:stats` |
+| `issue-reassess-stats` | `capability:stats` |
+| `security-tracker-stats-dashboard` | `capability:stats` |
+| `contributor-nomination` | `capability:stats` |
+| `list-steward-skills` | `capability:stats` |
+| `setup-steward` | `capability:setup` |
+| `setup-isolated-setup-install` | `capability:setup` |
+| `setup-isolated-setup-verify` | `capability:setup` |
+| `setup-isolated-setup-update` | `capability:setup` |
+| `setup-isolated-setup-doctor` | `capability:setup` + `capability:reassess` 
*(re-checks an installed sandbox against current spec — the phase is reassess 
on subject setup)* |
+| `setup-override-upstream` | `capability:setup` |
+| `write-skill` | `capability:setup` |
+
+## Capability to tool map
+
+Tools under [`tools/`](../tools/). Tools with two values (separated by
+`+`) carry both labels — the dual role is explained in each row.
+
+| Tool | Capability / capabilities | Role |
+|---|---|---|
+| [`tools/agent-isolation`](../tools/agent-isolation/) | `capability:setup` | 
Secure-agent sandbox helpers |
+| [`tools/cve-org`](../tools/cve-org/) | `capability:resolve` + 
`capability:intake` | Publishes to CVE.org *(resolve)* and records the 
resulting CVE state back into the tracker *(intake)* |
+| [`tools/dashboard-generator`](../tools/dashboard-generator/) | 
`capability:stats` | Self-contained HTML dashboard generator |
+| [`tools/dev`](../tools/dev/) | `capability:setup` | Framework dev-loop 
helpers |
+| [`tools/github`](../tools/github/) | `capability:setup` | GitHub REST / 
GraphQL substrate (called by every lifecycle phase — pure substrate, no single 
phase) |
+| [`tools/gmail`](../tools/gmail/) | `capability:setup` | Gmail API substrate |
+| [`tools/jira`](../tools/jira/) | `capability:setup` | JIRA REST substrate 
(read-only today; write subcommands tracked in 
[#301](https://github.com/apache/airflow-steward/issues/301)) |
+| [`tools/mail-source`](../tools/mail-source/) | `capability:setup` + 
`capability:intake` | Mail-source backend abstraction (mbox / IMAP / Mailman 
3); the abstraction is setup, every concrete read is part of the intake 
pipeline |
+| [`tools/ponymail`](../tools/ponymail/) | `capability:setup` + 
`capability:intake` | PonyMail archive substrate; same dual role as 
`mail-source` — substrate plus an intake-pipeline component |
+| [`tools/pr-management-stats`](../tools/pr-management-stats/) | 
`capability:stats` | PR-backlog analytics engine |
+| [`tools/privacy-llm`](../tools/privacy-llm/) | `capability:setup` | 
Privacy-LLM PII-scrubbing gate |
+| [`tools/probe-templates`](../tools/probe-templates/) | `capability:setup` | 
Sandbox-doctor probe templates |
+| [`tools/sandbox-lint`](../tools/sandbox-lint/) | `capability:setup` | 
Sandbox settings linter |
+| 
[`tools/security-tracker-stats-dashboard`](../tools/security-tracker-stats-dashboard/)
 | `capability:stats` | Security-tracker analytics engine |
+| [`tools/spec-loop`](../tools/spec-loop/) | `capability:setup` | Spec-driven 
build loop runner (Ralph-style) for framework development |
+| [`tools/skill-evals`](../tools/skill-evals/) | `capability:setup` + 
`capability:stats` | Eval harness for skills; the harness is setup 
infrastructure, the run output is governance evidence |
+| [`tools/skill-and-tool-validator`](../tools/skill-and-tool-validator/) | 
`capability:setup` | Skill-frontmatter and convention validator |
+| [`tools/spec-status-index`](../tools/spec-status-index/) | 
`capability:setup` + `capability:stats` | Index of spec / RFC implementation 
status — substrate that also doubles as a governance/stats view |
+| [`tools/vulnogram`](../tools/vulnogram/) | `capability:resolve` | ASF 
Vulnogram CVE-allocation client |
+
+A tool's capabilities are determined by its **use-case lifecycle
+phases**, not by which skills happen to consume it. `tools/github` is
+called by every triage / intake / fix / resolve skill but is tagged
+only `capability:setup` because it doesn't encode any one lifecycle
+phase — it is pure substrate. `tools/cve-org`, by contrast, exists
+specifically to *do* CVE publication and to record that result; both
+the resolve action and the intake of state into the tracker are
+first-class jobs of the tool, so it carries both labels.
+
+When a tool grows to serve a new lifecycle phase as a first-class
+feature (rather than as generic substrate that other skills happen
+to compose), add the new `capability:*` label to its README and to
+the table above.
+
+---
+
+## The rule
+
+When you create any of the following on this repository, declare the
+capability:
+
+### A GitHub issue
+
+Apply at least one `area:*` AND one `capability:*` label. If the issue
+genuinely spans capabilities, apply both — for example,
+[#337](https://github.com/apache/airflow-steward/issues/337) carries
+both `capability:reconciliation` and `capability:setup` because it
+covers a new substrate tool *and* a new sync-flow integration.
+
+### A pull request
+
+Same: `area:*` AND `capability:*`. Match the capability the change is
+*implementing*, not the file paths it happens to touch. A PR that
+adjusts the validator config to support a new triage rule is
+`capability:triage` (the change's purpose), not `capability:setup`
+(the file it edited).
+
+### A new tool under `tools/`
+
+Declare the tool's capability in the **first paragraph of its README**
+using the line:
+
+```markdown
+**Capability:** capability:NAME
+```
+
+If the tool serves more than one capability, list both. Substrate
+bridges (`tools/github`, `tools/gmail`, …) default to
+`capability:setup` unless they encode a specific lifecycle capability.
+
+### A new skill under `.claude/skills/`
+
+Declare the capability in the skill's frontmatter:
+
+```yaml
+---
+name: my-new-skill
+description: |
+  ...
+capability: capability:NAME
+---
+```
+
+The [`write-skill`](../.claude/skills/write-skill/SKILL.md) skill
+prompts for this on every new-skill scaffold.
+
+### A new doc under `docs/`
+
+Capability-specific docs (e.g. a guide for a single skill family)
+should link to this page and name the capability in their first
+paragraph. Cross-cutting docs (`MISSION.md`, top-level READMEs) need
+no capability marker.
+
+---
+
+## Why this exists
+
+The original `area:*` labels split issues by subject — useful for
+"what part of the codebase is this?" but unable to answer "what kind
+of thing is this?". The `capability:*` dimension fills that gap and
+is orthogonal: a triage-rule change in PR management
+(`area:pr-management` + `capability:triage`) and a triage-rule change
+in security (`area:security` + `capability:triage`) become trivially
+findable as a cohort even though they live in different families.
+
+Capability is also a forcing function for skill design: if a new skill
+doesn't fit any of the nine buckets cleanly, that's a signal worth
+inspecting before the skill ships.
diff --git a/tools/agent-isolation/README.md b/tools/agent-isolation/README.md
index b6e93f1..7e74550 100644
--- a/tools/agent-isolation/README.md
+++ b/tools/agent-isolation/README.md
@@ -14,6 +14,8 @@
 
 # `tools/agent-isolation/` — secure agent setup helpers
 
+**Capability:** capability:setup
+
 This directory ships the moving pieces the framework's
 [`docs/setup/secure-agent-setup.md`](../../docs/setup/secure-agent-setup.md) 
document
 references. It is not a Python project (unlike the sibling tools
diff --git a/tools/cve-org/README.md b/tools/cve-org/README.md
new file mode 100644
index 0000000..37a0e4c
--- /dev/null
+++ b/tools/cve-org/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/cve-org/`](#toolscve-org)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/cve-org/`
+
+**Capability:** capability:resolve + capability:intake
+
+CVE.org publication client. Submits CVE records via the CVE.org REST API; 
consumed by `security-cve-allocate` once a CVE has been allocated via the ASF 
Vulnogram path. See [`tool.md`](tool.md) for the protocol detail and `cve.org` 
field mapping.
diff --git a/tools/dashboard-generator/README.md 
b/tools/dashboard-generator/README.md
index c740a5f..20e8a15 100644
--- a/tools/dashboard-generator/README.md
+++ b/tools/dashboard-generator/README.md
@@ -17,6 +17,8 @@
 
 # Dashboard generator
 
+**Capability:** capability:stats
+
 Deterministic reference implementations of the dashboard that
 [`issue-reassess-stats`](../../.claude/skills/issue-reassess-stats/SKILL.md)
 produces. Adopters who want CI-rendered dashboards (refreshed on
diff --git a/tools/dev/README.md b/tools/dev/README.md
new file mode 100644
index 0000000..f0574f4
--- /dev/null
+++ b/tools/dev/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/dev/`](#toolsdev)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/dev/`
+
+**Capability:** capability:setup
+
+Framework dev-loop helpers (placeholder check, agent pre-commit hook). Invoked 
by prek and CI; not consumed by any skill directly. See the individual scripts 
in this directory for usage.
diff --git a/tools/github/README.md b/tools/github/README.md
new file mode 100644
index 0000000..478e647
--- /dev/null
+++ b/tools/github/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/github/`](#toolsgithub)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/github/`
+
+**Capability:** capability:setup
+
+GitHub REST + GraphQL substrate. Pure read/write wrapper used by every 
lifecycle phase (triage / intake / fix / resolve / stats). See 
[`tool.md`](tool.md) for the operation catalogue and the per-area files 
([`issue-template.md`](issue-template.md), [`labels.md`](labels.md), 
[`operations.md`](operations.md), [`project-board.md`](project-board.md), 
[`status-rollup.md`](status-rollup.md)) for specifics.
diff --git a/tools/gmail/README.md b/tools/gmail/README.md
new file mode 100644
index 0000000..73af9ba
--- /dev/null
+++ b/tools/gmail/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/gmail/`](#toolsgmail)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/gmail/`
+
+**Capability:** capability:setup
+
+Gmail API substrate. Read + draft-only — never sends. Used by the 
security-issue-import / sync / invalidate flows for inbound report intake and 
outbound courtesy-reply drafting. See [`tool.md`](tool.md) for the operation 
catalogue and the per-area files for ASF relay routing, draft backends, 
threading, search queries.
diff --git a/tools/jira/README.md b/tools/jira/README.md
index 89e8b78..98955e8 100644
--- a/tools/jira/README.md
+++ b/tools/jira/README.md
@@ -20,6 +20,8 @@
 
 # JIRA bridge
 
+**Capability:** capability:setup
+
 Read-only JIRA REST helpers for the `issue-*` skill family.
 Adopters with JIRA-based issue trackers wire this in as their
 tracker bridge; adopters using GitHub Issues or other trackers
diff --git a/tools/mail-source/README.md b/tools/mail-source/README.md
new file mode 100644
index 0000000..8aa915a
--- /dev/null
+++ b/tools/mail-source/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/mail-source/`](#toolsmail-source)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/mail-source/`
+
+**Capability:** capability:setup + capability:intake
+
+Mail-source backend abstraction. Pluggable backends (mbox, IMAP, future 
Mailman 3 / Hyperkitty) that feed the security-issue-import intake pipeline a 
uniform thread/message view. See [`contract.md`](contract.md) for the backend 
interface.
diff --git a/tools/ponymail/README.md b/tools/ponymail/README.md
new file mode 100644
index 0000000..7617de5
--- /dev/null
+++ b/tools/ponymail/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/ponymail/`](#toolsponymail)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/ponymail/`
+
+**Capability:** capability:setup + capability:intake
+
+PonyMail archive substrate. Read-only ASF mailing-list archive client; 
complements `gmail` for threads not present in the inbox. Used by 
security-issue-import + sync to cross-reference public mailing-list 
discussions. See [`tool.md`](tool.md) for the operation catalogue and 
[`operations.md`](operations.md) for usage.
diff --git a/tools/pr-management-stats/README.md 
b/tools/pr-management-stats/README.md
index cab47f5..017aea9 100644
--- a/tools/pr-management-stats/README.md
+++ b/tools/pr-management-stats/README.md
@@ -16,6 +16,8 @@
 
 # pr-management-stats reference implementation
 
+**Capability:** capability:stats
+
 Deterministic reference implementation of the data-fetch +
 classification contract that backs the
 [`pr-management-stats`](../../.claude/skills/pr-management-stats/SKILL.md) 
skill.
diff --git a/tools/privacy-llm/README.md b/tools/privacy-llm/README.md
new file mode 100644
index 0000000..1443451
--- /dev/null
+++ b/tools/privacy-llm/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/privacy-llm/`](#toolsprivacy-llm)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/privacy-llm/`
+
+**Capability:** capability:setup
+
+Privacy-LLM PII-scrubbing gate. Standalone redactor / checker pair that 
screens content for PII before it reaches an external LLM. See 
[`tool.md`](tool.md) and [`wiring.md`](wiring.md) for integration details, 
[`models.md`](models.md) for the model catalogue, and [`pii.md`](pii.md) for 
the PII taxonomy.
diff --git a/tools/probe-templates/README.md b/tools/probe-templates/README.md
index 4a65904..9020761 100644
--- a/tools/probe-templates/README.md
+++ b/tools/probe-templates/README.md
@@ -15,6 +15,8 @@
 
 # Probe templates
 
+**Capability:** capability:setup
+
 Runnable cross-family probe scripts that the
 [`issue-reproducer`](../../.claude/skills/issue-reproducer/SKILL.md)
 skill copies from when its Step 9 (optional cross-family probe)
diff --git a/tools/sandbox-lint/README.md b/tools/sandbox-lint/README.md
index 456f95c..7030036 100644
--- a/tools/sandbox-lint/README.md
+++ b/tools/sandbox-lint/README.md
@@ -16,6 +16,8 @@
 
 # `sandbox-lint`
 
+**Capability:** capability:setup
+
 Lints `.claude/settings.json` against the shipped baseline at
 `tools/sandbox-lint/expected.json`, and against the security
 invariants documented in `docs/security/threat-model.md`
diff --git a/tools/security-tracker-stats-dashboard/README.md 
b/tools/security-tracker-stats-dashboard/README.md
index 06dcced..8afd938 100644
--- a/tools/security-tracker-stats-dashboard/README.md
+++ b/tools/security-tracker-stats-dashboard/README.md
@@ -21,6 +21,8 @@
 
 # security-tracker-stats-dashboard
 
+**Capability:** capability:stats
+
 Generate a self-contained HTML dashboard of `<tracker>` repository
 statistics — issue-lifecycle bands (untriaged / triaged / PR-merged /
 fixed-released / closed-other), opened-vs-untriaged backlog, cumulative
diff --git a/tools/skill-validator/README.md 
b/tools/skill-and-tool-validator/README.md
similarity index 91%
rename from tools/skill-validator/README.md
rename to tools/skill-and-tool-validator/README.md
index 42c3677..4df62d2 100644
--- a/tools/skill-validator/README.md
+++ b/tools/skill-and-tool-validator/README.md
@@ -2,7 +2,7 @@
 <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
 **Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
 
-- [skill-validator](#skill-validator)
+- [skill-and-tool-validator](#skill-and-tool-validator)
   - [What it checks](#what-it-checks)
     - [Hard rules (failure)](#hard-rules-failure)
     - [SOFT advisories (warning, do not 
fail)](#soft-advisories-warning-do-not-fail)
@@ -14,7 +14,9 @@
 <!-- SPDX-License-Identifier: Apache-2.0
      https://www.apache.org/licenses/LICENSE-2.0 -->
 
-# skill-validator
+# skill-and-tool-validator
+
+**Capability:** capability:setup
 
 Validate framework skill definitions — YAML frontmatter, internal
 link integrity, and placeholder conventions.
@@ -54,13 +56,13 @@ the run. The reviewer has the final say on borderline cases.
 From the repo root:
 
 ```bash
-uv run --project tools/skill-validator --group dev pytest
+uv run --project tools/skill-and-tool-validator --group dev pytest
 ```
 
 Or install and run as CLI:
 
 ```bash
-uv run --project tools/skill-validator --group dev skill-validate
+uv run --project tools/skill-and-tool-validator --group dev 
skill-and-tool-validate
 ```
 
 CLI flags:
diff --git a/tools/skill-validator/pyproject.toml 
b/tools/skill-and-tool-validator/pyproject.toml
similarity index 93%
rename from tools/skill-validator/pyproject.toml
rename to tools/skill-and-tool-validator/pyproject.toml
index e1ad1a9..58072c8 100644
--- a/tools/skill-validator/pyproject.toml
+++ b/tools/skill-and-tool-validator/pyproject.toml
@@ -20,7 +20,7 @@ requires = ["hatchling"]
 build-backend = "hatchling.build"
 
 [project]
-name = "skill-validator"
+name = "skill-and-tool-validator"
 version = "0.1.0"
 description = "Validate framework skill definitions — YAML frontmatter, 
internal link integrity, and placeholder conventions."
 readme = "README.md"
@@ -31,7 +31,7 @@ license = { text = "Apache-2.0" }
 dependencies = []
 
 [project.scripts]
-skill-validate = "skill_validator:main"
+skill-and-tool-validate = "skill_and_tool_validator:main"
 
 [dependency-groups]
 dev = [
@@ -41,7 +41,7 @@ dev = [
 ]
 
 [tool.hatch.build.targets.wheel]
-packages = ["src/skill_validator"]
+packages = ["src/skill_and_tool_validator"]
 
 [tool.ruff]
 line-length = 110
diff --git a/tools/skill-validator/src/skill_validator/__init__.py 
b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py
similarity index 79%
rename from tools/skill-validator/src/skill_validator/__init__.py
rename to 
tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py
index 8b33723..802a63f 100644
--- a/tools/skill-validator/src/skill_validator/__init__.py
+++ b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py
@@ -44,9 +44,9 @@ SOFT categories surface as advisory warnings (stderr) without
 failing the run unless ``--strict`` is passed.
 
 Run from repo root:
-    uv run --project tools/skill-validator --group dev pytest
+    uv run --project tools/skill-and-tool-validator --group dev pytest
     # or after install:
-    skill-validate
+    skill-and-tool-validate
 """
 
 from __future__ import annotations
@@ -62,13 +62,53 @@ from pathlib import Path
 # ---------------------------------------------------------------------------
 
 SKILLS_DIR = Path(".claude/skills")
+TOOLS_DIR = Path("tools")
 DOCS_DIR = Path("docs")
 PROJECTS_TEMPLATE_DIR = Path("projects/_template")
 
-REQUIRED_FRONTMATTER_KEYS = {"name", "description", "license"}
+# Categories for the tool-validator block. Both HARD by default — every
+# tool must have a README that declares its capability.
+TOOL_README_CATEGORY = "tool-readme"
+TOOL_CAPABILITY_CATEGORY = "tool-capability"
+
+# Matches `**Capability:** capability:NAME` (and multi-value
+# `capability:NAME + capability:NAME + …`) on a single line.
+TOOL_CAPABILITY_RE = re.compile(r"^\*\*Capability:\*\*[ \t]+(.+)$", 
re.MULTILINE)
+
+# Capability-sync check: keeps docs/labels-and-capabilities.md tables aligned
+# with live skill frontmatter + tool README declarations.
+DOCS_LABELS_AND_CAPABILITIES = Path("docs/labels-and-capabilities.md")
+CAPABILITY_SYNC_CATEGORY = "capability-sync"
+_SKILL_TABLE_HEADER = "## Capability to skill map"
+_TOOL_TABLE_HEADER = "## Capability to tool map"
+# Tokens like `capability:setup`. Optional backticks around the token.
+_CAPABILITY_TOKEN_RE = re.compile(r"`?(capability:[a-z]+)`?")
+# Italic-parenthetical annotation in the docs tables: `*( … )*` — used for
+# future-state notes (e.g. "*(+ capability:reconciliation once #337 lands)*").
+# Stripped before extracting authoritative capability tokens. The terminator
+# is the literal sequence ``)*`` (close-paren immediately followed by an
+# asterisk), which lets the body span markdown links whose URLs contain
+# parens.
+_ITALIC_PARENS_RE = re.compile(r"\*\(.*?\)\*")
+
+REQUIRED_FRONTMATTER_KEYS = {"name", "description", "license", "capability"}
 OPTIONAL_FRONTMATTER_KEYS = {"when_to_use", "mode"}
 ALLOWED_LICENSES = {"Apache-2.0"}
 
+# Canonical capability taxonomy — docs/labels-and-capabilities.md is 
authoritative.
+# Skills may declare a single capability (string form) or several (YAML list 
form).
+ALLOWED_CAPABILITIES = {
+    "capability:triage",
+    "capability:review",
+    "capability:fix",
+    "capability:intake",
+    "capability:reconciliation",
+    "capability:resolve",
+    "capability:reassess",
+    "capability:stats",
+    "capability:setup",
+}
+
 
 def _read_mode_table() -> dict[str, str]:
     """Read the canonical MISSION mode table from ``docs/modes.md``."""
@@ -454,6 +494,31 @@ def validate_frontmatter(path: Path, text: str) -> 
Iterable[Violation]:
             f"frontmatter mode '{fm['mode']}' not in {sorted(ALLOWED_MODES)} 
(see docs/modes.md)",
         )
 
+    if fm.get("capability"):
+        # The frontmatter parser stores both forms as a single string:
+        #   single — `capability: capability:triage`            → 
"capability:triage"
+        #   list   — `capability:\n  - capability:intake\n …`   → "- 
capability:intake\n- capability:setup"
+        # Split on lines, strip `- ` prefix when present.
+        entries: list[str] = []
+        for raw_line in fm["capability"].splitlines():
+            line = raw_line.strip()
+            if not line:
+                continue
+            if line.startswith("- "):
+                entries.append(line[2:].strip())
+            else:
+                entries.append(line)
+        if not entries:
+            yield Violation(path, 1, "frontmatter key 'capability' is empty")
+        for entry in entries:
+            if entry not in ALLOWED_CAPABILITIES:
+                yield Violation(
+                    path,
+                    1,
+                    f"frontmatter capability '{entry}' not in 
{sorted(ALLOWED_CAPABILITIES)} "
+                    f"(see docs/labels-and-capabilities.md)",
+                )
+
     desc_len = len(fm.get("description", ""))
     wtu_len = len(fm.get("when_to_use", ""))
     total = desc_len + wtu_len
@@ -1148,6 +1213,267 @@ def collect_files_to_check(root: Path | None = None) -> 
list[Path]:
     return list(base.rglob("*.md"))
 
 
+def collect_tool_dirs(root: Path | None = None) -> list[Path]:
+    """Return every immediate sub-directory under tools/ that should be 
checked."""
+    base = (root or find_repo_root()) / TOOLS_DIR
+    if not base.exists():
+        return []
+    return sorted(d for d in base.iterdir() if d.is_dir() and not 
d.name.startswith("."))
+
+
+def validate_tools(root: Path | None = None) -> Iterable[Violation]:
+    """For each ``tools/<name>/`` directory, require:
+
+    1. A ``README.md`` to exist at the tool root.
+    2. The README to contain a ``**Capability:** capability:NAME`` line,
+       with NAME drawn from ``ALLOWED_CAPABILITIES``. Multi-value form is
+       ``**Capability:** capability:NAME + capability:NAME``.
+
+    Both are HARD checks — every tool must declare its capabilities so
+    the per-tool map in ``docs/labels-and-capabilities.md`` stays
+    authoritative.
+    """
+    for tool_dir in collect_tool_dirs(root):
+        readme = tool_dir / "README.md"
+        if not readme.exists():
+            yield Violation(
+                readme,
+                None,
+                f"tool '{tool_dir.name}' missing README.md — every 
tools/<name>/ must "
+                f"have a README declaring its capability per "
+                f"docs/labels-and-capabilities.md",
+                category=TOOL_README_CATEGORY,
+            )
+            continue
+
+        try:
+            text = readme.read_text(encoding="utf-8")
+        except OSError as exc:
+            yield Violation(readme, None, f"cannot read README.md: {exc}")
+            continue
+
+        match = TOOL_CAPABILITY_RE.search(text)
+        if match is None:
+            yield Violation(
+                readme,
+                1,
+                f"tool '{tool_dir.name}' README missing '**Capability:** 
capability:NAME' "
+                f"declaration (see docs/labels-and-capabilities.md)",
+                category=TOOL_CAPABILITY_CATEGORY,
+            )
+            continue
+
+        line_no = text[: match.start()].count("\n") + 1
+        # Split multi-value: `capability:NAME + capability:NAME + …`
+        raw = match.group(1).strip()
+        entries = [e.strip() for e in raw.split("+") if e.strip()]
+        if not entries:
+            yield Violation(
+                readme,
+                line_no,
+                f"tool '{tool_dir.name}' has '**Capability:**' line but no 
values parsed",
+                category=TOOL_CAPABILITY_CATEGORY,
+            )
+            continue
+        for entry in entries:
+            if entry not in ALLOWED_CAPABILITIES:
+                yield Violation(
+                    readme,
+                    line_no,
+                    f"tool '{tool_dir.name}' capability '{entry}' not in "
+                    f"{sorted(ALLOWED_CAPABILITIES)} (see 
docs/labels-and-capabilities.md)",
+                    category=TOOL_CAPABILITY_CATEGORY,
+                )
+
+
+def _parse_capability_doc_table(text: str, header: str) -> dict[str, set[str]]:
+    """Parse a markdown table rooted at *header* in labels-and-capabilities.md.
+
+    Returns a {entity-name: {capability:foo, capability:bar}} mapping. The
+    entity name is the first cell's bare identifier (drops the path prefix
+    for tools: ``tools/foo`` → ``foo``). Italic-parenthetical annotations
+    in the capability cell (``*(+ capability:X once #N lands)*``) are
+    stripped before parsing — they are future-state notes, not the
+    authoritative declaration.
+    """
+    if header not in text:
+        return {}
+    section = text.split(header, 1)[1]
+    next_h2 = section.find("\n## ")
+    if next_h2 > 0:
+        section = section[:next_h2]
+
+    out: dict[str, set[str]] = {}
+    for line in section.splitlines():
+        if not line.startswith("|"):
+            continue
+        # Skip the header / separator rows.
+        if line.startswith("|---") or line.startswith("| --- "):
+            continue
+        if "Capability" in line and ("Skill" in line or "Tool" in line or 
"skill" in line or "tool" in line):
+            continue
+        cells = [c.strip() for c in line.strip("|").split("|")]
+        if len(cells) < 2:
+            continue
+        name_cell, cap_cell = cells[0], cells[1]
+        # Entity name: `name` or [`name`](path) — pull the backtick-quoted 
token.
+        name_match = re.search(r"`([a-zA-Z0-9/_-]+)`", name_cell)
+        if not name_match:
+            continue
+        raw_name = name_match.group(1)
+        # Tools live under `tools/<name>` in the table; strip prefix.
+        name = raw_name.rsplit("/", 1)[-1]
+        # Strip italic-parenthetical future-state notes before token 
extraction.
+        cap_cell_clean = _ITALIC_PARENS_RE.sub("", cap_cell)
+        caps = set(_CAPABILITY_TOKEN_RE.findall(cap_cell_clean))
+        if caps:
+            out[name] = caps
+    return out
+
+
+def _live_skill_capabilities(repo_root: Path) -> dict[str, set[str]]:
+    """Read the {skill-name: {capability:foo, …}} mapping from live 
frontmatter."""
+    out: dict[str, set[str]] = {}
+    skills_dir = repo_root / SKILLS_DIR
+    if not skills_dir.exists():
+        return out
+    for skill_md in skills_dir.glob("*/SKILL.md"):
+        try:
+            text = skill_md.read_text(encoding="utf-8")
+        except OSError:
+            continue
+        fm = parse_frontmatter(text)
+        if fm is None or "capability" not in fm or not fm["capability"]:
+            continue
+        entries: set[str] = set()
+        for raw_line in fm["capability"].splitlines():
+            line = raw_line.strip()
+            if not line:
+                continue
+            if line.startswith("- "):
+                entries.add(line[2:].strip())
+            else:
+                entries.add(line)
+        if entries:
+            out[skill_md.parent.name] = entries
+    return out
+
+
+def _live_tool_capabilities(repo_root: Path) -> dict[str, set[str]]:
+    """Read the {tool-name: {capability:foo, …}} mapping from live tool 
READMEs."""
+    out: dict[str, set[str]] = {}
+    for tool_dir in collect_tool_dirs(repo_root):
+        readme = tool_dir / "README.md"
+        if not readme.exists():
+            continue
+        try:
+            text = readme.read_text(encoding="utf-8")
+        except OSError:
+            continue
+        match = TOOL_CAPABILITY_RE.search(text)
+        if match is None:
+            continue
+        raw = match.group(1).strip()
+        entries = {e.strip() for e in raw.split("+") if e.strip()}
+        if entries:
+            out[tool_dir.name] = entries
+    return out
+
+
+def validate_capability_sync(root: Path | None = None) -> Iterable[Violation]:
+    """Compare the docs/labels-and-capabilities.md tables against live state.
+
+    Both directions are checked:
+
+    - Every row in either table must correspond to a live skill / tool with
+      the same capability set (modulo italic-parenthetical future-state notes).
+    - Every live skill (with a ``capability:`` frontmatter field) and every
+      live tool (with a ``**Capability:**`` README declaration) must have a
+      matching row in the corresponding doc table.
+
+    Drift in either direction is a HARD ``capability-sync`` violation —
+    the docs are the canonical reference and must stay aligned with the
+    source.
+    """
+    repo_root = root or find_repo_root()
+    doc_path = repo_root / DOCS_LABELS_AND_CAPABILITIES
+    if not doc_path.exists():
+        yield Violation(
+            doc_path,
+            None,
+            "docs/labels-and-capabilities.md missing — cannot run 
capability-sync check",
+            category=CAPABILITY_SYNC_CATEGORY,
+        )
+        return
+
+    try:
+        doc_text = doc_path.read_text(encoding="utf-8")
+    except OSError as exc:
+        yield Violation(doc_path, None, f"cannot read 
labels-and-capabilities.md: {exc}")
+        return
+
+    doc_skills = _parse_capability_doc_table(doc_text, _SKILL_TABLE_HEADER)
+    doc_tools = _parse_capability_doc_table(doc_text, _TOOL_TABLE_HEADER)
+    live_skills = _live_skill_capabilities(repo_root)
+    live_tools = _live_tool_capabilities(repo_root)
+
+    # Skills — docs vs live, both directions.
+    for name, doc_caps in sorted(doc_skills.items()):
+        if name not in live_skills:
+            yield Violation(
+                doc_path,
+                None,
+                f"skill table row for '{name}' but no live SKILL.md with a 
'capability:' field "
+                f"found under .claude/skills/{name}/",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+            continue
+        if doc_caps != live_skills[name]:
+            yield Violation(
+                doc_path,
+                None,
+                f"skill '{name}' capability mismatch — docs={sorted(doc_caps)} 
live={sorted(live_skills[name])}",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+    for name in sorted(live_skills):
+        if name not in doc_skills:
+            yield Violation(
+                doc_path,
+                None,
+                f"live skill '{name}' has 'capability:' frontmatter but no row 
in the skill table "
+                f"in docs/labels-and-capabilities.md",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+
+    # Tools — docs vs live, both directions.
+    for name, doc_caps in sorted(doc_tools.items()):
+        if name not in live_tools:
+            yield Violation(
+                doc_path,
+                None,
+                f"tool table row for '{name}' but no live 
tools/{name}/README.md with a "
+                f"'**Capability:**' declaration found",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+            continue
+        if doc_caps != live_tools[name]:
+            yield Violation(
+                doc_path,
+                None,
+                f"tool '{name}' capability mismatch — docs={sorted(doc_caps)} 
live={sorted(live_tools[name])}",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+    for name in sorted(live_tools):
+        if name not in doc_tools:
+            yield Violation(
+                doc_path,
+                None,
+                f"live tool '{name}' has '**Capability:**' declaration but no 
row in the tool table "
+                f"in docs/labels-and-capabilities.md",
+                category=CAPABILITY_SYNC_CATEGORY,
+            )
+
+
 # ---------------------------------------------------------------------------
 # Lowercase -f field check (Pattern 2)
 # ---------------------------------------------------------------------------
@@ -1298,6 +1624,12 @@ def run_validation(root: Path | None = None) -> 
list[Violation]:
         violations.extend(validate_gh_list_limit(path, text))
         violations.extend(validate_lowercase_f_field(path, text))
 
+    # Tool-level checks: every tools/<name>/ has a README that declares its 
capability.
+    violations.extend(validate_tools(repo_root))
+
+    # Capability-sync check: the doc tables and the source must agree.
+    violations.extend(validate_capability_sync(repo_root))
+
     return violations
 
 
@@ -1330,14 +1662,14 @@ def main(argv: list[str] | None = None) -> int:
         soft = [v for v in filtered if v.category in SOFT_CATEGORIES]
 
     if not filtered:
-        print("skill-validator: OK (no violations)")
+        print("skill-and-tool-validator: OK (no violations)")
         return 0
 
     if soft:
         _print_soft_warnings(soft)
 
     if hard:
-        print(f"skill-validator: {len(hard)} violation(s) found\n")
+        print(f"skill-and-tool-validator: {len(hard)} violation(s) found\n")
         for v in hard:
             print(v)
         return 1
@@ -1383,7 +1715,7 @@ def _print_soft_warnings(soft: list[Violation]) -> None:
         by_file[v.path].append(v)
 
     print(
-        f"skill-validator: {len(soft)} SOFT warning(s) across "
+        f"skill-and-tool-validator: {len(soft)} SOFT warning(s) across "
         f"{len(by_file)} skill(s) — advisory, not blocking\n",
         file=sys.stderr,
     )
diff --git a/tools/skill-validator/tests/test_validator.py 
b/tools/skill-and-tool-validator/tests/test_validator.py
similarity index 83%
rename from tools/skill-validator/tests/test_validator.py
rename to tools/skill-and-tool-validator/tests/test_validator.py
index 0399230..4998f41 100644
--- a/tools/skill-validator/tests/test_validator.py
+++ b/tools/skill-and-tool-validator/tests/test_validator.py
@@ -23,7 +23,7 @@ from pathlib import Path
 
 import pytest
 
-from skill_validator import (
+from skill_and_tool_validator import (
     _MODE_STATUS_BY_NAME,
     _MODE_TAXONOMY,
     _OFF_MODES,
@@ -56,6 +56,7 @@ from skill_validator import (
     resolve_link,
     run_validation,
     slugify,
+    validate_capability_sync,
     validate_frontmatter,
     validate_gh_list_limit,
     validate_injection_guard,
@@ -65,6 +66,7 @@ from skill_validator import (
     validate_principle_compliance,
     validate_privacy_patterns,
     validate_security_patterns,
+    validate_tools,
     validate_trigger_preservation,
 )
 
@@ -75,7 +77,7 @@ from skill_validator import (
 
 class TestParseFrontmatter:
     def test_valid_frontmatter(self) -> None:
-        text = "---\nname: foo\ndescription: bar\nlicense: Apache-2.0\n---\n# 
heading\n"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n# heading\n"
         fm = parse_frontmatter(text)
         assert fm is not None
         assert fm["name"] == "foo"
@@ -89,7 +91,7 @@ class TestParseFrontmatter:
             "description: |\n"
             "  First line of description.\n"
             "  Second line.\n"
-            "license: Apache-2.0\n"
+            "capability: capability:setup\nlicense: Apache-2.0\n"
             "---\n"
         )
         fm = parse_frontmatter(text)
@@ -112,7 +114,7 @@ class TestParseFrontmatter:
             "  Paragraph one.\n"
             "\n"
             "  Paragraph two, which used to be dropped.\n"
-            "license: Apache-2.0\n"
+            "capability: capability:setup\nlicense: Apache-2.0\n"
             "---\n"
         )
         fm = parse_frontmatter(text)
@@ -136,13 +138,13 @@ class TestParseFrontmatter:
 class TestValidateFrontmatter:
     def test_valid(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
-        text = "---\nname: foo\ndescription: bar\nlicense: Apache-2.0\n---\n"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert violations == []
 
     def test_missing_name(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
-        text = "---\ndescription: bar\nlicense: Apache-2.0\n---\n"
+        text = "---\ndescription: bar\ncapability: capability:setup\nlicense: 
Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert len(violations) == 1
         assert "name" in violations[0].message
@@ -158,7 +160,7 @@ class TestValidateFrontmatter:
 
     def test_empty_value(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
-        text = "---\nname: \ndescription: bar\nlicense: Apache-2.0\n---\n"
+        text = "---\nname: \ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert any("name' is empty" in v.message for v in violations)
 
@@ -171,20 +173,20 @@ class TestValidateFrontmatter:
     def test_valid_mode(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
         for mode in ("Triage", "Mentoring", "Drafting", "Pairing"):
-            text = f"---\nname: foo\ndescription: bar\nlicense: 
Apache-2.0\nmode: {mode}\n---\n"
+            text = f"---\nname: foo\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\nmode: {mode}\n---\n"
             violations = list(validate_frontmatter(path, text))
             assert violations == [], f"mode '{mode}' should be valid"
 
     def test_invalid_mode(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
-        text = "---\nname: foo\ndescription: bar\nlicense: Apache-2.0\nmode: 
Auto-merge\n---\n"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\nmode: Auto-merge\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert any("mode" in v.message and "'Auto-merge'" in v.message for v 
in violations)
 
     def test_mode_optional(self, tmp_path: Path) -> None:
         # Skills without a mode (e.g. setup-* infrastructure) must not fail.
         path = tmp_path / "SKILL.md"
-        text = "---\nname: foo\ndescription: bar\nlicense: Apache-2.0\n---\n"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert violations == []
 
@@ -212,7 +214,7 @@ class TestValidateFrontmatter:
         path = tmp_path / "SKILL.md"
         desc = "a" * 800
         wtu = "b" * 700
-        text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use: 
{wtu}\nlicense: Apache-2.0\n---\n"
+        text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use: 
{wtu}\ncapability: capability:setup\nlicense: Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert violations == []
 
@@ -220,7 +222,7 @@ class TestValidateFrontmatter:
         path = tmp_path / "SKILL.md"
         desc = "a" * 1000
         wtu = "b" * (MAX_METADATA_CHARS - 1000 + 1)
-        text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use: 
{wtu}\nlicense: Apache-2.0\n---\n"
+        text = f"---\nname: foo\ndescription: {desc}\nwhen_to_use: 
{wtu}\ncapability: capability:setup\nlicense: Apache-2.0\n---\n"
         violations = list(validate_frontmatter(path, text))
         assert any("truncates" in v.message and str(MAX_METADATA_CHARS) in 
v.message for v in violations)
 
@@ -233,7 +235,7 @@ class TestValidateFrontmatter:
             "---\n"
             "name: foo\n"
             "description: bar\n"
-            "license: Apache-2.0\n"
+            "capability: capability:setup\nlicense: Apache-2.0\n"
             "argument-hint: [--quick|--standard|--deep] <idea>\n"
             "---\n"
         )
@@ -249,7 +251,7 @@ class TestValidateFrontmatter:
             "---\n"
             "name: setup-steward\n"
             "description: bar\n"
-            "license: Apache-2.0\n"
+            "capability: capability:setup\nlicense: Apache-2.0\n"
             "argument-hint: [adopt|upgrade|worktree-init|verify|override 
skill-name|unadopt]\n"
             "---\n"
         )
@@ -272,7 +274,7 @@ class TestValidateFrontmatter:
             f"name: foo\n"
             f"description: {desc}\n"
             f"when_to_use: {wtu}\n"
-            f"license: Apache-2.0\n"
+            f"capability: capability:setup\nlicense: Apache-2.0\n"
             f"argument-hint: {hint}\n"
             f"---\n"
         )
@@ -280,12 +282,58 @@ class TestValidateFrontmatter:
         assert violations == [], "argument-hint must not count toward 
description+when_to_use budget"
 
     def test_metadata_block_scalar_indicator_not_counted(self) -> None:
-        text = f"---\nname: foo\ndescription: |\n  {'a' * 100}\nlicense: 
Apache-2.0\n---\n"
+        text = f"---\nname: foo\ndescription: |\n  {'a' * 100}\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n"
         fm = parse_frontmatter(text)
         assert fm is not None
         assert not fm["description"].startswith("|")
         assert len(fm["description"]) == 100
 
+    def test_capability_single_string(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:triage\nlicense: Apache-2.0\n---\n"
+        violations = list(validate_frontmatter(path, text))
+        assert violations == []
+
+    def test_capability_yaml_list(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = (
+            "---\nname: foo\ndescription: bar\n"
+            "capability:\n  - capability:intake\n  - capability:setup\n"
+            "license: Apache-2.0\n---\n"
+        )
+        violations = list(validate_frontmatter(path, text))
+        assert violations == []
+
+    def test_capability_missing(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = "---\nname: foo\ndescription: bar\nlicense: Apache-2.0\n---\n"
+        violations = list(validate_frontmatter(path, text))
+        assert any("capability" in v.message for v in violations)
+
+    def test_capability_invalid_value(self, tmp_path: Path) -> None:
+        path = tmp_path / "SKILL.md"
+        text = "---\nname: foo\ndescription: bar\ncapability: 
capability:bogus\nlicense: Apache-2.0\n---\n"
+        violations = list(validate_frontmatter(path, text))
+        assert any("capability:bogus" in v.message for v in violations)
+
+    def test_capability_list_with_one_invalid_value(self, tmp_path: Path) -> 
None:
+        path = tmp_path / "SKILL.md"
+        text = (
+            "---\nname: foo\ndescription: bar\n"
+            "capability:\n  - capability:setup\n  - capability:invented\n"
+            "license: Apache-2.0\n---\n"
+        )
+        violations = list(validate_frontmatter(path, text))
+        # The "subject" of each violation is the first single-quoted token 
after
+        # the `capability ` word — e.g. "frontmatter capability 
'capability:invented' not in [...]".
+        # The valid entry should never be the subject; only the invalid one 
should be.
+        flagged_subjects = [
+            v.message.split("capability '")[1].split("'")[0]
+            for v in violations
+            if "capability '" in v.message and "not in" in v.message
+        ]
+        assert flagged_subjects == ["capability:invented"]
+
 
 # ---------------------------------------------------------------------------
 # Heading / anchor helpers
@@ -500,7 +548,7 @@ class TestFindRepoRoot:
         # Regression: the silent-pass bug fired only when CWD was inside the 
validator subtree.
         repo = Path(__file__).resolve().parents[3]
         assert (repo / ".claude" / "skills").is_dir(), "test setup 
precondition"
-        monkeypatch.chdir(repo / "tools" / "skill-validator")
+        monkeypatch.chdir(repo / "tools" / "skill-and-tool-validator")
         assert find_repo_root() == repo
 
     def test_explicit_start_outside_repo(self, tmp_path: Path) -> None:
@@ -522,11 +570,29 @@ class TestFindRepoRoot:
 
 class TestSubDocFiles:
     def _make_skill_dir(self, root: Path, skill_name: str = "setup-foo") -> 
Path:
-        """Return a skill directory pre-populated with a valid SKILL.md."""
+        """Return a skill directory pre-populated with a valid SKILL.md.
+
+        Also seeds a matching ``docs/labels-and-capabilities.md`` row so the
+        capability-sync check is satisfied — these tests are about sub-doc
+        handling, not the sync check.
+        """
         skill_dir = root / ".claude" / "skills" / skill_name
         skill_dir.mkdir(parents=True)
         (skill_dir / "SKILL.md").write_text(
-            f"---\nname: {skill_name}\ndescription: bar\nlicense: 
Apache-2.0\n---\n# body\n",
+            f"---\nname: {skill_name}\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n# body\n",
+            encoding="utf-8",
+        )
+        docs = root / "docs"
+        docs.mkdir(parents=True, exist_ok=True)
+        (docs / "labels-and-capabilities.md").write_text(
+            "# Labels and capabilities\n\n"
+            "## Capability to skill map\n\n"
+            "| Skill | Capability / capabilities |\n"
+            "|---|---|\n"
+            f"| `{skill_name}` | `capability:setup` |\n\n"
+            "## Capability to tool map\n\n"
+            "| Tool | Capability / capabilities | Role |\n"
+            "|---|---|---|\n",
             encoding="utf-8",
         )
         return skill_dir
@@ -621,7 +687,7 @@ class TestRunValidation:
         are excluded — they are advisory and surface as warnings, not
         failures. The main runtime gate is `--strict`.
         """
-        from skill_validator import SOFT_CATEGORIES
+        from skill_and_tool_validator import SOFT_CATEGORIES
 
         violations = [v for v in run_validation() if v.category not in 
SOFT_CATEGORIES]
         if violations:
@@ -834,7 +900,9 @@ class TestTriggerPreservation:
 # ---------------------------------------------------------------------------
 
 # Minimal valid SKILL.md frontmatter used across injection-guard tests.
-_GUARD_FM = "---\nname: test-skill\ndescription: bar\nlicense: 
Apache-2.0\n---\n"
+_GUARD_FM = (
+    "---\nname: test-skill\ndescription: bar\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n"
+)
 
 # A gh-pr-view signal that unambiguously looks like a workflow fetch step.
 _GH_PR_VIEW_SIGNAL = "2. **Fetch the PR.** `gh pr view <N> --json 
title,body`\n"
@@ -1212,7 +1280,7 @@ class TestSecurityPatterns:
 
 def _fenced_skill_lf(cmd: str) -> str:
     """Wrap *cmd* in a minimal SKILL.md with a fenced bash block."""
-    return f"---\nname: test\ndescription: test\nlicense: 
Apache-2.0\n---\n\n```bash\n{cmd}\n```\n"
+    return f"---\nname: test\ndescription: test\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n\n```bash\n{cmd}\n```\n"
 
 
 class TestLowercaseFField:
@@ -1282,7 +1350,7 @@ class TestLowercaseFField:
         """Inline backtick prose like ``-f title='...'`` must not fire."""
         path = tmp_path / "SKILL.md"
         text = (
-            "---\nname: test\ndescription: test\nlicense: Apache-2.0\n---\n\n"
+            "---\nname: test\ndescription: test\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n\n"
             "Avoid using `-f title='value'` — use `-F title=@file` instead.\n"
         )
         violations = list(validate_lowercase_f_field(path, text))
@@ -1292,7 +1360,7 @@ class TestLowercaseFField:
         """Bare prose outside a fenced block must not fire."""
         path = tmp_path / "SKILL.md"
         text = (
-            "---\nname: test\ndescription: test\nlicense: Apache-2.0\n---\n\n"
+            "---\nname: test\ndescription: test\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n\n"
             "Run: gh api milestones -f title='v1'\n"
         )
         violations = list(validate_lowercase_f_field(path, text))
@@ -1309,9 +1377,9 @@ class TestLowercaseFField:
     def test_violation_line_number_correct(self, tmp_path: Path) -> None:
         path = tmp_path / "SKILL.md"
         text = _fenced_skill_lf("gh api repos/<tracker>/milestones -f 
title='v1.0'")
-        # Layout: 1:--- 2:name 3:description 4:license 5:--- 6:blank 7:```bash 
8:command
+        # Layout: 1:--- 2:name 3:description 4:capability 5:license 6:--- 
7:blank 8:```bash 9:command
         violations = list(validate_lowercase_f_field(path, text))
-        assert violations[0].line == 8
+        assert violations[0].line == 9
 
     def test_lowercase_f_field_in_soft_categories(self) -> None:
         assert LOWERCASE_F_FIELD_CATEGORY in SOFT_CATEGORIES
@@ -1574,9 +1642,25 @@ class TestPrivacyPatternP6:
 
 
 def _skill_root(tmp_path: Path) -> Path:
-    """Create a minimal repo tree with .claude/skills/ and return the root."""
+    """Create a minimal repo tree with .claude/skills/ and return the root.
+
+    Also seeds an empty ``docs/labels-and-capabilities.md`` so the
+    capability-sync check doesn't fire its "missing doc" violation in
+    tests that don't exercise the sync check directly.
+    """
     skills = tmp_path / ".claude" / "skills"
     skills.mkdir(parents=True)
+    docs = tmp_path / "docs"
+    docs.mkdir(parents=True, exist_ok=True)
+    (docs / "labels-and-capabilities.md").write_text(
+        "# Labels and capabilities\n\n"
+        "## Capability to skill map\n\n"
+        "| Skill | Capability / capabilities |\n"
+        "|---|---|\n\n"
+        "## Capability to tool map\n\n"
+        "| Tool | Capability / capabilities | Role |\n"
+        "|---|---|---|\n"
+    )
     return tmp_path
 
 
@@ -1774,12 +1858,23 @@ class TestCollectDocFiles:
 
 
 def _make_valid_skill(root: Path, name: str) -> Path:
-    """Write a minimal valid SKILL.md under .claude/skills/<name>/."""
+    """Write a minimal valid SKILL.md under .claude/skills/<name>/ and add a
+    matching row to docs/labels-and-capabilities.md so the capability-sync
+    check stays satisfied."""
     skill_dir = root / ".claude" / "skills" / name
     skill_dir.mkdir(parents=True, exist_ok=True)
     (skill_dir / "SKILL.md").write_text(
-        f"---\nname: {name}\ndescription: A test skill.\nlicense: 
Apache-2.0\n---\n# Body\nSome content.\n"
+        f"---\nname: {name}\ndescription: A test skill.\ncapability: 
capability:setup\nlicense: Apache-2.0\n---\n# Body\nSome content.\n"
     )
+    # Inject a row into the skill table of the seeded doc.
+    doc = root / "docs" / "labels-and-capabilities.md"
+    if doc.exists():
+        text = doc.read_text()
+        row = f"| `{name}` | `capability:setup` |\n"
+        # Insert right after the skill table's separator row.
+        marker = "## Capability to skill map\n\n| Skill | Capability / 
capabilities |\n|---|---|\n"
+        if marker in text and row not in text:
+            doc.write_text(text.replace(marker, marker + row, 1))
     return skill_dir
 
 
@@ -1830,15 +1925,241 @@ class TestMain:
             "---\n"
             "name: soft-skill\n"
             "description: A test skill.\n"
-            "license: Apache-2.0\n"
+            "capability: capability:setup\nlicense: Apache-2.0\n"
             "---\n"
             "```bash\n"
             'gh pr comment 1 --body "attacker content"\n'
             "```\n"
         )
+        # Add a matching row to the seeded doc so the capability-sync check 
stays clean.
+        doc = root / "docs" / "labels-and-capabilities.md"
+        text = doc.read_text()
+        marker = "## Capability to skill map\n\n| Skill | Capability / 
capabilities |\n|---|---|\n"
+        doc.write_text(text.replace(marker, marker + "| `soft-skill` | 
`capability:setup` |\n", 1))
         monkeypatch.chdir(root)
 
         rc_normal = main([])
         rc_strict = main(["--strict"])
         assert rc_normal == 0
         assert rc_strict == 1
+
+
+# ---------------------------------------------------------------------------
+# Tool README + capability declaration validation
+# ---------------------------------------------------------------------------
+
+
+def _make_tools_root(tmp_path: Path) -> Path:
+    """Create a minimal repo layout: <tmp>/tools/ + <tmp>/.claude/skills/."""
+    root = tmp_path / "repo"
+    (root / "tools").mkdir(parents=True)
+    (root / ".claude" / "skills").mkdir(parents=True)
+    return root
+
+
+class TestValidateTools:
+    def test_tool_with_valid_readme(self, tmp_path: Path) -> None:
+        root = _make_tools_root(tmp_path)
+        tool = root / "tools" / "foo"
+        tool.mkdir()
+        (tool / "README.md").write_text("# tools/foo\n\n**Capability:** 
capability:setup\n\nFoo tool.\n")
+        violations = list(validate_tools(root))
+        assert violations == []
+
+    def test_tool_missing_readme(self, tmp_path: Path) -> None:
+        root = _make_tools_root(tmp_path)
+        (root / "tools" / "no-readme").mkdir()
+        violations = list(validate_tools(root))
+        assert len(violations) == 1
+        assert "missing README.md" in violations[0].message
+        assert violations[0].category == "tool-readme"
+
+    def test_tool_readme_without_capability(self, tmp_path: Path) -> None:
+        root = _make_tools_root(tmp_path)
+        tool = root / "tools" / "bare"
+        tool.mkdir()
+        (tool / "README.md").write_text("# bare\n\nDescription only, no 
capability line.\n")
+        violations = list(validate_tools(root))
+        assert len(violations) == 1
+        assert "missing '**Capability:**" in violations[0].message
+        assert violations[0].category == "tool-capability"
+
+    def test_tool_capability_invalid_value(self, tmp_path: Path) -> None:
+        root = _make_tools_root(tmp_path)
+        tool = root / "tools" / "bad"
+        tool.mkdir()
+        (tool / "README.md").write_text("# bad\n\n**Capability:** 
capability:bogus\n")
+        violations = list(validate_tools(root))
+        assert any("capability:bogus" in v.message for v in violations)
+
+    def test_tool_capability_multi_value(self, tmp_path: Path) -> None:
+        root = _make_tools_root(tmp_path)
+        tool = root / "tools" / "dual"
+        tool.mkdir()
+        (tool / "README.md").write_text("# dual\n\n**Capability:** 
capability:setup + capability:intake\n")
+        violations = list(validate_tools(root))
+        assert violations == []
+
+    def test_tool_capability_regex_does_not_slurp_past_line(self, tmp_path: 
Path) -> None:
+        # Regression guard: an earlier version of the regex matched 
`[A-Za-z0-9:+\s]+`
+        # which included newlines, so the parser captured prose from the next
+        # paragraph and reported false "invalid capability" errors.
+        root = _make_tools_root(tmp_path)
+        tool = root / "tools" / "with-prose"
+        tool.mkdir()
+        (tool / "README.md").write_text(
+            "# tools/with-prose\n\n"
+            "**Capability:** capability:setup\n\n"
+            "Some prose that follows the capability line and should NOT be 
parsed as part of it.\n"
+        )
+        violations = list(validate_tools(root))
+        assert violations == []
+
+
+# ---------------------------------------------------------------------------
+# Capability sync check: docs/labels-and-capabilities.md ↔ live source
+# ---------------------------------------------------------------------------
+
+
+def _seed_capability_repo(
+    tmp_path: Path,
+    *,
+    doc_skills: dict[str, str],
+    doc_tools: dict[str, str],
+    live_skills: dict[str, str],
+    live_tools: dict[str, str],
+) -> Path:
+    """Build a tiny repo with a labels-and-capabilities.md doc, skills, and 
tool READMEs.
+
+    `*_skills` maps skill-name → capability cell text (e.g. 
``capability:triage``).
+    `*_tools` maps tool-name → capability cell text (e.g. ``capability:setup + 
capability:intake``).
+    """
+    root = tmp_path / "repo"
+    (root / "docs").mkdir(parents=True)
+    (root / ".claude" / "skills").mkdir(parents=True)
+    (root / "tools").mkdir(parents=True)
+
+    skill_rows = "\n".join(f"| `{n}` | `{c}` |" for n, c in doc_skills.items())
+    tool_rows = "\n".join(f"| [`tools/{n}`](../tools/{n}/) | `{c}` | role |" 
for n, c in doc_tools.items())
+    doc_body = (
+        "# Labels and capabilities\n\n"
+        "## Capability to skill map\n\n"
+        "| Skill | Capability / capabilities |\n"
+        "|---|---|\n"
+        f"{skill_rows}\n\n"
+        "## Capability to tool map\n\n"
+        "| Tool | Capability / capabilities | Role |\n"
+        "|---|---|---|\n"
+        f"{tool_rows}\n"
+    )
+    (root / "docs" / "labels-and-capabilities.md").write_text(doc_body)
+
+    for skill, cap in live_skills.items():
+        d = root / ".claude" / "skills" / skill
+        d.mkdir()
+        (d / "SKILL.md").write_text(
+            f"---\nname: {skill}\ndescription: test\ncapability: 
{cap}\nlicense: Apache-2.0\n---\n"
+        )
+
+    for tool, cap in live_tools.items():
+        d = root / "tools" / tool
+        d.mkdir()
+        (d / "README.md").write_text(f"# {tool}\n\n**Capability:** {cap}\n")
+
+    return root
+
+
+class TestValidateCapabilitySync:
+    def test_aligned_passes(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={"alpha": "capability:triage"},
+            doc_tools={"omega": "capability:setup"},
+            live_skills={"alpha": "capability:triage"},
+            live_tools={"omega": "capability:setup"},
+        )
+        violations = list(validate_capability_sync(root))
+        assert violations == []
+
+    def test_skill_in_doc_but_not_live(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={"alpha": "capability:triage", "ghost": 
"capability:fix"},
+            doc_tools={},
+            live_skills={"alpha": "capability:triage"},
+            live_tools={},
+        )
+        violations = list(validate_capability_sync(root))
+        assert any("'ghost'" in v.message and "no live SKILL.md" in v.message 
for v in violations)
+
+    def test_live_skill_missing_from_doc(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={"alpha": "capability:triage"},
+            doc_tools={},
+            live_skills={"alpha": "capability:triage", "extra": 
"capability:fix"},
+            live_tools={},
+        )
+        violations = list(validate_capability_sync(root))
+        assert any("'extra'" in v.message and "no row in the skill table" in 
v.message for v in violations)
+
+    def test_skill_capability_mismatch(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={"alpha": "capability:triage"},
+            doc_tools={},
+            live_skills={"alpha": "capability:fix"},
+            live_tools={},
+        )
+        violations = list(validate_capability_sync(root))
+        assert any("'alpha' capability mismatch" in v.message for v in 
violations)
+
+    def test_tool_in_doc_but_not_live(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={},
+            doc_tools={"omega": "capability:setup", "ghost-tool": 
"capability:setup"},
+            live_skills={},
+            live_tools={"omega": "capability:setup"},
+        )
+        violations = list(validate_capability_sync(root))
+        assert any("'ghost-tool'" in v.message and "no live tools/" in 
v.message for v in violations)
+
+    def test_live_tool_missing_from_doc(self, tmp_path: Path) -> None:
+        root = _seed_capability_repo(
+            tmp_path,
+            doc_skills={},
+            doc_tools={"omega": "capability:setup"},
+            live_skills={},
+            live_tools={"omega": "capability:setup", "extra-tool": 
"capability:stats"},
+        )
+        violations = list(validate_capability_sync(root))
+        assert any(
+            "'extra-tool'" in v.message and "no row in the tool table" in 
v.message for v in violations
+        )
+
+    def test_italic_parens_annotation_is_stripped(self, tmp_path: Path) -> 
None:
+        # Doc row carries an italic-parenthetical future-state note.
+        # The token inside *( ... )* must NOT count as a declared capability.
+        root = tmp_path / "repo"
+        (root / "docs").mkdir(parents=True)
+        (root / ".claude" / "skills" / "alpha").mkdir(parents=True)
+        doc = (
+            "# Labels and capabilities\n\n"
+            "## Capability to skill map\n\n"
+            "| Skill | Capability / capabilities |\n"
+            "|---|---|\n"
+            "| `alpha` | `capability:intake` *(+ `capability:reconciliation` 
once [#1](https://x.y/issues/1) lands)* |\n\n"
+            "## Capability to tool map\n\n"
+            "| Tool | Capability / capabilities | Role |\n"
+            "|---|---|---|\n"
+        )
+        (root / "docs" / "labels-and-capabilities.md").write_text(doc)
+        (root / ".claude" / "skills" / "alpha" / "SKILL.md").write_text(
+            "---\nname: alpha\ndescription: test\ncapability: 
capability:intake\nlicense: Apache-2.0\n---\n"
+        )
+        (root / "tools").mkdir()
+        violations = list(validate_capability_sync(root))
+        # The parenthetical capability:reconciliation must NOT be flagged as a 
doc-side declared capability;
+        # the row's authoritative capability is just intake, which matches the 
live skill.
+        assert violations == [], [v.message for v in violations]
diff --git a/tools/skill-validator/uv.lock 
b/tools/skill-and-tool-validator/uv.lock
similarity index 99%
rename from tools/skill-validator/uv.lock
rename to tools/skill-and-tool-validator/uv.lock
index ff72e42..2be7473 100644
--- a/tools/skill-validator/uv.lock
+++ b/tools/skill-and-tool-validator/uv.lock
@@ -277,7 +277,7 @@ wheels = [
 ]
 
 [[package]]
-name = "skill-validator"
+name = "skill-and-tool-validator"
 version = "0.1.0"
 source = { editable = "." }
 
diff --git a/tools/skill-evals/README.md b/tools/skill-evals/README.md
index d3cfd66..86e0932 100644
--- a/tools/skill-evals/README.md
+++ b/tools/skill-evals/README.md
@@ -1,5 +1,7 @@
 # skill-evals
 
+**Capability:** capability:setup + capability:stats
+
 Behavioral eval harness for Apache Steward skills. Each eval suite tests a 
skill pipeline step by step, verifying that the model produces the correct 
structured JSON output for a fixed set of fixture cases.
 
 Nineteen suites are currently implemented:
diff --git a/tools/spec-loop/README.md b/tools/spec-loop/README.md
index 8255be0..9d8fc67 100644
--- a/tools/spec-loop/README.md
+++ b/tools/spec-loop/README.md
@@ -3,6 +3,8 @@
 
 # spec-loop
 
+**Capability:** capability:setup
+
 A spec-driven build loop for this framework, in the general
 [Ralph](https://ghuntley.com/ralph/) style (run a fresh agent context
 against a fixed prompt, repeat), adapted to the framework's
diff --git a/tools/spec-status-index/README.md 
b/tools/spec-status-index/README.md
index 844a709..3331d06 100644
--- a/tools/spec-status-index/README.md
+++ b/tools/spec-status-index/README.md
@@ -14,6 +14,8 @@
 
 # spec-status-index
 
+**Capability:** capability:setup + capability:stats
+
 A deterministic `uv` tool that reads spec-loop specs from
 `tools/spec-loop/specs/` and prints them grouped by status, so build
 iterations can choose the next work item mechanically.
diff --git a/tools/vulnogram/README.md b/tools/vulnogram/README.md
new file mode 100644
index 0000000..a6079a5
--- /dev/null
+++ b/tools/vulnogram/README.md
@@ -0,0 +1,16 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update 
-->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with 
[DocToc](https://github.com/thlorenz/doctoc)*
+
+- [`tools/vulnogram/`](#toolsvulnogram)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/licenses/LICENSE-2.0 -->
+
+# `tools/vulnogram/`
+
+**Capability:** capability:resolve
+
+ASF Vulnogram CVE-allocation client. OAuth-authenticated API that allocates a 
CVE ID through the ASF's Vulnogram instance and publishes the CVE record. 
Consumed by `security-cve-allocate`. See [`allocation.md`](allocation.md), 
[`record.md`](record.md), and [`bot-credits-policy.md`](bot-credits-policy.md) 
for the protocol.

Reply via email to