universal-plugin 0.6.0 → 0.8.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/bin/upx.mjs +12 -5
- package/com.github.copilot/agents/agentskills-specialist.agent.md +132 -0
- package/dist/cli.mjs +5806 -261
- package/governances/plugin-design.md +14 -0
- package/package.json +4 -1
- package/plugin.json +1 -1
- package/readme.md +12 -5
- package/schema/README.md +10 -0
- package/schema/claude-code-marketplace.json +1939 -0
- package/schema/extension.schema.json +842 -0
- package/skills/adopt-upx/README.md +4 -4
- package/skills/adopt-upx/SKILL.md +4 -4
- package/skills/doctor/README.md +3 -4
- package/skills/doctor/SKILL.md +44 -14
- package/skills/doctor/scripts/doctor.mjs +145 -37
- package/skills/{init → init-universal-plugin}/README.md +5 -2
- package/skills/{init → init-universal-plugin}/SKILL.md +38 -13
- package/skills/init-universal-plugin/references/adopt.md +206 -0
- package/skills/{init → init-universal-plugin}/references/detection.md +8 -2
- package/skills/{init → init-universal-plugin}/references/standard.md +5 -1
- package/skills/{init → init-universal-plugin}/references/vendors/claude-code.md +1 -1
- package/skills/init-universal-plugin/references/vendors/copilot-cli.md +201 -0
- package/skills/marketplace/SKILL.md +1 -1
- package/skills/migrate-plugin/SKILL.md +140 -26
- package/skills/migrate-plugin/evals/evals.json +6 -0
- package/skills/migrate-plugin/evals/trigger-queries.json +18 -0
- package/skills/publish-plugin/README.md +35 -0
- package/skills/publish-plugin/SKILL.md +118 -19
- package/skills/publish-plugin/evals/evals.json +12 -0
- package/skills/remove-plugin/README.md +1 -1
- package/skills/remove-plugin/SKILL.md +2 -2
- package/skills/version/SKILL.md +2 -2
- package/dist/run.mjs +0 -271
- package/skills/init/references/adopt.md +0 -118
- package/skills/init/references/vendors/copilot-cli.md +0 -53
- /package/skills/{init → init-universal-plugin}/assets/templates/agent.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/command.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/hooks.json +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/plugin.json +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/setup-command.md +0 -0
- /package/skills/{init → init-universal-plugin}/assets/templates/skill.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/create.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/frontmatter.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/update.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/vendors/codex.md +0 -0
- /package/skills/{init → init-universal-plugin}/references/vendors/cursor.md +0 -0
- /package/skills/{init → init-universal-plugin}/scripts/init.mjs +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Adopt the open standard
|
|
2
|
+
|
|
3
|
+
Convert something that is *already* a plugin — or already ships skills — onto the canonical
|
|
4
|
+
Agent Plugins Specification manifest, without changing what it does.
|
|
5
|
+
|
|
6
|
+
Two starting shapes land here:
|
|
7
|
+
|
|
8
|
+
- **A vendor-specific plugin** — it has one or more hand-written vendor manifests
|
|
9
|
+
(`.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, …) and no canonical root
|
|
10
|
+
`plugin.json`.
|
|
11
|
+
- **Bare public skills** — it ships `skills/<name>/SKILL.md` to users but has no plugin manifest of
|
|
12
|
+
any kind.
|
|
13
|
+
|
|
14
|
+
Adoption is **lossless by contract**: every vendor that worked before must still work after. Step 6
|
|
15
|
+
is the check that proves it — do not skip it.
|
|
16
|
+
|
|
17
|
+
## Step 0 — Confirm the user wants this
|
|
18
|
+
|
|
19
|
+
This is the skill's Phase 3 gate, and adoption always needs it: adoption rewrites the project's
|
|
20
|
+
manifest layout and turns hand-written vendor manifests into generated artifacts. Say that plainly
|
|
21
|
+
and get agreement before touching files. If the user declines,
|
|
22
|
+
route back to whatever they originally asked for.
|
|
23
|
+
|
|
24
|
+
Also confirm the working tree is clean (`git status`). The Step 6 diff is worthless if uncommitted
|
|
25
|
+
changes are mixed in.
|
|
26
|
+
|
|
27
|
+
## Step 1 — Inventory what exists
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
ls -d .claude-plugin .cursor-plugin .codex-plugin .github/plugin .plugin 2>/dev/null
|
|
31
|
+
test -f plugin.json && cat plugin.json
|
|
32
|
+
find . -name SKILL.md -not -path '*/node_modules/*' -not -path './.git/*'
|
|
33
|
+
ls .mcp.json .lsp.json hooks/ commands/ agents/ rules/ output-styles/ 2>/dev/null
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Record, for each vendor manifest found: its path, and every field it sets. You are about to
|
|
37
|
+
reproduce all of it.
|
|
38
|
+
|
|
39
|
+
**If a root `plugin.json` already exists**, read it before assuming anything. It is either the
|
|
40
|
+
canonical manifest (has `$schema` pointing at `agent-plugins.org` and an `extensions` object — in
|
|
41
|
+
which case there is nothing to adopt; route to `update.md`, or hand off to `doctor`, instead), or a legacy
|
|
42
|
+
Copilot CLI manifest that now collides with the canonical path and must be folded in.
|
|
43
|
+
|
|
44
|
+
## Step 2 — Sort every field into shared, vendor-specific, and undeliverable
|
|
45
|
+
|
|
46
|
+
Build three buckets from the manifests you inventoried:
|
|
47
|
+
|
|
48
|
+
- **Shared metadata** — the canonical top level is a **closed** set of exactly ten fields:
|
|
49
|
+
`$schema`, `name`, `version`, `description`, `author`, `homepage`, `repository`, `license`,
|
|
50
|
+
`keywords`, `extensions`. A field in that list stays at the top level. That set is the upstream
|
|
51
|
+
spec's, not this project's — see
|
|
52
|
+
[the published schema](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json).
|
|
53
|
+
- **Vendor-specific** — anything only one runtime understands (Cursor's `publisher`/`category`/
|
|
54
|
+
`tags`, Codex's `interface`). These go under
|
|
55
|
+
`extensions["org.cyberuni.universal-plugin"].harnesses.<vendor>`. The component paths
|
|
56
|
+
(`skills`, `commands`, `agents`, `hooks`, `mcpServers`, `rules`, `lspServers`, `outputStyles`)
|
|
57
|
+
are not top-level either — they go under that same extension namespace, one level up from
|
|
58
|
+
`harnesses`.
|
|
59
|
+
- **Undeliverable** — a field whose only consumer is Copilot CLI. Copilot CLI reads the canonical
|
|
60
|
+
root manifest directly and gets no derived file, so such a field has no home on either side: the
|
|
61
|
+
closed schema rejects it at the top level, and a `harnesses["copilot-cli"]` entry is never
|
|
62
|
+
written anywhere. Before dropping one, check
|
|
63
|
+
[`vendors/copilot-cli.md`](./vendors/copilot-cli.md) — the two fields that actually turn up here,
|
|
64
|
+
`category` and `tags`, have a spec-conformant home (`keywords`) and are not a loss.
|
|
65
|
+
|
|
66
|
+
Where two vendor manifests disagree on a shared field, **ask the user** which value is canonical
|
|
67
|
+
rather than picking one. A silent choice here is a silent behavior change for one of their runtimes.
|
|
68
|
+
|
|
69
|
+
### Name every field you are about to drop
|
|
70
|
+
|
|
71
|
+
A legacy root `plugin.json` is what makes the third bucket real. Before 0.6 the build wrote
|
|
72
|
+
Copilot CLI's output *to root*, so root carries whatever Copilot-specific fields the project set —
|
|
73
|
+
`category` and `tags` are the ones seen in the wild. After adoption root is the canonical manifest
|
|
74
|
+
and those fields have nowhere to go.
|
|
75
|
+
|
|
76
|
+
Nothing warns about this. The build's `undeliverable-override` warning and `doctor`'s matching
|
|
77
|
+
finding fire only on a `harnesses["copilot-cli"]` entry — which is what someone doing it *wrong*
|
|
78
|
+
writes. An adoption done correctly never creates one, so it gets silence. Enumerate the fields
|
|
79
|
+
yourself, from the pre-adoption root manifest, before you overwrite it:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
node -e '
|
|
83
|
+
const spec = ["$schema", "name", "version", "description", "author",
|
|
84
|
+
"homepage", "repository", "license", "keywords", "extensions"]
|
|
85
|
+
const root = JSON.parse(require("node:fs").readFileSync("plugin.json", "utf8"))
|
|
86
|
+
for (const k of Object.keys(root)) {
|
|
87
|
+
if (!spec.includes(k)) console.log(k, "=", JSON.stringify(root[k]))
|
|
88
|
+
}
|
|
89
|
+
'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Every name it prints is a field the canonical top level cannot hold. Sort each one:
|
|
93
|
+
|
|
94
|
+
| The field | Where it goes |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `vendorExtensions` — the whole pre-0.6 block | `extensions["org.cyberuni.universal-plugin"].harnesses` — **renamed, not dropped** |
|
|
97
|
+
| a component path (`skills`, `commands`, `agents`, …) | `extensions["org.cyberuni.universal-plugin"].<name>` — carried |
|
|
98
|
+
| a field a vendor with a *derived* manifest understands | that vendor's `harnesses` entry — carried |
|
|
99
|
+
| `category` / `tags` from the legacy Copilot output | fold into **`keywords`** — see below |
|
|
100
|
+
| anything left | **dropped** |
|
|
101
|
+
|
|
102
|
+
`vendorExtensions` is the row that catches people out, because the snippet prints it alongside the
|
|
103
|
+
genuinely undeliverable fields and it looks like one of them. It is not: it is the old name for
|
|
104
|
+
`harnesses`, and dropping it discards every per-harness override the project had. Carry the block
|
|
105
|
+
across, then sort the `copilot-cli` entry inside it by the rule above — that entry is the one whose
|
|
106
|
+
contents have no delivery path.
|
|
107
|
+
|
|
108
|
+
`plugin build` will not let this one pass quietly: a root manifest still carrying `vendorExtensions`
|
|
109
|
+
now exits 1 rather than reporting `built 0`, naming the signal and routing to `doctor`. If Step 5
|
|
110
|
+
fails that way, the block did not get carried across.
|
|
111
|
+
|
|
112
|
+
`category` and `tags` have their own row because they are the fields this whole step exists for, and
|
|
113
|
+
they are **not** simply dropped. Copilot CLI has no `plugin.json` handling for either — its manifest
|
|
114
|
+
validator knows `keywords` and treats these two as unknown-and-ignored — while `keywords` is both a
|
|
115
|
+
spec field and the one Copilot actually parses. So fold the values in rather than deleting them:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
category: "productivity" + tags: ["workflow", "planning", "verification"]
|
|
119
|
+
→ keywords: ["productivity", "workflow", "planning", "verification"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If the project also ships a `marketplace.json` catalog, `category` and `tags` are real fields on a
|
|
123
|
+
catalog entry and belong there as well — with the right types, because a wrong one is a fatal browse
|
|
124
|
+
error rather than a warning. [`vendors/copilot-cli.md`](./vendors/copilot-cli.md) has the evidence
|
|
125
|
+
and the exact validator messages.
|
|
126
|
+
|
|
127
|
+
**Report anything genuinely dropped to the user by name, with the value each held, before Step 3
|
|
128
|
+
writes anything.** That report is the only notice they get, and it is the difference between an informed
|
|
129
|
+
decision and a field that evaporates. Say what the field was for if you know; if a field looks load-
|
|
130
|
+
bearing and the user wants it kept, stop rather than shipping the adoption around it.
|
|
131
|
+
|
|
132
|
+
## Step 3 — Write the canonical manifest
|
|
133
|
+
|
|
134
|
+
Scaffold it, naming exactly the vendors you found in Step 1:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
node scripts/init.mjs --name <name> --vendor claude-code --vendor cursor
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Resolve `scripts/init.mjs` against this skill's directory; `npx universal-plugin plugin init` is the
|
|
141
|
+
fallback.
|
|
142
|
+
|
|
143
|
+
> `plugin init` writes a **minimal** manifest — `$schema`, `name`, and the `vendors` list. It does
|
|
144
|
+
> not read your existing vendor manifests. Carry the Step 2 buckets in by hand afterwards.
|
|
145
|
+
|
|
146
|
+
Then fill in the shared metadata and `harnesses` as laid out in
|
|
147
|
+
[`create.md`](./create.md) Step 5. Point the component paths at the directories that already exist —
|
|
148
|
+
adoption must not move files.
|
|
149
|
+
|
|
150
|
+
For the bare-public-skills case there is no metadata to carry over; supply `name`, `description`,
|
|
151
|
+
and `version`, set `"skills": "./skills/"`, and choose vendors with the user (see
|
|
152
|
+
[`create.md`](./create.md) Step 2).
|
|
153
|
+
|
|
154
|
+
## Step 4 — Decide what happens to the old manifests
|
|
155
|
+
|
|
156
|
+
The vendor manifests are now **build outputs**. They stay at the same paths, but they are
|
|
157
|
+
regenerated rather than edited.
|
|
158
|
+
|
|
159
|
+
- Commit them as-is first, so Step 6 has a baseline to diff against.
|
|
160
|
+
- Tell the user they are generated from here on, and that hand-edits will be overwritten by
|
|
161
|
+
`plugin build`.
|
|
162
|
+
- If the project has a legacy root `plugin.json` for Copilot CLI, that path is now the canonical
|
|
163
|
+
manifest. Copilot CLI gets no replacement file: it reads root directly, so the canonical manifest
|
|
164
|
+
*is* its manifest from here on. See the vendor output table in [`create.md`](./create.md) Step 2.
|
|
165
|
+
|
|
166
|
+
## Step 5 — Build
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx universal-plugin plugin build
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Step 6 — Prove it was lossless
|
|
173
|
+
|
|
174
|
+
This is the point of the whole procedure.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git diff -- plugin.json .claude-plugin .cursor-plugin .codex-plugin .github/plugin
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Root `plugin.json` is in that list on purpose, and it is the one path that behaves differently from
|
|
181
|
+
the other four.
|
|
182
|
+
|
|
183
|
+
- For `.claude-plugin`, `.cursor-plugin`, `.codex-plugin`, and `.github/plugin`, expect only
|
|
184
|
+
formatting and key-order churn. A hunk that changes a value is a finding.
|
|
185
|
+
- For root `plugin.json`, **expect real hunks** — the `$schema` line and the `extensions` object are
|
|
186
|
+
new, and the component paths moved under them. That is the rewrite working. Because the diff is
|
|
187
|
+
large by design it is the easy one to skim, and under the legacy layout root *was* a vendor
|
|
188
|
+
manifest — Copilot CLI's — so it is also the only file where a field can go missing without any
|
|
189
|
+
other check noticing. Read it, do not skim it.
|
|
190
|
+
|
|
191
|
+
Account for **every key that leaves root**. Each one must land in exactly one of three places: at the
|
|
192
|
+
canonical top level, under `extensions`, or on the dropped list you enumerated and reported in
|
|
193
|
+
Step 2. A key that left root and is on none of those three is a regression.
|
|
194
|
+
|
|
195
|
+
**Any field that disappeared unannounced is a regression**, not a cleanup. Trace it back: either it
|
|
196
|
+
belongs in the shared metadata, or it belongs in that vendor's `harnesses` entry, or it is a field
|
|
197
|
+
the build does not yet support — in which case stop and tell the user rather than shipping a quiet
|
|
198
|
+
capability loss.
|
|
199
|
+
|
|
200
|
+
Then confirm the plugin still loads. See [`create.md`](./create.md) Step 8 for local install.
|
|
201
|
+
|
|
202
|
+
## Step 7 — Hand off
|
|
203
|
+
|
|
204
|
+
- Audit the skills: [`create.md`](./create.md) Step 6.
|
|
205
|
+
- Shipping it on npm? → `migrate-plugin`.
|
|
206
|
+
- Listing it in the marketplace? → `publish-plugin`.
|
|
@@ -15,13 +15,19 @@ ls .mcp.json .lsp.json hooks/ commands/ agents/ rules/ output-styles/ 2>/dev/nul
|
|
|
15
15
|
| `.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/` manifest **below** a canonical root | build output | derived |
|
|
16
16
|
| a vendor manifest with **no** canonical root `plugin.json` | a vendor-specific plugin | adoptable |
|
|
17
17
|
| root `plugin.json` with neither `$schema` nor `extensions` | a legacy single-vendor manifest now sitting on the canonical path | adoptable |
|
|
18
|
+
| a top-level `vendorExtensions` block | the pre-0.6 name for `harnesses`; `plugin build` exits 1 on it | adoptable — carry the block, do not drop it |
|
|
18
19
|
| publicly-shipped skills and no manifest of any kind | skills shipped without a plugin | adoptable |
|
|
19
20
|
| `harnesses["copilot-cli"]` carrying fields | no delivery path — the canonical schema is closed | undeliverable |
|
|
21
|
+
| a non-spec field on a legacy root `plugin.json` (`category`, `tags`, …) | it was Copilot CLI's, and Copilot CLI has no derived manifest to move it to | undeliverable — and **nothing warns**; name it in Phase 3 |
|
|
20
22
|
| `.claude/skills/`, `.agents/skills/`, `.cursor/rules/` | the project's own tooling | not a plugin |
|
|
21
23
|
| `.github/plugin/plugin.json` | a path older builds wrote; shadowed by root and no longer generated | stale, safe to delete |
|
|
22
24
|
|
|
23
|
-
For every vendor manifest found, record its path and **every field it sets
|
|
24
|
-
all of it, and the Phase 5 diff is checked
|
|
25
|
+
For every vendor manifest found, record its path and **every field it sets** — root `plugin.json`
|
|
26
|
+
included, when it is the legacy kind. Adoption reproduces all of it, and the Phase 5 diff is checked
|
|
27
|
+
field by field. The canonical top level accepts exactly ten fields (`$schema`, `name`, `version`,
|
|
28
|
+
`description`, `author`, `homepage`, `repository`, `license`, `keywords`, `extensions`); everything
|
|
29
|
+
else either moves under `extensions` or is dropped, and
|
|
30
|
+
[`adopt.md`](./adopt.md) Step 2 is where you sort out which.
|
|
25
31
|
|
|
26
32
|
## Which skills count as public
|
|
27
33
|
|
|
@@ -36,10 +36,14 @@ lives under one namespaced key, `extensions["org.cyberuni.universal-plugin"]`:
|
|
|
36
36
|
| --- | --- |
|
|
37
37
|
| `vendors` | the build targets. When absent, the `harnesses` keys are the targets |
|
|
38
38
|
| `harnesses` | per-vendor overrides, keyed by vendor id. `{}` opts in with no overrides |
|
|
39
|
-
| `packagePath` | the npm package whose `package.json` carries the same version |
|
|
40
39
|
| component paths (`skills`, `commands`, `agents`, `hooks`, …) | where each component lives |
|
|
41
40
|
| `dependencies` | the plugins this plugin needs. Only Claude Code reads them; see [`vendors/claude-code.md`](./vendors/claude-code.md) |
|
|
42
41
|
|
|
42
|
+
`packagePath` does not belong here. The npm package whose `package.json` carries the same version is
|
|
43
|
+
named in `.agents/universal-plugin.json` beside `plugin.json`, as a path relative to the plugin root
|
|
44
|
+
(for example `"packagePath": "."`, or `"../../packages/<name>"` in a monorepo). It is release config
|
|
45
|
+
for the repository, not part of the manifest.
|
|
46
|
+
|
|
43
47
|
`vendors` and `harnesses` are separate on purpose: `vendors` says what to build, `harnesses` says
|
|
44
48
|
what each build gets. A vendor listed in `vendors` with no `harnesses` entry still builds; a
|
|
45
49
|
`harnesses` entry with no `vendors` list builds only while `vendors` is absent.
|
|
@@ -10,7 +10,7 @@ npx universal-plugin plugin build --vendor claude-code
|
|
|
10
10
|
|
|
11
11
|
The shared metadata from the canonical top level, plus the component paths, plus whatever
|
|
12
12
|
`extensions["org.cyberuni.universal-plugin"].harnesses["claude-code"]` sets. `$schema`, `extensions`,
|
|
13
|
-
`vendors`,
|
|
13
|
+
`vendors`, and `harnesses` are universal-plugin's own orchestration — they never
|
|
14
14
|
appear in a vendor manifest.
|
|
15
15
|
|
|
16
16
|
An empty `"claude-code": {}` is the normal case: it opts into the build with no overrides.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# GitHub Copilot CLI
|
|
2
|
+
|
|
3
|
+
Reads the **canonical root `plugin.json` directly**, so the build derives no vendor manifest for it.
|
|
4
|
+
It does derive its **components** — see [`com.github.copilot/` — the namespace directory](#comgithubcopilot--the-namespace-directory).
|
|
5
|
+
A plugin declaring none of the moved kinds is reported with status `canonical`, which is success, not
|
|
6
|
+
a skipped target.
|
|
7
|
+
|
|
8
|
+
## Why no manifest is derived
|
|
9
|
+
|
|
10
|
+
Copilot CLI searches four paths and takes the first match:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
.plugin/plugin.json → plugin.json → .github/plugin/plugin.json → .claude-plugin/plugin.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Root `plugin.json` — the canonical manifest — is second, so it always shadows the two below it. It
|
|
17
|
+
has consumed Open Plugin Spec v1 manifests since **v1.0.74** (2026-07-23, [changelog](https://github.com/github/copilot-cli/blob/main/changelog.md):
|
|
18
|
+
"Add support for Open Plugin Spec v1 plugin manifests and mcp.json configuration"), so it already
|
|
19
|
+
serves the canonical manifest as-is.
|
|
20
|
+
|
|
21
|
+
Earlier builds wrote `.github/plugin/plugin.json`. That path loses to root by construction and was
|
|
22
|
+
never read; a leftover copy is stale and safe to delete.
|
|
23
|
+
|
|
24
|
+
## Vendor-specific fields cannot be delivered
|
|
25
|
+
|
|
26
|
+
A `harnesses["copilot-cli"]` entry has nowhere to go. The canonical schema is closed
|
|
27
|
+
(`additionalProperties: false`), so a Copilot-only field cannot ride along in root, and there is no
|
|
28
|
+
derived file to put it in. The build warns:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
harnesses.copilot-cli sets category, tags, but copilot-cli reads the canonical plugin.json
|
|
32
|
+
directly — these fields are not delivered
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Treat that warning as a decision to make, not noise: either the field belongs to a vendor that has a
|
|
36
|
+
derived manifest, or it does not ship. Do not invent a path for it.
|
|
37
|
+
|
|
38
|
+
## `category` and `tags` belong to the catalog, not the manifest
|
|
39
|
+
|
|
40
|
+
A project on the pre-0.6 layout carries these on its **root** `plugin.json`, because root *was*
|
|
41
|
+
Copilot CLI's derived output. Adoption drops them from there, and that loses nothing — but not for
|
|
42
|
+
the reason the field table suggests.
|
|
43
|
+
|
|
44
|
+
**Copilot CLI has no `plugin.json` handling for either field.** Verified against the shipped
|
|
45
|
+
`@github/copilot-linux-x64` **1.0.83** runtime (the plugin loader is Rust in
|
|
46
|
+
`prebuilds/*/runtime.node`, not the bundled JS), and confirmed by running the binary. The manifest
|
|
47
|
+
validator carries a dedicated message for `keywords` and none for `category` or `tags`; both land on
|
|
48
|
+
the unknown-field path:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Plugin manifest "…": field "keywords" must be an array of strings (ignored)
|
|
52
|
+
Plugin manifest "…": unknown field "…" (ignored)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Unknown keys are **warn-and-ignore**: not rejected, not stripped from disk, dropped from the parsed
|
|
56
|
+
struct. A manifest carrying `category`, `tags`, and an outright bogus key installs cleanly with no
|
|
57
|
+
output about any of them. GitHub's published field table lists `category`/`tags` under the
|
|
58
|
+
`plugin.json` optional metadata fields anyway — as of 1.0.83 that part of the table does not match
|
|
59
|
+
the shipped loader.
|
|
60
|
+
|
|
61
|
+
**Where they are real is `marketplace.json`.** The catalog validator type-checks both on every
|
|
62
|
+
`plugins[]` entry, and a wrong type is a *fatal* browse failure, not a warning:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
Failed to browse marketplace: Invalid marketplace.json:
|
|
66
|
+
plugins.0.category: Expected string, received number,
|
|
67
|
+
plugins.0.tags: Expected array, received string
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
With valid values, nothing renders them — `marketplace browse` prints only `name` and `description`,
|
|
71
|
+
and `plugins list --json` emits neither field, nor `keywords`. No list, search, sort, or filter in
|
|
72
|
+
the CLI touches any of them. GitHub's own marketplace (`github/copilot-plugins`, 17 curated entries)
|
|
73
|
+
sets `category` and `tags` zero times while populating `keywords` on 15 of 17.
|
|
74
|
+
|
|
75
|
+
### So where does a migrating project put them
|
|
76
|
+
|
|
77
|
+
| Was | Goes |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `category` / `tags` on root `plugin.json` | fold into **`keywords`** — a spec field, in the closed set, and the one Copilot's manifest validator actually knows |
|
|
80
|
+
| the same values for marketplace discovery | the **catalog entry** in `marketplace.json`, where Copilot defines them |
|
|
81
|
+
|
|
82
|
+
Do not build a delivery path for these into the plugin manifest. There is nothing at the other end.
|
|
83
|
+
|
|
84
|
+
### Still open
|
|
85
|
+
|
|
86
|
+
- **Where the `unknown field (ignored)` warning surfaces.** It exists in the binary but reached no
|
|
87
|
+
output on `plugin install`, `plugin list`, or `plugins list`. Likely the interactive dashboard or
|
|
88
|
+
the session config-problems channel.
|
|
89
|
+
- **The full known-field allow-list for `plugin.json`.** The validator's field set is a Rust const
|
|
90
|
+
array in a stripped binary; only fields with dedicated messages are recoverable. The negative is
|
|
91
|
+
solid — neither `category` nor `tags` has any handling — but the positive list is not.
|
|
92
|
+
- **Server-side catalog search.** The runtime carries a remote catalog client with a `canSearch`
|
|
93
|
+
capability. Whether GitHub's hosted catalog indexes `category`/`tags` is outside what the shipped
|
|
94
|
+
code can answer, and it would be indexing `marketplace.json`, not a plugin manifest.
|
|
95
|
+
- **Extension, command, rule, and hook loading from the namespace directory** (below) was not
|
|
96
|
+
observed directly — only agents were proven by experiment. The rest rests on GitHub's GA post.
|
|
97
|
+
|
|
98
|
+
### Sources
|
|
99
|
+
|
|
100
|
+
- Shipped runtime: `@github/copilot-linux-x64` 1.0.83, `prebuilds/linux-x64/runtime.node`; validator
|
|
101
|
+
strings and live `plugin install` / `marketplace browse` / `plugins list --json` runs.
|
|
102
|
+
- Copilot CLI plugin reference (the field table this contradicts):
|
|
103
|
+
https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference
|
|
104
|
+
- GitHub's own catalog: https://github.com/github/copilot-plugins `.github/plugin/marketplace.json`
|
|
105
|
+
- Agent Plugins Specification v1.0.0 §8 (`extensions` is the sanctioned channel for non-spec data)
|
|
106
|
+
and the closed field set: https://agent-plugins.org/schemas/1.0.0/plugin.schema.json
|
|
107
|
+
|
|
108
|
+
## `com.github.copilot/` — the namespace directory
|
|
109
|
+
|
|
110
|
+
Declaring the canonical `$schema` puts a plugin in **spec mode**, and spec mode changes where
|
|
111
|
+
Copilot CLI looks for its *native* components. They move out of the plugin root and into a
|
|
112
|
+
reverse-domain directory that other runtimes ignore by design:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
com.github.copilot/agents/ commands/ rules/ hooks extensions/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Spec components stay where the spec puts them: `skills/` and `mcp.json` remain at the plugin root.
|
|
119
|
+
Only Copilot-native kinds move. `extensions/` — canvas extensions, added in **1.0.79**
|
|
120
|
+
(2026-08-10) — is one subdirectory of it.
|
|
121
|
+
|
|
122
|
+
The directory pairs with the manifest key of the same name: `com.github.copilot/` carries Copilot's
|
|
123
|
+
*files*, `extensions["com.github.copilot"]` carries Copilot's *data* (§8). Same namespace, two
|
|
124
|
+
surfaces.
|
|
125
|
+
|
|
126
|
+
**This is a real delivery path, and it narrows the rule above.** "A Copilot-only field has nowhere
|
|
127
|
+
to go" still holds for **manifest fields** — the canonical schema is closed and that argument is
|
|
128
|
+
untouched. It does **not** hold for **content**: Copilot-specific agents, commands, rules, and hooks
|
|
129
|
+
have a sanctioned home, and `extensions["com.github.copilot"]` is the right place for Copilot
|
|
130
|
+
metadata.
|
|
131
|
+
|
|
132
|
+
The list above is the shape; these are the exact paths, and `lsp.json` belongs on it too:
|
|
133
|
+
|
|
134
|
+
| Component | Spec-mode path | Root still read |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| agents | `com.github.copilot/agents/` | no |
|
|
137
|
+
| commands | `com.github.copilot/commands/` | no |
|
|
138
|
+
| rules | `com.github.copilot/rules/` | no |
|
|
139
|
+
| hooks | `com.github.copilot/hooks/hooks.json` | no |
|
|
140
|
+
| LSP servers | `com.github.copilot/lsp.json` | no |
|
|
141
|
+
| extensions (canvases) | `com.github.copilot/extensions/` | never had a root path |
|
|
142
|
+
| skills | `skills/` | **yes — does not move** |
|
|
143
|
+
| MCP servers | `mcp.json` | **yes — does not move** |
|
|
144
|
+
|
|
145
|
+
The namespace **replaces** the root rather than supplementing it, and an explicit component path in
|
|
146
|
+
the manifest does **not** opt back into root loading. The move landed in **1.0.80-0** and the
|
|
147
|
+
runtime's own changelog labels it breaking. Evidence and its confidence, including which kinds were
|
|
148
|
+
proven by experiment:
|
|
149
|
+
[`.research/copilot-spec-mode-namespace/`](../../../../../../.research/copilot-spec-mode-namespace/conclusion.md).
|
|
150
|
+
The published CLI plugin reference still says spec support is "additive on top of standard plugin
|
|
151
|
+
loading" and documents only the root layout — it is stale; do not build against it.
|
|
152
|
+
|
|
153
|
+
### What the build derives
|
|
154
|
+
|
|
155
|
+
`plugin build` derives the tree ([ADR-0015](../../../../.agents/spec/design/decisions/0015-copilot-spec-mode-namespace.md)),
|
|
156
|
+
so **authoring stays at the canonical locations**:
|
|
157
|
+
|
|
158
|
+
- `agents`, `commands` and `rules` are copied, resolved through the same `pathValue` contract
|
|
159
|
+
`skills` uses, defaults included.
|
|
160
|
+
- Agents are **renamed** on the way. Copilot CLI reads `agents/` as `.agent.md` files while the
|
|
161
|
+
canonical `agents/` is the Claude Code-shaped `*.md`, so a copy under the authored name would land
|
|
162
|
+
a file the runtime ignores. Commands and rules keep their authored names — the runtime documents no
|
|
163
|
+
extension for either.
|
|
164
|
+
- Hooks are **translated** into `com.github.copilot/hooks/hooks.json`, so a handler Copilot CLI
|
|
165
|
+
cannot run is dropped from that file with a warning rather than reported as ignored at runtime.
|
|
166
|
+
- A declared `lspServers` **path** is copied to `com.github.copilot/lsp.json`. An **inline** map is
|
|
167
|
+
not delivered: the file's top-level shape is undocumented, so the build warns instead of composing
|
|
168
|
+
one.
|
|
169
|
+
- `com.github.copilot/extensions/` is the inverse — authored there, never derived, and left alone by
|
|
170
|
+
`--clean`.
|
|
171
|
+
|
|
172
|
+
The vendor reports `built` at `com.github.copilot/` when it derives any of this, and keeps reporting
|
|
173
|
+
`canonical` at `plugin.json` when the plugin declares none of the moved kinds.
|
|
174
|
+
|
|
175
|
+
## Do not
|
|
176
|
+
|
|
177
|
+
- **Do not delete root `plugin.json` to "clean up" a Copilot target.** It is the source of truth and
|
|
178
|
+
the Copilot manifest at once.
|
|
179
|
+
- Do not write `.plugin/plugin.json`. It outranks root, so it would silently shadow the canonical
|
|
180
|
+
manifest with a copy nothing regenerates. `plugin build` names it as a pre-0.6 signal and exits 1 —
|
|
181
|
+
but only when nothing derived at all. A project whose other harnesses still build keeps the shadow
|
|
182
|
+
and gets no warning, so this stays a rule you follow rather than one the tool enforces.
|
|
183
|
+
- **Do not tell an author to move `agents/` into `com.github.copilot/`.** The root copy is the
|
|
184
|
+
canonical input every other vendor derives from; the namespace is a build output. Moving it
|
|
185
|
+
serves Copilot and strands the other three.
|
|
186
|
+
- **Do not hand-write the namespace directory.** `--clean` replaces everything the build derives
|
|
187
|
+
under it. `extensions/` is the one subtree it leaves alone, and the only one to author there.
|
|
188
|
+
|
|
189
|
+
## Hooks
|
|
190
|
+
|
|
191
|
+
Copilot CLI accepts **either casing**, and the casing selects the payload format: PascalCase gets the
|
|
192
|
+
Claude-compatible format, so the canonical file needs no translation. It is written to
|
|
193
|
+
`com.github.copilot/hooks/hooks.json` all the same, because that is where spec mode reads it — so a
|
|
194
|
+
handler Copilot CLI cannot run is **dropped from that derived file** with a warning, like any other
|
|
195
|
+
vendor's. See [`claude-code.md`](./claude-code.md).
|
|
196
|
+
|
|
197
|
+
## Dependencies
|
|
198
|
+
|
|
199
|
+
Copilot CLI reads no plugin dependency. Because it reads the canonical *manifest* directly, there is
|
|
200
|
+
no derived manifest to leave the declaration out of — it sits under `extensions`, which Copilot CLI ignores,
|
|
201
|
+
and the build reports it as ignored at runtime. See [`claude-code.md`](./claude-code.md).
|
|
@@ -195,7 +195,7 @@ a version the plugin does not have.
|
|
|
195
195
|
|
|
196
196
|
| Task | Skill |
|
|
197
197
|
|------|-------|
|
|
198
|
-
| Create or change the plugin being listed | `init` |
|
|
198
|
+
| Create or change the plugin being listed | `init-universal-plugin` |
|
|
199
199
|
| Check that the plugin's own manifests are current | `doctor` |
|
|
200
200
|
| Move the version users will install | `version` |
|
|
201
201
|
| Submit to the shared marketplace repository instead | `publish-plugin` |
|