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())

Reply via email to