Doctor
Purpose
Define the clawperator doctor report contract, the exact check sequence, critical-versus-advisory behavior, exit-code rules, and the remediation fields an agent can execute directly.
Sources
- Report contract:
apps/node/src/contracts/doctor.ts - CLI behavior and pretty output:
apps/node/src/cli/commands/doctor.ts - Check sequencing and
nextActions:apps/node/src/domain/doctor/DoctorService.ts - Critical check list:
apps/node/src/domain/doctor/criticalChecks.ts - Check implementations:
apps/node/src/domain/doctor/checks/
Command
clawperator doctor [--json] [--device <serial>] [--operator-package <pkg>] [--fix] [--full] [--check-only]
Flags:
| Flag | Valid values | Effect |
|---|---|---|
--device |
adb serial | targets one device explicitly |
--operator-package |
package name string | overrides the default operator package for package checks, launch, and handshake |
--fix |
flag | executes shell-type remediation steps during finalization |
--full |
flag | adds Java/build/install/launch/smoke checks |
--check-only |
flag | forces exit code 0 regardless of failures |
--json |
flag | returns the full DoctorReport JSON object |
Defaults:
- without
--operator-package, doctor usesprocess.env.CLAWPERATOR_OPERATOR_PACKAGEif set, otherwise the runtime default package - without
--device, doctor tries discovery first and may auto-resolve one connected device - without
--full, doctor skips Java/build/install/launch/smoke checks - without
--fix, doctor reports remediation steps but does not run them
DoctorReport Contract
DoctorReport is:
{
"ok": true,
"criticalOk": true,
"deviceId": "optional string",
"operatorPackage": "optional string",
"checks": [
{
"id": "host.node.version",
"status": "pass",
"code": "optional string",
"summary": "summary string",
"detail": "optional detail",
"fix": {
"title": "fix title",
"platform": "mac",
"steps": [
{ "kind": "shell", "value": "command" },
{ "kind": "manual", "value": "instruction" }
],
"docsUrl": "optional URL"
},
"deviceGuidance": {
"screen": "screen name",
"steps": ["manual on-device step"]
},
"evidence": {}
}
],
"nextActions": ["optional command or instruction"]
}
Field meaning:
| Field | Meaning |
|---|---|
ok |
currently the same value as criticalOk; true only when all critical checks avoided fail |
criticalOk |
true when every critical check status is not fail |
deviceId |
resolved device serial, if doctor could determine one |
operatorPackage |
package used for doctor checks |
checks |
ordered list of DoctorCheckResult entries |
nextActions |
deduplicated shell commands or manual instructions collected from failing/warning checks, plus a success hint when everything passed |
How nextActions Is Built
DoctorService.finalize() builds nextActions in this order:
- If all checks passed, add:
Docs: https://docs.clawperator.com/getting-started/first-time-setup/Try: clawperator snapshot --device <resolved_device>or the placeholder form if no device was resolved- For each non-pass check with
fix.steps: - if
--fixwas not used, append each shell or manual step value - if
--fixwas used, executeshellsteps best-effort and append onlymanualstep values - For each non-pass check with
deviceGuidance, append: On device, open <screen> and follow the listed steps.- Deduplicate the final list.
Machine-checkable implication:
nextActionsis remediation guidance, not proof of success- with
--fix,nextActionscan be less complete because attempted shell steps are omitted even if execution failed - always gate on
criticalOkandchecks[], not on whethernextActionsis empty
DoctorCheckResult Contract
Each entry in checks[] has:
| Field | Valid values | Meaning |
|---|---|---|
id |
string | stable check identifier such as device.discovery |
status |
pass, warn, fail |
check outcome |
code |
optional error code or runtime string | machine-usable reason for warnings or failures |
summary |
string | one-line status summary |
detail |
optional string | longer explanation or stderr |
fix.title |
string | remediation summary |
fix.platform |
mac, linux, win, any |
host platform scope for remediation |
fix.steps[].kind |
shell, manual |
whether the step can be executed directly or requires human action |
fix.steps[].value |
string | command or manual instruction |
fix.docsUrl |
optional URL | direct docs link for that failure family |
deviceGuidance.screen |
string | Android screen where the user should go |
deviceGuidance.steps[] |
string array | manual on-device guidance |
evidence |
optional object | structured proof such as versions, serials, or display metrics |
Passing JSON Example
{
"ok": true,
"criticalOk": true,
"deviceId": "emulator-5554",
"operatorPackage": "com.clawperator.operator.dev",
"checks": [
{
"id": "host.node.version",
"status": "pass",
"summary": "Node version v24.14.1 is compatible."
},
{
"id": "host.adb.presence",
"status": "pass",
"summary": "adb is installed.",
"evidence": {
"version": "Android Debug Bridge version 1.0.41"
}
},
{
"id": "device.discovery",
"status": "pass",
"summary": "Device emulator-5554 is connected and reachable.",
"evidence": {
"serial": "emulator-5554"
}
},
{
"id": "device.capability",
"status": "pass",
"summary": "Device shell is available.",
"evidence": {
"sdk": "34",
"wmSize": "Physical size: 1080x2400",
"wmDensity": "Physical density: 420"
}
},
{
"id": "readiness.apk.presence",
"status": "pass",
"summary": "Operator APK (com.clawperator.operator.dev) is installed."
},
{
"id": "readiness.version.compatibility",
"status": "pass",
"summary": "CLI 0.1.0 is compatible with installed APK 0.1.0.",
"evidence": {
"cliVersion": "0.1.0",
"apkVersion": "0.1.0",
"apkVersionCode": 1,
"operatorPackage": "com.clawperator.operator.dev"
}
},
{
"id": "readiness.handshake",
"status": "pass",
"summary": "Handshake successful.",
"detail": "Node successfully dispatched a command and received a valid result envelope."
}
],
"nextActions": [
"Docs: https://docs.clawperator.com/getting-started/first-time-setup/",
"Try: clawperator snapshot --device emulator-5554"
]
}
Success conditions:
- exit code is
0unless--check-onlychanged it criticalOk == true- every critical check has
status != "fail"
Failing JSON Example
{
"ok": false,
"criticalOk": false,
"deviceId": "emulator-5554",
"operatorPackage": "com.clawperator.operator.dev",
"checks": [
{
"id": "readiness.handshake",
"status": "fail",
"code": "RESULT_ENVELOPE_TIMEOUT",
"summary": "Handshake timed out.",
"detail": "No [Clawperator-Result] envelope received within 7000ms. Broadcast dispatch: sent. Operator package: com.clawperator.operator.dev. Device: emulator-5554. No correlated Android log lines were captured. This often indicates an APK/CLI version mismatch or an accessibility service issue. Run 'clawperator doctor --json --device emulator-5554 --operator-package com.clawperator.operator.dev' to diagnose.",
"fix": {
"title": "Grant accessibility permissions via adb",
"platform": "any",
"steps": [
{
"kind": "shell",
"value": "clawperator grant-device-permissions --device emulator-5554 --operator-package com.clawperator.operator.dev"
},
{
"kind": "shell",
"value": "clawperator snapshot --device emulator-5554 --operator-package com.clawperator.operator.dev --timeout 5000 --verbose"
}
],
"docsUrl": "https://docs.clawperator.com/troubleshooting/operator/"
},
"deviceGuidance": {
"screen": "Accessibility Settings",
"steps": [
"Ensure Clawperator Accessibility Service is ON in Android Settings"
]
}
}
],
"nextActions": [
"clawperator grant-device-permissions --device emulator-5554 --operator-package com.clawperator.operator.dev",
"clawperator snapshot --device emulator-5554 --operator-package com.clawperator.operator.dev --timeout 5000 --verbose",
"On device, open Accessibility Settings and follow the listed steps."
]
}
Failure conditions:
- exit code is
1unless--check-onlywas passed criticalOk == false- at least one critical check has
status == "fail"
Warning JSON Example
The main advisory-only case in current code is multiple connected devices without --device:
{
"ok": true,
"criticalOk": true,
"checks": [
{
"id": "device.discovery",
"status": "warn",
"code": "MULTIPLE_DEVICES_DEVICE_ID_REQUIRED",
"summary": "Multiple devices connected.",
"detail": "Specify --device to target a single device.",
"evidence": {
"devices": ["emulator-5554", "R58N12345AB"]
}
}
]
}
Meaning:
- the host environment is usable, so
criticalOkstaystrue - doctor still cannot continue into device-specific checks without an explicit target
- the next deterministic step is to rerun doctor with
--device <serial>
Check Sequence
Doctor runs checks in this order:
| Order | Check IDs | When they run |
|---|---|---|
| 1 | host.node.version, host.adb.presence, host.adb.server |
always |
| 2 | host.java.version, build.android.assemble |
only with --full |
| 3 | device.discovery |
always |
| 4 | device resolution via resolveDevice.ts |
after discovery when doctor still needs a target device |
| 5 | build.android.install, build.android.launch |
only with --full and after device resolution |
| 6 | device.capability |
after device resolution |
| 7 | readiness.apk.presence |
after device capability |
| 8 | readiness.version.compatibility |
only if APK presence passed |
| 9 | readiness.settings.dev_options, readiness.settings.usb_debugging |
after device capability and APK presence checks; these still run even when version compatibility was skipped |
| 10 | readiness.handshake |
only if APK presence passed and version compatibility passed |
| 11 | readiness.smoke |
only with --full, and only if APK presence and version compatibility passed |
Halting rule:
- doctor stops early only when a critical check returns
status == "fail" - warnings do not halt
- a
device.discoverywarning forMULTIPLE_DEVICES_DEVICE_ID_REQUIREDcauses early finalization later because no device can be resolved for subsequent device-specific checks
Critical Vs Advisory
Critical checks are any checks whose ID starts with one of these prefixes:
host.node.version
host.adb.presence
host.adb.server
host.java.version
device.discovery
device.capability
build.android.assemble
build.android.install
build.android.launch
readiness.apk.presence
readiness.version.compatibility
readiness.handshake
readiness.smoke
Advisory behavior:
- checks not matching those prefixes can still appear as
warn - advisory warnings remain in
checks[]but do not setcriticalOktofalse - in current code,
readiness.settings.*checks are warn-only when they fail
Important special case:
device.discoverywithMULTIPLE_DEVICES_DEVICE_ID_REQUIREDis awarn, not afail- even so, you still cannot proceed to device-specific execution without passing
--device
Exit Codes
cmdDoctor sets the exit code like this:
| Condition | Exit code |
|---|---|
--check-only passed |
0 |
otherwise and (report.criticalOk ?? report.ok) == true |
0 |
| otherwise | 1 |
Machine-checkable success gate:
- require exit code
0 - require
criticalOk == true - also require that you either resolved one device or intentionally handled the
device.discoverymulti-device warning before issuing device commands
--fix Behavior
--fix does not change the report shape. It changes how doctor handles fix.steps during finalization:
shellsteps are executed best-effort through the runtime shell runnermanualsteps are still only reported- when
--fixis active, attemptedshellsteps are omitted fromnextActionswhether they succeeded or failed - when
--fixis not active,shellandmanualsteps are both copied intonextActions - failed auto-fix attempts are intentionally swallowed so diagnostics remain deterministic
Use --fix when:
- you want unattended recovery loops for shell-based remediations
- you still plan to rerun
doctor --jsonafterward to verifycriticalOk == true
Do not treat --fix alone as success. The success gate remains the follow-up report.
Pretty Output
Pretty output is grouped into:
- critical checks
- advisory checks
- count of additional passed non-critical checks
- final summary line
Next actions:section
For a failing check, pretty output includes:
summarydetailwhen presentfix.title- each
fix.steps[].value Docs: <fix.docsUrl>when present- on-device guidance grouped under
On device (<screen>):
Check Reference
| Check ID | Statuses seen in current code | Typical codes | What it verifies |
|---|---|---|---|
host.node.version |
pass, fail |
NODE_TOO_OLD |
Node.js major version is at least 24 |
host.adb.presence |
pass, fail |
ADB_NOT_FOUND |
adb exists and can report a version |
host.adb.server |
pass, fail |
ADB_SERVER_FAILED |
adb server can start |
host.java.version |
pass, fail |
HOST_DEPENDENCY_MISSING or no explicit code |
Java 17 or 21 is available for full Android build checks |
build.android.assemble |
pass, fail |
ANDROID_BUILD_FAILED |
./gradlew :app:assembleDebug succeeds |
device.discovery |
pass, warn, fail |
NO_DEVICES, DEVICE_UNAUTHORIZED, DEVICE_OFFLINE, MULTIPLE_DEVICES_DEVICE_ID_REQUIRED, DEVICE_NOT_FOUND |
device discovery succeeded and the environment is targetable, or explains why explicit --device selection is still required |
build.android.install |
pass, fail |
ANDROID_INSTALL_FAILED |
./gradlew :app:installDebug succeeds |
build.android.launch |
pass, fail |
ANDROID_APP_LAUNCH_FAILED |
Operator main activity launches |
device.capability |
pass, fail |
DEVICE_SHELL_UNAVAILABLE or no explicit code |
shell access, SDK version, screen size, and density are readable |
readiness.apk.presence |
pass, warn, fail |
DEVICE_SHELL_UNAVAILABLE, OPERATOR_VARIANT_MISMATCH, OPERATOR_NOT_INSTALLED |
requested operator package is installed |
readiness.version.compatibility |
pass, fail |
VERSION_INCOMPATIBLE, APK_VERSION_UNREADABLE, APK_VERSION_INVALID, CLI_VERSION_INVALID |
CLI and installed APK are compatible |
readiness.settings.dev_options |
pass, warn |
DEVICE_DEV_OPTIONS_DISABLED |
developer options setting is enabled |
readiness.settings.usb_debugging |
pass, warn |
DEVICE_USB_DEBUGGING_DISABLED |
USB debugging setting is enabled |
readiness.handshake |
pass, fail |
DEVICE_ACCESSIBILITY_NOT_RUNNING, RESULT_ENVELOPE_TIMEOUT, BROADCAST_FAILED, OPERATOR_NOT_INSTALLED |
Node can dispatch and receive a valid result envelope |
readiness.smoke |
pass, fail |
SMOKE_OPEN_SETTINGS_FAILED |
smoke execution can open Settings and produce at least one successful snapshot_ui step |
Common Failure Recovery
NO_DEVICES
Meaning:
checkDeviceDiscoverysaw no adb entries at all
Recovery:
- connect a device or boot an emulator
- rerun
clawperator devices - rerun
clawperator doctor --json
DEVICE_UNAUTHORIZED
Meaning:
- the target device is visible to adb but waiting for RSA authorization
Recovery:
- unlock the device
- accept the USB debugging prompt
- rerun doctor
DEVICE_OFFLINE
Meaning:
- adb sees the device but it is not currently usable
Recovery:
adb kill-server
adb start-server
Then rerun doctor.
OPERATOR_NOT_INSTALLED
Meaning:
- the requested package was not found by
pm list packages
Recovery:
- if using release APKs, install the exact version doctor points to
- run the generated
clawperator operator setup --apk ...command fromnextActions
OPERATOR_VARIANT_MISMATCH
Meaning:
- the requested package is missing, but the alternate known package variant is installed
Recovery:
- either pass
--operator-packagefor the installed variant - or reinstall the intended variant
RESULT_ENVELOPE_TIMEOUT
Meaning:
- handshake broadcast was sent, but no
[Clawperator-Result]envelope arrived within 7000ms
Recovery:
- run
clawperator grant-device-permissions --device <serial> [--operator-package <pkg>] - rerun
clawperator snapshot --device <serial> [--operator-package <pkg>] --timeout 5000 --verbose - verify accessibility service is enabled
DEVICE_ACCESSIBILITY_NOT_RUNNING
Meaning:
- handshake returned an envelope, but the runtime reported an accessibility-related failure
Recovery:
- run
clawperator grant-device-permissions ... - follow
deviceGuidance.screen == "Accessibility Settings" - rerun doctor
Agent Sequence
Recommended doctor loop:
- Run
clawperator doctor --json [--device <serial>] [--operator-package <pkg>]. - Require exit code
0andcriticalOk == truebefore treating the environment as ready. - If
criticalOk == false, iterate throughchecks[]in order and branch on the first criticalfail. - If
fix.steps[].kind == "shell"and you trust the environment, either execute them yourself or rerun doctor with--fix. - If
deviceGuidanceis present, surfacedeviceGuidance.screenanddeviceGuidance.steps[]to the human operator. - Rerun
doctor --jsonafter remediation and requirecriticalOk == true. - Only then move on to device commands such as
snapshot.