diff --git a/CHANGELOG.md b/CHANGELOG.md index 2335927..9ee49d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `), 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. diff --git a/README.md b/README.md index a4890ca..65b8b83 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: @@ -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). diff --git a/docs/skills-catalog.md b/docs/skills-catalog.md index 8902d63..30bfb49 100644 --- a/docs/skills-catalog.md +++ b/docs/skills-catalog.md @@ -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: ``` diff --git a/examples/skills/prereview/skill.toml b/examples/skills/prereview/skill.toml index 3ab2728..b5c1ddf 100644 --- a/examples/skills/prereview/skill.toml +++ b/examples/skills/prereview/skill.toml @@ -26,7 +26,7 @@ consumes = ["plan@1"] [targets] requires = [] -mode = "gate" +mode = "skill" [lore] reads = ["conventions", "invariants"] diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 098f3a3..8ba9a20 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "kitbash", - "version": "0.10.0", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kitbash", - "version": "0.10.0", + "version": "0.11.0", "license": "Apache-2.0", "bin": { "kitbash": "dist/index.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index 55fd89c..22fc758 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/scripts/test.mjs b/packages/cli/scripts/test.mjs index cae22d7..53adf9c 100644 --- a/packages/cli/scripts/test.mjs +++ b/packages/cli/scripts/test.mjs @@ -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); @@ -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"); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/packages/cli/src/adapters.ts b/packages/cli/src/adapters.ts index 59f3f7a..f08e457 100644 --- a/packages/cli/src/adapters.ts +++ b/packages/cli/src/adapters.ts @@ -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 { @@ -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})`, ]; } @@ -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) }; }, }; } @@ -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) }; }, }; } diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index b2ab3a5..a0dc15c 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -177,7 +177,7 @@ export async function cmdInstall(args: string[]): Promise { // Review before install (spec §2: permissions are surfaced at install review). const m = skill.manifest; console.log(`review: ${name}@${version} — ${description}`); - console.log(` budget ${m.context.budget} tokens · standing ${m.context.standing} · ${m.context.disclosure} disclosure · mode ${m.targets.mode}`); + console.log(` budget ${m.context.budget} tok · standing ${m.context.standing} tok/session · ${m.context.disclosure} disclosure · mode ${m.targets.mode}`); console.log(` permissions: tools [${m.permissions.tools.join(", ") || "none"}] · network ${m.permissions.network ? "YES" : "no"} · write ${m.permissions.write ? "YES" : "no"}`); if (m.targets.requires.length) console.log(` requires: ${m.targets.requires.join(", ")}`); if (skill.bare) console.log(` ⚠ unmanifested (SKILL.md only) — defaults applied, no permissions or budget declared by the author`); @@ -276,18 +276,20 @@ export async function cmdList(): Promise { for (const s of skills) { const m = s.manifest; const bare = s.bare ? " [unmanifested]" : ""; - console.log(`${m.skill.name}@${m.skill.version} budget=${m.context.budget} standing=${m.context.standing} mode=${m.targets.mode}${bare} — ${m.skill.description}`); + console.log(`${m.skill.name}@${m.skill.version} budget=${m.context.budget}tok standing=${m.context.standing}tok/session mode=${m.targets.mode}${bare} — ${m.skill.description}`); } return 0; } export async function cmdDoctor(): Promise { const root = process.cwd(); - console.log("detected targets:"); + const detected = ADAPTERS.filter((a) => a.detect(root)).length; + console.log(`detected targets (${detected} of ${ADAPTERS.length} in this repo):`); for (const a of ADAPTERS) { const found = a.detect(root); const note = a.id === "agentsmd" ? " (floor: Codex, Gemini CLI, anything reading AGENTS.md)" : ""; - console.log(` ${found ? "✓" : "✗"} ${a.id}${note}`); + // `·` = not present in this repo (not a failure); `✗` is reserved for real problems below. + console.log(` ${found ? "✓" : "·"} ${a.id}${note}`); } const { skills, failures } = loadInstalledSkillsSafe(root); @@ -497,7 +499,8 @@ export async function cmdCompile(args: string[]): Promise { const files = new Map(); const owners = new Map(); // non-merge path → skill that wrote it, for conflict detection - const warnings: string[] = []; + const warnings: string[] = []; // actionable — fail --strict + const notes: string[] = []; // informational (measured standing cost) — never fail --strict // shared marker-merged files (AGENTS.md, GEMINI.md): start from pruned on-disk content const mergedFiles = new Map(); @@ -521,6 +524,7 @@ export async function cmdCompile(args: string[]): Promise { for (const adapter of adapters) { const out = adapter.emit(skill, emitBody, root); warnings.push(...out.warnings); + if (out.notes) notes.push(...out.notes); for (const f of out.files) { if (f.merge) { const current = mergedFiles.get(f.path) ?? pruneSections(readFileIfExists(root, f.path), installedNames); @@ -562,18 +566,38 @@ export async function cmdCompile(args: string[]): Promise { } for (const pruned of pruneStaleOutputs(root, new Set(files.keys()))) console.log(`✂ ${pruned}`); for (const w of warnings) console.log(`⚠ ${w}`); + for (const n of notes) console.log(`ℹ ${n}`); // the measurement — informational, not a failure if (!skills.length) { console.log("no skills installed — kitbash install to add one"); return 0; } - console.log(`compiled ${skills.length} skill(s) for ${adapters.length} agent target(s)`); + console.log(`compiled ${plural(skills.length, "skill")} for ${plural(adapters.length, "target")}`); + // The pitch is "every agent" — a partial fan-out on a fresh repo looks like a shortfall. + if (adapters.length < ADAPTERS.length && !hasExplicitTargets(root)) { + const missing = ADAPTERS.filter((a) => !adapters.includes(a)).map((a) => a.id); + console.log(` ${ADAPTERS.length - adapters.length} more target(s) available — add ${missing.slice(0, 3).join(", ")}${missing.length > 3 ? ", …" : ""} under [project].targets in ${CONFIG}, or create their agent dirs.`); + } if (strict && warnings.length) { - console.error(`--strict: failing on ${warnings.length} warning(s)`); + console.error(`--strict: failing on ${plural(warnings.length, "warning")}`); return 1; } return 0; } +/** English pluralization for count lines — replaces the terse "1 skill(s)". */ +function plural(n: number, word: string): string { + return `${n} ${word}${n === 1 ? "" : "s"}`; +} + +/** True when kitbash.toml explicitly sets [project].targets (so a partial set is intentional). */ +function hasExplicitTargets(root: string): boolean { + const p = join(root, CONFIG); + if (!existsSync(p)) return false; + const raw = parseToml(readFileSync(p, "utf8")); + const project = raw["project"]; + return !!(project && typeof project === "object" && !Array.isArray(project) && Array.isArray((project as Record)["targets"])); +} + /** Static-tier evals (SPEC §6): schema, dead refs, budgets, artifact/trigger shape, injection heuristics. * No eval file required — these always run. Audit/behavioral tiers need a runner (not in v0.3). */ type Check = { name: string; ok: boolean; warn?: boolean; detail?: string }; @@ -840,8 +864,9 @@ export async function cmdTest(args: string[]): Promise { } } if (!skills.length) { - console.error("no skills installed — kitbash install first"); - return 1; + // An empty set vacuously passes — matches compile/list, so CI scripting is consistent. + console.log("no skills installed — nothing to test."); + return 0; } const { failed, warned } = reportChecks(skills); @@ -873,8 +898,9 @@ export async function cmdLint(args: string[]): Promise { } else { skills = loadInstalledSkills(root); if (!skills.length) { - console.error("no skills installed — kitbash install or pass a path: kitbash lint "); - return 1; + // Empty set vacuously passes (matches compile/test); pass a path to lint an uninstalled skill. + console.log("no skills installed — nothing to lint (pass a path to lint an uninstalled skill)."); + return 0; } } @@ -980,6 +1006,9 @@ export async function cmdPreview(args: string[]): Promise { const { name, version } = skill.manifest.skill; console.log(`preview: ${name}@${version}\n`); + // Preview must show what compile emits, incl. the compiled permissions note, + // so its token numbers match `compile` and the benchmark for the same skill. + body = body + permissionsNote(skill.manifest); // Mirror compile: a bad [project].targets is an error, not a silent fall back // to every adapter (which would preview output the repo will never generate). @@ -996,6 +1025,7 @@ export async function cmdPreview(args: string[]): Promise { const standingLabel = adapter.loading === "eager" ? `~${bodyTokens} tok standing` : `lazy (0 tok standing)`; console.log(`─── ${adapter.id} [${adapter.loading}] ${standingLabel} ───`); for (const w of out.warnings) console.log(`⚠ ${w}`); + for (const n of out.notes ?? []) console.log(`ℹ ${n}`); for (const f of out.files) { console.log(`\n → ${f.path}\n`); console.log(f.content); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 95c3813..d17f5ac 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -16,12 +16,15 @@ type Command = { name: string; summary: string; run: (args: string[]) => Promise; + /** Listed for shape, not yet implemented — surfaced separately in help. */ + planned?: boolean; }; -function todo(name: string, milestone: string) { +function todo(name: string) { return async (_args: string[]): Promise => { - console.error(`kitbash ${name}: not implemented yet (lands in ${milestone} — see docs/roadmap.md)`); - return 2; + console.error(`kitbash ${name}: not implemented yet — planned, see the roadmap:`); + console.error(" https://github.com/singhharsh1708/kitbash/blob/main/docs/roadmap.md"); + return 7; // reserved exit code for a recognized-but-unbuilt command }; } @@ -32,45 +35,87 @@ const commands: Command[] = [ { name: "list", summary: "List installed skills with versions and context cost", run: cmdList }, { name: "compile", summary: "Emit native formats for every detected assistant (--strict)", run: cmdCompile }, { name: "doctor", summary: "Detect assistants, report total standing context cost", run: cmdDoctor }, - { name: "update", summary: "Update skills, showing instruction diffs before applying", run: todo("update", "v0.2") }, - { name: "diff", summary: "Instruction/permission/budget diff between two skill versions", run: todo("diff", "v0.2") }, - { name: "lint", summary: "Schema, context budgets, dead references, injection heuristics (--strict; accepts a name, path, or uninstalled source)", run: cmdLint }, - { name: "audit", summary: "Scan installed skills: permission drift, unsigned sources, injection heuristics", run: todo("audit", "v0.2") }, - { name: "preview", summary: "Render each adapter's output with per-agent token counts — works on uninstalled sources (gh:owner/repo)", run: cmdPreview }, - { name: "explain", summary: "Why a compilation degraded on a given adapter (accepts a name, path, or uninstalled source)", run: cmdExplain }, - { name: "test", summary: "Run a skill's static evals: schema, budgets, dead refs, injection heuristics (--strict)", run: cmdTest }, - { name: "gate", summary: "Run a gate-mode skill with a deterministic exit code", run: todo("gate", "v0.3") }, - { name: "search", summary: "Search the community index", run: todo("search", "v0.4") }, - { name: "publish", summary: "Validate and publish a skill to the index", run: todo("publish", "v0.4") }, - { name: "lore", summary: "Build, query, and curate repo intelligence", run: todo("lore", "v0.5") }, - { name: "run", summary: "Run a declared pipeline (e.g. kitbash run ship)", run: todo("run", "v0.5") }, + { name: "lint", summary: "Static checks: schema, budgets, dead refs, safety lints (--strict; name, path, or uninstalled source)", run: cmdLint }, + { name: "preview", summary: "Render each target's output with per-agent token counts — works on uninstalled sources", run: cmdPreview }, + { name: "explain", summary: "Why a compilation degraded on a given target (name, path, or uninstalled source)", run: cmdExplain }, + { name: "test", summary: "Run a skill's static evals (schema, budgets, dead refs, safety lints; --strict)", run: cmdTest }, + { name: "update", summary: "Update skills, showing instruction diffs before applying", run: todo("update"), planned: true }, + { name: "diff", summary: "Instruction/permission/budget diff between two skill versions", run: todo("diff"), planned: true }, + { name: "audit", summary: "Scan installed skills: permission drift, unsigned sources", run: todo("audit"), planned: true }, + { name: "gate", summary: "Run a gate-mode skill with a deterministic exit code", run: todo("gate"), planned: true }, + { name: "search", summary: "Search the community index", run: todo("search"), planned: true }, + { name: "publish", summary: "Validate and publish a skill to the index", run: todo("publish"), planned: true }, + { name: "lore", summary: "Build, query, and curate repo intelligence", run: todo("lore"), planned: true }, + { name: "run", summary: "Run a declared pipeline (e.g. kitbash run ship)", run: todo("run"), planned: true }, ]; -function usage(): void { - console.log(`kitbash ${VERSION} — write a skill once, run it in every coding agent\n`); - console.log("Usage: kitbash [args]\n"); +const HELP_FOOTER = "\nGlobal: kitbash --version · kitbash --help · kitbash help \nDocs: https://kitbash.vercel.app/docs"; + +function usage(out: (s: string) => void = console.log): void { + out(`kitbash ${VERSION} — write a skill once, run it in every coding agent\n`); + out("Usage: kitbash [args]\n"); + const working = commands.filter((c) => !c.planned); + const planned = commands.filter((c) => c.planned); const pad = Math.max(...commands.map((c) => c.name.length)); + for (const c of working) out(` ${c.name.padEnd(pad)} ${c.summary}`); + if (planned.length) { + out("\nPlanned (not yet implemented):"); + for (const c of planned) out(` ${c.name.padEnd(pad)} ${c.summary}`); + } + out(HELP_FOOTER); +} + +/** Nearest command name within edit distance 2, for did-you-mean. */ +function nearestCommand(input: string): string | undefined { + const dist = (a: string, b: string): number => { + const d = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]); + for (let j = 0; j <= b.length; j++) d[0]![j] = j; + for (let i = 1; i <= a.length; i++) + for (let j = 1; j <= b.length; j++) + d[i]![j] = Math.min(d[i - 1]![j]! + 1, d[i]![j - 1]! + 1, d[i - 1]![j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1)); + return d[a.length]![b.length]!; + }; + let best: { name: string; d: number } | undefined; for (const c of commands) { - console.log(` ${c.name.padEnd(pad)} ${c.summary}`); + const d = dist(input, c.name); + if (!best || d < best.d) best = { name: c.name, d }; } - console.log("\nDocs: https://github.com/singhharsh1708/kitbash"); + return best && best.d <= 2 ? best.name : undefined; } async function main(): Promise { const [cmd, ...args] = process.argv.slice(2); if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") { + // `kitbash help ` prints that command's summary. + const topic = cmd === "help" ? args[0] : undefined; + if (topic) { + const c = commands.find((x) => x.name === topic); + if (c) { + console.log(`kitbash ${c.name}${c.planned ? " (planned — not yet implemented)" : ""}\n ${c.summary}`); + return 0; + } + console.error(`kitbash: no such command "${topic}"`); + return 2; + } usage(); return 0; } - if (cmd === "--version" || cmd === "-v") { + if (cmd === "--version") { console.log(VERSION); return 0; } const command = commands.find((c) => c.name === cmd); if (!command) { - console.error(`kitbash: unknown command "${cmd}"\n`); - usage(); - return 1; + console.error(`kitbash: unknown command "${cmd}"`); + const near = nearestCommand(cmd); + if (near) console.error(` did you mean "${near}"?`); + console.error(" run 'kitbash help' to see all commands."); + return 2; + } + // Per-command help: `kitbash install --help`. + if (args.includes("--help") || args.includes("-h")) { + console.log(`kitbash ${command.name}${command.planned ? " (planned — not yet implemented)" : ""}\n ${command.summary}`); + return 0; } return command.run(args); } diff --git a/rfcs/0003-implementation-roadmap.md b/rfcs/0003-implementation-roadmap.md new file mode 100644 index 0000000..da5e93f --- /dev/null +++ b/rfcs/0003-implementation-roadmap.md @@ -0,0 +1,372 @@ +# RFC 0003 — implementation roadmap + +Companion to [RFC 0003](0003-v2-architecture.md). Assumes the RFC is accepted. This +is the PR-by-PR plan. Every PR leaves the repo working and CI green; no PR requires +rewriting an earlier one; each is independently mergeable. + +Ground truth (current tree): `commands.ts` 1093 · `ksf.ts` 305 · `adapters.ts` 285 · +`toml.ts` 185 · `lock.ts` 102 · `index.ts` 84 · `test.mjs` 906 · `benchmark.mjs` 163. +No `exports` in `package.json` (bin only). One e2e test. Regex-based lints. Opaque +string body flow. + +Two invariants govern ordering: +- **Freeze before build.** Any contract that a later PR or an external user depends + on (lock format, exit codes, `--json` envelope, adapter interface, tokenizer label) + is *reserved/frozen in Phase 0*, before the machinery exists, so no later PR breaks it. +- **Refactor before behavior.** The IR and render-layer refactors are behavior- + preserving and snapshot-guarded first; the one PR that changes output (native + permissions) lands only after the snapshot net exists. + +--- + +# Part 1 — dependency graph + +``` + ┌────────────────── PHASE 0 (freeze-or-never) ──────────────────┐ + │ │ + P0.1 lock version+ P0.2 edition P0.3 tokenizer P0.4 authors P0.5 delete + reserved fields marker honest label field fix publish/search + │ │ │ │ + deps-reserved + │ │ │ │ │ + │ │ │ │ P0.6 exit-code + │ │ │ │ enum (SPEC+wire) + │ │ │ │ │ + └───────────┬───────┴───────────────────┴────────┬───────────┴──────────────────┘ + │ │ + ▼ ▼ + ┌──────────────────────── PHASE 1 (make the wedge true) ────────────────────────┐ + │ │ + │ P1.1 extract shared render layer ──▶ P1.2 SkillIR type + buildIR │ + │ (behavior-preserving) │ │ + │ ▼ │ + │ P1.3 adapters → lower(IR) │ + │ (behavior-preserving, snapshot) │ + │ │ │ + │ P1.5 pass pipeline skeleton ◀───────┤ │ + │ │ ▼ │ + │ └──────────────▶ P1.4 inject-permissions pass + │ + │ native allowed-tools (FIRST output │ + │ change — needs snapshot net first) │ + │ │ + │ P1.7 conformance corpus + golden runner ──(guards P1.1–P1.4, prereq for)──┐ │ + │ + toml-test + fuzzer │ │ + │ ▼ │ + │ P0.3 label ──▶ P1.6 real BPE tokenizer (changes all numbers) ────▶ P1.8 earn│ + │ (independent of IR; needs corpus to guard) scripts │ + └────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────────────── PHASE 2 (pkg-mgr + trust) ─────────────────────────┐ + │ P2.1 resolved-commit pin + .git strip ──▶ P2.2 restore-from-lock, │ + │ (uses P0.1 reserved fields) compile --check │ + │ P2.3 @kitbash/core library split ──▶ P2.4 --json + diagnostics API │ + │ (splits commands.ts; uses P0.6 exit codes) │ + │ P2.5 signing/provenance + audit (uses P0.1 signature field, P2.1 commit) │ + │ P2.6 incremental+parallel cache (needs pure adapters from P1.3) │ + └────────────────────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌──────────────── PHASE 3 (ecosystem — post-adoption) ───────────────────────┐ + │ P3.1 third-party adapters (needs P1.3 lower + P0.1 pin + P2.5 verify) │ + │ P3.2 third-party passes (needs P1.5) P3.3 adapter certification (P1.7) │ + │ P3.4 source-as-identity + resolver + update/drift (needs P0.7 reserve, │ + │ P2.1 commit pin, P0.6 codes) P3.5 adapter registry (P3.1) │ + └────────────────────────────────────────────────────────────────────────────┘ +``` + +**Critical path** (the chain that gates everything): `P0.1/P0.6 → P1.1 → P1.2 → P1.3 +→ P1.4`, with `P1.7` (snapshots) landing beside P1.1 so P1.4's output change is safe. +Tokenizer (`P0.3 → P1.6`) is a parallel track. Phase 2/3 hang off Phase 0's frozen +contracts and Phase 1's pure adapters. + +--- + +# Part 2 — milestones + +## Milestone 1 — Freeze the contracts (Phase 0) +All small, mostly parallel, ship before any `1.0` tag. These paint no corners and +each is a clean first-or-second PR. + +### PR #1 — Lockfile version header + reserved fields +- **Motivation:** the lock format must be able to carry `resolved` (commit), + `signature`, `outputHash` later without a breaking on-disk change; reserve them and + a `version = N` header now. +- **Files:** `lock.ts` (`readLock`/`writeLock`/`LockEntry`), `test.mjs` (+lock-format + assertions), SPEC §8. +- **LOC:** ~60. **Review:** easy. **Risk:** low — old lockfiles (no version) read as + v0 and migrate on next write; forward-only. +- **Acceptance:** an old lock (no header) still loads; new writes carry `version = 1`; + unknown future fields are preserved on rewrite; drift detection unchanged. + +### PR #2 — Optional `edition` marker in `[skill]` +- **Motivation:** freeze-or-never opt-in to stricter validation without breaking + additive-only forward-compat. +- **Files:** `spec/schema/skill.schema.json`, `ksf.ts` (`validate`, `opt`), `SPEC.md`, + `test.mjs`. +- **LOC:** ~40. **Review:** easy. **Risk:** low — additive optional field, no behavior + until an edition is defined. +- **Acceptance:** `edition = "2026"` parses and round-trips; absent = current behavior; + an unknown edition value errors with a clear message. + +### PR #3 — Honest tokenizer label +- **Motivation:** stop implying per-target *tokenization*; stamp `estimate-v1` + everywhere a token number appears, so P1.6 is a visible, versioned upgrade. +- **Files:** `ksf.ts` (`estimateTokens` doc + an exported `TOKENIZER_ID`), + `adapters.ts` (warning text), `commands.ts` (doctor/compile/preview labels), + `benchmark.mjs`, `docs/benchmarks/README.md` (regen), site benchmark/docs prose. +- **LOC:** ~50 code + doc regen. **Review:** easy. **Risk:** low — labels only, no + number changes. +- **Acceptance:** every surface printing a token count also prints/records + `estimate-v1`; benchmark determinism gate still green. + +### PR #4 — Fix the dropped `authors` field +- **Motivation:** `authors` is schema'd + frozen but the loader discards it + (gap-hunt #27); surface it at install review. +- **Files:** `ksf.ts` (`SkillManifest`, `validate`), `commands.ts` (review block), + `test.mjs`. +- **LOC:** ~40. **Review:** easy. **Risk:** low. +- **Acceptance:** `authors` loads, appears in the review block, survives round-trip. + +### PR #5 — Retire conceded surface (`publish`/`search` + index source) and reserve `[dependencies]` +- **Motivation:** stop advertising abandoned scope and stop silently dropping declared + deps; freeze the command surface honestly. +- **Files:** `index.ts` (remove `publish`/`search` from the array), `commands.ts` + (`normalizeSource` — remove any index short-name path; add a loud WARN when + `[dependencies]` is non-empty at install), `SPEC.md` (mark `[dependencies]` + reserved), `docs/roadmap.md`, `test.mjs`. +- **LOC:** ~60. **Review:** easy–medium (surface change). **Risk:** low — removing + stubs no one scripts against; the deps WARN replaces a silent drop. +- **Acceptance:** `kitbash publish`/`search` no longer listed; a skill with a + `[dependencies]` entry installs with a visible WARN, not silent loss; help text and + spec agree. + +### PR #6 — Exit-code contract +- **Motivation:** stop double-using `2`; document a stable enum other tools script on. +- **Files:** `commands.ts` (all `return 1/2` sites → named codes), `index.ts` + (dispatch), `SPEC.md` (the table), `test.mjs` (assert codes per path). +- **LOC:** ~90. **Review:** medium (touches many return sites). **Risk:** medium — a + wrong code is a contract bug; mitigated by per-path tests. +- **Acceptance:** policy-block=3, safety-lint=4, integrity-drift=5, fetch=6, + not-implemented=7, usage=2, findings=1; documented; tested per path. + +## Milestone 2 — Snapshot net + render extraction (Phase 1 prep) + +### PR #7 — Conformance corpus + golden runner + toml-test + fuzzer +- **Motivation:** the safety net every later refactor needs, and the artifact that + makes KSF a standard. +- **New files:** `conformance/` (manifest/compile/measure fixtures, `checklist.json`, + `VERSION`), `packages/cli/scripts/conformance.mjs` (runner), `scripts/fuzz-toml.mjs`. +- **Files:** `package.json` (test scripts), CI (`ci.yml` add corpus + toml-test + + fuzz steps). +- **LOC:** ~400 (mostly data). **Review:** medium. **Risk:** low — additive; catches + everything downstream. +- **Acceptance:** corpus runs in CI; a deliberately-broken adapter output fails a + golden; `toml-test` valid/invalid corpus wired; fuzzer runs a bounded budget green. + +### PR #8 — Extract the shared render layer +- **Motivation:** nine hand-concatenations of frontmatter+header+body become one + renderer, so a template change can't silently drift and the IR migration has a seam. +- **Files:** `adapters.ts` (`fileAdapter`/`claudeCode`/`mergedFileAdapter`/`skillDir` + all call a new `renderFile(plan)`), new `render.ts`. Behavior-preserving. +- **LOC:** ~120 (net small; mostly moved). **Review:** medium. **Risk:** medium — + must be byte-identical output; **guarded entirely by PR #7 goldens.** +- **Acceptance:** all adapter goldens byte-identical before/after; no benchmark drift. + +## Milestone 3 — The IR (Phase 1 core) + +### PR #9 — Introduce `SkillIR` + `buildIR` (behavior-preserving) +- **Motivation:** the load-bearing v2 change; add the IR but adapters still read + `ir.body` as today's string, so output is unchanged. +- **New files:** `ir.ts` (`SkillIR`, `BodyBlock`, `NativeSlots`, `SkillRef`, + `buildIR`). **Files:** `commands.ts` (`cmdCompile` builds IR, passes it where it + passed `emitBody`), `adapters.ts` (`emit` signature accepts IR, reads `ir.body`). +- **LOC:** ~200. **Review:** hard (the contract). **Risk:** medium — guarded by + goldens; output must not change. +- **Acceptance:** goldens byte-identical; IR is serializable + content-hashable; the + `permissionsNote` prose still appears (moved into buildIR's body, not the driver). + +### PR #10 — Adapters consume slots; native `allowed-tools` on claude-code +- **Motivation:** the first *real* wedge win — claude-code lowers permissions to + native frontmatter instead of losing them to prose; triggers read from IR by every + adapter that supports them. +- **Files:** `adapters.ts` (`claudeCode.lower` emits `allowed-tools`; a `toClaudeTool + Grammar` mapper; shared prose fallback for the rest), `render.ts`, goldens (updated + intentionally), `benchmark.mjs`/README (regen — numbers move), site docs. +- **LOC:** ~160. **Review:** hard. **Risk:** **high — the first intentional output + change.** Mitigated: goldens updated in the same PR with the diff visible; the KSF + tool grammar → Claude grammar mapping is the subtle part. +- **Acceptance:** claude-code output carries a correct `allowed-tools:`; other targets + keep the prose note; degradation/explain unaffected; goldens + benchmark regenerated + and reviewed. + +### PR #11 — Pass pipeline skeleton +- **Motivation:** formalize validate→transform ordering; move the existing lints, + budget-check, and permission-injection into named passes so third-party passes have + a slot later. +- **New files:** `passes.ts` (`Pass`, the fixed core passes, the runner). **Files:** + `commands.ts` (`staticChecks`/`cmdCompile` call the pipeline), `ir.ts`. +- **LOC:** ~180. **Review:** hard. **Risk:** medium — pure reorganization of existing + logic; goldens + the full lint test-suite guard it. +- **Acceptance:** identical diagnostics and output vs before; the pipeline order is + deterministic and documented; a no-op third-party pass hook exists but is unused. + +## Milestone 4 — Exact measurement + capabilities (Phase 1 finish) + +### PR #12 — Real per-family BPE tokenizer +- **Motivation:** make the one uncontested differentiator true. +- **New files:** `tokenizer.ts` + vendored BPE table data (as data files, still + zero *runtime dependency*), `TokenizerId` registry. **Files:** `ksf.ts` + (`estimateTokens` → `countTokens(text, tokenizerId)`), `adapters.ts` (each declares + `tokenizer`), `commands.ts`/`benchmark.mjs` (use it), README/site (regen — all + numbers change), the `measure/` conformance fixtures. +- **LOC:** ~300 code + vendored tables. **Review:** hard. **Risk:** high — every + number moves; the honest label from PR #3 makes it a versioned event, not a + surprise. Independent of the IR track, so it can land in parallel after PR #7. +- **Acceptance:** counts match reference tokenizers within tolerance on the `measure/` + fixtures; `tokenizer@rev` stamped; benchmark regenerated. + +### PR #13 — Earn the `scripts` capability +- **Motivation:** close the degenerate `capabilities: []` matrix by *implementation*, + not by filling strings. +- **Files:** `adapters.ts` (skill-dir adapters copy `scripts/` into their output; + declare `capabilities: ["scripts"]`; a shared `CAPABILITIES` vocab imported by + `ksf.ts` + `adapters.ts` with a compile-time claimed-vs-delivered assertion), + `commands.ts` (emit wiring), goldens, `test.mjs`. +- **LOC:** ~140. **Review:** medium. **Risk:** medium — changes degradation output for + scripts-requiring skills; goldens guard it. +- **Acceptance:** a skill with `scripts/` + `requires=[scripts]` compiles the scripts + and reports NOT degraded on skill-dir targets, still degraded on aider/agentsmd; the + claimed-vs-delivered test passes. + +## Milestone 5 — Reproducibility + machine interface (Phase 2, after adoption signal) + +### PR #14 — Resolved-commit pin + `.git` strip +- **Files:** `commands.ts` (`fetchSource` runs `git rev-parse`, threads SHA), `lock.ts` + (`resolved` field — reserved in PR #1), integrity over stripped bytes, `test.mjs`. +- **LOC:** ~90. **Review:** medium. **Risk:** medium — integrity hash of the stripped + tree differs from today's; a one-time re-pin. Acceptance: lock carries the full SHA; + `.git` excluded from the hash; reinstall of the same SHA is byte-identical. + +### PR #15 — Restore-from-lock + `compile --check` +- **Files:** `commands.ts` (`cmdInstall` with no arg restores every locked skill; + `cmdCompile --check` fails if output would change), `index.ts`, `test.mjs`. +- **LOC:** ~120. **Review:** medium. **Risk:** low. Acceptance: a fresh clone with + only `kitbash.lock` restores all skills at their pinned SHAs; `--check` is CI-usable. + +### PR #16 — `@kitbash/core` library boundary +- **Motivation:** split the 1093-line `commands.ts` into a pure `core` (compile/ + validate/measure/lint returning data) + a thin CLI shell; add `package.json` + `exports`. +- **New files:** `core/` (moved logic), `cli.ts` (renderer). **Files:** `commands.ts` + shrinks to CLI glue, `index.ts`, `package.json` (`exports`, `types`). +- **LOC:** ~250 moved, ~80 new. **Review:** hard. **Risk:** medium — large move; + goldens + e2e guard behavior; done as pure relocation, no logic change. +- **Acceptance:** `import { compile } from "@kitbash/core"` returns a `CompileResult`; + the CLI behaves identically; e2e green. + +### PR #17 — `--json` + diagnostics API +- **Files:** `cli.ts`/`core` (envelope + `Diagnostic` codes), every command, `SPEC.md` + (envelope schema, diagnostic-code table), `test.mjs`. +- **LOC:** ~200. **Review:** medium. **Risk:** low — additive flag. Acceptance: + `--json` emits the versioned envelope with per-target cost + coded diagnostics; prose + mode unchanged; envelope schema documented. + +### PR #18 — Incremental + parallel compile (content-addressed cache) +- **Files:** `commands.ts`/`core` (cache keyed on IR-hash+tokenizer-rev+adapter-ver), + new `cache.ts`, `test.mjs` (cold==warm byte-equality). Needs pure adapters (PR #9/10). +- **LOC:** ~180. **Review:** hard. **Risk:** medium — a cache bug = wrong output; + the cold-vs-warm determinism test is the guard. Acceptance: warm cache skips + unchanged (skill,adapter) pairs; cold and warm output byte-identical. + +## Milestone 6 — Signing + ecosystem (Phase 2/3, post-adoption) + +### PR #19 — Signing/provenance + `audit` +- **Files:** `lock.ts` (`signature` — reserved PR #1), `commands.ts` (verify at + install, `[policy].trusted_signers`, TOFU, build `cmdAudit`), `SPEC.md`, `test.mjs`. +- **LOC:** ~220. **Review:** hard. **Risk:** high (a trust primitive). Acceptance: + signed skills verify against a trust root; unsigned fall back to TOFU with a + first-seen record; `audit` re-verifies integrity + signature + re-runs safety lints. + +### PR #20–#24 — Third-party adapters, passes, certification, source-as-identity + resolver + `update`, adapter registry +- Each hangs off the frozen Phase 0/1 contracts (see graph). Deferred until adoption; + scoped when real. Effort L each. + +--- + +# Part 3 — contributor suitability + +**Good first PRs** (self-contained, well-fenced by tests, no architectural context): +- PR #2 (edition marker), #3 (tokenizer label), #4 (authors field), #5 (retire stubs). + Each is <60 LOC, additive, with an obvious acceptance test. +- Parts of #7 (writing conformance *fixtures* — pure data — is ideal onboarding work). + +**Requires deep architectural knowledge** (touch the IR/pipeline/contract): +- PR #9 (SkillIR), #10 (native lowering + tool-grammar mapping), #11 (pass pipeline), + #12 (tokenizer correctness), #16 (core split), #18 (cache correctness), #19 (trust). + These need the maintainer or a trusted contributor; a wrong contract here is + expensive to unwind. + +**Middle tier** (mechanical but wide): #6 (exit codes), #8 (render extraction), #13 +(scripts capability), #14/#15/#17. + +--- + +# Part 4 — estimates + +- **Total implementation time** (one experienced maintainer, part-time): + - Milestone 1 (freeze): ~1–2 weeks. **This is the only urgent block at 0 users.** + - Milestones 2–4 (IR + measurement + capabilities — Phase 1): ~6–10 weeks. + - Milestones 5–6 (Phase 2/3): months, and **should not start until adoption + justifies it.** + - To a demonstrable v2 wedge (Milestones 1–4): ~2–3 months of focused work. +- **Biggest technical risks:** + 1. PR #10 — the KSF tool-grammar → Claude `allowed-tools` mapping (semantic, subtle, + the first output change). Mitigation: goldens + a dedicated grammar-mapping unit + test corpus. + 2. PR #12 — tokenizer correctness and keeping it zero-*runtime*-dependency while + vendoring large BPE tables. Mitigation: tables as data, reference-count fixtures. + 3. PR #16 — the `core` split is a big move that could regress behavior. Mitigation: + pure relocation, goldens + e2e as the net, no logic edits in the same PR. +- **Highest-risk migration:** PR #12 (real tokenizer) — it changes *every published + number* at once. It is only safe because PR #3 already turned the number into a + labeled, versioned quantity, so the change is a `estimate-v1 → cl100k@rev` changelog + event, not a silent shift. Ship it in its own release with the benchmark diff called + out. + +--- + +# Part 5 — exact implementation order + +No later PR rewrites an earlier one if built in this order. Within a milestone, PRs +without an arrow between them in Part 1 may land in any order or in parallel. + +``` + 1. PR #1 lock version + reserved fields ─┐ freeze the on-disk + CLI + 2. PR #6 exit-code contract │ contracts FIRST, so #14/#15/ + 3. PR #3 tokenizer honest label │ #16/#17/#19 build on frozen shapes + 4. PR #2 edition marker │ (2,3,4,5 are parallel-safe) + 5. PR #4 authors field │ + 6. PR #5 retire stubs + reserve deps ─┘ + ── ship: this is the 1.0-freeze release ───────────────────────────────────────── + 7. PR #7 conformance corpus + goldens + fuzz (the safety net — before any refactor) + 8. PR #8 extract shared render layer (guarded by #7; seam for the IR) + 9. PR #9 SkillIR + buildIR (no output change) (needs #8's seam) +10. PR #10 native allowed-tools (FIRST change) (needs #9 slots + #7 goldens) +11. PR #11 pass pipeline skeleton (needs #9; folds in existing lints) +12. PR #12 real BPE tokenizer (parallel track: needs only #3 + #7) +13. PR #13 earn scripts capability (needs #9/#10 lowering + #7) + ── ship: this is the "wedge is real and honest" release ────────────────────────── +14. PR #14 resolved-commit pin + .git strip (needs #1) +15. PR #15 restore-from-lock + compile --check (needs #14) +16. PR #16 @kitbash/core split (needs stable pipeline: after #11) +17. PR #17 --json + diagnostics (needs #16 + #6) +18. PR #18 incremental+parallel cache (needs pure adapters #9/#10) +19. PR #19 signing/provenance + audit (needs #1 signature + #14 commit) +20. PR #20+ ecosystem (adapters/passes/registry/resolver/update) — post-adoption +``` + +**The line to remember:** everything through PR #6 is *freeze-or-never* and cheap — +do it now. Everything #7–#13 makes the compiler wedge true and honest — do it to earn +users. Everything #14+ is package-manager and ecosystem weight — do it only once users +exist. The order above guarantees each contract is frozen before anything depends on +it, and each refactor is snapshot-guarded before the one PR that changes output. diff --git a/rfcs/0003-v2-architecture.md b/rfcs/0003-v2-architecture.md new file mode 100644 index 0000000..b81af6f --- /dev/null +++ b/rfcs/0003-v2-architecture.md @@ -0,0 +1,624 @@ +# RFC 0003 — Kitbash v2 architecture + +- Status: draft +- Author(s): Kitbash maintainers +- Depends on: [RFC 0001](0001-ksf.md), [RFC 0002](0002-ksf-1.0-stabilization.md) + +## Summary + +Kitbash today is a frontend that produces one opaque markdown string and N backends +that string-template it. That shape cannot support native per-target lowering, +exact measurement, third-party adapters, compiler passes, or incremental builds — +the things a standard build system needs. This RFC designs the v2 architecture that +adds the missing middle (a small IR + a pass pipeline) without betraying the four +things that make Kitbash worth using: **zero runtime dependencies, compile-to-native, +deterministic builds, install-time trust.** KSF stays backward compatible; the change +is internal. + +The guiding rule is the review's, made concrete: **every subsystem must justify its +existence, and the IR is minimal — a structured skill document, not a markdown AST.** + +--- + +## 1. Architecture + +``` + kitbash v2 — data flow + + skill.toml ─┐ + ├─▶ [1 PARSE] ─────▶ RawSkill ──▶ [2 VALIDATE] ──▶ ValidatedSkill + SKILL.md ───┘ toml.ts, (manifest schema+bounds (typed manifest + scripts/* frontmatter + raw body + SAFETY lints + raw body, + evals/* splitter) + files) = a validate pass no diagnostics + │ left unhandled) + ▼ + ┌──────────────────┐ + │ [3 BUILD IR] │ resolve templates, + │ ksf → SkillIR │ extract standing stub, + │ │ identify sections, + │ │ native slots, refs + └────────┬─────────┘ + │ SkillIR (see §3) + ▼ + ┌─────────────────────────────────┐ + │ [4 PASS PIPELINE] │ + │ ordered, some third-party: │ + │ validate → transform → (fixed) │ + │ • canonicalize │ + │ • resolve-refs │ + │ • budget-check (validate) │ + │ • inject-permissions (into IR, │ + │ NOT prose in the driver) │ + │ • │ + └───────────────┬─────────────────┘ + │ lowered SkillIR + ┌────────────────────────────┼────────────────────────────┐ + ▼ ▼ ▼ + [5 LOWER + EMIT] [5 LOWER + EMIT] [5 LOWER + EMIT] + adapter A.lower(IR) adapter B.lower(IR) adapter …N + → RenderPlan → RenderPlan → RenderPlan + → CompiledFile[] → CompiledFile[] → CompiledFile[] + (claude-code: native (agentsmd: prose (third-party npm + allowed-tools frontmatter) fallback for perms) adapter, pinned) + │ │ │ + └──────────────┬──────────────┴──────────────┬──────────────┘ + ▼ ▼ + [6 MEASURE] ┌──────────────┐ + tokenizer per adapter ◀──────│ TOKENIZER │ vendored BPE + (exact, family BPE) │ registry │ tables as data + │ │ tiktoken/SP │ (still zero-dep) + ▼ └──────────────┘ + [7 RECONCILE + WRITE] (the driver — core, not extensible) + • marker-merge shared files ┌──────────────┐ + • conflict detection │ CACHE │ content-addressed + • stale-output prune ◀───│ .kitbash/ │ (IR hash, tokenizer + • write CompiledFile[] │ cache/ │ rev, adapter ver) + • update kitbash.lock └──────────────┘ → incremental skip + │ + ▼ + native outputs + kitbash.lock + measurements + + ───────────────────────────────────────────────────────────────────────────────── + TRUST PIPELINE (cross-cutting, runs at fetch+install; a set of VALIDATE passes): + fetchSource ─▶ resolve commit (SHA) ─▶ strip .git ─▶ integrity hash + ─▶ safety lints (visible-text, dynamic-context, remote-exec, secrets) [HARD] + ─▶ behavioral heuristics [WARN] ─▶ [policy] gate [HARD] + ─▶ (optional) signature/attestation verify against trust root + ─▶ review block (manifest + resolved body) ─▶ y/N ─▶ pin in lock +``` + +Everything left of the pass pipeline is the **frontend** (parse/validate/IR). The +pass pipeline is the **middle** (the new part). Lowering+emit is the **backend**. +The cache and tokenizer are services the pipeline calls. The trust pipeline is a +cross-cutting set of validate passes gated at fetch/install, not compile. + +--- + +## 2. Compiler pipeline + +Eight stages. Each names its inputs, outputs, invariants, and where a third party +may extend it. **Only the pass pipeline (stage 4) and adapter set (stage 5) are +extensible; everything else is core and fixed.** + +### Stage 1 — Parse +- **In:** a skill directory (`skill.toml`, `SKILL.md`, `scripts/`, `evals/`). +- **Out:** `RawSkill { manifestTable, rawBody, frontmatter, files[] }`. +- **Invariants:** total function or a typed parse error; never partial. Parses + untrusted remote input, so it is a security boundary (see §9 of the review — the + TOML parser is named/fuzzed). +- **Extension:** none. The format grammar is fixed. + +### Stage 2 — Validate +- **In:** `RawSkill`. +- **Out:** `ValidatedSkill` or `Diagnostic[]` with a failure. Runs the schema, the + numeric/enum/type bounds, and the **safety lints as validate passes** (the trust + gate lives here, not bolted onto install). +- **Invariants:** if it returns a `ValidatedSkill`, every hard rule held; no silent + coercion (the 0.9.0 fix, formalized). Warn-level diagnostics carry forward. +- **Extension:** third-party **validate passes** may ADD diagnostics; they may never + remove or downgrade a core diagnostic (§5). + +### Stage 3 — Build IR +- **In:** `ValidatedSkill`. +- **Out:** `SkillIR` (§3): typed manifest carried intact + a light body model + (stub, sections, native slots, refs), with templates resolved. +- **Invariants:** template resolution is total (an unresolved `{{...}}` is a hard + diagnostic, per spec §3); the IR is serializable and content-hashable (feeds the + cache); building the IR does no target-specific work. +- **Extension:** none. The IR shape is a frozen contract (adapters and passes depend + on it). + +### Stage 4 — Pass pipeline +- **In:** `SkillIR`. +- **Out:** a transformed `SkillIR`. +- **Invariants:** passes are ordered deterministically; a pass is a pure function + `(SkillIR, PassCtx) → SkillIR`; validate passes run before transform passes; the + fixed core passes (canonicalize, resolve-refs, budget-check, inject-permissions) + always run and cannot be removed. **Permissions are injected into the IR here as + structured data, so each adapter can lower them natively or to prose — the driver + no longer flattens them to English before adapters run.** +- **Extension:** `[project].passes` names transform/validate passes (pinned npm + packages). Lowering is NOT a pass — it is adapter-owned (§5). + +### Stage 5 — Lower + Emit (per adapter) +- **In:** the lowered `SkillIR`, a `LowerCtx` (target root, active config). +- **Out:** `CompiledFile[]` per adapter, plus per-file `RenderPlan` metadata. +- **Invariants:** `adapter.lower` is pure (no IO, network, clock, or randomness); + it returns a plan, the core renders bytes (one YAML/marker/header renderer, not + nine hand-concatenations); output is byte-stable across runs and machines. +- **Extension:** the adapter set (§4) — first-party in-tree + third-party pinned. + +### Stage 6 — Measure +- **In:** each adapter's emitted content + its declared tokenizer id. +- **Out:** exact per-target loaded/standing token counts, stamped with + `tokenizer@rev`. +- **Invariants:** counts are reproducible for a given tokenizer revision; a + tokenizer upgrade is a versioned, changelog-visible event, never silent drift. +- **Extension:** the tokenizer registry (new BPE families as vendored data). + +### Stage 7 — Reconcile + Write +- **In:** all adapters' `CompiledFile[]`. +- **Out:** files on disk, updated `kitbash.lock`, stale outputs pruned. +- **Invariants:** marker-merge preserves user content; only kitbash-marked sections + are touched; prune removes only generated files, never colocated user files (the + 0.9.0 fix); conflict on two skills writing one path is surfaced. +- **Extension:** none. This is core correctness. + +### Stage 8 — Cache (incremental + parallel) +- **In/Out:** wraps stages 3–6. Key = hash of (SkillIR, tokenizer rev, adapter + version, pass set). A cache hit skips lower+emit+measure for that (skill, adapter) + pair. +- **Invariants:** cache is a pure optimization; a cold cache and a warm cache + produce byte-identical output (this is the determinism test). Adapters being pure + is what makes both caching and parallelism safe — the pipeline can lower every + (skill × adapter) pair concurrently with no shared state. +- **Extension:** none. + +--- + +## 3. Intermediate Representation + +**Yes, Kitbash needs an IR — but a small one.** The failure mode to avoid is a full +markdown AST: that over-couples to markdown internals, bloats the frozen contract, +and buys nothing, because adapters render *markdown-ish* text anyway. The IR exists +for exactly one reason: **to let each backend lower the cross-cutting concerns +(permissions, triggers, description, references) natively instead of receiving a +pre-flattened English string.** It is a structured skill document, not a syntax tree. + +```ts +interface SkillIR { + manifest: SkillManifest; // the already-typed manifest, carried intact + stub: string; // the standing stub (first real paragraph) + body: BodyBlock[]; // ordered blocks; prose stays opaque text + slots: NativeSlots; // structured data backends may render natively + refs: SkillRef[]; // scripts/, artifacts, prompts the body points at + provenance: { source: string; resolved: string; tokenizerHints: string[] }; +} + +type BodyBlock = + | { kind: "prose"; text: string } // opaque — not parsed into an AST + | { kind: "code"; lang: string; text: string } + | { kind: "heading"; level: number; text: string }; + +interface NativeSlots { // the reason the IR exists + description: string; + permissions: { tools: string[]; network: boolean; write: boolean }; + triggers: { commands: string[]; auto: string[]; events: string[] }; + disclosure: "lazy" | "eager"; +} + +type SkillRef = + | { kind: "script"; path: string } + | { kind: "artifact"; name: string; version: string } + | { kind: "prompt"; path: string }; +``` + +**Example.** For `prereview` (`tools = ["read","grep","bash:git diff *"]`, a +`/prereview` command, a `scripts/` dir), the IR carries `slots.permissions.tools` +and `slots.triggers.commands` as data and `refs: [{kind:"script",...}]`. + +**How adapters consume it.** Each adapter reads the slots and decides, per target: + +```ts +// claude-code: lower permissions NATIVELY (today they are lost to prose) +lower(ir: SkillIR): RenderPlan { + return { + files: [{ + path: `.claude/skills/${ir.manifest.skill.name}/SKILL.md`, + frontmatter: { + name: ir.slots.description ? ir.manifest.skill.name : ..., + description: ir.slots.description, + "allowed-tools": toClaudeToolGrammar(ir.slots.permissions.tools), // NATIVE + }, + body: ir.body, // core renders it + }], + // scripts/ ref → the adapter declares it copies scripts (earns the capability) + copies: ir.refs.filter(r => r.kind === "script"), + }; +} + +// agentsmd: no native permission slot → lower to a prose note (shared fallback) +lower(ir: SkillIR): RenderPlan { + return { files: [{ path: "AGENTS.md", merge: true, + body: [...ir.body, permissionsProse(ir.slots.permissions)] }] }; +} +``` + +This is the whole point: the same structured permission data lowers to a native +`allowed-tools:` on Claude Code and to a prose note on AGENTS.md — the "compiler + +conformance contract" wedge becomes real instead of prose-deep, and triggers stop +being read by one adapter of nine. + +--- + +## 4. Adapter plugin system + +```ts +interface Adapter { + readonly apiVersion: 1; // negotiated; core refuses mismatched majors + readonly id: string; // "claude-code", "acme-internal" + readonly capabilities: readonly Capability[]; // ONLY what lower() delivers (invariant) + readonly loading: "lazy" | "eager"; + readonly tokenizer: TokenizerId; // which family measures this target + detect(root: string): boolean; // pure + managedPaths(name: string): ManagedPath[]; // adapter OWNS what it writes+prunes + lower(ir: SkillIR, ctx: LowerCtx): RenderPlan; // pure: (IR, ctx) → plan; no IO +} +``` + +**Lifecycle:** discover → version-negotiate → `detect` → (cache check) → `lower` → +core emit → `measure`. A run touches an adapter only through these; the core never +hands it a filesystem or network handle. + +**Version negotiation.** `apiVersion` is a single integer. The core supports a +window (e.g. v1–v2); an adapter outside the window is refused with a clear +diagnostic, not silently run. Additive changes to `SkillIR`/`RenderPlan` bump the +minor and stay compatible; a breaking change bumps `apiVersion`. + +**Capability negotiation.** `capabilities` is validated against a single shared +`CAPABILITIES` vocabulary (imported by both the loader and the adapters, fixing the +double-definition). The compile-time invariant: an adapter may list a capability +**only if `lower` produces its primitive** (the `capabilities: []` correctness rule, +now enforced by a test that diffs claimed vs delivered). Degradation is computed from +real capabilities, so `explain` stops lying. + +**Managed paths.** Each adapter declares `managedPaths`; `MANAGED_DIRS` is derived +from the active adapters (a removed adapter's prune entry deletes itself — no dead +in-tree list). + +**Diagnostics.** `lower` may return diagnostics in the `RenderPlan`; it never throws +for expected conditions. All diagnostics use the shared code/severity contract (§6). + +**Plugin discovery.** First-party adapters are in-tree. Third-party: `[project].adapters += ["@acme/kitbash-adapter-foo"]` → dynamic `import()`, **gated by the same trust the +skills get**: the package is pinned in the lockfile with an integrity hash and must +pass a `[policy].allow_adapters` allowlist. A third-party adapter you have not pinned +does not load. + +**Security + determinism.** Adapters run in-process (no sandbox VM — that violates +"avoid unnecessary abstraction" and zero-dep), but they are **pure by contract**: +they receive only the IR and a data-only `LowerCtx`, never fs/net/clock handles. +Determinism is guaranteed structurally (no IO to be nondeterministic about) and +verified by the cold-vs-warm-cache byte-equality test. A third-party adapter that +tries to reach outside its inputs simply has nothing to reach through. + +--- + +## 5. Compiler pass system + +Passes model cross-cutting transforms the way LLVM/Babel/rustc do, but deliberately +smaller. + +```ts +interface Pass { + readonly name: string; + readonly kind: "validate" | "transform"; // NOT "lower" — lowering is adapter-owned + run(ir: SkillIR, ctx: PassCtx): { ir: SkillIR; diagnostics: Diagnostic[] }; +} +``` + +**Ordering.** Fixed phases, deterministic within each: +1. **validate passes** (core: schema-conformance, safety-lints, budget-check) — + run first, fail fast. +2. **transform passes** (core: canonicalize → resolve-refs → inject-permissions). +3. third-party passes run **after** their phase's core passes, in lockfile-declared + order (deterministic, pinned). + +**Optimization passes** in Kitbash's world = context-cost reductions: e.g. a +`dedupe-boilerplate` transform, or a `strip-nonessential` pass an author opts into. +These are transforms; they must preserve semantics and are off by default. + +**Validation passes** add diagnostics; **lowering is not a pass** — it is the +adapter's job, because lowering is inherently per-target and belongs with the code +that knows the target. + +**Intentionally NOT extensible:** parse, the IR shape, the *core* validate passes +(a third-party pass cannot disable the safety/trust lints — it can only add), emit + +reconcile (marker-merge/prune), the lockfile writer, and the trust pipeline. If a +third party could weaken the trust gate, the trust model is gone. This boundary is +the security-critical invariant of the whole design. + +--- + +## 6. Public API + +Two consumers: humans (CLI prose) and machines (library + `--json`). Both sit on one +core. + +```ts +// @kitbash/core — the stable library boundary (package.json "exports") +export function compile(dir: string, opts?: CompileOptions): CompileResult; +export function validate(dir: string): ValidationResult; // parse+validate, no emit +export function measure(dir: string, opts?): MeasureResult; // per-target exact counts +export function lint(dir: string): Diagnostic[]; + +interface CompileResult { + schemaVersion: 1; + skill: { name: string; version: string; source: string; resolved: string }; + outputs: { adapter: string; files: { path: string; bytes: number }[]; + loaded: number; standing: number; tokenizer: string }[]; + diagnostics: Diagnostic[]; + summary: { targets: number; errors: number; warnings: number }; +} + +interface Diagnostic { // the stable diagnostics contract + code: string; // "KB1004" — stable, documented, greppable + severity: "error" | "warning" | "info"; + message: string; + file?: string; span?: [number, number]; + data?: Record; // machine-actionable payload +} +``` + +```jsonc +// kitbash compile --json → one versioned envelope, the wedge as DATA +{ "schemaVersion": 1, "command": "compile", + "results": [ { "adapter": "claude-code", "loaded": 567, "standing": 40, + "tokenizer": "cl100k@2024-05", "files": [...] } ], + "diagnostics": [ { "code": "KB2003", "severity": "warning", + "message": "eager target carries 507 standing tokens", ... } ], + "summary": { "targets": 9, "errors": 0, "warnings": 3 } } +``` + +**Exit-code contract** (documented in SPEC, stop double-using 2): + +| code | meaning | +|---|---| +| 0 | success | +| 1 | findings (lint/test failures, degradation under --strict) | +| 2 | usage error | +| 3 | policy block | +| 4 | safety-lint block | +| 5 | integrity drift | +| 6 | fetch/network failure | +| 7 | not implemented | + +The CLI is a thin renderer over `@kitbash/core`; `--json` prints the envelope, no +flag prints prose. An editor, a CI action, or another tool builds on `core`. + +--- + +## 7. Testing strategy + +``` + what each layer catches + ┌───────────────────────────────────────────────────┐ + ▲ │ regression every fixed bug → a permanent fixture │ + │ ├───────────────────────────────────────────────────┤ + │ │ integration the real dist CLI in temp dirs │ process behavior, + │ │ (e2e, today's test.mjs — KEPT, not replaced) │ exit codes, prompts + │ ├───────────────────────────────────────────────────┤ + │ │ conformance data-only KSF corpus (§8) │ spec compliance, + │ │ run by ANY implementation │ cross-impl agreement + │ ├───────────────────────────────────────────────────┤ + │ │ snapshot skill × adapter → byte-exact goldens │ accidental output drift, + │ │ │ determinism + │ ├───────────────────────────────────────────────────┤ + │ │ property parse∘print roundtrip; compile is │ whole input classes: + │ │ idempotent; cold cache == warm cache │ determinism, no coercion + │ ├───────────────────────────────────────────────────┤ + │ │ fuzz TOML parser; lint regexes vs a │ parser crashes, ReDoS, + │ │ false-positive corpus │ lint FP/FN drift + │ ├───────────────────────────────────────────────────┤ + many │ unit parser, tokenizer, each lint, IR │ logic in isolation + └───────────────────────────────────────────────────┘ +``` + +- **unit** — the parser, the tokenizer per family (against reference token counts), + each lint detector, IR construction. Fast, most numerous. +- **parser** — a dedicated tier wiring the **BurntSushi `toml-test`** corpus, so the + KSF-TOML subset boundary is exactly where the spec claims. +- **property** — `parse(print(x)) == x`; `compile(compile(x)) == compile(x)` + (idempotent); `coldCache == warmCache` (determinism); no schema-invalid input is + silently coerced. +- **fuzz** — the TOML parser (crashes/hangs on untrusted remote input) and the lint + regexes (a corpus of real secrets + benign lookalikes guards FP/FN; catches ReDoS). +- **snapshot** — every (skill, adapter) pair to byte-exact goldens; guards the + maintainer's own adapter edits, not just users'. +- **conformance** — §8; the authority for "supports KSF." +- **integration** — today's e2e script, unchanged in shape (the review confirmed it + is the right *shape*, just insufficient alone). +- **regression** — the standing rule already in force: every fixed bug gets a fixture. + +--- + +## 8. Conformance suite + +`conformance/` is **data-only** so any implementation in any language can run it. + +``` +conformance/ + VERSION # the KSF version this corpus certifies (e.g. 1.0) + manifest/ # parse+validate fixtures + valid/xxxx.toml # input + valid/xxxx.json # expected parsed manifest + invalid/yyyy.toml + invalid/yyyy.error # expected diagnostic code(s), e.g. KB1002 + compile/ # lowering fixtures + /skill/ # a full skill dir + //… # expected byte-exact emitted files, per adapter + measure/ # measurement fixtures + /skill/ + /expected.json # { "tokenizer": "cl100k@rev", "loaded": N, "standing": M } + checklist.json # SPEC §9 MUST/SHOULD items → the fixtures that prove each +``` + +**Fixture format:** input file + expected output file (or expected error code). No +executable expectations — a runner compares its own output to the committed files. + +**Adapter certification:** an adapter is "KSF-conformant" if, over the `compile/` +fixtures naming it, it produces the committed goldens (MUST-tagged fixtures) and its +`capabilities` match what its output actually delivers. First-party adapters are +certified in CI; third-party adapters ship the corpus result in their README. + +**How an independent implementation becomes "KSF compliant":** run the corpus for a +given `VERSION`, report pass rate. Compliance = 100% of MUST fixtures + declared +handling of SHOULD fixtures. This — not Kitbash's source — is the authority. That is +the difference between a spec-shaped README and a standard. + +**Versioning:** the corpus is versioned with KSF. Additive-only within a major means +a KSF 1.1 corpus is a superset of 1.0; a certified 1.0 impl still passes the 1.0 +subset. A major bump ships a new corpus and a migration note. + +--- + +## 9. Versioning + +Four independent version lines, each with a job: + +| line | what it versions | rule | +|---|---|---| +| **KSF version** | the format (manifest fields, semantics) | frozen fields; additive-only within a major (RFC 0002) | +| **edition** (opt-in) | a skill's declared strictness target | optional `edition = "2026"` in `[skill]`; unset = lenient/forward-compat default | +| **compiler version** | the CLI/library (semver) | independent; old skills compile on new compilers unchanged | +| **adapter apiVersion** | the Adapter contract | integer; negotiated window; additive minor, breaking major | +| **lockfile version** | `kitbash.lock` on-disk format | `version = N` header; reader migrates old N forward | + +**The edition marker** (new, cheap, freeze-or-never) resolves the tension between +"forward-compat requires warn-not-fail on unknown fields" and "authors sometimes want +strictness." Unset: unknown fields warn (a newer skill compiles on an older +compiler). `edition = "2026"`: the compiler may hard-fail unknowns the edition +knows about. It gives an explicit opt-in to stricter behavior without abandoning the +additive-only default — the same move as Rust editions. + +**How compatibility holds for 10 years:** (1) additive-only within a KSF major keeps +old skills valid; (2) the conformance corpus per version makes "still compatible" +testable, not asserted; (3) the edition marker lets the format evolve strictness +without breaking un-editioned skills; (4) the lockfile `version` header lets the +on-disk format migrate; (5) the adapter `apiVersion` window lets the backend contract +move without orphaning third-party adapters overnight. A breaking change is always +possible — it just costs a KSF major, a new corpus, a migration note, and an edition. + +--- + +## 10. Migration (no rewrite) + +The v2 architecture is reached by **adding the middle and reserving contracts**, not +rewriting. The current code is largely kept: the manifest types, the lockfile hasher, +the lints, the marker-merge/prune, the e2e harness all survive. What changes is that +the opaque-string flow becomes an IR flow, and the frozen surfaces get reserved before +1.0. + +### Phase 0 — freeze-or-never (before any 1.0 tag) — effort: **S–M (weeks)** +The only genuinely urgent work at 0 users, because it is impossible to change after a +freeze: +- Reserve lockfile fields (`resolved` commit, `signature`, `outputHash`, `version = N` + header) even before the machinery behind them exists. +- Add the optional `edition` marker to `[skill]` (schema + loader). +- Freeze the Adapter interface, the exit-code enum, and the `--json` envelope schema + as documented contracts. +- Label the tokenizer honestly (`tokenizer = "estimate-v1"`) everywhere a number + appears; stop implying per-target *tokenization*. +- Delete the conceded `publish`/`search` stubs and the index short-name source form; + mark `[dependencies]` spec-reserved and WARN loudly instead of silently dropping. +- Reconcile the two validators so the frozen `authors` field stops being dropped. +- Fix source-as-identity path/key format (so `acme/review` and `globex/review` can + coexist later without a lockfile break). + +### Phase 1 — make the wedge true — effort: **L (months)** +- Introduce `SkillIR` + the pass pipeline; move permissions/triggers into IR passes; + adapters become `lower(IR)`. +- Vendor per-family BPE tokenizers as data; wire the measure stage; stamp + `tokenizer@rev`. +- Ship the data-only conformance corpus + per-adapter golden snapshots + `toml-test` + + the fuzzer. +- Earn the `scripts` capability (adapters copy `scripts/`), closing the degenerate + matrix by implementation. + +### Phase 2 — package-manager + trust credibility — effort: **L** +(build only once people install across machines/CI) +- Resolved-commit pin + `.git` strip + `install` (no args) = restore-from-lock + + `compile --check`. +- Optional signing/provenance + a trust root + the `audit` command. +- `@kitbash/core` library boundary + `--json` on all commands + the diagnostics API. +- Incremental + parallel compile via the content-addressed cache. + +### Phase 3 — ecosystem — effort: **L (post-adoption)** +- Third-party adapters (`[project].adapters`, pinned) and third-party passes. +- Adapter certification against the corpus. +- Version-range dependency resolution keyed on source identity, and `update` with a + capability-drift re-consent gate. +- A registry **of adapters/passes** (not of skills — skills stay decentralized, + git-URL-as-identity, Go-modules style). The one naming authority Kitbash needs is + for the code that extends it, not for the content it compiles. + +--- + +## Finally + +### 1. MUST happen before v1.0 +All of **Phase 0** (freeze-or-never), plus the honest tokenizer label, plus enough of +the **IR** that KSF 1.0 is not frozen around the opaque-string body model. If you +freeze the current shape, the IR becomes a KSF-2.0-major change later; if you land the +IR first, it is internal. Reserve every extension point (adapter apiVersion, `--json` +envelope, exit codes, lockfile fields, edition) — reserving is cheap now and +impossible after. + +### 2. Should wait until after adoption +Signing/provenance, the resolver + `update`, third-party adapters/passes, the +adapter registry, incremental/parallel caching. All are correct and correctly +ordered — but every one of them only pays off at a user count Kitbash does not have. +Building them on spec is polishing an engine no one drives. + +### 3. Should probably never be built +- A **skills hosting registry** — ceded to vercel-labs/skills (27k stars); Kitbash is + the build system, not the store. +- A **runtime harness** (learned instincts, session memory, a dashboard) — that is + ECC's product; competing there at 1/30,000th the size is a loss. +- A **config auditor** for your own `.claude`/mcp/hooks — that is AgentShield/ctxlint. +- A **plugin sandbox VM** — over-abstraction; purity-by-contract is enough and keeps + the code approachable. + +### 4. Biggest architectural risks +1. **Freezing KSF 1.0 around the string-templater shape** — makes the IR a major-bump + change and permanently caps the wedge at prose-deep. (Mitigation: land the IR in + Phase 1, before the freeze.) +2. **The tokenizer staying approximate** — the one uncontested differentiator is + dishonest until it is exact. (Mitigation: Phase 0 honest label, Phase 1 real BPE.) +3. **Adapter API churn** breaking third parties once they exist. (Mitigation: freeze + `apiVersion` + a support window before opening the door.) +4. **The trust gate being weakenable by a third-party pass** — would erase the trust + model. (Mitigation: core validate passes are non-removable by design; §5.) + +### 5. Biggest opportunities +1. **The conformance corpus** — the single thing that turns "a tool" into "a standard" + others implement. Nobody in this space has one. +2. **Exact per-target measurement** — the uncontested wedge, made true. +3. **Third-party adapters** — 50+ targets without the maintainer becoming the + bottleneck; the product thesis ("one source → N formats") only scales if N is not + gated by release cadence. +4. **The library API** — being the substrate editors/CI/other tools build on is what + "standard build system" actually means. + +### 6. Verdict +**Yes — this architecture can realistically carry Kitbash for a decade, but only +because it is small.** The IR is one struct, not a syntax tree. The pass pipeline is +two phases, not an optimizer framework. The plugin model is purity-by-contract, not a +sandbox. Every subsystem earns its place against a named failure in the current code. +The current shape *cannot* support the goal — it caps at nine hand-maintained string +templaters with a heuristic measurement. The v2 shape can, and it is reachable by +addition, not rewrite. The decisive move is **sequencing, not scope**: freeze the +contracts correctly now, make the wedge true and honest next, and defer everything +that only matters at scale until scale exists. If Kitbash does Phase 0 and enough of +Phase 1 to prove the wedge, it has an architecture worth standardizing — and the +discipline to have stopped there is what will keep it maintainable. diff --git a/site/changelog.html b/site/changelog.html index be1d399..8c2c01e 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.10.0Current CLI version
+
v0.11.0Current CLI version
8Compile targets
Apache-2.0License
@@ -105,10 +105,22 @@

Changelog

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

+
+
+

v0.11.0

+ 2026-07-28latest +
+

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).
+
+

v0.10.0

- 2026-07-26latest + 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.

Added

diff --git a/site/docs/adapters.html b/site/docs/adapters.html index b5a32c7..81cde14 100644 --- a/site/docs/adapters.html +++ b/site/docs/adapters.html @@ -120,7 +120,7 @@

Lazy vs eager

Kitbash compiles to the cheapest loading mode each target actually supports, so six of the nine 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 ~517 tokens standing every session (declared limit: 60)
+ 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.

Capability matrix

@@ -267,7 +267,7 @@

kitbash explain

To interrogate one pairing, ask directly. explain takes a skill — installed name, local path, or a remote source you have not installed — and an adapter id:

kitbash explain prereview agentsmd
prereview → agentsmd: no capability degradation
-   loading: agentsmd is eager — skill costs ~464 tokens standing every session (declared limit: 60)
+ loading: agentsmd is eager — skill costs ~560 tokens standing every session (declared limit: 60)

When capabilities are missing it itemizes them:

verify → cursor: degraded
    requires "scripts" — not supported by cursor; compiled instruction-only
@@ -276,7 +276,7 @@

kitbash explain

Failing the build

Warnings are informational by default. Pass --strict to kitbash compile and any warning — degradation, eager loading, output-path conflicts — turns into a non-zero exit:

kitbash compile --strict
-
--strict: failing on 2 warning(s)
+
--strict: failing on 2 warnings

That is the flag for CI. It converts "we quietly lost the script hook on three of five agents" into a red build.

The matrix is derived

diff --git a/site/docs/authoring.html b/site/docs/authoring.html index 3f87590..94594ae 100644 --- a/site/docs/authoring.html +++ b/site/docs/authoring.html @@ -319,7 +319,7 @@

kitbash compile

AGENTS.md test-gaps → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~364 tokens standing every session (declared limit: 60) -compiled 1 skill(s) for 2 agent target(s) +compiled 1 skill for 2 targets

That warning is not a mistake in your manifest — it is the honest cost of an eager target. Kitbash compiles to the cheapest mode each target supports, so Claude Code, Cursor, Copilot, Devin, Gemini CLI and the .agents/skills/ path honor disclosure = "lazy"; Cline, Aider and the AGENTS.md floor have no deferred mode and carry the whole body every session regardless. The gap is the case for keeping bodies short. See the token budget benchmark and adapters & targets.

kitbash test

diff --git a/site/docs/cli.html b/site/docs/cli.html index d115688..ce31b7e 100644 --- a/site/docs/cli.html +++ b/site/docs/cli.html @@ -150,7 +150,7 @@

Synopsis

lore Build, query, and curate repo intelligence run Run a declared pipeline (e.g. kitbash run ship) -Docs: https://github.com/singhharsh1708/kitbash +Docs: https://kitbash.vercel.app/docs

Every command runs against the current working directory as the project root. Skills live in .kitbash/skills/, config in kitbash.toml, pins in kitbash.lock.

Command summary

@@ -229,9 +229,9 @@

install

The prompt only appears when both stdin and stdout are TTYs. In a pipe, a CI runner, or a non-interactive shell, install proceeds without asking — --yes is for readability, not for unblocking automation.

$ kitbash install gh:singhharsh1708/kitbash/examples/skills/prereview
-review: prereview@0.1.0 — Reviews a diff against the team's actual standards
+review: prereview@0.1.0 — Review the working diff against this team's real standards before it ships
   budget 1500 tokens · standing 60 · lazy disclosure · mode skill
-  permissions: tools [read, grep, bash:git *] · network no · write no
+  permissions: tools [read, grep, bash:git diff *, bash:git log *] · network no · write no
 install prereview@0.1.0? [y/N] y
 installed prereview@0.1.0
   pinned in kitbash.lock
@@ -278,7 +278,7 @@ 

list

  • (none) Takes no arguments and no flags.
  • $ kitbash list
    -prereview@0.1.0  budget=1500  standing=60  mode=skill  — Reviews a diff against the team's actual standards
    +prereview@0.1.0  budget=1500  standing=60  mode=skill  — Review the working diff against this team's real standards before it ships
     release-notes@0.2.0  budget=2200  standing=80  mode=skill  — Turns merged PRs into a release note
     scratch@0.0.0  budget=6000  standing=250  mode=skill  [unmanifested]  — Imported skill (no manifest)

    An empty project says so instead of printing nothing:

    @@ -302,15 +302,15 @@

    compile

    .cursor/rules/prereview.mdc AGENTS.md removed .claude/commands/old-review.md (stale) - prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~517 tokens standing every session (declared limit: 60) -compiled 1 skill(s) for 3 agent target(s)
    + prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~560 tokens standing every session (declared limit: 60) +compiled 1 skill for 3 targets

    Warnings cover eager-target standing cost, capability degradation, unmanifested skills, and output-path conflicts where two skills write the same file. With --strict those same warnings end the run:

    $ kitbash compile --strict
      .claude/skills/prereview/SKILL.md
      AGENTS.md
    - prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~517 tokens standing every session (declared limit: 60)
    -compiled 1 skill(s) for 2 agent target(s)
    ---strict: failing on 1 warning(s)
    + prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~560 tokens standing every session (declared limit: 60) +compiled 1 skill for 2 targets +--strict: failing on 1 warning

    A budget violation stops the run outright:

     prereview: compiled body is ~1712 tokens, over its budget of 1500

    With nothing installed, compile still prunes stale output, then reports and succeeds:

    @@ -450,7 +450,7 @@

    explain

    $ kitbash explain prereview agentsmd
     prereview → agentsmd: no capability degradation
    -   loading: agentsmd is eager — skill costs ~464 tokens standing every session (declared limit: 60)
    + loading: agentsmd is eager — skill costs ~560 tokens standing every session (declared limit: 60)

    No adapter declares a capability today — emit() writes instructions but does not copy scripts/, install a hook, or wire a subagent — so a skill that requires any of scripts, hooks, subagents, or network reports degraded on every one of the nine targets, Claude Code included:

    $ kitbash explain prereview cursor
     prereview → cursor: degraded
    @@ -480,7 +480,7 @@ 

    preview

    --- name: prereview -description: "Reviews a diff against the team's actual standards" +description: "Review the working diff against this team's real standards before it ships" --- <!-- generated by kitbash — do not edit; source: .kitbash/skills/prereview @ 0.1.0 --> @@ -495,8 +495,8 @@

    preview

    Arguments: $ARGUMENTS -─── agentsmd [eager] ~507 tok standing ─── - prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~464 tokens standing every session (declared limit: 60) +─── agentsmd [eager] ~560 tok standing ─── + prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~560 tokens standing every session (declared limit: 60) AGENTS.md diff --git a/site/docs/index.html b/site/docs/index.html index 814a4b6..9765696 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -194,12 +194,13 @@

    How the pieces fit

    write = false

    kitbash compile reads every installed skill, detects which assistants the repo is set up for, and writes each one's native format:

    kitbash compile
    -
     compiled 1 skill → 3 targets
    +      
     compiled 1 skill for 3 targets
      .claude/skills/prereview/SKILL.md
    + .claude/commands/prereview.md
      .cursor/rules/prereview.mdc
      AGENTS.md §prereview
      prereview → agentsmd: agentsmd is eager and cannot lazy-load;
    -  this skill costs ~517 tokens standing every session
    + this skill costs ~560 tokens standing every session

    Per-file targets get a file of their own. Shared-file targets — AGENTS.md and Aider's CONVENTIONS.md — get a marker-delimited section merged into whatever is already there, so hand-written content survives a recompile and removed skills get pruned on the next one. AGENTS.md is the floor: it is always emitted, because everything that reads it (Codex among them) then gets the skill for free.

    Degradation is visible, never silent. When a target cannot honor something a skill declared — lazy loading, a required capability — the compiler says so on stdout and kitbash explain <skill> <adapter> says why. --strict turns those warnings into a non-zero exit for CI.

    diff --git a/site/docs/quickstart.html b/site/docs/quickstart.html index c1638f8..b79f538 100644 --- a/site/docs/quickstart.html +++ b/site/docs/quickstart.html @@ -130,9 +130,9 @@

    3. Install a skill

    Skills come from a GitHub repo, a subfolder of one, or a local path:

    kitbash install gh:singhharsh1708/kitbash/examples/skills/prereview

    Before anything is written to disk, Kitbash shows you what the skill declares and asks for confirmation:

    -
    review: prereview@0.1.0 — Reviews a diff against the team's actual standards
    -  budget 1500 tokens · standing 60 · lazy disclosure · mode skill
    -  permissions: tools [read, grep, bash:git *] · network no · write no
    +      
    review: prereview@0.1.0 — Review the working diff against this team's real standards before it ships
    +  budget 1500 tok · standing 60 tok/session · lazy disclosure · mode skill
    +  permissions: tools [read, grep, bash:git diff *, bash:git log *] · network no · write no
     install prereview@0.1.0? [y/N]

    This is the point. Installing a skill means letting someone else's instructions run with your agent's permissions. Read the review block before you answer. In scripts and CI, pass --yes to skip the prompt — see trust & review.

    @@ -142,12 +142,12 @@

    3. Install a skill

    4. Compile

    kitbash compile

    Kitbash detects which agents your repo is set up for and writes each one's native format:

    -
     compiled 1 skill → 3 targets
    +      
     compiled 1 skill for 2 targets
      .claude/skills/prereview/SKILL.md
    - .cursor/rules/prereview.mdc
    + .claude/commands/prereview.md
      AGENTS.md §prereview
      prereview → agentsmd: agentsmd is eager and cannot lazy-load;
    -  this skill costs ~517 tokens standing every session
    + 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.

    5. Inspect before you trust

    diff --git a/site/docs/trust.html b/site/docs/trust.html index 590b9d9..eebeb83 100644 --- a/site/docs/trust.html +++ b/site/docs/trust.html @@ -231,8 +231,8 @@

    Preview the compiled output

    kitbash preview gh:singhharsh1708/kitbash/examples/skills/prereview
    preview: prereview@0.1.0
     
    -─── agentsmd [eager] ~507 tok standing ───
    - prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~464 tokens standing every session (declared limit: 60)
    +─── agentsmd [eager] ~560 tok standing ───
    + prereview → agentsmd: agentsmd is eager and cannot lazy-load; this skill costs ~560 tokens standing every session (declared limit: 60)
     
       → AGENTS.md
     
    @@ -256,7 +256,7 @@ 

    2. Review at install

    The block

    kitbash install gh:singhharsh1708/kitbash/examples/skills/prereview
    review: prereview@0.1.0 — Review the working diff against this team's real standards before it ships
    -  budget 1500 tokens · standing 60 · lazy disclosure · mode gate
    +  budget 1500 tokens · standing 60 · lazy disclosure · mode skill
       permissions: tools [read, grep, bash:git diff *, bash:git log *] · network no · write no
     install prereview@0.1.0? [y/N] y
     installed prereview@0.1.0
    @@ -376,7 +376,7 @@ 

    A hard gate at install

    Policy is evaluated after the review block prints and before the prompt — and before anything is written. --yes does not bypass it. With allow_sources = ["gh:your-org/*"] and max_budget = 1000 in effect:

    kitbash install --yes file:./vendor/prereview
    review: prereview@0.1.0 — Review the working diff against this team's real standards before it ships
    -  budget 1500 tokens · standing 60 · lazy disclosure · mode gate
    +  budget 1500 tokens · standing 60 · lazy disclosure · mode skill
       permissions: tools [read, grep, bash:git diff *, bash:git log *] · network no · write no
        policy: source "file:./vendor/prereview" is not in allow_sources (gh:your-org/*)
        policy: prereview budget 1500 exceeds max_budget 1000
    diff --git a/site/index.html b/site/index.html
    index 8d5b58f..f4bf4fe 100644
    --- a/site/index.html
    +++ b/site/index.html
    @@ -151,7 +151,7 @@
           
         
         
    -

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

    +

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

    Write an agent skill once. Run it everywhere.

    Get started @@ -326,13 +326,13 @@

    #Pick a target