universal-plugin 0.5.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/com.github.copilot/agents/agentskills-specialist.agent.md +132 -0
- package/dist/cli.mjs +727 -43
- package/governances/plugin-design.md +14 -0
- package/package.json +4 -1
- package/plugin.json +1 -1
- package/readme.md +4 -0
- package/schema/README.md +10 -0
- package/schema/claude-code-marketplace.json +1939 -0
- package/schema/extension.schema.json +846 -0
- package/skills/doctor/README.md +3 -1
- package/skills/doctor/SKILL.md +27 -6
- package/skills/doctor/scripts/doctor.mjs +90 -0
- package/skills/init/README.md +4 -1
- package/skills/init/SKILL.md +35 -10
- package/skills/init/references/adopt.md +101 -13
- package/skills/init/references/detection.md +8 -2
- package/skills/init/references/vendors/copilot-cli.md +159 -11
- package/skills/marketplace/README.md +14 -2
- package/skills/marketplace/SKILL.md +45 -6
- package/skills/marketplace/scripts/validate.mjs +11 -0
package/skills/doctor/README.md
CHANGED
|
@@ -8,7 +8,9 @@ disk still matches it for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
|
|
|
8
8
|
`scripts/doctor.mjs` composes the shipped CLI's `plugin build --dry-run --format json` with the
|
|
9
9
|
filesystem facts that build cannot see — whether each derived manifest exists, whether it predates
|
|
10
10
|
the canonical manifest, whether a stale or shadowing manifest is lying around, whether the two
|
|
11
|
-
authored version numbers still agree,
|
|
11
|
+
authored version numbers still agree, whether shipped content has moved since the version did, and
|
|
12
|
+
whether every marketplace catalog at the repository root is a shape its runtime loads. It emits one
|
|
13
|
+
JSON object: `vendors`, `findings`, `ok`.
|
|
12
14
|
|
|
13
15
|
The skill supplies the judgment around it: which finding matters, and which skill owns its repair.
|
|
14
16
|
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -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,
|
|
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`
|
|
52
|
-
|
|
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,11 +78,23 @@ 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 |
|
|
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` |
|
|
88
|
+
|
|
89
|
+
## Catalogs are checked at the repository root
|
|
90
|
+
|
|
91
|
+
The marketplace catalogs sit above the plugin in a monorepo, so the catalog check runs against the
|
|
92
|
+
repository root rather than `--root`. It reports only a catalog that would be **refused**: a missing
|
|
93
|
+
one is not a fault, and nothing here has an opinion on which catalogs a repository ought to carry.
|
|
94
|
+
|
|
95
|
+
The detail names the key at fault, so hand it to `/universal-plugin:marketplace` as it stands. An
|
|
96
|
+
entry's fields are derived from the plugin's `plugin.json`, and the catalog's own `name` and `owner`
|
|
97
|
+
are authored in the catalog — which half is at fault decides where the repair goes.
|
|
79
98
|
|
|
80
99
|
## Checking staleness properly
|
|
81
100
|
|
|
@@ -129,7 +148,9 @@ is meant to ship — content that is still being worked on is not a finding to a
|
|
|
129
148
|
- **Never repair.** Report the finding and name the skill that owns it.
|
|
130
149
|
- **Never hand-edit a derived manifest to make a finding go away.** The next build overwrites it and
|
|
131
150
|
the finding comes back.
|
|
132
|
-
- Do not report `copilot-cli` writing no
|
|
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.
|
|
133
154
|
- Do not treat repo-private agent configuration (`.claude/skills/`, `.agents/skills/`) as part of the
|
|
134
155
|
plugin. Diagnosing a repository's own skill wiring is `buddy-agent-harness:doctor`.
|
|
135
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',
|
|
@@ -199,8 +261,36 @@ if (manifest.version !== undefined && packagePath === null) {
|
|
|
199
261
|
}
|
|
200
262
|
}
|
|
201
263
|
|
|
264
|
+
// The marketplace catalogs a user installs from. They sit at the *repository* root, above a plugin in
|
|
265
|
+
// a monorepo, and each is read by its runtime at install time — a catalog whose shape that runtime
|
|
266
|
+
// refuses fails in the user's terminal, not here. The shipped CLI owns the rules; this only asks.
|
|
267
|
+
for (const row of invalidCatalogs()) {
|
|
268
|
+
add(
|
|
269
|
+
'invalid-catalog',
|
|
270
|
+
'high',
|
|
271
|
+
`${row.path} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}`,
|
|
272
|
+
'/universal-plugin:marketplace',
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
202
276
|
report({ vendors })
|
|
203
277
|
|
|
278
|
+
/** Every catalog the repository carries that its runtime would refuse. Empty when there is nothing to
|
|
279
|
+
* read, when the CLI is too old to answer, or when every catalog is fine — a missing catalog is not a
|
|
280
|
+
* fault, and this reports no opinion on which ones a repository ought to carry. */
|
|
281
|
+
function invalidCatalogs() {
|
|
282
|
+
const catalogRoot = git('rev-parse', '--show-toplevel') ?? root
|
|
283
|
+
const result = fs.existsSync(bin)
|
|
284
|
+
? spawnSync(process.execPath, [bin, 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
|
|
285
|
+
encoding: 'utf8',
|
|
286
|
+
})
|
|
287
|
+
: spawnSync('npx', ['universal-plugin', 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
})
|
|
290
|
+
const rows = readJson_stdout(result.stdout)
|
|
291
|
+
return Array.isArray(rows) ? rows.filter((row) => row.status === 'invalid') : []
|
|
292
|
+
}
|
|
293
|
+
|
|
204
294
|
/** Runs git inside `root`, returning its stdout or `null` — a non-zero status, a missing git, and a
|
|
205
295
|
* directory outside any repository are all the same answer here: no history to read. */
|
|
206
296
|
function git(...args) {
|
package/skills/init/README.md
CHANGED
|
@@ -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
|
-
- [
|
|
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)
|
package/skills/init/SKILL.md
CHANGED
|
@@ -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
|
|
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` |
|
|
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,
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
-
|
|
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
|
|
44
|
+
## Step 2 — Sort every field into shared, vendor-specific, and undeliverable
|
|
45
45
|
|
|
46
|
-
Build
|
|
46
|
+
Build three buckets from the manifests you inventoried:
|
|
47
47
|
|
|
48
|
-
- **Shared metadata** —
|
|
49
|
-
`
|
|
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
|
|
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
|
|
89
|
-
[`create.md`](./create.md) Step 2
|
|
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
|
-
|
|
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
|
|
108
|
-
the shared metadata, or it belongs in that vendor's `harnesses` entry, or it is a field
|
|
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
|
|
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
|
|