diff --git a/pyproject.toml b/pyproject.toml index 82fca5352..e2959a4bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,6 +95,7 @@ package = false [tool.uv.workspace] members = [ "ai-tutors", + "skills", "tools/agent-guard", "tools/agent-isolation", "tools/bitbucket", diff --git a/skills/ci-runner-audit/scripts/scan_ci_runners.py b/skills/ci-runner-audit/scripts/scan_ci_runners.py index 45c9ad640..620b09112 100755 --- a/skills/ci-runner-audit/scripts/scan_ci_runners.py +++ b/skills/ci-runner-audit/scripts/scan_ci_runners.py @@ -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: @@ -50,12 +51,34 @@ } MACOS_ARM = {"macos-latest", "macos-14", "macos-15", "macos-26", "macos-13-xlarge"} -MACOS_X64 = {"macos-15-intel", "macos-26-intel", "macos-13", "macos-12", "macos-11", "macos-10.15", "macos-13-large"} +MACOS_X64 = { + "macos-15-intel", + "macos-26-intel", + "macos-13", + "macos-12", + "macos-11", + "macos-10.15", + "macos-13-large", +} MACOS_ANY = MACOS_ARM | MACOS_X64 -X64_TERMS = re.compile(r"(?i)(?:\bx64\b|\bx86_64\b|\bamd64\b|architecture:\s*['\"]?x64['\"]?|arch:\s*['\"]?(?:x64|x86_64|amd64)['\"]?)") -ARM_TERMS = re.compile(r"(?i)(?:\barm64\b|\baarch64\b|architecture:\s*['\"]?arm64['\"]?|arch:\s*['\"]?(?:arm64|aarch64)['\"]?)") -ARCH_KEYS = {"architecture", "arch", "target", "targets", "platform", "platforms", "os", "goarch", "node-arch"} +X64_TERMS = re.compile( + r"(?i)(?:\bx64\b|\bx86_64\b|\bamd64\b|architecture:\s*['\"]?x64['\"]?|arch:\s*['\"]?(?:x64|x86_64|amd64)['\"]?)" +) +ARM_TERMS = re.compile( + r"(?i)(?:\barm64\b|\baarch64\b|architecture:\s*['\"]?arm64['\"]?|arch:\s*['\"]?(?:arm64|aarch64)['\"]?)" +) +ARCH_KEYS = { + "architecture", + "arch", + "target", + "targets", + "platform", + "platforms", + "os", + "goarch", + "node-arch", +} def run(args: list[str]) -> str: @@ -93,14 +116,16 @@ def load_repos(cache_dir: Path, owner: str, refresh: bool) -> list[dict]: cache_dir.mkdir(parents=True, exist_ok=True) repo_file = cache_dir / f"{owner}-repos.jsonl" if refresh or not repo_file.exists(): - output = run([ - "gh", - "api", - "--paginate", - f"/orgs/{owner}/repos?per_page=100&type=public", - "--jq", - ".[] | select(.archived == false) | {full_name, default_branch}", - ]) + output = run( + [ + "gh", + "api", + "--paginate", + f"/orgs/{owner}/repos?per_page=100&type=public", + "--jq", + ".[] | select(.archived == false) | {full_name, default_branch}", + ] + ) repo_file.write_text(output, encoding="utf-8") return [json.loads(line) for line in repo_file.read_text(encoding="utf-8").splitlines() if line.strip()] @@ -139,13 +164,15 @@ def list_workflows_for_repo(repo: dict) -> list[dict]: for item in contents: path = item.get("path", "") if item.get("type") == "file" and re.search(r"\.ya?ml$", path): - workflows.append({ - "repo": full_name, - "branch": branch, - "path": path, - "url": item.get("download_url"), - "html_url": f"https://github.com/{full_name}/blob/{branch}/{path}", - }) + workflows.append( + { + "repo": full_name, + "branch": branch, + "path": path, + "url": item.get("download_url"), + "html_url": f"https://github.com/{full_name}/blob/{branch}/{path}", + } + ) return workflows @@ -160,7 +187,12 @@ def load_workflows(cache_dir: Path, owner: str, refresh: bool, workers: int) -> for future in as_completed(futures): workflows.extend(future.result()) with workflow_file.open("w", newline="", encoding="utf-8") as output: - writer = csv.DictWriter(output, delimiter="\t", fieldnames=["repo", "branch", "path", "url", "html_url"], lineterminator="\n") + writer = csv.DictWriter( + output, + delimiter="\t", + fieldnames=["repo", "branch", "path", "url", "html_url"], + lineterminator="\n", + ) writer.writeheader() writer.writerows(sorted(workflows, key=lambda row: (row["repo"], row["path"]))) with workflow_file.open(newline="", encoding="utf-8") as input_file: @@ -177,9 +209,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"])) @@ -199,17 +233,20 @@ 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") if isinstance(excludes, list): + def is_excluded(row: dict) -> bool: return any( - isinstance(item, dict) and all(str(row.get(k)).lower() == str(v).lower() for k, v in item.items()) + isinstance(item, dict) + and all(str(row.get(k)).lower() == str(v).lower() for k, v in item.items()) for item in excludes ) + rows = [row for row in rows if not is_excluded(row)] includes = matrix.get("include") @@ -295,24 +332,50 @@ def arch_hits(workflow: dict) -> list[dict]: if not isinstance(step, dict): continue step_if = str(step.get("if", "")).lower() - skip_non_macos_branch = any(token in step_if for token in [ - "runner.os == 'windows'", 'runner.os == "windows"', "matrix.os == 'windows", 'matrix.os == "windows', - "runner.os == 'linux'", 'runner.os == "linux"', "matrix.os == 'ubuntu", 'matrix.os == "ubuntu', - ]) + skip_non_macos_branch = any( + token in step_if + for token in [ + "runner.os == 'windows'", + 'runner.os == "windows"', + "matrix.os == 'windows", + 'matrix.os == "windows', + "runner.os == 'linux'", + 'runner.os == "linux"', + "matrix.os == 'ubuntu", + 'matrix.os == "ubuntu', + ] + ) if skip_non_macos_branch: 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)) if key_text in ARCH_KEYS or "arch" in key_text or "platform" in key_text: evidence = f"with.{key}={value}" if X64_TERMS.search(f"{key_text}: {value_text}"): - observed.append(("x64", name, uses, evidence, "setup-action" if uses.startswith("actions/setup-") else "action-input")) + observed.append( + ( + "x64", + name, + uses, + evidence, + "setup-action" if uses.startswith("actions/setup-") else "action-input", + ) + ) if ARM_TERMS.search(f"{key_text}: {value_text}"): - observed.append(("arm64", name, uses, evidence, "setup-action" if uses.startswith("actions/setup-") else "action-input")) + observed.append( + ( + "arm64", + name, + uses, + evidence, + "setup-action" if uses.startswith("actions/setup-") else "action-input", + ) + ) run_script = step.get("run") if isinstance(run_script, str): for line in run_script.splitlines(): @@ -326,18 +389,20 @@ def arch_hits(workflow: dict) -> list[dict]: for label, arch, matrix in contexts: for binary_arch, step_name, uses, evidence, confidence in observed: if arch and binary_arch != arch: - hits.append({ - **workflow, - "job": str(job_name), - "runner": label, - "runner_arch": arch, - "requested_arch": binary_arch, - "step": step_name, - "uses": uses, - "evidence": evidence, - "matrix": ",".join(f"{k}={v}" for k, v in matrix.items()), - "confidence": confidence, - }) + hits.append( + { + **workflow, + "job": str(job_name), + "runner": label, + "runner_arch": arch, + "requested_arch": binary_arch, + "step": step_name, + "uses": uses, + "evidence": evidence, + "matrix": ",".join(f"{k}={v}" for k, v in matrix.items()), + "confidence": confidence, + } + ) return hits @@ -347,13 +412,24 @@ def parallel_scan(workflows: list[dict], scanner, workers: int) -> list[dict]: futures = [executor.submit(scanner, workflow) for workflow in workflows if workflow.get("url")] for future in as_completed(futures): results.extend(future.result()) - return sorted(results, key=lambda row: (row.get("repo", ""), row.get("path", ""), row.get("job", ""), row.get("runner", ""), row.get("evidence", ""))) + return sorted( + results, + key=lambda row: ( + row.get("repo", ""), + row.get("path", ""), + row.get("job", ""), + row.get("runner", ""), + row.get("evidence", ""), + ), + ) def write_tsv(path: Path, rows: list[dict], fields: list[str]) -> None: path.parent.mkdir(parents=True, exist_ok=True) with path.open("w", newline="", encoding="utf-8") as output: - writer = csv.DictWriter(output, delimiter="\t", fieldnames=fields, extrasaction="ignore", lineterminator="\n") + writer = csv.DictWriter( + output, delimiter="\t", fieldnames=fields, extrasaction="ignore", lineterminator="\n" + ) writer.writeheader() writer.writerows(rows) @@ -362,7 +438,12 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("command", choices=["retired", "macos-arch", "all"]) parser.add_argument("--owner", default="apache") - parser.add_argument("--repo", action="append", default=[], help="Repository full name, e.g. apache/polaris. May be repeated.") + parser.add_argument( + "--repo", + action="append", + default=[], + help="Repository full name, e.g. apache/polaris. May be repeated.", + ) parser.add_argument("--repo-file", type=Path, help="File containing repository full names, one per line.") parser.add_argument("--scope-name", help="Output filename prefix for explicit repo/repo-file scans.") parser.add_argument("--cache-dir", type=Path, default=Path(".cache")) @@ -390,21 +471,64 @@ def main() -> int: if args.command in ("retired", "all"): retired = parallel_scan(workflows, retired_hits, args.workers) - write_tsv(args.out_dir / f"{prefix}-retired-gh-runners-confirmed.tsv", retired, ["repo", "path", "job", "runner", "html_url"]) + write_tsv( + args.out_dir / f"{prefix}-retired-gh-runners-confirmed.tsv", + retired, + ["repo", "path", "job", "runner", "html_url"], + ) print(f"retired_runner_hits={len(retired)}", file=sys.stderr) if args.command in ("macos-arch", "all"): arch = parallel_scan(workflows, arch_hits, args.workers) - write_tsv(args.out_dir / f"{prefix}-macos-arch-mismatch-candidates.tsv", arch, ["repo", "path", "job", "runner", "runner_arch", "requested_arch", "confidence", "step", "uses", "evidence", "matrix", "html_url"]) + write_tsv( + args.out_dir / f"{prefix}-macos-arch-mismatch-candidates.tsv", + arch, + [ + "repo", + "path", + "job", + "runner", + "runner_arch", + "requested_arch", + "confidence", + "step", + "uses", + "evidence", + "matrix", + "html_url", + ], + ) setup = [] seen = set() for row in arch: if row.get("confidence") == "setup-action": - key = (row.get("repo"), row.get("path"), row.get("job"), row.get("runner"), row.get("uses"), row.get("evidence")) + key = ( + row.get("repo"), + row.get("path"), + row.get("job"), + row.get("runner"), + row.get("uses"), + row.get("evidence"), + ) if key not in seen: seen.add(key) setup.append(row) - write_tsv(args.out_dir / f"{prefix}-macos-setup-action-arch-mismatches.tsv", setup, ["repo", "path", "job", "runner", "runner_arch", "requested_arch", "step", "uses", "evidence", "html_url"]) + write_tsv( + args.out_dir / f"{prefix}-macos-setup-action-arch-mismatches.tsv", + setup, + [ + "repo", + "path", + "job", + "runner", + "runner_arch", + "requested_arch", + "step", + "uses", + "evidence", + "html_url", + ], + ) print(f"macos_arch_candidates={len(arch)}", file=sys.stderr) print(f"setup_action_mismatches={len(setup)}", file=sys.stderr) diff --git a/skills/contributor-nomination/fetch.md b/skills/contributor-nomination/fetch.md index b5202208d..4dd4b54c0 100644 --- a/skills/contributor-nomination/fetch.md +++ b/skills/contributor-nomination/fetch.md @@ -199,7 +199,7 @@ calendar month to feed the activity timeline in ```python # Pseudocode — implement via jq or Python as convenient for event in all_events: - month = event["createdAt"][:7] # "YYYY-MM" + month = event["createdAt"][:7] # "YYYY-MM" buckets[month] += 1 ``` diff --git a/skills/contributor-sentiment/SKILL.md b/skills/contributor-sentiment/SKILL.md index ad05aacbc..682df2423 100644 --- a/skills/contributor-sentiment/SKILL.md +++ b/skills/contributor-sentiment/SKILL.md @@ -214,7 +214,7 @@ Aggregate counts per login. Compute Gini as: ```python sorted = sorted(counts) n = len(sorted) -gini = (2 * sum((i+1)*v for i,v in enumerate(sorted)) / (n * sum(sorted))) - (n+1)/n +gini = (2 * sum((i + 1) * v for i, v in enumerate(sorted)) / (n * sum(sorted))) - (n + 1) / n ``` Clamp to [0, 1]. If reviewer_count < 2, set `reviewer_load_gini: null` diff --git a/skills/issue-reproducer/verification.md b/skills/issue-reproducer/verification.md index ae85825a4..382061bf2 100644 --- a/skills/issue-reproducer/verification.md +++ b/skills/issue-reproducer/verification.md @@ -57,7 +57,7 @@ substring matching near common prefixes. **Bad:** ```python -if "xs" in output: # matches xsi too +if "xs" in output: # matches xsi too classified = "still-fails-same" ``` @@ -73,7 +73,8 @@ if (output.contains("foo")) { // matches foo-bar, foobar ```python import re -if re.search(r'\bxs="[^"]*"', output): # match only xs= attribute + +if re.search(r'\bxs="[^"]*"', output): # match only xs= attribute classified = "still-fails-same" ``` diff --git a/skills/list-skills/scripts/list_skills.py b/skills/list-skills/scripts/list_skills.py index 2592f331d..1111c9d04 100644 --- a/skills/list-skills/scripts/list_skills.py +++ b/skills/list-skills/scripts/list_skills.py @@ -109,10 +109,7 @@ def render(rows: list[tuple[str, str, str]], *, verbose: bool) -> str: else: lines.append(f" {name.ljust(width)} {desc}") lines.append("") - lines.append( - "Invoke a skill by typing /, or describe what " - "you want to do." - ) + lines.append("Invoke a skill by typing /, or describe what you want to do.") return "\n".join(lines) diff --git a/skills/pr-management-triage/scripts/pr_link.py b/skills/pr-management-triage/scripts/pr_link.py index f35f20953..397b95278 100644 --- a/skills/pr-management-triage/scripts/pr_link.py +++ b/skills/pr-management-triage/scripts/pr_link.py @@ -36,9 +36,7 @@ _REPOSITORY = re.compile(r"(?P[A-Za-z0-9_.-]+)/(?P[A-Za-z0-9_.-]+)\Z") -def parse_pr_reference( - reference: str, repository: str | None = None -) -> tuple[str, str]: +def parse_pr_reference(reference: str, repository: str | None = None) -> tuple[str, str]: """Return the display text and canonical URL for a GitHub PR reference.""" short_match = _SHORT_REFERENCE.fullmatch(reference) if short_match is not None: @@ -58,9 +56,7 @@ def parse_pr_reference( number = url_match.group("number") else: number_match = _NUMBER_REFERENCE.fullmatch(reference) - repository_match = ( - _REPOSITORY.fullmatch(repository) if repository is not None else None - ) + repository_match = _REPOSITORY.fullmatch(repository) if repository is not None else None if number_match is None or repository_match is None: raise ValueError( "expected OWNER/REPO#NUMBER, " @@ -101,9 +97,7 @@ def format_pr_reference( def main(argv: Sequence[str] | None = None) -> int: - parser = argparse.ArgumentParser( - description="Render GitHub pull-request references for terminal output." - ) + parser = argparse.ArgumentParser(description="Render GitHub pull-request references for terminal output.") parser.add_argument( "references", nargs="+", @@ -118,10 +112,7 @@ def main(argv: Sequence[str] | None = None) -> int: args = parser.parse_args(argv) try: - rendered = [ - format_pr_reference(reference, repository=args.repo) - for reference in args.references - ] + rendered = [format_pr_reference(reference, repository=args.repo) for reference in args.references] except ValueError as error: parser.error(str(error)) diff --git a/skills/pr-management-triage/tests/test_pr_link.py b/skills/pr-management-triage/tests/test_pr_link.py index 7005643ef..a3ea7f2a6 100644 --- a/skills/pr-management-triage/tests/test_pr_link.py +++ b/skills/pr-management-triage/tests/test_pr_link.py @@ -23,19 +23,16 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts")) -import pr_link # noqa: E402 +import pr_link class PrLinkTest(unittest.TestCase): def test_short_reference_uses_canonical_target(self) -> None: - rendered = pr_link.format_pr_reference( - "example/widget#66444", {"TERM": "xterm-256color"} - ) + rendered = pr_link.format_pr_reference("example/widget#66444", {"TERM": "xterm-256color"}) self.assertEqual( rendered, - "\033]8;;https://github.com/example/widget/pull/66444\033\\" - "example/widget#66444\033]8;;\033\\", + "\033]8;;https://github.com/example/widget/pull/66444\033\\example/widget#66444\033]8;;\033\\", ) def test_url_reference_is_normalised(self) -> None: @@ -59,8 +56,7 @@ def test_number_reference_uses_repository_context(self) -> None: self.assertEqual( rendered, - "\033]8;;https://github.com/example/widget/pull/66444\033\\" - "#66444\033]8;;\033\\", + "\033]8;;https://github.com/example/widget/pull/66444\033\\#66444\033]8;;\033\\", ) def test_no_color_uses_plain_text_fallback(self) -> None: diff --git a/skills/pyproject.toml b/skills/pyproject.toml new file mode 100644 index 000000000..c6562e571 --- /dev/null +++ b/skills/pyproject.toml @@ -0,0 +1,101 @@ +# 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/`, 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 + +[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//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 +# --project . python -m ` — see tools/dev/run-workspace-check.sh. +dev = [ + "mypy>=2.3.0", + "pytest>=9.1.1", + "ruff>=0.15.21", +] diff --git a/skills/setup-status/scripts/collect_status.py b/skills/setup-status/scripts/collect_status.py index 4e752e9f2..b6a0f97f9 100644 --- a/skills/setup-status/scripts/collect_status.py +++ b/skills/setup-status/scripts/collect_status.py @@ -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 @@ -86,7 +87,7 @@ def load_agent_targets() -> tuple[list[tuple[str, str, str, str]], str]: which holds in both the framework-source and snapshot layouts since the skill dir structure is identical in each. """ - agents_md = (Path(__file__).resolve().parent / ".." / ".." / "setup" / "agents.md") + agents_md = Path(__file__).resolve().parent / ".." / ".." / "setup" / "agents.md" try: text = agents_md.read_text(encoding="utf-8") except OSError: @@ -121,6 +122,7 @@ def load_agent_targets() -> tuple[list[tuple[str, str, str, str]], str]: return _FALLBACK_TARGETS, "fallback" return targets, "agents.md" + # Opt-in families the lock can record. Membership is read from each # skill's ``family:`` frontmatter key (see skills/setup/SKILL.md # Golden rule 8), NOT the name prefix — families like ``repo-health`` @@ -240,9 +242,7 @@ def collect_targets(root: Path, registry: list[tuple]) -> list[dict]: "entries": [], } if d.is_dir(): - entries = sorted( - p for p in d.iterdir() if p.name.startswith("magpie-") - ) + entries = sorted(p for p in d.iterdir() if p.name.startswith("magpie-")) rec["entries"] = [link_info(p, root) for p in entries] rec["magpie_count"] = len(rec["entries"]) rec["live_count"] = sum(1 for e in rec["entries"] if e["resolves"]) @@ -283,9 +283,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, @@ -299,7 +299,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, @@ -330,10 +330,7 @@ def override_dir_status(root: Path, dirname: str) -> dict: d = root / dirname if not d.is_dir(): return {"present": False, "has_readme": False, "skill_count": 0} - skill_files = [ - p for p in d.iterdir() - if p.is_file() and p.suffix == ".md" and p.name != "README.md" - ] + skill_files = [p for p in d.iterdir() if p.is_file() and p.suffix == ".md" and p.name != "README.md"] return { "present": True, "has_readme": (d / "README.md").is_file(), @@ -456,18 +453,12 @@ def render_markdown(d: dict) -> str: ov = d["overrides"] local_ov = d["local_overrides"] ov_text = f"present ({ov['skill_count']} skill(s))" if ov["present"] else "—" - local_ov_text = ( - f"present ({local_ov['skill_count']} skill(s))" - if local_ov["present"] - else "—" - ) + local_ov_text = f"present ({local_ov['skill_count']} skill(s))" if local_ov["present"] else "—" out.append( f"- **shared overrides** (`.apache-magpie-overrides/`): {ov_text} · " f"**personal overrides** (`.apache-magpie-local/`): {local_ov_text}" ) - out.append( - f"- **hook:** {'installed' if d['post_checkout_hook']['present'] else '—'}" - ) + out.append(f"- **hook:** {'installed' if d['post_checkout_hook']['present'] else '—'}") out.append("- → deep check (integrity, permissions, worktrees): `/magpie-setup verify`") return "\n".join(out) + "\n" diff --git a/skills/write-skill/scripts/init_skill.py b/skills/write-skill/scripts/init_skill.py index af46ce796..cbefa1b1d 100755 --- a/skills/write-skill/scripts/init_skill.py +++ b/skills/write-skill/scripts/init_skill.py @@ -302,8 +302,7 @@ def main(argv: list[str] | None = None) -> int: path = Path(args.path).expanduser().resolve() if path.exists() and not args.force and any(path.iterdir()): raise SystemExit( - f"{path} already exists and is non-empty; " - "use --force to overwrite, or pick a different --path." + f"{path} already exists and is non-empty; use --force to overwrite, or pick a different --path." ) path.mkdir(parents=True, exist_ok=True) diff --git a/uv.lock b/uv.lock index 4700afc34..5f67f445b 100644 --- a/uv.lock +++ b/uv.lock @@ -28,6 +28,7 @@ members = [ "magpie-bitbucket", "magpie-fossil", "magpie-maildir", + "magpie-skills", "magpie-sourcehut", "magpie-vcs", "oauth-draft", @@ -824,6 +825,27 @@ dev = [ { name = "ruff", specifier = ">=0.15.22" }, ] +[[package]] +name = "magpie-skills" +version = "0.1.0" +source = { virtual = "skills" } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=2.3.0" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.15.21" }, +] + [[package]] name = "magpie-sourcehut" version = "0.1.0"