diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0a429..70dac26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format: [Keep a Changelog](https://keepachangelog.com). Versioning: semver — for skills *and* for this CLI, breaking prompt changes are breaking changes. +## [0.13.0] — 2026-07-29 + +### Fixed +- **Cline was being charged a standing tax it stopped owing, and served the same skill twice.** The `cline` adapter compiled to `.clinerules/.md` — a *rule*, always active, whole body in context every session — and declared the target eager. Cline reads Agent Skills (`apps/vscode/src/core/storage/skill-directories.ts` scans `.cline/skills`, `.clinerules/skills`, `.claude/skills` and `.agents/skills`) and loads them progressively: name and description at startup, the body only when `use_skill` fires. So the target was billed ~539 standing tokens for a skill Cline was willing to lazy-load for ~40. Worse, in any repo that also had `.agents/`, Kitbash emitted the identical body to both paths and Cline scanned both — the always-on copy defeating the lazy one, and a tool whose entire pitch is measuring standing cost silently adding ~539 tokens of it. `cline` now compiles to `.agents/skills//SKILL.md`, the same vendor-neutral file `agents` and `zed` emit, which makes the duplicate structurally impossible: one file serves Cline, Codex, Cursor, Copilot and Zed at once. Detection gains `.cline/`. Stale `.clinerules/.md` output is pruned on the next compile; your own rule files there are untouched. +- **The measured tax moved with it**, because the numbers are read from the adapters rather than restated: **14×** for a manifested skill (~40 lazy vs ~560 eager) and **47×** for an unmanifested one, up from 13× and 45×. Cline leaving the eager set costs a row in a table, not the argument — `aider` and `agentsmd` still carry the whole body every session. Eight of the ten targets are now lazy. + +### Added +- **A skill authored `disclosure = "eager"` now warns on `cline`.** Cline loads every skill on demand, so an eager-authored skill does not get the always-resident body it asked for. Compiling that silently would fix one spec §2 conformance violation by introducing its mirror image; it warns instead, and fails `--strict`. + ## [0.12.0] — 2026-07-28 ### Added diff --git a/README.md b/README.md index 7b4060a..a284b5c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ If you've used npm for packages, Docker for containers, or ESLint for lint rules **Stable specification, experimental ecosystem.** The Kitbash Skill Format (KSF) core is stabilized through [RFC 0002](rfcs/0002-ksf-1.0-stabilization.md): the manifest fields are frozen and evolve additive-only within the major version, so you can author skills and write adapters against a contract that won't shift under you. The ecosystem around it — more adapters, the index, first-party skills — is still early. -> **Compiler insight** — Kitbash measures a skill's *standing token cost* (what it adds to your context every session) at compile time, before you ever install it. For prereview that's ~40 tokens on a lazy target; compiled to an eager one it's ~540 — a [13× per-session tax](docs/benchmarks/README.md) no other format surfaces. Run `npm run bench` for the numbers. +> **Compiler insight** — Kitbash measures a skill's *standing token cost* (what it adds to your context every session) at compile time, before you ever install it. For prereview that's ~40 tokens on a lazy target; compiled to an eager one it's ~560 — a [14× per-session tax](docs/benchmarks/README.md) no other format surfaces. Run `npm run bench` for the numbers. ## A quick look @@ -29,7 +29,7 @@ If you've used npm for packages, Docker for containers, or ESLint for lint rules That's an actual session. A third-party skill from the [skills.sh](https://www.skills.sh) convention gets installed and compiled into three agent formats. The thing to notice is the last warning: during compile, Kitbash measured the skill and pointed out that it quietly costs about 5,044 tokens on every request for agents that can't lazy-load. A converter would just translate the format. The compiler reads it and tells you what it's going to cost you. I haven't found another tool that surfaces that number. -That gap is measured, not asserted — see the [benchmark](docs/benchmarks/README.md). Kitbash compiles to the cheapest loading mode each target actually supports, so prereview costs ~40 standing tokens on a lazy target; the tax is what it costs on the targets whose only mode is eager — ~540 tokens, a 13× per-session gap that a team running four agents pays four times over. Reproduce it with `npm run bench`. +That gap is measured, not asserted — see the [benchmark](docs/benchmarks/README.md). Kitbash compiles to the cheapest loading mode each target actually supports, so prereview costs ~40 standing tokens on a lazy target; the tax is what it costs on the targets whose only mode is eager — ~560 tokens, a 14× per-session gap that a team running four agents pays four times over. Reproduce it with `npm run bench`. Install it (npm or Homebrew — see [Installation](#installation)), then in a repo: diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md index cae35bc..5bd8dc9 100644 --- a/docs/benchmarks/README.md +++ b/docs/benchmarks/README.md @@ -20,13 +20,13 @@ Token counts are estimates (~4 chars/token), the same estimator the compiler enf | agents | 567 | 40 | lazy | | zed | 567 | 40 | lazy | | copilot | 567 | 40 | lazy | -| cline | 539 | 539 | eager | +| cline | 567 | 40 | lazy | | windsurf | 569 | 40 | lazy | | gemini | 567 | 40 | lazy | | aider | 560 | 560 | eager | | agentsmd | 560 | 560 | eager | -**Standing tax:** ~40 tokens on a lazy target vs ~539 on an eager one — about **13× per session** for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it. +**Standing tax:** ~40 tokens on a lazy target vs ~560 on an eager one — about **14× per session** for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it. ## `review-checklist` — bare / unmanifested (no budget) @@ -37,13 +37,13 @@ Token counts are estimates (~4 chars/token), the same estimator the compiler enf | agents | 880 | 19 | lazy | | zed | 880 | 19 | lazy | | copilot | 880 | 19 | lazy | -| cline | 859 | 859 | eager | +| cline | 880 | 19 | lazy | | windsurf | 881 | 19 | lazy | | gemini | 880 | 19 | lazy | | aider | 885 | 885 | eager | | agentsmd | 885 | 885 | eager | -**Standing tax:** ~19 tokens on a lazy target vs ~859 on an eager one — about **45× per session** for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it. +**Standing tax:** ~19 tokens on a lazy target vs ~885 on an eager one — about **47× per session** for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it. ## Why this is the pitch diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index af4ce7d..14a6de6 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "kitbash", - "version": "0.12.0", + "version": "0.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kitbash", - "version": "0.12.0", + "version": "0.13.0", "license": "Apache-2.0", "bin": { "kitbash": "dist/index.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index 9b3f4a9..5541524 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "kitbash", - "version": "0.12.0", + "version": "0.13.0", "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/benchmark.mjs b/packages/cli/scripts/benchmark.mjs index 3dc68bd..c180141 100644 --- a/packages/cli/scripts/benchmark.mjs +++ b/packages/cli/scripts/benchmark.mjs @@ -44,18 +44,17 @@ function measure(tmp, target, skillName) { return estimateTokens(read(`.claude/skills/${skillName}/SKILL.md`)); case "cursor": return estimateTokens(read(`.cursor/rules/${skillName}.mdc`)); - // zed reads the same vendor-neutral path and emits the same bytes, so it - // measures identically — listed rather than folded in, because a reader - // looking up "what does Zed cost" must find a row. + // zed and cline read the same vendor-neutral path and emit the same bytes, + // so they measure identically — listed rather than folded in, because a + // reader looking up "what does Zed cost" must find a row. case "agents": case "zed": + case "cline": return estimateTokens(read(`.agents/skills/${skillName}/SKILL.md`)); case "copilot": return estimateTokens(read(`.github/skills/${skillName}/SKILL.md`)); case "gemini": return estimateTokens(read(`.gemini/skills/${skillName}/SKILL.md`)); - case "cline": - return estimateTokens(read(`.clinerules/${skillName}.md`)); case "windsurf": return estimateTokens(read(`.windsurf/rules/${skillName}.md`)); case "aider": diff --git a/packages/cli/scripts/test.mjs b/packages/cli/scripts/test.mjs index 37f4f72..09ed5e9 100644 --- a/packages/cli/scripts/test.mjs +++ b/packages/cli/scripts/test.mjs @@ -69,7 +69,8 @@ try { check("agentsmd output exists", existsSync(agents)); check("copilot output exists", existsSync(join(tmp, ".github/skills/prereview/SKILL.md"))); check("copilot uses the lazy skills dir, not always-on instructions", !existsSync(join(tmp, ".github/instructions/prereview.instructions.md"))); - check("cline output exists", existsSync(join(tmp, ".clinerules/prereview.md"))); + check("cline compiles to the lazy skills path, not a .clinerules rule", existsSync(join(tmp, ".agents/skills/prereview/SKILL.md"))); + check("cline no longer emits an always-on .clinerules rule", !existsSync(join(tmp, ".clinerules/prereview.md"))); check("windsurf output exists", existsSync(join(tmp, ".windsurf/rules/prereview.md"))); const windsurfOut = readFileSync(join(tmp, ".windsurf/rules/prereview.md"), "utf8"); check("windsurf rule is model_decision (lazy), not always-on", windsurfOut.startsWith("---\ntrigger: model_decision\n"), windsurfOut.slice(0, 120)); @@ -93,9 +94,9 @@ try { check("agentsmd markers present", agentsContent.includes("") && agentsContent.includes("")); check("eager-load warning surfaced", compile.out.includes("cannot lazy-load"), compile.out); // every eager target must report the standing cost; lazy targets must not - const eagerWarned = ["cline", "aider", "agentsmd"].every((t) => compile.out.includes(`→ ${t}: ${t} is eager and cannot lazy-load`)); + const eagerWarned = ["aider", "agentsmd"].every((t) => compile.out.includes(`→ ${t}: ${t} is eager and cannot lazy-load`)); check("all eager targets report standing cost", eagerWarned, compile.out); - const lazyQuiet = ["claude-code", "cursor", "windsurf", "agents", "copilot", "gemini"].every((t) => !compile.out.includes(`→ ${t}: ${t} is eager`)); + const lazyQuiet = ["claude-code", "cursor", "windsurf", "agents", "zed", "cline", "copilot", "gemini"].every((t) => !compile.out.includes(`→ ${t}: ${t} is eager`)); check("lazy targets do not warn", lazyQuiet, compile.out); const recompile = run(["compile"], tmp); @@ -253,7 +254,7 @@ try { check("compile prunes stale claude output", !existsSync(join(tmp, ".claude/skills/prereview")), pruneCompile.out); check("compile prunes stale cursor output", !existsSync(join(tmp, ".cursor/rules/prereview.mdc")), pruneCompile.out); check("compile prunes stale command shim", !existsSync(join(tmp, ".claude/commands/prereview.md")), pruneCompile.out); - check("compile prunes stale cline output", !existsSync(join(tmp, ".clinerules/prereview.md")), pruneCompile.out); + check("compile prunes stale cline skill", !existsSync(join(tmp, ".agents/skills/prereview/SKILL.md")), pruneCompile.out); const prunedAgents = readFileSync(join(tmp, "AGENTS.md"), "utf8"); check("compile prunes stale AGENTS.md section", !prunedAgents.includes("kitbash:begin prereview"), prunedAgents.slice(0, 200)); check("surviving skill section intact", prunedAgents.includes("kitbash:begin tidy-commits")); @@ -746,6 +747,63 @@ try { rmSync(zed, { recursive: true, force: true }); } +// --- cline: lazy skills, not an always-on rule, and never emitted twice --- + +const clineFix = mkdtempSync(join(tmpdir(), "kitbash-cline-")); +try { + mkdirSync(join(clineFix, ".clinerules")); + // The output the pre-0.13.0 adapter left behind, plus a user's own rule file + // that must survive: pruning is scoped to kitbash's own generated output. + writeFileSync( + join(clineFix, ".clinerules/prereview.md"), + "\n\nold body\n", + ); + writeFileSync(join(clineFix, ".clinerules/house-style.md"), "# My own rule\n\nKeep it.\n"); + + run(["init"], clineFix); + run(["install", `file:${fixture}`, "--yes"], clineFix); + const c = run(["compile"], clineFix); + + check("cline: emits a lazy skill", existsSync(join(clineFix, ".agents/skills/prereview/SKILL.md")), c.out); + check("cline: upgrade prunes the stale always-on rule", !existsSync(join(clineFix, ".clinerules/prereview.md")), c.out); + check("cline: a user's own .clinerules file is untouched", existsSync(join(clineFix, ".clinerules/house-style.md")), c.out); + check("cline: no longer reports a standing token cost", !c.out.includes("→ cline: cline is eager"), c.out); + + // The headline bug: .clinerules + .agents both present used to emit the same + // body twice, and Cline scans both paths — so the always-on copy defeated the + // lazy one. Sharing the path makes the duplicate structurally impossible. + mkdirSync(join(clineFix, ".agents"), { recursive: true }); + const both = run(["compile"], clineFix); + const emitted = (both.out.match(/→ \.agents\/skills\/prereview\/SKILL\.md/g) ?? []).length; + check("cline + agents emit the shared skill once, not twice", emitted === 1 && !both.out.includes("conflict:"), both.out); + check("cline + agents leave no second copy anywhere", !existsSync(join(clineFix, ".cline/skills/prereview/SKILL.md")), both.out); + + // The mirror-image conformance risk: Cline loads every skill on demand, so an + // eager-authored skill does not get what it asked for. That must be visible. + const eagerSrc = join(clineFix, "eager-src"); + mkdirSync(eagerSrc); + writeFileSync( + join(eagerSrc, "skill.toml"), + '[skill]\nname = "always-on"\nversion = "0.1.0"\ndescription = "A rule that must always be resident"\n[context]\nbudget = 500\ndisclosure = "eager"\n', + ); + writeFileSync(join(eagerSrc, "SKILL.md"), "Body of always-on.\n\nMore body.\n"); + run(["install", `file:${eagerSrc}`, "--yes"], clineFix); + const eagerOut = run(["compile"], clineFix); + check( + "cline: eager-authored skill warns that Cline loads on demand", + eagerOut.out.includes('always-on → cline: authored disclosure = "eager"'), + eagerOut.out, + ); + check("cline: that warning fails --strict", run(["compile", "--strict"], clineFix).status === 1); + check( + "cline: a lazy-authored skill draws no such warning", + !eagerOut.out.includes('prereview → cline: authored disclosure'), + eagerOut.out, + ); +} finally { + rmSync(clineFix, { recursive: true, force: true }); +} + // --- trust & review: pre-install review, --yes, [policy] allowlists --- const trust = mkdtempSync(join(tmpdir(), "kitbash-trust-")); diff --git a/packages/cli/src/adapters.ts b/packages/cli/src/adapters.ts index a41a676..886aeb1 100644 --- a/packages/cli/src/adapters.ts +++ b/packages/cli/src/adapters.ts @@ -278,16 +278,54 @@ function zedConstraints(skill: LoadedSkill): string[] { */ const zed = skillDirAdapter("zed", ".agents/skills", (root) => existsSync(join(root, ".zed")), zedConstraints); -// Cline rule files are always loaded — eager. -const cline = fileAdapter( +/** + * Cline reads Agent Skills, and has since it grew a skills loader + * (`apps/vscode/src/core/storage/skill-directories.ts`): `.cline/skills`, + * `.clinerules/skills`, `.claude/skills` and `.agents/skills` are all scanned. + * Loading is progressive — name and description at startup, the body only when + * `use_skill` fires. + * + * Until 0.13.0 this adapter wrote `.clinerules/.md`, which is a *rule*: + * always active, whole body in context, every session. That was wrong twice + * over. It billed Cline ~539 standing tokens for a skill Cline was willing to + * lazy-load for ~40, and in a repo that also had `.agents/` it emitted the same + * body to both paths — so Cline loaded the skill twice, and the always-on copy + * defeated the lazy one. A compiler whose entire claim is "cheapest loading + * mode the target actually supports" was picking the most expensive mode + * available and charging for it. + * + * Of the four scanned directories this emits to `.agents/skills`, the same + * vendor-neutral path `agents` and `zed` use, rather than the `.cline/skills` + * their docs nominate. Cline scans both equally, and sharing the path is what + * makes the duplicate structurally impossible: one file serves Cline, Codex, + * Cursor, Copilot and Zed at once, and a repo with several of their marker + * directories still compiles that skill exactly once. + */ +const cline = skillDirAdapter( "cline", - [], - "eager", - (root) => existsSync(join(root, ".clinerules")), - (name) => `.clinerules/${name}.md`, - () => "", + ".agents/skills", + (root) => existsSync(join(root, ".clinerules")) || existsSync(join(root, ".cline")), + clineConstraints, ); +/** + * The mirror of the bug above: Cline loads every skill on demand, so a skill + * authored `disclosure = "eager"` does not get the always-resident body its + * author asked for. Compiling that silently would trade one conformance + * violation (spec §2) for its reflection, so say it out loud. + * + * `.clinerules/.md` would still deliver eager loading, and a + * disclosure-aware two-path adapter is the obvious extension — but nobody has + * asked for it, and a warning is the honest minimum in the meantime. + */ +function clineConstraints(skill: LoadedSkill): string[] { + if (skill.manifest.context.disclosure !== "eager") return []; + const { name } = skill.manifest.skill; + return [ + `${name} → cline: authored disclosure = "eager", but Cline loads skills on demand — the body stays out of context until the model invokes it`, + ]; +} + /** * Windsurf became Devin Desktop on 2026-06-02; `.devin/rules/` is the preferred * path and `.windsurf/rules/` remains a supported fallback, so emit to whichever diff --git a/site/benchmark.html b/site/benchmark.html index 3d4af1f..8951594 100644 --- a/site/benchmark.html +++ b/site/benchmark.html @@ -4,17 +4,17 @@ The standing token tax, measured — Kitbash - + - + - + @@ -113,8 +113,8 @@

#The standing token tax, measured

-
13×Standing cost of a manifested skill on an eager target vs a lazy one
-
45×Same comparison for an unmanifested SKILL.md-only skill
+
14×Standing cost of a manifested skill on an eager target vs a lazy one
+
47×Same comparison for an unmanifested SKILL.md-only skill
9Targets measured, every one through kitbash compile
0Hand-entered numbers — the table is generated and CI fails if it drifts
@@ -148,7 +148,7 @@

#prereview — manifested, budget 1500, authored l agents56740lazy zed56740lazy copilot56740lazy - cline539539eager + cline56740lazy windsurf56940lazy gemini56740lazy aider560560eager @@ -156,7 +156,7 @@

#prereview — manifested, budget 1500, authored l
-

Standing tax: about 40 tokens on a lazy target against about 539 on an eager one — roughly 13× per session for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it.

+

Standing tax: about 40 tokens on a lazy target against about 560 on an eager one — roughly 14× per session for the identical skill. A team running four agents pays that gap four times over, forever, unless something measures it.

@@ -173,7 +173,7 @@

#review-checklist — bare, no manifest

agents88019lazy zed88019lazy copilot88019lazy - cline859859eager + cline88019lazy windsurf88119lazy gemini88019lazy aider885885eager @@ -181,7 +181,7 @@

#review-checklist — bare, no manifest

-

Standing tax: about 19 tokens on a lazy target against about 859 on an eager one — roughly 45× per session. The longer the skill, the worse the ratio, because the lazy stub stays small while the eager copy grows with the body.

+

Standing tax: about 19 tokens on a lazy target against about 885 on an eager one — roughly 47× per session. The longer the skill, the worse the ratio, because the lazy stub stays small while the eager copy grows with the body.

diff --git a/site/changelog.html b/site/changelog.html index 8ff5b67..b35519b 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.12.0Current CLI version
+
v0.13.0Current CLI version
8Compile targets
Apache-2.0License
@@ -105,10 +105,21 @@

Changelog

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

+
+
+

v0.13.0

+ 2026-07-29latest +
+

Fixed

+
  • Cline was being charged a standing tax it stopped owing, and served the same skill twice. The cline adapter compiled to .clinerules/<name>.md — a rule, always active, whole body in context every session — and declared the target eager. Cline reads Agent Skills (apps/vscode/src/core/storage/skill-directories.ts scans .cline/skills, .clinerules/skills, .claude/skills and .agents/skills) and loads them progressively: name and description at startup, the body only when use_skill fires. So the target was billed ~539 standing tokens for a skill Cline was willing to lazy-load for ~40. Worse, in any repo that also had .agents/, Kitbash emitted the identical body to both paths and Cline scanned both — the always-on copy defeating the lazy one, and a tool whose entire pitch is measuring standing cost silently adding ~539 tokens of it. cline now compiles to .agents/skills/<name>/SKILL.md, the same vendor-neutral file agents and zed emit, which makes the duplicate structurally impossible: one file serves Cline, Codex, Cursor, Copilot and Zed at once. Detection gains .cline/. Stale .clinerules/<name>.md output is pruned on the next compile; your own rule files there are untouched.
  • The measured tax moved with it, because the numbers are read from the adapters rather than restated: 14× for a manifested skill (~40 lazy vs ~560 eager) and 47× for an unmanifested one, up from 13× and 45×. Cline leaving the eager set costs a row in a table, not the argument — aider and agentsmd still carry the whole body every session. Eight of the ten targets are now lazy.
+

Added

+
  • A skill authored disclosure = "eager" now warns on cline. Cline loads every skill on demand, so an eager-authored skill does not get the always-resident body it asked for. Compiling that silently would fix one spec §2 conformance violation by introducing its mirror image; it warns instead, and fails --strict.
+
+

v0.12.0

- 2026-07-28latest + 2026-07-28

Added

  • zed adapter — the tenth target. Zed reads the vendor-neutral .agents/skills/<name>/SKILL.md path, but nothing in a Zed-only repo announces that: the agents adapter probes .agents/ and .codex/, neither of which Zed creates. A repo whose only agent marker was .zed/ therefore compiled to nothing but the eager AGENTS.md floor — paying a skill's whole body every session on an agent that can lazy-load it for free. The new target detects .zed/ and emits the byte-identical file agents does, so a repo with both compiles that skill once, not twice. Ten targets, nine output files.
  • Zed's silent frontmatter rejections are now visible. Zed's skill loader is stricter than KSF and drops a non-conforming skill at load with no diagnostic in the UI — the silent capability loss spec §2 forbids. Two constraints are checked and warned about at compile (so --strict fails on them) and in explain: name must match ^[a-z0-9]+(-[a-z0-9]+)*$, which rejects the doubled and trailing hyphens KSF's own name rule permits (tidy--commits is a legal skill name Zed will not load); and description must be non-empty and at most 1024 UTF-8 bytes, not characters — a trap for any non-ASCII description, where 600 characters can be 1,200 bytes. A manifested skill is bounded at 200 characters, so only an unmanifested SKILL.md, whose frontmatter is copied through unvalidated, can breach either bound. No disable-model-invocation key is emitted: KSF has no field meaning "never auto-invoke", so synthesizing one would guess at the author's intent.
diff --git a/site/docs/adapters.html b/site/docs/adapters.html index 8b9aa43..d87b910 100644 --- a/site/docs/adapters.html +++ b/site/docs/adapters.html @@ -110,15 +110,15 @@

Adapters & targets

-

An adapter turns one installed skill into the native format of one coding agent. kitbash compile runs every detected adapter over every installed skill and writes the result. Two emission styles exist: per-skill files (Claude Code, Cursor, the vendor-neutral .agents/skills/ path, Copilot, Cline, Devin, Gemini CLI) and marker-merged sections in a shared file (AGENTS.md, CONVENTIONS.md).

+

An adapter turns one installed skill into the native format of one coding agent. kitbash compile runs every detected adapter over every installed skill and writes the result. Two emission styles exist: per-skill files (Claude Code, Cursor, the vendor-neutral .agents/skills/ path that Cline and Zed also read, Copilot, Devin, Gemini CLI) and marker-merged sections in a shared file (AGENTS.md, CONVENTIONS.md).

Lazy vs eager

The single most important property of a target is how it loads a compiled skill.

    -
  • lazy The target keeps the skill out of the context window until it is invoked. Only a standing stub — a name and description the agent can match against — is resident. Claude Code SKILL.md files, Cursor agent-requested rules, the vendor-neutral .agents/skills/ path, the .github/skills/ and .gemini/skills/ directories Copilot and Gemini CLI read, and Devin rules with a model_decision trigger all work this way.
  • -
  • eager The target has no mechanism for deferred loading, so the entire compiled body sits in context every session. AGENTS.md, CONVENTIONS.md, and Cline rule files all work this way.
  • +
  • lazy The target keeps the skill out of the context window until it is invoked. Only a standing stub — a name and description the agent can match against — is resident. Claude Code SKILL.md files, Cursor agent-requested rules, the vendor-neutral .agents/skills/ path, the .github/skills/ and .gemini/skills/ directories Copilot and Gemini CLI read, the four skill directories Cline scans, and Devin rules with a model_decision trigger all work this way.
  • +
  • eager The target has no mechanism for deferred loading, so the entire compiled body sits in context every session. AGENTS.md and CONVENTIONS.md work this way.
-

Kitbash compiles to the cheapest loading mode each target actually supports, so seven of the ten carry only a stub. The standing token tax is what a skill costs on the targets whose only mode is eager: a skill authored with disclosure = "lazy" cannot lazy-load there — the target simply has nowhere to put a stub. Kitbash refuses to let that happen quietly: on every eager target, a lazy-authored skill produces a warning that names the real cost and the limit the author declared.

+

Kitbash compiles to the cheapest loading mode each target actually supports, so eight of the ten carry only a stub. The standing token tax is what a skill costs on the targets whose only mode is eager: a skill authored with disclosure = "lazy" cannot lazy-load there — the target simply has nowhere to put a stub. Kitbash refuses to let that happen quietly: on every eager target, a lazy-authored skill produces a warning that names the real cost and the limit the author declared.

 prereview → agentsmd: agentsmd is eager and cannot lazy-load;
   this skill costs ~560 tokens standing every session (declared limit: 60)

Multiply that by the number of installed skills and it is the whole context budget problem. The benchmark page quantifies the gap between lazy and eager delivery of the same skill.

@@ -166,9 +166,9 @@

Capability matrix

cline - .clinerules/<name>.md - .clinerules/ exists - eager + .agents/skills/<name>/SKILL.md + .clinerules/ or .cline/ exists + lazy none @@ -238,7 +238,9 @@

copilot

Before 0.8.0 this adapter wrote .github/instructions/<name>.instructions.md with applyTo: "**", a glob that matched every single request and made the target eager.

cline

-

Writes .clinerules/<name>.md with no frontmatter at all — header and body only. Cline rule files are always loaded, so this is eager.

+

Writes .agents/skills/<name>/SKILL.md — the same vendor-neutral file agents and zed emit. Cline scans four skill directories (.cline/skills, .clinerules/skills, .claude/skills and .agents/skills) and loads them progressively: name and description at startup, the body only when use_skill fires. So the target is lazy. Detection is .clinerules/ or .cline/.

+

Before 0.13.0 this adapter wrote .clinerules/<name>.md, which is a rule — always active, whole body in context, every session. That was wrong twice over. It billed Cline ~539 standing tokens for a skill Cline was willing to lazy-load for ~40, and in a repo that also had .agents/ it emitted the same body to both paths, so Cline loaded the skill twice and the always-on copy defeated the lazy one. Sharing the vendor-neutral path is what makes that duplicate structurally impossible: one file serves Cline, Codex, Cursor, Copilot and Zed at once. Stale .clinerules/<name>.md output is pruned on the next compile; your own rule files there are left alone.

+

Cline loads every skill on demand, so a skill authored disclosure = "eager" does not get the always-resident body its author asked for. That produces a warning rather than passing silently — fixing one conformance violation by introducing its mirror image would be no fix at all.

windsurf

Windsurf became Devin Desktop on 2026-06-02. .devin/rules/ is the preferred path and .windsurf/rules/ remains a supported fallback, so the adapter emits to whichever the repo actually has: .devin/rules/<name>.md when .devin/ exists, .windsurf/rules/<name>.md otherwise.

diff --git a/site/docs/config.html b/site/docs/config.html index 59faffe..f447825 100644 --- a/site/docs/config.html +++ b/site/docs/config.html @@ -149,7 +149,7 @@

targets

agents.agents/ or .codex/ zed.zed/ copilot.github/ - cline.clinerules/ + cline.clinerules/ or .cline/ windsurf.devin/ or .windsurf/ geminiGEMINI.md or .gemini/ aiderCONVENTIONS.md or .aider.conf.yml @@ -286,7 +286,7 @@

Generated agent outputs

cursor.cursor/rules/<name>.mdc agents.agents/skills/<name>/SKILL.md copilot.github/skills/<name>/SKILL.md - cline.clinerules/<name>.md + cline.agents/skills/<name>/SKILL.md windsurf.devin/rules/<name>.md, or .windsurf/rules/<name>.md when there is no .devin/ gemini.gemini/skills/<name>/SKILL.md aiderCONVENTIONS.md (merged section) diff --git a/site/docs/index.html b/site/docs/index.html index 43244de..4a8c767 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -112,12 +112,12 @@

Documentation

What Kitbash is

Every coding assistant invented its own extension format: .claude/skills/, .cursor/rules/*.mdc, .github/skills/, .clinerules, .windsurfrules, .gemini/skills/, CONVENTIONS.md, AGENTS.md, and the vendor-neutral .agents/skills/ path. A skill written for one of them does nothing for the rest of the team. Kitbash defines a single format — KSF, a directory with a skill.toml manifest and a SKILL.md body — and compiles it into all nine of those native outputs. There is no runtime: your agent reads its own format and never knows Kitbash was involved.

-

The compiler is the part that matters. Because a skill declares its context budget and its disclosure mode, kitbash compile can measure the skill's standing token cost — what it adds to your context window on every single request — for each target, before you ever ship it. Kitbash compiles to the cheapest loading mode each target actually supports: Claude Code, Cursor, Copilot, Devin, Gemini CLI and the .agents/skills/ path load a skill only when it is invoked. Cline, Aider and the AGENTS.md floor have no deferred mode and carry the whole body every session. In the benchmark the same skill costs roughly 40 standing tokens on a lazy target and roughly 540 on an eager one: a 13× per-session tax that a team running four agents pays four times over. A converter would translate the format and stop there.

+

The compiler is the part that matters. Because a skill declares its context budget and its disclosure mode, kitbash compile can measure the skill's standing token cost — what it adds to your context window on every single request — for each target, before you ever ship it. Kitbash compiles to the cheapest loading mode each target actually supports: Claude Code, Cursor, Copilot, Cline, Devin, Gemini CLI, Zed and the .agents/skills/ path load a skill only when it is invoked. Aider and the AGENTS.md floor have no deferred mode and carry the whole body every session. In the benchmark the same skill costs roughly 40 standing tokens on a lazy target and roughly 560 on an eager one: a 14× per-session tax that a team running four agents pays four times over. A converter would translate the format and stop there.

The KSF core is stabilized through RFC 0002: the manifest fields are frozen and evolve additive-only within the major version, so skills and third-party adapters can be written against a contract that will not shift. The ecosystem around the spec — more adapters, the index, first-party skills — is still early. Read current status before you plan around it.

9compile targets from one source directory
-
13×measured standing-cost gap, lazy vs eager
+
14×measured standing-cost gap, lazy vs eager
0runtime dependencies, in the CLI and in your repo
diff --git a/site/docs/quickstart.html b/site/docs/quickstart.html index d4bff90..2519ec1 100644 --- a/site/docs/quickstart.html +++ b/site/docs/quickstart.html @@ -148,7 +148,7 @@

4. Compile

AGENTS.md §prereview prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~560 tokens standing every session -

That last warning is the part other tools don't give you. Kitbash compiles to the cheapest loading mode each target supports, so lazy targets (Claude Code, Cursor, Devin, the .agents/skills/ path) load a skill only when it's invoked. Targets with no deferred mode carry the whole body in context every single session — a standing tax measured at 13×–45× in the benchmark.

+

That last warning is the part other tools don't give you. Kitbash compiles to the cheapest loading mode each target supports, so lazy targets (Claude Code, Cursor, Devin, the .agents/skills/ path) load a skill only when it's invoked. Targets with no deferred mode carry the whole body in context every single session — a standing tax measured at 14×–47× in the benchmark.

5. Inspect before you trust

Three read-only commands work on installed skills and on sources you haven't installed:

diff --git a/site/index.html b/site/index.html index 57df703..0a7e261 100644 --- a/site/index.html +++ b/site/index.html @@ -151,7 +151,7 @@
-

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

+

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

Write an agent skill once. Run it everywhere.

Get started @@ -195,7 +195,7 @@

#Every agent invented its own format

GitHub Copilot.github/skills/*/SKILL.md
CodexAGENTS.md
Devin (ex-Windsurf).devin/rules/*.md
-
Cline.clinerules/*.md
+
Cline.agents/skills/*/SKILL.md
Gemini CLI.gemini/skills/*/SKILL.md
AiderCONVENTIONS.md
@@ -342,13 +342,17 @@

#Pick a target