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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ package = false
[tool.uv.workspace]
members = [
"ai-tutors",
"skills",
"tools/agent-guard",
"tools/agent-isolation",
"tools/bitbucket",
Expand Down
18 changes: 11 additions & 7 deletions skills/ci-runner-audit/scripts/scan_ci_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@
import re
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any
from urllib.request import urlopen

try:
Expand Down Expand Up @@ -177,9 +178,11 @@ def load_workflows_for_repos(repo_names: list[str], workers: int) -> list[dict]:
repos.append(repo)
workflows: list[dict] = []
with ThreadPoolExecutor(max_workers=workers) as executor:
futures = [executor.submit(list_workflows_for_repo, repo) for repo in repos]
for future in as_completed(futures):
workflows.extend(future.result())
workflow_futures: list[Future[list[dict]]] = [
executor.submit(list_workflows_for_repo, repo) for repo in repos
]
for workflow_future in as_completed(workflow_futures):
workflows.extend(workflow_future.result())
return sorted(workflows, key=lambda row: (row["repo"], row["path"]))


Expand All @@ -199,8 +202,8 @@ def matrix_rows(matrix: object) -> list[dict]:
continue
keys.append(str(key))
values.append(value if isinstance(value, list) else [value])
rows = [{}]
for key, vals in zip(keys, values):
rows: list[dict[str, Any]] = [{}]
for key, vals in zip(keys, values, strict=True):
rows = [{**row, key: val} for row in rows for val in vals]

excludes = matrix.get("exclude")
Expand Down Expand Up @@ -303,7 +306,8 @@ def arch_hits(workflow: dict) -> list[dict]:
continue
name = str(step.get("name", ""))
uses = str(step.get("uses", ""))
action_inputs = step.get("with") if isinstance(step.get("with"), dict) else {}
raw_with = step.get("with")
action_inputs = raw_with if isinstance(raw_with, dict) else {}
for key, value in action_inputs.items():
key_text = str(key).lower()
value_text = " ".join(lower_values(value))
Expand Down
2 changes: 1 addition & 1 deletion skills/pr-management-triage/tests/test_pr_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))

import pr_link # noqa: E402
import pr_link


class PrLinkTest(unittest.TestCase):
Expand Down
111 changes: 111 additions & 0 deletions skills/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# 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. `skills/` is a tree of agent-facing
# SKILL.md files; a handful of them ship helper `scripts/` and `guards/`
# written in Python. Before this file existed nothing linted, type-checked,
# or ran them: the workspace checks iterate over `[tool.uv.workspace]
# members` and every member lived under `tools/`, so `skills/**` was
# invisible to ruff, mypy, and pytest alike.
#
# Declaring the tree as one workspace member is the lightest fix that reuses
# the existing machinery — `tools/dev/run-workspace-check.sh` auto-discovers
# which checks apply from the sections below, and `.github/workflows/tests.yml`
# emits a `pytest (skills)` job for any member with a
# `[tool.pytest.ini_options]` section. Per-skill packaging was the alternative
# and is far heavier: skills are symlinked into adopter repos one directory at
# a time, so build metadata inside each of them would leak into every adopter.
# This file is not symlinked — the relays point at `skills/<name>`, never at
# `skills/` itself.

[project]
name = "magpie-skills"
version = "0.1.0"
description = "Helper scripts and guards shipped alongside the agent-facing skill definitions in skills/."
requires-python = ">=3.11"
license = { text = "Apache-2.0" }
# stdlib-only — the helper scripts deliberately carry no runtime dependencies
# so an adopter can run them without installing anything.
dependencies = []

# Not an installable package: these are scripts invoked by path from a skill,
# not a library anything imports.
[tool.uv]
package = false

# `ruff format` is deliberately skipped for now. The formatter wants to reflow
# roughly 280 lines across six pre-existing scripts — long set literals, long
# regex constants — none of which this change touches. Folding that churn in
# here would bury the point of the change (turning the checks on) behind a
# mechanical reformat nobody can review line by line. Lint, types, and tests
# all run; formatting should land as its own commit, after which this skip can
# be deleted.
[tool.magpie.checks]
skip = ["ruff-format"]

[tool.ruff]
line-length = 110
target-version = "py311"

[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"UP", # pyupgrade
"SIM", # flake8-simplify
"C4", # flake8-comprehensions
"RUF", # ruff-specific
]
ignore = [
"E501", # line-too-long — the 110-char limit above is already generous
]

[tool.mypy]
python_version = "3.11"
files = ["."]
ignore_missing_imports = true
warn_unused_ignores = true
warn_redundant_casts = true
check_untyped_defs = true
no_implicit_optional = true
# Helper scripts and their tests are plain functions invoked by path, not a
# typed library surface, so the annotation requirements the `tools/` members
# apply would be noise here. `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"
# Collected from the whole tree: tests live next to the skill they cover
# (`skills/<name>/tests/`) rather than in one central directory.
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.15.21",
]
9 changes: 5 additions & 4 deletions skills/setup-status/scripts/collect_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import subprocess
import sys
from pathlib import Path
from typing import Any

# The agent-target registry is owned by skills/setup/agents.md
# ("## The registry") — the single source of truth. At runtime the
Expand Down Expand Up @@ -283,9 +284,9 @@ def compute_drift(committed: dict | None, local: dict | None) -> dict:
("ref", committed.get("ref"), local.get("source_ref")),
]
mismatches = [
{"field": f, "committed": c, "local": l}
for f, c, l in pairs
if c is not None and l is not None and c != l
{"field": field, "committed": committed, "local": local}
for field, committed, local in pairs
if committed is not None and local is not None and committed != local
]
return {
"checked": True,
Expand All @@ -299,7 +300,7 @@ def gitignore_coverage(root: Path, targets: list[dict]) -> dict:
gi = root / ".gitignore"
text = gi.read_text(encoding="utf-8") if gi.is_file() else ""
lines = {ln.strip() for ln in text.splitlines()}
cov = {
cov: dict[str, Any] = {
"present": gi.is_file(),
"snapshot_ignored": "/.apache-magpie/" in lines,
"local_lock_ignored": "/.apache-magpie.local.lock" in lines,
Expand Down
22 changes: 22 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading