team doctor

Reads the machine and says what the team needs before it can run: whether this file is the one the owner approved, whether herdr and each seat's CLI are installed, at the tested version and logged in, whether each launch names the model the file says — and, when it names none, what checks the model the seat really runs — whether each seat's launch line can run in the folder the seat starts in, whether a watch has run for the session, and what each approved budget check reads right now. up and add refuse until the missing ones that block them are done; the last line counts those.

Synopsis

team doctor [--session <name>] [--file <path>] [--login]

What it reads and writes

Reads the team file (or the one --file names) and its warnings, this machine's approval store, the session's state (.agents/team.state.json, for the watch's heartbeat), herdr's version and whether the session is running, and, for each CLI a seat uses, <binary> --version and its login check. A launch line is read the way team splits it — the first word that isn't a variable assignment, and the arguments written ./…, ../… or ~/… — and each is looked for where the line will run: the folder the seat starts in, which for a seat that works in worktrees is the lobby, never a launcher run to find out. The first word is checked before the rest is read: a line whose program is missing is a MISS whatever follows it, and a first word that is one fully quoted literal — "claude", 'zcash' — is checked with the quotes removed, the way a shell would run it, the quotes printed back with the word; a quoted "~/x" is a pathname with a literal ~, not the home. A program written as a path is read the way main's launcher check read it — it must be there and executable, not merely there. A line with a word that quotes or substitutes text is left alone and reported as not checked, never refused — the first word is the one exception, and only when it is a fully quoted literal, read above. A relative argument is a note, never a refusal — its meaning is not knowable and the command may create the path — with one exception: the first word is sh, bash or zsh (by name or by path), its first argument is the script, not an option (an argument starting with -), and that relative path is found from the project root and not from the seat's start folder (an option-bearing line such as zsh -x ../x is a note, not a refusal); the shell cannot start without it and is a MISS. The owner's doctor also runs each approved account's check command once, the way the watch runs it; the command's raw output is parsed and dropped, never shown. It writes nothing.

Who may run it

Anyone, in any terminal. It needs no approval of its own — reporting on the approval is its work. The budget checks run only for the owner, whose approvals they are; any other caller gets the same report without the readings. --file may be aimed by any caller: doctor writes nothing at all, whoever runs it and whatever file it reads.

Flags

FlagMeaning
--session <name>the herdr session to report on, instead of team.session
--file <path>the team file, instead of .agents/team.yaml
--loginread the seats' CLIs' logins alone, one line per CLI, instead of the whole report; herdr, the session and the machine are not read
--help, -hthe usage, and exit 0

What it prints

One line per finding, the level first, then two spaces:

ok    the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)
warn  claude-beacon: its name repeats "beacon"; the session already carries it
warn  claude-beacon: the launch starts Claude Opus 5.5, the file says Claude Sonnet 5.5
--    codex-scribe: the model is chosen by its launcher; checked on the running seat
MISS  install `codex`: it is not on the PATH (codex: codex-scribe)
--    session beacon is running
LevelMeaning
ok as it should be
warnworth knowing; up and add go ahead
MISSa reason to refuse: something has to be installed, logged in or approved first
-- neither: a note, like whether the session is running

The findings come in order: the file's own warnings, one warning per seat whose name or label repeats the project or the session (the session already carries it; the warning never refuses the file), the approval — ok with the signing's number, its date and the signing key's fingerprint (ok the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)) when the record verifies, MISS with the one-line repair when it does not: never approved on this machine, written before records were signed, or refused with the case — the override file when it is not the approved copy or cannot be parsed (a missing line either way: up and add refuse, and the approved copy stays in force — or the shipped profiles, when nothing was approved), each account whose check command is unapproved, or no longer matches the file hashed at approval (a warning either way: that account reads unknown, and up and add still run), the budget checks, then each seat of the approved file whose rules travel as a first message: its file in the project state folder — missing, a symbolic link, not a regular file, wider than 0600, not the owner's, unreadable, or holding something other than the approved rules text — is one warn (codex-scribe: its rules file differs from the approved rules; run \team remove codex-scribe --keep` then `team add codex-scribe` (or `team down` then `team up` for the whole team)— for a coordinator or operator,team downthenteam up(to restart the whole team) —upskips a ready seat, so only the relaunch writes the file), never a rewrite. herdr, one CLI at a time (its version, its login, then each of its seats' launchers and models), one line per seat whose launch line can't run where the seat starts (theMISSnames the start folder, the program that is missing or not executable, and when the same file resolves from the project root, its path to write instead; a line only said to be unchecked, or a relative path that exists nowhere yet, is a-- note —not checked: the command may create it), one note per seat the state records from a launch that predates the process identity — -- : launched before team recorded its process; run `team remove --keep` then `team add ` (or `team down` then `team up` for the whole team) to launch it again(for a coordinator or operator,team downthenteam up(to restart the whole team)), the relaunch being what records the identity — one note per live seat whose file still carries the placeholderversion: "0" initwrites, saying what it runs and the one edit that pins it (or a warning when the model family differs, so the one edit covers both) — the watch, and thetrustnote. A seat the file stops is left out of the CLI findings and the launch lines. A CLI outside its tested range keeps itswarnand says what that means: its screens are untested with this version, and a seat that isn't read at launch is left out, never typed into (herdr's version line says just where it sits — herdr has no screens). The last line counts them, and — the same ruleupandadd` refuse on — says how many of the missing ones block those commands, when any do:

team doctor: nothing missing, 1 warning
team doctor: 2 missing, 0 warnings: 2 of them block `up` and `add`

The trust note is the one finding that carries a whole block. A file whose trust still holds the relative entries an earlier release wrote — or none at all — is told every absolute entry the next up will require, as a block to paste under trust:: the lobby, the project root, the folder workspace.path places worktrees in, each seat's start folder when it sits outside the project, and every entry the owner added by hand, kept in absolute form. One line after the entries says which key of the file each one comes from and which old entry it replaces, and nothing the block lists widens what the old file trusted. An entry a rule of trust refuses — a folder that would cover the home, or the lobby and the approval store — is never suggested: one line names the key that forces it and leaves the choice of folder to the owner. The note is told only while the file really is legacy; once the absolute entries are written, the approval finding carries the next step.

A seat's model is judged by what can check it, and a launch that names none is judged by two questions. First, does the launch run the CLI's own binary, bare: the first word that is not a variable assignment is the binary's own name, with no path, wrapper or shell in front of it — claude and VAR=1 claude do; /opt/x/claude, env claude, npx claude and zsh -c claude run another program, whose choices are not the CLI's own. Second, can the CLI's screen name the model the file declares — asked with the same rules status reads the running seat's model with, so a model another maker spells may be unread on it. Both yes, and nothing prints: status and the watch read the model off the running seat's screen and flag a seat that runs something else. Either no, and one warning names what the owner can do:

warn  <seat>: the launch runs <first>, not <binary>, and names no model: if the launcher chooses the model, say so with model_from: launcher
warn  <seat>: no model flag, and this version can't read <declared> on this CLI's screen: nothing checks that it runs it
warn  <seat>: the launch names no model this version knows; the file says <declared>

The first is a launch that runs another program; the second, the CLI's own binary under a screen that cannot name the declared model, so nothing would flag a seat that runs something else; the third, a CLI this version knows to start on its last-used model. A seat may say outright that its launcher chooses the model, with model_from: launcher: the owner wrote the key and approved it, and the finding is a note — -- <seat>: the model is chosen by its launcher; checked on the running seat, or, when the screen cannot name the declared model, -- <seat>: the model is chosen by its launcher (declared in the file); this version can't read <declared> on this CLI's screen, so nothing checks it. A real multi-vendor file may still warn; every one of these lines names what the owner can do. The model the file declares is spelled by the seat's display in every line that names it.

The placeholder version: "0" a fresh init writes before the owner fills the release number no longer stops a seat's first launch — the launch compares the model family only — so a seat that runs despite it is said as information: -- <seat>: runs Claude Opus 5.5; the file's version "0" is the placeholder init writes — write "5.5" into the file, then run \team approve`. The note is said only for a live seat whose screen names a model of the family the file declares; a seat on another family keeps the launch's own line. No other spelling is the placeholder: version: "0.0"stops the launch as any mismatch would. Nothing is lost by it — a "0" never pinned a release — and the file's approval still covers the version as written, so pinning the number is one edit and oneteam approve`.

A seat the state records waiting at trust gets one line of its own. A fresh read of its pane must show the recorded trust dialog — the same reading team answer checks, and the reason the line moves to warn when it does not:

--    claude-beacon: waiting for owner at trust
warn  claude-beacon: waiting at trust; its pane can't be read
warn  claude-beacon: waiting at trust; the pane is not the trust dialog
warn  claude-beacon: waiting at trust; this version has no trust answer
warn  claude-beacon: waiting at trust; the folder is not the lobby

Every warn is a warn and never a MISS: it blocks team answer and every key team could send, and leaves the owner's own path through team up open. A seat in trust-sent-recovery is a warn with its repair — warn <seat>: trust sent; recovery required; the owner runs team up — and the line never claims a trust dialog is still on screen. A seat waiting at any other classification prints nothing new here: team status carries it.

The key's fingerprint is the first twelve hex digits of the signing key's public half. What it proves is narrow: an owner who noted it sees a replaced key — a process that only reads the key changes nothing team shows. team computes it, and a seat could also replace team.

The budget checks run from the approved budgets copy, the way the watch runs them: the approved command's file, hashed again before it runs, with the watch's timeout. A check that was changed after its approval never runs — the warning above says so. One line per account that names a check: the reading it gave (ok ), or that its output broke the contract, or that it failed or timed out (both warn: that account reads unknown; neither changes the exit code). The command's raw output is never shown, logged or written anywhere, and a reading is reported, not stored. A caller who is not the owner runs no check: one line says so, and everything else is as the owner's. An account nothing can read — its sources name the status line but no seat's CLI ships a quota pattern for it — has a warning of its own: no pattern can read this account. An account read only by a check is fine.

--login reads the logins alone: one line per CLI the file's seats use, in the order the seats first name them, then the same last line. A login check that passed is ok ; one that failed is MISS, with the command that logs in; a CLI that can't be asked — one with no login check, or no launch profile in this version — is -- .

A watch that has not run, or whose heartbeat is old, is MISS here but not a reason for up to refuse: up starts the watch itself.

Refusals

MessageExit
team doctor: unknown option --x / team doctor: unexpected "x" / team doctor: --session needs a value (each with the usage)2
team doctor: line <n>: <message> / team doctor: <message>2

Exit codes

  • 0 — nothing missing; warnings do not change this.
  • 1 — at least one MISS.
  • 2 — the invocation or the team file can't be read.

Examples

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

The account's check is a script of the owner's, hashed at approval; a fake one stands in here:

.agents/openai-quota
#!/bin/sh
echo "weekly 40% used"

Everything here is as it should be:

Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
ok    the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: nothing missing, 1 warning
exit 0

A CLI outside the range this version was tested with keeps its warning and says what that means — herdr's own version line says only where it sits, for the same reason:

Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
ok    the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
warn  claude 2.1.200 is older than the tested 2.1.288: its screens are untested with this version; a seat that isn't read at launch is left out, never typed into
ok    claude-code: logged in
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: nothing missing, 2 warnings
exit 0

A seat's doctor runs no check — only the owner's approvals may run them — and one line says so; everything else is as the owner's:

Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
ok    the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)
--    the budget checks were not run: only the owner runs them
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: nothing missing, 1 warning
exit 0

A seat on a CLI that is not installed needs the file approved again, since it is new, and the CLI installed:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: researcher
    name: codex-scribe
    label: researcher
    cli: codex
    vendor: openai
    model: GPT Codex
    version: "5"
    launch: codex --model gpt-5-codex
Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
MISS  run `team approve`: `limits` changed; seat codex-scribe is not in the approved file
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
MISS  install `codex`: it is not on the PATH (codex: codex-scribe)
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: 2 missing, 1 warning: 2 of them block `up` and `add`
exit 1

A launch that names another model than the file does is a warning, not a refusal — up starts what the launch says:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Sonnet
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: researcher
    name: codex-scribe
    label: researcher
    cli: codex
    vendor: openai
    model: GPT Codex
    version: "5"
    launch: codex --model gpt-5-codex
Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
MISS  run `team approve`: `limits` changed; seat claude-beacon changed; seat codex-scribe is not in the approved file
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
warn  claude-beacon: the launch starts Claude Opus 5.5, the file says Claude Sonnet 5.5
MISS  install `codex`: it is not on the PATH (codex: codex-scribe)
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: 2 missing, 2 warnings: 2 of them block `up` and `add`
exit 1

A launch that names no model is not warned for its own sake: what the seat really runs is read off its screen by status and the watch, and doctor says only what that reading cannot cover. Here one seat launches through a wrapper script and gets the warning, with the repair in it; the other starts the CLI's own binary bare, under a screen that can name the model the file declares, and prints nothing at all:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: researcher
    name: codex-scribe
    label: researcher
    cli: codex
    vendor: openai
    model: GPT Sol
    version: "6"
    launch: team-codex          # the wrapper picks the model and runs codex

  - role: implementer
    name: codex-reader
    label: implementer
    cli: codex
    vendor: openai
    model: GPT Sol
    version: "6"
    launch: codex
Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
MISS  run `team approve`: `limits` changed; seat codex-scribe is not in the approved file; seat codex-reader is not in the approved file
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
ok    codex 0.157.0
ok    codex: logged in
warn  codex-scribe: the launch runs team-codex, not codex, and names no model: if the launcher chooses the model, say so with model_from: launcher
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: 1 missing, 2 warnings: 1 of them block `up` and `add`
exit 1

The wrapper is told by the launch line's first words: none of them is the CLI's own binary, so the line runs something whose choices are not the CLI's own — but when the owner says so, with model_from: launcher on the seat, the same launch is a note instead, saying what really checks the model:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: researcher
    name: codex-scribe
    label: researcher
    cli: codex
    vendor: openai
    model: GPT Sol
    version: "6"
    launch: team-codex          # the wrapper picks the model and runs codex
    model_from: launcher

  - role: implementer
    name: codex-reader
    label: implementer
    cli: codex
    vendor: openai
    model: GPT Sol
    version: "6"
    launch: codex
Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
MISS  run `team approve`: `limits` changed; seat codex-scribe is not in the approved file; seat codex-reader is not in the approved file
ok    the check for openai reads weekly 40% used
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
ok    codex 0.157.0
ok    codex: logged in
--    codex-scribe: the model is chosen by its launcher; checked on the running seat
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: 1 missing, 1 warning: 1 of them block `up` and `add`
exit 1

model_from: launcher wins over the launch line's shape, whatever the shape is; the key is part of the seat's approval, as any seat key is. When the CLI's screen cannot name the declared model, the note says that instead — nothing checks the model, whatever the launcher does with it.

--login answers the one question that needs no herdr, no session and no machine — which CLIs have an account to run under. Two seats share a CLI, so it is said once:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: researcher
    name: codex-scribe
    label: researcher
    cli: codex
    vendor: openai
    model: GPT Codex
    version: "5"
    launch: codex --model gpt-5-codex
Terminal
 team doctor --login ; echo "exit $?"
ok    claude-code: logged in
MISS  log in to codex: `codex login`
team doctor: 1 missing, 0 warnings: 1 of them block `up` and `add`
exit 1

A check that was edited after its approval never runs: the file on disk no longer hashes to what the owner approved, and the account reads unknown until team approve is run again. The file itself is back to the approved one here; only the script changed:

.agents/team.yaml
format: 1
project: beacon
coordinator: claude-keeper
operator: claude-keeper

trust:
  - ~/.config/team/lobby
  - ~/Code/beacon

workspace:
  mode: shared

budgets:                       # owner-only; the checks run from the approved copy
  accounts:
    openai: { kind: subscription, reserve: 10%, sources: [check], check: .agents/openai-quota }

seats:
  - role: coordinator
    name: claude-keeper
    label: coordinator
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5

  - role: implementer
    name: claude-beacon
    label: implementer
    cli: claude-code
    vendor: anthropic
    model: Claude Opus
    version: "5.5"
    launch: claude --model claude-opus-5-5
.agents/openai-quota
#!/bin/sh
echo "weekly 99% used"
Terminal
 team doctor ; echo "exit $?"
warn  claude-beacon: its name repeats "beacon"; the session already carries it
ok    the file is the one the owner approved (approval #1, 2026-10-04, key fe21ef6293de)
warn  the check for openai changed after approval and was not run; that account reads unknown
ok    herdr 0.7.1
--    session beacon is running
ok    claude 2.1.288
ok    claude-code: logged in
ok    the watch is running
ok    the lobby ~/.config/team/lobby: will be created at the first launch
team doctor: nothing missing, 2 warnings
exit 0

An option it doesn't know stops it before it reads anything:

Terminal
 team doctor --nope ; echo "exit $?"
team doctor: unknown option --nope
Usage: team doctor [--session <name>] [--file <path>] [--login]
exit 2