buddy-agent-harness 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +9 -1
  5. package/bin/buddy-agent-harness.mjs +8 -3
  6. package/dist/cli-4ekejn5W.mjs +2735 -0
  7. package/dist/cli-4ekejn5W.mjs.map +1 -0
  8. package/dist/cli.d.mts +12 -2
  9. package/dist/cli.d.mts.map +1 -1
  10. package/dist/cli.mjs +2 -20
  11. package/dist/index.d.mts +389 -54
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +2 -2
  14. package/package.json +6 -5
  15. package/plugin.json +1 -1
  16. package/skills/doctor/SKILL.md +20 -34
  17. package/skills/doctor/references/bridges.md +23 -0
  18. package/skills/doctor/references/configuration.md +12 -0
  19. package/skills/doctor/references/harnesses/claude-code.md +39 -0
  20. package/skills/doctor/references/harnesses/codex.md +37 -0
  21. package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
  22. package/skills/doctor/references/harnesses/cursor.md +40 -0
  23. package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
  24. package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
  25. package/skills/doctor/references/harnesses/windsurf.md +27 -0
  26. package/skills/doctor/references/instructions.md +14 -0
  27. package/skills/doctor/references/mcp.md +31 -0
  28. package/skills/doctor/references/nonstandard.md +27 -0
  29. package/skills/doctor/scripts/doctor.mjs +7 -3
  30. package/skills/enhance/SKILL.md +4 -0
  31. package/skills/enhance/references/delegation.md +3 -1
  32. package/skills/init/README.md +1 -1
  33. package/skills/init/SKILL.md +19 -8
  34. package/skills/init/references/detection.md +5 -3
  35. package/skills/init/references/harnesses/cursor.md +9 -2
  36. package/skills/init/scripts/doctor.mjs +14 -0
  37. package/skills/init/scripts/init.mjs +7 -3
  38. package/skills/repair/README.md +21 -0
  39. package/skills/repair/SKILL.md +74 -0
  40. package/skills/repair/references/classes.md +49 -0
  41. package/skills/repair/scripts/doctor.mjs +14 -0
  42. package/src/cli.ts +42 -7
  43. package/src/command-output/command-output.ts +1 -4
  44. package/src/dep-plugins/dep-plugins.command.ts +168 -0
  45. package/src/dep-plugins/dep-plugins.ts +131 -0
  46. package/src/dep-plugins/generate.ts +115 -0
  47. package/src/dep-plugins/harness-plugins.ts +232 -0
  48. package/src/dep-plugins/reconcile.ts +132 -0
  49. package/src/dep-plugins/resolve.ts +117 -0
  50. package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
  51. package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
  52. package/src/diagnose-bridges/doctor-guidance.ts +636 -90
  53. package/src/diagnose-bridges/doctor.command.ts +55 -28
  54. package/src/diagnose-bridges/git-bridge-state.ts +30 -0
  55. package/src/diagnose-bridges/locator.ts +36 -0
  56. package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
  57. package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
  58. package/src/diagnose-mcp/mcp-baseline.ts +122 -0
  59. package/src/diagnose-mcp/mcp-model.ts +88 -0
  60. package/src/diagnose-mcp/mcp-secrets.ts +101 -0
  61. package/src/diagnose-mcp/mcp-sources.ts +131 -0
  62. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
  63. package/src/harness-registry/harness-registry.ts +97 -4
  64. package/src/harness-registry/mcp-config.ts +37 -0
  65. package/src/harness-registry/nonstandard-artifact.ts +39 -0
  66. package/src/index.ts +18 -2
  67. package/src/initialize-harnesses/init.command.ts +13 -18
  68. package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
  69. package/src/is-record/is-record.ts +12 -0
  70. package/src/skill-projection/skill-projection.ts +102 -16
  71. package/dist/cli.mjs.map +0 -1
  72. package/dist/init.command-zbntulIq.mjs +0 -1016
  73. package/dist/init.command-zbntulIq.mjs.map +0 -1
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { run } from "./cli.mjs";
1
2
  import { cli as cli$1 } from "clibuilder";
2
3
  //#region src/harness-registry/instruction-bridge.d.ts
3
4
  /**
@@ -30,6 +31,71 @@ type InstructionBridge = {
30
31
  key: string;
31
32
  };
32
33
  //#endregion
34
+ //#region src/harness-registry/mcp-config.d.ts
35
+ /**
36
+ * Where one harness keeps its MCP server configuration at one scope, as that harness's own vendor
37
+ * documents it.
38
+ *
39
+ * Recorded per scope like everything else in a `HarnessScope`, and populated at **project** scope
40
+ * only. Much of the world's MCP configuration lives at user scope — `~/.codex/config.toml`,
41
+ * `~/.claude.json`, `claude_desktop_config.json` — and the registry records user scope as
42
+ * described and diagnosable, never written. Reading a user's home directory into output that
43
+ * lands in every session's transcript is a wider blast radius than diagnosis needs, so those paths
44
+ * stay out until something needs them.
45
+ *
46
+ * A harness with no documented project-scope MCP file gets no entry. Copilot CLI is the case that
47
+ * matters: its own documentation states that `.vscode/mcp.json` "is not read by Copilot CLI", and
48
+ * names `~/.copilot/mcp-config.json` instead. That is a documented absence, not a gap — see
49
+ * `.research/mcp-canonical-location/` (E-MCP-11).
50
+ */
51
+ type McpConfig = {
52
+ /** Repository-relative path of the file, as the vendor documents it. */
53
+ path: string;
54
+ /**
55
+ * The key the servers sit under. Not one key across hosts: `mcpServers` for Claude Code, Cursor,
56
+ * and Gemini CLI, `mcp_servers` for Codex, `servers` for VS Code. Reading the wrong one finds
57
+ * nothing and reports a repository as unconfigured.
58
+ */
59
+ key: string;
60
+ /**
61
+ * How the file is serialized. It decides the parser, and it is also why comparison is semantic:
62
+ * the same server in TOML and in JSON shares no bytes.
63
+ */
64
+ format: "json" | "toml";
65
+ /**
66
+ * Set when the file holds far more than MCP configuration. `.gemini/settings.json` also carries
67
+ * the instruction bridge, so anything reading it must take its key and leave the rest alone —
68
+ * and anything writing it must never rewrite the file wholesale.
69
+ */
70
+ shared?: true;
71
+ };
72
+ //#endregion
73
+ //#region src/harness-registry/nonstandard-artifact.d.ts
74
+ /**
75
+ * Configuration a harness reads that no other harness can, declared per harness beside the paths
76
+ * `doctor` already knows about it.
77
+ *
78
+ * These are not faults. Every one of them works — for exactly one harness — which is the whole
79
+ * problem: the guidance in a Cursor rule reaches Cursor and nothing else, and nobody finds out
80
+ * except by noticing an agent behaving differently in another tool. `doctor` surfaces them so they
81
+ * can be converted, one at a time, toward a repository whose agent configuration is canonical and
82
+ * whose harness files are generated from it. Zero of these is the north star, not a release gate.
83
+ *
84
+ * `kind` carries the conversion, because the canonical form differs per kind and nothing else about
85
+ * the artifact does. It is what a finding routes on.
86
+ */
87
+ type NonstandardKind = "instructions" | "rule" | "command" | "skill" | "subagent";
88
+ type NonstandardArtifact = {
89
+ /**
90
+ * Repository-relative path. A file is reported as itself; a directory is reported per file
91
+ * below it, so each one can be converted on its own rather than as an all-or-nothing batch.
92
+ */
93
+ path: string;
94
+ /** Whether `path` names one file or a directory to walk. */
95
+ shape: "file" | "directory";
96
+ kind: NonstandardKind;
97
+ };
98
+ //#endregion
33
99
  //#region src/harness-registry/harness-registry.d.ts
34
100
  type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf";
35
101
  /** The two scopes a harness reads configuration at. Their roots differ; their shape does not. */
@@ -44,6 +110,15 @@ type HarnessScopeName = "project" | "user";
44
110
  * `skillsDirectory` is the projection target the harness needs to see `.agents/skills` at this
45
111
  * scope. A harness that reads `.agents/skills` natively there has none, and is never projected into.
46
112
  *
113
+ * `mcpConfig` is where this harness keeps its MCP servers at this scope. Unlike the other two it
114
+ * is not a bridge into a canonical file the harness cannot read — it is the harness's own
115
+ * configuration, which `doctor` compares against the golden set and never writes.
116
+ *
117
+ * `nonstandard` is what this harness reads that no other harness can. Unlike the three above it is
118
+ * not something the tool maintains — it is what a repository accumulated before it was canonical,
119
+ * and `doctor` reports it so it can be converted. Declared here because the paths are per harness
120
+ * and belong beside the harness's other paths, not in a second list that would drift from this one.
121
+ *
47
122
  * `instructionBridge` is the same question for `AGENTS.md`: what the harness needs at this scope in
48
123
  * order to read it. It belongs per scope for the same reason `skillsDirectory` does — the file
49
124
  * differs. Unlike `skillsDirectory`, the `init` command does not write it; the `init` skill does. It
@@ -53,6 +128,8 @@ type HarnessScope = {
53
128
  detect: string;
54
129
  skillsDirectory?: string;
55
130
  instructionBridge?: InstructionBridge;
131
+ mcpConfig?: McpConfig;
132
+ nonstandard?: readonly NonstandardArtifact[];
56
133
  };
57
134
  type Harness = {
58
135
  name: HarnessName;
@@ -86,7 +163,7 @@ type Harness = {
86
163
  *
87
164
  * See `.research/agentic-configuration-standards/` for the per-harness sources.
88
165
  */
89
- declare const harnessRegistry: readonly Harness[];
166
+ export declare const harnessRegistry: readonly Harness[];
90
167
  //#endregion
91
168
  //#region src/diagnose-bridges/diagnose-instructions.d.ts
92
169
  /** What is on disk where an instruction bridge belongs. */
@@ -112,14 +189,195 @@ type InstructionReport = {
112
189
  * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
113
190
  * skill's judgment.
114
191
  */
115
- declare function diagnoseInstructions(root: string, harnesses: readonly Harness[], cli: string): {
192
+ export declare function diagnoseInstructions(root: string, harnesses: readonly Harness[], cli: string): {
116
193
  instructions: InstructionReport[];
117
194
  findings: BridgeFinding[];
118
195
  };
119
196
  //#endregion
197
+ //#region src/diagnose-bridges/locator.d.ts
198
+ /**
199
+ * Where a finding is, in parts.
200
+ *
201
+ * A locator addresses a place inside a file — `.cursor/mcp.json#servers.linear.command` — because a
202
+ * file holding twenty servers is not an address. The parts are known wherever a finding is raised,
203
+ * and only the report and the repairs need the string, so the parts are what travel and this module
204
+ * is the one place they become one. Nothing ever recovers a part by splitting a locator back apart:
205
+ * a server may be named `io.github.foo`, and no split of the rendered string tells where the server
206
+ * name ends and the field begins.
207
+ */
208
+ /**
209
+ * Where a malformed file failed. **Position only.** A parser's own error message quotes the
210
+ * offending line back, and in a file of MCP configuration that line is exactly the one holding a
211
+ * credential — so neither the message nor the code block it carries is ever read. Line and column
212
+ * are enough to fix the file and carry nothing out of it.
213
+ */
214
+ type Position = {
215
+ line: number;
216
+ column: number;
217
+ };
218
+ type Locator = {
219
+ /** Repository-relative path of the file the finding is about. */
220
+ file: string;
221
+ /** The MCP server inside that file, when the finding is about one. */
222
+ server?: string;
223
+ /** The field inside that server, dotted for the per-name fields — `command`, `env.LINEAR_TOKEN`. */
224
+ field?: string;
225
+ /** Where a file that does not parse failed, when the parser gave a position. */
226
+ position?: Position | undefined;
227
+ };
228
+ //#endregion
229
+ //#region src/diagnose-bridges/doctor-guidance.d.ts
230
+ /**
231
+ * The one place `doctor`'s guidance is written. The command reads `detail` and `repair` to fill its
232
+ * `findings` and `help` sections; `skills/doctor/SKILL.md` is generated from the same table by
233
+ * `scripts/generate-skills.ts`, so the shipped skill cannot drift from what the command says.
234
+ */
235
+ /** Every way a skills bridge can fail, in the order `doctor` reports them. */
236
+ type BridgeProblem = "no-canonical" | "missing" | "degraded" | "stale" | "diverged-bridge" | "diverged-canonical" | "diverged-both" | "diverged-unknown" | "unpinned-copy";
237
+ /**
238
+ * Every way an instruction bridge can fail. Separate from `BridgeProblem` because the two share no
239
+ * repair: a skills bridge is rebuilt with `init` flags, and an instruction bridge is a file whose
240
+ * content is the user's, so every repair here goes back to the `init` skill.
241
+ */
242
+ /**
243
+ * Configuration that is present and **wrong**, as against a bridge that does not resolve. These are
244
+ * the faults the `repair` skill owns: none is expressible as an `init` flag, because `init`
245
+ * consolidates and creates but never corrects a file the user already wrote.
246
+ */
247
+ type ConfigurationFault = "deprecated-harness" | "ignored-bridge" | "unread-local-override" | "unloadable-skill";
248
+ /**
249
+ * Every way the golden MCP server set and a harness's copy of it can disagree, plus the two
250
+ * credential findings.
251
+ *
252
+ * A family of its own because nothing it says is shared with the others. A bridge either resolves
253
+ * or does not; two MCP files never share a byte, so every finding here is the result of a semantic
254
+ * comparison, and each one names a **server** and usually a **field** rather than a file.
255
+ */
256
+ type McpProblem = "mcp-golden-unreadable" | "mcp-target-unreadable" | "mcp-unprojected" | "mcp-undeclared" | "mcp-diverged-target" | "mcp-diverged-golden" | "mcp-diverged-both" | "mcp-diverged-unknown" | "mcp-literal-secret" | "mcp-committed-secret";
257
+ /**
258
+ * Configuration that works, for exactly one harness. Not wrong and not missing — the two states the
259
+ * other families cover — which is why it is a family of its own: every other finding says something
260
+ * is broken, and these say something reaches less far than the repository meant.
261
+ *
262
+ * Each one names the canonical form it converts to, because surfacing without a destination is just
263
+ * a list of files somebody already knows about.
264
+ */
265
+ type NonstandardProblem = "nonstandard-instructions" | "nonstandard-rule" | "nonstandard-command" | "nonstandard-skill" | "nonstandard-subagent";
266
+ type InstructionProblem = "no-instructions" | "instructions-missing" | "instructions-unbridged" | "instructions-unreadable";
267
+ /** Everything `doctor` can report against, across every section. */
268
+ type DoctorProblem = BridgeProblem | InstructionProblem | ConfigurationFault | McpProblem | NonstandardProblem;
269
+ /** Alias kept for callers that name the whole set rather than one section. */
270
+ type ConfigurationProblem = DoctorProblem;
271
+ /**
272
+ * What resolves one finding, for `doctor`'s own output. Two fields rather than one string, because
273
+ * the caller `doctor` is written for is an agent parsing TOON, and the question it has to answer is
274
+ * "can I execute this, or is this judgment I hand to a skill?" A single string leaves that
275
+ * answerable only by parsing English.
276
+ *
277
+ * The contract is exact, and it is what makes the field safe to act on blindly:
278
+ *
279
+ * - `command` non-empty — a shell invocation that, run verbatim, **completes** the repair.
280
+ * - `command` empty — no single invocation does; act on `instruction` and do not synthesize one.
281
+ *
282
+ * So a diagnostic worth running is not a `command`. `diverged-both` carries none even though
283
+ * `git diff --no-index …` is perfectly runnable: the diff shows what differs, it does not reconcile
284
+ * anything. That is what stops an agent executing every `command` it is handed from rebuilding a
285
+ * diverged bridge over whichever side holds the newer edit. A skill invocation is not a `command`
286
+ * either — nothing in a shell runs `/buddy-agent-harness:init`.
287
+ */
288
+ type RepairAction = {
289
+ /** A shell invocation that completes the repair, or empty when none does. */
290
+ command: string;
291
+ /** The imperative, in prose, always present and complete on its own. */
292
+ instruction: string;
293
+ };
294
+ type Repair = {
295
+ problem: DoctorProblem;
296
+ /** What `doctor` prints in the `findings` row for this problem. */
297
+ detail: string;
298
+ /**
299
+ * What fixes it, for `doctor`'s own output. `at` is where the finding is, in parts — the file
300
+ * alone for a bridge, the file, server, and field for an MCP finding; `cli` is how to invoke this
301
+ * tool. A repair that names only part of a locator reads it off `at` rather than splitting the
302
+ * rendered string, which no separator survives: a server may be named `io.github.foo`.
303
+ */
304
+ repair(at: Locator, cli: string): RepairAction;
305
+ /**
306
+ * What the shipped skill tells an agent to do instead. A repair that rebuilds a bridge delegates
307
+ * to the `init` skill rather than calling the `init` command, because rebuilding can move
308
+ * user-authored skills and that judgment is the `init` skill's, not `doctor`'s.
309
+ */
310
+ skillRepair(at: Locator): string;
311
+ };
312
+ export declare const bridgeRepairs: readonly Repair[];
313
+ export declare const instructionRepairs: readonly Repair[];
314
+ export declare const doctorRepairs: readonly Repair[];
315
+ export declare const doctorSkill: {
316
+ readonly name: "doctor";
317
+ readonly description: "Use this skill when a repository loads no project skills, when skills are missing after a clone, when a harness appears to be ignoring AGENTS.md, or when checking whether the agent configuration bridges into .claude/skills, CLAUDE.md, and the other harness files still resolve.";
318
+ };
319
+ /**
320
+ * The shipped `doctor` skill, rendered from the same table the command prints. `version` is the
321
+ * package version the skill ships with; it pins the `npx` invocation so the table and the CLI that
322
+ * produced it stay on the same breaking line.
323
+ *
324
+ * The finding tables are NOT here. They live in the reference pages below, and this file carries
325
+ * only what every reader needs: how to run it, how to read the report, and where to go next.
326
+ */
327
+ export declare function renderDoctorSkill(version: string): string;
328
+ //#endregion
120
329
  //#region src/diagnose-bridges/git-bridge-state.d.ts
121
330
  /** Which side of a diverged bridge moved since the last commit where the two agreed. */
122
331
  type DivergenceDirection = "bridge" | "canonical" | "both" | "unknown";
332
+ /** Whether a path is tracked, and if so whether its skip-worktree bit is still set. */
333
+ type TrackingState = "untracked" | "tracked" | "skip-worktree";
334
+ /**
335
+ * Reads git state for the two sides of a bridge. Every method degrades to "cannot tell" rather than
336
+ * throwing, so `doctor` still reports on a tarball, a worktree without git, or a repository with no
337
+ * commits.
338
+ */
339
+ declare class GitBridgeState {
340
+ private readonly root;
341
+ /** Path of `root` relative to the repository root, `''` at the top, `undefined` outside a repository. */
342
+ private readonly prefix;
343
+ constructor(root: string);
344
+ /**
345
+ * `init --copy` marks a bridge skip-worktree so a machine-local copy over a tracked symlink stops
346
+ * showing as a pending change. The bit is a hint that some checkouts clear, so it is verified
347
+ * rather than assumed.
348
+ */
349
+ trackingOf(path: string): TrackingState;
350
+ /**
351
+ * Whether a `.gitignore` rule matches this path. Asked of git rather than matched against the
352
+ * file, so a rule on a parent directory — the common way a bridge gets ignored — is caught.
353
+ * Outside a repository nothing is ignored, which is the same "cannot tell" degradation the rest
354
+ * of this class uses.
355
+ */
356
+ isIgnored(path: string): boolean;
357
+ /**
358
+ * The newest commits that touched any of these paths, newest first. Empty outside a repository,
359
+ * which is the same "cannot tell" degradation as everything else here.
360
+ *
361
+ * Bounded like `lastAgreedTree` is: a baseline that is not in the recent history of the two files
362
+ * is not a baseline anyone can act on, and walking a whole repository to prove its absence costs
363
+ * more than the answer is worth.
364
+ */
365
+ commitsTouching(paths: readonly string[]): string[];
366
+ /** One file's content at one commit, or `undefined` when it did not exist there. */
367
+ contentAt(commit: string, path: string): string | undefined;
368
+ /**
369
+ * Names the side that moved by finding the newest commit whose two trees agreed, then asking which
370
+ * of the working directories still matches it.
371
+ */
372
+ directionOf(bridge: string, canonical: string): DivergenceDirection;
373
+ /** The tree of the newest commit where both paths held identical content. */
374
+ private lastAgreedTree;
375
+ /**
376
+ * Compares a working directory against a git tree by object id. `hash-object` is used rather than
377
+ * hashing in-process so the comparison follows whatever object format the repository uses.
378
+ */
379
+ private matchesTree;
380
+ }
123
381
  //#endregion
124
382
  //#region src/diagnose-bridges/diagnose-bridges.d.ts
125
383
  type BridgeKind = "symlink" | "copy" | "file" | "none";
@@ -133,9 +391,14 @@ type BridgeReport = {
133
391
  };
134
392
  type BridgeFinding = {
135
393
  path: string;
394
+ /**
395
+ * Which problem this is. Emitted so a caller routes on a name rather than by matching `detail`
396
+ * prose, which would break the moment the wording is improved.
397
+ */
398
+ problem: DoctorProblem;
136
399
  detail: string;
137
- /** The command that repairs this finding, already carrying the bridge path. */
138
- repair: string;
400
+ /** What repairs this finding, already carrying the bridge path. Empty `command` means judgment. */
401
+ repair: RepairAction;
139
402
  };
140
403
  type DivergenceReport = {
141
404
  path: string;
@@ -162,84 +425,156 @@ type DiagnoseResult = {
162
425
  * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
163
426
  * moved, or repaired, so the caller decides what to run from the repair each finding carries.
164
427
  */
165
- declare function diagnoseBridges({ root, harnesses: preferred, cli }: DiagnoseOptions): DiagnoseResult;
428
+ export declare function diagnoseBridges({ root, harnesses: preferred, cli }: DiagnoseOptions): DiagnoseResult;
166
429
  //#endregion
167
- //#region src/diagnose-bridges/doctor.command.d.ts
168
- declare const doctorCommand: cli$1.Command;
169
- //#endregion
170
- //#region src/diagnose-bridges/doctor-guidance.d.ts
171
- /**
172
- * The one place `doctor`'s guidance is written. The command reads `detail` and `repair` to fill its
173
- * `findings` and `help` sections; `skills/doctor/SKILL.md` is generated from the same table by
174
- * `scripts/generate-skills.ts`, so the shipped skill cannot drift from what the command says.
175
- */
176
- /** Every way a skills bridge can fail, in the order `doctor` reports them. */
177
- type BridgeProblem = "no-canonical" | "missing" | "degraded" | "stale" | "diverged-bridge" | "diverged-canonical" | "diverged-both" | "diverged-unknown" | "unpinned-copy";
430
+ //#region src/diagnose-configuration/diagnose-configuration.d.ts
178
431
  /**
179
- * Every way an instruction bridge can fail. Separate from `BridgeProblem` because the two share no
180
- * repair: a skills bridge is rebuilt with `init` flags, and an instruction bridge is a file whose
181
- * content is the user's, so every repair here goes back to the `init` skill.
432
+ * The configuration half of `doctor`. Where `diagnose-bridges` asks whether a bridge still
433
+ * resolves, this asks whether the configuration around it is still *right* — a harness name that
434
+ * has been superseded, an instruction file that never reaches `AGENTS.md`, a bridge git-ignores.
435
+ *
436
+ * Everything here is present-and-wrong rather than missing. Configuration that is absent is
437
+ * `init`'s to create; configuration that is wrong is what the `repair` skill acts on, and this
438
+ * module is where it reads its work from. Detection has one home so the two cannot drift.
439
+ *
440
+ * Instruction bridges are deliberately not here: `diagnose-instructions` owns them, and every one
441
+ * of its repairs goes back to the `init` skill rather than to `repair`.
442
+ *
443
+ * Read-only, like every other part of `doctor`.
182
444
  */
183
- type InstructionProblem = "no-instructions" | "instructions-missing" | "instructions-unbridged" | "instructions-unreadable";
184
- /** Everything `doctor` can report against, across both sections. */
185
- type DoctorProblem = BridgeProblem | InstructionProblem;
186
- type Repair = {
187
- problem: DoctorProblem;
188
- /** What `doctor` prints in the `findings` row for this problem. */
445
+ type ConfigurationFinding = {
446
+ /** Repository-relative path the finding is about. */
447
+ path: string;
448
+ problem: ConfigurationProblem;
189
449
  detail: string;
450
+ /** What repairs it, already carrying the path. Every fault here is judgment, so `command` is empty. */
451
+ repair: RepairAction;
452
+ };
453
+ //#endregion
454
+ //#region src/diagnose-bridges/doctor.command.d.ts
455
+ type DoctorReport = {
456
+ bin: string;
457
+ bridges: DiagnoseResult["bridges"];
458
+ instructions: DiagnoseResult["instructions"];
459
+ divergence?: DiagnoseResult["divergence"];
190
460
  /**
191
- * The command that fixes it, for `doctor`'s own output. `path` is the repository-relative bridge
192
- * path; `cli` is how to invoke this tool.
461
+ * The repair is lifted out into `help`, so a finding row stays to the diagnosis itself. `problem`
462
+ * stays on the row: it is how a caller routes without parsing `detail` prose.
193
463
  */
194
- repair(path: string, cli: string): string;
464
+ findings: {
465
+ path: string;
466
+ problem: DoctorProblem;
467
+ detail: string;
468
+ }[] | string;
195
469
  /**
196
- * What the shipped skill tells an agent to do instead. A repair that rebuilds a bridge delegates
197
- * to the `init` skill rather than calling the `init` command, because rebuilding can move
198
- * user-authored skills and that judgment is the `init` skill's, not `doctor`'s.
470
+ * One entry per distinct repair. Two fields rather than a sentence, because the caller has to
471
+ * tell an executable repair from an instruction, and `RepairAction` is where that lives.
472
+ *
473
+ * Both keys are always emitted, `command` as an empty string when there is none. An optional key
474
+ * would drop the whole array out of TOON's tabular form into the nested list form — worse for
475
+ * exactly the consumer the default format exists for.
199
476
  */
200
- skillRepair(path: string): string;
477
+ help?: RepairAction[];
201
478
  };
202
- /** The skills bridges, reported in `bridges`. */
203
- declare const bridgeRepairs: readonly Repair[];
204
479
  /**
205
- * The instruction bridges, reported in `instructions`. Every repair is the `init` skill: these are
206
- * files a person wrote, or files carrying content beside the bridge, and deciding what to preserve
207
- * while restoring the bridge is judgment no flag carries. `repair` therefore names the skill in
208
- * both places rather than pretending a shell command exists.
480
+ * AXI §5: the healthy answer states the zero with context, so an agent does not re-run with other
481
+ * flags to confirm that an empty section really meant "nothing wrong".
209
482
  */
210
- declare const instructionRepairs: readonly Repair[];
211
- /** Both sections, in the order `doctor` reports them. */
212
- declare const doctorRepairs: readonly Repair[];
213
- declare const doctorSkill: {
214
- readonly name: "doctor";
215
- readonly description: "Use this skill when a repository loads no project skills, when skills are missing after a clone, when a harness appears to be ignoring AGENTS.md, or when checking whether the agent configuration bridges into .claude/skills, CLAUDE.md, and the other harness files still resolve.";
483
+ export declare function buildDoctorReport(bin: string, result: DiagnoseResult, configuration?: ConfigurationFinding[]): DoctorReport;
484
+ export declare const doctorCommand: cli$1.Command;
485
+ //#endregion
486
+ //#region src/diagnose-mcp/diagnose-mcp.d.ts
487
+ /**
488
+ * The MCP half of `doctor`: how a repository's **golden MCP server set** and the harness copies of
489
+ * it have drifted apart, and where a literal credential is sitting in either.
490
+ *
491
+ * The golden set is a file the user authors. That is what makes this capability possible at all:
492
+ * `init` reports MCP configuration rather than converting it because converting a config someone
493
+ * wrote for one harness into another's format has to invent values they never wrote, and `init`
494
+ * invents nothing. A set the user authored inverts the premise — a field they filled in is
495
+ * transcription. **No golden set means no drift diagnosis**, and nothing here invents anything
496
+ * either.
497
+ *
498
+ * Read-only, like the rest of `doctor`. Projecting the golden set forward and reconciling a
499
+ * target-side change back into it are writes, and they need an approval-gated home; this module is
500
+ * where that home reads its work from.
501
+ */
502
+ type DiagnoseMcpOptions = {
503
+ root: string;
504
+ git: GitBridgeState;
505
+ /** How to name this tool in the repair commands. */
506
+ cli: string;
216
507
  };
508
+ export declare function diagnoseMcp({ root, git, cli }: DiagnoseMcpOptions): ConfigurationFinding[];
509
+ //#endregion
510
+ //#region src/diagnose-mcp/mcp-model.d.ts
217
511
  /**
218
- * The shipped `doctor` skill, rendered from the same table the command prints. `version` is the
219
- * package version the skill ships with; it pins the `npx` invocation so the table and the CLI that
220
- * produced it stay on the same breaking line.
512
+ * The shape both sides of an MCP comparison are normalized into.
513
+ *
514
+ * No two MCP configuration files are ever byte-equal — the supported harnesses spread the same
515
+ * servers across six config keys and three serialization formats — so comparison cannot be a
516
+ * diff. Each side is parsed into this model and the models are compared.
517
+ *
518
+ * The field list is a **superset** of what the hosts accept, which is the whole premise of a
519
+ * golden set: `description`, `enabled`, and `timeout` are Goose's, `source` is Zed's, and a
520
+ * converter writing into either has to supply them. Supplying a value the user wrote is
521
+ * transcription; supplying one they did not is invention, and `init` invents nothing. See
522
+ * `.research/agentic-configuration-standards/` (E-MCP-05) for what each host makes up today.
221
523
  */
222
- declare function renderDoctorSkill(version: string): string;
524
+ type McpTransport = "stdio" | "http" | "sse";
525
+ type McpServer = {
526
+ transport?: McpTransport;
527
+ command?: string;
528
+ args?: readonly string[];
529
+ env?: Readonly<Record<string, string>>;
530
+ url?: string;
531
+ headers?: Readonly<Record<string, string>>;
532
+ description?: string;
533
+ enabled?: boolean;
534
+ timeout?: number;
535
+ source?: string;
536
+ };
537
+ /** Every field of the model, in the order a finding reports them. */
538
+ declare const mcpFields: readonly ["transport", "command", "args", "env", "url", "headers", "description", "enabled", "timeout", "source"];
539
+ type McpField = (typeof mcpFields)[number];
540
+ //#endregion
541
+ //#region src/diagnose-mcp/mcp-baseline.d.ts
542
+ /** Which side of a diverged server moved since the two last agreed. */
543
+ type McpDirection = "target" | "golden" | "both" | "unknown";
544
+ //#endregion
545
+ //#region src/diagnose-mcp/mcp-sources.d.ts
546
+ /** Where the golden set lives, and why it is namespaced rather than sitting at `.agents/mcp.json`. */
547
+ export declare const goldenSetPath = ".agents/buddy-agent-harness/mcp.toml";
223
548
  //#endregion
224
549
  //#region src/initialize-harnesses/init.command.d.ts
225
- declare const initCommand: cli$1.Command;
226
- declare const harnessCommand: cli$1.Command;
227
- declare function activate({ addCommand }: {
550
+ export declare const initCommand: cli$1.Command;
551
+ export declare const harnessCommand: cli$1.Command;
552
+ export declare function activate({ addCommand }: {
228
553
  addCommand(command: typeof harnessCommand): void;
229
554
  }): void;
230
555
  //#endregion
556
+ //#region src/skill-projection/skill-projection.d.ts
557
+ /**
558
+ * Which conflicting targets a run may replace. `true` is every one of them; a list names them
559
+ * individually, so a run invoked to fix one bridge cannot reach past it to another. `false` and an
560
+ * empty list both mean none, which is what makes a conflict stop the run.
561
+ */
562
+ type ForceSelection = boolean | readonly string[];
563
+ //#endregion
231
564
  //#region src/initialize-harnesses/initialize-harnesses.d.ts
232
565
  type InitializeOptions = {
233
566
  root: string;
234
567
  harnesses?: HarnessName[];
235
568
  copy?: boolean;
236
- force?: boolean;
569
+ force?: ForceSelection;
237
570
  };
238
571
  type InitializeResult = {
239
572
  root: string;
240
573
  harnesses: HarnessName[];
241
574
  native: HarnessName[];
242
575
  linked: HarnessName[];
576
+ /** Enabled harnesses left untouched because their target conflicts and `--force` did not name it. */
577
+ skipped: HarnessName[];
243
578
  /** Enabled harnesses whose name has been superseded, as `{ name, replacedBy }`. */
244
579
  deprecated: {
245
580
  name: HarnessName;
@@ -248,7 +583,7 @@ type InitializeResult = {
248
583
  skills: number;
249
584
  copied: boolean;
250
585
  };
251
- declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
586
+ export declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
252
587
  //#endregion
253
- export { type BridgeFinding, type BridgeKind, type BridgeProblem, type BridgeReport, type BridgeStatus, type DiagnoseOptions, type DiagnoseResult, type DivergenceDirection, type DivergenceReport, type DoctorProblem, type Harness, type HarnessName, type HarnessScope, type HarnessScopeName, type InitializeOptions, type InitializeResult, type InstructionBridge, type InstructionKind, type InstructionProblem, type InstructionReport, type InstructionStatus, type Repair, activate, bridgeRepairs, diagnoseBridges, diagnoseInstructions, doctorCommand, doctorRepairs, doctorSkill, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill };
588
+ export { type BridgeFinding, type BridgeKind, type BridgeProblem, type BridgeReport, type BridgeStatus, type ConfigurationFault, type ConfigurationProblem, type DiagnoseMcpOptions, type DiagnoseOptions, type DiagnoseResult, type DivergenceDirection, type DivergenceReport, type DoctorProblem, type DoctorReport, type Harness, type HarnessName, type HarnessScope, type HarnessScopeName, type InitializeOptions, type InitializeResult, type InstructionBridge, type InstructionKind, type InstructionProblem, type InstructionReport, type InstructionStatus, type McpConfig, type McpDirection, type McpField, type McpProblem, type McpServer, type McpTransport, type Repair, run };
254
589
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/harness-registry/instruction-bridge.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/initialize-harnesses/init.command.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;;;;;;;;;;;;;KAYY;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;KCzBS;;KAUA;;;;;;;;;;;;;;;;KAiBA;EACX;EACA;EACA,oBAAoB;;KAGT;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;cAuBD,0BAA0B;;;;KC5D3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;iBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;KCxGtC;;;KCGA;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;EACA;;EAEA;;KAGW;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;iBA8EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;cC9E/E,eAAe,MAAI;;;;;;;;;KCvCpB;;;;;;KAgBA;;KAOA,gBAAgB,gBAAgB;KAEhC;EACX,SAAS;;EAET;;;;;EAKA,OAAO,cAAc;;;;;;EAMrB,YAAY;;;cAsDA,wBAAwB;;;;;;;cAmExB,6BAA6B;;cA6B7B,wBAAwB;cAQxB;WACZ;WACA;;;;;;;iBAae,kBAAkB;;;cCpNrB,aAAa,MAAI;cA0DjB,gBAAgB,MAAI;iBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;KCnE1D;EACX;EACA,YAAY;EACZ;EACA;;KAEW;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;iBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;;;;;;;;;;;;;;KAYY;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;KCbS;;EAEX;;;;;;EAMA;;;;;EAKA;;;;;;EAMA;;;;;;;;;;;;;;;;;KCtBW;KAgBA;;;;;EAKX;;EAEA;EACA,MAAM;;;;KC/BK;;KAUA;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA;EACX;EACA;EACA,oBAAoB;EACpB,YAAY;EACZ,uBAAuB;;KAGZ;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAuBD,0BAA0B;;;;KCzE3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;wBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;;;;;;;;;;;;;;;;KC5FtC;EAAa;EAAc;;KAE3B;;EAEX;;EAEA;;EAEA;;EAEA,WAAW;;;;;;;;;;KCfA;;;;;;;;;;;KAqBA;;;;;;;;;KAUA;;;;;;;;;KAoBA;KAOA;;KAOA,gBAAgB,gBAAgB,qBAAqB,qBAAqB,aAAa;;KAGvF,uBAAuB;;;;;;;;;;;;;;;;;;KAmBvB;;EAEX;;EAEA;;KAGW;EACX,SAAS;;EAET;;;;;;;EAOA,OAAO,IAAI,SAAS,cAAc;;;;;;EAMlC,YAAY,IAAI;;qBAwKJ,wBAAwB;qBA4CxB,6BAA6B;qBAuM7B,wBAAwB;qBAOxB;WACZ;WACA;;;;;;;;;;wBAmCe,kBAAkB;;;;KC5jBtB;;KAGA;;;;;;cAwBC;mBAIA;;mBAFK;EAEjB,YAAY;;;;;;EASZ,WAAW,eAAe;;;;;;;EAa1B,UAAU;;;;;;;;;EAaV,gBAAgB;;EAMhB,UAAU,gBAAgB;;;;;EAS1B,YAAY,gBAAgB,oBAAoB;;UAYxC;;;;;UAmBA;;;;KC5GG;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;;;;;EAKA,SAAS;EACT;;EAEA,QAAQ;;KAGG;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;wBA2EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;;;;;;;;;;;;;;;KC1GhF;;EAEX;EACA,SAAS;EACT;;EAEA,QAAQ;;;;KCfG;EACX;EACA,SAAS;EACT,cAAc;EACd,aAAa;;;;;EAKb;IAAY;IAAc,SAAS;IAAe;;;;;;;;;;EASlD,OAAO;;;;;;wBAOQ,kBACf,aACA,QAAQ,gBACR,gBAAe,yBACb;qBAgCU,eAAe,MAAI;;;;;;;;;;;;;;;;;;KC7CpB;EACX;EACA,KAAK;;EAEL;;wBAgCe,cAAc,MAAM,KAAK,OAAO,qBAAqB;;;;;;;;;;;;;;;;KClDzD;KAEA;EACX,YAAY;EACZ;EACA;EACA,MAAM,SAAS;EACf;EACA,UAAU,SAAS;EACnB;EACA;EACA;EACA;;;cAIY;KAaD,mBAAmB;;;;KCnCnB;;;;qBCAC;;;qBCAA,aAAa,MAAI;qBAoDjB,gBAAgB,MAAI;wBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;;;;;;KCV1D;;;KCpDA;EACX;EACA,YAAY;EACZ;EACA,QAAQ;;KAEG;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER,SAAS;;EAET;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;wBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as doctorCommand, c as bridgeRepairs, d as instructionRepairs, f as renderDoctorSkill, i as initializeHarnesses, l as doctorRepairs, n as harnessCommand, o as diagnoseBridges, p as harnessRegistry, r as initCommand, s as diagnoseInstructions, t as activate, u as doctorSkill } from "./init.command-zbntulIq.mjs";
2
- export { activate, bridgeRepairs, diagnoseBridges, diagnoseInstructions, doctorCommand, doctorRepairs, doctorSkill, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill };
1
+ import { _ as harnessRegistry, a as initializeHarnesses, c as diagnoseBridges, d as goldenSetPath, f as bridgeRepairs, g as renderDoctorSkill, h as instructionRepairs, i as initCommand, l as diagnoseInstructions, m as doctorSkill, n as activate, o as buildDoctorReport, p as doctorRepairs, r as harnessCommand, s as doctorCommand, t as run, u as diagnoseMcp } from "./cli-4ekejn5W.mjs";
2
+ export { activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill, run };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "keywords": [
6
6
  "repobuddy",
@@ -46,15 +46,16 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@toon-format/toon": "^4.1.1",
49
- "clibuilder": "^10.0.0"
49
+ "clibuilder": "^10.0.0",
50
+ "smol-toml": "^1.8.0"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^24.12.4",
53
- "@vitest/coverage-v8": "^4.1.0",
54
- "tsdown": "^0.22.0",
54
+ "@vitest/coverage-v8": "^5.0.0",
55
+ "tsdown": "^0.23.0",
55
56
  "tsx": "^4.22.3",
56
57
  "typescript": "^7.0.0",
57
- "vitest": "^4.1.0"
58
+ "vitest": "^5.0.0"
58
59
  },
59
60
  "scripts": {
60
61
  "build": "tsdown",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "buddy-agent-harness",
4
- "version": "0.6.0",
4
+ "version": "0.8.0",
5
5
  "description": "Initialize standards-based agent configuration across agent harnesses",
6
6
  "author": {
7
7
  "name": "repobuddy"