From 8eab812909869119b9adacebe0e9ec06f75d0bd5 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:54:11 +0100 Subject: [PATCH 1/6] Move skills into category directories Co-Authored-By: Claude Fable 5.1 --- skills/{ => frameworks}/cloudinary-next/SKILL.md | 0 .../cloudinary-next/assets/app-router-signature-route.ts | 0 .../cloudinary-next/assets/server-action-delete.ts | 0 .../cloudinary-next/assets/server-action-upload.ts | 0 .../cloudinary-next/references/api-decision-tree.md | 0 .../cloudinary-next/references/cldimage-transformations.md | 0 skills/{ => frameworks}/cloudinary-next/references/cldimage.md | 0 skills/{ => frameworks}/cloudinary-next/references/environment.md | 0 skills/{ => frameworks}/cloudinary-next/references/imports.md | 0 .../{ => frameworks}/cloudinary-next/references/official-docs.md | 0 skills/{ => frameworks}/cloudinary-next/references/og-images.md | 0 skills/{ => frameworks}/cloudinary-next/references/overlays.md | 0 .../{ => frameworks}/cloudinary-next/references/project-setup.md | 0 .../cloudinary-next/references/quick-checklist.md | 0 .../cloudinary-next/references/responsive-images.md | 0 .../cloudinary-next/references/server-upload-delete.md | 0 .../{ => frameworks}/cloudinary-next/references/signed-uploads.md | 0 .../cloudinary-next/references/troubleshooting.md | 0 skills/{ => frameworks}/cloudinary-next/references/typescript.md | 0 .../{ => frameworks}/cloudinary-next/references/upload-widget.md | 0 .../{ => frameworks}/cloudinary-next/references/video-player.md | 0 skills/{ => frameworks}/cloudinary-react/SKILL.md | 0 .../cloudinary-react/references/signed-uploads.md | 0 .../cloudinary-react/references/troubleshooting.md | 0 .../cloudinary-react/references/typescript-patterns.md | 0 .../{ => frameworks}/cloudinary-react/references/video-player.md | 0 skills/{ => platform}/cloudinary-docs/SKILL.md | 0 skills/{ => platform}/cloudinary-transformations/SKILL.md | 0 .../cloudinary-transformations/references/advanced-features.md | 0 .../cloudinary-transformations/references/ai-transformations.md | 0 .../cloudinary-transformations/references/debugging.md | 0 .../cloudinary-transformations/references/examples.md | 0 .../references/named-transformations.md | 0 .../cloudinary-transformations/references/responsive-images.md | 0 .../cloudinary-transformations/references/transformation-costs.md | 0 .../references/video-transformations.md | 0 36 files changed, 0 insertions(+), 0 deletions(-) rename skills/{ => frameworks}/cloudinary-next/SKILL.md (100%) rename skills/{ => frameworks}/cloudinary-next/assets/app-router-signature-route.ts (100%) rename skills/{ => frameworks}/cloudinary-next/assets/server-action-delete.ts (100%) rename skills/{ => frameworks}/cloudinary-next/assets/server-action-upload.ts (100%) rename skills/{ => frameworks}/cloudinary-next/references/api-decision-tree.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/cldimage-transformations.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/cldimage.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/environment.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/imports.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/official-docs.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/og-images.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/overlays.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/project-setup.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/quick-checklist.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/responsive-images.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/server-upload-delete.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/signed-uploads.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/troubleshooting.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/typescript.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/upload-widget.md (100%) rename skills/{ => frameworks}/cloudinary-next/references/video-player.md (100%) rename skills/{ => frameworks}/cloudinary-react/SKILL.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/signed-uploads.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/troubleshooting.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/typescript-patterns.md (100%) rename skills/{ => frameworks}/cloudinary-react/references/video-player.md (100%) rename skills/{ => platform}/cloudinary-docs/SKILL.md (100%) rename skills/{ => platform}/cloudinary-transformations/SKILL.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/advanced-features.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/ai-transformations.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/debugging.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/examples.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/named-transformations.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/responsive-images.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/transformation-costs.md (100%) rename skills/{ => platform}/cloudinary-transformations/references/video-transformations.md (100%) diff --git a/skills/cloudinary-next/SKILL.md b/skills/frameworks/cloudinary-next/SKILL.md similarity index 100% rename from skills/cloudinary-next/SKILL.md rename to skills/frameworks/cloudinary-next/SKILL.md diff --git a/skills/cloudinary-next/assets/app-router-signature-route.ts b/skills/frameworks/cloudinary-next/assets/app-router-signature-route.ts similarity index 100% rename from skills/cloudinary-next/assets/app-router-signature-route.ts rename to skills/frameworks/cloudinary-next/assets/app-router-signature-route.ts diff --git a/skills/cloudinary-next/assets/server-action-delete.ts b/skills/frameworks/cloudinary-next/assets/server-action-delete.ts similarity index 100% rename from skills/cloudinary-next/assets/server-action-delete.ts rename to skills/frameworks/cloudinary-next/assets/server-action-delete.ts diff --git a/skills/cloudinary-next/assets/server-action-upload.ts b/skills/frameworks/cloudinary-next/assets/server-action-upload.ts similarity index 100% rename from skills/cloudinary-next/assets/server-action-upload.ts rename to skills/frameworks/cloudinary-next/assets/server-action-upload.ts diff --git a/skills/cloudinary-next/references/api-decision-tree.md b/skills/frameworks/cloudinary-next/references/api-decision-tree.md similarity index 100% rename from skills/cloudinary-next/references/api-decision-tree.md rename to skills/frameworks/cloudinary-next/references/api-decision-tree.md diff --git a/skills/cloudinary-next/references/cldimage-transformations.md b/skills/frameworks/cloudinary-next/references/cldimage-transformations.md similarity index 100% rename from skills/cloudinary-next/references/cldimage-transformations.md rename to skills/frameworks/cloudinary-next/references/cldimage-transformations.md diff --git a/skills/cloudinary-next/references/cldimage.md b/skills/frameworks/cloudinary-next/references/cldimage.md similarity index 100% rename from skills/cloudinary-next/references/cldimage.md rename to skills/frameworks/cloudinary-next/references/cldimage.md diff --git a/skills/cloudinary-next/references/environment.md b/skills/frameworks/cloudinary-next/references/environment.md similarity index 100% rename from skills/cloudinary-next/references/environment.md rename to skills/frameworks/cloudinary-next/references/environment.md diff --git a/skills/cloudinary-next/references/imports.md b/skills/frameworks/cloudinary-next/references/imports.md similarity index 100% rename from skills/cloudinary-next/references/imports.md rename to skills/frameworks/cloudinary-next/references/imports.md diff --git a/skills/cloudinary-next/references/official-docs.md b/skills/frameworks/cloudinary-next/references/official-docs.md similarity index 100% rename from skills/cloudinary-next/references/official-docs.md rename to skills/frameworks/cloudinary-next/references/official-docs.md diff --git a/skills/cloudinary-next/references/og-images.md b/skills/frameworks/cloudinary-next/references/og-images.md similarity index 100% rename from skills/cloudinary-next/references/og-images.md rename to skills/frameworks/cloudinary-next/references/og-images.md diff --git a/skills/cloudinary-next/references/overlays.md b/skills/frameworks/cloudinary-next/references/overlays.md similarity index 100% rename from skills/cloudinary-next/references/overlays.md rename to skills/frameworks/cloudinary-next/references/overlays.md diff --git a/skills/cloudinary-next/references/project-setup.md b/skills/frameworks/cloudinary-next/references/project-setup.md similarity index 100% rename from skills/cloudinary-next/references/project-setup.md rename to skills/frameworks/cloudinary-next/references/project-setup.md diff --git a/skills/cloudinary-next/references/quick-checklist.md b/skills/frameworks/cloudinary-next/references/quick-checklist.md similarity index 100% rename from skills/cloudinary-next/references/quick-checklist.md rename to skills/frameworks/cloudinary-next/references/quick-checklist.md diff --git a/skills/cloudinary-next/references/responsive-images.md b/skills/frameworks/cloudinary-next/references/responsive-images.md similarity index 100% rename from skills/cloudinary-next/references/responsive-images.md rename to skills/frameworks/cloudinary-next/references/responsive-images.md diff --git a/skills/cloudinary-next/references/server-upload-delete.md b/skills/frameworks/cloudinary-next/references/server-upload-delete.md similarity index 100% rename from skills/cloudinary-next/references/server-upload-delete.md rename to skills/frameworks/cloudinary-next/references/server-upload-delete.md diff --git a/skills/cloudinary-next/references/signed-uploads.md b/skills/frameworks/cloudinary-next/references/signed-uploads.md similarity index 100% rename from skills/cloudinary-next/references/signed-uploads.md rename to skills/frameworks/cloudinary-next/references/signed-uploads.md diff --git a/skills/cloudinary-next/references/troubleshooting.md b/skills/frameworks/cloudinary-next/references/troubleshooting.md similarity index 100% rename from skills/cloudinary-next/references/troubleshooting.md rename to skills/frameworks/cloudinary-next/references/troubleshooting.md diff --git a/skills/cloudinary-next/references/typescript.md b/skills/frameworks/cloudinary-next/references/typescript.md similarity index 100% rename from skills/cloudinary-next/references/typescript.md rename to skills/frameworks/cloudinary-next/references/typescript.md diff --git a/skills/cloudinary-next/references/upload-widget.md b/skills/frameworks/cloudinary-next/references/upload-widget.md similarity index 100% rename from skills/cloudinary-next/references/upload-widget.md rename to skills/frameworks/cloudinary-next/references/upload-widget.md diff --git a/skills/cloudinary-next/references/video-player.md b/skills/frameworks/cloudinary-next/references/video-player.md similarity index 100% rename from skills/cloudinary-next/references/video-player.md rename to skills/frameworks/cloudinary-next/references/video-player.md diff --git a/skills/cloudinary-react/SKILL.md b/skills/frameworks/cloudinary-react/SKILL.md similarity index 100% rename from skills/cloudinary-react/SKILL.md rename to skills/frameworks/cloudinary-react/SKILL.md diff --git a/skills/cloudinary-react/references/signed-uploads.md b/skills/frameworks/cloudinary-react/references/signed-uploads.md similarity index 100% rename from skills/cloudinary-react/references/signed-uploads.md rename to skills/frameworks/cloudinary-react/references/signed-uploads.md diff --git a/skills/cloudinary-react/references/troubleshooting.md b/skills/frameworks/cloudinary-react/references/troubleshooting.md similarity index 100% rename from skills/cloudinary-react/references/troubleshooting.md rename to skills/frameworks/cloudinary-react/references/troubleshooting.md diff --git a/skills/cloudinary-react/references/typescript-patterns.md b/skills/frameworks/cloudinary-react/references/typescript-patterns.md similarity index 100% rename from skills/cloudinary-react/references/typescript-patterns.md rename to skills/frameworks/cloudinary-react/references/typescript-patterns.md diff --git a/skills/cloudinary-react/references/video-player.md b/skills/frameworks/cloudinary-react/references/video-player.md similarity index 100% rename from skills/cloudinary-react/references/video-player.md rename to skills/frameworks/cloudinary-react/references/video-player.md diff --git a/skills/cloudinary-docs/SKILL.md b/skills/platform/cloudinary-docs/SKILL.md similarity index 100% rename from skills/cloudinary-docs/SKILL.md rename to skills/platform/cloudinary-docs/SKILL.md diff --git a/skills/cloudinary-transformations/SKILL.md b/skills/platform/cloudinary-transformations/SKILL.md similarity index 100% rename from skills/cloudinary-transformations/SKILL.md rename to skills/platform/cloudinary-transformations/SKILL.md diff --git a/skills/cloudinary-transformations/references/advanced-features.md b/skills/platform/cloudinary-transformations/references/advanced-features.md similarity index 100% rename from skills/cloudinary-transformations/references/advanced-features.md rename to skills/platform/cloudinary-transformations/references/advanced-features.md diff --git a/skills/cloudinary-transformations/references/ai-transformations.md b/skills/platform/cloudinary-transformations/references/ai-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/ai-transformations.md rename to skills/platform/cloudinary-transformations/references/ai-transformations.md diff --git a/skills/cloudinary-transformations/references/debugging.md b/skills/platform/cloudinary-transformations/references/debugging.md similarity index 100% rename from skills/cloudinary-transformations/references/debugging.md rename to skills/platform/cloudinary-transformations/references/debugging.md diff --git a/skills/cloudinary-transformations/references/examples.md b/skills/platform/cloudinary-transformations/references/examples.md similarity index 100% rename from skills/cloudinary-transformations/references/examples.md rename to skills/platform/cloudinary-transformations/references/examples.md diff --git a/skills/cloudinary-transformations/references/named-transformations.md b/skills/platform/cloudinary-transformations/references/named-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/named-transformations.md rename to skills/platform/cloudinary-transformations/references/named-transformations.md diff --git a/skills/cloudinary-transformations/references/responsive-images.md b/skills/platform/cloudinary-transformations/references/responsive-images.md similarity index 100% rename from skills/cloudinary-transformations/references/responsive-images.md rename to skills/platform/cloudinary-transformations/references/responsive-images.md diff --git a/skills/cloudinary-transformations/references/transformation-costs.md b/skills/platform/cloudinary-transformations/references/transformation-costs.md similarity index 100% rename from skills/cloudinary-transformations/references/transformation-costs.md rename to skills/platform/cloudinary-transformations/references/transformation-costs.md diff --git a/skills/cloudinary-transformations/references/video-transformations.md b/skills/platform/cloudinary-transformations/references/video-transformations.md similarity index 100% rename from skills/cloudinary-transformations/references/video-transformations.md rename to skills/platform/cloudinary-transformations/references/video-transformations.md From 62490331819e8a5bbedcd25253adfd71425b8a99 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:55:04 +0100 Subject: [PATCH 2/6] Add skills discovery check Runs the real skills CLI against the checkout and fails when the number of discovered skills differs from the number of SKILL.md files, so a skill dropped by bad frontmatter or wrong depth cannot merge silently. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/skills-discovery.yml diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml new file mode 100644 index 0000000..12fd496 --- /dev/null +++ b/.github/workflows/skills-discovery.yml @@ -0,0 +1,26 @@ +name: Skills discovery +on: + pull_request: + push: + branches: [main] +jobs: + discovery: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Every SKILL.md is discovered by the skills CLI + env: + INSTALL_INTERNAL_SKILLS: "1" + run: | + expected=$(find skills -name SKILL.md | wc -l | tr -d ' ') + output=$(npx -y skills@latest add . --list 2>&1 || true) + found=$(printf '%s' "$output" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) + echo "expected=$expected found=$found" + if [ "$found" != "$expected" ]; then + echo "::error::Discovery mismatch. A SKILL.md is being dropped (bad frontmatter, wrong depth, or missing name/description)." + printf '%s\n' "$output" + exit 1 + fi From bbb8972f6bd9f9e8ed57f3f853b5a1ec039fd029 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 13:55:40 +0100 Subject: [PATCH 3/6] Document catalog layout and authoring conventions README groups the skills table by category, leads the install section with --skill examples. CONTRIBUTING.md records the layout, frontmatter, description-shape, cross-reference, versioning and local-check rules. specs/ is gitignored. Co-Authored-By: Claude Fable 5.1 --- .gitignore | 1 + CONTRIBUTING.md | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 20 +++++++++- 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6de900 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +specs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cb69f05 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,99 @@ +# Contributing + +Rules for adding or changing skills in this repo. The `skills` CLI is unforgiving +about layout and frontmatter: a skill that breaks these rules is dropped from +discovery with no warning, so read this before opening a PR. + +## Layout + +Every skill lives at `skills///SKILL.md`, exactly two directories +deep. The CLI does not look any deeper, so a skill nested at depth three is +invisible to users. + +Categories: + +- `platform` — reference skills for a Cloudinary capability (uploads, + transformations, docs lookup) +- `frameworks` — patterns for using Cloudinary in a specific framework or SDK +- `use-cases` — end-to-end recipes that combine several capabilities +- `utilities` — tools that do a job using Cloudinary + +Category directories are created with their first skill. Do not add a README or +placeholder to an empty category. + +Users install skills by name, not by path, so the category is invisible to them. +Moving a skill between categories does not affect existing installs. + +## Frontmatter + +```yaml +--- +name: cloudinary-example +description: "Reference for . Use when ." +license: MIT +metadata: + author: cloudinary + version: '1.0.0' +--- +``` + +- `name` must equal the directory name. +- `name` and `description` must both be strings. If either is missing, or the + frontmatter fails to parse, the skill silently vanishes from discovery. +- A description containing a colon must be quoted. Unquoted, YAML reads the colon + as a nested key, the description is no longer a string, and the skill vanishes. +- Include `license`, `metadata.author`, and `metadata.version` (semver). +- Set `metadata.internal: true` on a work-in-progress skill that should not appear + in the default install. Users can reveal it with `INSTALL_INTERNAL_SKILLS=1`. + +## Description shapes + +Agents see the catalog as a flat list of names and descriptions, so every +description does two jobs. It opens with a clause that self-locates the skill in +that list, then follows with a "Use when..." sentence that carries the trigger +phrases. + +| Category | Opening shape | +|---|---| +| Platform | `Reference for .` | +| Frameworks | `Patterns for using Cloudinary in .` | +| Use-case | `End-to-end recipe for .` | +| Utility | `Tool that using Cloudinary.` | + +Rules: + +- The opening clause never contains a colon before its first period unless the + whole description is quoted. +- Trigger phrases live in the "Use when" sentence, not in the opening clause. + +Existing skills predate these shapes and are not yet retrofitted. New skills must +follow them. + +## Referencing other skills + +Refer to another skill by its name only, for example "use cloudinary-docs for +anything outside this skill's scope". Never reference another skill by file path. +Installed paths differ per agent and per install scope, so a path that works on +your machine will be wrong on someone else's. + +## Versioning + +Bump `metadata.version` on any content change to a skill, including its +references and assets. Moving or renaming files with identical content does not +bump the version; the lock-file hash is unchanged and `skills update` correctly +reports nothing to do. + +## Local check + +Before opening a PR, run the same check CI runs: + +```bash +INSTALL_INTERNAL_SKILLS=1 npx -y skills@latest add . --list +``` + +The `Found N skills` line must match the number of `SKILL.md` files under +`skills/`, and every skill name must appear in the list. A mismatch means a skill +is being dropped: check for bad frontmatter, an unquoted colon in the +description, a missing `name` or `description`, or a directory nested at the wrong +depth. CI (`.github/workflows/skills-discovery.yml`) fails the PR on the same +mismatch. diff --git a/README.md b/README.md index c9fe919..04d8bed 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,38 @@ The agent skills in this package help your AI coding assistant write correct Clo ## Available Skills +Skills are grouped by category below. The category is a folder in this repo only: you install a skill by its name, and the folder layout has no effect on how it is installed or used. + +### Platform + | Skill | Description | |---|---| | `cloudinary-docs` | Selects the most relevant markdown pages from the current documentation using the latest llms.txt. Use when answering Cloudinary questions or integrating Cloudinary into code. | | `cloudinary-transformations` | Turns natural language image and video transformation requirements into valid URL transformation strings that follow Cloudinary best practices. Use when building delivery URLs, applying transformations, optimizing media, or debugging transformation syntax errors. | + +### Frameworks + +| Skill | Description | +|---|---| | `cloudinary-react` | Provides opinionated React SDK patterns for configuration, common integration scenarios, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing React apps with Cloudinary. | | `cloudinary-next` | Provides opinionated Next.js SDK patterns for Server and Client Component boundaries, server-side uploads and deletes, and troubleshooting for frequent errors and TypeScript pitfalls. Use when developing Next.js apps with Cloudinary. | ## Install +Install the skills you need by name: + +```bash +npx skills add cloudinary-devs/skills --skill cloudinary-docs,cloudinary-transformations +npx skills add cloudinary-devs/skills --skill cloudinary-next +``` + +Or install everything: + ```bash npx skills add cloudinary-devs/skills ``` -Install all skills or select the ones you need. You can install globally or per project. +You can install globally or per project. > **💡 Tip:** [Cloudinary MCP servers](https://cloudinary.com/documentation/cloudinary_llm_mcp#mcp_servers) complement the Cloudinary Skills. Used together, skills and MCP servers improve the end-to-end workflow. For example, our skills spell out best practices such as using named transformations to apply the same transformation across many assets. Based on that, the model can decide which MCP tools to run to create named transformations that match your needs. From 4ba491fa054c031ae4116f7d5d6fff0f0d6f087a Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Fri, 4 Sep 2026 14:39:27 +0100 Subject: [PATCH 4/6] Disable colour in discovery check output GitHub Actions sets CI, which makes the skills CLI colour its output, so 'Found 5 skills' arrived as 'Found \e[32m5\e[39m skills' and the grep never matched (found=0, expected=5). Set NO_COLOR and strip any remaining ANSI escapes before grepping. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml index 12fd496..d87bace 100644 --- a/.github/workflows/skills-discovery.yml +++ b/.github/workflows/skills-discovery.yml @@ -14,10 +14,13 @@ jobs: - name: Every SKILL.md is discovered by the skills CLI env: INSTALL_INTERNAL_SKILLS: "1" + NO_COLOR: "1" run: | expected=$(find skills -name SKILL.md | wc -l | tr -d ' ') output=$(npx -y skills@latest add . --list 2>&1 || true) - found=$(printf '%s' "$output" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) + # Strip ANSI escapes: the CLI colours its output whenever CI is set. + plain=$(printf '%s' "$output" | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g') + found=$(printf '%s' "$plain" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) echo "expected=$expected found=$found" if [ "$found" != "$expected" ]; then echo "::error::Discovery mismatch. A SKILL.md is being dropped (bad frontmatter, wrong depth, or missing name/description)." From 90cb20e3949ee5377a7eae089416e76c6ca28861 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Mon, 7 Sep 2026 12:21:24 +0100 Subject: [PATCH 5/6] Add link-tracking and version-bump checks Two new CI jobs alongside discovery, both backed by scripts that run locally too: - links: every cloudinary.com link to a .md page or llms.txt must carry install_source and referrer, so docs traffic from skills is attributable. - versions: on pull requests, any skill whose content changed against the base must bump metadata.version. Pure moves with identical content are ignored, so restructures pass; new skills pass. CONTRIBUTING documents both rules and the local commands. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 20 ++++++++- CONTRIBUTING.md | 20 +++++++-- scripts/check-links.sh | 25 +++++++++++ scripts/check-versions.sh | 58 ++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100755 scripts/check-links.sh create mode 100755 scripts/check-versions.sh diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml index d87bace..18bbf13 100644 --- a/.github/workflows/skills-discovery.yml +++ b/.github/workflows/skills-discovery.yml @@ -1,10 +1,11 @@ -name: Skills discovery +name: Skills checks on: pull_request: push: branches: [main] jobs: discovery: + name: Every SKILL.md is discovered runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -27,3 +28,20 @@ jobs: printf '%s\n' "$output" exit 1 fi + + links: + name: Doc links carry tracking params + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: scripts/check-links.sh + + versions: + name: Changed skills bump their version + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: scripts/check-versions.sh "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb69f05..dd79912 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,19 +81,31 @@ your machine will be wrong on someone else's. Bump `metadata.version` on any content change to a skill, including its references and assets. Moving or renaming files with identical content does not bump the version; the lock-file hash is unchanged and `skills update` correctly -reports nothing to do. +reports nothing to do. CI enforces this on every PR: a skill with changed +content and an unchanged version fails the `versions` job. + +## Documentation links + +Every link to a Cloudinary docs `.md` page or `llms.txt` must carry +`?install_source=skillspack&referrer=-skill` so traffic from skills is +attributable. Use the same `referrer` value throughout a skill, for example +`trans-skill` or `react-skill`. CI fails the `links` job on any link missing +either param. ## Local check -Before opening a PR, run the same check CI runs: +Before opening a PR, run the same checks CI runs: ```bash INSTALL_INTERNAL_SKILLS=1 npx -y skills@latest add . --list +scripts/check-links.sh +scripts/check-versions.sh origin/main HEAD ``` The `Found N skills` line must match the number of `SKILL.md` files under `skills/`, and every skill name must appear in the list. A mismatch means a skill is being dropped: check for bad frontmatter, an unquoted colon in the description, a missing `name` or `description`, or a directory nested at the wrong -depth. CI (`.github/workflows/skills-discovery.yml`) fails the PR on the same -mismatch. +depth. The two scripts report any untracked doc link and any changed skill whose +version was not bumped. CI (`.github/workflows/skills-discovery.yml`) runs all +three and fails the PR on the same conditions. diff --git a/scripts/check-links.sh b/scripts/check-links.sh new file mode 100755 index 0000000..4f9fc2c --- /dev/null +++ b/scripts/check-links.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Every cloudinary.com link to a .md page or llms.txt must carry the +# install_source and referrer query params so docs traffic from skills is +# attributable. Usage: scripts/check-links.sh [dir] (default: skills) +set -u +dir="${1:-skills}" +url_re='https?://[A-Za-z0-9.-]*cloudinary\.com/[^[:space:])>"'"'"'`]*(\.md|/llms\.txt)([?#][^[:space:])>"'"'"'`]*)?' + +fail=0 +while IFS= read -r hit; do + file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; url="${rest#*:}" + missing="" + case "$url" in (*install_source=*) ;; (*) missing="install_source";; esac + case "$url" in (*referrer=*) ;; (*) missing="${missing:+$missing, }referrer";; esac + if [ -n "$missing" ]; then + echo "::error file=$file,line=$line::Missing $missing on $url" + fail=1 + fi +done < <(grep -rnoE "$url_re" "$dir" --include='*.md' --include='*.txt' --include='*.ts' --include='*.tsx' --include='*.js' | LC_ALL=C sort -u) + +if [ "$fail" -ne 0 ]; then + echo "Add '?install_source=skillspack&referrer=-skill' to each link above (see CONTRIBUTING.md)." + exit 1 +fi +echo "All .md and llms.txt links carry install_source and referrer." diff --git a/scripts/check-versions.sh b/scripts/check-versions.sh new file mode 100755 index 0000000..b67a41c --- /dev/null +++ b/scripts/check-versions.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Any skill whose content changed between BASE and HEAD must bump +# metadata.version in its SKILL.md. Pure moves/renames (identical content) do +# not count as changes. New skills pass. Usage: scripts/check-versions.sh BASE HEAD +set -u +base="${1:?base ref}"; head="${2:?head ref}" + +version_at() { # -> version string or empty + git show "$1:$2" 2>/dev/null | sed -nE "s/^ version: *['\"]?([^'\"]+)['\"]?[[:space:]]*$/\1/p" | head -1 +} +skill_name_of() { # -> name of the skill (dir holding nearest SKILL.md), or empty + local d; d=$(dirname "$2") + while [ "$d" != "." ] && [ "$d" != "skills" ]; do + if git cat-file -e "$1:$d/SKILL.md" 2>/dev/null; then basename "$d"; return; fi + d=$(dirname "$d") + done +} +skill_md_in() { # -> path of that skill's SKILL.md in ref, or empty + git ls-tree -r --name-only "$1" -- skills | grep -E "(^|/)$2/SKILL\.md$" | head -1 +} + +# Collect the names of skills touched by non-identical changes. +changed_names() { + git diff --name-status -M100% "$base" "$head" -- skills | while read -r status p1 p2; do + case "$status" in + (R100) continue ;; # pure move, identical content + (D) skill_name_of "$base" "$p1" ;; # deletion: file lived in base + (R*) skill_name_of "$head" "$p2" ;; + (*) skill_name_of "$head" "$p1" ;; + esac + done | sort -u +} +names=$(changed_names) + +if [ -z "$names" ]; then + echo "No skill content changed." + exit 0 +fi + +fail=0 +for name in $names; do + head_md=$(skill_md_in "$head" "$name") + base_md=$(skill_md_in "$base" "$name") + if [ -z "$head_md" ]; then echo "skip $name: removed"; continue; fi + hv=$(version_at "$head" "$head_md") + if [ -z "$base_md" ]; then echo "ok $name: new skill ($hv)"; continue; fi + bv=$(version_at "$base" "$base_md") + if [ -z "$hv" ]; then + echo "::error file=$head_md::$name has no metadata.version"; fail=1; continue + fi + if [ "$bv" = "$hv" ]; then + echo "::error file=$head_md,line=7::$name changed but metadata.version is still $hv. Bump it (see CONTRIBUTING.md, Versioning)." + fail=1 + else + echo "ok $name: $bv -> $hv" + fi +done +exit $fail From 341c5c8314c9f3547b148d8728d19e325775f152 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Wed, 16 Sep 2026 15:23:21 +0100 Subject: [PATCH 6/6] Assert discovered skill names and fail link check on zero matches Review feedback on #22: - Discovery check now compares the frontmatter name of every SKILL.md against the names the skills CLI lists, instead of just the count. A skill nested too deep or with broken frontmatter is reported by name. Moved out of the workflow into scripts/check-discovery.sh so it can be run locally like the other two checks. - check-links.sh fails when it matches no URLs at all, so an empty or mis-targeted run cannot pass as a false success. Success output now includes the number of links checked. - CONTRIBUTING local-check section updated to match. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/skills-discovery.yml | 17 +-------- CONTRIBUTING.md | 13 +++---- scripts/check-discovery.sh | 50 ++++++++++++++++++++++++++ scripts/check-links.sh | 8 ++++- 4 files changed, 65 insertions(+), 23 deletions(-) create mode 100755 scripts/check-discovery.sh diff --git a/.github/workflows/skills-discovery.yml b/.github/workflows/skills-discovery.yml index 18bbf13..30d8b82 100644 --- a/.github/workflows/skills-discovery.yml +++ b/.github/workflows/skills-discovery.yml @@ -12,22 +12,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 - - name: Every SKILL.md is discovered by the skills CLI - env: - INSTALL_INTERNAL_SKILLS: "1" - NO_COLOR: "1" - run: | - expected=$(find skills -name SKILL.md | wc -l | tr -d ' ') - output=$(npx -y skills@latest add . --list 2>&1 || true) - # Strip ANSI escapes: the CLI colours its output whenever CI is set. - plain=$(printf '%s' "$output" | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g') - found=$(printf '%s' "$plain" | grep -oE 'Found [0-9]+ skills?' | grep -oE '[0-9]+' || echo 0) - echo "expected=$expected found=$found" - if [ "$found" != "$expected" ]; then - echo "::error::Discovery mismatch. A SKILL.md is being dropped (bad frontmatter, wrong depth, or missing name/description)." - printf '%s\n' "$output" - exit 1 - fi + - run: scripts/check-discovery.sh links: name: Doc links carry tracking params diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd79912..9e2aa71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -97,15 +97,16 @@ either param. Before opening a PR, run the same checks CI runs: ```bash -INSTALL_INTERNAL_SKILLS=1 npx -y skills@latest add . --list +scripts/check-discovery.sh scripts/check-links.sh scripts/check-versions.sh origin/main HEAD ``` -The `Found N skills` line must match the number of `SKILL.md` files under -`skills/`, and every skill name must appear in the list. A mismatch means a skill -is being dropped: check for bad frontmatter, an unquoted colon in the -description, a missing `name` or `description`, or a directory nested at the wrong -depth. The two scripts report any untracked doc link and any changed skill whose +The discovery script compares the `name` in every `SKILL.md` under `skills/` +against the names the skills CLI actually lists, and names any skill that is +missing. A missing skill means it is being dropped: check for bad frontmatter, +an unquoted colon in the description, a missing `name` or `description`, or a +directory nested at the wrong depth. The other two scripts report any untracked +doc link (and fail if they find no links at all) and any changed skill whose version was not bumped. CI (`.github/workflows/skills-discovery.yml`) runs all three and fails the PR on the same conditions. diff --git a/scripts/check-discovery.sh b/scripts/check-discovery.sh new file mode 100755 index 0000000..4f112c8 --- /dev/null +++ b/scripts/check-discovery.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Every SKILL.md under skills/ must be discovered by the skills CLI, by name. +# The CLI drops a skill silently (bad frontmatter, wrong depth, missing +# name/description) whenever at least one other skill is found, so a count +# alone is not enough. Usage: scripts/check-discovery.sh [dir] (default: .) +set -u +dir="${1:-.}" + +# Expected: the frontmatter name of every SKILL.md. +expected=$(find "$dir/skills" -name SKILL.md | while IFS= read -r f; do + # First name: key inside the leading frontmatter block only. + sed -nE '1,/^---$/{s/^name:[[:space:]]*['"'"'"]?([^'"'"'"]+)['"'"'"]?[[:space:]]*$/\1/p;}' "$f" | head -1 +done | LC_ALL=C sort -u) + +if [ -z "$expected" ]; then + echo "::error::No SKILL.md with a name found under $dir/skills." + exit 1 +fi + +output=$(INSTALL_INTERNAL_SKILLS=1 NO_COLOR=1 npx -y skills@latest add "$dir" --list 2>&1 || true) +# Strip ANSI escapes (the CLI colours output whenever CI is set), then keep +# the lines between "Available Skills" and the trailing hint. Skill names are +# printed alone on a line; descriptions are multi-word, so a line that is a +# single slug token is a skill name. +found=$(printf '%s\n' "$output" \ + | sed -E 's/\x1b\[[0-9;?]*[a-zA-Z]//g' \ + | sed -n '/Available Skills/,/Use --skill/p' \ + | sed -E 's/^[^[:alnum:]]*//; s/[[:space:]]+$//' \ + | grep -xE '[a-z0-9][a-z0-9._-]*' \ + | LC_ALL=C sort -u) + +missing=$(comm -23 <(printf '%s\n' "$expected") <(printf '%s\n' "$found")) +extra=$(comm -13 <(printf '%s\n' "$expected") <(printf '%s\n' "$found")) + +if [ -n "$missing" ] || [ -n "$extra" ]; then + for n in $missing; do + echo "::error::Skill '$n' has a SKILL.md but is not discovered by the skills CLI (bad frontmatter, wrong depth, or missing name/description)." + done + for n in $extra; do + echo "::error::Skills CLI reports '$n' but no SKILL.md declares that name." + done + echo "--- expected ---"; printf '%s\n' "$expected" + echo "--- found ---"; printf '%s\n' "$found" + echo "--- cli output ---"; printf '%s\n' "$output" + exit 1 +fi + +count=$(printf '%s\n' "$expected" | wc -l | tr -d ' ') +echo "All $count skills discovered by name:" +printf ' %s\n' $expected diff --git a/scripts/check-links.sh b/scripts/check-links.sh index 4f9fc2c..0889eab 100755 --- a/scripts/check-links.sh +++ b/scripts/check-links.sh @@ -7,7 +7,9 @@ dir="${1:-skills}" url_re='https?://[A-Za-z0-9.-]*cloudinary\.com/[^[:space:])>"'"'"'`]*(\.md|/llms\.txt)([?#][^[:space:])>"'"'"'`]*)?' fail=0 +checked=0 while IFS= read -r hit; do + checked=$((checked + 1)) file="${hit%%:*}"; rest="${hit#*:}"; line="${rest%%:*}"; url="${rest#*:}" missing="" case "$url" in (*install_source=*) ;; (*) missing="install_source";; esac @@ -18,8 +20,12 @@ while IFS= read -r hit; do fi done < <(grep -rnoE "$url_re" "$dir" --include='*.md' --include='*.txt' --include='*.ts' --include='*.tsx' --include='*.js' | LC_ALL=C sort -u) +if [ "$checked" -eq 0 ]; then + echo "::error::No cloudinary.com .md or llms.txt links found under '$dir'. The check matched nothing, so it cannot pass." + exit 1 +fi if [ "$fail" -ne 0 ]; then echo "Add '?install_source=skillspack&referrer=-skill' to each link above (see CONTRIBUTING.md)." exit 1 fi -echo "All .md and llms.txt links carry install_source and referrer." +echo "All $checked .md and llms.txt links carry install_source and referrer."