universal-plugin 0.6.0 → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: doctor
3
- description: Use this skill to diagnose a universal agent plugin — when a runtime loads none of the plugin's skills, when a vendor manifest is missing or looks out of date after a pull, when a build prints warnings nobody has read, or when checking whether what the canonical plugin.json declares still matches what is on disk for Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "is my plugin set up right", "why isn't my plugin loading", "check the plugin", "are the vendor manifests current", or "what does this plugin declare".
3
+ description: Use this skill to diagnose a universal agent plugin — when `plugin build` reports "built 0" or "nothing to build", when it warns "No vendors declared in harnesses", when a repository still carries `.plugin/plugin.json` or a top-level `vendorExtensions` block after upgrading universal-plugin across a major, when a released version never reached the vendor manifests, when a runtime loads none of the plugin's skills, when a vendor manifest is missing or looks out of date after a pull, or when checking whether what the canonical plugin.json declares still matches what is on disk for Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "is my plugin set up right", "why isn't my plugin loading", "the build says built 0", "why did nothing get built", "check the plugin", "are the vendor manifests current", or "what does this plugin declare".
4
4
  ---
5
5
 
6
6
  # Plugin Doctor
@@ -43,13 +43,20 @@ Read `vendors[].status` literally:
43
43
 
44
44
  | Status | Means |
45
45
  | --- | --- |
46
- | `built` | the build writes this vendor's manifest |
46
+ | `built` | the build writes this vendor's output. For `copilot-cli` that output is the `com.github.copilot/` component tree, not a manifest |
47
47
  | `canonical` | the vendor reads root `plugin.json`; **no file is written, and that is correct** |
48
48
  | `skipped` | an unknown vendor id — a typo in `vendors` |
49
49
  | `failed` | the write itself failed; the finding names why |
50
50
 
51
- `copilot-cli` reporting `canonical` with `exists: false` is a healthy plugin, not a missing build.
52
- Never report it as a fault.
51
+ `copilot-cli` reporting `canonical` is a healthy plugin, not a missing build — root `plugin.json`
52
+ serves it, and a plugin declaring no agents, commands, rules, hooks, or LSP servers has nothing else
53
+ to derive. Never report it as a fault.
54
+
55
+ A plugin that **does** declare those reports `copilot-cli` as `built` at `com.github.copilot/`
56
+ instead. Declaring the canonical `$schema` moves them there: Copilot CLI stops reading them from the
57
+ plugin root entirely, so a root-only layout loads none of them and says nothing
58
+ ([ADR-0015](../../.agents/spec/design/decisions/0015-copilot-spec-mode-namespace.md)). That is what
59
+ `copilot-root-components` reports.
53
60
 
54
61
  If `node` is unavailable, read `scripts/doctor.mjs` and apply the same checks by hand: it composes
55
62
  `universal-plugin plugin build --dry-run --format json` with filesystem facts that build cannot see.
@@ -71,9 +78,10 @@ Each `code` below is what the script emits.
71
78
  | `codex-fields-missing` | Codex is targeted without `version` or `description`; the build fails and writes **nothing at all**, including for the other vendors | add both to the canonical top level |
72
79
  | `version-drift` | the `packagePath` `package.json` and the canonical manifest carry different versions | `/universal-plugin:version` |
73
80
  | `unreleased-content` | shipped content was committed after the commit that set the current version — a consumer keyed on that version never re-extracts it | `/universal-plugin:version` |
81
+ | `copilot-root-components` | agents, commands, rules, hooks, or LSP servers sit at the plugin root with no copy under `com.github.copilot/` — Copilot CLI reads them only from there in spec mode, so it loads none of them, silently | `universal-plugin plugin build` |
74
82
  | `stale-github-plugin` | a leftover `.github/plugin/plugin.json` from an older build — shadowed by root and no longer generated | `/universal-plugin:remove-plugin` |
75
83
  | `shadowing-manifest` | a `.plugin/plugin.json` exists — it outranks root in Copilot CLI's search order and silently shadows the canonical manifest | `/universal-plugin:remove-plugin` |
76
- | `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin | `/universal-plugin:init`, update route |
84
+ | `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin. On a repository still on the pre-0.6 layout the build stops rather than reporting an empty result, and the detail says so — read it beside `legacy-manifest` and `shadowing-manifest`, which name the signals | `/universal-plugin:init`, adopt route on the pre-0.6 layout, else update route |
77
85
  | `package-path-missing` | `packagePath` names a directory with no readable `package.json` | fix `packagePath`, or create the package |
78
86
  | `unparsable-manifest` | root `plugin.json` is not valid JSON | fix the syntax error |
79
87
  | `invalid-catalog` | a marketplace catalog at the repository root is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` |
@@ -140,7 +148,9 @@ is meant to ship — content that is still being worked on is not a finding to a
140
148
  - **Never repair.** Report the finding and name the skill that owns it.
141
149
  - **Never hand-edit a derived manifest to make a finding go away.** The next build overwrites it and
142
150
  the finding comes back.
143
- - Do not report `copilot-cli` writing no file as a fault. It reads the canonical manifest directly.
151
+ - Do not report `copilot-cli` writing no **manifest** as a fault. It reads the canonical manifest
152
+ directly. Its **components** are a separate question — a `copilot-root-components` finding is a real
153
+ fault, and `canonical` is only healthy for a plugin that declares none of the moved kinds.
144
154
  - Do not treat repo-private agent configuration (`.claude/skills/`, `.agents/skills/`) as part of the
145
155
  plugin. Diagnosing a repository's own skill wiring is `buddy-agent-harness:doctor`.
146
156
 
@@ -84,6 +84,57 @@ if (fs.existsSync(path.join(root, '.github/plugin/plugin.json'))) {
84
84
  )
85
85
  }
86
86
 
87
+ // Copilot CLI's spec mode reads its native components ONLY under com.github.copilot/ — declaring the
88
+ // canonical $schema moves them out of the plugin root (ADR-0015). A plugin that ships them at the
89
+ // root and has no namespace copy loads none of them on Copilot CLI, and nothing at runtime says so.
90
+ // Checked from the filesystem rather than from the build result, so it still fires on a repository
91
+ // whose build cannot run.
92
+ const COPILOT_NAMESPACE = 'com.github.copilot'
93
+ const declaredTargets = ext?.vendors ?? Object.keys(ext?.harnesses ?? {})
94
+ if (declaredTargets.includes('copilot-cli')) {
95
+ const nsDir = path.join(root, COPILOT_NAMESPACE)
96
+ const declaredPaths = (value, fallback) => {
97
+ if (value === undefined) return fallback === null ? [] : [fallback]
98
+ if (typeof value === 'string') return [value]
99
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'string')
100
+ if (value && Array.isArray(value.paths)) return value.paths.filter((entry) => typeof entry === 'string')
101
+ return []
102
+ }
103
+ const hasContent = (rel) => {
104
+ const abs = path.join(root, rel)
105
+ return fs.existsSync(abs) && fs.statSync(abs).isDirectory() && fs.readdirSync(abs).length > 0
106
+ }
107
+ const missing = []
108
+ // The same defaults the build derives from. Passing null here would make the check fire only for a
109
+ // manifest that names the path — and the manifest that names none is the common one.
110
+ for (const [kind, defaultPath] of [
111
+ ['agents', './agents/'],
112
+ ['commands', './commands/'],
113
+ ['rules', './rules/'],
114
+ ]) {
115
+ const roots = declaredPaths(ext?.[kind], defaultPath)
116
+ if (roots.some(hasContent) && !hasContent(path.join(COPILOT_NAMESPACE, kind))) missing.push(kind)
117
+ }
118
+ // hooks is a single file, declared as a path or inline in the manifest, and carries a default the
119
+ // manifest need not declare at all.
120
+ const inlineHooks = ext?.hooks !== null && typeof ext?.hooks === 'object' && 'hooks' in ext.hooks
121
+ const hookSources = inlineHooks ? [] : declaredPaths(ext?.hooks, './hooks/hooks.json')
122
+ const hasHooks = inlineHooks || hookSources.some((rel) => fs.existsSync(path.join(root, rel)))
123
+ if (hasHooks && !fs.existsSync(path.join(nsDir, 'hooks', 'hooks.json'))) missing.push('hooks')
124
+ const lspSources = declaredPaths(ext?.lspServers, null)
125
+ if (lspSources.some((rel) => fs.existsSync(path.join(root, rel))) && !fs.existsSync(path.join(nsDir, 'lsp.json'))) {
126
+ missing.push('lspServers')
127
+ }
128
+ if (missing.length > 0) {
129
+ add(
130
+ 'copilot-root-components',
131
+ 'high',
132
+ `${missing.join(', ')} sit at the plugin root with no copy under ${COPILOT_NAMESPACE}/ — Copilot CLI reads them only from there once the canonical $schema is declared, so it loads none of them`,
133
+ 'universal-plugin plugin build',
134
+ )
135
+ }
136
+ }
137
+
87
138
  // Ask the shipped CLI what it would write, without writing it.
88
139
  const bin = path.join(packageRoot, 'bin', 'universal-plugin.mjs')
89
140
  const cli = fs.existsSync(bin)
@@ -106,6 +157,17 @@ if (build === null) {
106
157
  'codex is targeted without version or description — the build writes nothing at all, for any vendor',
107
158
  'add both to the canonical top level',
108
159
  )
160
+ } else if (findings.some((f) => f.code === 'legacy-manifest' || f.code === 'shadowing-manifest')) {
161
+ // The build stops rather than reporting a definitive empty state when the layout explains the
162
+ // empty result (issue #61). That is still the no-vendors diagnosis — the layout findings above
163
+ // name the signals, and this names the consequence. Keyed on those findings rather than on the
164
+ // build's message, so the two never have to agree on wording.
165
+ add(
166
+ 'no-vendors',
167
+ 'medium',
168
+ 'no vendor is declared — the project is on the pre-0.6 layout, so the build derives nothing and no runtime reads this plugin',
169
+ '/universal-plugin:init, adopt route',
170
+ )
109
171
  } else {
110
172
  add(
111
173
  'build-failed',
@@ -53,5 +53,8 @@ touch CI, repository settings, or unrelated project files.
53
53
  ## References
54
54
 
55
55
  - [Spec](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md)
56
- - [Schema](https://raw.githubusercontent.com/cyberuni/universal-plugin/refs/heads/main/schema/v1.json)
56
+ - [Manifest schema (Agent Plugins Specification v1.0.0)](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json)
57
+ - [Extension schema](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/schema/extension.schema.json)
58
+ — the body of `extensions["org.cyberuni.universal-plugin"]`, also shipped in the package at
59
+ `schema/extension.schema.json`
57
60
  - [Examples](https://github.com/cyberuni/universal-plugin/tree/main/examples)
@@ -10,10 +10,18 @@ Give a project one canonical plugin manifest — a root `plugin.json` on the Age
10
10
  Specification — and derive from it the manifest each runtime expects.
11
11
 
12
12
  Most of the manifest is shared. The divergence is small and asymmetric: Copilot CLI reads the
13
- canonical `plugin.json` directly and gets no derived file at all, while Claude Code, Cursor, and
13
+ canonical `plugin.json` directly and gets no derived *manifest*, while Claude Code, Cursor, and
14
14
  Codex each read their own path. Keep that asymmetry in mind — this is a consolidation job, not a
15
15
  copy-everywhere job.
16
16
 
17
+ That asymmetry does not extend to components. Declaring the canonical `$schema` puts a plugin in
18
+ Open Plugin Spec mode, and Copilot CLI then reads its **native** components — agents, commands,
19
+ rules, hooks, LSP servers — from a `com.github.copilot/` directory rather than the plugin root.
20
+ Authoring still happens at the canonical locations: `plugin build` derives that directory
21
+ ([ADR-0015](../../.agents/spec/design/decisions/0015-copilot-spec-mode-namespace.md)), so do not tell
22
+ an author to move `agents/` into it — the root copy is what the other three vendors derive from. See
23
+ [`references/vendors/copilot-cli.md`](./references/vendors/copilot-cli.md).
24
+
17
25
  `references/standard.md` defines the baseline every plugin gets. Read the vendor file for each
18
26
  runtime you are enabling, and only those.
19
27
 
@@ -22,7 +30,7 @@ runtime you are enabling, and only those.
22
30
  | Claude Code | `.claude-plugin/plugin.json` | none | `references/vendors/claude-code.md` |
23
31
  | Cursor | `.cursor-plugin/plugin.json` | none | `references/vendors/cursor.md` |
24
32
  | Codex | `.codex-plugin/plugin.json` | `version`, `description` | `references/vendors/codex.md` |
25
- | GitHub Copilot CLI | none — reads root `plugin.json` | none | `references/vendors/copilot-cli.md` |
33
+ | GitHub Copilot CLI | none — reads root `plugin.json` | native components derived under `com.github.copilot/` ([ADR-0015](../../.agents/spec/design/decisions/0015-copilot-spec-mode-namespace.md)) | `references/vendors/copilot-cli.md` |
26
34
 
27
35
  This skill owns the **authoring** side: the plugin a project ships. Setting a repository up to
28
36
  *consume* skills — the `.agents/skills/` layout, `AGENTS.md`, per-harness bridges — is
@@ -78,9 +86,12 @@ Sort each finding into exactly one bucket:
78
86
  - **adoptable** — a hand-written vendor manifest with no canonical manifest above it, a legacy root
79
87
  `plugin.json` carrying neither `$schema` nor `extensions`, or publicly-shipped skills with no
80
88
  manifest at all. These become canonical content in Phase 4.
81
- - **undeliverable** — a vendor-specific field with no delivery path, most often a
82
- `harnesses["copilot-cli"]` override: the canonical schema is closed, so the field cannot ride
83
- along in root and the build warns about it. Report; do not invent a home for it.
89
+ - **undeliverable** — a vendor-specific field with no delivery path, because Copilot CLI reads the
90
+ canonical manifest directly and has no derived file of its own. Two shapes reach this bucket: a
91
+ `harnesses["copilot-cli"]` override, which the build warns about; and a non-spec field on a
92
+ **legacy root `plugin.json`** — `category`, `tags` — which nothing warns about at all, because the
93
+ correct adoption never writes a `harnesses` entry for it. Report every one by name; do not invent
94
+ a home for it.
84
95
  - **not a plugin** — repo-private agent configuration (`.claude/skills/`, `.agents/skills/`,
85
96
  `.cursor/rules/`). It is this project's own tooling, not something it distributes. Say nothing
86
97
  about packaging it.
@@ -95,6 +106,11 @@ hand-written vendor manifests become generated artifacts, which vendors will be
95
106
  canonical metadata will say (show `name`, `version`, and `description` verbatim), and what is being
96
107
  left alone and why.
97
108
 
109
+ An adoption also presents **every non-spec field it is about to drop, by name and value** — the
110
+ undeliverable bucket from Phase 2. Nothing downstream warns about those, so this is the only notice
111
+ the user gets. [`references/adopt.md`](./references/adopt.md) Step 2 has the command that enumerates
112
+ them.
113
+
98
114
  Get explicit approval before any step that deletes, replaces, or rewrites a user-authored file —
99
115
  adoption always crosses that line, because it turns manifests the user maintains into build output.
100
116
  Creating a missing `plugin.json`, a missing component directory, or a missing skill scaffold needs
@@ -167,11 +183,16 @@ override both surface there and both are silent capability loss if ignored.
167
183
  When the work was an adoption, the proof is the diff:
168
184
 
169
185
  ```bash
170
- git diff -- .claude-plugin .cursor-plugin .codex-plugin .github/plugin
186
+ git diff -- plugin.json .claude-plugin .cursor-plugin .codex-plugin .github/plugin
171
187
  ```
172
188
 
173
- Expect only formatting and key-order churn. Any field that disappeared is a regression, not a
174
- cleanup — trace it back to the shared metadata or to that vendor's `harnesses` entry before shipping.
189
+ From the four derived paths, expect only formatting and key-order churn. Root `plugin.json` is
190
+ different and is in the list on purpose: it shows real hunks — the `$schema`/`extensions` rewrite —
191
+ and under the legacy layout it *was* Copilot CLI's manifest, so it is the one file where a field can
192
+ vanish unnoticed. Read those hunks rather than skimming them.
193
+
194
+ Any field that disappeared is a regression, not a cleanup — trace it back to the shared metadata, to
195
+ that vendor's `harnesses` entry, or to the drop list you reported in Phase 3, before shipping.
175
196
 
176
197
  Audit each skill the plugin ships, per [`references/create.md`](./references/create.md) Step 6.
177
198
  Report what was created, adopted, derived, and left alone. For a fuller read of the plugin's state
@@ -186,7 +207,9 @@ This skill is not a formatter. If the project has one, run it over the written f
186
207
  - **Never hand-edit a `version` field.** Two authored files and every derived artifact fall out of
187
208
  sync. The `version` skill owns that move.
188
209
  - **Adoption is lossless by contract.** Every vendor that worked before must still work after, and
189
- the Phase 5 diff is the check that proves it.
210
+ the Phase 5 diff — root `plugin.json` included — is the check that proves it. Where a field
211
+ genuinely cannot be carried, losing it is a decision the user makes, not one the adoption makes
212
+ quietly: name it first.
190
213
  - **Do not package repo-private agent configuration.** A `.claude/skills/` directory is the project's
191
214
  own tooling; offering to publish it is wrong.
192
215
  - **Do not convert vendor settings without a documented mapping.** Hooks have one: author them in
@@ -215,5 +238,7 @@ This skill is not a formatter. If the project has one, run it over the written f
215
238
 
216
239
  - Governance: `npx universal-plugin governance show plugin-design`
217
240
  - Spec: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md
218
- - Schema: https://raw.githubusercontent.com/cyberuni/universal-plugin/refs/heads/main/schema/v1.json
241
+ - Manifest schema (Agent Plugins Specification v1.0.0): https://agent-plugins.org/schemas/1.0.0/plugin.schema.json
242
+ - Extension schema (the body of `extensions["org.cyberuni.universal-plugin"]`), shipped in the package at
243
+ `schema/extension.schema.json`: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/schema/extension.schema.json
219
244
  - Examples: https://github.com/cyberuni/universal-plugin/tree/main/examples
@@ -41,19 +41,94 @@ canonical manifest (has `$schema` pointing at `agent-plugins.org` and an `extens
41
41
  which case there is nothing to adopt; route to `update.md`, or hand off to `doctor`, instead), or a legacy
42
42
  Copilot CLI manifest that now collides with the canonical path and must be folded in.
43
43
 
44
- ## Step 2 — Sort every field into shared vs vendor-specific
44
+ ## Step 2 — Sort every field into shared, vendor-specific, and undeliverable
45
45
 
46
- Build two buckets from the manifests you inventoried:
46
+ Build three buckets from the manifests you inventoried:
47
47
 
48
- - **Shared metadata** — `name`, `version`, `description`, `author`, `homepage`, `repository`,
49
- `license`, `keywords`, and the component paths. These go at the canonical top level.
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).
50
53
  - **Vendor-specific** — anything only one runtime understands (Cursor's `publisher`/`category`/
51
- `tags`, Codex's `interface`, Copilot's `category`/`tags`). These go under
52
- `extensions["org.cyberuni.universal-plugin"].harnesses.<vendor>`.
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.
53
65
 
54
66
  Where two vendor manifests disagree on a shared field, **ask the user** which value is canonical
55
67
  rather than picking one. A silent choice here is a silent behavior change for one of their runtimes.
56
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
+
57
132
  ## Step 3 — Write the canonical manifest
58
133
 
59
134
  Scaffold it, naming exactly the vendors you found in Step 1:
@@ -85,8 +160,8 @@ regenerated rather than edited.
85
160
  - Tell the user they are generated from here on, and that hand-edits will be overwritten by
86
161
  `plugin build`.
87
162
  - If the project has a legacy root `plugin.json` for Copilot CLI, that path is now the canonical
88
- manifest — its derived Copilot output moves elsewhere. Check the vendor output table in
89
- [`create.md`](./create.md) Step 2 for the current path.
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.
90
165
 
91
166
  ## Step 5 — Build
92
167
 
@@ -99,14 +174,27 @@ npx universal-plugin plugin build
99
174
  This is the point of the whole procedure.
100
175
 
101
176
  ```bash
102
- git diff -- .claude-plugin .cursor-plugin .codex-plugin .github/plugin
177
+ git diff -- plugin.json .claude-plugin .cursor-plugin .codex-plugin .github/plugin
103
178
  ```
104
179
 
105
- Read every line of that diff. Expect only formatting and key-order churn.
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.
106
194
 
107
- **Any field that disappeared is a regression**, not a cleanup. Trace it back: either it belongs in
108
- the shared metadata, or it belongs in that vendor's `harnesses` entry, or it is a field the build
109
- does not yet support — in which case stop and tell the user rather than shipping a quiet
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
110
198
  capability loss.
111
199
 
112
200
  Then confirm the plugin still loads. See [`create.md`](./create.md) Step 8 for local install.
@@ -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**. Adoption reproduces
24
- all of it, and the Phase 5 diff is checked field by field.
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
 
@@ -1,9 +1,11 @@
1
1
  # GitHub Copilot CLI
2
2
 
3
- Reads the **canonical root `plugin.json` directly**. The build derives nothing for it and writes no
4
- file — `plugin build` reports it with status `canonical`, which is success, not a skipped target.
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.
5
7
 
6
- ## Why nothing is derived
8
+ ## Why no manifest is derived
7
9
 
8
10
  Copilot CLI searches four paths and takes the first match:
9
11
 
@@ -12,8 +14,9 @@ Copilot CLI searches four paths and takes the first match:
12
14
  ```
13
15
 
14
16
  Root `plugin.json` — the canonical manifest — is second, so it always shadows the two below it. It
15
- has consumed Open Plugin Spec v1 manifests since v1.0.74, so it already serves the canonical manifest
16
- as-is.
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.
17
20
 
18
21
  Earlier builds wrote `.github/plugin/plugin.json`. That path loses to root by construction and was
19
22
  never read; a leftover copy is stale and safe to delete.
@@ -32,22 +35,167 @@ directly — these fields are not delivered
32
35
  Treat that warning as a decision to make, not noise: either the field belongs to a vendor that has a
33
36
  derived manifest, or it does not ship. Do not invent a path for it.
34
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
+
35
175
  ## Do not
36
176
 
37
177
  - **Do not delete root `plugin.json` to "clean up" a Copilot target.** It is the source of truth and
38
178
  the Copilot manifest at once.
39
179
  - Do not write `.plugin/plugin.json`. It outranks root, so it would silently shadow the canonical
40
- manifest with a copy nothing regenerates.
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.
41
188
 
42
189
  ## Hooks
43
190
 
44
191
  Copilot CLI accepts **either casing**, and the casing selects the payload format: PascalCase gets the
45
- Claude-compatible format, so the canonical file reaches Copilot CLI unchanged. Because Copilot CLI
46
- reads that file directly, the build derives nothing for it — an `agent` handler is reported as ignored
47
- at runtime rather than dropped. See [`claude-code.md`](./claude-code.md).
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).
48
196
 
49
197
  ## Dependencies
50
198
 
51
- Copilot CLI reads no plugin dependency. Because it reads the canonical manifest directly, there is no
52
- derived file to leave the declaration out of — it sits under `extensions`, which Copilot CLI ignores,
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,
53
201
  and the build reports it as ignored at runtime. See [`claude-code.md`](./claude-code.md).