blob: 9c73124b5620f2cf8f61d97e043fbf7633088d99 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
---
default_stages: [commit, push]
default_language_version:
python: python3
# Node 22 is the current active LTS line. markdownlint-cli2 ≥ v0.18
# (and its string-width dependency from v6 onward) uses the regex
# `/v` flag, which requires Node ≥ 20 to parse. The previous Node
# 18.6.0 pin matched the framework's pre-PR-#18 tooling baseline
# but is incompatible with the markdownlint-cli2 v0.22.1 rev
# pinned below.
#
# Bumped 22.11.0 -> 22.13.0: markdownlint-cli2's transitive dep
# `@eslint/js@10.0.1` declares engines `^20.19.0 || ^22.13.0 || >=24`,
# so the old 22.11.0 pin failed `npm install` with EBADENGINE. 22.13.0
# is the lowest 22.x that satisfies it.
#
# Bumped 22.13.0 -> 22.20.0: ava@7.0.0 declares engines
# `^20.19 || ^22.20 || ^24.12 || >=25`; 22.13.0 fails EBADENGINE.
# 22.20.0 is the lowest 22.x that satisfies both constraints.
node: 22.20.0
minimum_prek_version: '0.3.5'
repos:
- repo: meta
hooks:
- id: identity
name: Print input to the static check hooks for troubleshooting
- id: check-hooks-apply
name: Check if all hooks apply to the repository
- repo: https://github.com/thlorenz/doctoc.git
rev: v2.5.0
hooks:
- id: doctoc
name: Add TOC for Markdown and RST files
files: ^.+\.(md|rst)$
# Skip agent-facing skill definitions across every skill-dir layout
# (.agents/skills canonical home, plus the .claude/skills and
# .github/skills relays, and the skills/ source): their YAML
# frontmatter must be the first thing in the file, which is
# incompatible with doctoc inserting a TOC block at the top. Also skip skill-evals fixture and
# README files, and the spec-loop specs/prompts (YAML-frontmatter
# specs and short single-purpose prompts, same rationale as skills)
# — short docs that don't warrant a TOC.
# Skip the PR template — GitHub pre-populates a new PR description
# with the template verbatim, so a TOC block becomes per-PR noise the
# contributor has to delete by hand.
exclude: ^(\.claude/skills/.*|\.agents/skills/.*|\.github/skills/.*|skills/.*|tools/cve-tool-vulnogram/generate-cve-json/SKILL\.md|tools/skill-evals/.*|tools/spec-loop/.*|\.github/PULL_REQUEST_TEMPLATE\.md)$
args:
- "--maxlevel"
- "3"
# Stamp an approved Apache-2.0 licence header into Markdown and .gitignore
# files so the Apache RAT licence check (.github/workflows/rat.yml) — and
# any reviewer — sees an approved header on every one. Markdown and
# .gitignore carry no comment header by convention, so RAT would otherwise
# flag (or silently skip) them. Plain Markdown gets an HTML-comment header
# on line 1; files with YAML front matter get a `# SPDX-License-Identifier`
# comment as the first line *inside* the front matter, so line 1 stays
# `---` and no new key is introduced for a front-matter validator to
# reject; .gitignore gets the full ASF header as `#` comments (the block
# .gitattributes carries), matching the convention that hash-comment
# config files carry the full header. The hook fixes files in place and
# fails the commit when it changes one (the end-of-file-fixer convention),
# so the contributor re-stages the now-stamped file. The `exclude` mirrors
# `.rat-excludes`: the third-party-licence detection fixtures deliberately
# contain non-Apache licence text and must not be stamped Apache-2.0.
- repo: local
hooks:
- id: add-license-headers
name: Stamp Apache licence header into Markdown and .gitignore
language: system
entry: tools/dev/add-license-headers.py
files: (\.md|(^|/)\.gitignore)$
exclude: ^tools/skill-evals/evals/pr-management-code-review/step-4-third-party-license/fixtures/.*$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-merge-conflict
name: Check that merge conflicts are not being committed
- id: detect-private-key
name: Detect if private key is added to the repository
# The next three hooks are *fixers*: they open every matched file
# in read-write mode (to normalise it in place) before checking
# whether a fix is even needed. The secure-agent sandbox
# write-denies two tracked path sets — the agent's own config
# (`.claude/settings.json`) and the self-adoption skill relays
# (`.claude/skills/*` symlinks) — because agents must never modify
# them. So on `prek run --all-files` (which `agent-pre-commit.sh`
# and the CI mirror both run) the read-write `open()` on those
# paths is denied and the whole run aborts, even though the files
# need no fix. Exclude them: `.claude/settings.json` is gated by
# the sandbox-lint baseline and the skill symlinks are validated by
# `symlink-lint`, so neither loses coverage by skipping EOF /
# whitespace / line-ending normalisation here. This keeps prek a
# single invocation that runs cleanly in-sandbox with no bypass.
- id: end-of-file-fixer
name: Make sure that there is an empty line at the end
exclude: &sandbox_write_denied ^\.claude/(settings\.json|skills/)
- id: mixed-line-ending
name: Detect if mixed line ending is used (\r vs. \r\n)
exclude: *sandbox_write_denied
- id: trailing-whitespace
name: Remove trailing whitespace at end of line
exclude: *sandbox_write_denied
# markdownlint — catches structurally-bad markdown. Config in
# `.markdownlint.json` enables only the rules that catch real bugs
# (broken anchors via MD051, dangling link references via MD053);
# style choices the existing docs already settled stay off.
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.22.1
hooks:
- id: markdownlint-cli2
name: markdownlint
files: \.md$
# Skip skill-evals fixture markdown: report.md / system-prompt.md /
# user-prompt-template.md are test data containing raw code diffs and
# CLI snippets (leading-underscore identifiers, `*args`, `--- /dev/null`,
# etc.). The eval runner reads them as plain text, never rendered, so
# prose rules like MD037 only ever false-positive on them. Suite
# README.md files are NOT excluded — they are real docs.
exclude: ^tools/skill-evals/evals/.+/fixtures/.+\.md$
# typos — fast spell-checker. Allowlist is `.typos.toml`.
# Override the default args (which include `--write-changes`) so
# the hook never silently rewrites files; CI surfaces typos as
# errors instead.
- repo: https://github.com/crate-ci/typos
rev: v1.47.2
hooks:
- id: typos
name: typos
args: [--force-exclude]
# lychee — link checker (was the standalone `link-check.yml`
# workflow; converted to this prek hook). Validates cross-file links,
# `#anchor` fragments, and external URLs across markdown / rst /
# `.md.j2`. Config in `.lychee.toml`.
#
# `language: rust` with `additional_dependencies: [cli:lychee]`
# means prek installs lychee itself (cargo install from crates.io)
# into an isolated, cached hook env — the hook does NOT depend on a
# locally-installed lychee, and the prek CI workflow needs no extra
# install step. Unpinned, so it tracks the **latest** lychee; the
# exact version is resolved when the hook env is first built and then
# held stable by the prek env cache until the cache busts (keyed on
# this file). lychee must be >= 0.24 for `.lychee.toml`'s
# `include_fragments = "anchor-only"` enum-string schema.
#
# `pass_filenames: false` + the trailing `.` arg → lychee scans the
# whole repo (so a renamed link target is caught no matter which
# file references it); `files:` only gates *whether* the whole-repo
# scan fires, i.e. it runs when any doc file changes and always on
# `prek run --all-files` (CI). External-URL results are cached for
# 7 days (`.lycheecache`, gitignored).
- repo: local
hooks:
- id: lychee
name: lychee (link check)
language: rust
entry: lychee
args: ["--config", ".lychee.toml", "--no-progress", "."]
additional_dependencies: ["cli:lychee"]
files: \.(md|rst|j2)$
pass_filenames: false
# Local placeholder linter — catches hardcoded references like
# `apache/airflow` or `Apache Airflow` that should be the
# placeholder tokens `<upstream>` / `<PROJECT>` per
# `AGENTS.md#placeholder-convention-used-in-skill-files`.
- repo: local
hooks:
- id: check-placeholders
name: check-placeholders
language: system
entry: tools/dev/check-placeholders.sh
files: ^(skills/.*|tools/.*)\.(md|sh|py|yaml|yml|toml)$
pass_filenames: false
# Vendor-neutrality score — regenerate the block embedded in
# docs/vendor-neutrality.md from repository metadata (tool README
# `**Vendor:**` / `**Harness:**` fields, skill frontmatter, the
# privacy-llm registry) so the published number never drifts from the
# tree. `--in-place` rewrites the block and exits non-zero when it
# changed, so a stale doc fails the commit and the contributor re-stages
# the regenerated file — the doctoc pattern, but for a computed score.
# `pass_filenames: false`: the scorer always reads the whole tree; the
# `files:` filter only gates *whether* the regen fires (and it always
# runs on `prek run --all-files` in CI). Deterministic and offline, so
# the same tree always yields the same block. Tool + tests + scoring
# rule live in tools/vendor-neutrality-score (README + tests).
- repo: local
hooks:
- id: vendor-neutrality-score
name: Regenerate the vendor-neutrality score in docs/vendor-neutrality.md
language: system
entry: uv run --project tools/vendor-neutrality-score vendor-neutrality-score --in-place
files: ^(tools/[^/]+/README\.md|tools/privacy-llm/models\.md|tools/vendor-neutrality-score/.*|skills/[^/]+/SKILL\.md|docs/vendor-neutrality\.md)$
pass_filenames: false
# Self-adoption symlink hygiene: reject cyclic symlinks and misdirected
# skill relays. `types: [symlink]` fires it on any staged symlink (always
# on `prek run --all-files`). Rules, rationale, and the pytest suite live
# in tools/symlink-lint (README + tests).
- repo: local
hooks:
- id: symlink-lint
name: symlink-lint
language: system
entry: python3 tools/symlink-lint/src/symlink_lint/__init__.py
types: [symlink]
pass_filenames: false
# Release-archive guard: build the source archive exactly as the release
# does (git archive of the export tree, honouring .gitattributes
# export-ignore) and reject any symlink a safe extractor refuses — a
# dangling link (its target got export-ignored) or a chain (target is
# itself a symlink, which ATR's upload validator rejects as "outside the
# extraction directory"). This is what -1'd an RC upload; the hook fails
# the commit before an RC is cut. Re-fires whenever .gitattributes, any
# symlink, or the skills/agent-view tree that the archive ships changes.
- repo: local
hooks:
- id: symlink-lint-archive
name: symlink-lint (release archive)
language: system
entry: python3 tools/symlink-lint/src/symlink_lint/__init__.py --archive
files: ^(\.gitattributes|\.(agents|claude|github|kiro)/skills/.*|skills/.*|projects/_template/.*)$
types_or: [symlink, file]
pass_filenames: false
# Workspace-membership guard. The single source of truth for
# which Python projects get pre-commit hooks + the CI pytest
# matrix is the `[tool.uv.workspace] members` array in the root
# `pyproject.toml`. If a new `tools/<name>/pyproject.toml`
# appears without being added to that array, both surfaces
# silently skip it — the very drift bug the workspace refactor
# was meant to prevent. This hook re-fires whenever any
# tools/**/pyproject.toml or the root pyproject changes.
- repo: local
hooks:
- id: check-workspace-members
name: check-workspace-members
language: system
entry: tools/dev/check-workspace-members.py
files: ^(tools/[^/]+(/[^/]+)?/pyproject\.toml|pyproject\.toml)$
pass_filenames: false
# Workspace-level static checks. Iterate over every uv-workspace
# member declared in the root `pyproject.toml`'s
# `[tool.uv.workspace] members` list via
# `tools/dev/run-workspace-check.sh`. The driver auto-discovers
# which checks apply per member (e.g. only runs `mypy` on members
# with a `[tool.mypy]` section in their pyproject) and honours an
# opt-out `[tool.magpie.checks] skip = [...]` block per member.
#
# Adding a new uv-managed tool requires NO edit here — append its
# path to `[tool.uv.workspace] members` in the root pyproject and
# all four hooks pick it up automatically.
- repo: local
hooks:
- id: workspace-ruff-check
name: ruff check (workspace)
language: system
entry: tools/dev/run-workspace-check.sh ruff "ruff check"
files: ^(tools/[^/]+(/[^/]+)?/(src|tests|pyproject\.toml)|pyproject\.toml|tools/dev/run-workspace-check\.sh)
pass_filenames: false
- id: workspace-ruff-format
name: ruff format (workspace)
language: system
entry: tools/dev/run-workspace-check.sh ruff-format "ruff format --check"
files: ^(tools/[^/]+(/[^/]+)?/(src|tests|pyproject\.toml)|pyproject\.toml|tools/dev/run-workspace-check\.sh)
pass_filenames: false
- id: workspace-mypy
name: mypy (workspace)
language: system
entry: tools/dev/run-workspace-check.sh mypy mypy
files: ^(tools/[^/]+(/[^/]+)?/(src|tests|pyproject\.toml)|pyproject\.toml|tools/dev/run-workspace-check\.sh)
pass_filenames: false
- id: workspace-pytest
name: pytest (workspace)
language: system
entry: tools/dev/run-workspace-check.sh pytest "pytest --color=yes"
files: ^(tools/[^/]+(/[^/]+)?/(src|tests|pyproject\.toml)|pyproject\.toml|tools/dev/run-workspace-check\.sh)
pass_filenames: false
# Validate `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-and-tool-validate
name: skill-and-tool-validate (skills, tools, capability sync)
language: system
entry: uv run --project tools/skill-and-tool-validator skill-and-tool-validate
files: ^(skills/.*\.md|tools/[^/]+/README\.md|docs/labels-and-capabilities\.md|tools/skill-and-tool-validator/(src|pyproject\.toml))
pass_filenames: false
# Validate `tools/spec-loop/specs/` frontmatter and required body sections
# via the `spec-validate` CLI. Re-fires on spec-source changes so rule
# updates get re-applied. `--project` (not `--directory`) so CWD stays at
# repo root; the directory argument is passed directly to the CLI so it
# scans the full specs tree even when only one file changed.
- repo: local
hooks:
- id: spec-validate
name: spec-validate (spec-loop specs frontmatter + sections)
language: system
entry: uv run --project tools/spec-validator spec-validate tools/spec-loop/specs/
files: ^(tools/spec-loop/specs/.*\.md|tools/spec-validator/(src|pyproject\.toml))
pass_filenames: false