From 573798e8ce498ab8dbee0125ddf6037d94dd4440 Mon Sep 17 00:00:00 2001 From: Valentin Schmidt Date: Sat, 5 Sep 2026 21:40:07 +0200 Subject: [PATCH 1/4] chore: hide maintainer skills from the skills CLI installer The Vercel `skills` CLI scans `.claude/skills/` and `.codex/skills/` next to `skills/`, so `npx skills add Tue-StudyOS/study-os-thesis` offered 12 skills to students: the ten public ones plus the two repo-internal simulation skills, which `--skill '*'` then installed as well. `metadata.internal: true` keeps them out of both the picker and the wildcard. The key is part of the Agent Skills frontmatter spec, so Claude Code and Codex still load them repo-locally. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H8uhg9be5GbE6QpPHipwta --- .claude/skills/create-thesis-sim-student/SKILL.md | 2 ++ .claude/skills/run-thesis-simulations/SKILL.md | 2 ++ .codex/skills/create-thesis-sim-student/SKILL.md | 2 ++ .codex/skills/run-thesis-simulations/SKILL.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/.claude/skills/create-thesis-sim-student/SKILL.md b/.claude/skills/create-thesis-sim-student/SKILL.md index e21aff3..c73db8b 100644 --- a/.claude/skills/create-thesis-sim-student/SKILL.md +++ b/.claude/skills/create-thesis-sim-student/SKILL.md @@ -1,6 +1,8 @@ --- name: create-thesis-sim-student description: Create new repo-local thesis-finder simulation student commands. Use when asked to add, scaffold, draft, or generate a new simulated thesis-finder student persona for Claude and Codex. +metadata: + internal: true --- # Create Thesis Simulation Student diff --git a/.claude/skills/run-thesis-simulations/SKILL.md b/.claude/skills/run-thesis-simulations/SKILL.md index e2949e1..f06e722 100644 --- a/.claude/skills/run-thesis-simulations/SKILL.md +++ b/.claude/skills/run-thesis-simulations/SKILL.md @@ -1,6 +1,8 @@ --- name: run-thesis-simulations description: Run and evaluate all repo-local thesis-finder simulation commands. Use when asked to run the thesis simulation suite, evaluate thesis-finder personas, execute all thesis-sim commands, or produce conversation and rating artifacts. +metadata: + internal: true --- # Run Thesis Simulations diff --git a/.codex/skills/create-thesis-sim-student/SKILL.md b/.codex/skills/create-thesis-sim-student/SKILL.md index e21aff3..c73db8b 100644 --- a/.codex/skills/create-thesis-sim-student/SKILL.md +++ b/.codex/skills/create-thesis-sim-student/SKILL.md @@ -1,6 +1,8 @@ --- name: create-thesis-sim-student description: Create new repo-local thesis-finder simulation student commands. Use when asked to add, scaffold, draft, or generate a new simulated thesis-finder student persona for Claude and Codex. +metadata: + internal: true --- # Create Thesis Simulation Student diff --git a/.codex/skills/run-thesis-simulations/SKILL.md b/.codex/skills/run-thesis-simulations/SKILL.md index e2949e1..f06e722 100644 --- a/.codex/skills/run-thesis-simulations/SKILL.md +++ b/.codex/skills/run-thesis-simulations/SKILL.md @@ -1,6 +1,8 @@ --- name: run-thesis-simulations description: Run and evaluate all repo-local thesis-finder simulation commands. Use when asked to run the thesis simulation suite, evaluate thesis-finder personas, execute all thesis-sim commands, or produce conversation and rating artifacts. +metadata: + internal: true --- # Run Thesis Simulations From 991ec62b6d34da46b21dd97b1605a29b0364d77f Mon Sep 17 00:00:00 2001 From: Valentin Schmidt Date: Sat, 5 Sep 2026 21:40:07 +0200 Subject: [PATCH 2/4] test: guard the skills CLI install surface Assert that the skill set the installer discovers across `skills/`, `.claude/skills/` and `.codex/skills/` is exactly the ten public skills, and that every copy of a maintainer skill carries `metadata.internal: true`. Extend the QA workflow's path filter to the two agent skill directories so the check runs when a maintainer skill changes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H8uhg9be5GbE6QpPHipwta --- .github/workflows/qa.yml | 2 + skills/tests/test_skills_cli_install.py | 71 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 skills/tests/test_skills_cli_install.py diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 5927e5f..4265cb8 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -5,6 +5,8 @@ on: pull_request: paths: - 'skills/**' + - '.claude/skills/**' + - '.codex/skills/**' - 'scripts/**' - 'pyproject.toml' - '.github/workflows/qa.yml' diff --git a/skills/tests/test_skills_cli_install.py b/skills/tests/test_skills_cli_install.py new file mode 100644 index 0000000..1e4ecb2 --- /dev/null +++ b/skills/tests/test_skills_cli_install.py @@ -0,0 +1,71 @@ +"""Deterministic checks for the `npx skills add` install surface. + +The Vercel skills CLI (https://github.com/vercel-labs/skills) is the one-command +route for local agents. It discovers skills in `skills/` *and* in agent +directories such as `.claude/skills/` and `.codex/skills/`, where this repo keeps +its maintainer-only simulation skills. Those must stay hidden behind +`metadata.internal: true`, or students get them listed in the picker and +installed by `--skill '*'`. +""" + +from __future__ import annotations + +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCANNED_DIRS = ( + REPO_ROOT / "skills", + REPO_ROOT / ".claude" / "skills", + REPO_ROOT / ".codex" / "skills", +) +PUBLIC_SKILLS = { + "build-student-profile", + "design-agent-skill", + "discover-company-candidates", + "discover-university-candidates", + "draft-thesis-contact", + "find-company-thesis-options", + "find-recent-papers", + "find-university-chairs", + "generate-thesis-directions", + "thesis-finder", +} +INTERNAL_SKILLS = { + "create-thesis-sim-student", + "run-thesis-simulations", +} +INTERNAL_MARKER = re.compile(r"^metadata:\n(?:[ \t]+.*\n)*?[ \t]+internal:[ \t]*true[ \t]*$", flags=re.MULTILINE) + + +def _frontmatter(skill_md: Path) -> str: + match = re.match(r"^---\n(?P.*?)\n---\n", skill_md.read_text(encoding="utf-8"), flags=re.DOTALL) + assert match, f"{skill_md} is missing YAML frontmatter" + return match.group("body") + "\n" + + +def _discovered_skills() -> dict[str, list[Path]]: + found: dict[str, list[Path]] = {} + for scanned_dir in SCANNED_DIRS: + assert scanned_dir.is_dir(), f"{scanned_dir} is missing" + for path in sorted(scanned_dir.iterdir()): + skill_md = path / "SKILL.md" + if path.is_dir() and skill_md.is_file(): + found.setdefault(path.name, []).append(skill_md) + return found + + +def test_installer_offers_exactly_the_public_skills() -> None: + offered = {name for name, skill_mds in _discovered_skills().items() if any(not INTERNAL_MARKER.search(_frontmatter(skill_md)) for skill_md in skill_mds)} + + assert offered == PUBLIC_SKILLS + + +def test_maintainer_skills_are_marked_internal() -> None: + discovered = _discovered_skills() + assert INTERNAL_SKILLS <= set(discovered) + + for name in INTERNAL_SKILLS: + for skill_md in discovered[name]: + assert INTERNAL_MARKER.search(_frontmatter(skill_md)), f"{skill_md} is missing metadata.internal: true" From 251905522f4586a329c8510fff40715df25a4687 Mon Sep 17 00:00:00 2001 From: Valentin Schmidt Date: Sat, 5 Sep 2026 21:40:19 +0200 Subject: [PATCH 3/4] docs: log the skills CLI install cleanup in STATUS Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H8uhg9be5GbE6QpPHipwta --- STATUS.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/STATUS.md b/STATUS.md index c438894..32670ae 100644 --- a/STATUS.md +++ b/STATUS.md @@ -274,6 +274,23 @@ swept. Not all six need to complete for a defensible thesis submission — see t ## Log +- **2026-09-05** — **`npx skills` install path cleaned up after Beat's feedback.** + Beat's mail called the install instructions far too long and asked us to use + for all local agents. The one-command route + already existed (INSTALL.md Route C, Step 0) but was dirty: the CLI also scans + `.claude/skills/` and `.codex/skills/`, so it found **12** skills — the ten public ones + plus the repo-internal `create-thesis-sim-student` and `run-thesis-simulations`, which + `--skill '*'` happily installed into a student's client. Marked both (in each agent + directory) with `metadata.internal: true`, which the CLI honours for the picker *and* + the `'*'` wildcard (`src/skills.ts`, `src/add.ts` in vercel-labs/skills); the key is + part of the Agent Skills frontmatter spec, so both skills still load repo-locally. + Verified end-to-end: `npx skills@latest add --list` now reports exactly 10, and + a real `--skill '*' --agent claude-code` install produces ten skill folders with their + `references/` intact. Added `skills/tests/test_skills_cli_install.py` (fails if a + maintainer skill loses the flag) and extended `qa.yml`'s path filter to the two agent + skill directories. INSTALL.md itself untouched — shortening it is a separate call. + Branch `feat/skills-cli-install`. + - **2026-07-05** — **Independent 1.0-readiness review completed and actioned; project re-paused with Phase 5 fully scoped.** Ran a from-scratch, deliberately skeptical review (`findings/no_db_universal_skill/2026-07-05-fable-1.0-readiness-review.md`) assessing From 6a50ab27959046357e496c5442feb4b3e96cbfc7 Mon Sep 17 00:00:00 2001 From: Valentin Schmidt Date: Sat, 5 Sep 2026 21:54:09 +0200 Subject: [PATCH 4/4] test: derive the public skill set from skills/ Copilot review on #81: the hard-coded list duplicated test_skill_package.py's EXPECTED_SKILLS and could drift. That test already pins the canonical ten, so this one only needs "nothing outside skills/ is offered to the installer". Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01H8uhg9be5GbE6QpPHipwta --- skills/tests/test_skills_cli_install.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/skills/tests/test_skills_cli_install.py b/skills/tests/test_skills_cli_install.py index 1e4ecb2..b7e2266 100644 --- a/skills/tests/test_skills_cli_install.py +++ b/skills/tests/test_skills_cli_install.py @@ -20,18 +20,6 @@ REPO_ROOT / ".claude" / "skills", REPO_ROOT / ".codex" / "skills", ) -PUBLIC_SKILLS = { - "build-student-profile", - "design-agent-skill", - "discover-company-candidates", - "discover-university-candidates", - "draft-thesis-contact", - "find-company-thesis-options", - "find-recent-papers", - "find-university-chairs", - "generate-thesis-directions", - "thesis-finder", -} INTERNAL_SKILLS = { "create-thesis-sim-student", "run-thesis-simulations", @@ -56,10 +44,16 @@ def _discovered_skills() -> dict[str, list[Path]]: return found +def _public_skills() -> set[str]: + """The published package, as pinned by test_skill_package.py.""" + skills_dir = REPO_ROOT / "skills" + return {path.name for path in skills_dir.iterdir() if path.is_dir() and path.name != "tests" and (path / "SKILL.md").is_file()} + + def test_installer_offers_exactly_the_public_skills() -> None: offered = {name for name, skill_mds in _discovered_skills().items() if any(not INTERNAL_MARKER.search(_frontmatter(skill_md)) for skill_md in skill_mds)} - assert offered == PUBLIC_SKILLS + assert offered == _public_skills() def test_maintainer_skills_are_marked_internal() -> None: