CLI: doctor
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.
No install needed
Section titled “No install needed”Nothing has to be installed to diagnose a repository. Run it from npm in the repository root:
npx -y buddy-agent-harness doctor --format textThat 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.
Why it exists
Section titled “Why it exists”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.
Options
Section titled “Options”| Option | Meaning |
|---|---|
--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|text | Choose 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.
Output
Section titled “Output”bin: ~/.local/bin/buddy-agent-harnessbridges[2]{harness,path,kind,status}: claude-code,.claude/skills,file,degraded windsurf,.windsurf/skills,none,missinginstructions[2]{harness,path,kind,status}: claude-code,CLAUDE.md,import,ok gemini-cli,.gemini/settings.json,none,missingfindings[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.mdhelp[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.
| Status | Meaning |
|---|---|
ok | The bridge resolves into .agents/skills. |
missing | Nothing is at the bridge path. |
degraded | The path is a regular file. This is the Windows checkout above. |
stale | The path is a symlink pointing somewhere other than .agents/skills, or at a target that no longer exists. |
diverged | The 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,okinstructions[2]{harness,path,kind,status}: claude-code,CLAUDE.md,import,ok gemini-cli,.gemini/settings.json,settings-entry,okfindings: 0 problems found — all 4 bridges resolveThe count spans both sections. Both are bridges, and a reader learning that nothing is wrong should not have to add two numbers together.
--format text
Section titled “--format text”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 itinit accepts the same flag.
Instruction bridges
Section titled “Instruction bridges”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
kindandstatusvocabularies differ.staleanddivergeddescribe a directory projection and mean nothing for a Markdown import or a JSON array entry. - The repair is never a command.
initwrites skills projections; the instruction files carry prose a person authored, so restoring a bridge without discarding what displaced it is theinitskill’s judgment. - A
bridgesrow is a directory the CLI wrote. Aninstructionsrow is a file the skill wrote. Merging them would make one section mean two things.
| Status | Meaning |
|---|---|
ok | The harness reaches AGENTS.md through this file. |
missing | Nothing is at the path. |
unbridged | The file is there and names AGENTS.md nowhere. |
unreadable | A 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,okAGENTS.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.
Divergence
Section titled “Divergence”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| Direction | What happened | What to do |
|---|---|---|
bridge | An agent wrote through the copy. | Propagate the bridge into .agents/skills, then re-run init --force. |
canonical | Ordinary staleness. | Refresh the bridge with init --copy --force. |
both | A genuine conflict. | Reconcile by hand. Rebuilding discards one side’s edit. |
unknown | No commit where the two agreed was found. | Reconcile by hand. |
The skip-worktree bit
Section titled “The skip-worktree bit”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.
Configuration findings
Section titled “Configuration findings”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.
| Finding | What is wrong |
|---|---|
deprecated-harness | a 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-bridge | a .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-override | an AGENTS.local.md. No harness reads that filename, so its content is invisible |
unloadable-skill | frontmatter 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.
MCP findings
Section titled “MCP findings”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.
Exit codes
Section titled “Exit codes”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.
No --fix
Section titled “No --fix”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:
| Finding | Repair |
|---|---|
no-canonical | buddy-agent-harness init |
missing | buddy-agent-harness init |
degraded | buddy-agent-harness init --copy --force |
stale | buddy-agent-harness init --force |
diverged | Depends on the direction; see above. |
unpinned-copy | git 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-unreadable | Fix 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.