Skip to content

feat(doctor): scope the sweep to machine checks with --scope machine - #2337

Merged
mlorentedev merged 3 commits into
mainfrom
feat/doctor-machine-scope
Oct 11, 2026
Merged

mlorentedev merged 3 commits into
mainfrom
feat/doctor-machine-scope

Conversation

@mlorentedev

@mlorentedev mlorentedev commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

dotf doctor --scope machine checks what converge produces and leaves out the owner's identity, so a fresh machine and a CI runner can be judged before the identity is restored (#2013 D10).

  • Every entry of the doctor sweep is classified machine or identity; there is no default kind, so an entry added without a decision fails TestSweep_EveryCheckIsClassified.
  • Under --scope machine the 15 identity entries (vault, age key, Bitwarden, DR drill, PAT expiry, NaN quota, configs rendered from secrets, spec issue state, branch protection) do not run. Each prints a SKIP under its own section naming the restore step in docs/runbooks/guide-new-machine.md; a skip never fails the run, and --fix runs no identity fix.
  • A plain dotf doctor is unchanged.
  • Two checks that mixed both kinds are split: checkAgeIdentity from checkSecretsTooling, and checkAgentSecretConfigs from checkOpenCode (its section renamed to "Agent configs from secrets", troubleshooting updated).
  • New runbook guide-new-machine.md: the one-liner, --plan, how to read the converge report, then the identity restore in order (age key, bw login + dotf secrets unlock, gh auth login, the vault).
  • ADR-045 amendment records D10, D11 (fix(converge): re-run records-harness after tools installs an agent #2333) and D12 (converge guides the restore on a TTY; its own PR).

Found by X1 run 38110204139: install.sh and both converges passed on fresh macOS and Ubuntu runners and only doctor failed, mostly on identity.

Spec

PLAT-001b-one-entrypoint: tasks.md PR 6a and the runbook task of PR 6; verification.md D10 entry.

Test plan

  • go build ./... && go vet ./... && GOOS=windows go vet ./... && go test ./...: exit 0
  • golangci-lint run ./... (2.12.2, cache cleaned): 0 issues
  • bats suites referencing doctor: exit 0
  • On this Mac, released 0.67.0 dotf doctor vs this branch's dotf doctor: 165 passed, 11 failed, 4 warned, 22 skipped vs 165 passed, 11 failed, 4 warned, 23 skipped. The section diff is the two split headers ("Age identity key", "Agent configs from secrets") plus "System packages", whose one SKIP is on main since 0.67.0, not from this PR.
  • This branch's dotf doctor --scope machine: 95 passed, 11 failed, 3 warned, 35 skipped, 15 identity SKIPs, each under its own header once, the same 11 FAILs (kubectl, hive daemon, deploy-dir drift: all machine)
  • Mutations M1-M5 (no SKIP, unset kind, renamed section, vault reclassified machine, ParseScope typo): each caught

Follow-ups

Knowledge

  • Lesson: none: the finding is a design decision, recorded in the ADR amendment
  • ADR: docs/adr/adr-045-one-entrypoint-converges-every-os.md
  • Runbook: docs/runbooks/guide-new-machine.md

Refs #2013

Every doctor check is now classified as machine (what converge produces) or
identity (the age key, Bitwarden, the gh login, the knowledge vault). Under
--scope machine the identity checks do not run; each prints a SKIP naming the
step of the new-machine runbook that restores it, and a skip never fails the
run. A plain dotf doctor is unchanged.

The age key is split from the secrets tooling check, and the configs rendered
from secrets from the opencode/pi binaries check, so each check has one kind.
@mlorentedev
mlorentedev marked this pull request as ready for review October 11, 2026 04:23
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 41 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: mlorentedev/dotfiles/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 14d9f48a-3c67-4afc-92de-8103cad96945

📥 Commits

Reviewing files that changed from the base of the PR and between f3256bb and 6679ad7.


📒 Files selected for processing (13)
  • cli/internal/cmd/doctor.go
  • cli/internal/doctor/checks_deploy.go
  • cli/internal/doctor/checks_launchability_test.go
  • cli/internal/doctor/checks_secrets_tooling.go
  • cli/internal/doctor/checks_secrets_tooling_test.go
  • cli/internal/doctor/doctor.go
  • cli/internal/doctor/scope_test.go
  • docs/adr/adr-045-one-entrypoint-converges-every-os.md
  • docs/runbooks/_index.md
  • docs/runbooks/guide-new-machine.md
  • docs/troubleshooting/secrets.md
  • specs/PLAT-001b-one-entrypoint/tasks.md
  • specs/PLAT-001b-one-entrypoint/verification.md

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment on lines +425 to 427
if pathExists(models) || pathExists(cfgPath) {
reportAgentLaunchability(sys, rep)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior Regression

REAL. The launchability WARN used to run when pi was configured (~/.pi present, the former piConfigured) or when opencode.jsonc existed. The new gate tests only models.json and opencode.jsonc. Trace: ~/.pi exists, agent/models.json and opencode.jsonc are absent, the keys are bw-backed and the daemon is locked. Before, doctor WARNed with the dotf secrets unlock remedy. Now it stays silent, which is the failure WIN-012 added this check for. This also contradicts the PR claim that a plain dotf doctor is unchanged. Fix: keep the ~/.pi directory test in the gate and add a case for it to the launchability test.

t.Errorf("the default scope skipped a check\n%s", all)
}

machine, _ := run(ScopeMachine)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vacuous Assertion

REAL (test gap). TestRun_MachineScopeSkipsEveryIdentityCheck discards the machine-scope exit code (machine, _ := run(ScopeMachine)). The PR and tasks.md state that a skip never fails the run, but no assertion checks it. Mutation: change the identity branch in Run from rep.Skip to rep.Fail with the same text. The SKIP count assertion still passes and the test stays green. Fix: assert on failures, for example that the machine output contains no FAIL line under an identity section heading.

Comment thread docs/runbooks/guide-new-machine.md Outdated
dotf doctor --scope machine
```

Every identity check prints a SKIP naming the step below that restores it. A FAIL here is a defect of the bootstrap, not of your restore, and belongs in a ticket. The from-zero CI job runs this same command on fresh macOS and Ubuntu runners.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inaccurate Documentation

REAL (documentation). The runbook says the from-zero CI job runs dotf doctor --scope machine. In this PR the job still runs plain dotf doctor, and the switch to --scope machine is an unchecked follow-up (#2331). Readers would assume CI already judges the machine scope. Fix: state that CI runs dotf doctor today and switches to --scope machine in #2331, or make that switch in this PR.

Comment on lines +220 to +278
func sweep(sys *System, cfg *Config, contract *Contract, rep *Report, opts Options, start string) []check {
return []check{
{kind: kindMachine, run: func() { checkCoreTools(sys, contract, rep) }},
{kind: kindMachine, run: func() { checkVersionedPaths(sys, rep) }},
{kind: kindMachine, run: func() { checkVersionMatch(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkSymlinks(sys, rep) }},
{kind: kindMachine, run: func() { checkProfileFiles(sys, contract, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkToolHomeEnvVars(sys, rep) }},
{kind: kindMachine, run: func() { checkOptionalTools(sys, cfg, contract, rep) }},
{kind: kindMachine, run: func() { checkMiseTools(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkSystemPackages(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkPython(sys, cfg, rep) }},
{kind: kindIdentity, section: "Knowledge vault (presence)", restore: restoreVault, run: func() { checkVault(sys, rep) }},
{kind: kindIdentity, section: "Knowledge vault hooks (secret gate)", restore: restoreVault, run: func() { checkVaultHooks(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkGitConfig(sys, rep, opts.Fix) }},
{kind: kindIdentity, section: "Auto-memory vault link", restore: restoreVault, run: func() { checkAutoMemoryLink(sys, start, rep, opts.Fix) }},
{kind: kindIdentity, section: "Auto-memory file shape", restore: restoreVault, run: func() { checkMemoryShape(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkPathFiles(sys, cfg, rep) }},
{kind: kindIdentity, section: "Secrets integrity", restore: restoreAge, run: func() { checkSecrets(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkSecretsTooling(sys, cfg, rep) }},
{kind: kindIdentity, section: "Age identity key", restore: restoreAge, run: func() { checkAgeIdentity(sys, rep) }},
{kind: kindIdentity, section: "Bitwarden reach (live secrets SSOT)", restore: restoreBW, run: func() { checkBitwardenReach(sys, rep) }},
{kind: kindIdentity, section: "bw serve daemon (optional local unlock cache)", restore: restoreBW, run: func() { checkBWServeDaemon(sys, cfg, rep) }},
{kind: kindIdentity, section: "Bitwarden mapping (registry -> vault)", restore: restoreBW, run: func() { checkBWMapping(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkAgentConfigSecrets(sys, rep) }},
{kind: kindMachine, run: func() { checkHiveBackendCanServe(sys, rep) }},
{kind: kindMachine, run: func() { checkHiveDaemonAnswers(sys, cfg, rep) }},
{kind: kindIdentity, section: "Disaster recovery", restore: restoreAge, run: func() { checkDisasterRecovery(sys, cfg, rep) }},
{kind: kindIdentity, section: "PAT expiry", restore: restoreBW, run: func() { checkPATExpiry(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkGuardHooks(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkTmux(sys, rep) }},
{kind: kindMachine, run: func() { checkOpenCode(sys, cfg, rep) }},
{kind: kindIdentity, section: "Agent configs from secrets", restore: restoreBW, run: func() { checkAgentSecretConfigs(sys, rep) }},
{kind: kindMachine, run: func() { checkCopilot(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkGolangciLint(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkModelMap(cfg, rep) }},
{kind: kindMachine, run: func() { checkModelPins(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkModelLimits(sys, cfg, rep) }},
{kind: kindIdentity, section: "NaN quota", restore: restoreBW, run: func() { checkNaNQuota(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkPiExtensions(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkPiPackageRequirements(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkHarnessDrift(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkDeployDrift(sys, cfg, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkHomeDeployDrift(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkDockerEngine(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkColimaSize(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkDockerCompose(sys, rep) }},
{kind: kindMachine, run: func() { checkDeployManifest(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkAgentPresence(sys, rep) }},
{kind: kindMachine, run: func() { checkAgentSkillsMigrated(cfg, rep) }},
{kind: kindMachine, run: func() { checkDotfProvenance(sys, cfg, rep) }},
{kind: kindMachine, run: func() { checkRepoDirResolves(sys, rep) }},
{kind: kindIdentity, section: "spec-issue-state", restore: restoreGH, run: func() { checkSpecIssueState(sys, rep) }},
{kind: kindIdentity, section: "branch-protection", restore: restoreGH, run: func() { checkBranchProtection(sys, rep) }},
{kind: kindMachine, run: func() { checkAntigravity(sys, rep) }},
{kind: kindMachine, run: func() { checkOrcaHook(sys, rep, opts.Fix) }},
{kind: kindMachine, run: func() { checkHookBinding(sys, rep, opts.Fix) }},
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Harness compliance

THEORETICAL. AGENTS.md Code Quality Rules set function length below 40 lines. sweep is about 59 lines. The rule has no exemption for literal tables; the Discipline Gate's executable-line exemption covers only the PR size count. Fix: split the table into per-group functions, or record an explicit exception.

@github-actions

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

2013 - Partially compliant

Compliant requirements:

  • Every doctor check is classified machine or identity, with no default kind, enforced by a test.
  • Identity checks are skipped under --scope machine with a SKIP naming the restore step.
  • Mixed checks are split into machine and identity parts.
  • Restore order is documented in docs/runbooks/guide-new-machine.md.
  • ADR-045 amendment records D10, D11 and D12.
  • Guards accompany the change (scope and sweep tests).

Non-compliant requirements:

Requires further human verification:

  • Windows CI leg stays green and required (the test plan shows only local GOOS=windows go vet).
  • dotf doctor output on the Mac and fresh runners matches the expected SKIP layout.
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified

⚙️ Agent run details
  • Model: anthropic/claude-haiku-5-5
  • Tokens: 45,777 in / 23,170 out / 68,947 total
  • Time cost: 108.8s
  • AI calls: 1

…each identity section to its SKIP

The sweep table ran past the 40-line function limit; four groups, in report
order, keep each short. The machine-scope test now asserts each identity
section holds only its SKIP, so a skip reported as a failure is caught. The
runbook no longer says the from-zero job already runs --scope machine.
@mlorentedev

Copy link
Copy Markdown
Owner Author

Review triage

Reviewer: PR-Agent (github-actions, review 5481888670, four inline findings on 1d1321a). Fixes in 57fb918.

# Finding Disposition
1 checks_deploy.go:427 launchability gate dropped the ~/.pi directory test Declined, not a regression. On main the gate is piConfigured := pathExists(filepath.Join(home, ".pi", "agent", "models.json")) (checks_deploy.go:376) `
2 scope_test.go:110 machine-scope exit code unchecked; Skip→Fail would pass Applied. The test now asserts each identity section holds only its SKIP line with its restore step. The reviewer's mutation (rep.Skip → rep.Fail) now fails TestRun_MachineScopeSkipsEveryIdentityCheck.
3 runbook says the from-zero job runs --scope machine Applied. It now says the job switches in #2331 and runs the full doctor until then.
4 sweep is 59 lines, over AGENTS.md's 40 Applied. Split into four groups in report order (14, 21, 16 and 20 lines). Section order of dotf doctor and --scope machine on this Mac is identical before and after (diffed).

Verification on 57fb918: go vet ./..., GOOS=windows go vet ./..., go test ./... exit 0; golangci-lint run ./... 0 issues.

@mlorentedev
mlorentedev merged commit 3f02e99 into main Oct 11, 2026
25 checks passed
@mlorentedev
mlorentedev deleted the feat/doctor-machine-scope branch October 11, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant