From 7d82e5b658950a76973ebee2d8e379a3e54fbbf0 Mon Sep 17 00:00:00 2001 From: Gio Della-Libera Date: Wed, 8 Jul 2026 14:34:57 -0700 Subject: [PATCH] Doctor: audit lint default selection (#100361) Summary: - audit Doctor lint default selection after the full lint-family backfill - make legacy state, skills readiness, session transcripts, and session snapshots explicit-only for default lint - document default lint vs --all/--only and keep plugin/SDK public contracts unchanged Validation: - Galin review: no blocking findings - maintainer accepted the default-lint compatibility tradeoff in PR comment - exact-head hosted gates passed for d5d88a0db14782a36f72aa1fbe5023570343f773: CI#28976811444 and Workflow Sanity#28976811343 - local broad pnpm check was blocked by a shrinkwrap guard failure that reproduces on origin/main and is unrelated to this five-file Doctor diff Co-authored-by: Gio Della-Libera <235387111+giodl73-repo@users.noreply.github.com> --- docs/gateway/doctor.md | 28 +++++++++++++++++-- src/flows/doctor-core-checks.test.ts | 1 + src/flows/doctor-core-checks.ts | 8 ++++-- src/flows/doctor-health-contributions.test.ts | 11 ++++++++ src/flows/doctor-health-contributions.ts | 2 ++ 5 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index d2b3b630189..b93f1f5bbc5 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -80,7 +80,9 @@ cat ~/.openclaw/openclaw.json ## Read-only lint mode -`openclaw doctor --lint` is the automation-friendly sibling of `openclaw doctor --fix`. Both run the same health checks; only the posture differs: +`openclaw doctor --lint` is the automation-friendly sibling of +`openclaw doctor --fix`. They share the same Doctor rule registry, but they do +not select or act on rules in the same way: | Mode | Prompts | Writes config/state | Output | Use it for | | ------------------------ | --------- | ----------------------- | ---------------------- | ------------------------------- | @@ -88,7 +90,27 @@ cat ~/.openclaw/openclaw.json | `openclaw doctor --fix` | sometimes | yes, with repair policy | friendly repair log | applying approved repairs | | `openclaw doctor --lint` | no | no | structured findings | CI, preflight, and review gates | -Health checks may provide an optional `repair()` implementation; `doctor --fix` applies it when present and falls back to the legacy doctor repair flow otherwise. The contract separates `detect()` (reports findings) from `repair()` (reports changes/diffs/side effects), which keeps a path open for a future `doctor --fix --dry-run` without turning lint checks into mutation planners. +Default `doctor --lint` runs the broad-safe automation profile: checks that are +static, local, and useful in CI or preflight output. It skips opt-in checks that +are advisory, environment-sensitive, live-service dependent, account/workspace +inventory, or historical cleanup. Use `doctor --lint --all` when you want the +full registered lint audit, including those opt-in checks, or `--only ` for +a targeted check. + +`doctor --fix` does not use the lint default profile and does not accept +`--all`. It runs Doctor's ordered repair path: modern health checks may provide +an optional `repair()` implementation, and older areas still use their legacy +Doctor repair flow. Some lint findings are intentionally diagnostic only, so a +check appearing in `--lint --all` does not mean `--fix` will mutate that area. +The contract separates `detect()` (reports findings) from `repair()` (reports +changes/diffs/side effects), which keeps a path open for a future +`doctor --fix --dry-run` without turning lint checks into mutation planners. + +Some built-in checks are default-disabled internally so they stay available to +`--all`, `--only`, and Doctor repair flows without becoming part of the default +`doctor --lint` automation profile. Finding severity is still emitted per +finding (`info`, `warning`, or `error`); default selection is not a severity +level. ```bash openclaw doctor --lint @@ -115,7 +137,7 @@ Exit codes: Flags: - `--severity-min info|warning|error` (default `warning`): controls both what prints and what causes a non-zero exit. -- `--all`: runs every registered check, including opt-in checks excluded from the default automation set. +- `--all`: runs every registered lint check, including opt-in checks excluded from the default automation set. - `--only ` (repeatable): run only the named check id(s); an unknown id is reported as an error finding. - `--skip ` (repeatable): exclude a check while keeping the rest of the run active. - `--json`, `--severity-min`, `--all`, `--only`, and `--skip` require `--lint`; plain `openclaw doctor` and `--fix` runs reject them. diff --git a/src/flows/doctor-core-checks.test.ts b/src/flows/doctor-core-checks.test.ts index e34da515706..82ae68be51f 100644 --- a/src/flows/doctor-core-checks.test.ts +++ b/src/flows/doctor-core-checks.test.ts @@ -357,6 +357,7 @@ describe("CORE_HEALTH_CHECKS", () => { "core/doctor/skills-readiness", ); + expect(check).toMatchObject({ defaultEnabled: false }); expect(check["repair"]).toBeTypeOf("function"); const findings = await check.detect({ diff --git a/src/flows/doctor-core-checks.ts b/src/flows/doctor-core-checks.ts index 52a8bcecd1f..4dda5aa9eab 100644 --- a/src/flows/doctor-core-checks.ts +++ b/src/flows/doctor-core-checks.ts @@ -422,11 +422,12 @@ const hooksModelCheck: HealthCheck = { }, }; -const legacyStateCheck: HealthCheck = { +const legacyStateCheck: HealthCheck & { readonly defaultEnabled: false } = { id: "core/doctor/legacy-state", kind: "core", description: "Legacy sessions, agent state, and channel auth paths have been migrated.", source: "doctor", + defaultEnabled: false, async detect(ctx) { const { detectLegacyStateMigrations } = await import("../commands/doctor-state-migrations.js"); const detected = await detectLegacyStateMigrations({ cfg: ctx.cfg }); @@ -874,12 +875,15 @@ const browserCheck: HealthCheck = { }, }; -function createSkillsReadinessCheck(deps: CoreHealthCheckDeps): HealthCheck { +function createSkillsReadinessCheck( + deps: CoreHealthCheckDeps, +): HealthCheck & { readonly defaultEnabled: false } { return { id: "core/doctor/skills-readiness", kind: "core", description: "Allowed skills are usable in the current runtime environment.", source: "doctor", + defaultEnabled: false, async detect(ctx, scope) { const unavailable = filterUnavailableSkillsForScope( await deps.detectUnavailableSkills(ctx.cfg), diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index 913efe5f3ab..140e2c0806c 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -1428,6 +1428,11 @@ describe("doctor health contributions", () => { it("passes the active config into legacy state migration", async () => { const contribution = requireDoctorContribution("doctor:legacy-state"); + const legacyStateCheck = CORE_HEALTH_CHECKS.find( + (check) => check.id === "core/doctor/legacy-state", + ); + expect(legacyStateCheck).toMatchObject({ defaultEnabled: false }); + const cfg = { session: { store: "/tmp/shared-sessions.json" } }; const detected = { preview: ["legacy sessions"], warnings: [] }; mocks.detectLegacyStateMigrations.mockResolvedValue(detected); @@ -1570,6 +1575,12 @@ describe("doctor health contributions", () => { expect(contributionIds).toContain("core/doctor/config-audit-scrub"); expect(contributionIds).toContain("core/doctor/session-transcripts"); expect(contributionIds).toContain("core/doctor/session-snapshots"); + expect( + contributionChecks.find((check) => check.id === "core/doctor/session-transcripts"), + ).toMatchObject({ defaultEnabled: false }); + expect( + contributionChecks.find((check) => check.id === "core/doctor/session-snapshots"), + ).toMatchObject({ defaultEnabled: false }); expect(contributionIds).toContain("core/doctor/plugin-registry"); expect(contributionIds).toContain("core/doctor/configured-plugin-installs"); expect(contributionIds).toContain("core/doctor/legacy-plugin-dependencies"); diff --git a/src/flows/doctor-health-contributions.ts b/src/flows/doctor-health-contributions.ts index 417e091ebdf..25cf6914221 100644 --- a/src/flows/doctor-health-contributions.ts +++ b/src/flows/doctor-health-contributions.ts @@ -1769,6 +1769,7 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] { healthChecks: { id: "core/doctor/session-transcripts", description: "Legacy or branchy session transcript files are represented as findings.", + defaultEnabled: false, async detect() { const { detectSessionTranscriptHealthIssues, sessionTranscriptIssueToHealthFinding } = await import("../commands/doctor-session-transcripts.js"); @@ -1801,6 +1802,7 @@ export function resolveDoctorHealthContributions(): DoctorHealthContribution[] { healthChecks: { id: "core/doctor/session-snapshots", description: "Stale cached session snapshot paths are represented as findings.", + defaultEnabled: false, async detect(ctx) { const { detectSessionSnapshotHealthIssues, sessionSnapshotIssueToHealthFinding } = await import("../commands/doctor-session-snapshots.js");