+
+ Editorial method:
+ {" "}
+ every figure on this page comes from a source you can open, and
+ carries the day it was read. Nothing here is estimated, modelled, or
+ taken from private telemetry. Where a question cannot be answered
+ from a public source, the limits section says so instead of filling
+ the gap.
+
+ Free forever, MIT licensed, and open source. Create a library, save
+ the skills your team actually uses, and stop guessing which one a
+ teammate should open.
+
+ >
+ )
+}
diff --git a/components/resources/resource-chrome.tsx b/components/resources/resource-chrome.tsx
index d3d766d9..d2aef268 100644
--- a/components/resources/resource-chrome.tsx
+++ b/components/resources/resource-chrome.tsx
@@ -9,6 +9,7 @@ import { FooterNavColumns } from "@/components/footer-nav"
import { ThemeToggle } from "@/components/theme-toggle"
import { TrackedLink } from "@/components/tracked-link"
import { Button } from "@/components/ui/button"
+import type { AgentSkillsByTheNumbersCtaPlacement } from "@/lib/seo/agent-skills-by-the-numbers/types"
import type { AgentSkillsSupportCtaPlacement } from "@/lib/seo/agent-skills-support/types"
import type { AgentSkillsCtaPlacement } from "@/lib/seo/agent-skills/types"
import type { AgentsMdVsSkillMdCtaPlacement } from "@/lib/seo/agents-md-vs-skill-md/types"
@@ -38,6 +39,7 @@ type ResourceHeaderLocation =
| "about_header"
| "connect_header"
| "developers_header"
+ | "agent_skills_by_the_numbers_header"
| "agent_skills_header"
| "agent_skills_support_header"
| "agents_md_header"
@@ -62,6 +64,7 @@ type ResourceHeaderLocation =
| "resources_header"
| "where_skills_header"
type ResourceCtaLocation =
+ | AgentSkillsByTheNumbersCtaPlacement
| AgentSkillsCtaPlacement
| AgentSkillsSupportCtaPlacement
| AgentsMdVsSkillMdCtaPlacement
diff --git a/lib/markdown/content-markdown.ts b/lib/markdown/content-markdown.ts
index 7523f2cd..8581a0b8 100644
--- a/lib/markdown/content-markdown.ts
+++ b/lib/markdown/content-markdown.ts
@@ -76,6 +76,7 @@ const unlabeledKeys = new Set([
"steps",
"entries",
"rows",
+ "tables",
"link",
"template",
"tree",
diff --git a/lib/seo/agent-skills-by-the-numbers/datapoints.ts b/lib/seo/agent-skills-by-the-numbers/datapoints.ts
new file mode 100644
index 00000000..c8f16600
--- /dev/null
+++ b/lib/seo/agent-skills-by-the-numbers/datapoints.ts
@@ -0,0 +1,245 @@
+/**
+ * Every published figure on /agent-skills-by-the-numbers, in one place.
+ *
+ * The page is dated and refreshed monthly. Keeping the numbers here rather
+ * than in the copy or the JSX means a refresh is an edit to this file: change
+ * `value`, change `measuredOn`, and the tables, the Markdown twin, the JSON-LD
+ * dates, and the tests all move with it. Prose in `index.ts` never repeats a
+ * figure it cannot reach from here.
+ *
+ * Rules for adding a row:
+ * - every figure comes from a source a reader can open. Nothing on this page
+ * is measured from this site's own instrumentation.
+ * - `sourceId` has to exist in `agentSkillsByTheNumbers.sources`, or the page
+ * fails its own test.
+ * - `measuredOn` is the day the figure was read, not the day it was written.
+ * - a figure that could not be read on refresh day is deleted, not carried
+ * forward with an old date.
+ */
+
+export interface AdoptionDatapoint {
+ /** Stable key, used by the tests and by the prose that refers to a row. */
+ id: string
+ /** Row label in the table. */
+ label: string
+ /** The figure, already formatted for display. */
+ value: string
+ /** What the figure counts, or for an install row, where the skill lives. */
+ detail: string
+ /** ISO date the figure was read from its source. */
+ measuredOn: string
+ /** Id of the entry in the page's source list. */
+ sourceId: string
+}
+
+/** The day the refresh behind this revision read its sources. */
+export const readOn = "2026-09-02" as const
+
+/** How many agent products read a SKILL.md file, at two levels of evidence. */
+export const clientDatapoints: readonly AdoptionDatapoint[] = [
+ {
+ id: "showcase-clients",
+ label: "Products on the Agent Skills client showcase",
+ value: "46",
+ detail:
+ "Agent products listed on the Client Showcase published with the Agent Skills specification, counted from the entries on the published page. Vendors list themselves there, so the figure records declared support rather than tested behavior, and it says nothing about how many people use the feature inside each product.",
+ measuredOn: "2026-09-02",
+ sourceId: "agentskills-clients",
+ },
+ {
+ id: "documented-clients",
+ label: "Of those, products whose own documentation states it",
+ value: "11",
+ detail:
+ "Claude Code, the Claude apps and Claude API, Claude Cowork, Codex, Cursor, GitHub Copilot, VS Code, Gemini CLI, OpenCode, goose, and Amp. Each vendor's own published documentation was fetched and read on August 20, 2026, and the eleven rows sit in the register behind our compatibility matrix. The other thirty-five showcase entries were not checked at that level.",
+ measuredOn: "2026-08-20",
+ sourceId: "skillsboard-support-register",
+ },
+]
+
+/** The size of the public directory of installable skills. */
+export const directoryDatapoints: readonly AdoptionDatapoint[] = [
+ {
+ id: "skills-sh-total",
+ label: "Skills on the public skills.sh directory",
+ value: "9,704",
+ detail:
+ "The totalSkills figure the directory reports in its own home page data. A skill enters the count once somebody installs it at least once through the npx skills add command, and forks and copies are flagged as separate entries rather than merged into the original. A skill kept inside a company, passed around as a copied folder, or installed by pointing an agent straight at a repository never appears.",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+]
+
+/**
+ * The eight most installed skills on that directory, in its own order. The
+ * only per-skill usage figures anyone publishes for this format.
+ */
+export const installDatapoints: readonly AdoptionDatapoint[] = [
+ {
+ id: "install-find-skills",
+ label: "find-skills",
+ value: "3,220,754",
+ detail: "vercel-labs/skills, flagged official",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-grill-me",
+ label: "grill-me",
+ value: "1,037,417",
+ detail: "mattpocock/skills",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-grill-with-docs",
+ label: "grill-with-docs",
+ value: "884,311",
+ detail: "mattpocock/skills",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-improve-codebase-architecture",
+ label: "improve-codebase-architecture",
+ value: "849,307",
+ detail: "mattpocock/skills",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-frontend-design",
+ label: "frontend-design",
+ value: "845,287",
+ detail: "anthropics/skills, flagged official",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-tdd",
+ label: "tdd",
+ value: "821,677",
+ detail: "mattpocock/skills",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-agent-browser",
+ label: "agent-browser",
+ value: "773,602",
+ detail: "vercel-labs/agent-browser, flagged official",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+ {
+ id: "install-setup-matt-pocock-skills",
+ label: "setup-matt-pocock-skills",
+ value: "757,707",
+ detail: "mattpocock/skills",
+ measuredOn: "2026-09-02",
+ sourceId: "skills-sh",
+ },
+]
+
+/**
+ * The repositories the format is written against, and the attention they have
+ * collected. Stars are a proxy for attention and for nothing else.
+ */
+export const repositoryDatapoints: readonly AdoptionDatapoint[] = [
+ {
+ id: "anthropic-skills-count",
+ label: "Skills published in anthropics/skills",
+ value: "19",
+ detail:
+ "Folders holding a SKILL.md under skills/ in Anthropic's reference repository, read from the git tree at commit 5304866. A twentieth SKILL.md sits under template/ and is a scaffold, so it is excluded. The same nineteen were there when we counted on August 18, 2026, so the first-party catalog is stable rather than growing.",
+ measuredOn: "2026-09-02",
+ sourceId: "anthropic-skills-repo",
+ },
+ {
+ id: "claude-code-bundled",
+ label: "Bundled skills documented in Claude Code",
+ value: "15",
+ detail:
+ "Entries marked as a bundled skill in the Claude Code commands reference: batch, claude-api, code-review, dataviz, debug, design, design-sync, doctor, fewer-permission-prompts, loop, run, run-skill-generator, simplify, verify, and workflow-authoring. Thirteen were documented there on August 18, 2026, so this is the first-party count that moved.",
+ measuredOn: "2026-09-02",
+ sourceId: "claude-code-commands",
+ },
+ {
+ id: "stars-superpowers",
+ label: "GitHub stars, obra/superpowers",
+ value: "280,721",
+ detail:
+ "A third-party skills framework and development methodology, and the most starred repository in this set. Read from the public GitHub REST API. A star records that somebody bookmarked a repository once, which is attention rather than installation, and it never expires.",
+ measuredOn: "2026-09-02",
+ sourceId: "github-rest-api",
+ },
+ {
+ id: "stars-anthropic-skills",
+ label: "GitHub stars, anthropics/skills",
+ value: "173,173",
+ detail:
+ "Anthropic's reference repository for the format, the one holding the nineteen skills counted above. Read from the public GitHub REST API in the same pass as the other three repositories in this table.",
+ measuredOn: "2026-09-02",
+ sourceId: "github-rest-api",
+ },
+ {
+ id: "stars-vercel-labs-skills",
+ label: "GitHub stars, vercel-labs/skills",
+ value: "30,228",
+ detail:
+ "The npx skills command line tool, which is what produces the install figures in the table above. Read from the public GitHub REST API. Its own find-skills is the most installed entry on the directory that tool feeds.",
+ measuredOn: "2026-09-02",
+ sourceId: "github-rest-api",
+ },
+ {
+ id: "stars-agentskills-spec",
+ label: "GitHub stars, agentskills/agentskills",
+ value: "24,968",
+ detail:
+ "The repository holding the Agent Skills specification and the client showcase counted at the top of this page. Read from the public GitHub REST API. It is the least starred repository here and the one every other entry implements.",
+ measuredOn: "2026-09-02",
+ sourceId: "github-rest-api",
+ },
+]
+
+/** Table rows for a section, derived so a figure is written once. */
+export function datapointRows(
+ datapoints: readonly AdoptionDatapoint[],
+): readonly { label: string; cells: readonly string[] }[] {
+ return datapoints.map((datapoint) => ({
+ label: datapoint.label,
+ cells: [datapoint.value, datapoint.detail, datapoint.measuredOn],
+ }))
+}
+
+/** Columns for a table of counts. */
+export const datapointColumns = [
+ "Figure",
+ "Value",
+ "What it counts",
+ "Read on",
+] as const
+
+/** Columns for the per-skill install table, where the detail is a repository. */
+export const installColumns = [
+ "Skill",
+ "Installs",
+ "Published from",
+ "Read on",
+] as const
+
+/**
+ * Datapoints whose detail column is a repository name rather than a sentence
+ * about what the figure counts. The tests hold the others to a full sentence.
+ */
+export const shortDetailIds: ReadonlySet = new Set(
+ installDatapoints.map((datapoint) => datapoint.id),
+)
+
+/** Every datapoint the page publishes, in the order it publishes them. */
+export const allDatapoints: readonly AdoptionDatapoint[] = [
+ ...clientDatapoints,
+ ...directoryDatapoints,
+ ...installDatapoints,
+ ...repositoryDatapoints,
+]
diff --git a/lib/seo/agent-skills-by-the-numbers/index.ts b/lib/seo/agent-skills-by-the-numbers/index.ts
new file mode 100644
index 00000000..86a777e9
--- /dev/null
+++ b/lib/seo/agent-skills-by-the-numbers/index.ts
@@ -0,0 +1,467 @@
+import type { OgTemplateContent } from "@/lib/og/template"
+import { agentSkillsByTheNumbersPath } from "@/lib/seo/agent-skills-by-the-numbers/types"
+import {
+ allDatapoints,
+ clientDatapoints,
+ datapointColumns,
+ datapointRows,
+ directoryDatapoints,
+ installColumns,
+ installDatapoints,
+ readOn,
+ repositoryDatapoints,
+} from "@/lib/seo/agent-skills-by-the-numbers/datapoints"
+import { agentSkillsSupportPath } from "@/lib/seo/agent-skills-support/types"
+import { agentSkillsPath } from "@/lib/seo/agent-skills/types"
+import { bestClaudeSkillsPath } from "@/lib/seo/best-claude-skills/types"
+import { claudeSkillsPath } from "@/lib/seo/claude-skills/types"
+import { guidePaths, type GuidePath } from "@/lib/seo/guides/types"
+import { manageAiSkillsPath } from "@/lib/seo/manage-ai-skills/types"
+import { whereToFindClaudeSkillsPath } from "@/lib/seo/where-to-find-claude-skills/types"
+
+export {
+ agentSkillsByTheNumbersPath,
+ type AgentSkillsByTheNumbersCtaPlacement,
+ type AgentSkillsByTheNumbersPath,
+} from "@/lib/seo/agent-skills-by-the-numbers/types"
+
+export interface AgentSkillsByTheNumbersSource {
+ /** Stable key referenced by the sections and datapoints it backs. */
+ id: string
+ label: string
+ href: string
+ note: string
+}
+
+export interface AgentSkillsByTheNumbersFaqEntry {
+ question: string
+ answer: string
+}
+
+export interface AgentSkillsByTheNumbersRelatedLink {
+ label: string
+ href: string
+ description: string
+}
+
+/**
+ * One contextual link out of a section, rendered as a sentence. The href union
+ * is the set of internal destinations this page is allowed to point at, so a
+ * path that does not exist fails the build instead of shipping as a dead link.
+ */
+export interface AgentSkillsByTheNumbersInlineLink {
+ lead: string
+ label: string
+ href:
+ | GuidePath
+ | typeof agentSkillsPath
+ | typeof agentSkillsSupportPath
+ | typeof bestClaudeSkillsPath
+ | typeof claudeSkillsPath
+ | typeof manageAiSkillsPath
+ | typeof whereToFindClaudeSkillsPath
+ trail: string
+}
+
+/** One rendered table of figures. */
+export interface AgentSkillsByTheNumbersTable {
+ caption: string
+ columns: readonly string[]
+ rows: readonly {
+ label: string
+ cells: readonly string[]
+ }[]
+}
+
+/** One section of figures, with the prose and the sources behind it. */
+export interface AgentSkillsByTheNumbersSection {
+ title: string
+ intro: string
+ tables: readonly AgentSkillsByTheNumbersTable[]
+ /** Prose that follows the tables, one paragraph per entry. */
+ notes: readonly string[]
+ link: AgentSkillsByTheNumbersInlineLink
+ sourceIds: readonly string[]
+}
+
+export interface AgentSkillsByTheNumbersDefinition {
+ path: typeof agentSkillsByTheNumbersPath
+ contentType: "article"
+ topics: readonly string[]
+ relatedGuidePaths: readonly (
+ | GuidePath
+ | typeof agentSkillsPath
+ | typeof agentSkillsSupportPath
+ | typeof manageAiSkillsPath
+ | typeof whereToFindClaudeSkillsPath
+ )[]
+ eyebrow: string
+ title: string
+ /** Full document title, including the brand suffix. */
+ seoTitle: string
+ description: string
+ /** The dateline printed under the title and repeated in the Markdown twin. */
+ dataNote: string
+ /** Scannable positioning above the fold. */
+ intro: readonly string[]
+ /** Answer-first summary, sized for extraction. */
+ answer: string
+ answerNotes: readonly string[]
+ answerSourceIds: readonly string[]
+ clients: AgentSkillsByTheNumbersSection
+ installs: AgentSkillsByTheNumbersSection
+ repositories: AgentSkillsByTheNumbersSection
+ notDocumented: {
+ title: string
+ intro: string
+ entries: readonly {
+ title: string
+ body: string
+ }[]
+ sourceIds: readonly string[]
+ }
+ faq: readonly AgentSkillsByTheNumbersFaqEntry[]
+ sources: readonly AgentSkillsByTheNumbersSource[]
+ related: readonly AgentSkillsByTheNumbersRelatedLink[]
+ og: OgTemplateContent
+ ogAlt: string
+ publishedAt: string
+ modifiedAt: string
+}
+
+export const agentSkillsByTheNumbers: AgentSkillsByTheNumbersDefinition = {
+ path: agentSkillsByTheNumbersPath,
+ contentType: "article",
+ topics: [
+ "agent skills",
+ "ecosystem data",
+ "install counts",
+ "statistics",
+ "SKILL.md",
+ ],
+ relatedGuidePaths: [
+ agentSkillsPath,
+ agentSkillsSupportPath,
+ whereToFindClaudeSkillsPath,
+ ],
+ eyebrow: "Ecosystem data",
+ title: "Agent skills by the numbers",
+ seoTitle: "Agent Skills by the Numbers | Skills Board",
+ description:
+ "Public, dated counts for the agent skills ecosystem with the source beside each one: 46 products on the official client showcase, 9,704 skills on the public skills.sh directory, and find-skills at 3,220,754 installs, all read on September 2, 2026.",
+ dataNote: "Data as of September 2, 2026, refreshed monthly.",
+ intro: [
+ "Forty-six agent products now appear on the showcase published with the Agent Skills specification, the public skills.sh directory lists 9,704 skills, and one skill on that directory has passed 3.2 million installs. Those are the figures anyone can check today, and this page puts them in one place with the source and the reading date beside each one.",
+ "Everything here is a public count: a published showcase page, a directory's own page data, a git tree, the GitHub REST API, and a vendor's own command reference. Nothing is estimated, and nothing comes from private telemetry.",
+ ],
+ answer:
+ "On September 2, 2026 the Agent Skills client showcase listed 46 agent products, eleven of which document reading a SKILL.md file in their own published documentation. The public skills.sh directory reported 9,704 skills the same day, counting a skill once somebody has installed it at least once through the npx skills add command. The most installed entry on that directory is find-skills, published from vercel-labs/skills, at 3,220,754 installs.",
+ answerNotes: [
+ "The figures sit at different levels of checkability, and each table says which. A showcase page, a git tree, and a star count are things you can open and count yourself. The install figures come from one tool's telemetry, which sees a skill only after that tool installs it, so nothing on this page counts a skill that stays inside a company.",
+ "Skills Board, the agent-native skills registry for teams, publishes this page and refreshes it monthly. A figure that cannot be read on refresh day is removed rather than carried forward with an old date.",
+ ],
+ answerSourceIds: [
+ "agentskills-clients",
+ "skillsboard-support-register",
+ "skills-sh",
+ ],
+ clients: {
+ title: "Products that read SKILL.md",
+ intro:
+ "The same population counted twice, at two levels of evidence, starting from the showcase published with the specification.",
+ tables: [
+ {
+ caption:
+ "Agent products listed as supporting the Agent Skills format, and the subset whose own documentation states it.",
+ columns: [...datapointColumns],
+ rows: datapointRows(clientDatapoints),
+ },
+ ],
+ notes: [
+ "The showcase is the most stable source on this page and the least informative about usage. A vendor appears once it says it reads SKILL.md, and it stays there whether one person or a million use the feature. It counts products on the supply side, not use on the demand side.",
+ "The gap between 46 and 11 is the part worth reading. Thirty-five entries are on the list without anybody here having opened the vendor's own documentation, so the defensible statement is that at least eleven products publish instructions you can follow today, and the rest have told the showcase they belong on it.",
+ ],
+ link: {
+ lead: "The eleven documented clients are unpacked one by one in",
+ label: "which AI clients read SKILL.md",
+ href: agentSkillsSupportPath,
+ trail: ", with the exact directories each vendor names.",
+ },
+ sourceIds: [
+ "agentskills-clients",
+ "agentskills-spec",
+ "skillsboard-support-register",
+ ],
+ },
+ installs: {
+ title: "Public skills and installs",
+ intro:
+ "The skills.sh directory is the only place anybody publishes a per-skill usage figure for this format, which makes it the strongest public adoption signal that exists and the one most worth reading carefully.",
+ tables: [
+ {
+ caption:
+ "The size of the public directory, and what its count includes.",
+ columns: [...datapointColumns],
+ rows: datapointRows(directoryDatapoints),
+ },
+ {
+ caption:
+ "The eight most installed skills on the directory, in its own order, read from its home page data on September 2, 2026.",
+ columns: [...installColumns],
+ rows: datapointRows(installDatapoints),
+ },
+ ],
+ notes: [
+ "These are telemetry from one tool. The directory sees a skill only when somebody runs npx skills add, so a team keeping skills in a private repository, copying a folder between machines, or pointing an agent straight at a GitHub URL contributes nothing to any figure in this section. Read the counts as a floor for public installable skills.",
+ "An install is also not a use. The count records the moment a file landed on a disk. It cannot say whether an agent ever loaded the skill, whether the person kept it, or whether it helped, and it does not decrease when somebody deletes the file.",
+ "One publisher holds five of the top eight. mattpocock/skills accounts for grill-me, grill-with-docs, improve-codebase-architecture, tdd, and setup-matt-pocock-skills. That is what a young distribution channel looks like: a small number of authors with an audience move most of the volume, and one popular bundle can reshape the order in a week.",
+ ],
+ link: {
+ lead: "The catalogs these installs come from, and what each one screens, are covered in",
+ label: "where to find Claude skills",
+ href: whereToFindClaudeSkillsPath,
+ trail: ".",
+ },
+ sourceIds: ["skills-sh", "skills-sh-api"],
+ },
+ repositories: {
+ title: "Reference repositories",
+ intro:
+ "What the first-party catalogs hold, and how much attention the repositories behind the format have collected.",
+ tables: [
+ {
+ caption:
+ "First-party skill counts and public star counts for the four repositories the format is written against.",
+ columns: [...datapointColumns],
+ rows: datapointRows(repositoryDatapoints),
+ },
+ ],
+ notes: [
+ "The reference repository is worth watching because it barely moves. Anthropic published nineteen skills in anthropics/skills on August 18, 2026 and publishes the same nineteen today, while the set bundled into Claude Code went from thirteen to fifteen. Growth in this format is happening in other people's repositories, which is why a directory built on install telemetry lists thousands while the first-party catalog holds nineteen.",
+ "Stars measure attention and nothing else. obra/superpowers carries more than ten times the stars of agentskills/agentskills, the repository holding the specification it implements. That tells you which repository people bookmark after reading about it, and nothing about which one runs on more machines.",
+ "One thing worth tracking has no number yet. SEP-2640, the proposal to carry skills over the Model Context Protocol, was still an open draft on September 2, 2026, opened on April 23 and last updated on August 29 with 33 comments on the pull request. If it lands, these counts gain a second distribution channel to measure.",
+ ],
+ link: {
+ lead: "What the specification in that last repository actually defines is covered in",
+ label: "Agent Skills, the open standard",
+ href: agentSkillsPath,
+ trail: ".",
+ },
+ sourceIds: [
+ "anthropic-skills-repo",
+ "claude-code-commands",
+ "github-rest-api",
+ "agentskills-repo",
+ "vercel-labs-skills-repo",
+ "superpowers-repo",
+ "mcp-sep-2640",
+ ],
+ },
+ notDocumented: {
+ title: "What none of this measures",
+ intro:
+ "Five questions this page does not answer, and the reason each one is out of reach.",
+ entries: [
+ {
+ title: "How many agent skills exist",
+ body: "No source counts them. The 9,704 on skills.sh is the largest public figure, and it sees a skill only after its own command line tool installs it, so private skills, copied folders, and skills installed straight from a repository URL are all missing, while a fork appears as an entry of its own. It is a floor for public installable skills, not a census.",
+ },
+ {
+ title: "How many people use agent skills",
+ body: "No vendor publishes it. Anthropic, OpenAI, Cursor, and the rest document that their products read SKILL.md and report nothing about how often. The showcase counts products, not seats. Any user figure in circulation is either a vendor's private number or somebody's estimate, and this page has neither.",
+ },
+ {
+ title: "Whether an install means a skill gets used",
+ body: "It does not. An install count records a file arriving on a disk. It cannot say whether an agent loaded the skill, whether the person kept it, or whether it changed any outcome, and it never goes down when somebody deletes the file.",
+ },
+ {
+ title: "Whether stars mean adoption",
+ body: "They do not. A star is a bookmark somebody left once, usually after reading about a repository rather than running it, and it never expires. Stars appear here because they are the only dated, public, cross-repository figure available, and they are labelled as attention for exactly that reason.",
+ },
+ {
+ title: "Anything happening inside a company",
+ body: "Every figure on this page counts something published. A skill in a private repository, a skill on one engineer's laptop, and a skill circulated inside a company are invisible to the showcase, to the directory, and to the star counts alike. A large share of real usage most likely sits there, and nothing public measures it.",
+ },
+ ],
+ sourceIds: [
+ "skills-sh",
+ "skills-sh-api",
+ "agentskills-clients",
+ "github-rest-api",
+ ],
+ },
+ faq: [
+ {
+ question: "How many agent skills exist?",
+ answer:
+ "No source counts them all. The largest public figure is the skills.sh directory, which reported 9,704 skills on September 2, 2026. It counts a skill once somebody installs it through its own command line tool, and counts forks separately, so private and hand-copied skills never appear in it.",
+ },
+ {
+ question: "How many products support agent skills?",
+ answer:
+ "The client showcase published with the Agent Skills specification listed 46 agent products on September 2, 2026. Eleven of those publish documentation of their own that states they read a SKILL.md file, checked on August 20, 2026. The remaining thirty-five are vendor self-listings that nobody here has verified.",
+ },
+ {
+ question: "What is the most installed agent skill?",
+ answer:
+ "On the public skills.sh directory it is find-skills, published from vercel-labs/skills, with 3,220,754 installs on September 2, 2026. Four more of the top eight come from mattpocock/skills, led by grill-me at 1,037,417. The figures count installs through one command line tool only.",
+ },
+ {
+ question: "How many skills does Anthropic publish?",
+ answer:
+ "Nineteen in the anthropics/skills repository, counted as folders holding a SKILL.md under the skills directory at commit 5304866 on September 2, 2026. A twentieth is a template scaffold. Claude Code separately documents fifteen bundled skills in its commands reference, up from thirteen on August 18, 2026.",
+ },
+ {
+ question: "How often is this page updated?",
+ answer:
+ "Monthly. Every figure was read from its source on September 2, 2026 and carries that date beside it. On each refresh the numbers are read again, and a figure whose source has become unreachable is deleted rather than carried forward with a stale date, so the page never ages quietly.",
+ },
+ ],
+ sources: [
+ {
+ id: "agentskills-clients",
+ label: "Agent Skills Client Showcase",
+ href: "https://agentskills.io/clients",
+ note: "The list of agent products that support the format, published with the specification. Counted from the published page on September 2, 2026.",
+ },
+ {
+ id: "agentskills-spec",
+ label: "Agent Skills specification",
+ href: "https://agentskills.io/specification",
+ note: "The format those clients implement, and what a file has to contain to be a skill at all.",
+ },
+ {
+ id: "agentskills-repo",
+ label: "agentskills/agentskills on GitHub",
+ href: "https://github.com/agentskills/agentskills",
+ note: "The repository holding the specification and the showcase. Star count read on September 2, 2026.",
+ },
+ {
+ id: "skillsboard-support-register",
+ label: "Agent Skills support: which AI clients read SKILL.md",
+ href: "https://www.skillsboard.sh/agent-skills-support",
+ note: "Our own register of the eleven clients whose published documentation states they read SKILL.md, each vendor page fetched and read on August 20, 2026.",
+ },
+ {
+ id: "skills-sh",
+ label: "skills.sh, the agent skills directory",
+ href: "https://www.skills.sh/",
+ note: "The public directory run by Vercel. Its totalSkills figure and its per-skill install counts were read from the home page data on September 2, 2026.",
+ },
+ {
+ id: "skills-sh-api",
+ label: "skills.sh API reference",
+ href: "https://www.skills.sh/docs/api",
+ note: "States that the directory ranks skills by install count and flags forks and copies with an isDuplicate field rather than merging them.",
+ },
+ {
+ id: "anthropic-skills-repo",
+ label: "anthropics/skills on GitHub",
+ href: "https://github.com/anthropics/skills",
+ note: "Anthropic's reference repository. Skill folders counted from the git tree at commit 5304866 on September 2, 2026.",
+ },
+ {
+ id: "claude-code-commands",
+ label: "Claude Code commands reference",
+ href: "https://code.claude.com/docs/en/commands",
+ note: "Lists bundled skills alongside built-in commands and marks each one as a skill. Counted on September 2, 2026.",
+ },
+ {
+ id: "github-rest-api",
+ label: "GitHub REST API, repositories endpoint",
+ href: "https://docs.github.com/en/rest/repos/repos",
+ note: "The endpoint behind every star count on this page, read for all four repositories on September 2, 2026.",
+ },
+ {
+ id: "vercel-labs-skills-repo",
+ label: "vercel-labs/skills on GitHub",
+ href: "https://github.com/vercel-labs/skills",
+ note: "The npx skills command line tool that produces the install counts, and the source of the most installed skill in the table.",
+ },
+ {
+ id: "superpowers-repo",
+ label: "obra/superpowers on GitHub",
+ href: "https://github.com/obra/superpowers",
+ note: "A third-party skills framework, included because it is the most starred repository in this set by a wide margin.",
+ },
+ {
+ id: "mcp-sep-2640",
+ label: "SEP-2640: Skills Extension",
+ href: "https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640",
+ note: "The open draft proposal to carry skills over the Model Context Protocol. Opened April 23, 2026, last updated August 29, 2026, still labelled draft when read on September 2, 2026.",
+ },
+ ],
+ related: [
+ {
+ label: "Agent Skills: the open standard",
+ href: agentSkillsPath,
+ description:
+ "What the specification defines, which agents implement it, and where each one looks on disk.",
+ },
+ {
+ label: "Agent Skills support: which AI clients read SKILL.md",
+ href: agentSkillsSupportPath,
+ description:
+ "The eleven documented clients as a compatibility matrix, with the directories each vendor names.",
+ },
+ {
+ label: "Where to find Claude skills",
+ href: whereToFindClaudeSkillsPath,
+ description:
+ "The catalogs and repositories these counts draw from, and what each one screens.",
+ },
+ {
+ label: "Claude skills: what they are and how to write one",
+ href: claudeSkillsPath,
+ description:
+ "The format behind every skill any of these sources counts, from frontmatter to installation.",
+ },
+ {
+ label: "Manage AI skills across your organization",
+ href: manageAiSkillsPath,
+ description:
+ "What each vendor's distribution mechanism covers, and the selection layer none of them records.",
+ },
+ {
+ label: "Best Claude skills: a curated register",
+ href: bestClaudeSkillsPath,
+ description:
+ "A worked example of counting carefully: entries read one by one against stated criteria.",
+ },
+ {
+ label: "How to write a SKILL.md file",
+ href: guidePaths.writeSkillMd,
+ description:
+ "The frontmatter fields and description rules behind every skill counted on this page.",
+ },
+ ],
+ og: {
+ eyebrow: "Ecosystem data",
+ title: [{ text: "Agent skills" }, { text: "by the numbers", accent: true }],
+ description:
+ "46 products on the official client showcase, 9,704 skills on the public skills.sh directory, and one skill past 3.2 million installs. Public sources only, each one dated.",
+ contextLabel: "skillsboard.sh",
+ chips: ["Public sources", "Dated", "Refreshed monthly"],
+ footnote: "Data as of September 2, 2026",
+ variant: "ink",
+ },
+ ogAlt:
+ "Agent skills by the numbers, a dated statistics page from Skills Board with the public source beside every figure.",
+ publishedAt: "2026-09-02",
+ modifiedAt: "2026-09-02",
+}
+
+/** Guard: every datapoint has to point at a source the page lists. */
+const sourceIds = new Set(
+ agentSkillsByTheNumbers.sources.map((source) => source.id),
+)
+
+for (const datapoint of allDatapoints) {
+ if (!sourceIds.has(datapoint.sourceId)) {
+ throw new Error(
+ `Datapoint ${datapoint.id} cites an unknown source: ${datapoint.sourceId}`,
+ )
+ }
+}
+
+export { allDatapoints, readOn }
diff --git a/lib/seo/agent-skills-by-the-numbers/types.ts b/lib/seo/agent-skills-by-the-numbers/types.ts
new file mode 100644
index 00000000..c29a6607
--- /dev/null
+++ b/lib/seo/agent-skills-by-the-numbers/types.ts
@@ -0,0 +1,20 @@
+/**
+ * Top-level route, for the same reason /manage-ai-skills and
+ * /claude-code-for-teams are: the page answers a head query itself rather than
+ * a task-shaped variation of it, so it does not sit under /guides. The path
+ * does not end in `-skills`, so it carries its own Markdown negotiation
+ * rewrite in `next.config.ts` the way /skill-examples does.
+ */
+export const agentSkillsByTheNumbersPath =
+ "/agent-skills-by-the-numbers" as const
+
+export type AgentSkillsByTheNumbersPath = typeof agentSkillsByTheNumbersPath
+
+/**
+ * CTA placements on the statistics page, kept in sync with the
+ * landing_cta_clicked union. The sticky shell CTA is
+ * agent_skills_by_the_numbers_header, following the naming every other
+ * marketing chrome uses for its nav slot.
+ */
+export type AgentSkillsByTheNumbersCtaPlacement =
+ `agent_skills_by_the_numbers_${"hero" | "inline" | "closing"}`
diff --git a/lib/seo/agent-skills/index.ts b/lib/seo/agent-skills/index.ts
index ce2d9a20..516e5a3b 100644
--- a/lib/seo/agent-skills/index.ts
+++ b/lib/seo/agent-skills/index.ts
@@ -705,6 +705,12 @@ export const agentSkills: AgentSkillsDefinition = {
},
],
related: [
+ {
+ label: "Agent skills by the numbers",
+ href: "/agent-skills-by-the-numbers",
+ description:
+ "Dated public counts of clients, published skills, installs, and repositories, with the source beside every figure.",
+ },
{
label: "Skill examples: real SKILL.md files, explained",
href: "/skill-examples",
diff --git a/lib/seo/resources.ts b/lib/seo/resources.ts
index a9c0e840..0a3e2323 100644
--- a/lib/seo/resources.ts
+++ b/lib/seo/resources.ts
@@ -1,3 +1,8 @@
+import { agentSkillsByTheNumbers } from "@/lib/seo/agent-skills-by-the-numbers"
+import {
+ agentSkillsByTheNumbersPath,
+ type AgentSkillsByTheNumbersPath,
+} from "@/lib/seo/agent-skills-by-the-numbers/types"
import { agentSkillsSupport } from "@/lib/seo/agent-skills-support"
import {
agentSkillsSupportPath,
@@ -76,6 +81,7 @@ export type ResourceContentType = "guide" | "article"
/** Every path the resource hub, related links, and sitemap can address. */
export type ResourcePath =
| GuidePath
+ | AgentSkillsByTheNumbersPath
| AgentSkillsPath
| AgentSkillsSupportPath
| AgentsMdVsSkillMdPath
@@ -108,6 +114,7 @@ export interface ResourceIndexEntry {
export const resourceEntries = [
...guides,
agentSkills,
+ agentSkillsByTheNumbers,
agentSkillsSupport,
anthropicSkills,
bestClaudeSkills,
@@ -170,6 +177,7 @@ const resourceClusterDefinitions = [
paths: [
claudeCodeForTeamsPath,
manageAiSkillsPath,
+ agentSkillsByTheNumbersPath,
whereToFindClaudeSkillsPath,
bestClaudeSkillsPath,
guidePaths.chooseFirstTeamSkill,
diff --git a/next.config.ts b/next.config.ts
index 140b2986..f538be4b 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -160,6 +160,11 @@ const nextConfig = {
destination: "/agent-skills-support",
permanent: true,
},
+ {
+ source: "/agent-skills-by-the-numbers/",
+ destination: "/agent-skills-by-the-numbers",
+ permanent: true,
+ },
{
source: "/claude-skills/",
destination: "/claude-skills",
@@ -294,6 +299,13 @@ const nextConfig = {
has: [MARKDOWN_ACCEPT],
destination: "/api/markdown?path=/agent-skills-support",
},
+ // `/agent-skills-by-the-numbers` does not end in `-skills` either, so the
+ // shared rule above never reaches it.
+ {
+ source: "/agent-skills-by-the-numbers",
+ has: [MARKDOWN_ACCEPT],
+ destination: "/api/markdown?path=/agent-skills-by-the-numbers",
+ },
{
source: "/developers",
has: [MARKDOWN_ACCEPT],
diff --git a/public/llms.txt b/public/llms.txt
index 8fa71301..52b1a4ac 100644
--- a/public/llms.txt
+++ b/public/llms.txt
@@ -50,6 +50,7 @@ Markdown versions: the home page, the resources, comparisons, and alternatives h
- [Best Claude skills: a curated register](https://www.skillsboard.sh/best-claude-skills): Twenty-seven register entries covering thirty-five Claude skills that cleared seven stated criteria, grouped by job, each read from its own SKILL.md with publisher, license, and where it runs, plus the nine popular candidates that were dropped and why
- [Anthropic skills: the first-party catalog](https://www.skillsboard.sh/anthropic-skills): Every skill Anthropic publishes itself, in three sets: the four pre-built document skills, the nineteen folders in anthropics/skills, and the thirteen bundled with Claude Code, with what each does, where it loads, and how it is licensed
- [Agent Skills: the open standard](https://www.skillsboard.sh/agent-skills): What the Agent Skills specification defines, which agents implement it and where each one looks on disk, what travels between them, and where to read real examples
+- [Agent skills by the numbers](https://www.skillsboard.sh/agent-skills-by-the-numbers): Public, dated counts for the agent skills ecosystem with the source beside each one, covering the 46 products on the official client showcase, the 11 whose own documentation states they read SKILL.md, the 9,704 skills and the eight most installed entries on the public skills.sh directory, the 19 skills in anthropics/skills and the 15 bundled with Claude Code, GitHub stars for four reference repositories, and what none of those figures measures
- [Skill examples: real SKILL.md files](https://www.skillsboard.sh/skill-examples): Eight example skills from anthropics/skills read file by file, the pattern each one demonstrates, verbatim excerpts with their source, what all nineteen examples declare in frontmatter, and the six places the examples diverge from the specification
- [Claude skills](https://www.skillsboard.sh/claude-skills): What a Claude Skill is, the SKILL.md format, where skills run, and how to install, write, and share one
- [Codex skills](https://www.skillsboard.sh/codex-skills): What a Codex skill is, the .agents/skills locations Codex scans, what transfers from a Claude skill, and how to add one
diff --git a/tests/agent-skills-by-the-numbers.test.mjs b/tests/agent-skills-by-the-numbers.test.mjs
new file mode 100644
index 00000000..b0d7bba4
--- /dev/null
+++ b/tests/agent-skills-by-the-numbers.test.mjs
@@ -0,0 +1,511 @@
+import assert from "node:assert/strict"
+import { access, readFile } from "node:fs/promises"
+import { test } from "node:test"
+
+import "./helpers/register-app-aliases.mjs"
+
+const { agentSkillsByTheNumbers, agentSkillsByTheNumbersPath } = await import(
+ "../lib/seo/agent-skills-by-the-numbers/index.ts"
+)
+const {
+ allDatapoints,
+ datapointColumns,
+ installColumns,
+ installDatapoints,
+ readOn,
+ shortDetailIds,
+} = await import("../lib/seo/agent-skills-by-the-numbers/datapoints.ts")
+const { agentSkills } = await import("../lib/seo/agent-skills/index.ts")
+const { resourceClusters, resourceEntries } = await import(
+ "../lib/seo/resources.ts"
+)
+const { markdownTwinAlternates, renderMarkdownTwin } = await import(
+ "../lib/markdown/twins.ts"
+)
+const { buildResourceArticleSchema } = await import(
+ "../lib/seo/resource-article-schema.ts"
+)
+const { default: sitemap } = await import("../app/sitemap.ts")
+const { default: nextConfig } = await import("../next.config.ts")
+
+const entry = agentSkillsByTheNumbers
+const canonical = `https://www.skillsboard.sh${agentSkillsByTheNumbersPath}`
+const markdown = renderMarkdownTwin(agentSkillsByTheNumbersPath) ?? ""
+
+/** Em dash and en dash are not allowed anywhere in published copy. */
+const dashPattern = /[–—]/
+
+/** The three figure sections, in the order the page renders them. */
+const sections = [entry.clients, entry.installs, entry.repositories]
+
+async function exists(relative) {
+ try {
+ await access(new URL(relative, import.meta.url))
+ return true
+ } catch {
+ return false
+ }
+}
+
+test("the statistics page is registered everywhere a resource is addressed", () => {
+ assert.equal(entry.path, "/agent-skills-by-the-numbers")
+ assert.ok(
+ resourceEntries.some((candidate) => candidate.path === entry.path),
+ "missing from the resource registry",
+ )
+ assert.ok(
+ resourceClusters.some((cluster) =>
+ cluster.entries.some((candidate) => candidate.path === entry.path),
+ ),
+ "missing from every topic cluster",
+ )
+ assert.ok(
+ sitemap().some((candidate) => candidate.url === canonical),
+ "missing from the sitemap",
+ )
+})
+
+test("no trace of the route this page was first published under", async () => {
+ assert.ok(!(await exists("../app/agent-skills-adoption")))
+ assert.ok(!(await exists("../lib/seo/agent-skills-adoption")))
+ assert.ok(!(await exists("../components/agent-skills-adoption")))
+
+ for (const file of ["../public/llms.txt", "../next.config.ts"]) {
+ const contents = await readFile(new URL(file, import.meta.url), "utf8")
+ assert.ok(
+ !contents.includes("agent-skills-adoption"),
+ `${file} still addresses the old route`,
+ )
+ }
+})
+
+test("the route renders the page and its social images", async () => {
+ for (const file of [
+ "page.tsx",
+ "layout.tsx",
+ "opengraph-image.tsx",
+ "twitter-image.tsx",
+ ]) {
+ assert.ok(
+ await exists(`../app${entry.path}/${file}`),
+ `app${entry.path}/${file} does not exist`,
+ )
+ }
+})
+
+test("the page mounts its own four CTA locations", async () => {
+ const layout = await readFile(
+ new URL(`../app${entry.path}/layout.tsx`, import.meta.url),
+ "utf8",
+ )
+ assert.ok(
+ layout.includes("agent_skills_by_the_numbers_header"),
+ "the shell reports another page's location",
+ )
+
+ const page = await readFile(
+ new URL(
+ "../components/agent-skills-by-the-numbers/agent-skills-by-the-numbers-page.tsx",
+ import.meta.url,
+ ),
+ "utf8",
+ )
+ for (const suffix of ["hero", "inline", "closing"]) {
+ assert.ok(
+ page.includes(`agent_skills_by_the_numbers_${suffix}`),
+ `the page never renders the agent_skills_by_the_numbers_${suffix} CTA`,
+ )
+ }
+
+ const events = await readFile(
+ new URL("../analytics/posthog/events.ts", import.meta.url),
+ "utf8",
+ )
+ for (const suffix of ["header", "hero", "inline", "closing"]) {
+ assert.ok(
+ events.includes(`"agent_skills_by_the_numbers_${suffix}"`),
+ `landing_cta_clicked cannot report agent_skills_by_the_numbers_${suffix}`,
+ )
+ }
+ assert.ok(
+ !events.includes("agent_skills_adoption"),
+ "the old CTA location names survive in the event union",
+ )
+})
+
+test("the page is listed in the static llms.txt", async () => {
+ const llms = await readFile(
+ new URL("../public/llms.txt", import.meta.url),
+ "utf8",
+ )
+ assert.ok(llms.includes(`${canonical})`), "missing from public/llms.txt")
+
+ // The sibling articles pin the single `Last reviewed` line to the date of
+ // the last full re-review of every pinned page. This page re-read its own
+ // sources only, so the shared line stays where it is and the page carries
+ // its own date instead, in `modifiedAt` and on the page itself.
+ assert.ok(
+ llms.includes("Last reviewed: 2026-08-19"),
+ "the shared review date moved without a full re-review",
+ )
+ assert.ok(
+ entry.modifiedAt >= "2026-08-19",
+ "the entry predates the last full llms.txt review",
+ )
+})
+
+test("the canonical URL is reachable with and without a trailing slash", async () => {
+ const { redirects, rewrites } = nextConfig
+ const redirectRules = await redirects()
+ const redirect = redirectRules.find(
+ (rule) =>
+ rule.source === `${entry.path}/` && rule.destination === entry.path,
+ )
+ assert.ok(redirect, "the trailing-slash spelling has no redirect")
+ assert.equal(redirect.permanent, true, "the redirect is not permanent")
+
+ // The path does not end in `-skills`, so the shared Accept rewrite does not
+ // reach it and it carries a rule of its own.
+ assert.doesNotMatch(entry.path.slice(1), /^[^/]*-skills$/)
+ const { beforeFiles } = await rewrites()
+ const negotiated = beforeFiles.find((rule) => rule.source === entry.path)
+ assert.ok(negotiated, "the Markdown Accept rewrite is missing")
+ assert.equal(negotiated.destination, `/api/markdown?path=${entry.path}`)
+})
+
+test("the Markdown twin carries every section, the tables, and the FAQ", () => {
+ assert.ok(markdown.startsWith(`# ${entry.title}\n`))
+ assert.ok(markdown.includes(`Canonical URL: ${canonical}`))
+ assert.deepEqual(markdownTwinAlternates(entry.path), {
+ canonical: entry.path,
+ types: { "text/markdown": `${entry.path}.md` },
+ })
+
+ for (const title of [
+ ...sections.map((section) => section.title),
+ entry.notDocumented.title,
+ ]) {
+ assert.ok(
+ markdown.includes(`## ${title}`),
+ `missing section heading: ${title}`,
+ )
+ }
+
+ for (const section of sections) {
+ for (const table of section.tables) {
+ const header = `| ${table.columns.join(" | ")} |`
+ assert.ok(markdown.includes(header), `${table.caption} lost its header`)
+ }
+ }
+
+ for (const item of entry.faq) {
+ assert.ok(markdown.includes(`### ${item.question}`), item.question)
+ assert.ok(markdown.includes(item.answer), "missing FAQ answer")
+ }
+})
+
+test("every published figure lives in the data module, dated and sourced", () => {
+ assert.ok(allDatapoints.length >= 12, "fewer than twelve datapoints")
+
+ const known = new Set(entry.sources.map((source) => source.id))
+ const ids = new Set()
+
+ for (const datapoint of allDatapoints) {
+ assert.ok(!ids.has(datapoint.id), `duplicate datapoint id: ${datapoint.id}`)
+ ids.add(datapoint.id)
+ assert.match(
+ datapoint.measuredOn,
+ /^\d{4}-\d{2}-\d{2}$/,
+ `${datapoint.id} has no reading date`,
+ )
+ assert.ok(
+ datapoint.measuredOn <= entry.modifiedAt,
+ `${datapoint.id} claims to have been read after the page was checked`,
+ )
+ assert.ok(
+ known.has(datapoint.sourceId),
+ `${datapoint.id} cites an unknown source: ${datapoint.sourceId}`,
+ )
+ // An install row uses the detail column for the publishing repository.
+ // Every other row has to say what its figure counts.
+ if (!shortDetailIds.has(datapoint.id)) {
+ assert.ok(
+ datapoint.detail.length > 80,
+ `${datapoint.id} does not say what it counts`,
+ )
+ }
+ }
+
+ // The refresh reads every figure on the same day, except where a row is
+ // explicitly carried from an earlier, dated reading of vendor documentation.
+ assert.equal(readOn, entry.modifiedAt)
+ assert.ok(
+ allDatapoints.filter((datapoint) => datapoint.measuredOn === readOn)
+ .length >= allDatapoints.length - 1,
+ "more than one figure is older than the refresh",
+ )
+})
+
+test("every figure on the page comes from a public source", () => {
+ const copy = JSON.stringify(entry)
+ for (const banned of [
+ "log drain",
+ "Search Console",
+ "Googlebot",
+ "crawler",
+ "impression",
+ "one small site",
+ "this site's logs",
+ ]) {
+ assert.ok(
+ !copy.toLowerCase().includes(banned.toLowerCase()),
+ `the page still publishes a first-party measurement: ${banned}`,
+ )
+ }
+
+ for (const source of entry.sources) {
+ assert.match(source.href, /^https:\/\//, `${source.id} has no public URL`)
+ }
+})
+
+test("every table renders the data module rather than its own numbers", () => {
+ const rowCount = sections.reduce(
+ (total, section) =>
+ total +
+ section.tables.reduce((count, table) => count + table.rows.length, 0),
+ 0,
+ )
+ assert.equal(rowCount, allDatapoints.length, "a datapoint renders nowhere")
+
+ const seen = new Set()
+ for (const section of sections) {
+ for (const table of section.tables) {
+ assert.ok(
+ table.columns.length === 4 && table.columns.at(-1) === "Read on",
+ `${table.caption} does not date its rows`,
+ )
+ for (const row of table.rows) {
+ assert.equal(
+ row.cells.length,
+ 3,
+ `${row.label} has the wrong cell count`,
+ )
+ const datapoint = allDatapoints.find(
+ (candidate) => candidate.label === row.label,
+ )
+ assert.ok(datapoint, `${row.label} is not backed by a datapoint`)
+ assert.ok(!seen.has(datapoint.id), `${datapoint.id} renders twice`)
+ seen.add(datapoint.id)
+ assert.equal(row.cells[0], datapoint.value)
+ assert.equal(row.cells[1], datapoint.detail)
+ assert.equal(row.cells[2], datapoint.measuredOn)
+ }
+ }
+ }
+ assert.equal(seen.size, allDatapoints.length)
+})
+
+test("the install table is the strongest public figure and says what it misses", () => {
+ const installTable = entry.installs.tables.find(
+ (table) => table.columns[0] === "Skill",
+ )
+ assert.ok(installTable, "the install table is missing")
+ assert.deepEqual(installTable.columns, [...installColumns])
+ assert.ok(
+ installDatapoints.length >= 5 && installDatapoints.length <= 10,
+ "the install table is outside the five to ten row range",
+ )
+ assert.equal(installTable.rows.length, installDatapoints.length)
+
+ const counts = installDatapoints.map((datapoint) =>
+ Number(datapoint.value.replaceAll(",", "")),
+ )
+ assert.deepEqual(
+ counts,
+ [...counts].sort((left, right) => right - left),
+ "the install table is not in descending order",
+ )
+
+ const notes = entry.installs.notes.join(" ")
+ assert.ok(
+ notes.includes("npx skills add"),
+ "the page does not say which tool the install figures come from",
+ )
+ assert.ok(
+ /install is also not a use/i.test(notes),
+ "the page does not declare the limit of install telemetry",
+ )
+})
+
+test("the answer opens on the figures and the intro does not open on a caveat", () => {
+ assert.ok(entry.answer.startsWith("On September 2, 2026"))
+ for (const figure of ["46", "9,704", "3,220,754"]) {
+ assert.ok(entry.answer.includes(figure), `the answer omits ${figure}`)
+ }
+
+ const first = entry.intro[0]
+ assert.match(
+ first,
+ /^Forty-six/,
+ "the intro does not open on a figure",
+ )
+ assert.doesNotMatch(
+ first,
+ /^(No |Nobody|There is no|Neither)/,
+ "the intro opens on a negation",
+ )
+})
+
+test("the page declares its limits without padding them", () => {
+ assert.ok(
+ entry.notDocumented.entries.length >= 4,
+ "fewer than four declared limits",
+ )
+ for (const item of entry.notDocumented.entries) {
+ assert.ok(item.body.length > 150, `${item.title} gives no detail`)
+ }
+
+ const gaps = JSON.stringify(entry.notDocumented)
+ for (const claim of [
+ "No source counts them",
+ "It does not",
+ "They do not",
+ ]) {
+ assert.ok(gaps.includes(claim), `the limits section drops: ${claim}`)
+ }
+})
+
+test("the page carries a dateline and a refresh cadence", () => {
+ assert.equal(entry.dataNote, "Data as of September 2, 2026, refreshed monthly.")
+ assert.ok(markdown.includes(entry.dataNote), "the twin drops the dateline")
+})
+
+test("the FAQ is self-contained, extractable, and cites its own dates", () => {
+ assert.ok(entry.faq.length >= 4 && entry.faq.length <= 5)
+
+ for (const item of entry.faq) {
+ const words = item.answer.trim().split(/\s+/).filter(Boolean).length
+ assert.ok(
+ words >= 40 && words <= 60,
+ `"${item.question}" answer is ${words} words`,
+ )
+ // An answer that quotes a figure has to date it. An answer with no
+ // figure in it has nothing to date.
+ if (/\d/.test(item.answer)) {
+ assert.match(
+ item.answer,
+ /2026/,
+ `"${item.question}" gives a figure without a date`,
+ )
+ }
+ }
+
+ for (const term of [
+ "how many agent skills exist",
+ "products support agent skills",
+ "most installed agent skill",
+ "how often is this page updated",
+ ]) {
+ assert.ok(
+ entry.faq.some((item) => item.question.toLowerCase().includes(term)),
+ `no FAQ entry addresses ${term}`,
+ )
+ }
+})
+
+test("no copy on the page uses an em dash or an en dash", () => {
+ assert.doesNotMatch(JSON.stringify(entry), dashPattern)
+ assert.doesNotMatch(markdown, dashPattern)
+})
+
+test("the product copy follows the positioning rules", () => {
+ const copy = JSON.stringify(entry)
+ assert.doesNotMatch(copy, /shared library/i)
+ assert.doesNotMatch(copy, /recommend/i)
+
+ const description = "agent-native skills registry for teams"
+ assert.ok(
+ copy.includes(description),
+ "the page does not use the agreed product description",
+ )
+ assert.equal(
+ copy.split(description).length - 1,
+ 1,
+ "the product is described more than once",
+ )
+})
+
+test("the page links out, and a sibling page links in", () => {
+ const outbound = new Set(
+ [
+ ...sections.map((section) => section.link.href),
+ ...entry.related.map((link) => link.href),
+ ].filter((href) => href.startsWith("/")),
+ )
+ assert.ok(outbound.size >= 6, "fewer than six internal outbound links")
+
+ for (const destination of [
+ "/agent-skills",
+ "/agent-skills-support",
+ "/claude-skills",
+ "/where-to-find-claude-skills",
+ ]) {
+ assert.ok(outbound.has(destination), `no link to ${destination}`)
+ }
+
+ assert.ok(
+ agentSkills.related.some((link) => link.href === entry.path),
+ `${agentSkills.path} does not link to ${entry.path}`,
+ )
+})
+
+test("the schema carries TechArticle, FAQPage, and a breadcrumb", () => {
+ const graph = buildResourceArticleSchema(entry)["@graph"]
+ const byType = (type) => graph.find((node) => node["@type"] === type)
+
+ const article = byType("TechArticle")
+ assert.ok(article, "missing TechArticle")
+ assert.equal(article.headline, entry.title)
+ assert.equal(article.url, canonical)
+ assert.equal(article.dateModified, entry.modifiedAt)
+ assert.deepEqual(
+ article.citation,
+ entry.sources.map((source) => source.href),
+ )
+
+ const faq = byType("FAQPage")
+ assert.ok(faq, "missing FAQPage")
+ assert.equal(faq.mainEntity.length, entry.faq.length)
+ assert.equal(faq.mainEntity[0].acceptedAnswer.text, entry.faq[0].answer)
+
+ const breadcrumbs = byType("BreadcrumbList")
+ assert.ok(breadcrumbs, "missing BreadcrumbList")
+ assert.equal(breadcrumbs.itemListElement.at(-1).item, canonical)
+})
+
+test("every section cites a source that the page actually lists", () => {
+ const known = new Set(entry.sources.map((source) => source.id))
+ assert.ok(entry.sources.length >= 9, "too few sources")
+ assert.equal(known.size, entry.sources.length, "a source id is duplicated")
+
+ const cited = [
+ entry.answerSourceIds,
+ ...sections.map((section) => section.sourceIds),
+ entry.notDocumented.sourceIds,
+ ]
+
+ for (const ids of cited) {
+ assert.ok(ids.length > 0, "a section cites nothing")
+ for (const id of ids) {
+ assert.ok(known.has(id), `unknown source id: ${id}`)
+ }
+ }
+
+ const used = new Set(cited.flat())
+ for (const source of entry.sources) {
+ assert.ok(used.has(source.id), `${source.id} is listed but never cited`)
+ }
+
+ // datapointColumns is the shape every count table shares.
+ assert.deepEqual(entry.clients.tables[0].columns, [...datapointColumns])
+})