cyberine
UsageRun recipes

Unified health check — cyberine doctor (T540)

cyberine doctor    # everything below, one report, offline-safe
cyberine doctor --liveness   # is each adapter FUNDED, not just keyed (see below)

One report covering the whole install, printed as pass/warn/fail lines with a fix hint on every warn/fail:

Only actionable lines are [WARN] (kbxeg). Configuration reports print as [INFO] and never affect Overall: a seat whose token file exists with mode 600, the seat routing policy, the seat rotation pointer and the step routing policy. Providers without a key are one [INFO] no key for N provider(s): ... line instead of one [WARN] per provider; the N/M providers configured line still warns at zero, and a totally unconfigured install is still the [FAIL] under Dispatchability. When the Claude Code plugin is absent, the missing bundled skills are one [WARN] naming cyberine plugin install. Measured on the author's machine 2026-09-28: 36 [WARN] lines became 10. Known Limitations and Deferred Work: the RETIRED-worker lines and the registry counts still print as [WARN]; cyberine fleet doctor prints the same info lines under its own INFO: heading.

  • Runtime — Node version vs the documented minimum (20+).

  • Config files — config.json (global/project/local) and hooks.json (global/project) JSON validity, distinguishing "missing" (fine) from "malformed" (flagged); which fleet.json is in effect.

  • Keybindings — loadUserBindings() warnings (unknown action ids, malformed file).

  • Cyberine home — $CYBERINE_HOME writability.

  • Run store — sanity of $CYBERINE_HOME/runs/*.json (the cross-project registry cyberine agents reads): corrupt record files, and records whose project directory no longer exists on disk. Also (T674) opens each record's own <runsDir>/<runId>/run.json and flags, distinctly from corrupt registry records, runs whose run.json is corrupt, unparseable, or structurally invalid (see below), and live records whose run dir has no run.json at all (an orphaned record); tombstoned (deleted) runs are skipped. The same corrupted-run count also renders live in the fleet TUI header as a corrupt stat (local mode) instead of only a stderr line the alt-screen buffer swallows. Run npm run selftest:doctor-run-corruption to re-run the truncation drill headlessly. Known limitation: the corrupt header stat covers local mode only (cyberine agents' global view and the web dashboard read the registry, not RunStore.list() — for those, cyberine doctor is the rendered channel), and the corrupted-run count is not exposed on /metrics yet.

    What "corrupt" means (kt7bx, 2026-08-27): a run.json is corrupt if it fails to read/parse (a torn write) OR if it parses as valid JSON but is not a structurally valid Run — describeInvalidRun() (src/store/runTypes.ts), shared by RunStore.readRun() (backing list()/get()/corruptedRunIds()) and cyberine doctor's checkStoreSanity(), so the two surfaces never disagree. Before this, JSON.parse alone was treated as validation: {}, or a record missing a field every consumer dereferences unguarded, came back kind: 'ok' — a "healthy" run that then threw downstream at whatever field a caller read first (a fleet-TUI row rendering undefined as its run id, cyberine cost/agents/digest crashing on run.steps). The check requires exactly the fields real Run consumers read without a ??/?. guard — runId, workflowName, state (one of the five RunState values), detail, createdAt, updatedAt, cwd (all plain strings/numbers), and steps (an array) — derived by grepping every run.<field> access across src/, not from the Run type declaration alone. Every other Run field is optional in the type and read that way by its callers (run.lineage?., run.paused ? ...), so it is deliberately NOT required here: failing a run over an absent optional field would be worse than the bug this closes. Verified against every run.json under this project's own .local/runs/ (273 real records at the time) before shipping: all 273 already carried the eight fields with the expected types, so the guard does not retroactively mark any of the operator's own run history corrupt.

    Known Limitations and Deferred Work: the check is shallow — it validates the top-level Run fields only, not the nested RunStep/WorkerRun shapes (a steps array whose elements are themselves malformed still passes), and it does not check that state transitions make sense (e.g. done with an empty steps array). It also does not validate any optional field's type when present (a wrong-typed lineage or budget is not caught). Full schema validation (Zod or similar) covering the whole Run/RunStep/WorkerRun tree is separate, unfiled work.

    Note (2026-08-26): cyberine doctor was always the CORRECT channel here — it already separated "run.json is corrupt/unparseable" from "run dir has no run.json at all". RunStore.list() did not: it collapsed both onto get() returning null and reported every one as corruption. On a real tree that meant 485 of 758 directories, because .local/runs/ is shared — LocalBrain.carry() writes capsule-only dirs (<runId>/context.md, no run.json beside it) into the same parent — and the warning interpolated the FULL joined id set, so every cyberine invocation that touches list() (the fleet TUI, cyberine cost, cyberine eval, registry backfill) dumped a single ~18KB stderr line. list() now skips a missing run.json silently and only counts a real read/parse failure, and the warning names a sample plus a remainder count instead of every id. A genuinely malformed run.json is still reported on stderr AND still surfaces through corruptedRunIds() — verify both halves with npm run selftest:runstore-corruption-scope. Measured on the tree that surfaced it: 758 directories, 273 with a parseable run.json, 485 without one (481 capsule-only, plus 3 holding only console.log and 1 only control.json.done), and ZERO genuinely unparseable — so every id in that 18KB line was a false positive.

    Two things the audit corrected about this file's own older comments, worth not re-learning: the web /api/runs route is NOT a RunStore.list() consumer (it reads the global registry via listRuns()), and the global fleet view loads records one at a time through get(), so a corrupt run vanishes from THAT view with only a per-id console.error — cyberine doctor remains the durable diagnostic surface for registered runs.

    Known limitation / deferred work: the capsule directories deliberately stay under .local/runs/ — cyberine loop --resume .local/runs/<id>/context.md references that path, so a .local/capsules/ namespace would be a migration across writer, reader, docs, and existing artifacts. (kt7bx, 2026-08-27, closed the sibling gap that used to live here: JSON.parse-only validation accepting a structurally wrong run.json as healthy — see "What 'corrupt' means" above for the fix and its own remaining limits.)

  • Providers — per-provider configured/missing (reuses isConfigured() + the PROVIDERS registry; never prints key values).

  • Spend & cap (T636) — daily-cap spent/remaining/resets, hard-cap spent/remaining (when CYBERINE_SPEND_CAP_USD is set), and any credit-ledger entry at low/exhausted status. See "Spend visibility" under "Hard spend cap".

  • Gateway — reachability probe (1.5s timeout) against the LiteLLM gateway, but ONLY when one is actually configured (opt-in signal); skipped entirely by default, so the whole report stays offline-safe.

  • Build-fleet CLIs — install + login state (src/core/cliAvailability.ts, same signal cyberine fleet list's KEY column uses).

  • Fleet policy (fleet.json) — reuses cyberine fleet doctor's exact checker (structural errors, semantic warnings, drift) so the two commands never disagree.

  • MCP servers — whether the cyberine-mcp binary (the fallback entry point, for a host with no plugin support; the plugin ships its own at plugin/.mcp.json) resolves on PATH, plus any externally-configured MCP servers (CYBERINE_MCP_SERVERS / CYBERINE_MCP_CONFIG) for shape + PATH-resolvable stdio commands — never actually connects.

  • claude-code: — Claude Code wiring: the plugin symlink status (missing with a cyberine plugin install fix hint, or loaded), one status per bundled skill (when the plugin is present), whether the bundled MCP server is accessible, and the claude CLI's own version (or a not-on-PATH warning). Shares its checks with claudeCodeWired() (src/core/claudeCodeWiring.ts) so this section and the bare cyberine nudge (see "Smart bare cyberine" above) never disagree on what "wired" means.

  • Hooks executability — for any hooks.json command that looks like a local script path (./foo.sh, /abs/path, ~/bin/foo), checks the file exists and is +x.

  • Repo readiness (kqyke) — for the git repo the command runs in: whether .gitignore covers .local/ (probed as git check-ignore -q .local/), whether any .local/ content is already tracked, whether .cyberine/config.json exists and records rules.applied (the precondition cyberine autopilot refuses on), and whether the task board exists and holds at least one task. Every gap is a [WARN] with its fix; outside a git repo the section is one [WARN] saying readiness was not checked. Added because a session asked "is this repo ready for Cyberine" read Overall: OK from this report on a repo with an unignored .local/, no applied rules and an empty board (.local/research/plugin-eval/2026-09-19-eval-residuals-verdicts.m3max.md).

    Known Limitations and Deferred Work: the section never FAILs, so Overall and the exit code do not move on a readiness gap, and it does not repeat the rest of the /cyberine:init probe (node, the claude CLI, provider auth, the handoff writer, a rules conflict check) — /cyberine:init stays the full readiness pass and the only one that fixes anything.

Exit code reflects failures: 0 when every check is pass/warn, 1 when at least one [FAIL] line was produced (a malformed config file, an unwritable $CYBERINE_HOME, or a structurally-broken fleet.json). Warnings (missing provider key, unreachable gateway, fleet drift, stale registry entries, a non-executable hook script) never fail the exit code — they are things to notice, not things blocking a healthy install. The chat /doctor command prints the identical report inline. Run npm run selftest:doctor for the headless smoke test (seeds a broken config + a key-free environment, no TTY needed).