From a66d475d15efe726d84d33547be6ba33878eb37e Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Sat, 22 Aug 2026 23:53:13 +0530 Subject: [PATCH] fix: accept the skill names the Agent Skills spec allows (0.24.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KSF's name rule required a leading letter and capped names at 41 characters. The spec allows any lowercase alphanumeric start and up to 64, so `2fa-setup` failed the rule, fell through to the directory-name fallback, lost its leading digit to a slug that strips everything before the first letter, and installed as `fa-setup` — a different skill from the one the author published, pinned and compiled under that name. The rule now accepts a leading digit and the spec's full length, and the slug keeps leading digits. Loosened in one direction only: `tidy--commits` and `tidy-` still load, since tightening a frozen field would break valid skills (RFC 0002); the name-convention lint added in 0.24.1 reports them. The directory-name fallback for an unloadable frontmatter name is unchanged — the spec wants name to match the parent directory. --- CHANGELOG.md | 6 ++++++ README.md | 2 +- packages/cli/package-lock.json | 4 ++-- packages/cli/package.json | 2 +- packages/cli/scripts/test.mjs | 30 ++++++++++++++++++++++++++++++ packages/cli/src/ksf.ts | 26 ++++++++++++++++++++++++-- site/changelog.html | 13 +++++++++++-- site/index.html | 2 +- 8 files changed, 76 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2873df..27c3bed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Format: [Keep a Changelog](https://keepachangelog.com). Versioning: semver — for skills *and* for this CLI, breaking prompt changes are breaking changes. +## [0.24.2] — 2026-08-08 + +### Fixed +- **KSF rejected skill names the Agent Skills spec allows, and quietly renamed the skill instead of saying so.** The name rule was `^[a-z][a-z0-9-]{1,40}$`: it required a leading letter and capped names at 41 characters, while [the spec](https://agentskills.io/specification) allows any lowercase alphanumeric start and up to 64. So `2fa-setup` — a perfectly legal Agent Skill — failed the rule, fell through to the directory-name fallback, had its leading digit stripped by a slug that dropped everything before the first letter, and installed as **`fa-setup`**: a different skill from the one its author published, pinned under that name in `kitbash.lock` and compiled under it into every target. The rule now accepts a leading digit and the spec's full 64-character length, and the slug keeps leading digits instead of eating them. +- The loosening is deliberate in one direction only. `tidy--commits` and `tidy-` still **load**, because tightening a value constraint on a frozen manifest field would turn valid existing skills into load failures (RFC 0002); they are reported by the `name-convention` lint added in 0.24.1, which is where a host-compatibility problem belongs. A frontmatter name KSF cannot load still falls back to the directory name, which is what the spec's name-matches-directory rule wants — that invariant is unchanged and still tested. + ## [0.24.1] — 2026-08-12 ### Fixed diff --git a/README.md b/README.md index a420ca9..d36cacf 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Already have skills? A plain SKILL.md folder — the skills.sh / Claude Skills c Already carrying a hand-written `CLAUDE.md`, `.cursor/rules/`, `AGENTS.md`, and the rest of the copy-per-agent set? `kitbash import` reads them back into a single skill, measures what each one costs in standing context, and reports where the copies have drifted apart — so `kitbash compile` can regenerate them all from that one source. It touches nothing on disk until you remove the originals yourself. -**Status.** v0.24.1, on npm and Homebrew, zero runtime dependencies, Node 20+. The KSF core is frozen and additive-only within the major version ([RFC 0002](rfcs/0002-ksf-1.0-stabilization.md)). Everything around it is early and labeled as such: `init`, `import`, `install`, `remove`, `list`, `compile`, `doctor`, `update`, `diff`, `lint`, `preview`, `explain`, and `test` work today; `audit`, `gate`, `search`, `publish`, `lore`, and `run` exit `7` and are on the [roadmap](docs/roadmap.md). One first-party skill ships (`prereview`); six more are specified but not built. Adoption is single-digit stars — if the measurement above is what you want, you are early. +**Status.** v0.24.2, on npm and Homebrew, zero runtime dependencies, Node 20+. The KSF core is frozen and additive-only within the major version ([RFC 0002](rfcs/0002-ksf-1.0-stabilization.md)). Everything around it is early and labeled as such: `init`, `import`, `install`, `remove`, `list`, `compile`, `doctor`, `update`, `diff`, `lint`, `preview`, `explain`, and `test` work today; `audit`, `gate`, `search`, `publish`, `lore`, and `run` exit `7` and are on the [roadmap](docs/roadmap.md). One first-party skill ships (`prereview`); six more are specified but not built. Adoption is single-digit stars — if the measurement above is what you want, you are early.

npm version diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 3760e86..3848c43 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "kitbash", - "version": "0.16.0", + "version": "0.24.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kitbash", - "version": "0.16.0", + "version": "0.24.2", "license": "Apache-2.0", "bin": { "kitbash": "dist/index.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index b24e262..1b7058c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "kitbash", - "version": "0.24.1", + "version": "0.24.2", "description": "The package manager and compiler for AI agent skills — write once, run in every coding agent", "license": "Apache-2.0", "author": "Harsh Singh", diff --git a/packages/cli/scripts/test.mjs b/packages/cli/scripts/test.mjs index e5f5288..0bae8ca 100644 --- a/packages/cli/scripts/test.mjs +++ b/packages/cli/scripts/test.mjs @@ -1947,6 +1947,36 @@ try { writeFileSync(join(ok, "skill.toml"), '[skill]\nname = "tidy-commits"\nversion = "1.0.0"\ndescription = "A skill with a spec-clean name"\n[context]\nbudget = 1500\n'); writeFileSync(join(ok, "SKILL.md"), "# Tidy\n\nBody.\n"); check("spec-name: a clean name is silent", !run(["lint", `file:${ok}`], nmTmp).out.includes("name-convention")); + + // Interop the other way: names the Agent Skills spec allows that KSF used to + // refuse. A leading digit is legal, and the old rule both rejected it and then + // silently installed the skill under a DIFFERENT name than its author declared. + const digit = join(nmTmp, "2fa-setup"); + mkdirSync(digit, { recursive: true }); + writeFileSync(join(digit, "SKILL.md"), "---\nname: 2fa-setup\ndescription: Set up two-factor authentication. Use when configuring 2FA for a service.\n---\nBody.\n"); + check("spec-name: a leading digit loads under its declared name", loadSkill(digit).manifest.skill.name === "2fa-setup"); + + const proj = mkdtempSync(join(tmpdir(), "kitbash-specname-proj-")); + try { + run(["init"], proj); + const ins = run(["install", `file:${digit}`, "--yes"], proj); + check("spec-name: and installs under it, not a mangled slug", ins.status === 0 && ins.out.includes("installed 2fa-setup@"), ins.out); + const lock = readFileSync(join(proj, "kitbash.lock"), "utf8"); + check("spec-name: the lockfile pins the declared name, not the mangled one", lock.includes('name = "2fa-setup"') && !lock.includes('name = "fa-setup"'), lock); + } finally { + rmSync(proj, { recursive: true, force: true }); + } + + // An unloadable declared name still falls back to the directory, which is what + // the spec's name-matches-directory rule wants — asserted by the name-invariant + // test above. What must NOT happen is that behaviour swallowing a name KSF can load. + + // A 64-character name is legal per the spec; the old 41-char cap rejected it. + const long = "a".repeat(64); + const longDir = join(nmTmp, long); + mkdirSync(longDir, { recursive: true }); + writeFileSync(join(longDir, "SKILL.md"), `---\nname: ${long}\ndescription: A skill with the longest name the Agent Skills spec permits.\n---\nBody.\n`); + check("spec-name: a 64-character name loads", loadSkill(longDir).manifest.skill.name === long); } finally { rmSync(nmTmp, { recursive: true, force: true }); } diff --git a/packages/cli/src/ksf.ts b/packages/cli/src/ksf.ts index d6c5613..edd2cd8 100644 --- a/packages/cli/src/ksf.ts +++ b/packages/cli/src/ksf.ts @@ -33,7 +33,21 @@ export interface LoadedSkill { } export const SKILLS_DIR = ".kitbash/skills"; -export const NAME_RE = /^[a-z][a-z0-9-]{1,40}$/; +/** + * What KSF will LOAD as a skill name: 1–64 characters, starting alphanumeric, + * then alphanumerics and hyphens. + * + * Deliberately looser than the Agent Skills spec, in both directions and for + * different reasons. It accepts `tidy--commits` and `tidy-`, which the spec + * forbids, because tightening a value constraint on a frozen manifest field + * would turn valid existing skills into load failures (RFC 0002); those names + * are reported by the `skills-spec` lint instead, which is where a + * host-compatibility problem belongs. And it accepts a leading digit and names + * up to 64 characters, which the old rule rejected outright — `2fa-setup` is a + * perfectly legal Agent Skill, and refusing to load one the whole ecosystem + * accepts was interop breakage, not strictness. + */ +export const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/; /** spec/schema/skill.schema.json: a trigger command is a slash plus a lowercase name — never a path. */ export const COMMAND_RE = /^\/[a-z][a-z0-9-]*$/; @@ -92,7 +106,15 @@ function loadBareSkill(dir: string, bodyPath: string, nameHint?: string): Loaded const fm = parseFrontmatter(raw); const body = raw.replace(FRONTMATTER_RE, "").trimStart(); - const slug = (s: string) => s.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 40); + // Spec-shaped slug: collapse runs of non-alphanumerics to single hyphens and trim + // them from both ends. Leading digits are kept — `2fa-setup` is a legal skill name. + const slug = (s: string) => + s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/, ""); + + // A frontmatter name is used only when KSF can load it. Otherwise the directory + // name wins: the Agent Skills spec requires `name` to match the parent directory, + // so deriving from the directory produces output every host accepts, where + // propagating a mismatched name would produce a skill they silently drop. const fallback = nameHint ? slug(nameHint) : slug(basename(dir)); const name = fm["name"] && NAME_RE.test(fm["name"]) ? fm["name"] : fallback; if (!NAME_RE.test(name)) throw new Error(`${dir}: cannot derive a valid skill name (got "${name}")`); diff --git a/site/changelog.html b/site/changelog.html index 213f8a0..63079a3 100644 --- a/site/changelog.html +++ b/site/changelog.html @@ -91,7 +91,7 @@

Changelog

Releases follow Keep a Changelog and semver — for skills and for this CLI, breaking prompt changes are breaking changes. The CLI is published to npm as kitbash and to Homebrew via singhharsh1708/tap. Tagged builds are on the GitHub releases page.

-
v0.24.1Current CLI version
+
v0.24.2Current CLI version
8Compile targets
Apache-2.0License
@@ -105,10 +105,19 @@

Changelog

Confirm with kitbash --version, which reads the installed package.json. Install and uninstall routes are covered on the installation page.

+
+
+

v0.24.2

+ 2026-08-08latest +
+

Fixed

+ +
+

v0.24.1

- 2026-08-12latest + 2026-08-12

Fixed

diff --git a/site/index.html b/site/index.html index 54b3c85..85cde00 100644 --- a/site/index.html +++ b/site/index.html @@ -151,7 +151,7 @@ -

Open format for AI agent skills · v0.24.1 · stable spec (RFC 0002)

+

Open format for AI agent skills · v0.24.2 · stable spec (RFC 0002)

Write an agent skill once. Run it everywhere.