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" }]