Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.12.0] — 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.

### Fixed
- **`explain` no longer answers "no capability degradation" about a skill the target refuses to load.** Adapters can now declare target-specific constraints separately from `emit()`, and `explain` reads them directly — a true statement that read as a false one.

## [0.11.0] — 2026-07-28

Credibility pass. A staff-level review of the shipped product found the docs, site, and CLI contradicting each other — fatal for a tool whose whole pitch is honest measurement. Every fix here aligns the surface with reality; none is a new feature.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<a href="https://www.npmjs.com/package/kitbash"><img src="https://img.shields.io/npm/v/kitbash?color=ffb454" alt="npm version"></a>
<a href="https://www.npmjs.com/package/kitbash"><img src="https://img.shields.io/npm/dm/kitbash?color=ffb454" alt="npm downloads"></a>
<a href="https://github.com/singhharsh1708/kitbash/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/singhharsh1708/kitbash/ci.yml?branch=main" alt="CI"></a>
<img src="https://img.shields.io/badge/agent_targets-9-ffb454" alt="9 agent targets">
<img src="https://img.shields.io/badge/agent_targets-10-ffb454" alt="10 agent targets">
<a href="LICENSE"><img src="https://img.shields.io/github/license/singhharsh1708/kitbash?color=8b96ab" alt="Apache-2.0"></a>
</p>

Expand Down Expand Up @@ -39,7 +39,7 @@ kitbash install gh:singhharsh1708/kitbash/examples/skills/prereview
kitbash compile
```

What's working right now: `init`, `install` (via `gh:`, `owner/repo`, or `file:`), and `compile` to nine targets — Claude Code, Cursor, the vendor-neutral `.agents/skills/` path, Copilot, Cline, Devin (ex-Windsurf), Gemini CLI, Aider's CONVENTIONS.md, and the AGENTS.md floor. Declared `/commands` compile down to native slash commands. You also get `doctor`, `list`, `remove`, budget enforcement, a content-hash lockfile with drift detection, stale-output pruning, and `--strict`. Evals, update diffs, and everything else are on the [roadmap](docs/roadmap.md).
What's working right now: `init`, `install` (via `gh:`, `owner/repo`, or `file:`), and `compile` to ten targets — Claude Code, Cursor, the vendor-neutral `.agents/skills/` path, Zed, Copilot, Cline, Devin (ex-Windsurf), Gemini CLI, Aider's CONVENTIONS.md, and the AGENTS.md floor. Declared `/commands` compile down to native slash commands. You also get `doctor`, `list`, `remove`, budget enforcement, a content-hash lockfile with drift detection, stale-output pruning, and `--strict`. Evals, update diffs, and everything else are on the [roadmap](docs/roadmap.md).

Already have skills? A plain SKILL.md folder — the [skills.sh](https://www.skills.sh) / Claude Skills convention — installs directly with `kitbash install owner/repo`. It's basically KSF without the manifest, so Kitbash fills in defaults and marks it `unmanifested` since nobody declared a budget or permissions for it. skills.sh is good at distributing skills; Kitbash is about treating them like real engineering artifacts.

Expand Down Expand Up @@ -172,7 +172,7 @@ v0.1 is intentionally a thin slice: KSF, `compile`, three adapters, and one skil
No. It's a compiler, a package manager, and a format spec. Prompt collections are the thing that gets compiled.

**I already use skills.sh / Claude skills.**
Keep them. They install directly with `kitbash install owner/repo`. You pick up nine targets, a lockfile, and a token-cost report, and you don't give anything up.
Keep them. They install directly with `kitbash install owner/repo`. You pick up ten targets, a lockfile, and a token-cost report, and you don't give anything up.

**What if I stop using Kitbash?**
Nothing breaks. The compiled output is plain files in your repo. Delete `kitbash.toml` and everything keeps working the way it does now.
Expand Down
2 changes: 2 additions & 0 deletions docs/benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Token counts are estimates (~4 chars/token), the same estimator the compiler enf
| claude-code | 567 | 40 | lazy |
| cursor | 568 | 40 | lazy |
| agents | 567 | 40 | lazy |
| zed | 567 | 40 | lazy |
| copilot | 567 | 40 | lazy |
| cline | 539 | 539 | eager |
| windsurf | 569 | 40 | lazy |
Expand All @@ -34,6 +35,7 @@ Token counts are estimates (~4 chars/token), the same estimator the compiler enf
| claude-code | 880 | 19 | lazy |
| cursor | 879 | 19 | lazy |
| agents | 880 | 19 | lazy |
| zed | 880 | 19 | lazy |
| copilot | 880 | 19 | lazy |
| cline | 859 | 859 | eager |
| windsurf | 881 | 19 | lazy |
Expand Down
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Deferred out of v0.1 on purpose: more adapters, more skills, index, evals tier 2
- Community index (registry repo, Homebrew-tap model): `kitbash install prereview` short names, `kitbash search`.
- `kitbash publish` (validates, tags, points the index).
- Loadouts: `kitbash install loadout:oss-maintainer`.
- ✅ Remaining adapters, landed early: `windsurf` (now Devin Desktop), `cline`, `aider`, and `agents` — the vendor-neutral `.agents/skills/` path that Codex, Cursor, Copilot, Gemini CLI, Roo, Amp, OpenCode, Zed and Antigravity all read — 9 targets total.
- ✅ Remaining adapters, landed early: `windsurf` (now Devin Desktop), `cline`, `aider`, `agents` — the vendor-neutral `.agents/skills/` path that Codex, Cursor, Copilot, Gemini CLI, Roo, Amp, OpenCode and Antigravity all read — and `zed`, which shares that path but detects `.zed/` and enforces Zed's own frontmatter rules. 10 targets total.
- Docs site with the skill catalog + measured eval results per skill.
- Skill badges, measurement-only: eval pass rate, compiled token cost, auto-derived compatibility matrix, signed status. No star ratings — measurement over popularity, by design.
- **Launch moment.** The demo is one command turning a bare repo into a four-assistant, team-standard setup. Ponytail proved a single good skill can pull 75k stars; ours ride on infrastructure others can build on, which is the durable version of that story.
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kitbash",
"version": "0.11.0",
"version": "0.12.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",
Expand Down
10 changes: 7 additions & 3 deletions packages/cli/scripts/benchmark.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Measured benchmark: one source skill → 8 targets, real token cost per target.
* Measured benchmark: one source skill → every target, real token cost per target.
*
* Runs the actual compile pipeline on committed fixtures (no network), then
* measures the emitted output the way an agent pays for it: standing cost
Expand Down Expand Up @@ -44,7 +44,11 @@ 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.
case "agents":
case "zed":
return estimateTokens(read(`.agents/skills/${skillName}/SKILL.md`));
case "copilot":
return estimateTokens(read(`.github/skills/${skillName}/SKILL.md`));
Expand All @@ -68,8 +72,8 @@ function measure(tmp, target, skillName) {
const tmp = mkdtempSync(join(tmpdir(), "kitbash-bench-"));
const skills = [];
try {
// Every target present so all eight adapters fire.
for (const d of [".claude", ".cursor", ".agents", ".clinerules", ".windsurf", ".github"]) mkdirSync(join(tmp, d));
// Every target present so every adapter fires.
for (const d of [".claude", ".cursor", ".agents", ".zed", ".clinerules", ".windsurf", ".github"]) mkdirSync(join(tmp, d));
writeFileSync(join(tmp, "GEMINI.md"), "");
writeFileSync(join(tmp, "CONVENTIONS.md"), "");

Expand Down
47 changes: 47 additions & 0 deletions packages/cli/scripts/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,53 @@ try {
rmSync(devin, { recursive: true, force: true });
}

// --- zed: .zed detection, and the frontmatter constraints Zed enforces silently ---

const zed = mkdtempSync(join(tmpdir(), "kitbash-zed-"));
try {
mkdirSync(join(zed, ".zed"));
run(["init"], zed);
run(["install", `file:${fixture}`, "--yes"], zed);
const c = run(["compile"], zed);

// The gap this target closes: before it, a Zed-only repo compiled to nothing
// but the eager AGENTS.md floor and paid the whole body every session.
const zedSkill = join(zed, ".agents/skills/prereview/SKILL.md");
check("zed: .zed alone emits the vendor-neutral skills path", existsSync(zedSkill), c.out);
check("zed: emits Zed's frontmatter", /^---\nname: prereview\ndescription: "/.test(readFileSync(zedSkill, "utf8")), c.out);
check("zed: a conforming skill draws no constraint warning", !c.out.includes("→ zed:"), c.out);

// zed and agents write the same path; a repo with both must compile it once.
const both = run(["compile"], zed); // .agents/ now exists, so the agents adapter fires too
const writes = (both.out.match(/→ \.agents\/skills\/prereview\/SKILL\.md/g) ?? []).length;
check("zed + agents write the shared path once, no conflict", writes === 1 && !both.out.includes("conflict:"), both.out);

// Zed's own loader is stricter than KSF and rejects without a diagnostic, so
// an unmanifested SKILL.md — frontmatter copied through unvalidated — is the
// one input that can reach it malformed.
const badZed = (name, description) => {
const d = join(zed, `${name}-src`);
mkdirSync(d);
writeFileSync(join(d, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n\nBody one.\n\nBody two.\n`);
run(["install", `file:${d}`, "--yes"], zed);
};
badZed("tidy--commits", "Doubled hyphen is legal KSF, illegal in Zed");
badZed("empty-desc", '""');
badZed("wide-desc", "é".repeat(600)); // 600 chars, 1200 bytes — the byte-vs-char trap

const bad = run(["compile"], zed);
check("zed: doubled hyphen in name warned", bad.out.includes("tidy--commits → zed: name must match"), bad.out);
check("zed: empty description warned", bad.out.includes("empty-desc → zed: description is empty"), bad.out);
check("zed: description measured in bytes, not characters", bad.out.includes("wide-desc → zed: description is 1200 bytes"), bad.out);
check("zed: constraint breaches are warnings, so --strict fails", run(["compile", "--strict"], zed).status === 1);

// explain must not answer "no capability degradation" about a skill the target discards
const ex = run(["explain", "tidy--commits", "zed"], zed);
check("zed: explain surfaces the rejection", ex.status === 0 && ex.out.includes("✗ tidy--commits → zed: name must match"), ex.out);
} finally {
rmSync(zed, { recursive: true, force: true });
}

// --- trust & review: pre-install review, --yes, [policy] allowlists ---

const trust = mkdtempSync(join(tmpdir(), "kitbash-trust-"));
Expand Down
74 changes: 71 additions & 3 deletions packages/cli/src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export interface Adapter {
loading: "eager" | "lazy";
detect(root: string): boolean;
emit(skill: LoadedSkill, body: string, root: string): AdapterOutput;
/**
* Constraints this target enforces on a skill beyond what KSF validates, as
* warnings. emit() folds these into its own; `explain` reads them directly, so
* it can't answer "no degradation" about a skill the target silently discards.
*/
lint?(skill: LoadedSkill): string[];
}

export const GENERATED_MARK = "generated by kitbash";
Expand Down Expand Up @@ -173,14 +179,23 @@ const copilot = skillDirAdapter("copilot", ".github/skills", (root) => existsSyn
* Agent Skills directories (agentskills.io): a folder per skill whose SKILL.md
* carries `name` and `description` in frontmatter. Hosts inject only that
* metadata at session start and load the body on demand, so these are lazy.
*
* `lint` reports constraints a specific host enforces on that frontmatter beyond
* what KSF itself validates.
*/
function skillDirAdapter(id: string, dir: string, detect: (root: string) => boolean): Adapter {
function skillDirAdapter(
id: string,
dir: string,
detect: (root: string) => boolean,
lint?: (skill: LoadedSkill) => string[],
): Adapter {
return {
id,
// Writes only SKILL.md — no scripts/ copied. Claim nothing emit() doesn't deliver.
capabilities: [],
loading: "lazy",
detect,
...(lint ? { lint } : {}),
emit(skill, body) {
const { name, description } = skill.manifest.skill;
return {
Expand All @@ -190,7 +205,7 @@ function skillDirAdapter(id: string, dir: string, detect: (root: string) => bool
content: `---\nname: ${name}\ndescription: ${yamlString(description)}\n---\n${header(skill)}\n\n${body}`,
},
],
warnings: degradationWarnings(skill, this),
warnings: [...degradationWarnings(skill, this), ...(lint?.(skill) ?? [])],
};
},
};
Expand All @@ -210,6 +225,59 @@ const agents = skillDirAdapter(
(root) => existsSync(join(root, ".agents")) || existsSync(join(root, ".codex")),
);

/**
* Zed's skill loader (`crates/agent_skills/agent_skills.rs`) is stricter than
* KSF about frontmatter, and it fails *silently* — a skill that violates either
* constraint is dropped with no diagnostic in the UI, which is the silent
* capability loss spec §2 forbids. So the adapter says what Zed will not:
*
* - `name` must match ^[a-z0-9]+(-[a-z0-9]+)*$. KSF's NAME_RE is looser and
* permits a doubled or trailing hyphen (`tidy--commits`, `tidy-`), both of
* which Zed rejects. Its 64-character cap is unreachable from here: KSF names
* are capped at 41. `name` must also equal the containing directory name,
* which holds by construction — the directory IS the name.
* - `description` must be non-empty and at most 1024 UTF-8 *bytes*, not
* characters. A manifested skill is bounded at 200 characters (600 bytes at
* worst), so only an unmanifested SKILL.md — whose frontmatter is copied
* through unvalidated — can breach either bound.
*
* No `disable-model-invocation` key is emitted. Zed accepts one, but KSF has no
* field that means "never auto-invoke" (`triggers.auto` is a hint *for*
* auto-invocation), so synthesizing a value would be guessing at the author's
* intent and suppressing a skill they expect to fire.
*/
const ZED_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
const ZED_DESCRIPTION_MAX_BYTES = 1024;
const SILENTLY_DROPPED = "Zed drops the skill at load with no diagnostic";

function zedConstraints(skill: LoadedSkill): string[] {
const { name, description } = skill.manifest.skill;
const out: string[] = [];
if (!ZED_NAME_RE.test(name)) {
out.push(`${name} → zed: name must match ${ZED_NAME_RE.source} — no leading, trailing or doubled hyphen; ${SILENTLY_DROPPED}`);
}
const bytes = new TextEncoder().encode(description).length;
if (!description.trim()) {
out.push(`${name} → zed: description is empty; ${SILENTLY_DROPPED}`);
} else if (bytes > ZED_DESCRIPTION_MAX_BYTES) {
out.push(
`${name} → zed: description is ${bytes} bytes, over Zed's ${ZED_DESCRIPTION_MAX_BYTES}-byte limit (the limit is bytes, not characters); ${SILENTLY_DROPPED}`,
);
}
return out;
}

/**
* Zed reads the same vendor-neutral path, so this writes the byte-identical file
* the `agents` adapter does — a repo with both `.zed/` and `.agents/` compiles
* that skill once, not twice. What makes it its own target is detection and the
* constraint check: `.zed/` is the only marker a Zed-only repo has, and without
* this adapter such a repo compiles 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.
*/
const zed = skillDirAdapter("zed", ".agents/skills", (root) => existsSync(join(root, ".zed")), zedConstraints);

// Cline rule files are always loaded — eager.
const cline = fileAdapter(
"cline",
Expand Down Expand Up @@ -260,7 +328,7 @@ const aider = mergedFileAdapter(
/** The floor: everything that reads AGENTS.md (Codex and many others). */
const agentsmd = mergedFileAdapter("agentsmd", "AGENTS.md", () => true);

export const ADAPTERS: Adapter[] = [claudeCode, cursor, agents, copilot, cline, windsurf, gemini, aider, agentsmd];
export const ADAPTERS: Adapter[] = [claudeCode, cursor, agents, zed, copilot, cline, windsurf, gemini, aider, agentsmd];

/** Replace or append a skill's marker-delimited section in shared-file content. */
export function mergeSection(existing: string, name: string, section: string): string {
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,11 @@ export async function cmdExplain(args: string[]): Promise<number> {
}

const skillName = skill.manifest.skill.name;
// Target-specific constraints first: a skill the target refuses to load at all
// makes "no capability degradation" a true statement that reads as a false one.
const rejected = adapter.lint?.(skill) ?? [];
for (const r of rejected) console.log(`✗ ${r}`);

const missing = skill.manifest.targets.requires.filter((r) => !adapter.capabilities.includes(r));
if (!missing.length) {
console.log(`${skillName} → ${adapterName}: no capability degradation`);
Expand Down
2 changes: 1 addition & 1 deletion site/404.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ <h3>CLI reference</h3>
</a>
<a class="doc-link" href="docs/adapters">
<h3>Adapters &amp; targets</h3>
<p>All nine outputs and what each one supports, from Claude Code down to the <code>AGENTS.md</code> fallback.</p>
<p>All ten targets and what each one supports, from Claude Code down to the <code>AGENTS.md</code> fallback.</p>
</a>
<a class="doc-link" href="benchmark">
<h3>Benchmark</h3>
Expand Down
Loading