Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/skills/create-thesis-sim-student/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/run-thesis-simulations/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions .codex/skills/create-thesis-sim-student/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions .codex/skills/run-thesis-simulations/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
pull_request:
paths:
- 'skills/**'
- '.claude/skills/**'
- '.codex/skills/**'
- 'scripts/**'
- 'pyproject.toml'
- '.github/workflows/qa.yml'
Expand Down
17 changes: 17 additions & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<https://github.com/vercel-labs/skills> 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 <repo> --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
Expand Down
65 changes: 65 additions & 0 deletions skills/tests/test_skills_cli_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""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",
)
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<body>.*?)\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 _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()


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"
Loading