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/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new ffc9a71 feat(list-skills): human-facing index that auto-generates
from frontmatter (#164)
ffc9a71 is described below
commit ffc9a712a4eab3b446956d28ea7b1fe434a46785
Author: Yeonguk Choo <[email protected]>
AuthorDate: Fri May 15 21:19:50 2026 +0900
feat(list-skills): human-facing index that auto-generates from frontmatter
(#164)
---
.claude/skills/list-steward-skills/SKILL.md | 111 ++++++++++++++++
.../list-steward-skills/scripts/list_skills.py | 147 +++++++++++++++++++++
2 files changed, 258 insertions(+)
diff --git a/.claude/skills/list-steward-skills/SKILL.md
b/.claude/skills/list-steward-skills/SKILL.md
new file mode 100644
index 0000000..8398e7b
--- /dev/null
+++ b/.claude/skills/list-steward-skills/SKILL.md
@@ -0,0 +1,111 @@
+---
+name: list-steward-skills
+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.
+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
+ table of contents for the skills"*, or types `/list-steward-skills`.
+ This is a help-style overview for humans onboarding to the
+ repository — agents route via the live frontmatter
+ `description` field directly and do not need this index to
+ choose a skill.
+license: Apache-2.0
+---
+
+<!-- SPDX-License-Identifier: Apache-2.0
+ https://www.apache.org/licenses/LICENSE-2.0 -->
+
+<!-- Placeholder convention (see
AGENTS.md#placeholder-convention-used-in-skill-files):
+ <project-config> → adopting project's `.apache-steward/` directory
+ <tracker> → value of `tracker_repo:` in <project-config>/project.md
+ <upstream> → value of `upstream_repo:` in
<project-config>/project.md
+ <framework> → `.apache-steward/apache-steward` in adopters; `.` in
+ the framework standalone -->
+
+# list-steward-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.
+
+---
+
+## Prerequisites
+
+- Python 3.9+ on `PATH` with `PyYAML` importable. The framework's
+ Python toolchain already meets this; no extra setup.
+
+---
+
+## Step 1 — Run the listing script
+
+Run the bundled script and present its output to the user
+verbatim:
+
+```bash
+python3 .claude/skills/list-steward-skills/scripts/list_skills.py
+```
+
+For a layout that puts each description on its own indented line
+(easier to read when descriptions are long), pass `--verbose`:
+
+```bash
+python3 .claude/skills/list-steward-skills/scripts/list_skills.py --verbose
+```
+
+The script:
+
+- 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.
+
+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.
+
+---
+
+## Step 2 — Hand the output to the user
+
+Quote the script output back to the user as-is. Do not
+paraphrase, summarise, or re-order — the value of this skill is
+that the listing is the canonical, deterministic view of what
+exists. If the user asks for more detail on a specific skill,
+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/`.
+- **No paraphrasing.** Always present the script output verbatim.
+ Paraphrasing reintroduces the staleness this skill exists to
+ prevent.
+
+---
+
+## References
+
+- [`scripts/list_skills.py`](scripts/list_skills.py) — the
+ 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.
+- [`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/.claude/skills/list-steward-skills/scripts/list_skills.py
b/.claude/skills/list-steward-skills/scripts/list_skills.py
new file mode 100644
index 0000000..d6a0da5
--- /dev/null
+++ b/.claude/skills/list-steward-skills/scripts/list_skills.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+"""Print a human-readable index of skills in this repository.
+
+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``).
+
+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.
+
+Usage::
+
+ python3 .claude/skills/list-steward-skills/scripts/list_skills.py
+ python3 .claude/skills/list-steward-skills/scripts/list_skills.py --verbose
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import sys
+from collections import defaultdict
+from pathlib import Path
+
+import yaml
+
+# 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",)
+
+
+def find_skills_dir(start: Path) -> Path:
+ """Resolve ``.claude/skills/`` from the script's location."""
+ # Script lives at
.claude/skills/list-steward-skills/scripts/list_skills.py;
+ # parents[2] is .claude/skills.
+ return start.resolve().parents[2]
+
+
+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 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 load_frontmatter(skill_md: Path) -> dict:
+ text = skill_md.read_text(encoding="utf-8")
+ 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 {}
+
+
+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))))
+ 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))
+
+ 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("")
+ for family in sorted(grouped):
+ entries = grouped[family]
+ lines.append(f"{family}/ ({len(entries)})")
+ for name, desc in entries:
+ if verbose:
+ lines.append(f" {name}")
+ lines.append(f" {desc}")
+ else:
+ lines.append(f" {name.ljust(width)} {desc}")
+ lines.append("")
+ lines.append(
+ "Invoke a skill by typing /<skill-name>, 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.",
+ )
+ parser.add_argument(
+ "--verbose",
+ "-v",
+ action="store_true",
+ help="Place description on its own indented line per skill.",
+ )
+ 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)
+ if not rows:
+ print(f"no skills found under {skills_dir}", file=sys.stderr)
+ return 1
+ print(render(rows, verbose=args.verbose))
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())