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 d5d88a0db1: 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>
This commit is contained in:
Gio Della-Libera
2026-07-08 14:34:57 -07:00
committed by GitHub
parent e8c1780999
commit 7d82e5b658
5 changed files with 45 additions and 5 deletions

View File

@@ -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 <id>` 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 <id>` (repeatable): run only the named check id(s); an unknown id is reported as an error finding.
- `--skip <id>` (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.

View File

@@ -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({

View File

@@ -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),

View File

@@ -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");

View File

@@ -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");