From 4c40cf878374466b7dd2769f8b3c4101ec8b1910 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 03:34:03 -0700 Subject: [PATCH] chore(plugins): inventory doctor deprecation compat --- .../openclaw-release-maintainer/SKILL.md | 35 ++- CHANGELOG.md | 1 + docs/plugins/compatibility.md | 16 + .../doctor/shared/deprecation-compat.test.ts | 78 +++++ .../doctor/shared/deprecation-compat.ts | 274 ++++++++++++++++++ src/plugins/compat/registry.test.ts | 25 ++ src/plugins/compat/registry.ts | 76 +++++ 7 files changed, 493 insertions(+), 12 deletions(-) create mode 100644 src/commands/doctor/shared/deprecation-compat.test.ts create mode 100644 src/commands/doctor/shared/deprecation-compat.ts diff --git a/.agents/skills/openclaw-release-maintainer/SKILL.md b/.agents/skills/openclaw-release-maintainer/SKILL.md index dd004c28a35..3aa8fbb179b 100644 --- a/.agents/skills/openclaw-release-maintainer/SKILL.md +++ b/.agents/skills/openclaw-release-maintainer/SKILL.md @@ -25,12 +25,22 @@ Use this skill for release and publish-time workflow. Keep ordinary development - Before release branching, commit any dirty files in coherent groups, push, pull/rebase, then run `/changelog` on `main` and commit/push/pull that changelog rewrite immediately before creating the release branch. -- During release planning, inspect `src/plugins/compat/registry.ts` before - branching and again before final publish. For every deprecated or - removal-pending compatibility record whose `removeAfter` date is on or before - the release date, either remove the compatibility path where safe and - validate the affected tests, or write down why removal is blocked and get - explicit maintainer approval before shipping the expired compatibility path. +- During release planning, inspect both `src/plugins/compat/registry.ts` and + `src/commands/doctor/shared/deprecation-compat.ts` before branching and again + before final publish. For every deprecated or removal-pending compatibility + record whose `removeAfter` date is on or before the release date, either + remove the compatibility path where safe and validate the affected tests, or + write down why removal is blocked and get explicit maintainer approval before + shipping the expired compatibility path. +- When removing deprecated runtime/config compatibility, preserve any doctor + migration, repair, or hint that is still needed by supported upgrade paths. + Doctor-side compatibility should stay tracked in + `src/commands/doctor/shared/deprecation-compat.ts` until maintainers confirm + the repair is no longer needed. +- Revalidate compatibility replacement text during release planning. The + recommended replacement can shift as plugin ownership, externalization, and + config footprint move, so do not blindly copy stale replacement annotations + into release notes. - Do not delete or rewrite beta tags after they leave the machine. If a published or pushed beta needs a fix, commit the fix on the release branch and increment to the next `-beta.N`. @@ -123,12 +133,13 @@ Use this skill for release and publish-time workflow. Keep ordinary development `CHANGELOG.md` version section, not highlights or an excerpt. When creating or editing a release, extract from `## YYYY.M.D` through the line before the next level-2 heading and use that complete block as the release notes. -- When preparing release notes, scan `src/plugins/compat/registry.ts` for - plugin compatibility records with `warningStarts` or `removeAfter` within 7 - days after the release date. Add an `Upcoming deprecations` note to the - release notes when any exist, including the compatibility code, target date, - replacement, and a link to the record's `docsPath` or `/plugins/compatibility` - when no more specific deprecation page exists. +- When preparing release notes, scan `src/plugins/compat/registry.ts` and + `src/commands/doctor/shared/deprecation-compat.ts` for compatibility records + with `warningStarts` or `removeAfter` within 7 days after the release date. + Add an `Upcoming deprecations` note to the release notes when any exist, + including the compatibility code, target date, replacement, and a link to the + record's `docsPath` or `/plugins/compatibility` when no more specific + deprecation page exists. - When cutting a mac release with a beta GitHub prerelease: - tag `vYYYY.M.D-beta.N` from the release commit - create a prerelease titled `openclaw YYYY.M.D-beta.N` diff --git a/CHANGELOG.md b/CHANGELOG.md index 18b5e8c8406..f4fea0bacab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex. - Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet. +- Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc. - Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc. - Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex. - Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @codex. diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md index 127e5ca0bb0..579c4572cbc 100644 --- a/docs/plugins/compatibility.md +++ b/docs/plugins/compatibility.md @@ -31,6 +31,18 @@ The registry is the source for maintainer planning and future plugin inspector checks. If a plugin-facing behavior changes, add or update the compatibility record in the same change that adds the adapter. +Doctor repair and migration compatibility is tracked separately at +`src/commands/doctor/shared/deprecation-compat.ts`. Those records cover old +config shapes, install-ledger layouts, and repair shims that may need to stay +available after the runtime compatibility path is removed. + +Release sweeps should check both registries. Do not delete a doctor migration +just because the matching runtime or config compatibility record expired; first +verify there is no supported upgrade path that still needs the repair. Also +revalidate each replacement annotation during release planning because plugin +ownership and config footprint can change as providers and channels move out of +core. + ## Plugin inspector package The plugin inspector should live outside the core OpenClaw repo as a separate @@ -112,6 +124,10 @@ Current compatibility records include: - persisted plugin registry disable and install-migration env flags while repair flows migrate operators to `openclaw plugins registry --refresh` and `openclaw doctor --fix` +- legacy plugin-owned web search, web fetch, and x_search config paths while + doctor migrates them to `plugins.entries..config` +- legacy `plugins.installs` authored config and bundled plugin load-path + aliases while install metadata moves into the state-managed plugin ledger New plugin code should prefer the replacement listed in the registry and in the specific migration guide. Existing plugins can keep using a compatibility path diff --git a/src/commands/doctor/shared/deprecation-compat.test.ts b/src/commands/doctor/shared/deprecation-compat.test.ts new file mode 100644 index 00000000000..2f09ecbba55 --- /dev/null +++ b/src/commands/doctor/shared/deprecation-compat.test.ts @@ -0,0 +1,78 @@ +import fs from "node:fs"; +import { describe, expect, it } from "vitest"; +import { + getDoctorDeprecationCompatRecord, + isDoctorDeprecationCompatCode, + listDeprecatedDoctorDeprecationCompatRecords, + listDoctorDeprecationCompatRecords, +} from "./deprecation-compat.js"; + +const datePattern = /^\d{4}-\d{2}-\d{2}$/u; + +const requiredDoctorCompatCodes = [ + "doctor-agent-runtime-embedded-harness", + "doctor-plugin-install-config-ledger", + "doctor-bundled-plugin-load-paths", + "doctor-web-search-plugin-config", + "doctor-web-fetch-plugin-config", + "doctor-x-search-plugin-config", +] as const; + +function parseDate(date: string): Date { + return new Date(`${date}T00:00:00Z`); +} + +function addUtcMonths(date: Date, months: number): Date { + const next = new Date(date); + next.setUTCMonth(next.getUTCMonth() + months); + return next; +} + +describe("doctor deprecation compatibility inventory", () => { + it("keeps compatibility codes unique and lookup-safe", () => { + const records = listDoctorDeprecationCompatRecords(); + const codes = records.map((record) => record.code); + + expect(new Set(codes).size).toBe(codes.length); + expect(isDoctorDeprecationCompatCode("doctor-web-search-plugin-config")).toBe(true); + expect(isDoctorDeprecationCompatCode("missing-code")).toBe(false); + expect(getDoctorDeprecationCompatRecord("doctor-web-search-plugin-config").owner).toBe( + "provider", + ); + }); + + it("tracks the known doctor migrations that protect plugin/config rollout", () => { + for (const code of requiredDoctorCompatCodes) { + expect(isDoctorDeprecationCompatCode(code), code).toBe(true); + } + }); + + it("requires dated deprecation metadata with a three-month maximum window", () => { + for (const record of listDeprecatedDoctorDeprecationCompatRecords()) { + expect(record.deprecated, record.code).toMatch(datePattern); + expect(record.warningStarts, record.code).toMatch(datePattern); + expect(record.removeAfter, record.code).toMatch(datePattern); + if (!record.warningStarts || !record.removeAfter) { + throw new Error(`${record.code} is missing deprecation window dates`); + } + const maxRemoveAfter = addUtcMonths(parseDate(record.warningStarts), 3); + const removeAfter = parseDate(record.removeAfter); + expect(removeAfter <= maxRemoveAfter, record.code).toBe(true); + } + }); + + it("keeps every record actionable", () => { + for (const record of listDoctorDeprecationCompatRecords()) { + expect(record.introduced, record.code).toMatch(datePattern); + expect(record.source, record.code).toBeTruthy(); + expect(record.migration, record.code).toBeTruthy(); + expect(record.replacement, record.code).toBeTruthy(); + expect(record.docsPath, record.code).toMatch(/^\//u); + expect(fs.existsSync(record.migration), `${record.code}: ${record.migration}`).toBe(true); + expect(record.tests.length, record.code).toBeGreaterThan(0); + for (const testPath of record.tests) { + expect(fs.existsSync(testPath), `${record.code}: ${testPath}`).toBe(true); + } + } + }); +}); diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts new file mode 100644 index 00000000000..8f473f6d817 --- /dev/null +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -0,0 +1,274 @@ +export type DoctorDeprecationCompatStatus = "active" | "deprecated" | "removal-pending" | "removed"; + +export type DoctorDeprecationCompatOwner = + | "agent-runtime" + | "audio" + | "browser" + | "channel" + | "config" + | "gateway" + | "plugin" + | "provider" + | "tools" + | "tts"; + +export type DoctorDeprecationCompatRecord = { + code: Code; + status: DoctorDeprecationCompatStatus; + owner: DoctorDeprecationCompatOwner; + introduced: string; + deprecated?: string; + warningStarts?: string; + removeAfter?: string; + source: string; + migration: string; + replacement: string; + docsPath: string; + tests: readonly string[]; + notes?: string; +}; + +const TODAY = "2026-04-26"; +const MAX_REMOVE_AFTER = "2026-07-26"; + +function deprecatedCompatRecord( + record: Omit< + DoctorDeprecationCompatRecord, + "deprecated" | "warningStarts" | "removeAfter" | "status" + > & + Partial< + Pick< + DoctorDeprecationCompatRecord, + "deprecated" | "removeAfter" | "status" | "warningStarts" + > + >, +): DoctorDeprecationCompatRecord { + return { + status: "deprecated", + deprecated: TODAY, + warningStarts: TODAY, + removeAfter: MAX_REMOVE_AFTER, + ...record, + }; +} + +// Doctor migrations and repair shims can outlive the runtime/config compatibility +// path they repair. Release removals must check this inventory before deleting +// doctor fixes, and replacement notes should be revalidated against the current +// architecture because ownership and config footprint can shift during rollout. +export const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ + deprecatedCompatRecord({ + code: "doctor-agent-runtime-embedded-harness", + owner: "agent-runtime", + introduced: "2026-04-25", + source: "agents.defaults.embeddedHarness; agents.list[].embeddedHarness", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.agentRuntime and agents.list[].agentRuntime", + docsPath: "/plugins/sdk-agent-harness", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + notes: + "Runtime-policy naming changed during the plugin architecture work; verify replacement wording against current agentRuntime docs before removal.", + }), + deprecatedCompatRecord({ + code: "doctor-agent-sandbox-persession", + owner: "agent-runtime", + introduced: "2026-04-26", + source: "agents.defaults.sandbox.perSession; agents.list[].sandbox.perSession", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.*.sandbox.scope", + docsPath: "/cli/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-top-level-memory-search", + owner: "config", + introduced: "2026-04-26", + source: "memorySearch", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.memorySearch", + docsPath: "/cli/doctor", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-top-level-heartbeat", + owner: "config", + introduced: "2026-04-26", + source: "heartbeat", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.heartbeat and channels.defaults.heartbeat", + docsPath: "/automation", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-gateway-bind-host-aliases", + owner: "gateway", + introduced: "2026-04-26", + source: "gateway.bind host aliases such as 0.0.0.0 and localhost", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.gateway.ts", + replacement: "gateway.bind.mode values such as lan, loopback, custom, tailnet, and auto", + docsPath: "/gateway/configuration", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-audio-transcription-command", + owner: "audio", + introduced: "2026-04-26", + source: "audio.transcription", + migration: "src/commands/doctor/shared/legacy-config-migrations.audio.ts", + replacement: "tools.media.audio.models", + docsPath: "/tools/media-overview", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-thread-binding-ttl", + owner: "channel", + introduced: "2026-04-26", + source: "threadBindings.ttlHours", + migration: "src/commands/doctor/shared/legacy-config-migrations.channels.ts", + replacement: "threadBindings.idleHours", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-dm-aliases", + owner: "channel", + introduced: "2026-04-26", + source: "channels..dm.policy and channels..dm.allowFrom", + migration: "src/config/channel-compat-normalization.ts", + replacement: "channels..dmPolicy and channels..allowFrom", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/channel-legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-channel-streaming-aliases", + owner: "channel", + introduced: "2026-04-26", + source: "streamMode, scalar streaming, chunkMode, blockStreaming, draftChunk, nativeStreaming", + migration: "src/config/channel-compat-normalization.ts", + replacement: "channels..streaming.*", + docsPath: "/channels/channel-routing", + tests: ["src/commands/doctor/shared/channel-legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-tts-provider-aliases", + owner: "tts", + introduced: "2026-04-26", + source: "messages.tts.openai/elevenlabs/edge and plugins.entries.voice-call.config.tts aliases", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.tts.ts", + replacement: "messages.tts.providers. and microsoft instead of edge", + docsPath: "/tools/tts", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-plugin-install-config-ledger", + owner: "plugin", + introduced: "2026-04-25", + source: "plugins.installs in authored config", + migration: "src/config/plugin-install-config-migration.ts", + replacement: "state-managed plugins/installs.json install ledger", + docsPath: "/cli/plugins#registry", + tests: [ + "src/config/io.write-config.test.ts", + "src/commands/doctor/shared/plugin-registry-migration.test.ts", + ], + }), + deprecatedCompatRecord({ + code: "doctor-bundled-plugin-load-paths", + owner: "plugin", + introduced: "2026-04-25", + source: "plugins.load.paths entries that point at bundled plugin source/dist locations", + migration: "src/commands/doctor/shared/bundled-plugin-load-paths.ts", + replacement: "packaged bundled plugins and the persisted plugin registry", + docsPath: "/cli/plugins#registry", + tests: ["src/commands/doctor/shared/bundled-plugin-load-paths.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-web-search-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.search.apiKey and tools.web.search.", + migration: "src/commands/doctor/shared/legacy-web-search-migrate.ts", + replacement: "plugins.entries..config.webSearch", + docsPath: "/tools/web", + tests: ["src/commands/doctor/shared/legacy-web-search-migrate.test.ts"], + notes: + "Provider/plugin ownership can move as bundled providers externalize; verify the current manifest owner before deleting migration support.", + }), + deprecatedCompatRecord({ + code: "doctor-web-fetch-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.fetch.firecrawl", + migration: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts", + replacement: "plugins.entries.firecrawl.config.webFetch", + docsPath: "/tools/web-fetch", + tests: ["src/commands/doctor/shared/legacy-web-fetch-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-x-search-plugin-config", + owner: "provider", + introduced: "2026-04-26", + source: "tools.web.x_search.apiKey", + migration: "src/commands/doctor/shared/legacy-x-search-migrate.ts", + replacement: "plugins.entries.xai.config.webSearch.apiKey", + docsPath: "/tools/grok-search", + tests: [ + "src/commands/doctor/shared/legacy-x-search-migrate.test.ts", + "src/commands/doctor/shared/legacy-config-migrate.test.ts", + ], + }), + deprecatedCompatRecord({ + code: "doctor-talk-provider-shape", + owner: "tts", + introduced: "2026-04-26", + source: "legacy talk provider scalar fields and provider/provider ids", + migration: "src/commands/doctor/shared/legacy-talk-config-normalizer.ts", + replacement: "talk.providers.", + docsPath: "/tools/tts", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + }), + deprecatedCompatRecord({ + code: "doctor-legacy-tools-by-sender", + owner: "tools", + introduced: "2026-04-26", + source: "untyped toolsBySender keys", + migration: "src/commands/doctor/shared/legacy-tools-by-sender.ts", + replacement: "typed id:, e164:, username:, or name: sender keys", + docsPath: "/tools/exec-approvals", + tests: ["src/commands/doctor/shared/legacy-tools-by-sender.test.ts"], + }), +] as const satisfies readonly DoctorDeprecationCompatRecord[]; + +export type DoctorDeprecationCompatCode = + (typeof DOCTOR_DEPRECATION_COMPAT_RECORDS)[number]["code"]; +export type KnownDoctorDeprecationCompatRecord = + DoctorDeprecationCompatRecord; + +const doctorDeprecationCompatRecordByCode = new Map< + DoctorDeprecationCompatCode, + KnownDoctorDeprecationCompatRecord +>(DOCTOR_DEPRECATION_COMPAT_RECORDS.map((record) => [record.code, record])); + +export function listDoctorDeprecationCompatRecords(): readonly KnownDoctorDeprecationCompatRecord[] { + return DOCTOR_DEPRECATION_COMPAT_RECORDS; +} + +export function listDeprecatedDoctorDeprecationCompatRecords(): readonly KnownDoctorDeprecationCompatRecord[] { + return DOCTOR_DEPRECATION_COMPAT_RECORDS.filter((record) => + (["deprecated", "removal-pending"] as readonly string[]).includes(record.status), + ); +} + +export function isDoctorDeprecationCompatCode(code: string): code is DoctorDeprecationCompatCode { + return doctorDeprecationCompatRecordByCode.has(code as DoctorDeprecationCompatCode); +} + +export function getDoctorDeprecationCompatRecord( + code: DoctorDeprecationCompatCode, +): KnownDoctorDeprecationCompatRecord { + const record = doctorDeprecationCompatRecordByCode.get(code); + if (!record) { + throw new Error(`Unknown doctor deprecation compatibility code: ${code}`); + } + return record; +} diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index 2aa2aa95254..7897546e0f4 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -90,6 +90,31 @@ const knownDeprecatedSurfaceMarkers = [ file: "src/plugin-sdk/test-utils.ts", marker: "Deprecated compatibility alias", }, + { + code: "plugin-install-config-ledger", + file: "src/config/plugin-install-config-migration.ts", + marker: "stripShippedPluginInstallConfigRecords", + }, + { + code: "bundled-plugin-load-path-aliases", + file: "src/commands/doctor/shared/bundled-plugin-load-paths.ts", + marker: "plugins.load.paths", + }, + { + code: "plugin-owned-web-search-config", + file: "src/commands/doctor/shared/legacy-web-search-migrate.ts", + marker: "tools.web.search", + }, + { + code: "plugin-owned-web-fetch-config", + file: "src/commands/doctor/shared/legacy-web-fetch-migrate.ts", + marker: "tools.web.fetch.firecrawl", + }, + { + code: "plugin-owned-x-search-config", + file: "src/commands/doctor/shared/legacy-x-search-migrate.ts", + marker: "tools.web.x_search", + }, ] as const; function parseDate(date: string): Date { diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index ae7a905473a..49aee47b7fa 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -267,6 +267,82 @@ export const PLUGIN_COMPAT_RECORDS = [ diagnostics: ["postinstall migration skip", "postinstall migration force deprecation warning"], tests: ["src/commands/doctor/shared/plugin-registry-migration.test.ts"], }, + { + code: "plugin-install-config-ledger", + status: "deprecated", + owner: "config", + introduced: "2026-04-25", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "state-managed `plugins/installs.json` install ledger", + docsPath: "/cli/plugins#registry", + surfaces: ["plugins.installs authored config", "plugin install/update migration"], + diagnostics: ["config write migration warning", "doctor registry migration"], + tests: [ + "src/config/io.write-config.test.ts", + "src/commands/doctor/shared/plugin-registry-migration.test.ts", + ], + }, + { + code: "bundled-plugin-load-path-aliases", + status: "deprecated", + owner: "config", + introduced: "2026-04-25", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "packaged bundled plugins resolved through the persisted plugin registry", + docsPath: "/cli/plugins#registry", + surfaces: ["plugins.load.paths entries pointing at bundled plugin source/dist paths"], + diagnostics: ["doctor bundled plugin load-path warning"], + tests: ["src/commands/doctor/shared/bundled-plugin-load-paths.test.ts"], + }, + { + code: "plugin-owned-web-search-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries..config.webSearch`", + docsPath: "/tools/web", + surfaces: ["tools.web.search.apiKey", "tools.web.search."], + diagnostics: ["doctor legacy web-search config migration"], + tests: ["src/commands/doctor/shared/legacy-web-search-migrate.test.ts"], + }, + { + code: "plugin-owned-web-fetch-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries.firecrawl.config.webFetch`", + docsPath: "/tools/web-fetch", + surfaces: ["tools.web.fetch.firecrawl"], + diagnostics: ["doctor legacy web-fetch config migration"], + tests: ["src/commands/doctor/shared/legacy-web-fetch-migrate.test.ts"], + }, + { + code: "plugin-owned-x-search-config", + status: "deprecated", + owner: "provider", + introduced: "2026-04-26", + deprecated: "2026-04-26", + warningStarts: "2026-04-26", + removeAfter: "2026-07-26", + replacement: "`plugins.entries.xai.config.webSearch.apiKey`", + docsPath: "/tools/grok-search", + surfaces: ["tools.web.x_search.apiKey"], + diagnostics: ["doctor legacy x_search config migration"], + tests: [ + "src/commands/doctor/shared/legacy-x-search-migrate.test.ts", + LEGACY_CONFIG_MIGRATE_TEST_PATH, + ], + }, { code: "plugin-activate-entrypoint-alias", status: "deprecated",