From f7203d662bb5512bcdd9a121f8ad5ef4a1968058 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Wed, 12 Aug 2026 00:34:35 +0530 Subject: [PATCH 1/2] chore(release): script the Homebrew bump, and stop overclaiming about Cursor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TAP_TOKEN still is not set, so the homebrew job skips with a warning and the formula gets bumped by hand once per release — which meant copying a sha256 between terminals seven times over the last few releases. scripts/bump-tap.sh runs the same steps the workflow does, so the digest is computed from the tarball rather than retyped. Dry run is the default, because the tap is a separate public repo and a wrong digest there breaks `brew install` for everyone until somebody notices. It refuses a version that is not on npm yet (the formula would 404) and is a no-op when the formula already points at that version. An audit while writing it confirmed all eight hand-bumped releases (0.17.0 through 0.22.0) carry a sha256 matching their real tarball, so nothing shipped broken — but the process had no check that would have caught it if one had. Also softens the comment excluding cursor from VENDOR_NEUTRAL_ALIASES. It asserted the .cursor/rules file is "not a second copy" of the .agents/skills one; they are different mechanisms, but Cursor does read .agents/skills, so in a repo with both it is offered the same body twice, and whether it dedupes is unverified. That is now stated as unverified rather than settled — the same class of confident-but-unchecked comment that hid the copilot/gemini duplication until 0.22.0. --- docs/release-checklist.md | 16 ++++++- packages/cli/src/adapters.ts | 12 ++++-- scripts/bump-tap.sh | 84 ++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 4 deletions(-) create mode 100755 scripts/bump-tap.sh diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 4dabc2e..4419d6b 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -131,8 +131,22 @@ Actions) as `TAP_TOKEN`. Without it the tap step skips with a warning and the formula is bumped by hand. The GitHub release does **not** need this — it runs on the built-in token. +Until that secret exists, bump the tap with `scripts/bump-tap.sh` rather than by +hand — it runs the same steps the workflow does, so the digest is computed from +the downloaded tarball instead of being copied between terminals: + +```bash +scripts/bump-tap.sh 0.22.0 # show the change, touch nothing +scripts/bump-tap.sh 0.22.0 --push # commit and push it +``` + +Dry run is the default: the tap is a separate public repo, and a wrong digest +there breaks `brew install` for everyone until somebody notices. The script +refuses a version that is not on npm yet (the formula would 404) and is a no-op +when the formula already points at that version. + ### Manual fallback If the workflow is unavailable: `npm publish` from `packages/cli` (the web-auth token goes stale between releases — expect to run `npm login --auth-type=web` -first), then bump the tap formula by hand. +first), then `scripts/bump-tap.sh --push`. diff --git a/packages/cli/src/adapters.ts b/packages/cli/src/adapters.ts index 9d69d7a..600a647 100644 --- a/packages/cli/src/adapters.ts +++ b/packages/cli/src/adapters.ts @@ -247,9 +247,15 @@ const agents = skillDirAdapter( * skill names, so a skill present at two roots is loaded twice. * * Zed and Cline are absent from this map because they already compile to - * `.agents/skills/` and so cannot duplicate. Cursor is absent because it - * compiles to `.cursor/rules/*.mdc`, a rules file rather than a skill - * directory — a different mechanism, not a second copy of the same one. + * `.agents/skills/` and so cannot duplicate. + * + * Cursor is absent for a weaker reason, stated plainly rather than dressed up: + * it compiles to `.cursor/rules/.mdc`, a description-triggered rule rather + * than a skill directory, so the two are different mechanisms — but Cursor does + * read `.agents/skills/`, so in a repo with both it is offered the same body + * twice. Whether it dedupes, warns, or simply lists both is UNVERIFIED, and + * dropping the rule on a guess would cost Cursor users their only trigger. Left + * as-is deliberately until someone checks; do not promote this to a claim. */ export const VENDOR_NEUTRAL_ALIASES: { id: string; dir: string }[] = [ { id: "copilot", dir: ".github/skills" }, diff --git a/scripts/bump-tap.sh b/scripts/bump-tap.sh new file mode 100755 index 0000000..b6d179d --- /dev/null +++ b/scripts/bump-tap.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Bump the Homebrew formula to a published version. +# +# The release workflow does this automatically, but only when the TAP_TOKEN +# secret exists (a PAT with contents:write on singhharsh1708/homebrew-tap). +# Until it does, the homebrew job skips with a warning and the bump falls to a +# human — which historically meant copying a sha256 by hand, once per release. +# This runs the same steps the workflow does, so the digest is never retyped. +# +# scripts/bump-tap.sh 0.22.0 # show what would change, touch nothing +# scripts/bump-tap.sh 0.22.0 --push # commit and push it +# +# Dry run is the default on purpose: this pushes to a separate public repo, and +# a wrong digest there breaks `brew install` for everyone until it is noticed. +set -euo pipefail + +VERSION="${1:-}" +PUSH="${2:-}" +TAP_REPO="singhharsh1708/homebrew-tap" + +if [ -z "$VERSION" ]; then + echo "usage: scripts/bump-tap.sh [--push]" >&2 + exit 2 +fi +if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "error: '$VERSION' is not a semver version (expected e.g. 0.22.0)" >&2 + exit 2 +fi + +# Refuse to point the formula at something nobody can install. +published=$(npm view "kitbash@${VERSION}" version 2>/dev/null || true) +if [ "$published" != "$VERSION" ]; then + echo "error: kitbash@${VERSION} is not on npm yet — publish first, or the formula will 404" >&2 + exit 1 +fi + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +url="https://registry.npmjs.org/kitbash/-/kitbash-${VERSION}.tgz" +# The tarball can lag the publish by a few seconds on the CDN, same as in CI. +for i in 1 2 3 4 5; do + if curl -fsSL "$url" -o "$work/kitbash.tgz"; then break; fi + echo "tarball not on the registry yet, retrying ($i/5)" + sleep 10 +done +[ -s "$work/kitbash.tgz" ] || { echo "error: could not download $url" >&2; exit 1; } + +# shasum on macOS, sha256sum on Linux — this script runs on a maintainer laptop. +if command -v sha256sum >/dev/null 2>&1; then + sha=$(sha256sum "$work/kitbash.tgz" | cut -d' ' -f1) +else + sha=$(shasum -a 256 "$work/kitbash.tgz" | cut -d' ' -f1) +fi + +GIT_ASKPASS= git -c credential.helper='!gh auth git-credential' \ + clone -q "https://github.com/${TAP_REPO}.git" "$work/tap" + +formula="$work/tap/Formula/kitbash.rb" +[ -f "$formula" ] || { echo "error: $formula not found in $TAP_REPO" >&2; exit 1; } + +# Same substitution the workflow performs. +if command -v gsed >/dev/null 2>&1; then SED=gsed; else SED=sed; fi +$SED -i.bak -E "s|url \".*\"|url \"${url}\"|; s|sha256 \".*\"|sha256 \"${sha}\"|" "$formula" +rm -f "${formula}.bak" + +echo +git -C "$work/tap" --no-pager diff -- Formula/kitbash.rb +echo + +if git -C "$work/tap" diff --quiet -- Formula/kitbash.rb; then + echo "formula is already at ${VERSION} — nothing to do" + exit 0 +fi + +if [ "$PUSH" != "--push" ]; then + echo "dry run — re-run with --push to commit and push the change above" + exit 0 +fi + +git -C "$work/tap" commit -qam "kitbash ${VERSION}" +GIT_ASKPASS= git -C "$work/tap" -c credential.helper='!gh auth git-credential' push -q origin HEAD +echo "pushed kitbash ${VERSION} to ${TAP_REPO}" +echo "verify with: brew update && brew info singhharsh1708/tap/kitbash" From bbe7f278938e871762ba50dd0f41d0de34ae5948 Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Wed, 12 Aug 2026 00:38:18 +0530 Subject: [PATCH 2/2] feat(compile): report the Cursor rule/skill overlap instead of asserting it away MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cursor reads .agents/skills/ as well as .cursor/rules/, and the rule this compiler emits — alwaysApply: false, a description, no globs — is exactly what Cursor's docs call a dynamic rule: the shape its own /migrate-to-skills command converts into a skill. Both land in Cursor's "Agent Decides" pool, and Cursor documents no dedup between them, so a repo with both paths offers one capability twice and pays each entry's description up front. Compile now reports that, with the duplicated cost measured, and names the [project].targets line that resolves it. The rule is still emitted. Agent Skills only reached general availability in Cursor 2.4, so dropping it would silently cost anyone on an older Cursor their only trigger — worse than a duplicated description. Reporting keeps the choice with the reader rather than making it for them on partial evidence. This started as a comment in the previous release claiming the rule was "not a second copy" of the skill. It is one, through a different mechanism — the same class of confident, unverified comment that hid the copilot/gemini duplication until 0.22.0. Verified against Cursor's own docs and staff posts before changing anything. Adds 7 tests. 0.23.0. --- CHANGELOG.md | 11 +++++++++++ packages/cli/package.json | 2 +- packages/cli/scripts/test.mjs | 35 +++++++++++++++++++++++++++++++++++ packages/cli/src/adapters.ts | 33 ++++++++++++++++++++++++++------- packages/cli/src/commands.ts | 8 +++++++- site/changelog.html | 17 +++++++++++++++-- site/index.html | 2 +- 7 files changed, 96 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad86ab..1d67cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ Format: [Keep a Changelog](https://keepachangelog.com). Versioning: semver — for skills *and* for this CLI, breaking prompt changes are breaking changes. +## [0.23.0] — 2026-08-12 + +### Added +- **Compile reports the Cursor rule/skill overlap.** Cursor reads `.agents/skills/` as well as `.cursor/rules/`, and the rule this compiler emits — `alwaysApply: false`, a `description`, no `globs` — is exactly what Cursor's docs call a *dynamic rule*, the shape its own `/migrate-to-skills` command converts into a skill. Both land in Cursor's "Agent Decides" pool, and Cursor documents no dedup between them, so a repo with both paths offers one capability twice and pays each entry's description up front. Compile now says so, with the duplicated cost measured, and points at the `[project].targets` line that resolves it. + + The rule is still emitted. Agent Skills only reached general availability in Cursor 2.4, so dropping it would silently cost anyone on an older version their only trigger — a worse outcome than a duplicated description. The tradeoff is now visible instead of buried in a source comment. +- **`scripts/bump-tap.sh`** — the Homebrew formula bump, scripted. `TAP_TOKEN` is still unset, so the release workflow's tap job skips with a warning and the formula is bumped by hand; this runs the same steps the workflow does, so the digest is computed from the downloaded tarball rather than copied between terminals. Dry run by default, refuses a version that is not on npm yet, and is a no-op when the formula already points at that version. (An audit while writing it confirmed all eight hand-bumped releases, 0.17.0 through 0.22.0, carry a sha256 matching their real tarball.) + +### Fixed +- Corrected the comment excluding `cursor` from the vendor-neutral dedup, which asserted the rule file was "not a second copy" of the skill. It is a second offer of the same capability through a different mechanism — the same class of confident, unverified comment that hid the Copilot and Gemini duplication until 0.22.0. + ## [0.22.0] — 2026-08-09 ### Fixed diff --git a/packages/cli/package.json b/packages/cli/package.json index 47a0fb3..46e7e4b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "kitbash", - "version": "0.22.0", + "version": "0.23.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 05d08d9..fd86c47 100644 --- a/packages/cli/scripts/test.mjs +++ b/packages/cli/scripts/test.mjs @@ -1814,6 +1814,41 @@ try { rmSync(onlyTmp, { recursive: true, force: true }); } +// ── cursor rule / skill overlap ────────────────────────────────────────────── +// Cursor reads .agents/skills/ as well as .cursor/rules/, and the rule shape +// emitted here is the "dynamic rule" its own /migrate-to-skills converts into a +// skill. Both sit in one selection pool with no documented dedup, so the overlap +// is reported — but the rule is still written, because Agent Skills only reached +// GA in Cursor 2.4 and dropping it would cost older Cursor its only trigger. +const curTmp = mkdtempSync(join(tmpdir(), "kitbash-cursor-")); +try { + mkdirSync(join(curTmp, ".cursor"), { recursive: true }); + mkdirSync(join(curTmp, ".agents"), { recursive: true }); + run(["init"], curTmp); + run(["install", `file:${fixture}`, "--yes"], curTmp); + const c = run(["compile"], curTmp); + check("cursor-overlap: the rule is still written", existsSync(join(curTmp, ".cursor/rules/prereview.mdc")), c.out); + check("cursor-overlap: the skill is written too", existsSync(join(curTmp, ".agents/skills/prereview/SKILL.md"))); + check("cursor-overlap: the duplication is reported", c.out.includes("offered twice"), c.out); + check("cursor-overlap: with a measured cost", /~\d+ tok of trigger description is charged twice/.test(c.out), c.out); + check("cursor-overlap: it is a note, so --strict still passes", run(["compile", "--strict"], curTmp).status === 0); +} finally { + rmSync(curTmp, { recursive: true, force: true }); +} + +// Without .agents/, cursor is the only mechanism and there is nothing to report. +const curOnly = mkdtempSync(join(tmpdir(), "kitbash-cursoronly-")); +try { + mkdirSync(join(curOnly, ".cursor"), { recursive: true }); + run(["init"], curOnly); + run(["install", `file:${fixture}`, "--yes"], curOnly); + const c2 = run(["compile"], curOnly); + check("cursor-overlap: no note when .agents is absent", !c2.out.includes("offered twice"), c2.out); + check("cursor-overlap: and the rule is still emitted", existsSync(join(curOnly, ".cursor/rules/prereview.mdc"))); +} finally { + rmSync(curOnly, { recursive: true, force: true }); +} + if (failures) { console.error(`\n${failures} test(s) failed`); process.exit(1); diff --git a/packages/cli/src/adapters.ts b/packages/cli/src/adapters.ts index 600a647..baa9395 100644 --- a/packages/cli/src/adapters.ts +++ b/packages/cli/src/adapters.ts @@ -249,19 +249,38 @@ const agents = skillDirAdapter( * Zed and Cline are absent from this map because they already compile to * `.agents/skills/` and so cannot duplicate. * - * Cursor is absent for a weaker reason, stated plainly rather than dressed up: - * it compiles to `.cursor/rules/.mdc`, a description-triggered rule rather - * than a skill directory, so the two are different mechanisms — but Cursor does - * read `.agents/skills/`, so in a repo with both it is offered the same body - * twice. Whether it dedupes, warns, or simply lists both is UNVERIFIED, and - * dropping the rule on a guess would cost Cursor users their only trigger. Left - * as-is deliberately until someone checks; do not promote this to a claim. + * Cursor is absent for a reason that is real but narrower than it looks, and + * worth stating exactly. Cursor reads `.agents/skills/`, and a rule shaped like + * the one this compiler emits (`alwaysApply: false`, a `description`, no + * `globs`) is what Cursor's own docs call a *dynamic rule* — the shape its + * `/migrate-to-skills` command converts into a skill. Both land in the same + * "Agent Decides" pool, and Cursor documents no dedup between them, so in a repo + * with both paths it sees two entries offering one capability and pays each + * one's description up front. + * + * The rule is still emitted anyway, because dropping it is not free: Agent + * Skills only reached general availability in Cursor 2.4, and before that + * `.cursor/rules/*.mdc` was the only mechanism that worked. Removing it would + * silently cost users on older Cursor their one trigger, to save a duplicated + * description on newer ones. Compile reports the duplication instead — see + * cursorSkillOverlapNote — so the cost is visible and the choice stays the + * reader's. */ export const VENDOR_NEUTRAL_ALIASES: { id: string; dir: string }[] = [ { id: "copilot", dir: ".github/skills" }, { id: "gemini", dir: ".gemini/skills" }, ]; +/** + * What the Cursor overlap costs, in the terms this tool measures everything else + * in. `descriptions` are the skill descriptions written into both the rule and + * the skill; each is charged once per entry, and Cursor keeps both entries. + */ +export function cursorSkillOverlapNote(descriptions: string[]): string { + const dup = descriptions.reduce((sum, d) => sum + estimateTokens(d), 0); + return `cursor: each skill is offered twice — as a rule (.cursor/rules/) and as a skill (.agents/skills/), which Cursor also reads. Both sit in its "Agent Decides" pool with no documented dedup, so ~${dup} tok of trigger description is charged twice. Cursor's own /migrate-to-skills converts rules of this shape into skills — drop "cursor" from [project].targets if every Cursor you support is 2.4 or newer.`; +} + /** * Zed's skill loader (`crates/agent_skills/agent_skills.rs`) is stricter than * KSF about frontmatter, and it fails *silently* — a skill that violates either diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index b1ab01c..6224a50 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -6,7 +6,7 @@ import { createRequire } from "node:module"; import { createInterface } from "node:readline"; import { tmpdir } from "node:os"; import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; -import { ADAPTERS, AGENT_PLUGIN_DIR, agentPluginManifest, GENERATED_MARK, mergeSection, pruneSections, readFileIfExists, VENDOR_NEUTRAL_ALIASES, type CompiledFile } from "./adapters.js"; +import { ADAPTERS, AGENT_PLUGIN_DIR, agentPluginManifest, cursorSkillOverlapNote, GENERATED_MARK, mergeSection, pruneSections, readFileIfExists, VENDOR_NEUTRAL_ALIASES, 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 { collectImports, driftGroups, type ImportedSource } from "./importers.js"; @@ -943,6 +943,12 @@ export async function cmdCompile(args: string[]): Promise { // and Codex dedupes root paths but not skill names, so it loads the skill twice. // Any existing copy is removed by the prune pass below, since nothing wrote it. if (adapters.some((a) => a.id === "agents")) { + // Cursor reads .agents/skills/ too, but its rule file is a different + // mechanism and older Cursor has no skills support, so the overlap is + // reported rather than resolved by dropping one side. + if (adapters.some((a) => a.id === "cursor") && skills.length) { + notes.push(cursorSkillOverlapNote(skills.map((s) => s.manifest.skill.description))); + } for (const alias of VENDOR_NEUTRAL_ALIASES) { if (!adapters.some((a) => a.id === alias.id)) continue; const dropped = [...files.keys()].filter((p) => p.startsWith(`${alias.dir}/`)); diff --git a/site/changelog.html b/site/changelog.html index 74a2022..dc504c5 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.22.0Current CLI version
+
v0.23.0Current CLI version
8Compile targets
Apache-2.0License
@@ -105,10 +105,23 @@

Changelog

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

+
+
+

v0.23.0

+ 2026-08-12latest +
+

Added

+
  • Compile reports the Cursor rule/skill overlap. Cursor reads .agents/skills/ as well as .cursor/rules/, and the rule this compiler emits — alwaysApply: false, a description, no globs — is exactly what Cursor's docs call a dynamic rule, the shape its own /migrate-to-skills command converts into a skill. Both land in Cursor's "Agent Decides" pool, and Cursor documents no dedup between them, so a repo with both paths offers one capability twice and pays each entry's description up front. Compile now says so, with the duplicated cost measured, and points at the [project].targets line that resolves it.
+

The rule is still emitted. Agent Skills only reached general availability in Cursor 2.4, so dropping it would silently cost anyone on an older version their only trigger — a worse outcome than a duplicated description. The tradeoff is now visible instead of buried in a source comment.

+
  • scripts/bump-tap.sh — the Homebrew formula bump, scripted. TAP_TOKEN is still unset, so the release workflow's tap job skips with a warning and the formula is bumped by hand; this runs the same steps the workflow does, so the digest is computed from the downloaded tarball rather than copied between terminals. Dry run by default, refuses a version that is not on npm yet, and is a no-op when the formula already points at that version. (An audit while writing it confirmed all eight hand-bumped releases, 0.17.0 through 0.22.0, carry a sha256 matching their real tarball.)
+

Fixed

+
  • Corrected the comment excluding cursor from the vendor-neutral dedup, which asserted the rule file was "not a second copy" of the skill. It is a second offer of the same capability through a different mechanism — the same class of confident, unverified comment that hid the Copilot and Gemini duplication until 0.22.0.
+
+

v0.22.0

- 2026-08-09latest + 2026-08-09

Fixed

  • A repo with both .agents/ and .github/ (or .gemini/) got the same skill written twice. The agents adapter's detection is narrow so the vendor-neutral path is not forced on repos that never asked for it — but that only covers the case where .agents/ is absent, not the far more common one where a repo has it and a native skills directory. The result was byte-identical SKILL.md files in both places, which the source comment beside that adapter had explicitly claimed would not happen.
diff --git a/site/index.html b/site/index.html index a31a5c0..c04680e 100644 --- a/site/index.html +++ b/site/index.html @@ -151,7 +151,7 @@ -

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

+

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

Write an agent skill once. Run it everywhere.