koryph doctor
koryph doctor checks the health of the koryph installation.
Without --project it runs global checks against ~/.koryph.
With --project <id> it runs project-scoped checks against the named project.
Usage
koryph doctor [--json] [--fix]
koryph doctor --project <project-id> [--json]
| Flag | Description |
|---|---|
--project <id> |
Run project-scoped checks for the named registered project |
--json |
Emit the report as a JSON object instead of a text table |
--fix |
Remove zombie slot lease files and stale demand heartbeats (global mode) |
Exit codes
| Code | Meaning |
|---|---|
0 |
All checks passed |
1 |
One or more warnings |
2 |
One or more errors |
Checks
layout
Verifies that ~/.koryph exists with the required subdirectory skeleton
(registry.d/, quota/, slots/). A missing home directory or subdirectory
is an error — run koryph init to repair.
binaries
Checks that git, claude, and bd are on PATH. Missing binaries are
warnings (the CLI can still run for some commands).
beads-version
Checks that the resolved bd binary (the KORYPH_BD_BIN override, else bd
on PATH) is new enough to emit the parent field koryph relies on. Older
bd releases (≤ 1.0.3) omit parent from bd list --json, which silently
degrades koryph — the TUI Queue tab renders flat (every issue a top-level row,
no epic folds) and parent-linked views mis-group — with no error anywhere else.
A too-old bd is a warning whose remediation is tailored to how bd was
installed: a bd resolved from a /nix/store/ path is flagged as
nix-provided, with advice to bump the flake/devshell pin (or set
KORYPH_BD_BIN to a newer bd as a stopgap); otherwise it suggests a
package-manager upgrade. The same preflight prints a one-line warning at the
top of koryph run and in the koryph tui header, so the degradation is never
silent.
In project mode (koryph doctor --project <id>), when the stale bd is
nix-provided and the project's flake.nix pins a beads input, doctor makes
a concrete upgrade offer: it names the exact
nix flake lock --update-input <input> command and the project directory.
Adding --fix runs that command (re-locking flake.lock to the pinned
version); you then reload the devshell (direnv reload or re-enter
nix develop) so the running bd picks up the new version. This turns a
stale-lock warning into a one-command remedy.
registry
Parses every *.json file in registry.d/. A file that is not valid JSON
is an error.
auth-mode
Reports, for every registered account, its effective auth mode
(subscription, api-key, or oauth-token), where its credential comes
from, and its identity fingerprint prefix — never the credential value
itself:
subscription(the default) needs no credential and is always ok.api-key/oauth-tokenwith avaultcredential reports the vault provider and key reference; with anenvcredential it reports the environment variable name (never its value).- A non-subscription account with no credential configured is an error. One with a credential but no identity fingerprint yet is a warning — the fingerprint is captured at the account's next dispatch.
- A per-runtime account override (
runtime_accounts) is reported separately only when its resolved auth mode, credential, or fingerprint diverges from the record's own; a runtime that just mirrors the record is not repeated.
governor
Validates governor.json when present. An absent file is fine — koryph
falls back to the default cap. A corrupt or zero-value file is an
error/warning respectively.
memory-floor
Flags any pool in governor.json with no explicit min_free_memory_mb
(koryph-4rk6.1) — the exact gap ("anthropic had a floor, personal/work
didn't") behind the 2026-07-21 OOM incident. A pool with a positive floor or
an explicit negative (deliberately disabled) is ok; a pool at the raw,
never-set 0 is a warning naming the koryph governor set
--min-free-memory-mb command to pin one. koryph run self-heals this on
every startup by backfilling DefaultMinFreeMemoryMB onto any such pool, so
in practice this check mostly catches drift from a governor.json edited or
restored outside of koryph.
zombie-leases
Scans slots/*.json for lease files whose tracked PID is no longer alive.
Each zombie is a warning; pass --fix to remove them immediately.
koryph doctor --fix
stale-demand
Scans slots/demand/*.json for demand heartbeats whose engine PID is dead or
whose updated_at timestamp is older than 10 minutes. Each stale entry is a
warning; pass --fix to remove them.
quota-calibration
Reads each quota/<account>.json and checks whether both window_ceiling_usd
and weekly_ceiling_usd are greater than zero. An uncalibrated account is a
warning — the governor runs in advisory-only mode until calibrated:
koryph quota calibrate --account personal --window 5h \
--observed-usd 4.20 --observed-pct 21
vault-providers
When ~/.koryph/vault.json is present, checks that the first binary in
each provider's fetch template (e.g. pass-cli for ProtonPass, op for
1Password) is on PATH. A missing binary is a warning.
proxy
For every registered project with an agent_proxy block configured (see
Headroom integration), runs four checks:
- loopback — confirms
base_urlis still anhttp://loopback address (the registry already refuses to load a non-loopback value, but a hand-edited record could bypass that; a non-loopbackbase_urlis an error). - health —
GET <base_url><health>must return 2xx. Unreachable or non-2xx is an error; an unconfiguredhealthpath is a warning. - pin — when
pinis set, compares it against the"pin"field in the health response. A mismatch is an error with refuse-to-route guidance (a different proxy version is running than the registry expects); a health response with no"pin"field is a warning (cannot verify). - routing verification — compares koryph's own ledger count of
dispatches routed to this proxy's arm against the proxy's self-reported
forwarded-request counter (
GET <base_url><stats>,statsdefaulting to/stats). This exists because health+pin only prove the proxy process is up and correctly versioned, not that dispatched traffic is actually flowing through it — a proxy that is healthy and correctly pinned but silently bypassed (e.g. aChildEnvSpec/ANTHROPIC_BASE_URLwiring regression) would otherwise pass doctor clean while every dispatch goes direct. Outcomes: - No proxied-arm dispatches recorded yet → ok (nothing to verify).
- Stats endpoint unreachable, non-2xx, or its JSON body has no field
doctor recognizes as a request counter (a
requests/request_count/total_requests-style name, top level or nested one level) → warning naming the limitation. Never a silent pass. - Dispatches recorded but the proxy reports zero upstream-seen requests → error — configured but not in path; refuse-to-route until the wiring is fixed.
- Dispatches recorded and the proxy reports a nonzero count → ok.
Project-mode checks
When --project <id> is given, koryph doctor runs against the project's
repository root instead of ~/.koryph. These checks mirror what
koryph onboard validates structurally (no network calls, no subprocess
invocations):
project-config
Loads and validates koryph.project.json. A missing or corrupt file is an
error.
git-repo
Verifies that .git exists at the project root. A missing .git is an
error.
hooks-wiring
Checks .claude/settings.json for the three koryph hook markers:
bd prime (SessionStart), agent-boundary-guard.sh (PreToolUse Bash), and
worktree-guard.sh (PreToolUse Bash|Edit|Write). Each missing marker is a
warning — run koryph rules install to repair.
signing
Inspects the project's signing block in koryph.project.json:
- Absent block → ok (signing not configured).
- Provider set but public_key not captured → warning (run
koryph signing setup).
- Invalid config shape (caught by project-config parse) → error.
protected-paths
Validates the protected_paths list for empty entries (error) and
duplicate entries (warning).
stalled-runs
Scans the project ledger (.plan-logs/koryph/) for runs in running status
where any non-terminal slot has not been updated for more than 30 minutes.
Each stalled slot is a warning. Investigate manually — stalled agents may
need to be stopped with koryph stop.
koryph doctor --project koryph
orphan-worktrees
Lists git worktrees registered under the project's worktree root
(default <parent>/<repo>-worktrees/) whose branch starts with agent/ but
have no corresponding active slot in any currently-running ledger run. Each
orphan is a warning. Koryph never removes a dirty worktree automatically;
review and remove manually:
git worktree remove --force path/to/orphan
ci-assets
Checks whether the koryph gate pipeline CI workflow is installed and up to date with the current template rendering.
The check is skipped (ok) when the project has no recognisable forge remote (not a GitHub repository or no git remote configured).
When the gate pipeline is absent or its content drifted from what
koryph ci setup would render, the finding is a warning with the exact
remediation command:
koryph ci setup --project <id>
When the installed file matches the current template the finding is ok.
The gate pipeline path is .github/workflows/koryph-gate.yml for GitHub
projects. Install or update it with:
koryph ci setup --project <id>
koryph ci setup --project <id> --force # overwrite a locally modified file
container release
When release.container is configured, doctor validates the separately
generated GHCR image-release workflow:
- container-release-block compares the
release.containerblock with.github/workflows/container.yml. A configured container without the file, or the file without the configuration, is a warning. Re-runkoryph release setupafter configuring a container; remove a no-longer-used workflow when disabling one. Invalid container fields are reported by the project-config error before doctor renders any expected workflow. - container-dockerfile checks for the repository-root
Dockerfileused by the generated workflow's defaultcontext: .build. A missing file (or a directory at that path) is a warning; add a regularDockerfileor disable the container release. - container-workflow-drift compares the installed workflow with the
current renderer output. A mismatch is a warning with the exact
koryph release setupremediation. The check is skipped when no container release is configured or when the consistency check already found the file missing. - container-publication-gate confirms that the publish job depends on a
detected Release PR merge. A hand edit that could publish on an ordinary
mainpush is a warning; rerunkoryph release setupto restore the gate. - container-permission-scope confirms that
packages: write, OIDC, and attestation permissions live only on the post-gate publish job. A write permission at workflow scope or another job, or a missing publish permission, is a warning; rerunkoryph release setupto restore least privilege.
See Releasing projects for the GHCR, Dockerfile, tag, signing, SBOM, and provenance contract.
epic-validations
Reports open epics carrying validation:parked (exceeded max_rounds,
awaiting an operator decision) or validation:degraded (validator infra
failure) as warnings, each naming koryph epic validate <id> as the
recovery command. See Epic validation for the full
label vocabulary.
unvalidated-epics
Reports open epics whose children are all closed but that never
themselves closed — validation was never triggered, or the docs-bead
close-after-docs path stalled after the docs bead closed. This is the
offline counterpart to the live loop's hourly health-patrol backstop (see
Self-healing: stranded completed epics):
useful when no koryph run loop is currently active to self-heal it. Each
finding is a warning naming koryph epic validate <id> as the recovery
command.
JSON output
{
"at": "2026-07-02T12:00:00Z",
"home": "/Users/you/.koryph",
"findings": [
{ "check": "layout", "level": "ok", "message": "layout ok" },
{ "check": "zombie-leases", "level": "ok", "message": "fixed",
"fixed": true }
],
"fixed_count": 1
}
In project mode the response includes a "project" field and "home" is the
project's repo root:
{
"at": "2026-07-02T12:00:00Z",
"home": "/Users/you/src/myproject",
"project": "myproject",
"findings": [
{ "check": "project-config", "level": "ok", "message": "project_id=myproject work_source=bd" },
{ "check": "stalled-runs", "level": "warn", "message": "stalled slot: run=20260702-100000 phase=bead-x status=running age=1h30m0s" }
]
}
The top-level fixed_count is non-zero only when --fix was passed and
files were actually removed (global mode only).