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/09 21:53:26 UTC
(magpie) branch main updated: fix(list-skills): discover every installed skill, drop the PyYAML import (#1187)
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 826e13da fix(list-skills): discover every installed skill, drop the PyYAML import (#1187)
826e13da is described below
commit 826e13da1abdb981dcd94182ffce3e7c7eb68bd2
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Wed Sep 9 23:53:20 2026 +0200
fix(list-skills): discover every installed skill, drop the PyYAML import (#1187)
* fix(list-skills): discover every installed skill, drop PyYAML
`list-skills` resolved its skill set from `Path(__file__)`, so under a
per-family marketplace plugin install it listed only the family that
happens to ship it — 5 skills out of 74. It also derived each skill's
family from the name prefix, against Golden rule 8, which invented
families like `write/` and `optimize/` for skills declaring `utilities`
and split `repo-health` and `contributor-growth` across headings.
Discovery is now installation-aware and repository-first: the agent
target directories an install writes into, the framework's own `skills/`
when the repository is the framework checkout, and the sibling plugins in
the marketplace cache when the script runs from one. Rows de-duplicate by
the name a person would type, so relays collapse while a skill reachable
from two install methods keeps both. Grouping reads the `family:`
frontmatter key; a skill declaring none lands in `other`.
The script was also the only one under `skills/` importing a third-party
module, against the stdlib-only contract in `skills/pyproject.toml`, and
crashed with `ModuleNotFoundError: No module named 'yaml'` on any
interpreter without PyYAML — including every plugin install. Frontmatter
is now parsed with the standard library, as `collect_status.py` and the
validator already do, and PEP 723 inline metadata states the contract so
`uv run --script` and a bare `python3` behave identically.
Generated-by: Claude Code (Opus 5)
* fix(list-skills): keep Step 1's command literal so the evals can pin it
The previous commit rewrote Step 1 to use the `<framework>` placeholder.
Models resolve that to an absolute path, which is machine-specific and
cannot be pinned in an eval fixture, so step-1-command cases 1-4 failed:
expected `<framework>/skills/…`, got `/Users/…/skills/…`.
Step 1 goes back to the literal repository-relative
`.claude/skills/magpie-list-skills/scripts/list_skills.py`, with an
explicit instruction not to expand it, and the four fixtures return to
their upstream values — they never needed to change. The marketplace
exception moves below the primary command and keeps its own case, which
passed throughout.
Generated-by: Claude Code (Opus 5)
---
skills/list-skills/SKILL.md | 108 +++++--
skills/list-skills/scripts/list_skills.py | 314 ++++++++++++++++-----
skills/list-skills/tests/test_list_skills.py | 254 +++++++++++++++++
tools/skill-evals/README.md | 2 +-
tools/skill-evals/evals/list-skills/README.md | 12 +-
.../case-5-marketplace-install/case-meta.json | 6 +
.../case-5-marketplace-install/expected.json | 4 +
.../fixtures/case-5-marketplace-install/report.md | 9 +
.../fixtures/case-1-standard-output/report.md | 80 +++---
.../case-2-summary-request-verbatim/report.md | 80 +++---
.../case-3-filter-request-verbatim/report.md | 80 +++---
tools/spec-loop/specs/meta-and-quality-tooling.md | 21 +-
12 files changed, 753 insertions(+), 217 deletions(-)
diff --git a/skills/list-skills/SKILL.md b/skills/list-skills/SKILL.md
index 667b0b4e..a7000408 100644
--- a/skills/list-skills/SKILL.md
+++ b/skills/list-skills/SKILL.md
@@ -5,12 +5,15 @@ name: magpie-list-skills
family: utilities
mode: Meta
description: |
- Print a human-readable index of every skill in this repository,
- grouped by family prefix (`pr-management`, `security`, `setup`,
- …) with each skill's name and the first sentence of its
- `description`. The listing is generated on every run from the
- live `.claude/skills/*/SKILL.md` files, so it never goes stale
- when skills are added, removed, or rewritten.
+ Print a human-readable index of every skill installed for this
+ repository, grouped by the family each one declares, with the
+ name to invoke it by and the first sentence of its
+ `description`. Discovery is installation-aware: it covers a
+ pinned snapshot install, the framework checkout, and
+ marketplace plugin installs, so the index matches what the
+ agent can actually run. Generated on every run from live
+ `SKILL.md` frontmatter, so it never goes stale when skills are
+ added, removed, or rewritten.
when_to_use: |
Invoke when a human asks *"what skills are available"*, *"list
the skills"*, *"show me the skills in this repo"*, *"give me a
@@ -35,20 +38,34 @@ license: Apache-2.0
# list-skills
-Print a human-readable index of the skills in this repository.
-The index is generated on every run from the live
-`.claude/skills/*/SKILL.md` files — there is no cached copy to
-keep in sync. The skill exists for humans (newcomers reading the
-repo, maintainers checking what is available); agents route
-invocations via the same frontmatter the script reads, so this
-skill is purely informational.
+Print a human-readable index of the skills installed for this
+repository. The index is generated on every run from live
+`SKILL.md` frontmatter — there is no cached copy to keep in sync.
+The skill exists for humans (newcomers reading the repo,
+maintainers checking what is available); agents route invocations
+via the same frontmatter the script reads, so this skill is
+purely informational.
+
+What counts as "installed" depends on how Magpie was put in
+place, so the script covers all three shapes and labels which one
+each entry came from:
+
+| Install | Where the skills live | Invocation shown |
+|---|---|---|
+| Pinned snapshot | `.agents/skills/` plus the per-agent relays beside it | `/magpie-<skill>` |
+| Framework checkout | the repo's own `skills/` | `/magpie-<skill>` |
+| Marketplace plugin | the plugin cache this script runs from, and its sibling plugins | `/<plugin>:<skill>` |
---
## Prerequisites
-- Python 3.9+ on `PATH` with `PyYAML` importable. The framework's
- Python toolchain already meets this; no extra setup.
+- Python 3.11+ on `PATH`. Nothing else — the script is
+ stdlib-only, as `skills/pyproject.toml` requires of every
+ helper script in this tree, and declares that contract in
+ [PEP 723](https://peps.python.org/pep-0723/) inline metadata.
+ `uv run --script` and a bare `python3` therefore behave
+ identically.
---
@@ -61,26 +78,54 @@ verbatim:
python3 .claude/skills/magpie-list-skills/scripts/list_skills.py
```
+Run that command **literally**, as written — do not expand it to
+an absolute path. It is a repository-relative path that resolves
+under both install methods that put skills in the repository: a
+pinned snapshot install and the framework checkout both carry
+`.claude/skills/magpie-list-skills` as a symlink onto the real
+skill directory.
+
For a layout that puts each description on its own indented line
-(easier to read when descriptions are long), pass `--verbose`:
+(easier to read when descriptions are long), pass `--verbose`; to
+inspect a repository other than the enclosing one, pass `--root`:
```bash
python3 .claude/skills/magpie-list-skills/scripts/list_skills.py --verbose
+python3 .claude/skills/magpie-list-skills/scripts/list_skills.py --root /path/to/repo
```
-The script:
+**Marketplace installs are the one exception.** They write nothing
+into the repository, so that path does not exist — the skill lives
+in the plugin cache. Build the command from the base directory
+reported for this skill instead:
-- walks `.claude/skills/*/SKILL.md` relative to its own location;
-- parses each skill's YAML frontmatter for `name` + `description`;
-- groups skills by family prefix (the first hyphen-separated
- token, with `pr-management` recognised as a two-token family —
- see [`KNOWN_TWO_TOKEN_FAMILIES`](scripts/list_skills.py));
-- prints each skill with the first sentence of its description.
+```bash
+python3 <the base directory reported for this skill>/scripts/list_skills.py
+```
+
+The script:
-When a new multi-token family appears (e.g. a hypothetical
-`docs-build-*`), add the prefix to `KNOWN_TWO_TOKEN_FAMILIES` in
-[`scripts/list_skills.py`](scripts/list_skills.py); otherwise the
-new skills land under the single-token head.
+- resolves the repository from `git rev-parse --show-toplevel`
+ (or `--root`), **not** from its own location — under a
+ per-family plugin install its own location is one family, not
+ the whole install;
+- walks the agent-target directories that install writes into
+ (`.agents/skills/` and its relays — the registry in
+ [`../setup/agents.md`](../setup/agents.md) is the source of
+ truth), the framework's own `skills/` when the repo is the
+ framework checkout, and the sibling plugins in the marketplace
+ cache when it is running from one;
+- de-duplicates by the name you would type, so relay directories
+ collapse to one entry while a skill available from *two*
+ install methods keeps both — they are two different things to
+ type;
+- groups by each skill's declared `family:` frontmatter key, per
+ Golden rule 8. Family is **never** inferred from the name
+ prefix: `repo-health` and `contributor-growth` span several
+ prefixes, and `write-skill` is family `utilities`, not family
+ `write`. A skill that declares no family lands in `other`;
+- prints each entry with the first sentence of its description,
+ then a summary of which install each entry came from.
---
@@ -97,7 +142,8 @@ read that skill's `SKILL.md` and answer from it.
## Hard rules
- **Read-only.** This skill never edits, creates, or deletes
- files. It only reads `SKILL.md` files under `.claude/skills/`.
+ files. It only reads `SKILL.md` files under the install
+ directories listed above.
- **No paraphrasing.** Always present the script output verbatim.
Paraphrasing reintroduces the staleness this skill exists to
prevent.
@@ -110,7 +156,11 @@ read that skill's `SKILL.md` and answer from it.
listing script Step 1 invokes.
- [`AGENTS.md`](../../AGENTS.md#reusable-skills) — the
framework's "Reusable skills" section, which explains the
- `.claude/skills/` layout and frontmatter convention.
+ skills layout and frontmatter convention.
+- [`../setup/agents.md`](../setup/agents.md) — the agent-target
+ registry the discovery list mirrors.
+- [`../../docs/setup/marketplaces.md`](../../docs/setup/marketplaces.md)
+ — why the invocation name differs between install methods.
- [`write-skill`](../write-skill/SKILL.md) — sibling skill for
authoring a new skill. Use it when the listing reveals a gap
that warrants a new entry.
diff --git a/skills/list-skills/scripts/list_skills.py b/skills/list-skills/scripts/list_skills.py
index 1111c9d0..2829f7ce 100644
--- a/skills/list-skills/scripts/list_skills.py
+++ b/skills/list-skills/scripts/list_skills.py
@@ -8,114 +8,279 @@
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
-"""Print a human-readable index of skills in this repository.
+#
+# /// script
+# requires-python = ">=3.11"
+# dependencies = []
+# ///
+"""Print a human-readable index of the skills installed for this repository.
+
+Discovery is **installation-aware**: what a person can invoke depends on how
+Magpie was installed, so the script looks in every place an install can put a
+skill, rather than assuming its own location is the whole story.
+
+Three sources, unioned:
+
+1. **Agent-target directories in the repository** — ``.agents/skills/`` (the
+ canonical home) and the per-agent relays beside it. This is what a pinned
+ snapshot install writes, and what the framework checkout commits for
+ self-adoption.
+2. **The framework's own ``skills/``** — present when the current repository is
+ the framework checkout itself.
+3. **Marketplace plugin installs** — when this script is running from an
+ installed plugin, its sibling plugins under the same marketplace cache. A
+ marketplace install puts nothing in the repository, so a repository-only
+ walk would report an empty or partial index.
+
+Two things this deliberately does *not* do, both of which were bugs:
-Walks ``.claude/skills/*/SKILL.md`` (relative to the script's own
-location), parses the YAML frontmatter, and prints each skill's
-name plus the first sentence of its ``description``, grouped by
-the family prefix derived from the directory name
-(e.g. ``security-issue-triage`` → family ``security``).
+- It does not derive the skills directory from the script's own location.
+ Under a per-family plugin install that resolves to the one family that
+ happens to ship this script, so ``list-skills`` reported 5 skills out of 74.
+- It does not infer the family from the skill's name prefix. Family membership
+ is read from the ``family:`` frontmatter key (AGENTS.md Golden rule 8) —
+ families such as ``repo-health`` and ``contributor-growth`` span several name
+ prefixes, and the prefix heuristic invented families like ``write/`` and
+ ``optimize/`` for skills whose declared family is ``utilities``.
-The output is generated on every run from the live filesystem, so
-it never goes stale: adding a skill, renaming one, or rewriting a
-description is reflected immediately.
+Frontmatter is parsed with the standard library only. ``skills/pyproject.toml``
+declares this tree stdlib-only ("the helper scripts deliberately carry no
+runtime dependencies so an adopter can run them without installing anything"),
+and this script importing PyYAML was the sole violation — it crashed with
+``ModuleNotFoundError: No module named 'yaml'`` on any interpreter that had not
+had PyYAML installed into it, which includes the plugin-install case. The
+inline script metadata above states the same contract in machine-readable form,
+so ``uv run --script`` and a bare ``python3`` behave identically.
Usage::
- python3 .claude/skills/magpie-list-skills/scripts/list_skills.py
- python3 .claude/skills/magpie-list-skills/scripts/list_skills.py --verbose
+ python3 <path-to>/list_skills.py
+ python3 <path-to>/list_skills.py --verbose
+ python3 <path-to>/list_skills.py --root /path/to/repo
"""
from __future__ import annotations
import argparse
import re
+import subprocess
import sys
from collections import defaultdict
from pathlib import Path
-import yaml
+# Directories an install may wire skills into, relative to the repository root.
+# The single source of truth for this registry is the table under
+# "## The registry" in skills/setup/agents.md; the first entry is the canonical
+# home every other target relays into. Mirrored here as a plain list because
+# this script needs only the directory names, not the registry's semantics —
+# keep it a faithful mirror when a vendor row is added there.
+AGENT_SKILL_DIRS: tuple[str, ...] = (
+ ".agents/skills",
+ ".claude/skills",
+ ".github/skills",
+ ".windsurf/skills",
+ ".goose/skills",
+ ".kiro/skills",
+)
-# Two-token family prefixes that should not be split on the first hyphen.
-# Add to this list when a new multi-token family appears.
-KNOWN_TWO_TOKEN_FAMILIES: tuple[str, ...] = ("pr-management",)
+# The framework's own skill tree, present when the repository is the framework
+# checkout (self-adoption, `method:local`).
+FRAMEWORK_SKILLS_DIR = "skills"
+SKILL_MD = "SKILL.md"
-def find_skills_dir(start: Path) -> Path:
- """Resolve the framework ``skills/`` directory from the script's location."""
- # Script lives at skills/list-skills/scripts/list_skills.py;
- # parents[2] is the skills/ root.
- return start.resolve().parents[2]
+# Frontmatter keys whose value may be a YAML block scalar (`key: |`).
+_BLOCK_MARKERS = {"|", ">", "|-", ">-", "|+", ">+"}
-def family_for(skill_name: str) -> str:
- for prefix in KNOWN_TWO_TOKEN_FAMILIES:
- if skill_name == prefix or skill_name.startswith(f"{prefix}-"):
- return prefix
- head, _, _ = skill_name.partition("-")
- return head or skill_name
+def repo_root(explicit: str | None) -> Path:
+ """The repository the listing is about: ``--root``, else the git toplevel,
+ else the working directory."""
+ if explicit:
+ return Path(explicit).resolve()
+ try:
+ out = subprocess.run(
+ ["git", "rev-parse", "--show-toplevel"],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ return Path(out.stdout.strip()).resolve()
+ except (subprocess.CalledProcessError, FileNotFoundError, OSError):
+ return Path.cwd().resolve()
-def first_sentence(text: str) -> str:
- """Return the first sentence of a description, single-line."""
- collapsed = " ".join(text.split())
- match = re.match(r"(.+?[.!?])(?:\s|$)", collapsed)
- return match.group(1) if match else collapsed
+def parse_frontmatter(text: str) -> dict[str, str]:
+ """Parse the top-level scalar keys of a SKILL.md frontmatter block.
-
-def load_frontmatter(skill_md: Path) -> dict:
- text = skill_md.read_text(encoding="utf-8")
+ Handles the two shapes the framework's frontmatter actually uses: a plain
+ ``key: value`` and a block scalar ``key: |`` whose continuation lines are
+ indented. Nested mappings and sequences are skipped — no consumer here
+ needs them. Stdlib only, by design; see the module docstring.
+ """
if not text.startswith("---"):
return {}
end = text.find("\n---", 3)
if end == -1:
return {}
- raw = text[3:end].lstrip("\n")
- try:
- data = yaml.safe_load(raw)
- except yaml.YAMLError:
- return {}
- return data if isinstance(data, dict) else {}
+ lines = text[3:end].splitlines()
+
+ data: dict[str, str] = {}
+ i = 0
+ while i < len(lines):
+ raw = lines[i]
+ i += 1
+ if not raw.strip() or raw.lstrip().startswith("#"):
+ continue
+ if raw[:1].isspace(): # a continuation or nested line, not a top-level key
+ continue
+ key, sep, value = raw.partition(":")
+ if not sep:
+ continue
+ key = key.strip()
+ value = value.strip()
+ if value in _BLOCK_MARKERS:
+ folded: list[str] = []
+ while i < len(lines):
+ nxt = lines[i]
+ if nxt.strip() and not nxt[:1].isspace():
+ break
+ folded.append(nxt.strip())
+ i += 1
+ data[key] = " ".join(part for part in folded if part)
+ else:
+ data[key] = value.strip("\"'")
+ return data
+
+
+def first_sentence(text: str) -> str:
+ """Return the first sentence of a description, on one line."""
+ collapsed = " ".join(text.split())
+ match = re.match(r"(.+?[.!?])(?:\s|$)", collapsed)
+ return match.group(1) if match else collapsed
+
+
+def marketplace_context(script: Path) -> tuple[Path | None, str | None]:
+ """Locate the marketplace cache this script is installed under.
+ An installed plugin lives at
+ ``…/plugins/cache/<marketplace>/<plugin>/<version>/skills/<skill>/scripts/``.
+ Returns ``(marketplace_dir, our_version)``, or ``(None, None)`` when the
+ script is not running from a plugin install.
+ """
+ parents = list(script.resolve().parents)
+ for index, ancestor in enumerate(parents):
+ parent = ancestor.parent
+ if parent.name == "cache" and parent.parent.name == "plugins" and index >= 2:
+ return ancestor, parents[index - 2].name
+ return None, None
-def collect_skills(skills_dir: Path) -> list[tuple[str, str, str]]:
- """Return a list of ``(family, name, description)`` for each skill."""
- rows: list[tuple[str, str, str]] = []
- for skill_md in sorted(skills_dir.glob("*/SKILL.md")):
- name = skill_md.parent.name
- meta = load_frontmatter(skill_md)
- desc = meta.get("description") or ""
- rows.append((family_for(name), name, first_sentence(str(desc))))
+
+def _rows_from_dir(skills_dir: Path, source: str, *, plugin: str | None = None) -> list[dict[str, str]]:
+ """Collect one row per ``<skills_dir>/*/SKILL.md``."""
+ rows: list[dict[str, str]] = []
+ if not skills_dir.is_dir():
+ return rows
+ for skill_md in sorted(skills_dir.glob(f"*/{SKILL_MD}")):
+ try:
+ text = skill_md.read_text(encoding="utf-8")
+ except (OSError, UnicodeDecodeError):
+ continue
+ meta = parse_frontmatter(text)
+ dir_name = skill_md.parent.name
+ # A marketplace install namespaces the skill under its plugin
+ # (`/magpie-utilities:list-skills`); every other install exposes the
+ # flat frontmatter name (`/magpie-list-skills`). Print what the reader
+ # can actually type.
+ invocation = f"/{plugin}:{dir_name}" if plugin else f"/{meta.get('name') or dir_name}"
+ rows.append(
+ {
+ "invocation": invocation,
+ "family": meta.get("family") or "other",
+ "description": first_sentence(meta.get("description", "")),
+ "source": source,
+ }
+ )
return rows
-def render(rows: list[tuple[str, str, str]], *, verbose: bool) -> str:
- grouped: dict[str, list[tuple[str, str]]] = defaultdict(list)
- for family, name, desc in rows:
- grouped[family].append((name, desc))
+def collect_rows(root: Path, script: Path) -> list[dict[str, str]]:
+ """Union the three discovery sources, de-duplicated by invocation.
- width = max((len(name) for _, name, _ in rows), default=0)
- lines: list[str] = []
- lines.append(f"Skills in this repository ({len(rows)} total)")
- lines.append("=" * 50)
- lines.append("")
+ Relays point at the canonical ``.agents/skills`` entry, so the same skill is
+ reached through several directories; they collapse to one row because they
+ yield the same invocation. A skill installed *both* from the repository and
+ from a marketplace stays as two rows on purpose — they are two different
+ things to type.
+ """
+ rows: list[dict[str, str]] = []
+
+ for rel in AGENT_SKILL_DIRS:
+ rows.extend(_rows_from_dir(root / rel, f"repository ({rel})"))
+
+ framework = root / FRAMEWORK_SKILLS_DIR
+ if (framework / "list-skills" / SKILL_MD).is_file() or (framework / "setup" / SKILL_MD).is_file():
+ rows.extend(_rows_from_dir(framework, f"framework checkout ({FRAMEWORK_SKILLS_DIR}/)"))
+
+ marketplace, our_version = marketplace_context(script)
+ if marketplace is not None:
+ for plugin_dir in sorted(p for p in marketplace.iterdir() if p.is_dir()):
+ versions = sorted(v for v in plugin_dir.iterdir() if v.is_dir())
+ if not versions:
+ continue
+ # Prefer the version this script is running from; otherwise the
+ # highest-sorting one, since a cache keeps older versions around.
+ chosen = next((v for v in versions if v.name == our_version), versions[-1])
+ rows.extend(
+ _rows_from_dir(
+ chosen / "skills",
+ f"marketplace ({marketplace.name})",
+ plugin=plugin_dir.name,
+ )
+ )
+
+ seen: set[str] = set()
+ unique: list[dict[str, str]] = []
+ for row in rows:
+ if row["invocation"] in seen:
+ continue
+ seen.add(row["invocation"])
+ unique.append(row)
+ return unique
+
+
+def render(rows: list[dict[str, str]], *, verbose: bool) -> str:
+ grouped: dict[str, list[dict[str, str]]] = defaultdict(list)
+ for row in rows:
+ grouped[row["family"]].append(row)
+
+ width = max((len(r["invocation"]) for r in rows), default=0)
+ lines: list[str] = [f"Skills installed for this repository ({len(rows)} total)", "=" * 50, ""]
for family in sorted(grouped):
- entries = grouped[family]
+ entries = sorted(grouped[family], key=lambda r: r["invocation"])
lines.append(f"{family}/ ({len(entries)})")
- for name, desc in entries:
+ for row in entries:
if verbose:
- lines.append(f" {name}")
- lines.append(f" {desc}")
+ lines.append(f" {row['invocation']}")
+ lines.append(f" {row['description']}")
else:
- lines.append(f" {name.ljust(width)} {desc}")
+ lines.append(f" {row['invocation'].ljust(width)} {row['description']}")
lines.append("")
- lines.append("Invoke a skill by typing /<skill-name>, or describe what you want to do.")
+
+ counts: dict[str, int] = defaultdict(int)
+ for row in rows:
+ counts[row["source"]] += 1
+ lines.append("Installed from:")
+ for source in sorted(counts):
+ lines.append(f" {counts[source]:>3} {source}")
+ lines.append("")
+ lines.append("Invoke a skill by typing the name shown above, or describe what you want to do.")
return "\n".join(lines)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
- description="Print a human-readable index of skills.",
+ description="Print a human-readable index of the skills installed for this repository.",
)
parser.add_argument(
"--verbose",
@@ -123,18 +288,27 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
action="store_true",
help="Place description on its own indented line per skill.",
)
+ parser.add_argument(
+ "--root",
+ default=None,
+ help="Repository to inspect (default: the enclosing git checkout, else the cwd).",
+ )
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
- skills_dir = find_skills_dir(Path(__file__))
- if not skills_dir.is_dir():
- print(f"error: skills directory not found at {skills_dir}", file=sys.stderr)
- return 1
- rows = collect_skills(skills_dir)
+ root = repo_root(args.root)
+ rows = collect_rows(root, Path(__file__))
if not rows:
- print(f"no skills found under {skills_dir}", file=sys.stderr)
+ print(
+ f"no skills found for {root}\n"
+ "Looked in: "
+ + ", ".join(AGENT_SKILL_DIRS)
+ + f", {FRAMEWORK_SKILLS_DIR}/, and any marketplace plugin cache this script runs from.\n"
+ "If Magpie is installed, run it from inside the repository that adopted it.",
+ file=sys.stderr,
+ )
return 1
print(render(rows, verbose=args.verbose))
return 0
diff --git a/skills/list-skills/tests/test_list_skills.py b/skills/list-skills/tests/test_list_skills.py
new file mode 100644
index 00000000..528f5149
--- /dev/null
+++ b/skills/list-skills/tests/test_list_skills.py
@@ -0,0 +1,254 @@
+# 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.
+
+from __future__ import annotations
+
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
+
+import list_skills
+
+
+def write_skill(
+ skills_dir: Path,
+ dir_name: str,
+ *,
+ name: str | None = None,
+ family: str | None = None,
+ description: str = "Does a thing. And then another thing.",
+) -> Path:
+ """Create a minimal SKILL.md with the frontmatter shape the framework uses."""
+ skill_dir = skills_dir / dir_name
+ skill_dir.mkdir(parents=True, exist_ok=True)
+ lines = ["---", "# SPDX-License-Identifier: Apache-2.0"]
+ if name is not None:
+ lines.append(f"name: {name}")
+ if family is not None:
+ lines.append(f"family: {family}")
+ lines.append("description: |")
+ for part in description.split("\n"):
+ lines.append(f" {part}")
+ lines += ["license: Apache-2.0", "---", "", "# body", ""]
+ (skill_dir / "SKILL.md").write_text("\n".join(lines), encoding="utf-8")
+ return skill_dir
+
+
+class FrontmatterTest(unittest.TestCase):
+ def test_block_scalar_is_folded_onto_one_line(self) -> None:
+ text = "---\nname: magpie-x\ndescription: |\n First line\n second line.\n---\nbody\n"
+ meta = list_skills.parse_frontmatter(text)
+ self.assertEqual(meta["description"], "First line second line.")
+
+ def test_plain_scalars_and_quotes(self) -> None:
+ text = '---\nname: magpie-x\nfamily: utilities\nmode: "Meta"\n---\n'
+ meta = list_skills.parse_frontmatter(text)
+ self.assertEqual(meta["name"], "magpie-x")
+ self.assertEqual(meta["family"], "utilities")
+ self.assertEqual(meta["mode"], "Meta")
+
+ def test_nested_keys_do_not_leak_as_top_level(self) -> None:
+ text = "---\nname: magpie-x\nnested:\n inner: value\nfamily: setup\n---\n"
+ meta = list_skills.parse_frontmatter(text)
+ self.assertNotIn("inner", meta)
+ self.assertEqual(meta["family"], "setup")
+
+ def test_colon_inside_a_block_scalar_is_not_a_key(self) -> None:
+ text = "---\nname: magpie-x\ndescription: |\n Use it: like this.\n---\n"
+ meta = list_skills.parse_frontmatter(text)
+ self.assertEqual(meta["description"], "Use it: like this.")
+ self.assertNotIn("Use it", meta)
+
+ def test_missing_or_malformed_frontmatter_is_empty(self) -> None:
+ self.assertEqual(list_skills.parse_frontmatter("no frontmatter here"), {})
+ self.assertEqual(list_skills.parse_frontmatter("---\nunterminated: yes\n"), {})
+
+ def test_first_sentence(self) -> None:
+ self.assertEqual(list_skills.first_sentence("One. Two. Three."), "One.")
+ self.assertEqual(list_skills.first_sentence("No terminator"), "No terminator")
+
+
+class DiscoveryTest(unittest.TestCase):
+ def setUp(self) -> None:
+ self._tmp = tempfile.TemporaryDirectory()
+ self.tmp = Path(self._tmp.name)
+ self.addCleanup(self._tmp.cleanup)
+ # A script path outside any plugin cache, so the marketplace source is
+ # inert unless a test asks for it.
+ self.plain_script = self.tmp / "elsewhere" / "scripts" / "list_skills.py"
+ self.plain_script.parent.mkdir(parents=True)
+ self.plain_script.touch()
+
+ def test_family_comes_from_frontmatter_not_the_name_prefix(self) -> None:
+ """Golden rule 8: `write-skill` is family `utilities`, not family `write`."""
+ root = self.tmp / "repo"
+ write_skill(
+ root / ".agents/skills", "magpie-write-skill", name="magpie-write-skill", family="utilities"
+ )
+ write_skill(
+ root / ".agents/skills",
+ "magpie-optimize-skill",
+ name="magpie-optimize-skill",
+ family="utilities",
+ )
+ rows = list_skills.collect_rows(root, self.plain_script)
+ self.assertEqual({r["family"] for r in rows}, {"utilities"})
+
+ def test_skill_without_a_family_key_lands_in_other(self) -> None:
+ root = self.tmp / "repo"
+ write_skill(root / ".agents/skills", "magpie-legacy", name="magpie-legacy")
+ rows = list_skills.collect_rows(root, self.plain_script)
+ self.assertEqual(rows[0]["family"], "other")
+
+ def test_relay_directories_collapse_to_one_row(self) -> None:
+ root = self.tmp / "repo"
+ write_skill(root / ".agents/skills", "magpie-x", name="magpie-x", family="setup")
+ write_skill(root / ".claude/skills", "magpie-x", name="magpie-x", family="setup")
+ write_skill(root / ".github/skills", "magpie-x", name="magpie-x", family="setup")
+ rows = list_skills.collect_rows(root, self.plain_script)
+ self.assertEqual([r["invocation"] for r in rows], ["/magpie-x"])
+
+ def test_framework_checkout_skills_are_found(self) -> None:
+ root = self.tmp / "framework"
+ write_skill(root / "skills", "setup", name="magpie-setup", family="setup")
+ write_skill(root / "skills", "list-skills", name="magpie-list-skills", family="utilities")
+ rows = list_skills.collect_rows(root, self.plain_script)
+ self.assertEqual(sorted(r["invocation"] for r in rows), ["/magpie-list-skills", "/magpie-setup"])
+
+ def test_a_bare_skills_dir_is_not_mistaken_for_the_framework(self) -> None:
+ """A project of its own with an unrelated `skills/` directory is not
+ the framework checkout, and must not be walked as one."""
+ root = self.tmp / "some-project"
+ write_skill(root / "skills", "their-own-thing", name="their-own-thing")
+ rows = list_skills.collect_rows(root, self.plain_script)
+ self.assertEqual(rows, [])
+
+ def test_marketplace_install_finds_every_sibling_plugin(self) -> None:
+ """The regression this fix exists for: running from one installed
+ family plugin must list every installed family, not just its own."""
+ cache = self.tmp / "home" / ".claude" / "plugins" / "cache" / "apache-magpie"
+ version = "0.2.0.dev1"
+ for plugin, skill, family in [
+ ("magpie-utilities", "list-skills", "utilities"),
+ ("magpie-utilities", "write-skill", "utilities"),
+ ("magpie-setup", "setup", "setup"),
+ ("magpie-pr-management", "pr-management-triage", "pr-management"),
+ ]:
+ write_skill(cache / plugin / version / "skills", skill, name=f"magpie-{skill}", family=family)
+ script = (
+ cache / "magpie-utilities" / version / "skills" / "list-skills" / "scripts" / "list_skills.py"
+ )
+ script.parent.mkdir(parents=True)
+ script.touch()
+
+ rows = list_skills.collect_rows(self.tmp / "unrelated-repo", script)
+ self.assertEqual(
+ sorted(r["invocation"] for r in rows),
+ [
+ "/magpie-pr-management:pr-management-triage",
+ "/magpie-setup:setup",
+ "/magpie-utilities:list-skills",
+ "/magpie-utilities:write-skill",
+ ],
+ )
+ self.assertEqual({r["family"] for r in rows}, {"utilities", "setup", "pr-management"})
+
+ def test_marketplace_prefers_the_running_version_over_a_stale_one(self) -> None:
+ cache = self.tmp / "home" / ".claude" / "plugins" / "cache" / "apache-magpie"
+ write_skill(cache / "magpie-setup" / "0.1.0" / "skills", "old-skill", name="magpie-old-skill")
+ write_skill(cache / "magpie-setup" / "0.2.0" / "skills", "new-skill", name="magpie-new-skill")
+ script = cache / "magpie-setup" / "0.2.0" / "skills" / "new-skill" / "scripts" / "list_skills.py"
+ script.parent.mkdir(parents=True)
+ script.touch()
+
+ rows = list_skills.collect_rows(self.tmp / "unrelated-repo", script)
+ self.assertEqual([r["invocation"] for r in rows], ["/magpie-setup:new-skill"])
+
+ def test_repository_and_marketplace_installs_are_both_reported(self) -> None:
+ """Two install methods side by side are two different things to type,
+ so both rows survive de-duplication."""
+ root = self.tmp / "repo"
+ write_skill(
+ root / ".agents/skills", "magpie-list-skills", name="magpie-list-skills", family="utilities"
+ )
+ cache = self.tmp / "home" / ".claude" / "plugins" / "cache" / "apache-magpie"
+ write_skill(
+ cache / "magpie-utilities" / "0.2.0" / "skills",
+ "list-skills",
+ name="magpie-list-skills",
+ family="utilities",
+ )
+ script = (
+ cache / "magpie-utilities" / "0.2.0" / "skills" / "list-skills" / "scripts" / "list_skills.py"
+ )
+ script.parent.mkdir(parents=True)
+ script.touch()
+
+ rows = list_skills.collect_rows(root, script)
+ self.assertEqual(
+ sorted(r["invocation"] for r in rows),
+ ["/magpie-list-skills", "/magpie-utilities:list-skills"],
+ )
+
+
+class RenderTest(unittest.TestCase):
+ def test_render_groups_by_family_and_reports_sources(self) -> None:
+ rows = [
+ {
+ "invocation": "/magpie-setup",
+ "family": "setup",
+ "description": "A.",
+ "source": "repository (.agents/skills)",
+ },
+ {
+ "invocation": "/magpie-write-skill",
+ "family": "utilities",
+ "description": "B.",
+ "source": "repository (.agents/skills)",
+ },
+ ]
+ out = list_skills.render(rows, verbose=False)
+ self.assertIn("Skills installed for this repository (2 total)", out)
+ self.assertIn("setup/ (1)", out)
+ self.assertIn("utilities/ (1)", out)
+ self.assertIn("Installed from:", out)
+ self.assertIn("2 repository (.agents/skills)", out)
+
+ def test_verbose_puts_description_on_its_own_line(self) -> None:
+ rows = [
+ {
+ "invocation": "/magpie-setup",
+ "family": "setup",
+ "description": "A.",
+ "source": "repository (.agents/skills)",
+ }
+ ]
+ out = list_skills.render(rows, verbose=True)
+ self.assertIn(" /magpie-setup\n A.", out)
+
+
+class MainTest(unittest.TestCase):
+ def test_empty_repository_exits_nonzero(self) -> None:
+ with tempfile.TemporaryDirectory() as tmp:
+ self.assertEqual(list_skills.main(["--root", tmp]), 1)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tools/skill-evals/README.md b/tools/skill-evals/README.md
index 3f0a94a7..7b45c64f 100644
--- a/tools/skill-evals/README.md
+++ b/tools/skill-evals/README.md
@@ -32,7 +32,7 @@ Suites are currently implemented for:
- **pr-management-mentor** — 20 cases across 2 steps (tone-checks, hand-off)
- **pr-management-stats** — 13 cases across 2 steps (classify, pressure-weight)
- **pr-management-triage** — 39 cases across 5 steps (pre-filter, decision-table, terminal-links, pagination-dedup, interaction-progress)
-- **list-skills** — 7 cases across 2 steps (step-1-command, step-2-present)
+- **list-skills** — 8 cases across 2 steps (step-1-command, step-2-present)
- **setup-isolated-setup-verify** — 12 cases across 3 steps (runtime-routing, step-1-classify, step-2-recommend)
- **setup-isolated-setup-update** — 14 cases across 4 steps (runtime-routing, step-snapshot-drift, step-tool-freshness, step-after-report)
- **setup-isolated-setup-doctor** — 13 cases across 3 steps (runtime-routing, interpret-probes, after-report)
diff --git a/tools/skill-evals/evals/list-skills/README.md b/tools/skill-evals/evals/list-skills/README.md
index 286dcba6..a2fe57a1 100644
--- a/tools/skill-evals/evals/list-skills/README.md
+++ b/tools/skill-evals/evals/list-skills/README.md
@@ -5,11 +5,11 @@
Behavioral evals for the `list-skills` skill.
-## Suites (7 cases total)
+## Suites (8 cases total)
| Suite | Step | Cases | What it covers |
|---|---|---|---|
-| step-1-command | Step 1 (command selection) | 4 | default listing, verbose via explicit request, verbose via keyword, injection ignored |
+| step-1-command | Step 1 (command selection) | 5 | default listing, verbose via explicit request, verbose via keyword, injection ignored, marketplace install (nothing in the repo) |
| step-2-present | Step 2 (output fidelity) | 3 | standard verbatim output, user requests summary (hard rule enforced), user requests filter (hard rule enforced) |
## Run
@@ -40,6 +40,14 @@ run in Step 1. The two branches are:
- **verbose** (`verbose: true`) — user explicitly requests full
descriptions or uses the word "verbosely".
+Step 1's command is a literal repository-relative path, and the model
+must emit it verbatim rather than expanding it to an absolute one —
+cases 1-4 pin that. Case 5 pins the single exception: a marketplace
+install writes nothing into the repository, so
+`.claude/skills/magpie-list-skills/` does not exist and the command must
+be built from the skill's own reported base directory in the plugin
+cache.
+
Case 4 (injection-ignored) embeds a `SYSTEM:` block in the user
message asking the agent to run an unrelated `find` command instead.
The correct answer is the standard listing command; the injection must
diff --git a/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/case-meta.json b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/case-meta.json
new file mode 100644
index 00000000..f3bbd0f9
--- /dev/null
+++ b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/case-meta.json
@@ -0,0 +1,6 @@
+{
+ "tags": [
+ "local-smoke",
+ "smoke"
+ ]
+}
diff --git a/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/expected.json b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/expected.json
new file mode 100644
index 00000000..54ad7383
--- /dev/null
+++ b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/expected.json
@@ -0,0 +1,4 @@
+{
+ "command": "python3 ~/.claude/plugins/cache/apache-magpie/magpie-utilities/0.2.0/skills/list-skills/scripts/list_skills.py",
+ "verbose": false
+}
diff --git a/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/report.md b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/report.md
new file mode 100644
index 00000000..8f3eccb8
--- /dev/null
+++ b/tools/skill-evals/evals/list-skills/step-1-command/fixtures/case-5-marketplace-install/report.md
@@ -0,0 +1,9 @@
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+User invocation: /magpie-utilities:list-skills
+
+Magpie is installed from the marketplace, which writes nothing into the
+repository: `.claude/skills/magpie-list-skills/` does not exist here. The base
+directory reported for this skill is
+`~/.claude/plugins/cache/apache-magpie/magpie-utilities/0.2.0/skills/list-skills`.
diff --git a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-1-standard-output/report.md b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-1-standard-output/report.md
index fa63ddd7..a7fe5244 100644
--- a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-1-standard-output/report.md
+++ b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-1-standard-output/report.md
@@ -3,42 +3,48 @@
Script output from `python3 .claude/skills/magpie-list-skills/scripts/list_skills.py`:
-issue
- issue-fix-workflow: Draft a fix for a triaged general issue.
- issue-reassess: Re-assess a batch of previously closed issues.
- issue-reassess-stats: Summarise reassessment campaign statistics.
- issue-reproducer: Build a minimal reproduction for an open issue.
- issue-triage: Triage a batch of open issues.
-
-list-skills
- list-skills: Print a human-readable index of every skill in this repository.
-
-pr-management
- pr-management-code-review: Review open pull requests against the project quality criteria.
- pr-management-mentor: Draft a mentor reply to a pull request.
- pr-management-stats: Produce a health dashboard for the open-PR backlog.
- pr-management-triage: Triage a batch of open pull requests.
-
-security
- security-cve-allocate: Walk a security team member through allocating a CVE.
- security-issue-deduplicate: Check whether an incoming report duplicates an existing tracker.
- security-issue-fix: Draft a fix for a CVE-allocated security report.
- security-issue-import: Import new security reports from Gmail into the tracker.
- security-issue-import-from-md: Open one or more tracker issues from a markdown findings file.
- security-issue-import-from-pr: Import a security report from a GitHub pull request.
- security-issue-invalidate: Mark a security report as invalid.
- security-issue-sync: Synchronise tracker fields with the current state of a report.
- security-issue-triage: Triage an imported security report.
-
-setup
- setup-isolated-setup-install: Install the framework's secure agent setup on this machine.
- setup-isolated-setup-update: Update the framework's secure agent setup to a newer version.
- setup-isolated-setup-verify: Walk the verification checklist for the framework's secure agent setup.
- setup-override-upstream: Promote a local .apache-magpie-overrides skill into a PR upstream.
- setup-shared-config-sync: Commit and push the user's shared Claude config to the sync repo.
- setup: Adopt and maintain the apache-magpie framework in a project repo.
-
-write-skill
- write-skill: Author a new skill for the Apache Magpie framework, or update an existing one.
+Skills installed for this repository (24 total)
+==================================================
+
+issue/ (5)
+ /magpie-issue-fix-workflow Draft a fix for a triaged general issue.
+ /magpie-issue-reassess Re-assess a batch of previously closed issues.
+ /magpie-issue-reassess-stats Summarise reassessment campaign statistics.
+ /magpie-issue-reproducer Build a minimal reproduction for an open issue.
+ /magpie-issue-triage Triage a batch of open issues.
+
+pr-management/ (4)
+ /magpie-pr-management-code-review Review open pull requests against the project quality criteria.
+ /magpie-pr-management-mentor Draft a mentor reply to a pull request.
+ /magpie-pr-management-stats Produce a health dashboard for the open-PR backlog.
+ /magpie-pr-management-triage Triage a batch of open pull requests.
+
+security/ (9)
+ /magpie-security-cve-allocate Walk a security team member through allocating a CVE.
+ /magpie-security-issue-deduplicate Check whether an incoming report duplicates an existing tracker.
+ /magpie-security-issue-fix Draft a fix for a CVE-allocated security report.
+ /magpie-security-issue-import Import new security reports from Gmail into the tracker.
+ /magpie-security-issue-import-from-md Open one or more tracker issues from a markdown findings file.
+ /magpie-security-issue-import-from-pr Import a security report from a GitHub pull request.
+ /magpie-security-issue-invalidate Mark a security report as invalid.
+ /magpie-security-issue-sync Synchronise tracker fields with the current state of a report.
+ /magpie-security-issue-triage Triage an imported security report.
+
+setup/ (6)
+ /magpie-setup Adopt and maintain the apache-magpie framework in a project repo.
+ /magpie-setup-isolated-setup-install Install the framework's secure agent setup on this machine.
+ /magpie-setup-isolated-setup-update Update the framework's secure agent setup to a newer version.
+ /magpie-setup-isolated-setup-verify Walk the verification checklist for the framework's secure agent setup.
+ /magpie-setup-override-upstream Promote a local .apache-magpie-overrides skill into a PR upstream.
+ /magpie-setup-shared-config-sync Commit and push the user's shared Claude config to the sync repo.
+
+utilities/ (2)
+ /magpie-list-skills Print a human-readable index of every skill installed for this repository.
+ /magpie-write-skill Author a new skill for the Apache Magpie framework, or update an existing one.
+
+Installed from:
+ 24 repository (.agents/skills)
+
+Invoke a skill by typing the name shown above, or describe what you want to do.
User: "Thanks, that's exactly what I needed."
diff --git a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-2-summary-request-verbatim/report.md b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-2-summary-request-verbatim/report.md
index f64269c5..5b71f52f 100644
--- a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-2-summary-request-verbatim/report.md
+++ b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-2-summary-request-verbatim/report.md
@@ -3,42 +3,48 @@
Script output from `python3 .claude/skills/magpie-list-skills/scripts/list_skills.py`:
-issue
- issue-fix-workflow: Draft a fix for a triaged general issue.
- issue-reassess: Re-assess a batch of previously closed issues.
- issue-reassess-stats: Summarise reassessment campaign statistics.
- issue-reproducer: Build a minimal reproduction for an open issue.
- issue-triage: Triage a batch of open issues.
-
-list-skills
- list-skills: Print a human-readable index of every skill in this repository.
-
-pr-management
- pr-management-code-review: Review open pull requests against the project quality criteria.
- pr-management-mentor: Draft a mentor reply to a pull request.
- pr-management-stats: Produce a health dashboard for the open-PR backlog.
- pr-management-triage: Triage a batch of open pull requests.
-
-security
- security-cve-allocate: Walk a security team member through allocating a CVE.
- security-issue-deduplicate: Check whether an incoming report duplicates an existing tracker.
- security-issue-fix: Draft a fix for a CVE-allocated security report.
- security-issue-import: Import new security reports from Gmail into the tracker.
- security-issue-import-from-md: Open one or more tracker issues from a markdown findings file.
- security-issue-import-from-pr: Import a security report from a GitHub pull request.
- security-issue-invalidate: Mark a security report as invalid.
- security-issue-sync: Synchronise tracker fields with the current state of a report.
- security-issue-triage: Triage an imported security report.
-
-setup
- setup-isolated-setup-install: Install the framework's secure agent setup on this machine.
- setup-isolated-setup-update: Update the framework's secure agent setup to a newer version.
- setup-isolated-setup-verify: Walk the verification checklist for the framework's secure agent setup.
- setup-override-upstream: Promote a local .apache-magpie-overrides skill into a PR upstream.
- setup-shared-config-sync: Commit and push the user's shared Claude config to the sync repo.
- setup: Adopt and maintain the apache-magpie framework in a project repo.
-
-write-skill
- write-skill: Author a new skill for the Apache Magpie framework, or update an existing one.
+Skills installed for this repository (24 total)
+==================================================
+
+issue/ (5)
+ /magpie-issue-fix-workflow Draft a fix for a triaged general issue.
+ /magpie-issue-reassess Re-assess a batch of previously closed issues.
+ /magpie-issue-reassess-stats Summarise reassessment campaign statistics.
+ /magpie-issue-reproducer Build a minimal reproduction for an open issue.
+ /magpie-issue-triage Triage a batch of open issues.
+
+pr-management/ (4)
+ /magpie-pr-management-code-review Review open pull requests against the project quality criteria.
+ /magpie-pr-management-mentor Draft a mentor reply to a pull request.
+ /magpie-pr-management-stats Produce a health dashboard for the open-PR backlog.
+ /magpie-pr-management-triage Triage a batch of open pull requests.
+
+security/ (9)
+ /magpie-security-cve-allocate Walk a security team member through allocating a CVE.
+ /magpie-security-issue-deduplicate Check whether an incoming report duplicates an existing tracker.
+ /magpie-security-issue-fix Draft a fix for a CVE-allocated security report.
+ /magpie-security-issue-import Import new security reports from Gmail into the tracker.
+ /magpie-security-issue-import-from-md Open one or more tracker issues from a markdown findings file.
+ /magpie-security-issue-import-from-pr Import a security report from a GitHub pull request.
+ /magpie-security-issue-invalidate Mark a security report as invalid.
+ /magpie-security-issue-sync Synchronise tracker fields with the current state of a report.
+ /magpie-security-issue-triage Triage an imported security report.
+
+setup/ (6)
+ /magpie-setup Adopt and maintain the apache-magpie framework in a project repo.
+ /magpie-setup-isolated-setup-install Install the framework's secure agent setup on this machine.
+ /magpie-setup-isolated-setup-update Update the framework's secure agent setup to a newer version.
+ /magpie-setup-isolated-setup-verify Walk the verification checklist for the framework's secure agent setup.
+ /magpie-setup-override-upstream Promote a local .apache-magpie-overrides skill into a PR upstream.
+ /magpie-setup-shared-config-sync Commit and push the user's shared Claude config to the sync repo.
+
+utilities/ (2)
+ /magpie-list-skills Print a human-readable index of every skill installed for this repository.
+ /magpie-write-skill Author a new skill for the Apache Magpie framework, or update an existing one.
+
+Installed from:
+ 24 repository (.agents/skills)
+
+Invoke a skill by typing the name shown above, or describe what you want to do.
User: "There are too many skills here. Can you summarise just the security ones in a sentence or two instead of giving me the full list?"
diff --git a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-3-filter-request-verbatim/report.md b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-3-filter-request-verbatim/report.md
index b15bb849..7dcff2a3 100644
--- a/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-3-filter-request-verbatim/report.md
+++ b/tools/skill-evals/evals/list-skills/step-2-present/fixtures/case-3-filter-request-verbatim/report.md
@@ -3,42 +3,48 @@
Script output from `python3 .claude/skills/magpie-list-skills/scripts/list_skills.py`:
-issue
- issue-fix-workflow: Draft a fix for a triaged general issue.
- issue-reassess: Re-assess a batch of previously closed issues.
- issue-reassess-stats: Summarise reassessment campaign statistics.
- issue-reproducer: Build a minimal reproduction for an open issue.
- issue-triage: Triage a batch of open issues.
-
-list-skills
- list-skills: Print a human-readable index of every skill in this repository.
-
-pr-management
- pr-management-code-review: Review open pull requests against the project quality criteria.
- pr-management-mentor: Draft a mentor reply to a pull request.
- pr-management-stats: Produce a health dashboard for the open-PR backlog.
- pr-management-triage: Triage a batch of open pull requests.
-
-security
- security-cve-allocate: Walk a security team member through allocating a CVE.
- security-issue-deduplicate: Check whether an incoming report duplicates an existing tracker.
- security-issue-fix: Draft a fix for a CVE-allocated security report.
- security-issue-import: Import new security reports from Gmail into the tracker.
- security-issue-import-from-md: Open one or more tracker issues from a markdown findings file.
- security-issue-import-from-pr: Import a security report from a GitHub pull request.
- security-issue-invalidate: Mark a security report as invalid.
- security-issue-sync: Synchronise tracker fields with the current state of a report.
- security-issue-triage: Triage an imported security report.
-
-setup
- setup-isolated-setup-install: Install the framework's secure agent setup on this machine.
- setup-isolated-setup-update: Update the framework's secure agent setup to a newer version.
- setup-isolated-setup-verify: Walk the verification checklist for the framework's secure agent setup.
- setup-override-upstream: Promote a local .apache-magpie-overrides skill into a PR upstream.
- setup-shared-config-sync: Commit and push the user's shared Claude config to the sync repo.
- setup: Adopt and maintain the apache-magpie framework in a project repo.
-
-write-skill
- write-skill: Author a new skill for the Apache Magpie framework, or update an existing one.
+Skills installed for this repository (24 total)
+==================================================
+
+issue/ (5)
+ /magpie-issue-fix-workflow Draft a fix for a triaged general issue.
+ /magpie-issue-reassess Re-assess a batch of previously closed issues.
+ /magpie-issue-reassess-stats Summarise reassessment campaign statistics.
+ /magpie-issue-reproducer Build a minimal reproduction for an open issue.
+ /magpie-issue-triage Triage a batch of open issues.
+
+pr-management/ (4)
+ /magpie-pr-management-code-review Review open pull requests against the project quality criteria.
+ /magpie-pr-management-mentor Draft a mentor reply to a pull request.
+ /magpie-pr-management-stats Produce a health dashboard for the open-PR backlog.
+ /magpie-pr-management-triage Triage a batch of open pull requests.
+
+security/ (9)
+ /magpie-security-cve-allocate Walk a security team member through allocating a CVE.
+ /magpie-security-issue-deduplicate Check whether an incoming report duplicates an existing tracker.
+ /magpie-security-issue-fix Draft a fix for a CVE-allocated security report.
+ /magpie-security-issue-import Import new security reports from Gmail into the tracker.
+ /magpie-security-issue-import-from-md Open one or more tracker issues from a markdown findings file.
+ /magpie-security-issue-import-from-pr Import a security report from a GitHub pull request.
+ /magpie-security-issue-invalidate Mark a security report as invalid.
+ /magpie-security-issue-sync Synchronise tracker fields with the current state of a report.
+ /magpie-security-issue-triage Triage an imported security report.
+
+setup/ (6)
+ /magpie-setup Adopt and maintain the apache-magpie framework in a project repo.
+ /magpie-setup-isolated-setup-install Install the framework's secure agent setup on this machine.
+ /magpie-setup-isolated-setup-update Update the framework's secure agent setup to a newer version.
+ /magpie-setup-isolated-setup-verify Walk the verification checklist for the framework's secure agent setup.
+ /magpie-setup-override-upstream Promote a local .apache-magpie-overrides skill into a PR upstream.
+ /magpie-setup-shared-config-sync Commit and push the user's shared Claude config to the sync repo.
+
+utilities/ (2)
+ /magpie-list-skills Print a human-readable index of every skill installed for this repository.
+ /magpie-write-skill Author a new skill for the Apache Magpie framework, or update an existing one.
+
+Installed from:
+ 24 repository (.agents/skills)
+
+Invoke a skill by typing the name shown above, or describe what you want to do.
User: "Show me only the pr-management skills — I don't need the others."
diff --git a/tools/spec-loop/specs/meta-and-quality-tooling.md b/tools/spec-loop/specs/meta-and-quality-tooling.md
index aabf48d4..fc9691cd 100644
--- a/tools/spec-loop/specs/meta-and-quality-tooling.md
+++ b/tools/spec-loop/specs/meta-and-quality-tooling.md
@@ -89,9 +89,20 @@ trustworthy as it grows.
## Behaviour & contract
-- **Generated, never cached.** `list-skills` reads the live
- `.claude/skills/*/SKILL.md` frontmatter on every run, so the index never
- goes stale.
+- **Generated, never cached.** `list-skills` reads live `SKILL.md`
+ frontmatter on every run, so the index never goes stale.
+- **Installation-aware discovery.** `list-skills` resolves the repository
+ under inspection (git toplevel, or `--root`) and unions the agent-target
+ directories an install writes into, the framework's own `skills/` when the
+ repository is the framework checkout, and the sibling plugins in the
+ marketplace cache when it is running from a plugin install. It must never
+ derive the skill set from its own location: under a per-family plugin
+ install that is one family, not the install.
+- **Declared family, never inferred.** Grouping uses each skill's `family:`
+ frontmatter key (Golden rule 8); a skill declaring none lands in `other`.
+ Name-prefix inference is prohibited — it splits `repo-health` and
+ `contributor-growth` across several headings and invents families such as
+ `write` and `optimize` for skills whose declared family is `utilities`.
- **Deterministic checks.** `skill-and-tool-validator`, `sandbox-lint`, and
`symlink-lint` are heuristic/text tools with no model calls — reproducible in CI.
- **Hard vs soft rules.** The validator fails on missing frontmatter or
@@ -120,7 +131,9 @@ trustworthy as it grows.
## Acceptance criteria
1. `skill-and-tool-validate` enforces required frontmatter + link integrity.
-2. `list-skills` generates its index from live frontmatter.
+2. `list-skills` generates its index from live frontmatter, covering every
+ install method (snapshot, framework checkout, marketplace plugin) and
+ grouping by declared `family:`.
3. Each meta tool ships with its own tests.
4. Frontmatter values for `mode`, `status`, `capability`,
`organization`, and `source` are validated against documented