@ssheleg/make-skill 0.23.5 → 0.25.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.25.0 — the installer refuses the shadow it documents, loudly
4
+
5
+ - **The family audit of 2026-08-29 reproduced the shadow live:** a bare
6
+ `npx @ssheleg/telegram-dev` created three plain copies in `~/.claude/skills/` while the
7
+ telegram-dev plugin was enabled. Every member's bin installer had the same hole, and every
8
+ member's CI tested only a fresh `HOME`, so the plugin-present case had never run anywhere.
9
+ This is the standards repository, so the canon lands here first and its own installer is
10
+ the reference implementation.
11
+ - **This repository's own check was the fail-open class, and the new canon names it.** Both
12
+ installers detected the plugin by the `~/.claude/plugins/marketplaces/make-skill`
13
+ directory alone — which under-reports (a `directory`-sourced marketplace has no dir
14
+ there; plugin names differ from marketplace names) — and the refusal exited **0**, which
15
+ reads as success to every script above it. `distribution.md` gains "The installer must
16
+ refuse the shadow it documents": detect from the TARGET home's
17
+ `installed_plugins.json` (keys are `<name>@<marketplace>`), refuse with the remedy and a
18
+ non-zero exit, offer `--force` as the recorded deliberate choice, fail open on a missing
19
+ or corrupt JSON, and gate only the `~/.claude` write — no other agent has plugins.
20
+ - **`bin/make-skill.js` and `install.sh` implement it:** exit `3` on refusal, the remedy
21
+ names the spec read from the JSON (`claude plugin update make-skill@<marketplace>`) plus
22
+ the family launcher, `--force` overrides, absence/corruption of the JSON installs as
23
+ before, and the marketplaces-dir read is kept as the fallback signal. The last line of a
24
+ successful install now says how the next version arrives (the v0.24.0 canon, applied to
25
+ this repo's own CLI).
26
+ - **`test/installer_test.js` joins `npm test` and CI** — 11 cases against throwaway HOMEs:
27
+ fresh / rerun-skip / `--force` / unknown-arg, plugin-present refusal (exit code, remedy
28
+ text, nothing written), a differently-named marketplace in the spec, corrupt JSON failing
29
+ open, no false refusal on other plugins or a `make-skill-extra` prefix-collider, the
30
+ marketplaces-dir fallback, and the same matrix for `install.sh`. Watched failing before
31
+ trusted: **6 of 11 red** against the pre-fix installers (`git stash` the two, run, pop).
32
+ The suite follows the house residue rule — a failing case keeps its HOME, and the run
33
+ ends by saying what it left.
34
+
35
+ ## v0.24.0 — the plugin surface as it is now, and how updates reach a machine
36
+
37
+ - **Four plugin features that landed after this skill last read the docs.** Re-read
38
+ 2026-08-28 against Claude Code 2.1.236; the previous pass was 2026-07-30 against 2.1.212.
39
+ `archive` sources install a zip over HTTPS with no git and no npm (2.1.224); `command`
40
+ sources let a local command print the plugin directory (2.1.229) and are the one class an
41
+ organisation can switch off wholesale with `disableCommandPluginSources`; entry-level
42
+ `headersHelper` mints registry headers and **requires `strict: false`** (2.1.238); a
43
+ `skills` path of `"."` scans the plugin root instead of `skills/` (2.1.221). Bare-name
44
+ sources under `metadata.pluginRoot` are in too (2.1.239).
45
+ - The `headersHelper` contract is written out because every clause of it is a way to fail
46
+ silently — ten seconds, 500 ASCII characters, stdout JSON — and because Claude Code
47
+ **strips every environment variable whose name contains `TOKEN`, `SECRET`, `KEY` or
48
+ `AUTH`** before running it. A helper that reads its credential from one of those is handed
49
+ nothing, and the failure surfaces as a permissions error at the registry.
50
+ - **`distribution.md` gains "How updates reach the machine — decide it, then SAY it".** An
51
+ installer that never mentions updates has still chosen a model, and the model is *never*.
52
+ Claude Code carries a per-marketplace `autoUpdate` flag in `known_marketplaces.json` that
53
+ is **not in the documented settings surface**; `/plugin` writes it and third-party
54
+ installers write it directly. Measured on one machine 2026-08-28: of 20 marketplaces, the
55
+ 2 installed by `vercel-labs/plugins` carry it and 18 do not, because
56
+ `claude plugin marketplace add` never sets it.
57
+ - The section states the trap rather than a preference: for packs that **compose**,
58
+ per-marketplace auto-update moves each on its own clock and produces a combination nobody
59
+ tested — the same reason a family launcher refuses a per-member argument. For packs that
60
+ stand alone it is the better answer. Either way the installer's last line says how the
61
+ next version arrives, because "Installed" is not a complete sentence.
62
+
3
63
  ## v0.23.5 — the channel that sends the installs, on npm too
4
64
 
5
65
  - The `skills.sh` badge and the canonical `homepage` reached GitHub in the previous cycle and stopped
package/bin/make-skill.js CHANGED
@@ -18,6 +18,44 @@ const os = require('os');
18
18
  const ROOT = path.resolve(__dirname, '..');
19
19
  const REPO = 'ssheleg/make-skill';
20
20
 
21
+ // Exit codes are the contract: 0 installed or skipped, 1 corrupted package,
22
+ // 2 usage error, 3 refused — the plugin channel owns this agent (--force overrides).
23
+ const EXIT_PLUGIN_PRESENT = 3;
24
+
25
+ /**
26
+ * The plugin spec (`<name>@<marketplace>`) installed for `name` in this home,
27
+ * or null.
28
+ *
29
+ * `installed_plugins.json` is the record of what is actually installed. The
30
+ * `plugins/marketplaces/<name>` directory — the only thing this installer read
31
+ * until v0.25.0 — under-reports: a marketplace added from a local `directory`
32
+ * source has no dir there at all, and plugin names differ from marketplace
33
+ * names, so a check keyed on it stays green while the shadow lands. Absence
34
+ * and corruption both read as "no plugin": the fresh HOME is the common case,
35
+ * and an installer that crashes on a parse error refuses the machines that
36
+ * need it most.
37
+ */
38
+ function installedPluginSpec(home, name) {
39
+ try {
40
+ const raw = fs.readFileSync(
41
+ path.join(home, '.claude', 'plugins', 'installed_plugins.json'), 'utf8');
42
+ const parsed = JSON.parse(raw);
43
+ const plugins =
44
+ parsed && typeof parsed === 'object' &&
45
+ parsed.plugins && typeof parsed.plugins === 'object'
46
+ ? parsed.plugins
47
+ : parsed;
48
+ if (!plugins || typeof plugins !== 'object') return null;
49
+ for (const spec of Object.keys(plugins)) {
50
+ if (spec === name) return `${name}@${name}`;
51
+ if (spec.startsWith(name + '@')) return spec;
52
+ }
53
+ } catch {
54
+ // missing or corrupt = no plugin — fail open on absence, never crash
55
+ }
56
+ return null;
57
+ }
58
+
21
59
  function version() {
22
60
  try {
23
61
  return require(path.join(ROOT, 'package.json')).version;
@@ -35,6 +73,12 @@ Usage:
35
73
  npx @ssheleg/make-skill --version
36
74
  npx @ssheleg/make-skill --help
37
75
 
76
+ Exit codes:
77
+ 0 installed or skipped 2 usage error
78
+ 1 corrupted package 3 refused: the make-skill PLUGIN is installed in
79
+ this home — a plain copy would shadow it (pass
80
+ --force to write it anyway)
81
+
38
82
  Other install paths:
39
83
  Claude Code plugin: /plugin marketplace add ${REPO}
40
84
  /plugin install make-skill@make-skill
@@ -123,24 +167,39 @@ function main(argv) {
123
167
 
124
168
  // One channel per agent. A plain ~/.claude/skills/make-skill beside an
125
169
  // installed plugin is two listings of the same skill, and the stale copy wins
126
- // — the exact trap this canon documents. Refuse rather than create it.
170
+ // — the exact shadow this canon forbids. Refuse rather than create it, and
171
+ // refuse LOUDLY: until v0.25.0 this check keyed on the marketplaces/ dir
172
+ // alone and exited 0 — the fail-open class. A directory-sourced marketplace
173
+ // has no dir there, plugin names differ from marketplace names, and a refusal
174
+ // that exits 0 reads as success to every script above it. Reproduced live
175
+ // 2026-08-29: a bare `npx @ssheleg/telegram-dev` shipped three shadows past
176
+ // exactly this hole while the plugin was enabled.
177
+ const spec = installedPluginSpec(home, 'make-skill');
127
178
  const marketplace = path.join(home, '.claude', 'plugins', 'marketplaces', 'make-skill');
128
- if (fs.existsSync(marketplace) && !force) {
129
- console.log(
130
- 'skip: make-skill is already installed as a Claude Code plugin\n' +
131
- ` (${marketplace}).\n` +
132
- ' Installing a plain copy into ~/.claude/skills would shadow it, and the\n' +
133
- ' stale copy is the one that wins. Update the plugin instead:\n' +
134
- ' claude plugin marketplace update make-skill\n' +
135
- ' claude plugin update make-skill@make-skill\n' +
136
- ' Pass --force if you really want both.'
179
+ const viaMarketplaceDir = !spec && fs.existsSync(marketplace);
180
+ if ((spec || viaMarketplaceDir) && !force) {
181
+ const found = spec
182
+ ? `installed as the Claude Code plugin ${spec}\n` +
183
+ ' (declared in ~/.claude/plugins/installed_plugins.json)'
184
+ : `registered as a Claude Code marketplace\n (${marketplace})`;
185
+ console.error(
186
+ `refused: make-skill is already ${found}.\n` +
187
+ ' A plain copy in ~/.claude/skills/make-skill would shadow the plugin\n' +
188
+ ' and serve this frozen version forever. Update the plugin channel\n' +
189
+ ' instead:\n' +
190
+ ' claude plugin marketplace update make-skill\n' +
191
+ ` claude plugin update ${spec || 'make-skill@make-skill'}\n` +
192
+ ' Family launcher (updates every member, prunes shadow copies):\n' +
193
+ ' npx --yes sshlg-skills@latest update\n' +
194
+ ' Pass --force to write the plain copy anyway — a deliberate choice\n' +
195
+ ' to run two channels, where the stale one wins.'
137
196
  );
138
197
  // Offered here too. The skill IS present on this machine — as the plugin —
139
198
  // so the routing block is exactly as wanted as on the install path. Two
140
199
  // doors into one install that behave differently is how a feature comes to
141
200
  // exist for half its users.
142
201
  offerRouters();
143
- return 0;
202
+ return EXIT_PLUGIN_PRESENT;
144
203
  }
145
204
 
146
205
  installOne(
@@ -151,6 +210,15 @@ function main(argv) {
151
210
  force
152
211
  );
153
212
  offerRouters();
213
+ // The last line says how the next version arrives — "Installed" is not a
214
+ // complete sentence. Auto-update is off on purpose: this member composes
215
+ // with its family, and per-marketplace autoUpdate moves each member on its
216
+ // own clock, into combinations nobody tested together.
217
+ console.log(
218
+ '\nUpdates: rerun `npx @ssheleg/make-skill@latest --force`, or refresh the\n' +
219
+ 'whole family with `npx --yes sshlg-skills@latest update` (every channel,\n' +
220
+ 'and it prunes plain copies that would shadow a plugin).'
221
+ );
154
222
  return 0;
155
223
  }
156
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/make-skill",
3
- "version": "0.23.5",
3
+ "version": "0.25.0",
4
4
  "description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way — conformance to the Agent Skills open standard AND Anthropic's platform rules (front-matter limits, disclosure budgets, per-surface runtime limits, the Skills API, evals) plus the Claude Code plugin reference (manifest schemas, component layout, claude plugin validate --strict), marketplace repo layout, version sync, validator + CI, multi-channel distribution (plugin, vercel skills CLI, npx, Cursor), npm gotchas, the review checklist for third-party skills, and MCP / A2A rules for protocol-connected skills. This package is the installer CLI.",
5
5
  "keywords": [
6
6
  "skill",
@@ -45,6 +45,6 @@
45
45
  "node": ">=16"
46
46
  },
47
47
  "scripts": {
48
- "test": "python3 test/validate.py && python3 test/plant_guard_test.py && python3 test/checker_parity_test.py && python3 test/residue_test.py"
48
+ "test": "python3 test/validate.py && python3 test/plant_guard_test.py && python3 test/checker_parity_test.py && python3 test/residue_test.py && node test/installer_test.js"
49
49
  }
50
50
  }
@@ -3,7 +3,7 @@
3
3
  "name": "make-skill",
4
4
  "displayName": "Make Skill",
5
5
  "description": "Create, retrofit, audit, and ship agent skills & Claude Code plugins the proven ssheleg way: conformance to the Agent Skills open standard, Anthropic's platform rules (surfaces, Skills API, evals) and the Claude Code plugin reference, marketplace repo layout, version sync, validator + CI, multi-channel distribution (plugin, vercel skills CLI, npx, Cursor), npm gotchas, end-to-end first publish, the review checklist for third-party skills, plus MCP / A2A references for protocol-connected skills.",
6
- "version": "0.23.5",
6
+ "version": "0.25.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -5,7 +5,7 @@ license: MIT
5
5
  compatibility: Authoring works on any agent. The bundled scripts/ need python3. Publishing steps need git, gh, node and npm; the plugin gates need the claude CLI. Not usable on the Claude API surface, which has no network and no runtime package install.
6
6
  metadata:
7
7
  author: ssheleg
8
- version: "0.23.5"
8
+ version: "0.25.0"
9
9
  homepage: https://github.com/ssheleg/make-skill
10
10
  ---
11
11
 
@@ -10,7 +10,10 @@ Upstream (read against these, not from memory — the schema grows every release
10
10
  - Marketplaces — <https://code.claude.com/docs/en/plugin-marketplaces>
11
11
  - Skills — <https://code.claude.com/docs/en/skills>
12
12
 
13
- *Read from the docs on 2026-07-30 against Claude Code 2.1.212. Re-read before
13
+ *Read from the docs on 2026-07-30 against Claude Code 2.1.212, and again 2026-08-28
14
+ against 2.1.236 — that pass added the `archive` and `command` sources, entry-level
15
+ `headersHelper`, bare-name sources under `metadata.pluginRoot`, and `"skills": ["."]`,
16
+ all of which landed between the two readings. Re-read before
14
17
  trusting a version-gated field in a new quarter.*
15
18
 
16
19
  The [Agent Skills spec](https://agentskills.io/specification) (see
@@ -128,6 +131,12 @@ marketplace-only `source`, `category`, `tags`, `strict`, `relevance`,
128
131
  | `url` | `{"source":"url","url":"https://…​.git","ref?":…,"sha?":…}` |
129
132
  | `git-subdir` | `{"source":"git-subdir","url":"…","path":"packages/x","ref?":…,"sha?":…}` — sparse clone |
130
133
  | `npm` | `{"source":"npm","package":"@scope/pkg","version?":…,"registry?":…}` — no git SHA to fall back on, so set `version` in the manifest or the entry, else it resolves to `unknown` and updates never fire |
134
+ | `archive` **2.1.224+** | `{"source":"archive","url":"https://…/x-2.1.0.zip","sha256?":"<64 hex>"}` — a zip over HTTPS, **no git and no npm on the machine**. The digest is optional and is the only thing standing between a URL and whatever is behind it today; set it |
135
+ | `command` **2.1.229+** | `{"source":"command","command":"my-tool plugin-path","timeout?":60,"mode?":"copy"}` — a local command prints the directory. `timeout` defaults to 60s, max 600; `mode` is `copy` (default) or `link`. An organisation can be configured to refuse this entire class with `disableCommandPluginSources`, so it is the one source that may be unavailable for reasons no error in your repo explains |
136
+
137
+ **Bare-name sources, 2.1.239+.** With `metadata.pluginRoot` set on the marketplace,
138
+ an entry may write `"source": "my-plugin"` and have it resolve under that root — the
139
+ `./` requirement above applies only without it.
131
140
 
132
141
  `sha` beats `ref` when both are set. **Marketplace source ≠ plugin source**: the
133
142
  first says where `marketplace.json` lives (supports `ref` only), the second where
@@ -137,6 +146,23 @@ each plugin comes from (`ref` and `sha`).
137
146
  to it. `strict: false` makes the entry the entire definition — and a
138
147
  `plugin.json` that also declares components is then a load error.
139
148
 
149
+ **`headersHelper` on an entry, 2.1.238+ — and it needs `strict: false`.** Beside
150
+ `source`, it names a command that mints HTTP headers for a private registry, and it runs
151
+ **only** when that one plugin is installed or updated. The marketplace-level form is
152
+ different: declared in `extraKnownMarketplaces` on a `url` marketplace, it runs before
153
+ every catalog fetch and archive download on that origin, and one run is reused for 60
154
+ seconds. Both are separate from the MCP `headersHelper` documented later in this file.
155
+
156
+ The command's contract, because each clause is a way to fail silently: one JSON object of
157
+ header names to string values on stdout, exit 0, **within 10 seconds**; at most 500
158
+ printable ASCII characters with no run of four spaces; run through `sh` (or `cmd.exe`)
159
+ from `~/.claude`. Claude Code **strips every variable whose name contains `TOKEN`,
160
+ `SECRET`, `KEY` or `AUTH`** from its environment — so a helper that reads its credential
161
+ from one of those is handed nothing, and the failure looks like a permissions problem at
162
+ the registry. It receives `CLAUDE_CODE_MARKETPLACE_URL`, `CLAUDE_CODE_MARKETPLACE_NAME`,
163
+ `CLAUDE_CODE_PLUGIN_NAME` and `CLAUDE_CODE_PLUGIN_ARCHIVE_URL` instead. The user has to
164
+ accept the command before it runs at all.
165
+
140
166
  Version resolution order: `plugin.json` → marketplace entry → git SHA →
141
167
  `unknown`.
142
168
 
@@ -145,7 +171,7 @@ Version resolution order: `plugin.json` → marketplace entry → git SHA →
145
171
  | Component | Default location |
146
172
  |---|---|
147
173
  | manifest | `.claude-plugin/plugin.json` |
148
- | skills | `skills/<name>/SKILL.md` |
174
+ | skills | `skills/<name>/SKILL.md` — and **`"skills": ["."]` is legal from 2.1.221**: it scans the plugin ROOT for `<name>/SKILL.md` instead of `skills/`, which is what a repository whose skills sit at the top level needs |
149
175
  | commands | `commands/*.md` (flat skills; prefer `skills/` for new plugins) |
150
176
  | agents | `agents/*.md` |
151
177
  | workflows | `workflows/*.js` |
@@ -10,7 +10,8 @@ API or claude.ai instead is `references/surfaces.md`.
10
10
  - The distributable repo layout (tree, public-repo floor, version sync, gates)
11
11
  - 1. Claude Code plugin
12
12
  - 2. vercel-labs skills CLI
13
- - 3. npx installer — npm gotchas, installer implementation traps
13
+ - 3. npx installer — npm gotchas; the installer must refuse the shadow it
14
+ documents; how updates reach the machine; implementation traps
14
15
  - First publish — the 11-step sequence
15
16
  - 4. Cursor
16
17
  - 5. Ship a FAMILY via an umbrella repo
@@ -212,6 +213,83 @@ package's own repo, `npx` resolves the local package and reports a false
212
213
  - **`npx` inside the package's own repo** resolves the local package → a false
213
214
  `command not found`. Always e2e-test from another cwd.
214
215
 
216
+ ### The installer must refuse the shadow it documents
217
+
218
+ The shadow rule at the top of this file is not only about the skills CLI: **a member's
219
+ own `npx @scope/<name>` installer is the other machine that recreates the shadow.** It
220
+ writes `~/.claude/skills/<name>` on request, and on a machine where the same skill is
221
+ installed as a Claude Code plugin, that write is a plain copy that outranks the plugin
222
+ and serves the version it was copied from forever. Measured 2026-08-29: a bare
223
+ `npx @ssheleg/telegram-dev` created three plain copies in `~/.claude/skills/` while the
224
+ `telegram-dev` plugin was enabled — and every bin installer in the family had the same
225
+ hole, because every member's CI tested a fresh `HOME` only, so the plugin-present case
226
+ had never run anywhere.
227
+
228
+ The canon, for every bin installer that targets `~/.claude/skills/`:
229
+
230
+ - **Detect the plugin from the TARGET home before writing.** Read that home's
231
+ `~/.claude/plugins/installed_plugins.json` and look for a plugin whose name matches
232
+ the skill under ANY marketplace — the keys are `<name>@<marketplace>`, and the two
233
+ names differ often. A `~/.claude/plugins/marketplaces/<name>` directory also signals
234
+ the plugin channel, but only as the fallback read: it **under-reports** — a
235
+ marketplace added from a local `directory` source has no dir there at all, and a
236
+ differently-named marketplace never matches the skill name. A presence check keyed on
237
+ that directory alone stays green while the shadow lands, which is the fail-open
238
+ class: it looks at the wrong file and then exits 0.
239
+ - **Refuse, and exit non-zero.** Print what was found, why the plain copy is refused,
240
+ and the plugin-channel remedy — `claude plugin marketplace update <name>` then
241
+ `claude plugin update <name>@<marketplace>` with the spec read from the JSON, or the
242
+ family launcher (`npx --yes sshlg-skills@latest update`) for a member that composes
243
+ with its family. A refusal that exits 0 reads as success to every script above it:
244
+ this repository's own installer shipped exactly that half-measure until v0.25.0 —
245
+ marketplace-dir check, a `skip:` message, exit 0.
246
+ - **`--force` is the deliberate override**, named inside the refusal itself. Two
247
+ channels on one agent is a choice someone may make on purpose, and the flag is where
248
+ that choice gets recorded instead of happening by accident.
249
+ - **Absence fails open; corruption never crashes.** A missing or unparsable
250
+ `installed_plugins.json` reads as "no plugin" — the fresh HOME is the common case,
251
+ and an installer that dies on a parse error refuses the machines that need it most.
252
+ - **Only Claude Code has plugins.** The check gates the `~/.claude` write alone;
253
+ installs into other agents' skill directories are untouched by it.
254
+ - **CI runs the plugin-present case, not only a fresh HOME.** A fake HOME whose
255
+ `installed_plugins.json` declares the plugin, asserting all three at once: the
256
+ non-zero exit, the remedy in the output, and that nothing was written — plus the
257
+ `--force` path installing and the fresh HOME still installing. Reference
258
+ implementation: this repository's `bin/make-skill.js` and `test/installer_test.js`.
259
+
260
+ ### How updates reach the machine — decide it, then SAY it
261
+
262
+ An installer that never mentions updates has still chosen an update model: **never**. The
263
+ operator installs once, the pack drifts, and nothing tells them. Decide between three, and
264
+ print the answer at the end of the install.
265
+
266
+ **Claude Code's own switch is a per-marketplace `autoUpdate` flag** in
267
+ `~/.claude/plugins/known_marketplaces.json`. It is real and it is **not in the documented
268
+ settings surface** — not in the settings reference, not in the marketplace page; `/plugin`
269
+ writes it, and third-party installers write it directly. `vercel-labs/plugins` prompts
270
+ `Enable auto-updates? [Y/n]`, defaults to yes, and only asks when the marketplace is new,
271
+ the target is Claude Code or Cursor, and the install did **not** go through the official
272
+ CLI. Measured on one machine 2026-08-28: of 20 marketplaces, the 2 installed by that tool
273
+ carry `autoUpdate: true` and the other 18 have no such key — **`claude plugin marketplace
274
+ add` does not set it**, so a plugin installed the official way never auto-updates unless
275
+ somebody turns it on.
276
+
277
+ | Model | Who updates | Right when |
278
+ |---|---|---|
279
+ | **Per-marketplace `autoUpdate`** | Claude Code, in the background, each marketplace on its own clock | packs are **independent** — one can move without the others meaning anything |
280
+ | **A launcher command** (`npx <name> update`) | the operator, in one pass, whole set | packs **compose** — they are released and tested together |
281
+ | **Nothing** | nobody | never; this is the default you get by not choosing |
282
+
283
+ **The trap, and it is the whole reason this section exists.** For a FAMILY that composes,
284
+ per-marketplace auto-update is actively wrong: each member moves on its own schedule and
285
+ the machine ends up in a combination the maintainers never tested together. That is the
286
+ same reason a family launcher refuses a per-member argument. If your packs are
287
+ independent, `autoUpdate` is the better answer and you should offer it; if they compose,
288
+ offer the launcher and say plainly that auto-update is off **on purpose**.
289
+
290
+ Either way the last thing the installer prints is how the next version arrives. "Installed"
291
+ is not a complete sentence.
292
+
215
293
  ### Installer implementation traps (read while WRITING the CLI)
216
294
 
217
295
  - **Piped stdin + readline:** sequential `rl.question()` drops buffered lines.
@@ -323,7 +401,8 @@ claude plugin update <name>@<name> # full id required
323
401
  `claude plugin validate ./plugins/<name> --strict` and
324
402
  `claude plugin validate . --strict` → both exit 0.
325
403
  3. Functional tests: installer against a scratch `HOME` (fresh / rerun-skip /
326
- `--force`), `node --check` on the CLI, piped-menu tests.
404
+ `--force` / **plugin-present refusal**, per the canon in §3), `node --check`
405
+ on the CLI, piped-menu tests.
327
406
  4. Conventional commit; push; confirm CI `success`; tag `v<ver>` and push the tag;
328
407
  `gh release create` from the CHANGELOG section (or let the release workflow
329
408
  below do it).