diff --git a/.claude/skills/create-thesis-sim-student/SKILL.md b/.claude/skills/create-thesis-sim-student/SKILL.md index 77f3a06..9cb6c35 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 21ce448..aa17056 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 77f3a06..9cb6c35 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 21ce448..aa17056 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 diff --git a/.github/workflows/codex-multiturn-evals.yml b/.github/workflows/codex-multiturn-evals.yml index 0da3666..187c04c 100644 --- a/.github/workflows/codex-multiturn-evals.yml +++ b/.github/workflows/codex-multiturn-evals.yml @@ -52,7 +52,7 @@ jobs: python-version: "3.13" - name: Install deterministic test dependencies - run: python -m pip install pytest + run: python -m pip install pytest pyyaml - name: Install optional DeepEval dependency if: ${{ inputs.run_deepeval }} diff --git a/CHANGELOG.md b/CHANGELOG.md index be99546..f8fe347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,12 @@ This project follows Semantic Versioning for the released skill package: ### Added -- ... +- One-command install for skills-directory clients: `npx skills add + Tue-StudyOS/study-os-thesis --skill '*'` (third-party Vercel `skills` CLI), documented as + Route C Step 0 in INSTALL.md. The release archives and the manual copy route are unchanged. + Pinning a release requires the tag as a URL path (`/tree/skills-vX.Y.Z`); the `repo@tag` + shorthand is accepted but ignored. +- Test: every shipped `SKILL.md` frontmatter must parse under a strict YAML reader. ### Changed @@ -21,16 +26,16 @@ This project follows Semantic Versioning for the released skill package: ### Fixed -- ... +- `thesis-finder`'s frontmatter description contained an unquoted `: `, which made it + invalid YAML. Strict parsers — including the `skills` CLI — skipped the entry-point skill + silently, leaving an install that dangles at the first hand-off. +- Repo-local maintainer skills (`create-thesis-sim-student`, `run-thesis-simulations`) are + marked `metadata.internal`, so external installers no longer offer them to students. ### Removed - ... -### Breaking Changes - -- None. - ## [2.1.0] - 2026-08-20 **This release adds two new ways to install the same skills.** Until now the only shape was diff --git a/INSTALL.md b/INSTALL.md index c03b8be..c6b3140 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -28,7 +28,7 @@ outside the skills folder. | | Route A — Claude app | Route B — ChatGPT, Gemini, or any chat | Route C — Claude Code, Codex, Gemini CLI | |---|---|---|---| | For | Claude Pro/Max users | Anyone else, including free ChatGPT and Gemini | People already working in a terminal agent | -| Install | Upload **one zip** | Attach **one file**, paste one text block | Copy **ten folders** into a directory | +| Install | Upload **one zip** | Attach **one file**, paste one text block | One command, or copy **ten folders** into a directory | | Terminal needed | No | No | Yes | All three run the same searches and produce the same output. Whichever you pick, @@ -93,6 +93,40 @@ works, but nothing is remembered once that chat ends. they call each other. A partial install fails at the first hand-off. (Routes A and B have no such trap — everything travels in one artifact.) +## Step 0 — The one-command shortcut (optional) + +If you have **Node 22.20 or newer**, one command installs all ten skills into the client you +use — Claude Code, Codex, Cursor, OpenCode and some seventy others: + +```bash +npx skills@latest add Tue-StudyOS/study-os-thesis --skill '*' --agent claude-code -g +``` + +- `--skill '*'` is **not optional**. Without it you are asked to pick from a list, and any + pick short of all ten hits the hand-off trap above. +- `--agent` names your client — `claude-code`, `codex`, `cursor`, … Leave the flag out and it + asks you. +- `-g` installs for your whole account (`~/.claude/skills/`). Drop it and the skills land in + the current folder only (`./.claude/skills/`), which is what you want if you keep one + directory per thesis search. +- The command tracks `main`. To pin a release instead, give the full URL with the tag in it: + + ```bash + npx skills@latest add https://github.com/Tue-StudyOS/study-os-thesis/tree/skills-v2.1.0 --skill '*' --agent claude-code -g + ``` + + The shorter `repo@tag` form is accepted but the tag is ignored, so you silently get `main`. + +Restart your client afterwards and continue at **Run it**. + +`skills` is a third-party installer made by Vercel, not part of this project. It downloads +this repository and copies the ten folders from `skills/` into your client's skills +directory — by hand, that is exactly Steps 1 and 2 below, and the result is the same files. +If you would rather not run an unfamiliar installer, or you have no Node, just start at +Step 1. + +--- + ## Step 1 — Get the files Download the latest release archive from @@ -232,6 +266,7 @@ single step is the difference between continuing and starting over. | **Routes A/B:** it asks the full interview again on a later visit | The chat was not started inside the project/GPT/Gem, or the session file was never added to it | Open a chat inside the container holding your session file and type `thesis-finder` again. If no session file was ever saved, the interview has to be redone. | | **Route B:** it answers from general knowledge instead of following the instructions | The document was attached but the instructions block was never pasted in | Paste the block from `-----BEGIN INSTRUCTIONS-----` into the container's instructions field. Without it the assistant has no reason to open the document. | | **Route C:** agent finds *some* skills but breaks partway ("I don't have a skill called `discover-university-candidates`"), or the folder listing looks wrong | Wrong folder level, or a partial install | The path must be `/thesis-finder/SKILL.md`, **not** `/study-os-thesis-skills-vX.Y.Z/thesis-finder/SKILL.md`. Move the contents up one level and make sure **all** skill folders are there. | +| **Route C:** `npx skills` refuses to start, or warns `EBADENGINE` | Node is older than 22.20, or missing entirely | Skip Step 0 and install by hand from Step 1 — same files, no Node needed. | | Results are thin, generic, or the agent says it cannot verify anything | No web access, or search quota exhausted | Confirm web search is enabled in your client and that you have quota left. These skills cannot work offline — everything they output is verified live, by design. | --- diff --git a/README.md b/README.md index 3a8d2ee..897fb4a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,23 @@ what the target client can load, not by preference. | **ChatGPT, Gemini** — Projects, Custom GPTs, Gems | `thesis-finder-portable-vX.Y.Z.md` + `-instructions-vX.Y.Z.txt` | One document, named sections | Neither loads Agent Skills at all. Their containers hold an instructions box and a handful of files. | | **Any chat, nothing installed** | the same portable `.md` | Attach and type `thesis-finder` | Works, but nothing is remembered after the chat ends. | +**Or install straight from this repo.** Any client that reads a skills directory can also +skip the release archive and pull `skills/` directly with the third-party +[`skills`](https://github.com/vercel-labs/skills) CLI: + +```bash +npx skills@latest add Tue-StudyOS/study-os-thesis --skill '*' --agent claude-code -g +``` + +It copies the same ten folders into that client's skills directory — the archive route by +other means. Two consequences: `--skill '*'` is required, because a partial pick breaks the +name-based hand-offs, and the CLI installs the **default branch** — pinning a release needs +the tag spelled out as a URL path +(`add https://github.com/Tue-StudyOS/study-os-thesis/tree/skills-v2.1.0`), since the shorter +`repo@tag` form parses but ignores the tag. The CLI parses frontmatter with a strict YAML reader and +silently drops any skill that fails it, so `skills/*/SKILL.md` frontmatter is +`yaml.safe_load`-clean and tested as such. + ### Why three and not one Each client breaks the previous form in a specific way: @@ -184,7 +201,7 @@ independent reference list at all and says so to the student at run time. ## Quality gates -Tests are dependency-free (`pytest` only) and run from the repo root: +Tests need only `pytest` and `pyyaml`, and run from the repo root: ```bash python -m pip install -e ".[dev]" diff --git a/STATUS.md b/STATUS.md index 0065155..9047a56 100644 --- a/STATUS.md +++ b/STATUS.md @@ -370,6 +370,30 @@ architecture-tagged. W optional. ## Log +- **2026-08-30** — **One-command install via the Vercel `skills` CLI — and the frontmatter bug + it exposed.** Branch `feat/skills-cli-install`. `npx skills add Tue-StudyOS/study-os-thesis` + already worked against our layout without any change: the CLI scans `skills//SKILL.md`, + which is what we ship. Running it revealed that it found **11 skills and skipped + `thesis-finder`** — the entry point — with a YAML parse error. Cause: `Supports multi-session + continuity: detects prior searches` in the frontmatter description. An unquoted `: ` inside a + plain scalar is a nested mapping to a strict YAML reader. Claude Code's lenient parser and our + own regex-based bundlers never noticed; `yaml.safe_load` fails on it too. Every external + installer would therefore have handed students exactly the partial install INSTALL.md warns + about. Fixed with an em dash (no quoting, so the bundler regexes are untouched), guarded by a + new strict-YAML test over `skills/`, `.claude/skills/`, and `.codex/skills/` — verified to + fail when the colon is put back. `pyyaml` added to the `dev` extra for it, and the README's + "dependency-free (`pytest` only)" claim corrected. Also marked the two maintainer-only skills + (`create-thesis-sim-student`, `run-thesis-simulations`) `metadata.internal`, which the CLI + honours: the public listing went 12 → 10, and Claude Code still loads them locally. Documented + as Route C **Step 0** in INSTALL.md plus a README paragraph, with the two caveats that matter: + `--skill '*'` is mandatory (partial picks break the hand-offs) and the CLI installs the + **default branch**. The `repo@tag` shorthand parses but silently ignores the tag — caught + by testing the published branch rather than only the local tree; pinning needs the tag as + a URL path (`/tree/skills-v2.1.0`), and refs containing `/` are misparsed there. Verified + end-to-end over the network against the fixed tree: 10 skills, `thesis-finder` present, + no skip warning. Routes A, B, and the manual Route C copy are + untouched — no files moved, no frontmatter quoted, no build script changed. + - **2026-08-20 (d)** — **Task AP: degree programs resolved by rule, not by lookup. The catalog was already lying.** Branch `feat/degree-program-discovery`, off `main`. diff --git a/pyproject.toml b/pyproject.toml index 76cb670..c8eb90b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ license = "MIT" [project.optional-dependencies] dev = [ "pytest>=8", + "pyyaml>=6", ] eval = [ "deepeval", diff --git a/skills/tests/test_skill_package.py b/skills/tests/test_skill_package.py index b36ea43..cfebd06 100644 --- a/skills/tests/test_skill_package.py +++ b/skills/tests/test_skill_package.py @@ -5,6 +5,9 @@ import re from pathlib import Path +import pytest +import yaml + SKILLS_DIR = Path(__file__).resolve().parents[1] REPO_ROOT = SKILLS_DIR.parent @@ -85,6 +88,25 @@ def test_skill_frontmatter_is_portable_and_trigger_rich() -> None: assert set(fields) == {"name", "description"} +def test_every_shipped_skill_frontmatter_parses_as_strict_yaml() -> None: + """External installers (e.g. `npx skills add`) parse frontmatter with a strict YAML + reader and silently skip a skill whose frontmatter fails. An unquoted `: ` inside a + description is enough to drop the entry point from an install.""" + skill_files = [skill_dir / "SKILL.md" for skill_dir in _skill_dirs()] + for agent_dir in (REPO_ROOT / ".claude" / "skills", REPO_ROOT / ".codex" / "skills"): + skill_files.extend(sorted(path for path in agent_dir.glob("*/SKILL.md") if not path.parent.is_symlink())) + + for skill_md in skill_files: + _, _, rest = skill_md.read_text(encoding="utf-8").partition("---\n") + frontmatter, separator, _ = rest.partition("\n---") + assert separator, f"{skill_md} has no closing frontmatter delimiter" + try: + parsed = yaml.safe_load(frontmatter) + except yaml.YAMLError as error: + raise AssertionError(f"{skill_md} frontmatter is not valid YAML: {error}") from error + assert parsed["name"] and parsed["description"] + + def test_referenced_skill_resources_exist() -> None: reference_pattern = re.compile(r"`(references/[^`]+)`") diff --git a/skills/thesis-finder/SKILL.md b/skills/thesis-finder/SKILL.md index f16ed0f..8ea2aa8 100644 --- a/skills/thesis-finder/SKILL.md +++ b/skills/thesis-finder/SKILL.md @@ -1,6 +1,6 @@ --- name: thesis-finder -description: Single entry point for thesis discovery. Helps a student sharpen what kind of thesis fits them and then find where to write it. Builds the student profile through an inline interview if not yet present, then routes to university chair discovery (find-university-chairs), company thesis discovery (find-company-thesis-options), or both, based on student choice. Records the student's own thesis self-understanding before and after the search. Supports multi-session continuity: detects prior searches and resumes without re-interviewing. Use when a student wants to find where to write their thesis — no prior skill invocation needed. +description: Single entry point for thesis discovery. Helps a student sharpen what kind of thesis fits them and then find where to write it. Builds the student profile through an inline interview if not yet present, then routes to university chair discovery (find-university-chairs), company thesis discovery (find-company-thesis-options), or both, based on student choice. Records the student's own thesis self-understanding before and after the search. Supports multi-session continuity — detects prior searches and resumes without re-interviewing. Use when a student wants to find where to write their thesis — no prior skill invocation needed. --- # Thesis Finder