What
Python under skills/ is not linted, type-checked, or tested by CI. The workspace checks that run ruff, mypy, and pytest iterate over uv workspace members and are filed under
^(tools/[^/]+(/[^/]+)?/(src|tests|pyproject\.toml)|pyproject\.toml|tools/dev/run-workspace-check\.sh)
skills/** matches none of that. There are eight Python files under skills/ today — helper scripts and guards:
skills/ci-runner-audit/scripts/scan_ci_runners.py
skills/list-skills/scripts/list_skills.py
skills/pr-management-triage/guards/mark_ready.py
skills/pr-management-triage/guards/mention.py
skills/pr-management-triage/scripts/pr_link.py
skills/security-issue-fix/guards/security_language.py
skills/setup-status/scripts/collect_status.py
skills/write-skill/scripts/init_skill.py
check-placeholders covers them (^(skills/.*|tools/.*)\.(md|sh|py|yaml|yml|toml)$), but that is a placeholder check, not lint, types, or tests.
Why it matters
#1049 added skills/pr-management-triage/tests/test_pr_link.py — the first test file anywhere under skills/ — alongside a 133-line pr_link.py. The tests pass locally (9 of them) but nothing in CI collects them. Reverting pr_link.py would leave the checks green.
Running ruff over those files by hand immediately finds a real, if trivial, hit: RUF100 Unused 'noqa' directive (unused: E402) at tests/test_pr_link.py:26. Exactly the class of thing the repo lints for everywhere else.
Two of the eight files are guards/ — mark_ready.py, mention.py, security_language.py — which exist specifically to stop an agent doing something unsafe. Uncovered guard code is the least comfortable place for this gap to sit.
This is the third instance of the same shape recently, and worth naming as a pattern rather than three incidents:
Different mechanisms, same failure mode: a check that looks present, reports nothing, and is believed.
Suggested fix
The ai-tutors remedy does not transfer. That worked because ai-tutors/ is a directory of files with one script, so a pyproject.toml plus workspace membership was natural. skills/ is different: skills are agent-facing markdown with helper scripts, symlinked into adopter repos. Making each skill a Python package would be heavy and would put build metadata into a tree that adopters symlink.
Two lighter options:
- One
pytest invocation over skills/ wired into the existing tests workflow and a prek hook — collecting skills/**/tests/ and skills/**/test_*.py, with ruff and mypy given the same paths. No per-skill packaging.
- A single
skills/pyproject.toml used only as a config carrier ([tool.ruff], [tool.mypy], [tool.pytest.ini_options], package = false) and added to the workspace, so the existing driver picks the tree up as one member. Closer to how the repo already works, at the cost of one metadata file inside skills/.
Option 1 keeps skills/ free of packaging metadata; option 2 reuses machinery that already exists and gets a pytest (skills) job for free. Either is fine — the important part is that something runs.
Note check-workspace-members will need to stay in step with whichever is chosen: it discovers members by scanning tools/ and top-level directories (widened in #1011), so a skills/ member would need to be discoverable or explicitly exempt.
What
Python under
skills/is not linted, type-checked, or tested by CI. The workspace checks that runruff,mypy, andpytestiterate over uv workspace members and are filed underskills/**matches none of that. There are eight Python files underskills/today — helper scripts and guards:check-placeholderscovers them (^(skills/.*|tools/.*)\.(md|sh|py|yaml|yml|toml)$), but that is a placeholder check, not lint, types, or tests.Why it matters
#1049 added
skills/pr-management-triage/tests/test_pr_link.py— the first test file anywhere underskills/— alongside a 133-linepr_link.py. The tests pass locally (9 of them) but nothing in CI collects them. Revertingpr_link.pywould leave the checks green.Running
ruffover those files by hand immediately finds a real, if trivial, hit:RUF100 Unused 'noqa' directive (unused: E402)attests/test_pr_link.py:26. Exactly the class of thing the repo lints for everywhere else.Two of the eight files are
guards/—mark_ready.py,mention.py,security_language.py— which exist specifically to stop an agent doing something unsafe. Uncovered guard code is the least comfortable place for this gap to sit.This is the third instance of the same shape recently, and worth naming as a pattern rather than three incidents:
ai-tutors/a uv workspace member.MANUALinstead of grading.Different mechanisms, same failure mode: a check that looks present, reports nothing, and is believed.
Suggested fix
The
ai-tutorsremedy does not transfer. That worked becauseai-tutors/is a directory of files with one script, so apyproject.tomlplus workspace membership was natural.skills/is different: skills are agent-facing markdown with helper scripts, symlinked into adopter repos. Making each skill a Python package would be heavy and would put build metadata into a tree that adopters symlink.Two lighter options:
pytestinvocation overskills/wired into the existing tests workflow and a prek hook — collectingskills/**/tests/andskills/**/test_*.py, withruffandmypygiven the same paths. No per-skill packaging.skills/pyproject.tomlused only as a config carrier ([tool.ruff],[tool.mypy],[tool.pytest.ini_options],package = false) and added to the workspace, so the existing driver picks the tree up as one member. Closer to how the repo already works, at the cost of one metadata file insideskills/.Option 1 keeps
skills/free of packaging metadata; option 2 reuses machinery that already exists and gets apytest (skills)job for free. Either is fine — the important part is that something runs.Note
check-workspace-memberswill need to stay in step with whichever is chosen: it discovers members by scanningtools/and top-level directories (widened in #1011), so askills/member would need to be discoverable or explicitly exempt.