Doctor (preflight)

Rendered from docs/doctor.md in the repository.

Everything a deploy needs used to be checked at the moment of deploying, and nowhere else. So every unmet precondition was learned the same way: a command you expected to succeed failed instead.

doctor inverts that. It answers "would a deploy work, and if not, why" without attempting one and without changing anything.

agent-container doctor                 # this project's environments + the machine
agent-container doctor acme            # narrow to one environment
agent-container doctor --json          # the machine-readable report

It changes nothing — and that is enforced structurally#

doctor writes no file, creates no container, volume or image, and touches no host-registry entry. That is not a promise about intent: the command composes read-only readers and deliberately cannot reach the deploy path's setup helpers.

That matters more than it sounds. The helpers a deploy calls first are the ones that mutate, and the most dangerous is migrate_flat_state() — it opens up, redeploy and list, it relocates files on disk, it is idempotent, and its own docstring says "safe to call repeatedly." It reads as harmless. A test walks the transitive closure of reachable names from the command and asserts those helpers are unreachable, which catches what a before/after snapshot cannot: a call that exists but that the test project never happened to trigger.

One deliberate exception: an SSH socket-forward for a provisioned host. It creates none of the artifact kinds above and does not outlive the command, and without it every provisioned host would report unreachable — a false negative on the check you asked for. The line is nothing that outlives the command.

Three statuses, and unknown is one of them#

Status Meaning
pass the check was answered, and the answer is fine
fail the check was answered, and it is a problem
unknown the check could not be answered

A check that cannot complete reports unknown — never pass. A diagnostic that reports healthy is what stops you looking further, so failing open is worse than not checking at all. Reporting fail instead would be the opposite error: it manufactures work that may not exist.

Every finding — including an unknown — names the action that resolves it, not just the symptom. A finding without a remedy cannot be constructed.

Severity, and what the exit code means#

Severity belongs to the check, not to the run: "an unreachable secondary host is advisory" is a fact about that check, and deriving it per-run from the outcome would make the same condition blocking on Tuesday and advisory on Wednesday.

Code Meaning
0 a deploy would succeed — advisories and unknowns permitted
1 at least one blocking check failed
2 doctor itself could not run

Nothing above 2: 3 means pending registration tool-wide (Feature 019), so a doctor returning it would tell an automated caller something false about an SSH key.

An unknown never produces 1. Exit 1 asserts that a deploy would not work, and unknown is precisely the state in which that assertion cannot be made. Failing the run on one would break doctor && up for anyone whose secondary host happened to be slow — and a diagnostic people stop chaining is a diagnostic nobody runs.

agent-container doctor && agent-container up acme    # the intended idiom

What is checked#

Check Severity Notes
runtime-present blocking docker or podman on PATH
tool-version advisory unknown when unresolvable — which is also why freshness is unknown
user-config advisory absent is a pass; a fresh machine has none
host-reachability advisory each host individually, bounded; one dead host never suppresses another
layout blocking pre-011 layout — the same remedy string a deploy prints
credentials blocking see below
port-availability blocking held by its own container is a pass
image-freshness advisory local label comparison; no registry

Credentials are checked without being resolved#

For a manager source, resolving is the prompt: op read against an approval-gated item raises a system dialog, and doctor must never do that as a side effect of answering a question. So:

Source What is checked Result
env is the variable set pass / fail
file does the path exist pass / fail
keychain, onepassword, bitwarden, command is the resolver binary on PATH fail if absent, else unknown

No credential value is ever retrieved — stronger than "never printed", because a value never read cannot leak through a log, a traceback, or a field somebody adds later.

The binary check is not a consolation prize: "op is not installed" is the most common real failure on a new machine, and it is free to detect.

Image freshness needs a rebuild first#

build stamps the building CLI's version into the image as an org.opencontainers.image.version label, and doctor compares it locally — no network, no registry round-trip. A label rather than an ENV, because reading it must not start a container.

An image with no label predates stamping and reports unknown — never fresh, never stale. So every image built before this feature reports unknown until rebuilt, which is correct rather than unfortunate: reporting it stale would nag you into a rebuild you may not need, and reporting it fresh would assert something nobody knows.

When build cannot determine its own version it omits the label and says so, rather than stamp 0.0.0+unknown — a meaningless value that looks like an answer is worse than an honest absence.

Outside a project#

doctor degrades to machine-level checks and says plainly that no project was found. That is a success, not an error — the case it matters most for is a new machine, before any project exists.

What it deliberately does not do#