From 3f26c17748e4038fff7796bd453ad569e3eb1537 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Wed, 9 Sep 2026 19:17:23 +0200 Subject: [PATCH] fix(cli): carry the catalog's recommended field under metadata, where Claude Code does not warn `claude plugin validate` warned `Unknown field 'recommended'` on our source catalog and on every claude build, and CI now runs that validation on each pull request. Measured against Claude Code's validator: a plugin entry's `metadata` object is the one place a catalog may carry its own field without a warning, while `category`, `tags` and `keywords` are official and mean something else. The field moves there: the source catalog, the fixtures, the schema, the repository validator, the parser (which reads no other spelling) and the claude-style catalog every build writes. The nine-cell golden is re-baselined for claude and cursor, whose catalogs now carry `metadata.recommended`. Red first: the parser test failed with `expected false to be true`, the catalog builder test with `expected undefined to strictly equal { recommended: false }`. Closes #800 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60 --- .claude-plugin/marketplace.json | 16 ++++++++-------- .../schemas/claude-marketplace-manifest.json | 9 +++++++-- cli/src/contexts/distribution/domain/catalog.ts | 8 +++++++- cli/src/contexts/tools/domain/build-contract.ts | 2 +- .../contexts/tools/domain/marketplace-catalog.ts | 14 ++++++++++++-- .../distribution/domain/catalog.unit.test.ts | 12 ++++++++++-- .../plugin/plugin-search-use-case.unit.test.ts | 4 ++-- .../domain/marketplace-catalog.unit.test.ts | 11 ++++++----- .../claude-marketplace-manifest.unit.test.ts | 2 +- cli/tests/e2e/plugin-install.e2e.test.ts | 2 +- .../.claude-plugin/marketplace.json | 2 +- .../.claude-plugin/marketplace.json | 12 ++++++------ .../framework-v2/.claude-plugin/marketplace.json | 2 +- .../framework/.claude-plugin/marketplace.json | 2 +- .../.claude-plugin/marketplace.json | 4 ++-- .../golden/snapshots/framework-build/golden.json | 4 ++-- .../prompts/plugin-pick-use-case.unit.test.ts | 10 +++++----- docs/CREATE_PLUGIN.md | 2 +- scripts/__tests__/validate-json.test.js | 2 +- scripts/validate-json.mjs | 4 +++- 20 files changed, 78 insertions(+), 46 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 76dce5b28..b6571ce7d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,56 +12,56 @@ "source": "./plugins/aidd-context", "description": "Knowledge production: project bootstrap, project init, generation of context artifacts (skills, agents, rules, commands, hooks), mermaid diagrams, learn, discovery", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-dev", "source": "./plugins/aidd-dev", "description": "Code transformation: plan, implement, assert, audit, review, test, refactor, debug, for-sure. Hosts engineering agents.", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-vcs", "source": "./plugins/aidd-vcs", "description": "External artifacts: commit, pull-request, release-tag, issue-create", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-pm", "source": "./plugins/aidd-pm", "description": "Product backlog artifacts, refinement, Product Briefs, Epics, User Stories, Tasks, Spikes, Defects, requirements, and specs.", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-orchestrator", "source": "./plugins/aidd-orchestrator", "description": "Orchestration: synchronous SDLC pipeline, async issue-to-PR automation, and product backlog.", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-refine", "source": "./plugins/aidd-refine", "description": "Meta-cognition: refine input through brainstorming, refine output through challenge, blind-spot scanning, and fact-checking.", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-ui", "source": "./plugins/aidd-ui", "description": "ALPHA, not ready for use. UI and UX concern: design, review, and improve frontend interfaces.", "strict": true, - "recommended": false + "metadata": { "recommended": false } }, { "name": "aidd-telemetry", "source": "./plugins/aidd-telemetry", "description": "Measurement: journals every session so a unit of work can be tied to what it cost. The hooks record on plain node; its three skills reach the `aidd` CLI to turn it on, answer and check.", "strict": true, - "recommended": false + "metadata": { "recommended": false } } ] } diff --git a/cli/assets/schemas/claude-marketplace-manifest.json b/cli/assets/schemas/claude-marketplace-manifest.json index 2694b40da..b2bf6bce8 100644 --- a/cli/assets/schemas/claude-marketplace-manifest.json +++ b/cli/assets/schemas/claude-marketplace-manifest.json @@ -50,8 +50,13 @@ "strict": { "type": "boolean" }, - "recommended": { - "type": "boolean" + "metadata": { + "type": "object", + "properties": { + "recommended": { + "type": "boolean" + } + } } } } diff --git a/cli/src/contexts/distribution/domain/catalog.ts b/cli/src/contexts/distribution/domain/catalog.ts index cf4dcc3fa..9cd46b409 100644 --- a/cli/src/contexts/distribution/domain/catalog.ts +++ b/cli/src/contexts/distribution/domain/catalog.ts @@ -36,7 +36,7 @@ function parseEntry(raw: unknown, index: number): PluginCatalogEntry { const entry: PluginCatalogEntry = { name: obj.name, source, - recommended: typeof obj.recommended === "boolean" ? obj.recommended : false, + recommended: recommendedUnderMetadata(obj.metadata), strict: typeof obj.strict === "boolean" ? obj.strict : false, }; @@ -46,6 +46,12 @@ function parseEntry(raw: unknown, index: number): PluginCatalogEntry { return entry; } +function recommendedUnderMetadata(metadata: unknown): boolean { + if (metadata === null || typeof metadata !== "object") return false; + const { recommended } = metadata as Record; + return typeof recommended === "boolean" ? recommended : false; +} + export function hasRelativePluginSources(catalog: PluginCatalog): boolean { return catalog.plugins.some( (entry) => entry.source.kind === "local" && !isAbsolute(entry.source.path) diff --git a/cli/src/contexts/tools/domain/build-contract.ts b/cli/src/contexts/tools/domain/build-contract.ts index 488c944a2..799c6c337 100644 --- a/cli/src/contexts/tools/domain/build-contract.ts +++ b/cli/src/contexts/tools/domain/build-contract.ts @@ -142,7 +142,7 @@ export interface SourcePluginEntryRef { readonly version?: string; readonly description?: string; readonly strict?: boolean; - readonly recommended?: boolean; + readonly metadata?: { readonly recommended?: boolean }; readonly [key: string]: unknown; } diff --git a/cli/src/contexts/tools/domain/marketplace-catalog.ts b/cli/src/contexts/tools/domain/marketplace-catalog.ts index 470e7e792..40afff08f 100644 --- a/cli/src/contexts/tools/domain/marketplace-catalog.ts +++ b/cli/src/contexts/tools/domain/marketplace-catalog.ts @@ -15,7 +15,12 @@ import type { FileWriter } from "../../../kernel/ports/file-writer.js"; import type { PluginPresence } from "./build-contract.js"; type SrcEntry = - | { version?: string; description?: string; strict?: boolean; recommended?: boolean } + | { + version?: string; + description?: string; + strict?: boolean; + metadata?: { recommended?: boolean }; + } | undefined; /** Marketplace-mode agent transform shared by claude and copilot: the frontmatter is kept @@ -91,7 +96,12 @@ export function buildClaudeStyleCatalogEntry( version, }; if (typeof srcEntry?.strict === "boolean") entry.strict = srcEntry.strict; - if (typeof srcEntry?.recommended === "boolean") entry.recommended = srcEntry.recommended; + const metadata = srcEntry?.metadata; + const recommended = + metadata !== null && typeof metadata === "object" + ? (metadata as { recommended?: unknown }).recommended + : undefined; + if (typeof recommended === "boolean") entry.metadata = { recommended }; return entry; } diff --git a/cli/tests/contexts/distribution/domain/catalog.unit.test.ts b/cli/tests/contexts/distribution/domain/catalog.unit.test.ts index 7ff8c0092..03abcadde 100644 --- a/cli/tests/contexts/distribution/domain/catalog.unit.test.ts +++ b/cli/tests/contexts/distribution/domain/catalog.unit.test.ts @@ -14,14 +14,14 @@ const VALID_RAW = { name: "dev", source: { kind: "local", path: "./plugins/dev" }, description: "Dev plugin", - recommended: true, + metadata: { recommended: true }, strict: true, }, { name: "pm", source: { kind: "github", repo: "ai-driven-dev/aidd-pm" }, description: "PM plugin", - recommended: false, + metadata: { recommended: false }, strict: false, }, ], @@ -94,6 +94,14 @@ describe("parsePluginCatalog", () => { expect(catalog.plugins[0].recommended).toBe(false); }); + it("reads recommended under the entry's metadata alone, the one place Claude Code does not warn on", () => { + const raw = { + plugins: [{ name: "x", source: { kind: "local", path: "./x" }, recommended: true }], + }; + const catalog = parsePluginCatalog(raw); + expect(catalog.plugins[0].recommended).toBe(false); + }); + it("defaults strict to false when absent", () => { const raw = { plugins: [{ name: "x", source: { kind: "local", path: "./x" } }] }; const catalog = parsePluginCatalog(raw); diff --git a/cli/tests/contexts/framework/application/plugin/plugin-search-use-case.unit.test.ts b/cli/tests/contexts/framework/application/plugin/plugin-search-use-case.unit.test.ts index 17e17fdd8..8719269ce 100644 --- a/cli/tests/contexts/framework/application/plugin/plugin-search-use-case.unit.test.ts +++ b/cli/tests/contexts/framework/application/plugin/plugin-search-use-case.unit.test.ts @@ -86,8 +86,8 @@ describe("PluginSearchUseCase", () => { it("filters by --recommended", async () => { const fs = new InMemoryFileAdapter(); seedMarketplace(fs, MKT1_PATH, [ - { name: "a", source: { kind: "github", repo: "x/y" }, recommended: true }, - { name: "b", source: { kind: "github", repo: "x/y" }, recommended: false }, + { name: "a", source: { kind: "github", repo: "x/y" }, metadata: { recommended: true } }, + { name: "b", source: { kind: "github", repo: "x/y" }, metadata: { recommended: false } }, ]); const registry = new InMemoryMarketplaceRegistry(); await registry.save( diff --git a/cli/tests/contexts/tools/domain/marketplace-catalog.unit.test.ts b/cli/tests/contexts/tools/domain/marketplace-catalog.unit.test.ts index a3b9a9d53..5567e9ea6 100644 --- a/cli/tests/contexts/tools/domain/marketplace-catalog.unit.test.ts +++ b/cli/tests/contexts/tools/domain/marketplace-catalog.unit.test.ts @@ -241,19 +241,20 @@ describe("buildClaudeStyleCatalogEntry", () => { expect(entry.version).toBe("1.0.0"); }); - it("passes through strict and recommended when present", () => { + it("passes strict through, and recommended under metadata, where Claude Code does not warn", () => { const entry = buildClaudeStyleCatalogEntry("aidd-dev", "desc", "1.0.0", { strict: true, - recommended: false, + metadata: { recommended: false }, }); expect(entry.strict).toBe(true); - expect(entry.recommended).toBe(false); + expect(entry.metadata).toStrictEqual({ recommended: false }); + expect(entry).not.toHaveProperty("recommended"); }); - it("omits strict and recommended when absent", () => { + it("omits strict and metadata when absent", () => { const entry = buildClaudeStyleCatalogEntry("aidd-dev", "desc", "1.0.0", undefined); expect(entry.strict).toBeUndefined(); - expect(entry.recommended).toBeUndefined(); + expect(entry.metadata).toBeUndefined(); }); it("only includes strict when it is boolean (not string/number)", () => { diff --git a/cli/tests/contexts/translate/infrastructure/claude-marketplace-manifest.unit.test.ts b/cli/tests/contexts/translate/infrastructure/claude-marketplace-manifest.unit.test.ts index bab60810a..8732ba4aa 100644 --- a/cli/tests/contexts/translate/infrastructure/claude-marketplace-manifest.unit.test.ts +++ b/cli/tests/contexts/translate/infrastructure/claude-marketplace-manifest.unit.test.ts @@ -56,7 +56,7 @@ describe("claude-marketplace-manifest.json schema", () => { description: "Dev plugin", version: "1.0.0", strict: true, - recommended: true, + metadata: { recommended: true }, }, ], }) diff --git a/cli/tests/e2e/plugin-install.e2e.test.ts b/cli/tests/e2e/plugin-install.e2e.test.ts index 90e33ff83..9625940f0 100644 --- a/cli/tests/e2e/plugin-install.e2e.test.ts +++ b/cli/tests/e2e/plugin-install.e2e.test.ts @@ -149,7 +149,7 @@ describe.concurrent("E2E: aidd plugin marketplace", () => { source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", description: "Sample", - recommended: true, + metadata: { recommended: true }, }, ]); await runCli(["marketplace", "add", "local", marketDir, "--yes"], projectDir, fakeHome); diff --git a/cli/tests/fixtures/framework-codex/.claude-plugin/marketplace.json b/cli/tests/fixtures/framework-codex/.claude-plugin/marketplace.json index 0ad0af71e..b6563e6f5 100644 --- a/cli/tests/fixtures/framework-codex/.claude-plugin/marketplace.json +++ b/cli/tests/fixtures/framework-codex/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "source": "./plugins/aidd-codex-fixture", "description": "Fixture plugin for codex build target tests", "strict": true, - "recommended": true + "metadata": { "recommended": true } } ] } diff --git a/cli/tests/fixtures/framework-real/.claude-plugin/marketplace.json b/cli/tests/fixtures/framework-real/.claude-plugin/marketplace.json index 9e0c74a82..b430ec222 100644 --- a/cli/tests/fixtures/framework-real/.claude-plugin/marketplace.json +++ b/cli/tests/fixtures/framework-real/.claude-plugin/marketplace.json @@ -11,7 +11,7 @@ "source": "./plugins/aidd-context", "description": "Knowledge production: project bootstrap, project init, context generation, mermaid diagrams, learn, discovery", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-dev", @@ -19,7 +19,7 @@ "source": "./plugins/aidd-dev", "description": "Code transformation: SDLC orchestrator, plan, assert, audit, review, test, refactor, debug, for-sure. Hosts agents.", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-vcs", @@ -27,7 +27,7 @@ "source": "./plugins/aidd-vcs", "description": "External artifacts: commit, pull-request, release-tag, issue-create", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-pm", @@ -35,7 +35,7 @@ "source": "./plugins/aidd-pm", "description": "Product management (release candidate): ticket-info, user-stories-create, prd", "strict": true, - "recommended": true + "metadata": { "recommended": true } }, { "name": "aidd-async-dev", @@ -43,7 +43,7 @@ "source": "./plugins/aidd-async-dev", "description": "Async development orchestration: turn ready issues into pull requests, then iterate on review feedback until a human takes over.", "strict": true, - "recommended": false + "metadata": { "recommended": false } }, { "name": "aidd-refine", @@ -51,7 +51,7 @@ "source": "./plugins/aidd-refine", "description": "Meta-cognition: refine input through brainstorming, refine output through challenge and condensed communication mode.", "strict": true, - "recommended": true + "metadata": { "recommended": true } } ] } diff --git a/cli/tests/fixtures/framework-v2/.claude-plugin/marketplace.json b/cli/tests/fixtures/framework-v2/.claude-plugin/marketplace.json index 4babf2bfd..9568ebe45 100644 --- a/cli/tests/fixtures/framework-v2/.claude-plugin/marketplace.json +++ b/cli/tests/fixtures/framework-v2/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "source": "./plugins/aidd-test", "description": "Test plugin", "strict": true, - "recommended": true + "metadata": { "recommended": true } } ] } diff --git a/cli/tests/fixtures/framework/.claude-plugin/marketplace.json b/cli/tests/fixtures/framework/.claude-plugin/marketplace.json index 21f862318..ed4146f22 100644 --- a/cli/tests/fixtures/framework/.claude-plugin/marketplace.json +++ b/cli/tests/fixtures/framework/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "source": "./plugins/aidd-test", "description": "Test plugin", "strict": true, - "recommended": true + "metadata": { "recommended": true } } ] } diff --git a/cli/tests/fixtures/framework/marketplace-sample/.claude-plugin/marketplace.json b/cli/tests/fixtures/framework/marketplace-sample/.claude-plugin/marketplace.json index ff724bd62..0891b410a 100644 --- a/cli/tests/fixtures/framework/marketplace-sample/.claude-plugin/marketplace.json +++ b/cli/tests/fixtures/framework/marketplace-sample/.claude-plugin/marketplace.json @@ -4,14 +4,14 @@ "name": "dev", "source": { "kind": "local", "path": "./plugins/dev" }, "description": "Dev plugin", - "recommended": true, + "metadata": { "recommended": true }, "strict": true }, { "name": "pm", "source": { "kind": "github", "repo": "ai-driven-dev/aidd-pm" }, "description": "PM plugin", - "recommended": false, + "metadata": { "recommended": false }, "strict": false } ] diff --git a/cli/tests/golden/snapshots/framework-build/golden.json b/cli/tests/golden/snapshots/framework-build/golden.json index c21e1f80a..ae1e4c4af 100644 --- a/cli/tests/golden/snapshots/framework-build/golden.json +++ b/cli/tests/golden/snapshots/framework-build/golden.json @@ -597,7 +597,7 @@ "plugins/aidd-async-dev/hooks/hooks.json": "78922a784ee78e9e50587e93628cd3b9d4dfbe49087adc4514e6781cea38cbb9", "plugins/aidd-async-dev/agents/async-orchestrator.md": "8b29ba73f27414e75c89c6453c0557212790d00d6becfadba363466999f2c9de", "plugins/aidd-async-dev/.claude-plugin/plugin.json": "04cce92dc7c10e8041807220c255005c0510abbbb0322176f2a8a254808e4466", - ".claude-plugin/marketplace.json": "d660f4eb03d90f2b2384c9ff61626d6ded7ede5664816681991ddebe622e099f" + ".claude-plugin/marketplace.json": "d197f330a5787647d58c7dc20253c8ef8b59c4dce115fdd7f12de45ccec7b18e" }, "cursor": { "plugins/aidd-vcs/skills/04-issue-create/SKILL.md": "41fb7f904c88a07eb3ab94c6137c130166fa44069ed76322fc6eb78b81a7b5a4", @@ -797,7 +797,7 @@ "plugins/aidd-async-dev/hooks/hooks.json": "78922a784ee78e9e50587e93628cd3b9d4dfbe49087adc4514e6781cea38cbb9", "plugins/aidd-async-dev/agents/async-orchestrator.md": "8b29ba73f27414e75c89c6453c0557212790d00d6becfadba363466999f2c9de", "plugins/aidd-async-dev/.cursor-plugin/plugin.json": "8d9b5b5edc8ac5d51cdb24bc5605b30ed3149616bfbbcc69db45f35d477315d5", - ".cursor-plugin/marketplace.json": "d660f4eb03d90f2b2384c9ff61626d6ded7ede5664816681991ddebe622e099f" + ".cursor-plugin/marketplace.json": "d197f330a5787647d58c7dc20253c8ef8b59c4dce115fdd7f12de45ccec7b18e" }, "claude:flat": { ".mcp.json": "1bbb467a5d737b5c65daf9fe8551c9ced50608a09f523d5d20baa9150f266f73", diff --git a/cli/tests/presentation/prompts/plugin-pick-use-case.unit.test.ts b/cli/tests/presentation/prompts/plugin-pick-use-case.unit.test.ts index 4ece7fb09..38642d781 100644 --- a/cli/tests/presentation/prompts/plugin-pick-use-case.unit.test.ts +++ b/cli/tests/presentation/prompts/plugin-pick-use-case.unit.test.ts @@ -95,7 +95,7 @@ describe("PluginPickUseCase", () => { name: "sample-plugin", source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", - recommended: true, + metadata: { recommended: true }, }, ]); await registry.save( @@ -170,7 +170,7 @@ describe("PluginPickUseCase", () => { source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", description: "A sample plugin used in tests", - recommended: true, + metadata: { recommended: true }, strict: true, }, ]); @@ -246,7 +246,7 @@ describe("PluginPickUseCase โ€” what it asks, and when it asks nothing", () => { source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", description: "A sample plugin used in tests", - recommended: true, + metadata: { recommended: true }, }, { name: "bare-plugin", source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0" }, ]); @@ -274,7 +274,7 @@ describe("PluginPickUseCase โ€” what it hands the installer", () => { name: "sample-plugin", source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", - recommended: true, + metadata: { recommended: true }, strict: true, }, ]); @@ -304,7 +304,7 @@ describe("PluginPickUseCase โ€” what it hands the installer", () => { name: "sample-plugin", source: { kind: "local", path: PLUGIN_FIXTURE }, version: "1.0.0", - recommended: true, + metadata: { recommended: true }, }, ]); await registerMarketplace(registry, "local", MKT_DIR); diff --git a/docs/CREATE_PLUGIN.md b/docs/CREATE_PLUGIN.md index 8d3e6ceb5..232882f01 100644 --- a/docs/CREATE_PLUGIN.md +++ b/docs/CREATE_PLUGIN.md @@ -15,7 +15,7 @@ Pick a name (lowercase, `aidd-`). For the directory shape, `plugin.json`, and ## ๐Ÿ“ Register -- **Marketplace** โ€” append an entry to `.claude-plugin/marketplace.json`: `name`, `source` (required, `./plugins/aidd-`), `strict: true`, `recommended: false` (keeps it off the curated install path until it stabilises). +- **Marketplace** โ€” append an entry to `.claude-plugin/marketplace.json`: `name`, `source` (required, `./plugins/aidd-`), `strict: true`, `metadata: { "recommended": false }` (keeps it off the curated install path until it stabilises; under `metadata` because Claude Code warns on any other field of its own). - **Release** โ€” add the package to `release-please-config.json` `packages` **and** `.release-please-manifest.json`, or it never versions. ## ๐Ÿงช Try locally diff --git a/scripts/__tests__/validate-json.test.js b/scripts/__tests__/validate-json.test.js index 48a293d7d..05095641d 100644 --- a/scripts/__tests__/validate-json.test.js +++ b/scripts/__tests__/validate-json.test.js @@ -84,7 +84,7 @@ describe("validate-json", () => { }); it("names a marketplace plugin listed twice and a source that is not there", async () => { - const plugin = { name: "dup", version: "1.0.0", source: "./plugins/dup", description: "d", strict: true, recommended: false }; + const plugin = { name: "dup", version: "1.0.0", source: "./plugins/dup", description: "d", strict: true, metadata: { recommended: false } }; const root = tree({ ".claude-plugin/marketplace.json": { name: "m", version: "1.0.0", description: "d", owner: { name: "o" }, plugins: [plugin, plugin] }, }); diff --git a/scripts/validate-json.mjs b/scripts/validate-json.mjs index a57f3128d..2a721912e 100755 --- a/scripts/validate-json.mjs +++ b/scripts/validate-json.mjs @@ -157,7 +157,9 @@ export function createValidator({ root, loadSchema = fetchSchema }) { if (names.has(plugin.name)) fail(file, `duplicate plugin name: ${plugin.name}`); names.add(plugin.name); if (typeof plugin.strict !== "boolean") fail(file, `${label}.strict must be boolean`); - if (typeof plugin.recommended !== "boolean") fail(file, `${label}.recommended must be boolean`); + if (typeof plugin.metadata?.recommended !== "boolean") { + fail(file, `${label}.metadata.recommended must be boolean`); + } if (typeof plugin.source === "string") await pathExists(file, plugin.source, `${label}.source`, root); } }