universal-plugin 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +28 -0
  5. package/agents/agentskills-specialist.md +6 -11
  6. package/com.github.copilot/agents/agentskills-specialist.agent.md +6 -11
  7. package/dist/cli.mjs +164 -12
  8. package/package.json +1 -1
  9. package/plugin.json +1 -1
  10. package/readme.md +1 -1
  11. package/references/plugin-design.md +19 -33
  12. package/references/slash-invocation.md +2 -2
  13. package/skills/adopt-upx/scripts/rewrite-upx.mjs +1 -1
  14. package/skills/build-plugin/SKILL.md +2 -3
  15. package/skills/build-plugin/scripts/build.mjs +1 -1
  16. package/skills/doctor-universal-plugin/README.md +1 -1
  17. package/skills/doctor-universal-plugin/SKILL.md +18 -6
  18. package/skills/doctor-universal-plugin/scripts/doctor.mjs +225 -2
  19. package/skills/init-universal-plugin/README.md +1 -1
  20. package/skills/init-universal-plugin/SKILL.md +62 -3
  21. package/skills/init-universal-plugin/references/create.md +10 -7
  22. package/skills/init-universal-plugin/references/detection.md +1 -1
  23. package/skills/init-universal-plugin/references/frontmatter.md +1 -1
  24. package/skills/init-universal-plugin/references/release-tools.md +272 -0
  25. package/skills/init-universal-plugin/references/standard.md +3 -3
  26. package/skills/init-universal-plugin/references/vendors/copilot-cli.md +3 -0
  27. package/skills/init-universal-plugin/scripts/init.mjs +1 -1
  28. package/skills/marketplace/scripts/add.mjs +1 -1
  29. package/skills/marketplace/scripts/install-docs.mjs +1 -1
  30. package/skills/marketplace/scripts/marketplace.mjs +1 -1
  31. package/skills/marketplace/scripts/update.mjs +1 -1
  32. package/skills/marketplace/scripts/validate.mjs +1 -1
  33. package/skills/migrate-plugin/SKILL.md +1 -2
  34. package/skills/publish-plugin/SKILL.md +7 -6
  35. package/skills/version/README.md +7 -3
  36. package/skills/version/SKILL.md +129 -10
  37. package/skills/version/scripts/version.mjs +1 -1
@@ -10,7 +10,7 @@ description: Deploy the application after running release checks.
10
10
  invocation-policy: user
11
11
  ---
12
12
 
13
- Deploy $ARGUMENTS.
13
+ Deploy the environment the user names in the invocation.
14
14
  ```
15
15
 
16
16
  | Policy | Meaning | Default |
@@ -22,7 +22,7 @@ Deploy $ARGUMENTS.
22
22
  `plugin build` derives vendor behavior from that policy:
23
23
 
24
24
  - Claude Code uses the same `SKILL.md` and adds its native invocation flag.
25
- - Cursor receives a thin `.cursor/commands/<skill>.md` prompt insert for `user` and `both` skills.
25
+ - Cursor uses the same `SKILL.md` and its native invocation flag; no per-skill command is derived.
26
26
  - Codex uses the skill itself. A user invokes it with `$skill` or `/skills`, so no command is derived. Codex custom prompts are deprecated and home-only, and `plugin build` never writes outside the plugin tree.
27
27
  - Copilot CLI receives no derived command. Its `/skill-name` form is only a prompt hint, not deterministic invocation.
28
28
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/rewrite-upx.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.17.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.18.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  //#region src/skill-scripts/rewrite-upx.ts
@@ -47,7 +47,7 @@ node scripts/build.mjs --root <plugin-root>
47
47
 
48
48
  Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI the project installed
49
49
  (`pnpm exec universal-plugin`), else the CLI pinned to the version this skill shipped with
50
- (`npx universal-plugin@0.17.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
50
+ (`npx universal-plugin@0.18.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
51
51
  must name the directory that holds the canonical `plugin.json`.
52
52
 
53
53
  The command never prompts, so it is safe to run unattended.
@@ -132,8 +132,7 @@ Validation runs before the first write, so a failed build leaves the tree untouc
132
132
  - **Expect a build to pin skill `npx` references or decide a version.** Build derives and copies
133
133
  whatever `plugin.json` carries. Stamping is the release-time `plugin version`, not this step.
134
134
  - **Expect a build to copy governances into skills.** A skill loads a reference through the
135
- `reference` skill in the `cyber-agentkit` plugin; the build does not copy them and has no
136
- `--check` flag.
135
+ `reference` skill in the `cyber-agentkit` plugin; the build does not copy them.
137
136
 
138
137
  ## Related skills
139
138
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.17.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.18.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -35,7 +35,7 @@ on purpose, so it folds into that command rather than competing with it.
35
35
  ## Boundaries
36
36
 
37
37
  Diagnoses the plugin a project ships. A repository's own agent wiring — `.agents/skills/`,
38
- `AGENTS.md`, per-harness bridges — is `buddy-agent-harness:doctor`.
38
+ `AGENTS.md`, per-harness bridges — is `buddy-agent-harness:doctor-buddy-agent-harness`.
39
39
 
40
40
  ## References
41
41
 
@@ -95,6 +95,14 @@ Each `code` below is what the script emits.
95
95
  | `package-path-missing` | `packagePath` names a directory with no readable `package.json` | fix `packagePath`, or create the package |
96
96
  | `package-path-unknown` | the CLI could not report `packagePath` (a version too old to read it), so `version-drift` and `unreleased-content` were skipped rather than guessed | upgrade universal-plugin |
97
97
  | `misplaced-package-path` | `plugin.json` declares `packagePath` under `extensions["org.cyberuni.universal-plugin"]`, where the CLI never reads it — the plugin is silently treated as not shipping to npm | move it to `.agents/universal-plugin.json`, relative to the plugin root |
98
+ | `changesets-no-package` | the repository is released with changesets, the plugin ships to no npm package, and its root has no `package.json` — changesets releases only a package, so `changeset version` and `changeset publish` fail | `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) — `plugin init` writes a private `package.json` |
99
+ | `changesets-private-untagged` | a non-npm plugin in a changesets repository, and `.changeset/config.json` does not set `"privatePackages": { "version": true, "tag": true }` — `changeset publish` tags no private package, so no release is made | set `privatePackages` in `.changeset/config.json` |
100
+ | `changesets-no-publish-script` | a non-npm plugin in a changesets repository, and a workflow runs `changesets/action` with no `publish-script` (`publish` in v1) — after the Version Packages PR merges the action does nothing, so no tag or GitHub release is made | add `publish-script: pnpm release` to the step, with a `release` script running `changeset publish` |
101
+ | `release-please-no-restamp` | the plugin ships to no npm package, `release-please-config.json` bumps its `plugin.json` through `extra-files`, and no workflow runs `plugin version` — release-please runs no command before its release commit, so the release PR leaves the vendor manifests, catalogs, skill pins and compiled scripts on the old version | add a workflow step on the release PR branch running `universal-plugin plugin version --from-manifest` and pushing the stamped files — `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`) has the workflow |
102
+ | `release-please-restamp-no-ci` | a workflow restamps the release PR with `plugin version --from-manifest` and pushes with the default `GITHUB_TOKEN` — that push starts no workflow run, so the PR's checks never run on its head commit and a required check blocks the merge. A `workflow_dispatch` re-run does not fix it: its checks do not count toward a required check | mint a GitHub App token with `actions/create-github-app-token` and pass it as `token:` to `release-please-action` and `actions/checkout` — `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`) has the workflow |
103
+ | `release-it-no-stamp` | a release-it config (`.release-it.*`, or the `release-it` key of `package.json`) has no hook running `plugin version` — release-it commits and tags the release with `plugin.json` and the vendor manifests still on the previous version | add `"hooks": { "after:bump": "universal-plugin plugin version ${version}" }` to the config |
104
+ | `semantic-release-no-stamp` | a semantic-release config (`.releaserc*`, `.config/releaserc*`, `release.config.*`, or the `release` key of `package.json`) has no `@semantic-release/exec` `prepareCmd` running `plugin version` — the release is tagged with `plugin.json`, the vendor manifests and the catalogs still on the previous version | add `["@semantic-release/exec", { "prepareCmd": "universal-plugin plugin version ${nextRelease.version}" }]` before `@semantic-release/git` — `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) wires it |
105
+ | `semantic-release-npm-unshipped` | the plugin ships to no npm package, and its semantic-release config lists `@semantic-release/npm` or relies on the default `plugins` list, which includes it — with no `package.json` the release fails (`ENOPKG`), and with one the plugin still rewrites its version | remove `@semantic-release/npm`, writing the `plugins` list out if the config relies on the defaults — `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) wires it |
98
106
  | `unparsable-manifest` | root `plugin.json` is not valid JSON | fix the syntax error |
99
107
  | `invalid-catalog` | a marketplace catalog — at the repository root, or at a `--marketplace-root` clone — is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` (pnpm install: `pnpm-universal-plugin-marketplace`) |
100
108
  | `marketplace-root-missing` | a `--marketplace-root` path does not exist, so it could not be checked at all | clone the marketplace repository, or fix the path |
@@ -143,7 +151,7 @@ user can run, or ask before running it yourself.
143
151
 
144
152
  ## Version drift
145
153
 
146
- A version lives in five places. Two are authored: the canonical `plugin.json`, and the `package.json`
154
+ A version lives in six places. Two are authored: the canonical `plugin.json`, and the `package.json`
147
155
  at the `packagePath` the CLI reports (`config get --key packagePath`), resolved from the plugin root.
148
156
  The rest are stamped from them by `plugin version`: the vendor manifests, this plugin's entry in each
149
157
  repository catalog, the `npx`/`upx <package>@<version>` pins in the skills, and each `mcpServers`
@@ -172,7 +180,7 @@ it does not, offer to add it — ask first, because both are the user's files:
172
180
  semantic-release bot opens.
173
181
 
174
182
  Use the locked `universal-plugin` from the repository's devDependencies when it has one; otherwise
175
- `npx universal-plugin@0.17.0`, pinned. This check matters most where the release tool picks the
183
+ `npx universal-plugin@0.18.0`, pinned. This check matters most where the release tool picks the
176
184
  number, because nothing else runs between that bump and publish.
177
185
 
178
186
  ## Unreleased content
@@ -187,9 +195,13 @@ The script compares the shipped paths — the canonical manifest, the skills dir
187
195
  emits `unreleased-content` for anything committed since. Uncommitted work is not reported; it has not
188
196
  shipped.
189
197
 
190
- Two cases are deliberately silent. A plugin that declares `packagePath` is skipped, because there the
191
- release picks the number ([ADR-0010](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0010-version-policy.md) §2) and content waiting ahead of the last released version is the
192
- normal state of a branch. A tree with no git history is skipped rather than guessed at.
198
+ Two cases are deliberately silent. A plugin whose release picks the number is skipped, because
199
+ content waiting ahead of the last released version is the normal state of a branch there
200
+ ([ADR-0010](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0010-version-policy.md) §6). That is a plugin that declares `packagePath`, or a plugin a release tool versions:
201
+ changesets (`.changeset/` and a private or absent plugin-root `package.json`), release-please (its
202
+ config bumps this `plugin.json` through `extra-files`), semantic-release (`.releaserc*`,
203
+ `release.config.*`, or a `release` key in `package.json`), or release-it (a `.release-it.*` file, or a
204
+ `release-it` key in `package.json`). A tree with no git history is skipped rather than guessed at.
193
205
 
194
206
  The repair is the bump, and it belongs to `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`). Judge first whether the change
195
207
  is meant to ship — content that is still being worked on is not a finding to act on.
@@ -203,7 +215,7 @@ is meant to ship — content that is still being worked on is not a finding to a
203
215
  directly. Its **components** are a separate question — a `copilot-root-components` finding is a real
204
216
  fault, and `canonical` is only healthy for a plugin that declares none of the moved kinds.
205
217
  - Do not treat repo-private agent configuration (`.claude/skills/`, `.agents/skills/`) as part of the
206
- plugin. Diagnosing a repository's own skill wiring is `buddy-agent-harness:doctor`.
218
+ plugin. Diagnosing a repository's own skill wiring is `buddy-agent-harness:doctor-buddy-agent-harness`.
207
219
 
208
220
  ## Related skills
209
221
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/doctor.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.17.0","entry":"skills/doctor-universal-plugin/scripts/doctor.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.18.0","entry":"skills/doctor-universal-plugin/scripts/doctor.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
@@ -51,6 +51,17 @@ const marketplaceRoots = argv.flatMap((arg, i) => arg === "--marketplace-root" ?
51
51
  const findings = [];
52
52
  /** semantic-release's version placeholder until its publish run stamps the real number. */
53
53
  const UNSTAMPED_VERSION = "0.0.0-development";
54
+ /** The config files release-it reads, besides the `release-it` key of package.json. */
55
+ const RELEASE_IT_FILES = [
56
+ "json",
57
+ "yaml",
58
+ "yml",
59
+ "toml",
60
+ "js",
61
+ "cjs",
62
+ "mjs",
63
+ "ts"
64
+ ].map((ext) => `.release-it.${ext}`);
54
65
  const add = (code, severity, detail, repair) => findings.push({
55
66
  code,
56
67
  severity,
@@ -187,7 +198,7 @@ if (packagePath !== void 0 && !packageMissing) {
187
198
  if (manifest.version !== void 0 && pkg.version !== UNSTAMPED_VERSION && pkg.version !== manifest.version) add("version-drift", "high", `plugin.json is ${manifest.version}, ${packagePath}/package.json is ${pkg.version}`, "/universal-plugin:version");
188
199
  }
189
200
  }
190
- if (manifest.version !== void 0 && packagePath === null) {
201
+ if (manifest.version !== void 0 && packagePath === null && releaseTool() === null) {
191
202
  const introduced = commitThatSetVersion(manifest.version);
192
203
  if (introduced !== null) {
193
204
  const files = (git("diff", "--name-only", `${introduced}..HEAD`, "--", ...shippedPaths()) ?? "").split("\n").filter(Boolean);
@@ -197,6 +208,10 @@ if (manifest.version !== void 0 && packagePath === null) {
197
208
  }
198
209
  }
199
210
  }
211
+ for (const finding of changesetsReleaseFindings()) add(...finding);
212
+ for (const finding of releasePleaseReleaseFindings()) add(...finding);
213
+ for (const file of releaseItConfigsWithoutStamp()) add("release-it-no-stamp", "high", `${file} configures release-it, but no hook runs plugin version — the release is tagged with plugin.json and the vendor manifests still on the previous version`, "add \"hooks\": { \"after:bump\": \"universal-plugin plugin version ${version}\" } to the release-it config");
214
+ for (const finding of semanticReleaseFindings()) add(...finding);
200
215
  const missingMarketplaceRoots = marketplaceRoots.filter((r) => !fs.existsSync(path.resolve(r)));
201
216
  for (const r of missingMarketplaceRoots) add("marketplace-root-missing", "high", `--marketplace-root ${r} does not exist`, "clone the marketplace repository, or fix the path");
202
217
  const existingMarketplaceRoots = marketplaceRoots.filter((r) => fs.existsSync(path.resolve(r)));
@@ -222,6 +237,214 @@ function invalidCatalogs(extraRoots) {
222
237
  })) : [];
223
238
  });
224
239
  }
240
+ /** The release-config gaps of a non-npm plugin in a changesets repository: no `packagePath`, and a
241
+ * plugin-root package.json that is absent or private. An npm-shipped plugin is not checked here —
242
+ * `changeset publish` there publishes to npm whatever the private-package settings say. */
243
+ function changesetsReleaseFindings() {
244
+ if (packagePath !== null) return [];
245
+ const repoRoot = git("rev-parse", "--show-toplevel") ?? root;
246
+ const workspace = changesetsWorkspace(repoRoot);
247
+ if (workspace === void 0) return [];
248
+ const pkg = readJson(path.join(root, "package.json"));
249
+ const out = [];
250
+ if (pkg === null) out.push([
251
+ "changesets-no-package",
252
+ "high",
253
+ "the repository is released with changesets, but the plugin root has no package.json — changesets releases only a package, so changeset version and changeset publish fail",
254
+ "/universal-plugin:init-universal-plugin — it writes a private package.json (name, \"private\": true, version)"
255
+ ]);
256
+ const setting = readJson(path.join(workspace, ".changeset", "config.json"))?.privatePackages;
257
+ if (!(typeof setting === "object" && setting !== null && setting.version !== false && setting.tag === true)) out.push([
258
+ "changesets-private-untagged",
259
+ "high",
260
+ ".changeset/config.json does not set \"privatePackages\": { \"version\": true, \"tag\": true } — changeset publish creates no tag for a private package, so no release is made",
261
+ "set \"privatePackages\": { \"version\": true, \"tag\": true } in .changeset/config.json"
262
+ ]);
263
+ for (const workflow of changesetsWorkflowsWithoutPublish(repoRoot)) out.push([
264
+ "changesets-no-publish-script",
265
+ "high",
266
+ `${workflow} runs changesets/action with no publish script — after the Version Packages PR merges it does nothing, so no tag or release is made`,
267
+ "add `publish-script: pnpm release` (changesets/action v1: `publish:`) to the changesets/action step, with a \"release\" script running changeset publish"
268
+ ]);
269
+ return out;
270
+ }
271
+ /** The directory holding `.changeset/` for a non-npm plugin a changesets repository releases: the
272
+ * plugin root or the repository root, with a plugin-root package.json that is absent or private.
273
+ * `undefined` when changesets does not release this plugin as a non-npm one. */
274
+ function changesetsWorkspace(repoRoot) {
275
+ const workspace = [root, repoRoot].find((dir) => fs.existsSync(path.join(dir, ".changeset")));
276
+ if (workspace === void 0) return void 0;
277
+ const pkg = readJson(path.join(root, "package.json"));
278
+ return pkg !== null && pkg.private !== true ? void 0 : workspace;
279
+ }
280
+ /** The release tool that picks a non-npm plugin's next version, or `null` when the author picks it
281
+ * (ADR-0010 §6). Each tool moves the number at release time, from what accumulated since the last
282
+ * release: changesets from changeset files, release-please and semantic-release from commits,
283
+ * release-it from its prompt or increment argument. Config is looked for where each tool loads it:
284
+ * the plugin root and the repository root. */
285
+ function releaseTool() {
286
+ const repoRoot = git("rev-parse", "--show-toplevel") ?? root;
287
+ if (changesetsWorkspace(repoRoot) !== void 0) return "changesets";
288
+ const rpConfig = readJson(path.join(repoRoot, "release-please-config.json"));
289
+ if (rpConfig !== null && releasePleaseBumpsManifest(rpConfig, repoRoot)) return "release-please";
290
+ const dirs = [.../* @__PURE__ */ new Set([root, repoRoot])];
291
+ if (dirs.some((dir) => semanticReleaseConfig(dir) !== null)) return "semantic-release";
292
+ if (dirs.some((dir) => hasConfig(dir, RELEASE_IT_FILES, "release-it"))) return "release-it";
293
+ return null;
294
+ }
295
+ /** Whether `dir` holds one of the config files `files`, or a package.json carrying `key`. */
296
+ function hasConfig(dir, files, key) {
297
+ if (files.some((file) => fs.existsSync(path.join(dir, file)))) return true;
298
+ const pkg = readJson(path.join(dir, "package.json"));
299
+ return pkg !== null && typeof pkg === "object" && Object.hasOwn(pkg, key);
300
+ }
301
+ /** The release-please gap of a non-npm plugin: `release-please-config.json` bumps this plugin's
302
+ * plugin.json through `extra-files`, and no workflow runs `plugin version` to restamp the rest, or
303
+ * the restamp is pushed with the default `GITHUB_TOKEN` and so gets no checks. A plugin with `packagePath` is not checked: its number comes from the package, through
304
+ * `--from-package`. */
305
+ function releasePleaseReleaseFindings() {
306
+ if (packagePath !== null) return [];
307
+ const repoRoot = git("rev-parse", "--show-toplevel") ?? root;
308
+ const config = readJson(path.join(repoRoot, "release-please-config.json"));
309
+ if (config === null || !releasePleaseBumpsManifest(config, repoRoot)) return [];
310
+ if (!workflowsMention(repoRoot, /plugin version|version\.mjs/)) return [[
311
+ "release-please-no-restamp",
312
+ "high",
313
+ "release-please-config.json bumps plugin.json through extra-files, but no workflow runs plugin version — the release PR leaves the vendor manifests, catalogs, skill pins and compiled scripts on the old version",
314
+ "add a workflow step on the release PR branch that runs `universal-plugin plugin version --from-manifest` and pushes the stamped files (see the version skill)"
315
+ ]];
316
+ return restampWorkflowsWithoutCi(repoRoot).map((workflow) => [
317
+ "release-please-restamp-no-ci",
318
+ "medium",
319
+ `${workflow} pushes the --from-manifest restamp with the default GITHUB_TOKEN — that push starts no workflow run, so the release PR's checks never run on its head commit and a required check blocks the merge`,
320
+ "mint a GitHub App token with actions/create-github-app-token and pass it to release-please-action and actions/checkout as `token:` (see the version skill)"
321
+ ]);
322
+ }
323
+ /** Workflow files that restamp with `--from-manifest`, push, and leave the push on the default
324
+ * `GITHUB_TOKEN`, which starts no workflow run. Read as text, so it clears a file on any sign of
325
+ * another credential: a GitHub App token, a deploy key, or a `token:` input that is not the default
326
+ * token. A workflow dispatch does not clear it: its checks do not count toward a required check. */
327
+ function restampWorkflowsWithoutCi(repoRoot) {
328
+ const startsCi = /create-github-app-token|ssh-key:|token:\s*\$\{\{(?!\s*(?:secrets\.GITHUB_TOKEN|github\.token)\b)/;
329
+ return workflowFiles(repoRoot).filter(({ text }) => /--from-manifest/.test(text) && /git push/.test(text) && !startsCi.test(text)).map(({ file }) => file);
330
+ }
331
+ /** Whether a release-please config lists this plugin's plugin.json in `extra-files`. A package's own
332
+ * `extra-files` replaces the top-level default. A path is relative to the package path, or to the
333
+ * repository root with a leading `/`. */
334
+ function releasePleaseBumpsManifest(config, repoRoot) {
335
+ const target = path.join(root, "plugin.json");
336
+ const packages = config.packages ?? {};
337
+ return Object.entries(packages).some(([packageDir, options]) => {
338
+ const extra = options?.["extra-files"] ?? config["extra-files"];
339
+ if (!Array.isArray(extra)) return false;
340
+ return extra.some((entry) => {
341
+ const file = typeof entry === "string" ? entry : entry?.path;
342
+ if (typeof file !== "string") return false;
343
+ return (file.startsWith("/") ? path.join(repoRoot, file) : path.join(repoRoot, packageDir, file)) === target;
344
+ });
345
+ });
346
+ }
347
+ /** The semantic-release wiring gaps, for the config at the plugin root and at the repository root.
348
+ * Each config is read as text, so a JavaScript config is checked only for what it names. A config
349
+ * that `extends` a shareable config is skipped: the wiring may live in the shared one. */
350
+ function semanticReleaseFindings() {
351
+ const repoRoot = git("rev-parse", "--show-toplevel") ?? root;
352
+ return [.../* @__PURE__ */ new Set([root, repoRoot])].flatMap((dir) => {
353
+ const config = semanticReleaseConfig(dir);
354
+ if (config === null || /\bextends\b/.test(config.text)) return [];
355
+ const label = path.relative(root, path.join(dir, config.file)) || config.file;
356
+ const out = [];
357
+ if (!/\bprepareCmd\b['"]?\s*:\s*(?:[>|][-+]?\s*\n\s*)?[^,\n}]*(?:\bplugin\s+version\b|version\.mjs)/.test(config.text)) out.push([
358
+ "semantic-release-no-stamp",
359
+ "high",
360
+ `${label} configures semantic-release, but no @semantic-release/exec prepareCmd runs plugin version — the release is tagged with plugin.json, the vendor manifests and the catalogs still on the previous version`,
361
+ "add [\"@semantic-release/exec\", { \"prepareCmd\": \"universal-plugin plugin version ${nextRelease.version}\" }] before @semantic-release/git — /universal-plugin:init-universal-plugin wires it"
362
+ ]);
363
+ const runsNpm = /@semantic-release\/npm(?![\w-])/.test(config.text) || !/\bplugins\b/.test(config.text);
364
+ if (packagePath === null && runsNpm) out.push([
365
+ "semantic-release-npm-unshipped",
366
+ "high",
367
+ `${label} runs @semantic-release/npm, but the plugin ships to no npm package — with no package.json the release fails (ENOPKG), and with one it still rewrites that version`,
368
+ "remove @semantic-release/npm from the plugins list (write the list out if the config relies on the defaults) — /universal-plugin:init-universal-plugin wires it"
369
+ ]);
370
+ return out;
371
+ });
372
+ }
373
+ /** The semantic-release config semantic-release would read in `dir`, as text, or `null` when there
374
+ * is none. The search order is cosmiconfig 9's default for the module name `release`: the `release`
375
+ * key of package.json, then the files below. The first one found is the only one read. */
376
+ function semanticReleaseConfig(dir) {
377
+ const rc = [
378
+ "",
379
+ ".json",
380
+ ".yaml",
381
+ ".yml",
382
+ ".js",
383
+ ".ts",
384
+ ".cjs",
385
+ ".mjs"
386
+ ];
387
+ const files = [
388
+ ...rc.map((ext) => `.releaserc${ext}`),
389
+ ...rc.map((ext) => `.config/releaserc${ext}`),
390
+ ...[
391
+ ".js",
392
+ ".ts",
393
+ ".cjs",
394
+ ".mjs"
395
+ ].map((ext) => `release.config${ext}`)
396
+ ];
397
+ const fromPackage = readJson(path.join(dir, "package.json"))?.release;
398
+ if (fromPackage !== void 0 && fromPackage !== null) return {
399
+ file: "package.json",
400
+ text: JSON.stringify(fromPackage, null, 2)
401
+ };
402
+ for (const file of files) {
403
+ const abs = path.join(dir, file);
404
+ if (fs.existsSync(abs) && fs.statSync(abs).isFile()) return {
405
+ file,
406
+ text: fs.readFileSync(abs, "utf8")
407
+ };
408
+ }
409
+ return null;
410
+ }
411
+ /** Whether any workflow file's text matches. */
412
+ function workflowsMention(repoRoot, pattern) {
413
+ return workflowFiles(repoRoot).some(({ text }) => pattern.test(text));
414
+ }
415
+ /** The workflow files under `.github/workflows`, each with its path from the repository root and its text. */
416
+ function workflowFiles(repoRoot) {
417
+ const dir = path.join(repoRoot, ".github", "workflows");
418
+ if (!fs.existsSync(dir)) return [];
419
+ return fs.readdirSync(dir).filter((file) => /\.ya?ml$/.test(file)).map((file) => ({
420
+ file: `.github/workflows/${file}`,
421
+ text: fs.readFileSync(path.join(dir, file), "utf8")
422
+ }));
423
+ }
424
+ /** Workflow files that run changesets/action and give it no publish command. Read as text: a step
425
+ * names it as `publish-script:` (v2) or `publish:` (v1). */
426
+ function changesetsWorkflowsWithoutPublish(repoRoot) {
427
+ const dir = path.join(repoRoot, ".github", "workflows");
428
+ if (!fs.existsSync(dir)) return [];
429
+ return fs.readdirSync(dir).filter((file) => /\.ya?ml$/.test(file)).filter((file) => {
430
+ const text = fs.readFileSync(path.join(dir, file), "utf8");
431
+ return /uses:\s*['"]?changesets\/action@/.test(text) && !/^\s*(publish-script|publish)\s*:/m.test(text);
432
+ }).map((file) => `.github/workflows/${file}`);
433
+ }
434
+ /** release-it configs, at the plugin root or the repository root, that never call `plugin version`.
435
+ * release-it reads `.release-it.{json,yaml,yml,toml,js,cjs,mjs,ts}` or the `release-it` key of
436
+ * package.json. Each is read as text, so the check is only that the stamp is named somewhere. */
437
+ function releaseItConfigsWithoutStamp() {
438
+ const repoRoot = git("rev-parse", "--show-toplevel") ?? root;
439
+ const stamps = (text) => /\bplugin\s+version\b/.test(text);
440
+ return [.../* @__PURE__ */ new Set([root, repoRoot])].flatMap((dir) => {
441
+ const label = (file) => path.relative(root, path.join(dir, file)) || file;
442
+ const found = RELEASE_IT_FILES.filter((file) => fs.existsSync(path.join(dir, file))).filter((file) => !stamps(fs.readFileSync(path.join(dir, file), "utf8"))).map(label);
443
+ const config = readJson(path.join(dir, "package.json"))?.["release-it"];
444
+ if (config !== void 0 && !stamps(JSON.stringify(config))) found.push(label("package.json"));
445
+ return found;
446
+ });
447
+ }
225
448
  /** Runs git inside `root`, returning its stdout or `null` — a non-zero status, a missing git, and a
226
449
  * directory outside any repository are all the same answer here: no history to read. */
227
450
  function git(...args) {
@@ -47,7 +47,7 @@ hold everywhere belongs in the Markdown body. See `references/frontmatter.md`, w
47
47
  ## Boundaries
48
48
 
49
49
  Plugin authoring only. Setting a repository up to *consume* skills — `AGENTS.md`, the
50
- `.agents/skills/` layout, per-harness bridges — is `buddy-agent-harness:init`. This skill does not
50
+ `.agents/skills/` layout, per-harness bridges — is `buddy-agent-harness:init-buddy-agent-harness`. This skill does not
51
51
  touch CI, repository settings, or unrelated project files.
52
52
 
53
53
  ## References
@@ -34,7 +34,7 @@ runtime you are enabling, and only those.
34
34
 
35
35
  This skill owns the **authoring** side: the plugin a project ships. Setting a repository up to
36
36
  *consume* skills — the `.agents/skills/` layout, `AGENTS.md`, per-harness bridges — is
37
- `buddy-agent-harness:init`, not this.
37
+ `buddy-agent-harness:init-buddy-agent-harness`, not this.
38
38
 
39
39
  ## Prerequisites
40
40
 
@@ -70,6 +70,9 @@ covers moving one that sits in the wrong place.
70
70
 
71
71
  Inventory both the canonical surface (root `plugin.json`, its `extensions` block, the component
72
72
  directories it names) and every pre-existing vendor artifact listed in `references/detection.md`.
73
+ Note the release tool the repository uses, if any: `.changeset/` (changesets), `.releaserc*`,
74
+ `release.config.*`, or a `release` key in `package.json` (semantic-release), `.release-it.*` or a
75
+ `release-it` key in `package.json` (release-it), or `release-please-config.json` (release-please).
73
76
  Read what you find. Write nothing yet.
74
77
 
75
78
  ## 2. Classify
@@ -126,9 +129,63 @@ plan, and ask before running init: it rewrites the user's root `package.json`. I
126
129
  run init without `--npm` and wire `files` by hand. A custom `version` script is never rewritten;
127
130
  init prints the line to append, so relay it and let the user place it.
128
131
 
132
+ A plugin that ships to **no** npm package in a changesets repository gets its own plan line.
133
+ Changesets releases only a package, so when the repository root carries `.changeset/` and the plugin
134
+ root has no `package.json`, `plugin init` writes a minimal private one: `name` from the plugin name,
135
+ `"private": true`, and `version` from the `plugin.json` it replaces (else `0.0.0`), with no `files`,
136
+ `main`, or `exports`. It also wires the root `version` script as above and adds
137
+ `"release": "changeset publish"`, which tags the release for changesets/action. Offer this in the
138
+ plan and show the file; if the user declines, add `--no-changesets` to the init call. Relay init's
139
+ notes: a plugin below the repository root must be a workspace package, and `.changeset/config.json`
140
+ needs `"privatePackages": { "version": true, "tag": true }` before `changeset publish` tags anything.
141
+
142
+ A plugin released with **semantic-release** gets the release wiring as a plan line too.
143
+ `plugin init` writes nothing for it, so the skill edits the config itself, once the user approves.
144
+ Add `@semantic-release/exec` with
145
+ `prepareCmd: "universal-plugin plugin version ${nextRelease.version}"`, and add
146
+ `@semantic-release/git` with the stamped files as `assets`. For a plugin that ships to no npm
147
+ package, also drop `@semantic-release/npm`: `npmPublish: false` is not enough, because the plugin
148
+ still edits `package.json`. Show the `plugins` list before and after, and edit the config file the
149
+ survey found, in its own format. [`references/release-tools.md`](./references/release-tools.md) has
150
+ the config, the plugin order, the full `assets` list, and the packages the release job must
151
+ install. If the user declines, leave the config alone.
152
+
153
+ A plugin released with **release-it** gets its wiring as a plan line as well. Add an `after:bump`
154
+ hook running `universal-plugin plugin version ${version}`, after any hook the user already has. For a
155
+ plugin that ships to no npm package, also set `"npm": false`, and run the hook through the pinned
156
+ `npx universal-plugin@0.18.0`. release-it then reads the current number from the latest git tag,
157
+ so when `git describe --tags` finds none, propose tagging the current commit with the version
158
+ `plugin.json` holds (`git tag v1.2.0`). Without that tag the first release starts from `0.0.0` and
159
+ fails. Create the tag only once the user approves, and leave pushing it to the user.
160
+
161
+ A plugin released with **release-please** gets its wiring as a plan line too. For a plugin that
162
+ ships to no npm package, write `release-please-config.json` with `release-type: simple` and an
163
+ `extra-files` entry bumping `plugin.json`. Put the version `plugin.json` holds in
164
+ `.release-please-manifest.json`, so the first release PR starts from it. The restamp runs in a
165
+ workflow step on the release PR and pushes with a GitHub App token, so the PR's checks run. Show that
166
+ step and the App setup, and hand both to the user.
167
+
168
+ [`references/release-tools.md`](./references/release-tools.md) has the configs for release-it and
169
+ release-please, the npm-shipped variants, and the tag and manifest checks. Show each file before and
170
+ after. If the user declines, leave the config alone.
171
+
172
+ When the survey found **no** release tool, ask the user how the plugin will be released before
173
+ running init. A new repository may simply not have set one up yet:
174
+
175
+ - **changesets.** Set up `.changeset/` first (`buddy-changesets:init-changesets` when it is
176
+ installed, else `npx @changesets/cli init`), then run init. It then finds `.changeset/` and writes
177
+ the private `package.json` above.
178
+ - **semantic-release.** Propose a new `.releaserc.json` carrying the wiring above, and write it once
179
+ the user approves. No `package.json` is needed.
180
+ - **release-it.** Propose a new `.release-it.json` carrying the wiring above, with the initial tag
181
+ for a plugin that ships to no npm package. Write it once the user approves.
182
+ - **release-please.** Propose `release-please-config.json` and `.release-please-manifest.json` as
183
+ above, and hand over the workflow step. Write them once the user approves.
184
+ - **None.** Run init as planned. The author picks each number with `version`.
185
+
129
186
  ## 4. Apply
130
187
 
131
- Route by what Phase 2 found. Read exactly the reference for the work at hand — do not load all six.
188
+ Route by what Phase 2 found. Read exactly the reference for the work at hand — do not load them all.
132
189
 
133
190
  | What Phase 2 found | Do this | Reference |
134
191
  | --- | --- | --- |
@@ -227,6 +284,8 @@ This skill is not a formatter. If the project has one, run it over the written f
227
284
  - **Offer adoption once.** If the user declines, or asked for something unrelated, drop it and do
228
285
  what they asked.
229
286
  - Plugin authoring only. Do not change CI workflows, repository settings, or unrelated project files.
287
+ The release tool's config is the one file outside the plugin this skill edits, and only once the
288
+ user approves it in Phase 3. The one git write is release-it's initial tag, on the same approval.
230
289
 
231
290
  ## Related skills
232
291
 
@@ -240,7 +299,7 @@ This skill is not a formatter. If the project has one, run it over the written f
240
299
  | Publish a packaged plugin to the marketplace | `publish-plugin` |
241
300
  | Bump the pinned `universal-plugin@<version>` the project *calls* (not the plugin's own version) | `upgrade-plugin` |
242
301
  | Rewrite `npx` pins to the `upx` runner | `adopt-upx` |
243
- | Set a repository up to *consume* skills (`AGENTS.md`, `.agents/skills/`) | `buddy-agent-harness:init` |
302
+ | Set a repository up to *consume* skills (`AGENTS.md`, `.agents/skills/`) | `buddy-agent-harness:init-buddy-agent-harness` |
244
303
 
245
304
  ## References
246
305
 
@@ -34,7 +34,7 @@ entry in both `vendors` and `harnesses` under
34
34
 
35
35
  Infer from context; ask only if ambiguous. [`standard.md`](./standard.md) has the component table and
36
36
  the layout they go in; the `plugin-design` reference decides which component a given need calls
37
- for. The universal minimum is `skills/<name>/SKILL.md` plus `.mcp.json`.
37
+ for. The universal minimum is `skills/<name>/SKILL.md` plus `mcp.json`.
38
38
 
39
39
  ## Step 4 — Scaffold
40
40
 
@@ -43,10 +43,13 @@ node scripts/init.mjs --name <plugin-name> --vendor claude-code --vendor cursor
43
43
  ```
44
44
 
45
45
  Resolve `scripts/init.mjs` against this skill's directory; it runs the project's installed CLI, else
46
- the pinned `npx universal-plugin@0.17.0`. Add `--npm` when an npm package ships the plugin,
46
+ the pinned `npx universal-plugin@0.18.0`. Add `--npm` when an npm package ships the plugin,
47
47
  which also wires that `package.json`'s `files` to carry the derived manifests, copies its `version`
48
48
  into the manifest, and, in a changesets repository, wires the root `version` script to run
49
49
  `plugin version --from-package` (SKILL.md Phase 3 asks before that rewrite).
50
+ Without `--npm`, in a changesets repository with no `package.json` at the plugin root, init writes a
51
+ private `package.json` so changesets can release the plugin (SKILL.md Phase 3 offers it; pass
52
+ `--no-changesets` when the user declines).
50
53
 
51
54
  `init` writes a minimal manifest — `$schema`, `name`, the vendor list, and the package `version`
52
55
  under `--npm`. Fill in the Step 1
@@ -83,10 +86,10 @@ delivery path at all.
83
86
  Run the mechanical validator from the aced **improve-skill** skill:
84
87
 
85
88
  ```bash
86
- node "<path to aced improve-skill>/scripts/validate.mts" --path skills/<skill-name>
89
+ node "<path to aced improve-skill>/scripts/validate.mjs" --path skills/<skill-name>
87
90
  ```
88
91
 
89
- Fix every CRITICAL finding, then invoke the **audit-skill** skill for the full review.
92
+ Fix every CRITICAL finding, then invoke the `aced:improve-skill` skill for the full review.
90
93
  [`frontmatter.md`](./frontmatter.md) covers what has to hold across runtimes.
91
94
 
92
95
  ## Step 7 — Build
@@ -98,8 +101,8 @@ npx universal-plugin plugin build
98
101
  Useful flags: `--dry-run` to see the plan, `--verbose` for field-by-field decisions, `--vendor <id>`
99
102
  for one target, `--clean` to delete derived manifests first.
100
103
 
101
- Read the warnings. An unknown vendor id, an undeliverable Copilot override, and a failed Codex prompt
102
- write all surface there rather than as errors.
104
+ Read the warnings. An unknown vendor id and an undeliverable Copilot override both surface there
105
+ rather than as errors.
103
106
 
104
107
  ## Step 8 — Install locally to test
105
108
 
@@ -113,7 +116,7 @@ restart for Claude Code, **Developer: Reload Window** for Cursor. `--list` shows
113
116
  without writing; `--vendor <id>` narrows it; `plugin uninstall` removes it again.
114
117
 
115
118
  Codex and Copilot CLI scan no local plugin directory, so they report as `unsupported`. Reach those
116
- through a repository-local marketplace — `publish-plugin`.
119
+ through a repository-local marketplace — `marketplace`.
117
120
 
118
121
  Do not hand-write a symlink for this. The recipe that circulated for it named
119
122
  `~/.claude/plugins/local/`, which does not exist, and a symlink into Cursor's local directory is
@@ -40,7 +40,7 @@ Only a skill the project *distributes* belongs to a plugin.
40
40
  | `.claude/skills/`, `.agents/skills/`, `.cursor/rules/` | **No** — repo-private tooling |
41
41
 
42
42
  If the only skills are in private locations, say nothing about adoption. A repository that configures
43
- its own agents is not a plugin waiting to happen; `buddy-agent-harness:init` is the skill for that
43
+ its own agents is not a plugin waiting to happen; `buddy-agent-harness:init-buddy-agent-harness` is the skill for that
44
44
  side of the line.
45
45
 
46
46
  ## Making the adoption offer
@@ -30,7 +30,7 @@ universal-plugin reads `invocation-policy` from each `SKILL.md` and projects it
30
30
  | --- | --- | --- |
31
31
  | `both` (default) | user- and model-invocable | nothing |
32
32
  | `user` | explicit invocation only | writes `disable-model-invocation: true` into the skill's frontmatter |
33
- | `model` | model-invocable only, not user-facing | writes `user-invocable: false`, and emits no Codex prompt |
33
+ | `model` | model-invocable only, not user-facing | writes `user-invocable: false` |
34
34
 
35
35
  Two consequences worth knowing before you run a build:
36
36