Skip to content

CLI: doctor

Terminal window
buddy-agent-harness doctor [--root <directory>] [--harness <names>] [--format toon|json|text]

doctor reports whether a repository’s harness bridges still resolve: the skill bridges init creates into .agents/skills, and the instruction bridges the init skill writes into AGENTS.md. It is read-only: it never creates, moves, or repairs anything, so it is safe to run at any point, including from a session-start hook.

Nothing has to be installed to diagnose a repository. Run it from npm in the repository root:

Terminal window
npx -y buddy-agent-harness doctor --format text

That is the whole dependency: one npx invocation, on a repository you cloned rather than one you set up. The plugin and its doctor skill add an agent that runs this for you and reads the report, not a capability the command lacks.

A committed directory symlink such as .claude/skills../.agents/skills degrades badly on a native Windows checkout, and it degrades silently.

Creating a symlink on Windows needs SeCreateSymbolicLinkPrivilege, which means Administrator or Developer Mode. Git for Windows gates it separately with core.symlinks, which its installer leaves off by default. With core.symlinks=false git does not error: it checks the symlink out as a regular file whose contents are the target path. The harness looks for a directory, finds a file, and loads zero project skills, with no warning anywhere.

core.symlinks is per-clone. It is not distributed by .gitattributes or by committed config, so it cannot be enforced from the repository. Detection is the only reliable lever, and it is cheap: the path exists but is not a directory.

OptionMeaning
--root <directory>Selects the directory to diagnose. Defaults to the current directory.
--harness <names>Comma-separated harnesses to check in addition to Claude Code and Cursor, such as codex,windsurf.
--format toon|json|textChoose token-efficient TOON output (default), JSON, or a human-readable text report.

The bridge list is derived from the same registry init projects into, so doctor always describes the bridges init actually creates rather than a separate list that can drift.

bin: ~/.local/bin/buddy-agent-harness
bridges[2]{harness,path,kind,status}:
claude-code,.claude/skills,file,degraded
windsurf,.windsurf/skills,none,missing
instructions[2]{harness,path,kind,status}:
claude-code,CLAUDE.md,import,ok
gemini-cli,.gemini/settings.json,none,missing
findings[3]{path,problem,detail}:
.claude/skills,degraded,expected a directory but found a regular file — checkout without core.symlinks
.windsurf/skills,missing,no bridge at this path — the harness sees zero project skills
.gemini/settings.json,instructions-missing,no instruction bridge at this path — the harness reads none of AGENTS.md
help[3]{command,instruction}:
buddy-agent-harness init --copy --force,run `buddy-agent-harness init --copy --force` to rebuild .claude/skills as a real directory
buddy-agent-harness init,run `buddy-agent-harness init` to create the bridge at .windsurf/skills
"","hand .gemini/settings.json to `/buddy-agent-harness:init`, which writes the bridge into it"

Each help row is one repair, in two columns. command is a shell invocation that runs exactly as given and completes the repair; instruction is the same repair in the imperative. command is empty when no single invocation does the job — the third row above, where the repair is the init skill’s judgment and nothing in a shell does it.

That emptiness is the whole point of the split: a caller can tell an executable repair from an instruction without parsing prose. A runnable invocation quoted inside an instruction is not a command either — diverged-both names git diff --no-index because the diff shows you what differs, not because running it reconciles anything.

kind is what is on disk now (symlink, copy, file, or none), and status is whether it works.

StatusMeaning
okThe bridge resolves into .agents/skills.
missingNothing is at the bridge path.
degradedThe path is a regular file. This is the Windows checkout above.
staleThe path is a symlink pointing somewhere other than .agents/skills, or at a target that no longer exists.
divergedThe bridge is a copy whose contents no longer match .agents/skills.

A healthy repository says so outright rather than printing an empty section, so an agent does not re-run with different flags to confirm:

bridges[2]{harness,path,kind,status}:
claude-code,.claude/skills,symlink,ok
windsurf,.windsurf/skills,symlink,ok
instructions[2]{harness,path,kind,status}:
claude-code,CLAUDE.md,import,ok
gemini-cli,.gemini/settings.json,settings-entry,ok
findings: 0 problems found — all 4 bridges resolve

The count spans both sections. Both are bridges, and a reader learning that nothing is wrong should not have to add two numbers together.

TOON is the default because it is what an agent parses. --format text renders the same result for a person, with each collection as an aligned table:

bin: ~/.local/bin/buddy-agent-harness
bridges:
harness path kind status
claude-code .claude/skills file degraded
windsurf .windsurf/skills none missing
instructions:
harness path kind status
claude-code CLAUDE.md import ok
gemini-cli .gemini/settings.json none missing
findings:
path problem detail
.claude/skills degraded expected a directory but found a regular file — checkout without core.symlinks
.windsurf/skills missing no bridge at this path — the harness sees zero project skills
.gemini/settings.json instructions-missing no instruction bridge at this path — the harness reads none of AGENTS.md
help:
command instruction
buddy-agent-harness init --copy --force run `buddy-agent-harness init --copy --force` to rebuild .claude/skills as a real directory
buddy-agent-harness init run `buddy-agent-harness init` to create the bridge at .windsurf/skills
hand .gemini/settings.json to `/buddy-agent-harness:init`, which writes the bridge into it

init accepts the same flag.

A repository that consolidated into AGENTS.md needs a second bridge per harness that cannot read it. Claude Code reads CLAUDE.md, so it gets one holding @AGENTS.md. Gemini CLI reads the context.fileName array in .gemini/settings.json, and AGENTS.md is not in its default list, so without that entry it reads no instructions at all.

These fail as silently as a skills bridge and cost more. Losing a skills bridge costs a repository its skills; losing the instruction bridge costs it every instruction it has.

They are a separate instructions section rather than more bridges rows, because nothing about them is shared:

  • The kind and status vocabularies differ. stale and diverged describe a directory projection and mean nothing for a Markdown import or a JSON array entry.
  • The repair is never a command. init writes skills projections; the instruction files carry prose a person authored, so restoring a bridge without discarding what displaced it is the init skill’s judgment.
  • A bridges row is a directory the CLI wrote. An instructions row is a file the skill wrote. Merging them would make one section mean two things.
StatusMeaning
okThe harness reaches AGENTS.md through this file.
missingNothing is at the path.
unbridgedThe file is there and names AGENTS.md nowhere.
unreadableA settings file that does not parse as JSON, once its comments are removed.

unbridged is the case with no equivalent on the skills side, and the reason these checks exist. Nothing looks wrong: CLAUDE.md is present, and it holds something a well-meaning agent wrote over the import; or .gemini/settings.json is present, and another tool rewrote it without the entry. The repair never replaces the file, because the content that displaced the bridge may be the only copy of it.

Bridges are reported per file, not per harness. An import bridges the AGENTS.md beside it and nothing deeper, so every directory holding a nested AGENTS.md gets its own row:

instructions[3]{harness,path,kind,status}:
claude-code,CLAUDE.md,import,ok
claude-code,apps/web/CLAUDE.md,none,missing
claude-code,packages/core/CLAUDE.md,import,ok

AGENTS.md files under a dot-directory or node_modules are not counted. .agents/AGENTS.md is canonical shared instructions rather than instructions scoped to a subtree, and a vendored one is not this repository’s to bridge.

A repository with no root AGENTS.md gets one finding saying so, and no bridge rows — there is nothing for a bridge to point at.

.gemini/settings.json may legally carry comments — the Gemini CLI loader strips them before parsing — so doctor strips them too. Reporting a commented settings file as broken would be a false alarm on a file that works. A trailing comma is still a parse error, because nothing documents it as accepted. Harness Differences covers the disagreement between the two settings.json files.

Which harnesses are checked is the same question as for skills: the registry records an instruction bridge per harness, and --harness gates both kinds together. Codex, Cursor, Copilot CLI, and Devin Desktop read AGENTS.md where it lies, so they get no rows — see Harness Differences.

A symlink makes write-back drift structurally impossible: an agent that edits .claude/skills/<name>/SKILL.md edits .agents/skills/<name>/SKILL.md. A copy does not. The edit lands in the copy, and the two sides drift apart.

“These two differ” is not actionable, so a diverged bridge also gets a direction, computed against the last commit where the two agreed:

divergence[1]{path,direction}:
.claude/skills,bridge
DirectionWhat happenedWhat to do
bridgeAn agent wrote through the copy.Propagate the bridge into .agents/skills, then re-run init --force.
canonicalOrdinary staleness.Refresh the bridge with init --copy --force.
bothA genuine conflict.Reconcile by hand. Rebuilding discards one side’s edit.
unknownNo commit where the two agreed was found.Reconcile by hand.

Running init --copy --force over a tracked symlink turns the bridge from a symlink blob into a directory of real files, leaving a permanently dirty tree holding something that must never be committed. git update-index --skip-worktree is the intended tool for a tracked path deliberately different on one machine.

It is a hint rather than a guarantee, because some checkout and merge operations clear it. So doctor verifies the bit is still set on a tracked copy rather than assuming it, and reports the path as dirty-and-uncommittable if it has been lost.

Alongside the bridge and instruction sections, doctor reports configuration that is present and wrong. None of these is a resolution problem — every one resolves fine.

FindingWhat is wrong
deprecated-harnessa skills projection under a harness name that has been superseded. The replacement reads .agents/skills natively, so the correction removes the projection rather than renaming it
ignored-bridgea .gitignore rule matches a bridge, usually a blanket .claude/. Asked of git check-ignore, so a rule on a parent directory is caught
unread-local-overridean AGENTS.local.md. No harness reads that filename, so its content is invisible
unloadable-skillfrontmatter that does not parse, or no description — the two faults that make a harness skip a skill outright. A name that mismatches its directory is only a warning, so it is deliberately not reported

These four repair through the repair skill, which offers each correction with its before and after and writes only what you approve.

Each finding row carries three fields: problem is its name and the only thing a program should branch on, path is what it is about, and detail is prose for a person. Route on problem, never on detail — the prose is meant to be read, and improving its wording must not change what a caller does.

--harness does not affect these checks. Every one requires a projection to exist on disk, and a projection cannot exist without its harness’s own directory, which selects that harness already.

Where a repository keeps a golden MCP server set at .agents/buddy-agent-harness/mcp.toml, doctor compares it against each harness’s project-scope MCP configuration and reports drift in both directions, as mcp-* findings in the same findings section. Literal credentials in any MCP file are reported whether or not a golden set exists, and a secret’s value never enters the report — the finding carries a locator like .cursor/mcp.json#servers.linear.headers.Authorization and nothing else.

These findings stay within the command’s contract: read-only, --harness-independent for the same reason as the configuration findings, and routed on problem. What the golden set is, which file each harness reads, and every mcp-* finding with its repair are on MCP Servers.

doctor exits 0 even when it has findings. The diagnosis succeeded, and a non-zero code reads to an agent as “this command is broken, try something else.” A --strict flag for CI, the one caller that genuinely wants a failing process, is not implemented yet.

Every repair is already expressible with existing init flags, and each finding names the exact command — or says plainly, with an empty command, that no command does it:

FindingRepair
no-canonicalbuddy-agent-harness init
missingbuddy-agent-harness init
degradedbuddy-agent-harness init --copy --force
stalebuddy-agent-harness init --force
divergedDepends on the direction; see above.
unpinned-copygit ls-files -z <path> | xargs -0 git update-index --skip-worktree
no-instructions/buddy-agent-harness:init
instructions-missing/buddy-agent-harness:init
instructions-unbridged/buddy-agent-harness:init
instructions-unreadableFix the JSON by hand, then /buddy-agent-harness:init

no-canonical is the one finding that is not about a bridge: .agents/skills itself is absent, so nothing can resolve into it. no-instructions is its counterpart for AGENTS.md. unpinned-copy is the skip-worktree case, and it is reported against a bridge whose status is still ok.

The four instruction repairs name a skill rather than a shell command, because no shell command does the job. They carry an empty command for exactly that reason, so a caller never has to work out which rows it can run.

A --fix flag would reimplement that logic and drift from it. On the Windows case it would likely reimplement it wrongly: the naive repair is to recreate the link, which is precisely the operation that already failed on that machine. --copy is the branch that works there. The three-way divergence case has no safe automatic answer at all.