You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/09/08 02:41:55 UTC
(magpie) branch main updated: ci(dev): deterministic gates for doc/tree drift, test coverage, and a shared toolchain (#1164)
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/magpie.git
The following commit(s) were added to refs/heads/main by this push:
new 6e93c2d0 ci(dev): deterministic gates for doc/tree drift, test coverage, and a shared toolchain (#1164)
6e93c2d0 is described below
commit 6e93c2d07ee3b01c276fc5aae0545290ed70d313
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Tue Sep 8 04:41:49 2026 +0200
ci(dev): deterministic gates for doc/tree drift, test coverage, and a shared toolchain (#1164)
* ci(dev): add check-doc-sync — deterministic gates for doc/tree drift
This session fixed a batch of documentation that had drifted from the
tree: ten specs missing from both index files, per-family and per-mode
skill counts stale in three places, and catalogue totals stale in a
fourth. Every one was found by reading, not by a check. Left as is, they
come back.
check-doc-sync.py makes five of them mechanical:
- every tools/spec-loop/specs/*.md is listed in BOTH overview.md and
README.md;
- 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 parses 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.
Each check was verified to fail on the exact defect it exists for, by
reintroducing that defect and confirming the reported number matched what
had actually been wrong. A check that cannot go red is worse than no
check, since it reads as evidence — the lesson from #1161, where an
unauthenticated CLI produced a fully green eval run.
The fifth check found three undocumented scripts on its first run
(agent-pre-commit.sh, check-placeholders.sh, run-workspace-check.sh), so
tools/dev/README.md now carries a table saying what each of the seven
guards rather than naming a couple in passing.
The counts are deliberately narrow: the total-skills check reads an
allowlist of files rather than sweeping the repo, because plenty of docs
legitimately count a subset ("Nine skills cover the staged path") and a
greedy scan would flag those as drift forever.
Generated-by: Claude Code (Opus 5)
* test(dev): bring tools/dev under the workspace checks
The scripts in tools/dev are the gates that guard everyone else's
changes, and they were the one Python tree nothing checked: the
workspace checks iterate `[tool.uv.workspace] members`, and tools/dev
was not one. So ruff, mypy, and pytest all skipped it.
Declares the tree as a workspace member, the same config-carrier pattern
`skills/pyproject.toml` uses, and adds 20 tests for check-doc-sync.py.
The tests are written around the red case. Each check builds a miniature
repo in tmp_path and asserts the drift is reported with both numbers,
then that the corrected tree is silent — because a gate that cannot fail
is worse than no gate, it reads as evidence while measuring nothing.
Also covered: a spec listed in only one of the two indexes still counts
as missing, a README table row that names no family is not read as a
family with a wrong count, a mode nothing declares must say 0 rather
than be skipped, every stale total on a page is reported rather than
just the first, and the totals check stays inside its allowlist.
Script filenames are hyphenated, so the tests load the module through
importlib rather than a plain import.
Bringing the tree under the checks surfaced four lint findings in
pre-existing scripts; three are fixed (an unused unpacked variable, two
loops ruff can express directly) and RUF003 is ignored with a reason —
the repo's prose style uses en dashes throughout, and rewriting comments
to hyphens to satisfy a homoglyph check would make them worse. Four
files were reformatted. Every script was re-run afterwards to confirm
behaviour is unchanged.
Generated-by: Claude Code (Opus 5)
* refactor(workspace): share the dev toolchain via magpie-dev
Every workspace member repeated the same three pins in its own
`[dependency-groups] dev`, under an instruction in the root pyproject to
"keep the version bounds here and in every member in lockstep". They were
not in lockstep:
mypy >=2.1.0, >=2.3.0, >=1.10 (three floors)
pytest >=9.1.1, >=8.0 (two)
ruff >=0.16.1, >=0.16.2 (two)
tools/skill-reconciler-diff declared only pytest, and tools/fossil had no
dev group at all despite being ruff-checked — its checks ran against
whatever happened to be in the environment. Nothing detected any of it.
The duplication was the defect and the lockstep instruction was the
workaround.
tools/dev becomes `magpie-dev`, a metadata-only wheel whose dependencies
are the toolchain. Every other member's dev group becomes
`["magpie-dev"]`, with the workspace source declared once at the root.
Members with extras keep them (tools/gmail/oauth-draft retains mcp).
Bumping a version is now one edit and the whole workspace moves.
Each member's environment stays self-contained: the checks still run
`uv run --directory <member> --project . python -m <tool>`, so nothing
leaks in from the root. Only the declaration is shared.
Two properties checked rather than assumed. Check discovery keys off the
config sections (`[tool.ruff]`, `[tool.mypy]`,
`[tool.pytest.ini_options]`) and not the dev group, so giving a member
more tools does not silently enrol it in more checks. And the whole
battery was re-run after deleting every member `.venv`, not against a
warm cache: ruff 30/30, ruff-format 30/30, mypy 27/27, pytest 33/33.
One real behaviour change: tools/bitbucket moves from mypy>=1.10 /
pytest>=8.0 to the current floors. It passes the fresh sweep, but that is
a version bump rather than a no-op.
Generated-by: Claude Code (Opus 5)
* ci(dev): verify every workspace member's tests actually run
Both the CI pytest matrix and the workspace sweep are driven by the
presence of `[tool.pytest.ini_options]`. A project can therefore carry a
full tests/ directory and be executed by nothing: the job is simply never
emitted, so there is no red tick for anyone to notice. Nothing checked
this.
check-workspace-members.py now reports three shapes:
- tests on disk, no [tool.pytest.ini_options] → the tests never run;
- [tool.pytest.ini_options], no tests on disk → the CI job runs and
collects nothing, so its green tick proves nothing;
- neither → no tests at all.
A project that genuinely should not be tested declares it with
`[tool.magpie.checks] skip = ["pytest"]`, the same opt-out the sweep and
the CI matrix already honour — so an exemption is visible in the file
rather than being a silent absence from a matrix nobody reads.
All 33 members pass today, so this locks in the current state rather than
fixing a present gap. The hook now also re-fires on test files, since
adding or deleting tests changes the answer.
Ten tests cover it, including the false negative that would make the
whole check worthless: a test file vendored inside a member's .venv or
site-packages must not make an untested member look tested. Each of the
three failure shapes was also verified by hand against a real member,
and the declared opt-out confirmed to silence all three.
Generated-by: Claude Code (Opus 5)
---
.pre-commit-config.yaml | 46 ++-
ai-tutors/pyproject.toml | 14 +-
pyproject.toml | 27 +-
skills/pyproject.toml | 14 +-
tools/agent-guard/pyproject.toml | 14 +-
tools/agent-isolation/pyproject.toml | 14 +-
tools/bitbucket/pyproject.toml | 6 +-
.../generate-cve-json/pyproject.toml | 14 +-
tools/cve-tool-vulnogram/oauth-api/pyproject.toml | 14 +-
tools/dev/README.md | 42 ++-
tools/dev/add-license-headers.py | 15 +-
tools/dev/check-doc-sync.py | 224 ++++++++++++++
tools/dev/check-family-plugins.py | 33 +-
tools/dev/check-workspace-members.py | 93 +++++-
tools/dev/pyproject.toml | 89 ++++++
tools/dev/tests/test_check_doc_sync.py | 306 +++++++++++++++++++
tools/dev/tests/test_check_workspace_members.py | 135 +++++++++
tools/egress-gateway/pyproject.toml | 6 +-
tools/fossil/pyproject.toml | 6 +
tools/github-body-field/pyproject.toml | 14 +-
tools/github-rollup/pyproject.toml | 14 +-
tools/gmail/oauth-draft/pyproject.toml | 13 +-
tools/jira/pyproject.toml | 14 +-
tools/maildir/pyproject.toml | 14 +-
tools/permission-audit/pyproject.toml | 14 +-
tools/pilot-report-validator/pyproject.toml | 6 +-
tools/pr-management-stats/pyproject.toml | 14 +-
tools/preflight-audit/pyproject.toml | 14 +-
tools/privacy-llm/checker/pyproject.toml | 14 +-
tools/privacy-llm/redactor/pyproject.toml | 14 +-
tools/sandbox-lint/pyproject.toml | 14 +-
.../pyproject.toml | 6 +-
tools/skill-and-tool-validator/pyproject.toml | 6 +-
tools/skill-evals/pyproject.toml | 14 +-
tools/skill-reconciler-diff/pyproject.toml | 2 +-
tools/sourcehut/pyproject.toml | 6 +-
tools/spec-inventory/pyproject.toml | 6 +-
tools/spec-status-index/pyproject.toml | 14 +-
tools/spec-validator/pyproject.toml | 6 +-
tools/symlink-lint/pyproject.toml | 14 +-
tools/vcs/pyproject.toml | 14 +-
tools/vendor-neutrality-score/pyproject.toml | 14 +-
uv.lock | 334 ++++++---------------
43 files changed, 1168 insertions(+), 539 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 49973702..0b53b6aa 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -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
diff --git a/ai-tutors/pyproject.toml b/ai-tutors/pyproject.toml
index 66d70dbe..942514bd 100644
--- a/ai-tutors/pyproject.toml
+++ b/ai-tutors/pyproject.toml
@@ -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"]
diff --git a/pyproject.toml b/pyproject.toml
index 33c65ca6..169bf575 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,16 +41,20 @@ 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
@@ -58,9 +62,7 @@ requires-python = ">=3.11"
# 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]
@@ -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",
diff --git a/skills/pyproject.toml b/skills/pyproject.toml
index 12b64d47..5308937c 100644
--- a/skills/pyproject.toml
+++ b/skills/pyproject.toml
@@ -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"]
diff --git a/tools/agent-guard/pyproject.toml b/tools/agent-guard/pyproject.toml
index 15011420..7bc32ed7 100644
--- a/tools/agent-guard/pyproject.toml
+++ b/tools/agent-guard/pyproject.toml
@@ -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"]
diff --git a/tools/agent-isolation/pyproject.toml b/tools/agent-isolation/pyproject.toml
index de03c9c4..168759dd 100644
--- a/tools/agent-isolation/pyproject.toml
+++ b/tools/agent-isolation/pyproject.toml
@@ -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"]
diff --git a/tools/bitbucket/pyproject.toml b/tools/bitbucket/pyproject.toml
index bb6ae271..56654f66 100644
--- a/tools/bitbucket/pyproject.toml
+++ b/tools/bitbucket/pyproject.toml
@@ -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"]
diff --git a/tools/cve-tool-vulnogram/generate-cve-json/pyproject.toml b/tools/cve-tool-vulnogram/generate-cve-json/pyproject.toml
index 3686ccab..5a760b91 100644
--- a/tools/cve-tool-vulnogram/generate-cve-json/pyproject.toml
+++ b/tools/cve-tool-vulnogram/generate-cve-json/pyproject.toml
@@ -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"]
diff --git a/tools/cve-tool-vulnogram/oauth-api/pyproject.toml b/tools/cve-tool-vulnogram/oauth-api/pyproject.toml
index 81f03346..c1585986 100644
--- a/tools/cve-tool-vulnogram/oauth-api/pyproject.toml
+++ b/tools/cve-tool-vulnogram/oauth-api/pyproject.toml
@@ -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"]
diff --git a/tools/dev/README.md b/tools/dev/README.md
index d39df6ff..1ddaf7f5 100644
--- a/tools/dev/README.md
+++ b/tools/dev/README.md
@@ -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 -->
@@ -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 [...]
+| [`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.
diff --git a/tools/dev/add-license-headers.py b/tools/dev/add-license-headers.py
index cf1bf40f..464c2cfd 100755
--- a/tools/dev/add-license-headers.py
+++ b/tools/dev/add-license-headers.py
@@ -82,10 +82,7 @@ _SCAN_LINES = 12
# 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:
@@ -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:
@@ -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:
diff --git a/tools/dev/check-doc-sync.py b/tools/dev/check-doc-sync.py
new file mode 100755
index 00000000..fdc3dd02
--- /dev/null
+++ b/tools/dev/check-doc-sync.py
@@ -0,0 +1,224 @@
+#!/usr/bin/env python3
+# 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.
+
+"""Check the documentation claims that must track the tree, and silently rot.
+
+Four checks, all mechanical, each one written after the drift it catches was
+found by hand:
+
+1. **Spec-index completeness.** Every ``tools/spec-loop/specs/<name>.md`` is
+ listed in both ``overview.md`` and ``README.md``. Ten specs were absent from
+ both — including per-family specs and one added four PRs earlier — because
+ nothing checked, and an index that lists two-thirds of its subject reads
+ exactly like one that lists all of it.
+
+2. **Per-family skill counts in README.md.** The family table's "N skills" cell
+ against the live ``family:`` frontmatter. Two cells were wrong: one went
+ stale when three skills landed, the other had been drifting for months.
+
+3. **Per-mode skill counts in docs/modes.md.** The *Modes at a glance* table's
+ Skill-count column against the 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.
+
+4. **Total-skill claims in prose.** A small allowlist of files whose "N skills"
+ phrasing means the whole catalogue.
+
+5. **Every script in ``tools/dev/`` is named in its README.** These scripts are
+ the framework's own gates, and an undocumented one is invisible to the next
+ contributor who has to decide whether it applies to their change. Naming it
+ is the minimum; the README says what each guards.
+
+Why counting is worth a hook at all: every one of these is a number a human has
+to remember to update while thinking about something else, and none of them
+breaks anything when wrong. They just quietly mislead the next reader.
+
+Run from the repo root:
+
+ python3 tools/dev/check-doc-sync.py
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+SKILLS_DIR = Path("skills")
+SPECS_DIR = Path("tools/spec-loop/specs")
+SPEC_INDEXES = (SPECS_DIR / "overview.md", SPECS_DIR / "README.md")
+# The index files themselves are not specs.
+SPEC_INDEX_NAMES = {p.name for p in SPEC_INDEXES}
+
+# Files whose bare "N skills" phrasing means the whole catalogue. Deliberately
+# an allowlist rather than a repo-wide sweep: plenty of docs legitimately count
+# a subset ("Nine skills cover the staged path"), and a greedy scan would flag
+# those as drift forever.
+TOTAL_COUNT_FILES = (Path("docs/setup/marketplaces.md"),)
+
+# Dev scripts must each be named in tools/dev/README.md. Suffixes rather than a
+# mode check: a script is a script whether or not its executable bit survived a
+# checkout.
+DEV_DIR = Path("tools/dev")
+DEV_SCRIPT_SUFFIXES = (".py", ".sh")
+
+# `| [**security**](docs/security/README.md) | opt-in | … | 15 skills, [`docs/…`] |`
+_README_FAMILY_ROW = re.compile(
+ r"^\|\s*\[?\*\*(?P<family>[a-z-]+)\*\*\]?[^|]*\|.*?\|\s*(?P<count>\d+) skills?[,)]",
+)
+# `| **Triage** | *(Agentic Triage)* … | stable (…) | 35 |`
+_MODES_GLANCE_ROW = re.compile(r"^\|\s*\*\*(?P<mode>[A-Za-z ]+?)\*\*\s*\|.*\|\s*(?P<count>\d+)\s*\|\s*$")
+_BARE_TOTAL = re.compile(r"\b(?P<count>\d+) skills\b")
+
+
+def _frontmatter(path: Path) -> str:
+ text = path.read_text(encoding="utf-8")
+ m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
+ return m.group(1) if m else ""
+
+
+def _key_counts(key: str) -> dict[str, int]:
+ """Count live skills by a single-valued frontmatter key (``family``/``mode``)."""
+ counts: dict[str, int] = {}
+ for skill_md in sorted(SKILLS_DIR.glob("*/SKILL.md")):
+ m = re.search(rf"^{key}:\s*(\S+)\s*$", _frontmatter(skill_md), re.M)
+ if m:
+ counts[m.group(1)] = counts.get(m.group(1), 0) + 1
+ return counts
+
+
+def check_spec_index(errors: list[str]) -> None:
+ if not SPECS_DIR.is_dir():
+ return
+ index_text = {p: p.read_text(encoding="utf-8") for p in SPEC_INDEXES if p.is_file()}
+ for spec in sorted(SPECS_DIR.glob("*.md")):
+ if spec.name in SPEC_INDEX_NAMES:
+ continue
+ for index, text in index_text.items():
+ if f"({spec.name})" not in text:
+ errors.append(
+ f"{index}: spec '{spec.name}' is not listed — every spec belongs in both indexes"
+ )
+
+
+def check_readme_family_counts(errors: list[str]) -> None:
+ readme = Path("README.md")
+ if not readme.is_file():
+ return
+ live = _key_counts("family")
+ for lineno, line in enumerate(readme.read_text(encoding="utf-8").splitlines(), 1):
+ m = _README_FAMILY_ROW.match(line)
+ if not m:
+ continue
+ family, declared = m.group("family"), int(m.group("count"))
+ actual = live.get(family)
+ if actual is None:
+ continue # a table row that is not a skill family
+ if declared != actual:
+ errors.append(
+ f"README.md:{lineno}: family '{family}' says {declared} skills; "
+ f"live family: frontmatter has {actual}"
+ )
+
+
+def check_modes_glance_counts(errors: list[str]) -> None:
+ modes = Path("docs/modes.md")
+ if not modes.is_file():
+ return
+ text = modes.read_text(encoding="utf-8")
+ if "## Modes at a glance" not in text:
+ return
+ glance = text.split("## Modes at a glance", 1)[1].split("\n## ", 1)[0]
+ live = _key_counts("mode")
+ offset = text[: text.index("## Modes at a glance")].count("\n") + 1
+ for lineno, line in enumerate(glance.splitlines(), offset):
+ m = _MODES_GLANCE_ROW.match(line)
+ if not m:
+ continue
+ mode, declared = m.group("mode").strip(), int(m.group("count"))
+ actual = live.get(mode)
+ if actual is None:
+ # A mode with no skills (e.g. one deliberately switched off) must
+ # declare 0 rather than be skipped.
+ if declared != 0:
+ errors.append(
+ f"docs/modes.md:{lineno}: mode '{mode}' says {declared} skills; "
+ f"no skill declares that mode"
+ )
+ continue
+ if declared != actual:
+ errors.append(
+ f"docs/modes.md:{lineno}: mode '{mode}' says {declared} skills; "
+ f"live mode: frontmatter has {actual}"
+ )
+
+
+def check_total_counts(errors: list[str], total: int) -> None:
+ for path in TOTAL_COUNT_FILES:
+ if not path.is_file():
+ continue
+ for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
+ for m in _BARE_TOTAL.finditer(line):
+ declared = int(m.group("count"))
+ if declared != total:
+ errors.append(f"{path}:{lineno}: says {declared} skills; the catalogue has {total}")
+
+
+def check_dev_scripts_documented(errors: list[str]) -> None:
+ readme = DEV_DIR / "README.md"
+ if not DEV_DIR.is_dir() or not readme.is_file():
+ return
+ text = readme.read_text(encoding="utf-8")
+ for script in sorted(DEV_DIR.iterdir()):
+ if not script.is_file() or script.suffix not in DEV_SCRIPT_SUFFIXES:
+ continue
+ if script.name not in text:
+ errors.append(
+ f"{readme}: '{script.name}' is not named — every script in "
+ f"{DEV_DIR}/ must be documented there"
+ )
+
+
+def main() -> int:
+ if not SKILLS_DIR.is_dir():
+ print("check-doc-sync: run from the repository root", file=sys.stderr)
+ return 2
+ total = len(list(SKILLS_DIR.glob("*/SKILL.md")))
+ errors: list[str] = []
+ check_spec_index(errors)
+ check_readme_family_counts(errors)
+ check_modes_glance_counts(errors)
+ check_total_counts(errors, total)
+ check_dev_scripts_documented(errors)
+
+ if errors:
+ print("check-doc-sync: documentation is out of step with the tree.\n", file=sys.stderr)
+ for err in errors:
+ print(f" {err}", file=sys.stderr)
+ print(
+ f"\n{len(errors)} problem(s). These are counts and index entries a human has to "
+ "remember to update; nothing breaks when they are wrong, which is why they drift.",
+ file=sys.stderr,
+ )
+ return 1
+ print(f"check-doc-sync: OK ({total} skills; spec indexes, declared counts, and dev-script docs agree).")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/tools/dev/check-family-plugins.py b/tools/dev/check-family-plugins.py
index 128ec324..e29bc372 100644
--- a/tools/dev/check-family-plugins.py
+++ b/tools/dev/check-family-plugins.py
@@ -36,6 +36,7 @@ Run with `--fix` to propagate the version from `pyproject.toml` and regenerate
the family plugins + symlinks + marketplace entries from the frontmatter. A
release bump therefore has one edit point: `pyproject.toml`, then `--fix`.
"""
+
from __future__ import annotations
import argparse
@@ -140,9 +141,7 @@ def load_json(path: Path):
return None, f"{path}: cannot read/parse ({exc})"
-def validate_manifest(
- path: Path, expected_name: str, inherited: dict | None = None
-) -> list[str]:
+def validate_manifest(path: Path, expected_name: str, inherited: dict | None = None) -> list[str]:
"""A plugin.json must exist, be valid JSON, name itself correctly, and
declare its skills + a description. Per-family manifests additionally carry
the root manifest's shared metadata (`inherited`) verbatim."""
@@ -161,8 +160,7 @@ def validate_manifest(
for key, want in (inherited or {}).items():
if data.get(key) != want:
errors.append(
- f"{path}: {key!r} is {data.get(key)!r}, expected {want!r} "
- f"(inherited from {ROOT_MANIFEST})"
+ f"{path}: {key!r} is {data.get(key)!r}, expected {want!r} (inherited from {ROOT_MANIFEST})"
)
return errors
@@ -222,9 +220,7 @@ def check_ecosystem_versions(version: str) -> list[str]:
if err:
errors.append(err)
elif have != version:
- errors.append(
- f"{path}: version is {have!r}, expected {version!r} (from {PYPROJECT})"
- )
+ errors.append(f"{path}: version is {have!r}, expected {version!r} (from {PYPROJECT})")
return errors
@@ -362,7 +358,7 @@ def check(fam: dict[str, set[str]]) -> list[str]:
errors.append(f"{MARKETPLACE}: missing the all-in-one 'magpie' plugin entry")
if not HOOK_SCRIPT.is_file():
errors.append(f"{HOOK_SCRIPT}: missing (referenced by the all-in-one plugin's SessionStart hook)")
- root_data, root_err = load_json(ROOT_MANIFEST)
+ root_data, _root_err = load_json(ROOT_MANIFEST)
if root_data is not None and "check-upgrade.sh" not in json.dumps(root_data.get("hooks", {})):
errors.append(f"{ROOT_MANIFEST}: SessionStart hook does not reference hooks/check-upgrade.sh")
@@ -422,7 +418,7 @@ def check(fam: dict[str, set[str]]) -> list[str]:
# 4) No orphan plugin dirs (a magpie-<x> with no skills declaring family x).
for pdir in sorted(PLUGINS.glob("magpie-*")):
- family = pdir.name[len("magpie-"):]
+ family = pdir.name[len("magpie-") :]
if family not in fam:
errors.append(f"orphan plugin '{pdir.name}': no skill declares family '{family}'")
@@ -483,8 +479,7 @@ def fix(fam: dict[str, set[str]]) -> int:
for e in meta_errs:
print(f" - {e}", file=sys.stderr)
print(
- f"\nCannot regenerate: the family plugins inherit "
- f"{', '.join(INHERITED)} from {ROOT_MANIFEST}.",
+ f"\nCannot regenerate: the family plugins inherit {', '.join(INHERITED)} from {ROOT_MANIFEST}.",
file=sys.stderr,
)
return 1
@@ -537,12 +532,14 @@ def fix(fam: dict[str, set[str]]) -> int:
)
return 1
for family, skills in sorted(fam.items()):
- keep.append({
- "name": f"magpie-{family}",
- "source": f"./plugins/magpie-{family}",
- "version": shared["version"],
- "description": f"Apache Magpie {family} family ({len(skills)} skills).",
- })
+ keep.append(
+ {
+ "name": f"magpie-{family}",
+ "source": f"./plugins/magpie-{family}",
+ "version": shared["version"],
+ "description": f"Apache Magpie {family} family ({len(skills)} skills).",
+ }
+ )
market["plugins"] = keep
MARKETPLACE.write_text(json.dumps(market, indent=2) + "\n", encoding="utf-8")
print("Regenerated per-family plugins + marketplace entries from frontmatter.")
diff --git a/tools/dev/check-workspace-members.py b/tools/dev/check-workspace-members.py
index 3262c330..fef9eb95 100755
--- a/tools/dev/check-workspace-members.py
+++ b/tools/dev/check-workspace-members.py
@@ -30,6 +30,27 @@ Run as a prek hook on every pyproject.toml change. Exit code 0 if
the two sets agree; 1 otherwise, with a diff explaining what to add
or remove.
+It also checks that every declared member's tests actually run. The CI
+pytest matrix and the workspace pytest sweep are both driven by the
+presence of a `[tool.pytest.ini_options]` section, so a project can carry
+a full `tests/` directory and still never be executed by anything — the
+job simply is not emitted, and nobody sees a failure because nobody sees
+a run. Three ways that goes wrong, all reported here:
+
+ * tests on disk, no `[tool.pytest.ini_options]` — the tests never run;
+ * `[tool.pytest.ini_options]`, no tests on disk — the CI job runs and
+ collects nothing, so a green tick proves nothing;
+ * neither — the project has no tests at all.
+
+A project that genuinely should not be tested declares it, rather than
+being silently absent:
+
+ [tool.magpie.checks]
+ skip = ["pytest"]
+
+which is the same opt-out `tools/dev/run-workspace-check.sh` and the CI
+matrix already honour.
+
Scope: only `tools/*/pyproject.toml` and `tools/*/*/pyproject.toml`
(maxdepth-3). The root `pyproject.toml` and any deeper nested
pyprojects (e.g. inside `tests/` fixtures or vendored deps) are
@@ -93,12 +114,56 @@ def read_workspace_members() -> set[str]:
try:
return set(data["tool"]["uv"]["workspace"]["members"])
except KeyError:
- sys.stderr.write(
- "error: root pyproject.toml has no [tool.uv.workspace] members\n"
- )
+ sys.stderr.write("error: root pyproject.toml has no [tool.uv.workspace] members\n")
sys.exit(2)
+def has_test_files(member: Path) -> bool:
+ """True when the member carries at least one pytest-discoverable file.
+
+ Scans for both naming conventions and ignores anything inside a virtual
+ environment or an installed package, which would otherwise make a member
+ with no tests of its own look tested.
+ """
+ for pattern in ("test_*.py", "*_test.py"):
+ for found in member.rglob(pattern):
+ parts = set(found.parts)
+ if ".venv" in parts or "site-packages" in parts or "node_modules" in parts:
+ continue
+ return True
+ return False
+
+
+def check_tests_run(member_dirs: list[Path]) -> list[str]:
+ """Report members whose tests do not actually execute anywhere."""
+ problems: list[str] = []
+ for member in member_dirs:
+ rel = member.relative_to(ROOT)
+ with (member / "pyproject.toml").open("rb") as f:
+ tool = tomllib.load(f).get("tool", {})
+ if "pytest" in tool.get("magpie", {}).get("checks", {}).get("skip", []):
+ continue # deliberate, declared opt-out
+ configured = "ini_options" in tool.get("pytest", {})
+ present = has_test_files(member)
+ if present and not configured:
+ problems.append(
+ f"{rel}: has test files but no [tool.pytest.ini_options] — the CI "
+ f"matrix and the workspace pytest sweep are both driven by that "
+ f"section, so these tests never run"
+ )
+ elif configured and not present:
+ problems.append(
+ f"{rel}: declares [tool.pytest.ini_options] but has no test files — "
+ f"the CI job runs and collects nothing, so its green tick proves nothing"
+ )
+ elif not present and not configured:
+ problems.append(
+ f"{rel}: has no tests. Add them, or declare the exemption with "
+ f'[tool.magpie.checks] skip = ["pytest"]'
+ )
+ return problems
+
+
def main() -> int:
member_dirs = find_member_dirs()
declared = read_workspace_members()
@@ -106,18 +171,18 @@ def main() -> int:
missing = sorted(found_paths - declared)
stale = sorted(declared - found_paths)
+ # Only meaningful for members that are actually declared; an undeclared
+ # one is already reported above and its test wiring is moot until it is.
+ untested = check_tests_run([p for p in member_dirs if str(p.relative_to(ROOT)) in declared])
- if not missing and not stale:
+ if not missing and not stale and not untested:
return 0
out = sys.stderr.write
- out("error: uv workspace members list drifts from on-disk pyprojects\n")
+ out("error: workspace members are not wired the way CI assumes\n")
out("\n")
if missing:
- out(
- "Found `tools/.../pyproject.toml` that is NOT in "
- "`[tool.uv.workspace] members`:\n"
- )
+ out("Found `tools/.../pyproject.toml` that is NOT in `[tool.uv.workspace] members`:\n")
for p in missing:
out(f" + {p!r}\n")
out("\n")
@@ -128,14 +193,16 @@ def main() -> int:
"pytest matrix.\n\n"
)
if stale:
- out(
- "Workspace members list references paths that no longer "
- "have a pyproject.toml on disk:\n"
- )
+ out("Workspace members list references paths that no longer have a pyproject.toml on disk:\n")
for p in stale:
out(f" - {p!r}\n")
out("\n")
out("Remove each stale entry from the root pyproject.toml.\n\n")
+ if untested:
+ out("Workspace members whose tests do not run:\n")
+ for problem in untested:
+ out(f" ! {problem}\n")
+ out("\n")
return 1
diff --git a/tools/dev/pyproject.toml b/tools/dev/pyproject.toml
new file mode 100644
index 00000000..44e1b2e6
--- /dev/null
+++ b/tools/dev/pyproject.toml
@@ -0,0 +1,89 @@
+# 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.
+
+# Config carrier, not a package. `tools/dev/` is a directory of standalone
+# helper scripts invoked by prek and CI by path, never imported as a library.
+# Before this file existed the tree was invisible to ruff, mypy, and pytest:
+# the workspace checks iterate `[tool.uv.workspace] members`, and these gates
+# — the ones that guard everyone else's changes — were themselves unguarded.
+#
+# Declaring the tree as one workspace member is the same lightest-fix pattern
+# `skills/pyproject.toml` uses: `tools/dev/run-workspace-check.sh` auto-discovers
+# which checks apply from the sections below, and the CI matrix emits a
+# `pytest (dev)` job for any member with a `[tool.pytest.ini_options]` section.
+#
+# Script filenames are hyphenated (`check-doc-sync.py`), which is not an
+# importable module name, so tests load them through `importlib.util` rather
+# than a plain import.
+
+[project]
+name = "magpie-dev"
+version = "0.1.0"
+description = "Framework dev-loop helper scripts invoked by prek and CI."
+requires-python = ">=3.11"
+license = { text = "Apache-2.0" }
+# The shared static-analysis + test toolchain, declared once. Every other
+# workspace member depends on this project in its `dev` group instead of
+# repeating the three pins — see the note above `[project.dependencies]` in the
+# root pyproject.toml. The scripts themselves are stdlib-only; these are the
+# tools that check them and everything else.
+dependencies = [
+ "mypy>=2.3.0",
+ "pytest>=9.1.1",
+ "ruff>=0.16.2",
+]
+
+# Built as a metadata-only wheel: this project ships no importable module (the
+# scripts are hyphenated and invoked by path), but it has to be installable for
+# other members to depend on it.
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+bypass-selection = true
+
+[tool.ruff]
+line-length = 110
+target-version = "py311"
+
+[tool.ruff.lint]
+select = ["E", "W", "F", "I", "B", "UP", "SIM", "C4", "RUF"]
+ignore = [
+ "E501", # line-too-long — the 110-char limit above is already generous
+ "RUF003", # ambiguous en dash in a comment — the repo's prose style uses
+ # en dashes throughout; rewriting comments to hyphens to satisfy
+ # a homoglyph check would make them worse, and these files carry
+ # no non-ASCII identifiers, which is what the rule guards
+]
+
+[tool.mypy]
+python_version = "3.11"
+files = ["."]
+ignore_missing_imports = true
+warn_unused_ignores = true
+check_untyped_defs = true
+no_implicit_optional = true
+# Scripts invoked by path, not a typed library surface. `check_untyped_defs`
+# above still type-checks the bodies, which is where the value is.
+disallow_untyped_defs = false
+disallow_incomplete_defs = false
+
+[tool.pytest.ini_options]
+minversion = "8.0"
+addopts = "-ra -q"
+testpaths = ["tests"]
diff --git a/tools/dev/tests/test_check_doc_sync.py b/tools/dev/tests/test_check_doc_sync.py
new file mode 100644
index 00000000..13ce08d9
--- /dev/null
+++ b/tools/dev/tests/test_check_doc_sync.py
@@ -0,0 +1,306 @@
+# 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.
+
+"""Tests for ``check-doc-sync.py``.
+
+Every test builds a miniature repository in ``tmp_path`` and runs one check
+against it. The point of each is the **red** case: a gate that cannot fail is
+worse than no gate, because a green run reads as evidence while measuring
+nothing. So each check is exercised in both directions — drift is reported, and
+the corrected tree is silent.
+
+The script's filename is hyphenated, which is not an importable module name, so
+it is loaded through ``importlib.util``.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import os
+from collections.abc import Iterator
+from pathlib import Path
+from types import ModuleType
+
+import pytest
+
+_SCRIPT = Path(__file__).resolve().parents[1] / "check-doc-sync.py"
+
+
+def _load() -> ModuleType:
+ spec = importlib.util.spec_from_file_location("check_doc_sync", _SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+mod = _load()
+
+
+@pytest.fixture
+def repo(tmp_path: Path) -> Iterator[Path]:
+ """A miniature repo, cd'd into — the script resolves paths relative to cwd."""
+ (tmp_path / "skills").mkdir()
+ (tmp_path / "tools" / "spec-loop" / "specs").mkdir(parents=True)
+ (tmp_path / "tools" / "dev").mkdir(parents=True)
+ (tmp_path / "docs" / "setup").mkdir(parents=True)
+ prev = Path.cwd()
+ os.chdir(tmp_path)
+ try:
+ yield tmp_path
+ finally:
+ os.chdir(prev)
+
+
+def _skill(repo: Path, name: str, family: str, mode: str) -> None:
+ d = repo / "skills" / name
+ d.mkdir()
+ (d / "SKILL.md").write_text(
+ f"---\nname: magpie-{name}\nfamily: {family}\nmode: {mode}\n---\n\n# {name}\n",
+ encoding="utf-8",
+ )
+
+
+def _errors(fn, *args) -> list[str]:
+ errs: list[str] = []
+ fn(errs, *args)
+ return errs
+
+
+# ---------------------------------------------------------------------------
+# 1. Spec-index completeness
+# ---------------------------------------------------------------------------
+
+
+def _specs(repo: Path, names: list[str], listed_in_overview: list[str], listed_in_readme: list[str]) -> None:
+ specs = repo / "tools" / "spec-loop" / "specs"
+ for n in names:
+ (specs / n).write_text(f"# {n}\n", encoding="utf-8")
+ (specs / "overview.md").write_text(
+ "\n".join(f"| Area | [{n}]({n}) |" for n in listed_in_overview), encoding="utf-8"
+ )
+ (specs / "README.md").write_text(
+ "\n".join(f"- [`{n}`]({n})," for n in listed_in_readme), encoding="utf-8"
+ )
+
+
+def test_spec_listed_in_both_indexes_is_silent(repo: Path) -> None:
+ _specs(repo, ["adapters.md"], ["adapters.md"], ["adapters.md"])
+ assert _errors(mod.check_spec_index) == []
+
+
+def test_spec_missing_from_overview_is_reported(repo: Path) -> None:
+ _specs(repo, ["adapters.md"], [], ["adapters.md"])
+ errs = _errors(mod.check_spec_index)
+ assert len(errs) == 1
+ assert "overview.md" in errs[0] and "adapters.md" in errs[0]
+
+
+def test_spec_missing_from_readme_is_reported(repo: Path) -> None:
+ """Listed in one index is not listed. This is the real-world shape: ten
+ specs were in neither, and a spec in only one reads as indexed."""
+ _specs(repo, ["adapters.md"], ["adapters.md"], [])
+ errs = _errors(mod.check_spec_index)
+ assert len(errs) == 1
+ assert "README.md" in errs[0]
+
+
+def test_index_files_are_not_themselves_specs(repo: Path) -> None:
+ _specs(repo, [], [], [])
+ assert _errors(mod.check_spec_index) == []
+
+
+# ---------------------------------------------------------------------------
+# 2. Per-family counts in README.md
+# ---------------------------------------------------------------------------
+
+
+def _readme_family(repo: Path, family: str, declared: int) -> None:
+ (repo / "README.md").write_text(
+ "| Family | Type | Modes | Purpose | Detail |\n|---|---|---|---|---|\n"
+ f"| [**{family}**](docs/{family}/README.md) | opt-in | Triage | Does things. "
+ f"| {declared} skills, [`docs/{family}/`](docs/{family}/) |\n",
+ encoding="utf-8",
+ )
+
+
+def test_matching_family_count_is_silent(repo: Path) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _skill(repo, "b", "security", "Triage")
+ _readme_family(repo, "security", 2)
+ assert _errors(mod.check_readme_family_counts) == []
+
+
+def test_stale_family_count_is_reported_with_both_numbers(repo: Path) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _skill(repo, "b", "security", "Triage")
+ _skill(repo, "c", "security", "Drafting")
+ _readme_family(repo, "security", 2)
+ errs = _errors(mod.check_readme_family_counts)
+ assert len(errs) == 1
+ assert "says 2 skills" in errs[0] and "has 3" in errs[0]
+
+
+def test_table_row_that_is_not_a_family_is_ignored(repo: Path) -> None:
+ """The table carries rows whose bolded cell names no skill family. Those
+ must not be read as a family with a wrong count."""
+ _skill(repo, "a", "security", "Triage")
+ _readme_family(repo, "not-a-family", 99)
+ assert _errors(mod.check_readme_family_counts) == []
+
+
+# ---------------------------------------------------------------------------
+# 3. Per-mode counts in docs/modes.md
+# ---------------------------------------------------------------------------
+
+
+def _modes(repo: Path, rows: list[tuple[str, int]]) -> None:
+ body = "\n".join(f"| **{m}** | *(Agentic {m})* Does things. | stable | {n} |" for m, n in rows)
+ (repo / "docs" / "modes.md").write_text(
+ "# Modes\n\n## Modes at a glance\n\n"
+ "| Mode | Purpose | Status | Skill count |\n|---|---|---|---|\n" + body + "\n\n## Triage\n",
+ encoding="utf-8",
+ )
+
+
+def test_matching_mode_count_is_silent(repo: Path) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _modes(repo, [("Triage", 1)])
+ assert _errors(mod.check_modes_glance_counts) == []
+
+
+def test_stale_mode_count_is_reported(repo: Path) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _skill(repo, "b", "security", "Triage")
+ _modes(repo, [("Triage", 1)])
+ errs = _errors(mod.check_modes_glance_counts)
+ assert len(errs) == 1
+ assert "says 1 skills" in errs[0] and "has 2" in errs[0]
+
+
+def test_mode_with_no_skills_must_declare_zero(repo: Path) -> None:
+ """A deliberately-off mode declares 0. A non-zero count for a mode nothing
+ uses is drift, not an exemption."""
+ _skill(repo, "a", "security", "Triage")
+ _modes(repo, [("Triage", 1), ("Agentic Autonomous", 0)])
+ assert _errors(mod.check_modes_glance_counts) == []
+
+ _modes(repo, [("Triage", 1), ("Agentic Autonomous", 4)])
+ errs = _errors(mod.check_modes_glance_counts)
+ assert len(errs) == 1
+ assert "no skill declares that mode" in errs[0]
+
+
+def test_missing_glance_table_is_not_an_error(repo: Path) -> None:
+ (repo / "docs" / "modes.md").write_text("# Modes\n\nNo glance table here.\n", encoding="utf-8")
+ assert _errors(mod.check_modes_glance_counts) == []
+
+
+# ---------------------------------------------------------------------------
+# 4. Catalogue totals in prose
+# ---------------------------------------------------------------------------
+
+
+def test_matching_total_is_silent(repo: Path) -> None:
+ (repo / "docs" / "setup" / "marketplaces.md").write_text("Installs 2 skills.\n", encoding="utf-8")
+ assert _errors(mod.check_total_counts, 2) == []
+
+
+def test_every_stale_total_is_reported_not_just_the_first(repo: Path) -> None:
+ (repo / "docs" / "setup" / "marketplaces.md").write_text(
+ "Installs 71 skills.\nAll 71 skills load.\nThe 71 skills are namespaced.\n", encoding="utf-8"
+ )
+ errs = _errors(mod.check_total_counts, 74)
+ assert len(errs) == 3
+ assert all("says 71 skills" in e and "has 74" in e for e in errs)
+
+
+def test_totals_check_reads_only_the_allowlist(repo: Path) -> None:
+ """A doc outside the allowlist may legitimately count a subset — 'Nine
+ skills cover the staged path' — so a greedy scan would flag it forever."""
+ (repo / "docs" / "elsewhere.md").write_text("Nine skills cover the staged path.\n", encoding="utf-8")
+ assert _errors(mod.check_total_counts, 74) == []
+
+
+# ---------------------------------------------------------------------------
+# 5. Dev scripts are documented
+# ---------------------------------------------------------------------------
+
+
+def _dev(repo: Path, scripts: list[str], readme_names: list[str]) -> None:
+ dev = repo / "tools" / "dev"
+ for s in scripts:
+ (dev / s).write_text("#!/bin/sh\n", encoding="utf-8")
+ (dev / "README.md").write_text(
+ "\n".join(f"| [`{n}`]({n}) | does a thing |" for n in readme_names), encoding="utf-8"
+ )
+
+
+def test_documented_scripts_are_silent(repo: Path) -> None:
+ _dev(repo, ["check-a.py", "check-b.sh"], ["check-a.py", "check-b.sh"])
+ assert _errors(mod.check_dev_scripts_documented) == []
+
+
+def test_undocumented_script_is_reported(repo: Path) -> None:
+ _dev(repo, ["check-a.py", "check-b.sh"], ["check-a.py"])
+ errs = _errors(mod.check_dev_scripts_documented)
+ assert len(errs) == 1
+ assert "check-b.sh" in errs[0]
+
+
+def test_non_script_files_are_not_required_to_be_documented(repo: Path) -> None:
+ _dev(repo, [], [])
+ (repo / "tools" / "dev" / "notes.txt").write_text("scratch\n", encoding="utf-8")
+ (repo / "tools" / "dev" / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
+ assert _errors(mod.check_dev_scripts_documented) == []
+
+
+# ---------------------------------------------------------------------------
+# main()
+# ---------------------------------------------------------------------------
+
+
+def test_main_exits_2_outside_a_repo_root(repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ (repo / "skills").rmdir()
+ assert mod.main() == 2
+ assert "run from the repository root" in capsys.readouterr().err
+
+
+def test_main_exits_1_and_names_every_problem(repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _readme_family(repo, "security", 9)
+ _modes(repo, [("Triage", 9)])
+ _specs(repo, ["adapters.md"], [], [])
+ _dev(repo, ["check-x.sh"], [])
+ (repo / "docs" / "setup" / "marketplaces.md").write_text("Installs 9 skills.\n", encoding="utf-8")
+
+ assert mod.main() == 1
+ err = capsys.readouterr().err
+ for fragment in ("family 'security'", "mode 'Triage'", "adapters.md", "check-x.sh", "says 9 skills"):
+ assert fragment in err
+
+
+def test_main_exits_0_on_a_consistent_tree(repo: Path, capsys: pytest.CaptureFixture[str]) -> None:
+ _skill(repo, "a", "security", "Triage")
+ _readme_family(repo, "security", 1)
+ _modes(repo, [("Triage", 1)])
+ _specs(repo, ["adapters.md"], ["adapters.md"], ["adapters.md"])
+ _dev(repo, ["check-x.sh"], ["check-x.sh"])
+ (repo / "docs" / "setup" / "marketplaces.md").write_text("Installs 1 skills.\n", encoding="utf-8")
+
+ assert mod.main() == 0
+ assert "OK" in capsys.readouterr().out
diff --git a/tools/dev/tests/test_check_workspace_members.py b/tools/dev/tests/test_check_workspace_members.py
new file mode 100644
index 00000000..f8e1685a
--- /dev/null
+++ b/tools/dev/tests/test_check_workspace_members.py
@@ -0,0 +1,135 @@
+# 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.
+
+"""Tests for the test-coverage half of ``check-workspace-members.py``.
+
+The failure this guards is quiet by construction: the CI pytest matrix and the
+workspace sweep are both driven by the presence of ``[tool.pytest.ini_options]``,
+so a project can carry a full ``tests/`` directory and never be executed by
+anything. There is no red tick to notice — the job is simply never emitted.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+from pathlib import Path
+from types import ModuleType
+
+import pytest
+
+_SCRIPT = Path(__file__).resolve().parents[1] / "check-workspace-members.py"
+
+
+def _load() -> ModuleType:
+ spec = importlib.util.spec_from_file_location("check_workspace_members", _SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+mod = _load()
+
+
+def _member(root: Path, name: str, *, ini: bool, tests: bool, skip: bool = False) -> Path:
+ d = root / name
+ d.mkdir(parents=True)
+ body = '[project]\nname = "x"\nversion = "0.1.0"\n'
+ if ini:
+ body += '\n[tool.pytest.ini_options]\nminversion = "8.0"\n'
+ if skip:
+ body += '\n[tool.magpie.checks]\nskip = ["pytest"]\n'
+ (d / "pyproject.toml").write_text(body, encoding="utf-8")
+ if tests:
+ (d / "tests").mkdir()
+ (d / "tests" / "test_thing.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
+ return d
+
+
+@pytest.fixture
+def root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
+ monkeypatch.setattr(mod, "ROOT", tmp_path)
+ return tmp_path
+
+
+def test_tests_and_config_present_is_silent(root: Path) -> None:
+ m = _member(root, "good", ini=True, tests=True)
+ assert mod.check_tests_run([m]) == []
+
+
+def test_tests_without_config_are_reported(root: Path) -> None:
+ """The quiet failure: real tests that nothing ever executes."""
+ m = _member(root, "orphaned", ini=False, tests=True)
+ problems = mod.check_tests_run([m])
+ assert len(problems) == 1
+ assert "never run" in problems[0]
+
+
+def test_config_without_tests_is_reported(root: Path) -> None:
+ """The inverse: a green CI job that collected nothing."""
+ m = _member(root, "hollow", ini=True, tests=False)
+ problems = mod.check_tests_run([m])
+ assert len(problems) == 1
+ assert "collects nothing" in problems[0]
+
+
+def test_neither_is_reported(root: Path) -> None:
+ m = _member(root, "untested", ini=False, tests=False)
+ problems = mod.check_tests_run([m])
+ assert len(problems) == 1
+ assert "has no tests" in problems[0]
+
+
+def test_declared_opt_out_is_honoured(root: Path) -> None:
+ """A project that genuinely should not be tested says so, rather than
+ being silently absent from the matrix."""
+ m = _member(root, "config-carrier", ini=False, tests=False, skip=True)
+ assert mod.check_tests_run([m]) == []
+
+
+def test_opt_out_also_silences_the_other_two_shapes(root: Path) -> None:
+ assert mod.check_tests_run([_member(root, "a", ini=False, tests=True, skip=True)]) == []
+ assert mod.check_tests_run([_member(root, "b", ini=True, tests=False, skip=True)]) == []
+
+
+def test_alternative_test_naming_is_discovered(root: Path) -> None:
+ m = _member(root, "suffixed", ini=True, tests=False)
+ (m / "tests").mkdir()
+ (m / "tests" / "thing_test.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
+ assert mod.check_tests_run([m]) == []
+
+
+def test_vendored_tests_do_not_count_as_the_members_own(root: Path) -> None:
+ """A test file inside an installed package or a virtualenv would otherwise
+ make an untested member look tested — the exact false negative that makes
+ this check worthless."""
+ m = _member(root, "vendored", ini=True, tests=False)
+ vendored = m / ".venv" / "lib" / "python3.11" / "site-packages" / "dep" / "tests"
+ vendored.mkdir(parents=True)
+ (vendored / "test_dep.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
+ problems = mod.check_tests_run([m])
+ assert len(problems) == 1
+ assert "collects nothing" in problems[0]
+
+
+def test_every_member_is_reported_not_just_the_first(root: Path) -> None:
+ members = [
+ _member(root, "one", ini=False, tests=True),
+ _member(root, "two", ini=True, tests=False),
+ _member(root, "three", ini=False, tests=False),
+ ]
+ assert len(mod.check_tests_run(members)) == 3
diff --git a/tools/egress-gateway/pyproject.toml b/tools/egress-gateway/pyproject.toml
index d052a246..26b037d8 100644
--- a/tools/egress-gateway/pyproject.toml
+++ b/tools/egress-gateway/pyproject.toml
@@ -66,8 +66,4 @@ addopts = "-ra -q"
testpaths = ["tests"]
[dependency-groups]
-dev = [
- "mypy>=2.1.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
diff --git a/tools/fossil/pyproject.toml b/tools/fossil/pyproject.toml
index 7fae2426..58fe0d48 100644
--- a/tools/fossil/pyproject.toml
+++ b/tools/fossil/pyproject.toml
@@ -78,3 +78,9 @@ disallow_incomplete_defs = false
minversion = "8.0"
addopts = "-ra -q"
testpaths = ["tests"]
+
+[dependency-groups]
+# The shared toolchain comes from `magpie-dev` (tools/dev). This member had no
+# dev group at all, so its checks ran on whatever happened to be in the
+# environment.
+dev = ["magpie-dev"]
diff --git a/tools/github-body-field/pyproject.toml b/tools/github-body-field/pyproject.toml
index db102a94..8f3b0ee1 100644
--- a/tools/github-body-field/pyproject.toml
+++ b/tools/github-body-field/pyproject.toml
@@ -81,12 +81,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"]
diff --git a/tools/github-rollup/pyproject.toml b/tools/github-rollup/pyproject.toml
index e6adb423..391cbd8f 100644
--- a/tools/github-rollup/pyproject.toml
+++ b/tools/github-rollup/pyproject.toml
@@ -67,12 +67,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"]
diff --git a/tools/gmail/oauth-draft/pyproject.toml b/tools/gmail/oauth-draft/pyproject.toml
index b849770f..f43b9629 100644
--- a/tools/gmail/oauth-draft/pyproject.toml
+++ b/tools/gmail/oauth-draft/pyproject.toml
@@ -110,14 +110,11 @@ 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.
+# 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 = [
- "mypy>=2.3.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
- # Needed to import and test `oauth_draft.mcp_server` (the `mcp` extra).
+ "magpie-dev",
"mcp>=2.0.0",
]
diff --git a/tools/jira/pyproject.toml b/tools/jira/pyproject.toml
index 2c811acc..9d0d1ecc 100644
--- a/tools/jira/pyproject.toml
+++ b/tools/jira/pyproject.toml
@@ -50,12 +50,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"]
diff --git a/tools/maildir/pyproject.toml b/tools/maildir/pyproject.toml
index afe5b6d5..71601df4 100644
--- a/tools/maildir/pyproject.toml
+++ b/tools/maildir/pyproject.toml
@@ -80,12 +80,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"]
diff --git a/tools/permission-audit/pyproject.toml b/tools/permission-audit/pyproject.toml
index 934e6f9a..ff093ddd 100644
--- a/tools/permission-audit/pyproject.toml
+++ b/tools/permission-audit/pyproject.toml
@@ -70,12 +70,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"]
diff --git a/tools/pilot-report-validator/pyproject.toml b/tools/pilot-report-validator/pyproject.toml
index 7c8bab72..773928d4 100644
--- a/tools/pilot-report-validator/pyproject.toml
+++ b/tools/pilot-report-validator/pyproject.toml
@@ -47,11 +47,7 @@ ignore = ["E501"]
"tests/**" = ["B", "SIM"]
[dependency-groups]
-dev = [
- "mypy>=2.1.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
[tool.pytest.ini_options]
minversion = "8.0"
diff --git a/tools/pr-management-stats/pyproject.toml b/tools/pr-management-stats/pyproject.toml
index 8a176335..f9c149ab 100644
--- a/tools/pr-management-stats/pyproject.toml
+++ b/tools/pr-management-stats/pyproject.toml
@@ -37,12 +37,8 @@ testpaths = ["tests"]
pythonpath = [".", "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"]
diff --git a/tools/preflight-audit/pyproject.toml b/tools/preflight-audit/pyproject.toml
index 337c7307..623690d5 100644
--- a/tools/preflight-audit/pyproject.toml
+++ b/tools/preflight-audit/pyproject.toml
@@ -80,12 +80,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"]
diff --git a/tools/privacy-llm/checker/pyproject.toml b/tools/privacy-llm/checker/pyproject.toml
index d517ae9a..2dcbe89c 100644
--- a/tools/privacy-llm/checker/pyproject.toml
+++ b/tools/privacy-llm/checker/pyproject.toml
@@ -81,12 +81,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"]
diff --git a/tools/privacy-llm/redactor/pyproject.toml b/tools/privacy-llm/redactor/pyproject.toml
index caa14e3b..e69808fe 100644
--- a/tools/privacy-llm/redactor/pyproject.toml
+++ b/tools/privacy-llm/redactor/pyproject.toml
@@ -84,12 +84,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"]
diff --git a/tools/sandbox-lint/pyproject.toml b/tools/sandbox-lint/pyproject.toml
index 69d03667..050c18d4 100644
--- a/tools/sandbox-lint/pyproject.toml
+++ b/tools/sandbox-lint/pyproject.toml
@@ -81,12 +81,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"]
diff --git a/tools/security-tracker-stats-dashboard/pyproject.toml b/tools/security-tracker-stats-dashboard/pyproject.toml
index 57d47e52..620baa7c 100644
--- a/tools/security-tracker-stats-dashboard/pyproject.toml
+++ b/tools/security-tracker-stats-dashboard/pyproject.toml
@@ -52,8 +52,4 @@ addopts = "-ra -q"
testpaths = ["tests"]
[dependency-groups]
-dev = [
- "mypy>=2.1.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
diff --git a/tools/skill-and-tool-validator/pyproject.toml b/tools/skill-and-tool-validator/pyproject.toml
index 6fa33657..40262bb5 100644
--- a/tools/skill-and-tool-validator/pyproject.toml
+++ b/tools/skill-and-tool-validator/pyproject.toml
@@ -82,8 +82,4 @@ addopts = "-ra -q"
testpaths = ["tests"]
[dependency-groups]
-dev = [
- "mypy>=2.3.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
diff --git a/tools/skill-evals/pyproject.toml b/tools/skill-evals/pyproject.toml
index 7887bf9c..eb18e766 100644
--- a/tools/skill-evals/pyproject.toml
+++ b/tools/skill-evals/pyproject.toml
@@ -56,12 +56,8 @@ minversion = "8.0"
addopts = "-ra -q"
[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"]
diff --git a/tools/skill-reconciler-diff/pyproject.toml b/tools/skill-reconciler-diff/pyproject.toml
index 80269933..e121fcc9 100644
--- a/tools/skill-reconciler-diff/pyproject.toml
+++ b/tools/skill-reconciler-diff/pyproject.toml
@@ -35,7 +35,7 @@ skill-reconciler-diff = "skill_reconciler_diff:main"
packages = ["src/skill_reconciler_diff"]
[dependency-groups]
-dev = ["pytest>=8.0"]
+dev = ["magpie-dev"]
[tool.ruff]
line-length = 110
diff --git a/tools/sourcehut/pyproject.toml b/tools/sourcehut/pyproject.toml
index b670c1a0..edfd95ce 100644
--- a/tools/sourcehut/pyproject.toml
+++ b/tools/sourcehut/pyproject.toml
@@ -29,11 +29,7 @@ license = { text = "Apache-2.0" }
dependencies = []
[dependency-groups]
-dev = [
- "ruff>=0.16.1",
- "mypy>=2.3.0",
- "pytest>=9.1.1",
-]
+dev = ["magpie-dev"]
[project.scripts]
magpie-sourcehut = "magpie_sourcehut:main"
diff --git a/tools/spec-inventory/pyproject.toml b/tools/spec-inventory/pyproject.toml
index 600f3b85..4634eee7 100644
--- a/tools/spec-inventory/pyproject.toml
+++ b/tools/spec-inventory/pyproject.toml
@@ -64,11 +64,7 @@ disallow_untyped_defs = false
disallow_incomplete_defs = false
[dependency-groups]
-dev = [
- "mypy>=2.1.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
[tool.pytest.ini_options]
minversion = "8.0"
diff --git a/tools/spec-status-index/pyproject.toml b/tools/spec-status-index/pyproject.toml
index 76a6908a..232514f4 100644
--- a/tools/spec-status-index/pyproject.toml
+++ b/tools/spec-status-index/pyproject.toml
@@ -80,12 +80,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"]
diff --git a/tools/spec-validator/pyproject.toml b/tools/spec-validator/pyproject.toml
index 2df835b0..ade3373a 100644
--- a/tools/spec-validator/pyproject.toml
+++ b/tools/spec-validator/pyproject.toml
@@ -47,11 +47,7 @@ ignore = ["E501"]
"tests/**" = ["B", "SIM"]
[dependency-groups]
-dev = [
- "mypy>=2.1.0",
- "pytest>=9.1.1",
- "ruff>=0.16.1",
-]
+dev = ["magpie-dev"]
[tool.pytest.ini_options]
minversion = "8.0"
diff --git a/tools/symlink-lint/pyproject.toml b/tools/symlink-lint/pyproject.toml
index 784a535a..630562fd 100644
--- a/tools/symlink-lint/pyproject.toml
+++ b/tools/symlink-lint/pyproject.toml
@@ -81,12 +81,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"]
diff --git a/tools/vcs/pyproject.toml b/tools/vcs/pyproject.toml
index 55498003..f8197cf3 100644
--- a/tools/vcs/pyproject.toml
+++ b/tools/vcs/pyproject.toml
@@ -81,12 +81,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"]
diff --git a/tools/vendor-neutrality-score/pyproject.toml b/tools/vendor-neutrality-score/pyproject.toml
index 59de74cb..57d5702a 100644
--- a/tools/vendor-neutrality-score/pyproject.toml
+++ b/tools/vendor-neutrality-score/pyproject.toml
@@ -80,12 +80,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"]
diff --git a/uv.lock b/uv.lock
index eb346942..ec53ee5b 100644
--- a/uv.lock
+++ b/uv.lock
@@ -25,6 +25,7 @@ members = [
"github-rollup",
"jira-bridge",
"magpie-bitbucket",
+ "magpie-dev",
"magpie-fossil",
"magpie-maildir",
"magpie-skills",
@@ -56,19 +57,13 @@ source = { editable = "tools/agent-guard" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "agent-isolation"
@@ -77,19 +72,13 @@ source = { editable = "tools/agent-isolation" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "ai-tutors"
@@ -98,19 +87,13 @@ source = { virtual = "ai-tutors" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "annotated-types"
@@ -141,20 +124,16 @@ source = { virtual = "." }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
+ { name = "magpie-dev" },
{ name = "prek" },
- { name = "pytest" },
- { name = "ruff" },
]
[package.metadata]
[package.metadata.requires-dev]
dev = [
- { name = "mypy", specifier = ">=2.3.0" },
+ { name = "magpie-dev", editable = "tools/dev" },
{ name = "prek", specifier = ">=0.4.12" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
]
[[package]]
@@ -491,19 +470,13 @@ source = { editable = "tools/privacy-llm/checker" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "click"
@@ -592,20 +565,14 @@ dependencies = [
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
requires-dist = [{ name = "proxy-py", specifier = ">=2.4,<3" }]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "generate-cve-json"
@@ -614,19 +581,13 @@ source = { editable = "tools/cve-tool-vulnogram/generate-cve-json" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "github-body-field"
@@ -635,19 +596,13 @@ source = { editable = "tools/github-body-field" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "github-rollup"
@@ -656,19 +611,13 @@ source = { editable = "tools/github-rollup" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "google-auth"
@@ -769,19 +718,13 @@ source = { editable = "tools/jira" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "jsonschema"
@@ -933,18 +876,29 @@ source = { editable = "tools/bitbucket" }
[package.dev-dependencies]
dev = [
+ { name = "magpie-dev" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
+
+[[package]]
+name = "magpie-dev"
+version = "0.1.0"
+source = { editable = "tools/dev" }
+dependencies = [
{ name = "mypy" },
{ name = "pytest" },
{ name = "ruff" },
]
[package.metadata]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=1.10" },
- { name = "pytest", specifier = ">=8.0" },
- { name = "ruff", specifier = ">=0.16.1" },
+requires-dist = [
+ { name = "mypy", specifier = ">=2.3.0" },
+ { name = "pytest", specifier = ">=9.1.1" },
+ { name = "ruff", specifier = ">=0.16.2" },
]
[[package]]
@@ -952,6 +906,16 @@ name = "magpie-fossil"
version = "0.1.0"
source = { editable = "tools/fossil" }
+[package.dev-dependencies]
+dev = [
+ { name = "magpie-dev" },
+]
+
+[package.metadata]
+
+[package.metadata.requires-dev]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
+
[[package]]
name = "magpie-maildir"
version = "0.1.0"
@@ -959,19 +923,13 @@ source = { editable = "tools/maildir" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "magpie-skills"
@@ -980,19 +938,13 @@ source = { virtual = "skills" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.2" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "magpie-sourcehut"
@@ -1001,19 +953,13 @@ source = { editable = "tools/sourcehut" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "magpie-vcs"
@@ -1022,19 +968,13 @@ source = { editable = "tools/vcs" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "mcp"
@@ -1158,10 +1098,8 @@ mcp = [
[package.dev-dependencies]
dev = [
+ { name = "magpie-dev" },
{ name = "mcp" },
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
]
[package.metadata]
@@ -1173,10 +1111,8 @@ provides-extras = ["mcp"]
[package.metadata.requires-dev]
dev = [
+ { name = "magpie-dev", editable = "tools/dev" },
{ name = "mcp", specifier = ">=2.0.0" },
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
]
[[package]]
@@ -1225,19 +1161,13 @@ source = { editable = "tools/permission-audit" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "pilot-report-validator"
@@ -1246,19 +1176,13 @@ source = { editable = "tools/pilot-report-validator" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "pluggy"
@@ -1276,19 +1200,13 @@ source = { virtual = "tools/pr-management-stats" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "preflight-audit"
@@ -1297,19 +1215,13 @@ source = { editable = "tools/preflight-audit" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "prek"
@@ -1568,19 +1480,13 @@ source = { editable = "tools/privacy-llm/redactor" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "referencing"
@@ -1779,19 +1685,13 @@ source = { editable = "tools/sandbox-lint" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "security-tracker-stats-dashboard"
@@ -1800,19 +1700,13 @@ source = { editable = "tools/security-tracker-stats-dashboard" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "skill-and-tool-validator"
@@ -1821,19 +1715,13 @@ source = { editable = "tools/skill-and-tool-validator" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "skill-evals"
@@ -1842,19 +1730,13 @@ source = { editable = "tools/skill-evals" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "skill-reconciler-diff"
@@ -1863,13 +1745,13 @@ source = { editable = "tools/skill-reconciler-diff" }
[package.dev-dependencies]
dev = [
- { name = "pytest" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [{ name = "pytest", specifier = ">=8.0" }]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "spec-inventory"
@@ -1878,19 +1760,13 @@ source = { editable = "tools/spec-inventory" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "spec-status-index"
@@ -1899,19 +1775,13 @@ source = { editable = "tools/spec-status-index" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "spec-validator"
@@ -1920,19 +1790,13 @@ source = { editable = "tools/spec-validator" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "sse-starlette"
@@ -1967,19 +1831,13 @@ source = { editable = "tools/symlink-lint" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "truststore"
@@ -2040,19 +1898,13 @@ source = { editable = "tools/vendor-neutrality-score" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.1.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]
[[package]]
name = "vulnogram-api"
@@ -2061,16 +1913,10 @@ source = { editable = "tools/cve-tool-vulnogram/oauth-api" }
[package.dev-dependencies]
dev = [
- { name = "mypy" },
- { name = "pytest" },
- { name = "ruff" },
+ { name = "magpie-dev" },
]
[package.metadata]
[package.metadata.requires-dev]
-dev = [
- { name = "mypy", specifier = ">=2.3.0" },
- { name = "pytest", specifier = ">=9.1.1" },
- { name = "ruff", specifier = ">=0.16.1" },
-]
+dev = [{ name = "magpie-dev", editable = "tools/dev" }]