Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,21 +268,59 @@ repos:
entry: python3 tools/dev/check-family-plugins.py
files: ^(skills/.*/SKILL\.md|plugins/.*|\.claude-plugin/(marketplace|plugin)\.json|\.codex-plugin/plugin\.json|\.agents/plugins/marketplace\.json|(plugin|marketplace)\.json|gemini-extension\.json|apm\.yml|pyproject\.toml|hooks/check-upgrade\.sh)$
pass_filenames: false
# Documentation claims that must track the tree. Four mechanical checks,
# each added after the drift it catches was found by hand:
# - every tools/spec-loop/specs/*.md is listed in BOTH index files
# (overview.md and README.md) — ten specs were absent from both,
# including per-family specs and one added four PRs earlier;
# - README.md's family table "N skills" cells match live `family:`
# frontmatter;
# - docs/modes.md's *Modes at a glance* Skill-count column matches live
# `mode:` frontmatter (the validator's modes-doc rule checks section
# membership but reads only the mode and status columns, so the counts
# were unguarded);
# - bare "N skills" totals in docs/setup/marketplaces.md match the
# catalogue;
# - every script in tools/dev/ is named in tools/dev/README.md — these are
# the framework's own gates, and an undocumented one is invisible to the
# next contributor deciding whether it applies to their change.
# None of these break anything when wrong, which is exactly why they rot:
# they are numbers a human must remember to update while thinking about
# something else.
- repo: local
hooks:
- id: check-doc-sync
name: check-doc-sync (spec indexes + declared skill counts)
language: system
entry: python3 tools/dev/check-doc-sync.py
files: ^(skills/[^/]+/SKILL\.md|tools/spec-loop/specs/.*\.md|README\.md|docs/modes\.md|docs/setup/marketplaces\.md|tools/dev/.*)$
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.
# was meant to prevent.
#
# It also verifies every declared member's tests actually run. Both
# the CI matrix and the workspace sweep key off the presence of
# `[tool.pytest.ini_options]`, so a project can carry a full tests/
# directory and never be executed by anything — no red tick, because
# no job. Reported: tests with no config (never run), config with no
# tests (a green job that collected nothing), and neither. A project
# that should not be tested declares `[tool.magpie.checks] skip =
# ["pytest"]` rather than being quietly absent.
#
# Re-fires on any member pyproject, the root pyproject, or any test
# file — adding or deleting tests changes the answer.
- repo: local
hooks:
- id: check-workspace-members
name: check-workspace-members
name: check-workspace-members (membership + tests actually run)
language: system
entry: tools/dev/check-workspace-members.py
files: ^(tools/[^/]+(/[^/]+)?/pyproject\.toml|pyproject\.toml)$
files: ^((tools/[^/]+(/[^/]+)?|ai-tutors|skills)/pyproject\.toml|pyproject\.toml|.*/tests?/.*\.py|.*/test_[^/]+\.py|.*_test\.py)$
pass_filenames: false
# Workspace-level static checks. Iterate over every uv-workspace
# member declared in the root `pyproject.toml`'s
Expand Down
14 changes: 5 additions & 9 deletions ai-tutors/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,8 @@ addopts = "-ra -q"
testpaths = ["."]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.3.0",
"pytest>=9.1.1",
"ruff>=0.16.1",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
27 changes: 18 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,26 +41,28 @@ requires-python = ">=3.11"
#
# - `prek` is the static-check / pre-commit runner. CI installs it
# via `uv sync --group dev` and invokes `uv run prek …`.
# - `ruff`, `mypy`, `pytest` are the shared static-analysis +
# test toolchain. They are declared here for root-level ad-hoc
# use AND, pinned to the same lower bounds, in every workspace
# member's own `[dependency-groups] dev` block — so each
# sub-project's environment is self-contained. The workspace
# - `magpie-dev` (tools/dev) carries the shared static-analysis +
# test toolchain — ruff, mypy, pytest — declared in ONE place and
# depended on by every workspace member's own `dev` group, so each
# sub-project's environment is still self-contained. The workspace
# checks run each tool against the member's own project via
# `uv run --directory <member> --project . …` (see
# `tools/dev/run-workspace-check.sh`), so a member is never
# dependent on tools leaking in from the shared root environment.
# Keep the version bounds here and in every member in lockstep.
#
# Bump a toolchain version in `tools/dev/pyproject.toml` and the
# whole workspace moves together. The previous arrangement repeated
# the three pins in every member with an instruction to keep them in
# lockstep, and they had already drifted into three different mypy
# floors, two pytest floors, and two ruff floors.
#
# Lower bounds mirror what individual tools needed at their last
# bump; the upper bound is enforced implicitly by the 7-day
# `[tool.uv] exclude-newer` cooldown below, so the resolved version
# is "the most recent that is at least 7 days old".
dev = [
"prek>=0.4.12",
"ruff>=0.16.1",
"mypy>=2.3.0",
"pytest>=9.1.1",
"magpie-dev",
]

[tool.uv]
Expand Down Expand Up @@ -92,11 +94,18 @@ package = false
# per-tool install time). This is desired — drift between tools'
# pinned versions is harder to spot when each has its own lock
# file.
[tool.uv.sources]
# `magpie-dev` is the workspace's shared dev toolchain (tools/dev). Declared
# here once so every member can name it in its `dev` group without repeating
# the source; workspace members inherit the root's sources.
magpie-dev = { workspace = true }

[tool.uv.workspace]
members = [
"ai-tutors",
"skills",
"tools/agent-guard",
"tools/dev",
"tools/agent-isolation",
"tools/bitbucket",
"tools/egress-gateway",
Expand Down
14 changes: 5 additions & 9 deletions skills/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,8 @@ addopts = "-ra -q"
testpaths = ["."]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.3.0",
"pytest>=9.1.1",
"ruff>=0.16.2",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
14 changes: 5 additions & 9 deletions tools/agent-guard/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,8 @@ addopts = "-ra -q"
testpaths = ["tests"]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.1.0",
"pytest>=9.1.1",
"ruff>=0.16.1",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
14 changes: 5 additions & 9 deletions tools/agent-isolation/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ addopts = "-ra -q"
testpaths = ["tests"]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.1.0",
"pytest>=9.1.1",
"ruff>=0.16.1",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
6 changes: 1 addition & 5 deletions tools/bitbucket/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,4 @@ addopts = "-ra -q"
testpaths = ["tests"]

[dependency-groups]
dev = [
"mypy>=1.10",
"pytest>=8.0",
"ruff>=0.16.1",
]
dev = ["magpie-dev"]
14 changes: 5 additions & 9 deletions tools/cve-tool-vulnogram/generate-cve-json/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,8 @@ addopts = "-ra -q"
testpaths = ["tests"]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.3.0",
"pytest>=9.1.1",
"ruff>=0.16.1",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
14 changes: 5 additions & 9 deletions tools/cve-tool-vulnogram/oauth-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,8 @@ addopts = "-ra -q"
testpaths = ["tests"]

[dependency-groups]
# Shared static-analysis + test toolchain, pinned to the same versions as the
# workspace root so every sub-project's own environment is self-contained.
# The workspace checks run each tool via `uv run --directory <member>
# --project . python -m <tool>` — see tools/dev/run-workspace-check.sh.
dev = [
"mypy>=2.3.0",
"pytest>=9.1.1",
"ruff>=0.16.1",
]
# The shared toolchain (mypy, pytest, ruff) comes from `magpie-dev`
# (tools/dev), declared once for the whole workspace. The checks run each tool
# via `uv run --directory <member> --project . python -m <tool>` — see
# tools/dev/run-workspace-check.sh.
dev = ["magpie-dev"]
42 changes: 41 additions & 1 deletion tools/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*

- [`tools/dev/`](#toolsdev)
- [The shared dev toolchain](#the-shared-dev-toolchain)
- [The scripts](#the-scripts)
- [Prerequisites](#prerequisites)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand All @@ -18,9 +20,47 @@

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.

## The shared dev toolchain

`tools/dev` is also the workspace's **toolchain project**, `magpie-dev`. It
declares ruff, mypy, and pytest as its dependencies, and every other workspace
member names `magpie-dev` in its own `[dependency-groups] dev` instead of
repeating the pins. Bump a version here and the whole workspace moves together.

Each member's environment stays self-contained — the checks still run
`uv run --directory <member> --project . python -m <tool>`, so no member depends
on tools leaking in from the root environment. Only the *declaration* is shared.

Why it changed: the pins used to be repeated in every member with an instruction
to keep them in lockstep. They had drifted into three different mypy floors, two
pytest floors, and two ruff floors, one member had no dev group at all, and
nothing detected any of it — the duplication was the bug, and the instruction to
keep it consistent was the workaround.

The project builds as a metadata-only wheel: it ships no importable module,
because the scripts are hyphenated and invoked by path, but it has to be
installable for other members to depend on it.

## The scripts

| Script | What it does |
|---|---|
| [`check-doc-sync.py`](check-doc-sync.py) | Guards the documentation claims that track the tree and rot silently: spec-index completeness (every `tools/spec-loop/specs/*.md` listed in **both** `overview.md` and `README.md`), the per-family skill counts in the root `README.md`, the per-mode counts in `docs/modes.md`'s *Modes at a glance* table, the bare catalogue totals in `docs/setup/marketplaces.md`, and that every script here is named in this file. |
| [`check-family-plugins.py`](check-family-plugins.py) | Validates the marketplace plugins against the skills' `family:` frontmatter — version parity across every ecosystem manifest, Agent Plugins 1.0 conformance, and one well-formed per-family plugin whose `skills/` symlinks match the family exactly. `--fix` regenerates them. |
| [`check-placeholders.sh`](check-placeholders.sh) | Fails the build on hardcoded project references in skill and tool docs, which must use `<PROJECT>` / `<project>` / `<tracker>` / `<upstream>` instead. Carries both casings and matches spaced variants. |
| [`check-workspace-members.py`](check-workspace-members.py) | Catches a new `tools/<name>/pyproject.toml` that was never added to `[tool.uv.workspace] members` — an omission that silently drops the tool from both the pre-commit hooks and the CI pytest matrix. Also verifies each member's tests actually run: both surfaces key off `[tool.pytest.ini_options]`, so a project can carry a full `tests/` directory and be executed by nothing. Reports tests-without-config, config-without-tests, and neither; `[tool.magpie.checks] skip = ["pytest"]` is the declared exemption. |
| [`run-workspace-check.sh`](run-workspace-check.sh) | Runs one static-check or test command across every workspace member, auto-discovering which members a given check applies to. The four `workspace-*` hooks call it, so adding a tool needs no edit to the pre-commit config. |
| [`add-license-headers.py`](add-license-headers.py) | Stamps the SPDX licence header into Markdown files that lack one. |
| [`agent-pre-commit.sh`](agent-pre-commit.sh) | Wrapper for `prek run --all-files`, for agent use. An agent running `pytest` / `ruff` / `mypy` individually still misses the rest of the CI gate (doctoc, markdownlint, typos, the checks above); this runs what CI runs. |

Each `check-doc-sync.py` check was added after the drift it catches had been
found by hand. None of them break anything when wrong, which is precisely why
they need a machine rather than a reviewer: they are numbers and index entries
a human has to remember to update while thinking about something else.

## Prerequisites

- **Runtime:** Bash + coreutils; `check-workspace-members.py` runs under `python3`.
- **Runtime:** Bash + coreutils; `check-workspace-members.py`, `check-family-plugins.py`, `check-doc-sync.py`, and `add-license-headers.py` run under `python3` (standard library only).
- **CLIs:** `uv` (the workspace checks run `uv run`), `git`, and `prek` (or `pre-commit`) — these scripts wire up the framework's hooks.
- **Credentials / auth:** None.
- **Network:** Local checks; `uv` may resolve workspace dependencies from PyPI (`pypi.org`, `files.pythonhosted.org`) on first sync.
15 changes: 3 additions & 12 deletions tools/dev/add-license-headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,7 @@
# semantically wrong and corrupt the eval. RAT excludes them too, so they
# need no header. Kept in the script (not just the prek `exclude:`) so a
# direct or `--all` invocation cannot stamp them either.
EXCLUDE_PREFIXES = (
"tools/skill-evals/evals/pr-management-code-review/"
"step-4-third-party-license/fixtures/",
)
EXCLUDE_PREFIXES = ("tools/skill-evals/evals/pr-management-code-review/step-4-third-party-license/fixtures/",)


def _already_stamped(lines: list[str]) -> bool:
Expand All @@ -99,10 +96,7 @@ def _already_stamped(lines: list[str]) -> bool:
SPDX) is not treated as a declaration, so such a file still gets a real
header.
"""
for line in lines[:_SCAN_LINES]:
if "SPDX-License-Identifier:" in line:
return True
return False
return any("SPDX-License-Identifier:" in line for line in lines[:_SCAN_LINES])


def stamp(path: Path) -> bool:
Expand All @@ -129,10 +123,7 @@ def _tracked_markdown() -> list[Path]:

def main(argv: list[str]) -> int:
args = argv[1:]
if "--all" in args:
targets = _tracked_markdown()
else:
targets = [Path(a) for a in args if a.endswith(".md")]
targets = _tracked_markdown() if "--all" in args else [Path(a) for a in args if a.endswith(".md")]

modified: list[Path] = []
for path in targets:
Expand Down
Loading