| # 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 |