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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@

Format: [Keep a Changelog](https://keepachangelog.com). Versioning: semver — for skills *and* for this CLI, breaking prompt changes are breaking changes.

## [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.

### Fixed
- **The flagship number was reported three ways.** `compile` warned ~517, `preview` showed ~507, and the benchmark said 560 for the same skill on the same target — because the compiled permissions note and the adapter's own wrapper were counted inconsistently. All three now report the true cost of the emitted file (560), and `preview` renders exactly what `compile` writes.
- **The measurement no longer fails `--strict`.** The standing-cost report was pushed into the same `warnings` array as real problems, so `compile --strict` failed on the one bundled skill — the product punishing the one number it exists to surface. It is now an informational note (`ℹ`), not a warning; `--strict` still fails on genuine warnings (budget overruns, degradation, conflicts).
- **Stub commands cited already-passed milestones.** `kitbash update` said "lands in v0.2" on a 0.10 build. The eight unimplemented commands now point at the roadmap with no stale version, and `--help` lists them under a separate "Planned (not yet implemented)" section instead of mixing them with working commands.
- **The bundled example advertised `mode = gate`** while `kitbash gate` is unimplemented; it now ships as `mode = skill`, runnable end to end with shipped commands.
- **Exit codes are consistent and conventional:** unknown command is `2` (usage) with a did-you-mean suggestion instead of `1` + a full help dump; an unbuilt command is `7`; `test`/`lint` on an empty repo return `0` (vacuous pass) to match `compile`/`list`, so CI scripting agrees across commands.
- **`doctor`** shows undetected targets with `·` (not present in this repo) instead of `✗` (which now means a real problem), with an "N of 9 in this repo" count.
- **`compile`** on a partial fan-out prints how many more targets are available and how to enable them, so "compiled for 2 targets" no longer reads as a shortfall against the "every agent" pitch.
- Website and docs corrected: the interactive preview no longer shows a fictional 1,480-token cost, the quickstart review/compile blocks match the real CLI, the standing-tax figure is stated once, README marks which skills ship vs are planned, and "KSF" is expanded on first use.

### Added
- Per-command help (`kitbash install --help`, `kitbash help <command>`), a did-you-mean suggestion on a mistyped command, and proper pluralization in count lines ("1 skill for 2 targets", not "1 skill(s) for 2 agent target(s)"). Token counts carry units (`standing 60 tok/session`). Removed the `-v` version alias (it collides with the near-universal `-v` = verbose).

## [0.10.0] — 2026-07-26

Secrets and behavioral checks — adopted from the field-tested rule set of a dedicated agent-config scanner, but scoped to what Kitbash does: scan the skill being installed, statically, with no network. Kitbash does not audit your own agent config (settings.json, MCP servers, hooks) — that is a different tool's job.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ Kitbash is an open format for AI agent skills, plus a compiler that turns one sk

If you've used npm for packages, Docker for containers, or ESLint for lint rules, it's the same idea for agent skills.

**Stable specification, experimental ecosystem.** The 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.
**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. On a lazy target that's ~40 tokens; 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 ~540 — a [13× per-session tax](docs/benchmarks/README.md) no other format surfaces. Run `npm run bench` for the numbers.

## A quick look

Expand All @@ -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 the same skill 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 — ~540 tokens, a 13× 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:

Expand Down Expand Up @@ -158,7 +158,7 @@ max_budget = 6000 # cap per-skill context budget

## Flagship skills

A few of the skills that ship with it: `/prereview` reviews your diff against your team's actual standards, `/excavate` answers "why is this code like this?" and shows its work, `/triage` sorts out red CI runs, `/plan` turns issues into file-level plans, `/verify` proves a change works by actually driving it, `/migrate` runs checkpointed migration campaigns, and `/onboard` writes living codebase tours.
One skill ships today: `/prereview`, which reviews your diff against your team's actual standards. Six more are designed in detail but not yet built — `/excavate` answers "why is this code like this?" and shows its work, `/triage` sorts out red CI runs, `/plan` turns issues into file-level plans, `/verify` proves a change works by actually driving it, `/migrate` runs checkpointed migration campaigns, and `/onboard` writes living codebase tours. They're on the [roadmap](docs/roadmap.md).

Full specs, plus the list of things we decided not to build, are in [docs/skills-catalog.md](docs/skills-catalog.md).

Expand Down
2 changes: 2 additions & 0 deletions docs/skills-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

First-party skills, dogfooded on this repo before release. Each exists because it moves a real lever — fewer review cycles, faster onboarding, fewer bugs — not because a model *can* do it. Selection rule: if a frontier model already does it well from a bare prompt, it's not a skill; skills earn their place through project grounding (lore), enforcement (gates), or composition (artifacts).

**Status:** one skill ships today — `/prereview`, in `examples/skills/`. The other six below are designed here in full but not yet built; see the [roadmap](roadmap.md).

Pipeline they form:

```
Expand Down
2 changes: 1 addition & 1 deletion examples/skills/prereview/skill.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ consumes = ["plan@1"]

[targets]
requires = []
mode = "gate"
mode = "skill"

[lore]
reads = ["conventions", "invariants"]
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.10.0",
"version": "0.11.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
16 changes: 10 additions & 6 deletions packages/cli/scripts/test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ try {

const install = run(["install", `file:${fixture}`], tmp);
check("install exits 0", install.status === 0, install.out);
check("install reports budget", install.out.includes("budget 1500 tokens"), install.out);
check("install reports budget", install.out.includes("budget 1500 tok"), install.out);

const dup = run(["install", `file:${fixture}`], tmp);
check("duplicate install rejected", dup.status === 1, dup.out);
Expand All @@ -59,7 +59,7 @@ try {

const compile = run(["compile"], tmp);
check("compile exits 0", compile.status === 0, compile.out);
check("compile summary", compile.out.includes("compiled 1 skill(s) for 9 agent target(s)"), compile.out);
check("compile summary", compile.out.includes("compiled 1 skill for 9 targets"), compile.out);

const claude = join(tmp, ".claude/skills/prereview/SKILL.md");
const cursor = join(tmp, ".cursor/rules/prereview.mdc");
Expand Down Expand Up @@ -103,7 +103,9 @@ try {
check("recompile idempotent (single AGENTS.md section)", recompile.status === 0 && markerCount === 1, `markers=${markerCount}`);

const strict = run(["compile", "--strict"], tmp);
check("--strict fails on warnings", strict.status === 1, strict.out);
// prereview's eager standing cost is an informational NOTE, not a warning, so --strict passes.
check("--strict passes when only the measurement note is present", strict.status === 0, strict.out);
check("eager standing surfaced as an informational note (not a warning)", compile.out.includes("ℹ") && compile.out.includes("adds ~") && !/⚠[^\n]*eager/.test(compile.out), compile.out);

const doctor = run(["doctor"], tmp);
check("doctor exits 0", doctor.status === 0, doctor.out);
Expand Down Expand Up @@ -137,6 +139,8 @@ try {
const bareOut = readFileSync(join(tmp, ".claude/skills/tidy-commits/SKILL.md"), "utf8");
check("bare skill frontmatter not doubled", bareOut.startsWith("---\nname: tidy-commits\n"), bareOut.slice(0, 120));
check("bare warning surfaced at compile", bareCompile.out.includes("tidy-commits: unmanifested"), bareCompile.out);
// --strict still fails on a REAL warning (the unmanifested bare skill), just not on the measurement note.
check("--strict fails on a genuine warning (unmanifested)", run(["compile", "--strict"], tmp).status === 1);

// static-tier evals: kitbash test
const testClean = run(["test", "prereview"], tmp);
Expand Down Expand Up @@ -281,7 +285,7 @@ try {
check("no command prints usage, exits 0", help.status === 0 && help.out.includes("Usage: kitbash"), help.out);

const unknown = run(["bogus-command"], neg);
check("unknown command exits 1 with usage", unknown.status === 1 && unknown.out.includes('unknown command "bogus-command"'), unknown.out);
check("unknown command exits 2 with a hint", unknown.status === 2 && unknown.out.includes('unknown command "bogus-command"') && unknown.out.includes("kitbash help"), unknown.out);

const installNoArg = run(["install"], neg);
check("install with no source exits 1 with usage", installNoArg.status === 1 && installNoArg.out.includes("usage: kitbash install"), installNoArg.out);
Expand All @@ -295,13 +299,13 @@ try {
check("install missing local path exits 1 with clear message", missingLocal.status === 1 && missingLocal.out.includes("local path not found"), missingLocal.out);

const testEmpty = run(["test"], neg);
check("test with no skills exits 1", testEmpty.status === 1 && testEmpty.out.includes("no skills installed"), testEmpty.out);
check("test with no skills exits 0 (vacuous pass)", testEmpty.status === 0 && testEmpty.out.includes("nothing to test"), testEmpty.out);

const testMissing = run(["test", "ghost"], neg);
check("test on a non-installed skill exits 1", testMissing.status === 1 && testMissing.out.includes("ghost is not installed"), testMissing.out);

const lintEmpty = run(["lint"], neg);
check("lint with no skills exits 1", lintEmpty.status === 1 && lintEmpty.out.includes("no skills installed"), lintEmpty.out);
check("lint with no skills exits 0 (vacuous pass)", lintEmpty.status === 0 && lintEmpty.out.includes("nothing to lint"), lintEmpty.out);

const lintMissing = run(["lint", "ghost"], neg);
check("lint on a non-installed skill exits 1", lintMissing.status === 1 && lintMissing.out.includes("not found"), lintMissing.out);
Expand Down
24 changes: 14 additions & 10 deletions packages/cli/src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ export interface CompiledFile {

export interface AdapterOutput {
files: CompiledFile[];
/** Actionable problems (degradation, budget) — these fail `compile --strict`. */
warnings: string[];
/** Informational (the measured standing cost) — surfaced, but not a --strict failure. */
notes?: string[];
}

export interface Adapter {
Expand Down Expand Up @@ -63,14 +66,17 @@ function markers(name: string): { begin: string; end: string } {

/**
* A skill authored to lazy-load (disclosure = "lazy") cannot lazy-load on an eager
* target — the whole body sits in context every session. Surface that cost so the
* warning matches reality on ALL eager targets, not just the shared-file ones.
* target — the whole emitted file sits in context every session. This is measured
* from the ACTUAL emitted files (not the pre-wrap body) so the number matches what
* `preview` and the benchmark report for the same skill. It is a NOTE, not a warning:
* it is the product's headline measurement, not a problem, so it never fails --strict.
*/
function eagerStandingWarning(skill: LoadedSkill, adapter: Adapter, body: string): string[] {
function eagerStandingNote(skill: LoadedSkill, adapter: Adapter, files: CompiledFile[]): string[] {
if (adapter.loading !== "eager" || skill.manifest.context.disclosure !== "lazy") return [];
const cost = files.reduce((sum, f) => sum + estimateTokens(f.content), 0);
const { name } = skill.manifest.skill;
return [
`${name} → ${adapter.id}: ${adapter.id} is eager and cannot lazy-load; this skill costs ~${estimateTokens(body)} tokens standing every session (declared limit: ${skill.manifest.context.standing})`,
`${name} → ${adapter.id}: ${adapter.id} is eager and cannot lazy-load, so this skill adds ~${cost} tokens standing every session (a lazy target pays 0; declared limit ${skill.manifest.context.standing})`,
];
}

Expand All @@ -85,8 +91,8 @@ function mergedFileAdapter(id: string, file: string, detect: (root: string) => b
const { name } = skill.manifest.skill;
const { begin, end } = markers(name);
const section = `${begin}\n${header(skill)}\n\n## Skill: ${name}\n\n${body.trim()}\n${end}`;
const warnings = [...degradationWarnings(skill, this), ...eagerStandingWarning(skill, this, body)];
return { files: [{ path: file, content: section, merge: true }], warnings };
const files = [{ path: file, content: section, merge: true }];
return { files, warnings: degradationWarnings(skill, this), notes: eagerStandingNote(skill, this, files) };
},
};
}
Expand All @@ -108,10 +114,8 @@ function fileAdapter(
emit(skill, body, root) {
const { name } = skill.manifest.skill;
const content = `${frontmatter(skill)}${header(skill)}\n\n${body}`;
return {
files: [{ path: pathFor(name, root), content }],
warnings: [...degradationWarnings(skill, this), ...eagerStandingWarning(skill, this, body)],
};
const files = [{ path: pathFor(name, root), content }];
return { files, warnings: degradationWarnings(skill, this), notes: eagerStandingNote(skill, this, files) };
},
};
}
Expand Down
Loading