buddy-agent-harness 1.0.1 → 1.2.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 (47) 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/CHANGELOG.md +44 -0
  5. package/dist/cli.mjs +2166 -702
  6. package/dist/index.d.mts +10 -3
  7. package/dist/index.d.mts.map +1 -1
  8. package/dist/index.mjs +597 -431
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/skill-scripts/doctor.mjs +74 -67
  11. package/dist/skill-scripts/init.mjs +74 -67
  12. package/dist/skill-scripts/mcp.mjs +74 -67
  13. package/llms.txt +8 -6
  14. package/package.json +5 -4
  15. package/plugin.json +1 -1
  16. package/skills/doctor-buddy-agent-harness/README.md +1 -1
  17. package/skills/doctor-buddy-agent-harness/SKILL.md +9 -2
  18. package/skills/doctor-buddy-agent-harness/references/worktrees.md +17 -0
  19. package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +74 -67
  20. package/skills/enhance/SKILL.md +18 -10
  21. package/skills/enhance/references/agent-worktrees.md +59 -0
  22. package/skills/enhance/references/list-identifiers.history.md +9 -1
  23. package/skills/enhance/references/list-identifiers.md +6 -9
  24. package/skills/enhance/references/references-as-links.history.md +9 -0
  25. package/skills/enhance/references/references-as-links.md +76 -0
  26. package/skills/enhance/scripts/doctor.mjs +74 -67
  27. package/skills/init-buddy-agent-harness/SKILL.md +8 -5
  28. package/skills/init-buddy-agent-harness/references/agent-worktrees.md +59 -0
  29. package/skills/init-buddy-agent-harness/scripts/doctor.mjs +74 -67
  30. package/skills/init-buddy-agent-harness/scripts/init.mjs +74 -67
  31. package/skills/repair/README.md +2 -2
  32. package/skills/repair/SKILL.md +3 -3
  33. package/skills/repair/scripts/doctor.mjs +74 -67
  34. package/skills/repair/scripts/mcp.mjs +74 -67
  35. package/src/cli.ts +13 -3
  36. package/src/command-output/command-output.ts +28 -5
  37. package/src/dep-plugins/dep-plugins.command.ts +4 -0
  38. package/src/diagnose-bridges/doctor-guidance.ts +66 -1
  39. package/src/diagnose-bridges/doctor.command.ts +69 -49
  40. package/src/diagnose-bridges/git-bridge-state.ts +5 -0
  41. package/src/diagnose-worktrees/diagnose-worktrees.ts +69 -0
  42. package/src/governance-overrides/governance.command.ts +13 -8
  43. package/src/index.ts +1 -0
  44. package/src/initialize-harnesses/init.command.ts +9 -3
  45. package/src/project-mcp/mcp.command.ts +15 -9
  46. package/src/reconcile-mcp/reconcile.command.ts +17 -11
  47. package/src/references/references.ts +1 -0
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { declaredDependencies, listReferences, listReferences as listReferences$1, packageDir, parseReferenceName, referenceLayers as referenceLayers$1, referenceNames, repoReferencesDir, resolveReference, searchReferences, skillsDirectories } from "cyber-agentkit";
2
2
  import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { cli, command, t } from "clibuilder";
4
- import { CliError, document, exitCodes, isCliError, printAnswer, result } from "clibuilder/axi";
4
+ import { CliError, document, empty, exitCodes, homeHeader, isCliError, printAnswer, ref, result } from "clibuilder/axi";
5
5
  import { homedir, platform } from "node:os";
6
6
  import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { parse, stringify } from "smol-toml";
@@ -10,15 +10,276 @@ import { fileURLToPath } from "node:url";
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { parseTOML } from "toml-eslint-parser";
12
12
  import { findNodeAtLocation, parseTree } from "jsonc-parser";
13
+ //#region src/harness-registry/harness-registry.ts
14
+ /**
15
+ * Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
16
+ * drop a projection it may need.
17
+ */
18
+ function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
19
+ const listed = directories ?? [];
20
+ const [first] = listed;
21
+ if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
22
+ return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
23
+ }
24
+ /**
25
+ * The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
26
+ * reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
27
+ * E-GEM-03, E-COPILOT-04, E-CUR-08).
28
+ */
29
+ const globalInstructions = ".agents/AGENTS.md";
30
+ /**
31
+ * Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
32
+ * (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
33
+ */
34
+ const harnessRegistry = [
35
+ {
36
+ name: "claude-code",
37
+ project: {
38
+ detect: ".claude",
39
+ ...skillsProjection("claude-code", "project"),
40
+ shadowedBy: [
41
+ "CLAUDE.md",
42
+ ".claude/CLAUDE.md",
43
+ "CLAUDE.local.md"
44
+ ],
45
+ mcpConfig: {
46
+ path: ".mcp.json",
47
+ key: "mcpServers",
48
+ format: "json",
49
+ dialect: "claude-code"
50
+ },
51
+ nonstandard: [
52
+ {
53
+ path: ".claude/commands",
54
+ shape: "directory",
55
+ kind: "command"
56
+ },
57
+ {
58
+ path: ".claude/rules",
59
+ shape: "directory",
60
+ kind: "rule"
61
+ },
62
+ {
63
+ path: ".claude/agents",
64
+ shape: "directory",
65
+ kind: "subagent"
66
+ }
67
+ ]
68
+ },
69
+ user: {
70
+ detect: ".claude",
71
+ ...skillsProjection("claude-code", "user"),
72
+ relocatedBy: "CLAUDE_CONFIG_DIR",
73
+ emptyRelocatesToStart: true,
74
+ instructionBridge: {
75
+ kind: "import",
76
+ path: ".claude/CLAUDE.md",
77
+ line: `@~/${globalInstructions}`
78
+ }
79
+ }
80
+ },
81
+ {
82
+ name: "cursor",
83
+ project: {
84
+ detect: ".cursor",
85
+ ...skillsProjection("cursor", "project"),
86
+ mcpConfig: {
87
+ path: ".cursor/mcp.json",
88
+ key: "mcpServers",
89
+ format: "json",
90
+ dialect: "cursor"
91
+ },
92
+ nonstandard: [
93
+ {
94
+ path: ".cursorrules",
95
+ shape: "file",
96
+ kind: "instructions"
97
+ },
98
+ {
99
+ path: ".cursor/rules",
100
+ shape: "directory",
101
+ kind: "rule"
102
+ },
103
+ {
104
+ path: ".cursor/commands",
105
+ shape: "directory",
106
+ kind: "command"
107
+ },
108
+ {
109
+ path: ".cursor/skills",
110
+ shape: "directory",
111
+ kind: "skill"
112
+ }
113
+ ]
114
+ },
115
+ user: {
116
+ detect: ".cursor",
117
+ ...skillsProjection("cursor", "user")
118
+ }
119
+ },
120
+ {
121
+ name: "codex",
122
+ project: {
123
+ detect: ".codex",
124
+ ...skillsProjection("codex", "project"),
125
+ mcpConfig: {
126
+ path: ".codex/config.toml",
127
+ key: "mcp_servers",
128
+ format: "toml",
129
+ dialect: "codex",
130
+ shared: true
131
+ },
132
+ nonstandard: [{
133
+ path: ".codex/skills",
134
+ shape: "directory",
135
+ kind: "skill"
136
+ }]
137
+ },
138
+ user: {
139
+ detect: ".codex",
140
+ ...skillsProjection("codex", "user"),
141
+ instructionBridge: {
142
+ kind: "symlink",
143
+ path: ".codex/AGENTS.md"
144
+ },
145
+ shadowedBy: [".codex/AGENTS.override.md"],
146
+ relocatedBy: "CODEX_HOME"
147
+ }
148
+ },
149
+ {
150
+ name: "copilot-cli",
151
+ project: {
152
+ detect: ".github/skills",
153
+ ...skillsProjection("copilot-cli", "project"),
154
+ nonstandard: [
155
+ {
156
+ path: ".github/copilot-instructions.md",
157
+ shape: "file",
158
+ kind: "instructions"
159
+ },
160
+ {
161
+ path: ".github/instructions",
162
+ shape: "directory",
163
+ kind: "instructions"
164
+ },
165
+ {
166
+ path: ".github/skills",
167
+ shape: "directory",
168
+ kind: "skill"
169
+ }
170
+ ]
171
+ },
172
+ user: {
173
+ detect: ".copilot",
174
+ ...skillsProjection("copilot-cli", "user"),
175
+ relocatedBy: "COPILOT_HOME",
176
+ instructionBridge: {
177
+ kind: "symlink",
178
+ path: ".copilot/copilot-instructions.md"
179
+ }
180
+ }
181
+ },
182
+ {
183
+ name: "gemini-cli",
184
+ project: {
185
+ detect: ".gemini",
186
+ ...skillsProjection("gemini-cli", "project"),
187
+ instructionBridge: {
188
+ kind: "settings-entry",
189
+ path: ".gemini/settings.json",
190
+ key: "context.fileName"
191
+ },
192
+ mcpConfig: {
193
+ path: ".gemini/settings.json",
194
+ key: "mcpServers",
195
+ format: "json",
196
+ dialect: "gemini-cli",
197
+ shared: true
198
+ },
199
+ nonstandard: [{
200
+ path: "GEMINI.md",
201
+ shape: "file",
202
+ kind: "instructions"
203
+ }, {
204
+ path: ".gemini/skills",
205
+ shape: "directory",
206
+ kind: "skill"
207
+ }]
208
+ },
209
+ user: {
210
+ detect: ".gemini",
211
+ ...skillsProjection("gemini-cli", "user"),
212
+ instructionBridge: {
213
+ kind: "symlink",
214
+ path: ".gemini/GEMINI.md"
215
+ }
216
+ }
217
+ },
218
+ {
219
+ name: "devin-desktop",
220
+ project: { detect: ".devin" }
221
+ },
222
+ {
223
+ name: "windsurf",
224
+ project: {
225
+ detect: ".windsurf",
226
+ skillsDirectory: ".windsurf/skills",
227
+ nonstandard: [{
228
+ path: ".windsurfrules",
229
+ shape: "file",
230
+ kind: "instructions"
231
+ }, {
232
+ path: ".windsurf/rules",
233
+ shape: "directory",
234
+ kind: "rule"
235
+ }]
236
+ },
237
+ deprecated: "devin-desktop"
238
+ }
239
+ ];
240
+ const harnessNames = new Set(harnessRegistry.map((harness) => harness.name));
241
+ function isHarnessName(value) {
242
+ return harnessNames.has(value);
243
+ }
244
+ /**
245
+ * An unknown name throws rather than being dropped — silently falling back to the defaults would
246
+ * misreport what was diagnosed.
247
+ */
248
+ function parseHarnesses(value) {
249
+ const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
250
+ const unsupported = requested.filter((name) => !isHarnessName(name));
251
+ if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
252
+ return requested.filter(isHarnessName);
253
+ }
254
+ /** Enabled whether or not the repository already contains their directories. */
255
+ const defaultHarnesses = ["claude-code", "cursor"];
256
+ function directoryExists(path) {
257
+ try {
258
+ return lstatSync(path).isDirectory();
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+ /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
264
+ function selectHarnesses(root, preferred) {
265
+ const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
266
+ return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
267
+ }
268
+ //#endregion
13
269
  //#region src/command-output/command-output.ts
14
270
  /**
15
- * Reports a failure on stderr, not stdout — stdout carries what an agent parses — and fails the cli
16
- * with the exit code. A `CliError` the cli already reports is rethrown untouched.
271
+ * AXI §6: a failure is a coded error on stdout, in the caller's format, with a `help` line saying
272
+ * what to do next. A `CliError` the command already built is rethrown untouched; anything else
273
+ * becomes `command-failed`, exit code 1.
17
274
  */
18
- function fail(error, fallback) {
275
+ function fail(error, fallback, help) {
19
276
  if (isCliError(error)) throw error;
20
- process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
21
- throw CliError.reported(exitCodes.error);
277
+ throw new CliError(error instanceof Error ? error.message : fallback, {
278
+ code: "command-failed",
279
+ exitCode: exitCodes.error,
280
+ ...help ? { help } : {},
281
+ cause: error
282
+ });
22
283
  }
23
284
  /**
24
285
  * An answer that comes with a failing exit code. The cli prints only an answer a command returns, and
@@ -39,6 +300,22 @@ function collapseHome(home, path) {
39
300
  function displayBinPath(home, executable) {
40
301
  return executable ? collapseHome(home, executable) : "buddy-agent-harness";
41
302
  }
303
+ /**
304
+ * `parseHarnesses` for a command's `--harness`: an unsupported name is a usage error, exit code 2.
305
+ * The registry stays free of clibuilder, which the library entry treats as an optional peer.
306
+ */
307
+ function parseHarnessOption(value) {
308
+ try {
309
+ return parseHarnesses(value);
310
+ } catch (error) {
311
+ throw new CliError(error.message, {
312
+ code: "unsupported-harness",
313
+ exitCode: exitCodes.usage,
314
+ help: "Pass --harness as a comma-separated list of the supported names.",
315
+ cause: error
316
+ });
317
+ }
318
+ }
42
319
  //#endregion
43
320
  //#region src/dep-plugins/dep-plugins.ts
44
321
  /** Generic marketplace names: a second registration under one of these replaces the first, silently. */
@@ -94,7 +371,7 @@ function serializeCatalog(catalog) {
94
371
  }
95
372
  //#endregion
96
373
  //#region src/dep-plugins/resolve.ts
97
- function readJson$1(path) {
374
+ function readJson$2(path) {
98
375
  try {
99
376
  return JSON.parse(readFileSync(path, "utf8"));
100
377
  } catch {
@@ -109,7 +386,7 @@ function readDepPlugin(pkg, fromManifest) {
109
386
  package: pkg,
110
387
  reason: "unresolved"
111
388
  };
112
- const manifest = readJson$1(join(dir, "plugin.json"));
389
+ const manifest = readJson$2(join(dir, "plugin.json"));
113
390
  if (!manifest || typeof manifest !== "object") return {
114
391
  ok: false,
115
392
  package: pkg,
@@ -122,7 +399,7 @@ function readDepPlugin(pkg, fromManifest) {
122
399
  package: pkg,
123
400
  reason: "unnamed"
124
401
  };
125
- const installed = readJson$1(join(dir, "package.json"));
402
+ const installed = readJson$2(join(dir, "package.json"));
126
403
  const version = typeof installed?.version === "string" ? installed.version : void 0;
127
404
  if (!version) return {
128
405
  ok: false,
@@ -215,7 +492,7 @@ function writeCatalog(root, catalog) {
215
492
  //#endregion
216
493
  //#region src/dep-plugins/harness-plugins.ts
217
494
  const COPILOT_LIMITATION = "Copilot CLI rejects an npm marketplace source, so it cannot install from this catalog. It needs a path-sourced catalog, which cannot reach node_modules from a subdirectory.";
218
- function readJson(path) {
495
+ function readJson$1(path) {
219
496
  try {
220
497
  return JSON.parse(readFileSync(path, "utf8"));
221
498
  } catch {
@@ -233,7 +510,7 @@ function claudeConfigDir(home, env = process.env) {
233
510
  return env["CLAUDE_CONFIG_DIR"] || join(home, ".claude");
234
511
  }
235
512
  function readClaudeState(configDir) {
236
- const installedFile = readJson(join(configDir, "plugins", "installed_plugins.json"));
513
+ const installedFile = readJson$1(join(configDir, "plugins", "installed_plugins.json"));
237
514
  const installed = [];
238
515
  for (const [id, entries] of Object.entries(installedFile?.plugins ?? {})) {
239
516
  if (!Array.isArray(entries)) continue;
@@ -256,7 +533,7 @@ function readClaudeState(configDir) {
256
533
  });
257
534
  }
258
535
  }
259
- const knownFile = readJson(join(configDir, "plugins", "known_marketplaces.json"));
536
+ const knownFile = readJson$1(join(configDir, "plugins", "known_marketplaces.json"));
260
537
  const marketplaces = [];
261
538
  for (const [name, value] of Object.entries(knownFile ?? {})) marketplaces.push({
262
539
  name,
@@ -394,350 +671,101 @@ function commandFor(runtime, action, marketplace) {
394
671
  return refresh ? `${refresh} && ${update}` : update;
395
672
  }
396
673
  case "uninstall": return runtime.uninstall(action.subject);
397
- }
398
- }
399
- function notesFor(derived) {
400
- const notes = [];
401
- if (derived.pnp) notes.push("Yarn Plug’n’Play is active: dependencies live inside zip archives, so no harness can install from them. Set nodeLinker: node-modules, or unplug the plugin dependencies.");
402
- if (derived.genericName) notes.push(`Marketplace name "${derived.catalog.name}" is generic. Registering a second marketplace under a name already in use replaces the first silently, and the install that follows reports "already installed" — the repository then runs the other one's versions.`);
403
- for (const { package: pkg } of derived.unresolved) notes.push(`Declared dependency "${pkg}" is not installed; install dependencies before generating.`);
404
- return notes;
405
- }
406
- const depPluginsCommand = command({
407
- name: "dep-plugins",
408
- description: "Derive the marketplace catalog for plugins shipped by this repository’s dependencies, so a harness can install them. Writes the catalog; installing is the harness’s own job.",
409
- options: {
410
- root: {
411
- description: "Repository directory. Defaults to the current directory.",
412
- type: t.string().optional()
413
- },
414
- check: {
415
- description: "Report whether the catalog is current without writing it. For CI and postinstall.",
416
- type: t.boolean().optional()
417
- }
418
- },
419
- run(args) {
420
- try {
421
- const root = args.root ?? process.cwd();
422
- const derived = deriveCatalog(root);
423
- const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
424
- const home = homedir();
425
- const notes = notesFor(derived);
426
- const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
427
- const harness = [];
428
- const actions = [];
429
- for (const runtime of detected) {
430
- const state = runtime.state(home);
431
- const delta = reconcile({
432
- catalog: derived.catalog,
433
- catalogDir: CATALOG_DIR,
434
- root: resolve(root),
435
- installed: state.installed,
436
- marketplaces: state.marketplaces,
437
- scoped: runtime.scoped
438
- });
439
- if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
440
- harness.push({
441
- runtime: runtime.name,
442
- status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
443
- });
444
- for (const action of delta.actions) actions.push({
445
- runtime: runtime.name,
446
- do: commandFor(runtime, action, derived.catalog.name),
447
- subject: action.subject,
448
- why: action.detail
449
- });
450
- }
451
- if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
452
- const report = {
453
- catalog: CATALOG_PATH,
454
- marketplace: derived.catalog.name,
455
- outcome,
456
- plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
457
- name: plugin,
458
- package: pkg,
459
- version
460
- })) : "0 dependencies ship a plugin — nothing to install",
461
- harness: harness.length ? harness : "no supported harness detected on this machine",
462
- actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
463
- ...notes.length ? { notes } : {}
464
- };
465
- const answer = result(report);
466
- return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
467
- } catch (error) {
468
- return fail(error, "Catalog generation failed.");
469
- }
470
- }
471
- });
472
- //#endregion
473
- //#region src/diagnose-bridges/directory-files.ts
474
- /** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
475
- function filesUnder(directory) {
476
- try {
477
- return readdirSync(directory, {
478
- recursive: true,
479
- withFileTypes: true
480
- }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
481
- } catch {
482
- return [];
483
- }
484
- }
485
- //#endregion
486
- //#region src/harness-registry/harness-registry.ts
487
- /**
488
- * Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
489
- * drop a projection it may need.
490
- */
491
- function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
492
- const listed = directories ?? [];
493
- const [first] = listed;
494
- if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
495
- return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
496
- }
497
- /**
498
- * The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
499
- * reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
500
- * E-GEM-03, E-COPILOT-04, E-CUR-08).
501
- */
502
- const globalInstructions = ".agents/AGENTS.md";
503
- /**
504
- * Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
505
- * (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
506
- */
507
- const harnessRegistry = [
508
- {
509
- name: "claude-code",
510
- project: {
511
- detect: ".claude",
512
- ...skillsProjection("claude-code", "project"),
513
- shadowedBy: [
514
- "CLAUDE.md",
515
- ".claude/CLAUDE.md",
516
- "CLAUDE.local.md"
517
- ],
518
- mcpConfig: {
519
- path: ".mcp.json",
520
- key: "mcpServers",
521
- format: "json",
522
- dialect: "claude-code"
523
- },
524
- nonstandard: [
525
- {
526
- path: ".claude/commands",
527
- shape: "directory",
528
- kind: "command"
529
- },
530
- {
531
- path: ".claude/rules",
532
- shape: "directory",
533
- kind: "rule"
534
- },
535
- {
536
- path: ".claude/agents",
537
- shape: "directory",
538
- kind: "subagent"
539
- }
540
- ]
541
- },
542
- user: {
543
- detect: ".claude",
544
- ...skillsProjection("claude-code", "user"),
545
- relocatedBy: "CLAUDE_CONFIG_DIR",
546
- emptyRelocatesToStart: true,
547
- instructionBridge: {
548
- kind: "import",
549
- path: ".claude/CLAUDE.md",
550
- line: `@~/${globalInstructions}`
551
- }
552
- }
553
- },
554
- {
555
- name: "cursor",
556
- project: {
557
- detect: ".cursor",
558
- ...skillsProjection("cursor", "project"),
559
- mcpConfig: {
560
- path: ".cursor/mcp.json",
561
- key: "mcpServers",
562
- format: "json",
563
- dialect: "cursor"
564
- },
565
- nonstandard: [
566
- {
567
- path: ".cursorrules",
568
- shape: "file",
569
- kind: "instructions"
570
- },
571
- {
572
- path: ".cursor/rules",
573
- shape: "directory",
574
- kind: "rule"
575
- },
576
- {
577
- path: ".cursor/commands",
578
- shape: "directory",
579
- kind: "command"
580
- },
581
- {
582
- path: ".cursor/skills",
583
- shape: "directory",
584
- kind: "skill"
585
- }
586
- ]
587
- },
588
- user: {
589
- detect: ".cursor",
590
- ...skillsProjection("cursor", "user")
591
- }
592
- },
593
- {
594
- name: "codex",
595
- project: {
596
- detect: ".codex",
597
- ...skillsProjection("codex", "project"),
598
- mcpConfig: {
599
- path: ".codex/config.toml",
600
- key: "mcp_servers",
601
- format: "toml",
602
- dialect: "codex",
603
- shared: true
604
- },
605
- nonstandard: [{
606
- path: ".codex/skills",
607
- shape: "directory",
608
- kind: "skill"
609
- }]
610
- },
611
- user: {
612
- detect: ".codex",
613
- ...skillsProjection("codex", "user"),
614
- instructionBridge: {
615
- kind: "symlink",
616
- path: ".codex/AGENTS.md"
617
- },
618
- shadowedBy: [".codex/AGENTS.override.md"],
619
- relocatedBy: "CODEX_HOME"
620
- }
621
- },
622
- {
623
- name: "copilot-cli",
624
- project: {
625
- detect: ".github/skills",
626
- ...skillsProjection("copilot-cli", "project"),
627
- nonstandard: [
628
- {
629
- path: ".github/copilot-instructions.md",
630
- shape: "file",
631
- kind: "instructions"
632
- },
633
- {
634
- path: ".github/instructions",
635
- shape: "directory",
636
- kind: "instructions"
637
- },
638
- {
639
- path: ".github/skills",
640
- shape: "directory",
641
- kind: "skill"
642
- }
643
- ]
674
+ }
675
+ }
676
+ function notesFor(derived) {
677
+ const notes = [];
678
+ if (derived.pnp) notes.push("Yarn Plug’n’Play is active: dependencies live inside zip archives, so no harness can install from them. Set nodeLinker: node-modules, or unplug the plugin dependencies.");
679
+ if (derived.genericName) notes.push(`Marketplace name "${derived.catalog.name}" is generic. Registering a second marketplace under a name already in use replaces the first silently, and the install that follows reports "already installed" — the repository then runs the other one's versions.`);
680
+ for (const { package: pkg } of derived.unresolved) notes.push(`Declared dependency "${pkg}" is not installed; install dependencies before generating.`);
681
+ return notes;
682
+ }
683
+ const depPluginsCommand = command({
684
+ name: "dep-plugins",
685
+ description: "Derive the marketplace catalog for plugins shipped by this repository’s dependencies, so a harness can install them. Writes the catalog; installing is the harness’s own job.",
686
+ options: {
687
+ root: {
688
+ description: "Repository directory. Defaults to the current directory.",
689
+ type: t.string().optional()
644
690
  },
645
- user: {
646
- detect: ".copilot",
647
- ...skillsProjection("copilot-cli", "user"),
648
- relocatedBy: "COPILOT_HOME",
649
- instructionBridge: {
650
- kind: "symlink",
651
- path: ".copilot/copilot-instructions.md"
652
- }
691
+ check: {
692
+ description: "Report whether the catalog is current without writing it. For CI and postinstall.",
693
+ type: t.boolean().optional()
653
694
  }
654
695
  },
655
- {
656
- name: "gemini-cli",
657
- project: {
658
- detect: ".gemini",
659
- ...skillsProjection("gemini-cli", "project"),
660
- instructionBridge: {
661
- kind: "settings-entry",
662
- path: ".gemini/settings.json",
663
- key: "context.fileName"
664
- },
665
- mcpConfig: {
666
- path: ".gemini/settings.json",
667
- key: "mcpServers",
668
- format: "json",
669
- dialect: "gemini-cli",
670
- shared: true
671
- },
672
- nonstandard: [{
673
- path: "GEMINI.md",
674
- shape: "file",
675
- kind: "instructions"
676
- }, {
677
- path: ".gemini/skills",
678
- shape: "directory",
679
- kind: "skill"
680
- }]
681
- },
682
- user: {
683
- detect: ".gemini",
684
- ...skillsProjection("gemini-cli", "user"),
685
- instructionBridge: {
686
- kind: "symlink",
687
- path: ".gemini/GEMINI.md"
696
+ examples: [{
697
+ command: "buddy-agent-harness dep-plugins",
698
+ description: "Write the marketplace catalog for dependency plugins"
699
+ }, {
700
+ command: "buddy-agent-harness dep-plugins --check",
701
+ description: "Fail when the catalog is stale (CI)"
702
+ }],
703
+ run(args) {
704
+ try {
705
+ const root = args.root ?? process.cwd();
706
+ const derived = deriveCatalog(root);
707
+ const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
708
+ const home = homedir();
709
+ const notes = notesFor(derived);
710
+ const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
711
+ const harness = [];
712
+ const actions = [];
713
+ for (const runtime of detected) {
714
+ const state = runtime.state(home);
715
+ const delta = reconcile({
716
+ catalog: derived.catalog,
717
+ catalogDir: CATALOG_DIR,
718
+ root: resolve(root),
719
+ installed: state.installed,
720
+ marketplaces: state.marketplaces,
721
+ scoped: runtime.scoped
722
+ });
723
+ if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
724
+ harness.push({
725
+ runtime: runtime.name,
726
+ status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
727
+ });
728
+ for (const action of delta.actions) actions.push({
729
+ runtime: runtime.name,
730
+ do: commandFor(runtime, action, derived.catalog.name),
731
+ subject: action.subject,
732
+ why: action.detail
733
+ });
688
734
  }
735
+ if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
736
+ const report = {
737
+ catalog: CATALOG_PATH,
738
+ marketplace: derived.catalog.name,
739
+ outcome,
740
+ plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
741
+ name: plugin,
742
+ package: pkg,
743
+ version
744
+ })) : "0 dependencies ship a plugin — nothing to install",
745
+ harness: harness.length ? harness : "no supported harness detected on this machine",
746
+ actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
747
+ ...notes.length ? { notes } : {}
748
+ };
749
+ const answer = result(report);
750
+ return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
751
+ } catch (error) {
752
+ return fail(error, "Catalog generation failed.");
689
753
  }
690
- },
691
- {
692
- name: "devin-desktop",
693
- project: { detect: ".devin" }
694
- },
695
- {
696
- name: "windsurf",
697
- project: {
698
- detect: ".windsurf",
699
- skillsDirectory: ".windsurf/skills",
700
- nonstandard: [{
701
- path: ".windsurfrules",
702
- shape: "file",
703
- kind: "instructions"
704
- }, {
705
- path: ".windsurf/rules",
706
- shape: "directory",
707
- kind: "rule"
708
- }]
709
- },
710
- deprecated: "devin-desktop"
711
754
  }
712
- ];
713
- const harnessNames = new Set(harnessRegistry.map((harness) => harness.name));
714
- function isHarnessName(value) {
715
- return harnessNames.has(value);
716
- }
717
- /**
718
- * An unknown name throws rather than being dropped — silently falling back to the defaults would
719
- * misreport what was diagnosed.
720
- */
721
- function parseHarnesses(value) {
722
- const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
723
- const unsupported = requested.filter((name) => !isHarnessName(name));
724
- if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
725
- return requested.filter(isHarnessName);
726
- }
727
- /** Enabled whether or not the repository already contains their directories. */
728
- const defaultHarnesses = ["claude-code", "cursor"];
729
- function directoryExists(path) {
755
+ });
756
+ //#endregion
757
+ //#region src/diagnose-bridges/directory-files.ts
758
+ /** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
759
+ function filesUnder(directory) {
730
760
  try {
731
- return lstatSync(path).isDirectory();
761
+ return readdirSync(directory, {
762
+ recursive: true,
763
+ withFileTypes: true
764
+ }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
732
765
  } catch {
733
- return false;
766
+ return [];
734
767
  }
735
768
  }
736
- /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
737
- function selectHarnesses(root, preferred) {
738
- const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
739
- return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
740
- }
741
769
  //#endregion
742
770
  //#region src/diagnose-bridges/locator.ts
743
771
  /** The locator as one string: the finding's `path`, and what a repair names. */
@@ -776,6 +804,7 @@ const initSkillInvocation = `/buddy-agent-harness:init-buddy-agent-harness`;
776
804
  */
777
805
  const pnpmSkillRef = (invocation) => `\`${invocation}\` (pnpm install: \`pnpm-${commandInvocation}-${invocation.slice(invocation.indexOf(":") + 1)}\`)`;
778
806
  const initSkillRef = pnpmSkillRef(initSkillInvocation);
807
+ const enhanceSkillRef = pnpmSkillRef("/buddy-agent-harness:enhance");
779
808
  const repairSkillRef = pnpmSkillRef(repairSkillInvocation);
780
809
  /** Where the user authors the golden MCP server set, named in every MCP repair that points at it. */
781
810
  const goldenSet = ".agents/buddy-agent-harness/mcp.toml";
@@ -1141,6 +1170,35 @@ const nonstandardTable = {
1141
1170
  }
1142
1171
  };
1143
1172
  repairsOf(nonstandardTable);
1173
+ /** Who offers every worktree repair, named the same way in each row. */
1174
+ const worktreeOffer = `${initSkillRef} offers it, or ${enhanceSkillRef} where the repository already has an AGENTS.md`;
1175
+ const worktreeTable = {
1176
+ "worktrees-untracked": {
1177
+ detail: ".claude/worktrees/ is not git-ignored, so every agent worktree Claude Code creates there shows up as untracked in git status",
1178
+ repair: ({ file }) => ({
1179
+ command: "",
1180
+ instruction: `add \`.claude/worktrees/\` to ${file} — ${worktreeOffer}`
1181
+ }),
1182
+ skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the .gitignore line`
1183
+ },
1184
+ "worktrees-scanned": {
1185
+ detail: "this config does not exclude .claude/worktrees, so the tool walks into each agent worktree, a whole checkout with its own config, and Biome stops on the nested root configuration",
1186
+ repair: ({ file }) => ({
1187
+ command: "",
1188
+ instruction: `add \`"!**/.claude/worktrees"\` to \`files.includes\` in ${file} — ${worktreeOffer}`
1189
+ }),
1190
+ skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the exclusion`
1191
+ },
1192
+ "worktree-base-unset": {
1193
+ detail: "worktree.baseRef is unset, so agent worktrees branch from the default branch rather than the current HEAD — work on a feature branch starts without its commits",
1194
+ repair: ({ file }) => ({
1195
+ command: "",
1196
+ instruction: `decide whether agent worktrees should branch from HEAD, and if so set \`"worktree": { "baseRef": "head" }\` in ${file} — ${worktreeOffer}`
1197
+ }),
1198
+ skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which explains the trade-off and offers the setting`
1199
+ }
1200
+ };
1201
+ repairsOf(worktreeTable);
1144
1202
  /**
1145
1203
  * Annotated over the whole union rather than inferred, so an added problem fails to compile until
1146
1204
  * its section carries it.
@@ -1151,7 +1209,8 @@ const doctorTable = {
1151
1209
  ...globalInstructionTable,
1152
1210
  ...configurationTable,
1153
1211
  ...mcpTable,
1154
- ...nonstandardTable
1212
+ ...nonstandardTable,
1213
+ ...worktreeTable
1155
1214
  };
1156
1215
  const doctorRepairs = repairsOf(doctorTable);
1157
1216
  /** The repair for one problem. Total by construction: the table is keyed by the union. */
@@ -1163,7 +1222,7 @@ function repairFor(problem) {
1163
1222
  }
1164
1223
  const doctorSkill = {
1165
1224
  name: "doctor-buddy-agent-harness",
1166
- 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 and the other harness files still resolve."
1225
+ description: "Checks whether a repository's agent configuration bridges into .claude/skills and the other harness files still resolve — when a repository loads no project skills, when skills are missing after a clone, or when a harness appears to be ignoring AGENTS.md."
1167
1226
  };
1168
1227
  /** Written into the generated skill so a reader knows not to edit it in place. */
1169
1228
  const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand. -->";
@@ -1230,6 +1289,7 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
1230
1289
  | \`references/configuration.md\` | a finding about the configuration around the bridges rather than a bridge |
1231
1290
  | \`references/mcp.md\` | any finding whose path is an MCP locator — **always** before acting on a credential finding |
1232
1291
  | \`references/nonstandard.md\` | a finding about configuration only one harness can read |
1292
+ | \`references/worktrees.md\` | a finding about the agent worktrees Claude Code creates under \`.claude/worktrees/\` |
1233
1293
  | \`references/harnesses/<name>.md\` | the paths and files one named harness uses |
1234
1294
 
1235
1295
  ## Rules
@@ -1240,6 +1300,12 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
1240
1300
  - Never repeat a value from a file an \`mcp-literal-secret\` or \`mcp-committed-secret\` finding points at. The report withheld it on purpose, and quoting it back puts it in the transcript anyway.
1241
1301
  - Never write outside the repository. A \`globalInstructions\` repair is a step for the user to take in their own home directory; hand it over exactly as reported.
1242
1302
  - Write instructions in \`AGENTS.md\`, never in \`CLAUDE.md\`. Content written there reaches one harness, drifts from the canonical file, and — because Claude Code prefers it — takes \`AGENTS.md\` out of that harness's context entirely.
1303
+
1304
+ ## Done when
1305
+
1306
+ - **C1.** The reply names each problem the report found and its cause, or, when the report found none, says the configuration is healthy and calls for no change.
1307
+ - **C2.** No bridge, skill, or instruction file changed: no \`init\` ran, and nothing was written to \`CLAUDE.md\`.
1308
+ - **C3.** The reply repeats no value that a secret finding withheld.
1243
1309
  `;
1244
1310
  }
1245
1311
  //#endregion
@@ -2052,6 +2118,43 @@ function diagnoseNonstandard({ root, cli }) {
2052
2118
  }
2053
2119
  return findings.sort((left, right) => left.path.localeCompare(right.path));
2054
2120
  }
2121
+ const biomeConfigs = ["biome.json", "biome.jsonc"];
2122
+ const claudeSettings = ".claude/settings.json";
2123
+ /** `undefined` when the file is missing or does not parse; either way Claude Code reads no base from it. */
2124
+ function readJson(path) {
2125
+ try {
2126
+ return JSON.parse(readFileSync(path, "utf8"));
2127
+ } catch {
2128
+ return;
2129
+ }
2130
+ }
2131
+ /**
2132
+ * Checked only where `.claude/` exists, the sign the repository is worked in with Claude Code.
2133
+ * Biome is the one tool checked: it is the confirmed failure, and the others are offered by the
2134
+ * skills without a finding.
2135
+ */
2136
+ function diagnoseWorktrees({ root, git, cli }) {
2137
+ if (!existsSync(join(root, ".claude"))) return [];
2138
+ const findings = [];
2139
+ const add = (path, problem) => {
2140
+ const { detail, repair } = repairFor(problem);
2141
+ findings.push({
2142
+ path,
2143
+ problem,
2144
+ detail,
2145
+ repair: repair({ file: path }, cli)
2146
+ });
2147
+ };
2148
+ if (git.isRepository() && !git.isIgnored(".claude/worktrees/")) add(".gitignore", "worktrees-untracked");
2149
+ for (const config of biomeConfigs) {
2150
+ const path = join(root, config);
2151
+ if (existsSync(path) && !readFileSync(path, "utf8").includes(".claude/worktrees")) add(config, "worktrees-scanned");
2152
+ }
2153
+ const settings = readJson(join(root, claudeSettings));
2154
+ const worktree = isRecord(settings) ? settings["worktree"] : void 0;
2155
+ if (!isRecord(worktree) || worktree["baseRef"] === void 0) add(claudeSettings, "worktree-base-unset");
2156
+ return findings;
2157
+ }
2055
2158
  //#endregion
2056
2159
  //#region src/governance-overrides/governance-overrides.ts
2057
2160
  function machineWideDir(platform, programData, names) {
@@ -2204,10 +2307,13 @@ function referenceLayers({ packageRoot = dirname(packageGovernancesDir()), ...op
2204
2307
  }
2205
2308
  });
2206
2309
  }
2207
- const referenceCommand = createReferenceCommand({ plugin: {
2208
- name: PACKAGE_PLUGIN,
2209
- root: dirname(packageGovernancesDir())
2210
- } });
2310
+ const referenceCommand = createReferenceCommand({
2311
+ plugin: {
2312
+ name: PACKAGE_PLUGIN,
2313
+ root: dirname(packageGovernancesDir())
2314
+ },
2315
+ path: "buddy-agent-harness reference"
2316
+ });
2211
2317
  //#endregion
2212
2318
  //#region src/skill-projection/skill-projection.ts
2213
2319
  function skillNames(skillsDirectory) {
@@ -2660,6 +2766,10 @@ var GitBridgeState = class {
2660
2766
  this.root = root;
2661
2767
  this.repoRootPrefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
2662
2768
  }
2769
+ /** Whether `root` is inside a git work tree at all; a check that needs git's answer skips when not. */
2770
+ isRepository() {
2771
+ return this.repoRootPrefix !== void 0;
2772
+ }
2663
2773
  /** The skip-worktree bit is a hint some checkouts clear, so it is verified rather than assumed. */
2664
2774
  trackingOf(path) {
2665
2775
  if (this.repoRootPrefix === void 0) return "untracked";
@@ -2922,64 +3032,82 @@ function buildDoctorReport(bin, result, configuration = [], held = []) {
2922
3032
  help: [...new Map(findings.map((finding) => [`${finding.repair.command}\u0000${finding.repair.instruction}`, finding.repair])).values()]
2923
3033
  };
2924
3034
  }
2925
- const doctorCommand = command({
2926
- name: "doctor",
2927
- description: "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only.",
2928
- options: {
2929
- root: {
2930
- description: "Repository or package directory. Defaults to the current directory.",
2931
- type: t.string().optional()
2932
- },
2933
- harness: {
2934
- description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
2935
- type: t.string().optional()
2936
- }
3035
+ const doctorOptions = {
3036
+ root: {
3037
+ description: "Repository or package directory. Defaults to the current directory.",
3038
+ type: t.string().optional()
2937
3039
  },
2938
- async run(args) {
2939
- try {
2940
- const harnesses = parseHarnesses(args.harness);
2941
- const root = args.root ?? process.cwd();
2942
- const home = homedir();
2943
- const diagnosis = diagnoseBridges({
3040
+ harness: {
3041
+ description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
3042
+ type: t.string().optional()
3043
+ }
3044
+ };
3045
+ const doctorDescription = "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only.";
3046
+ /** The live report `doctor` answers with, shared with the bare invocation of the CLI. */
3047
+ async function diagnoseReport(args) {
3048
+ try {
3049
+ const harnesses = parseHarnessOption(args.harness);
3050
+ const root = args.root ?? process.cwd();
3051
+ const home = homedir();
3052
+ const diagnosis = diagnoseBridges({
3053
+ root,
3054
+ ...harnesses.length ? { harnesses } : {},
3055
+ cli: commandInvocation,
3056
+ home,
3057
+ env: process.env
3058
+ });
3059
+ const git = new GitBridgeState(root);
3060
+ const configuration = [
3061
+ ...diagnoseConfiguration({
2944
3062
  root,
2945
- ...harnesses.length ? { harnesses } : {},
2946
- cli: commandInvocation,
2947
- home,
2948
- env: process.env
2949
- });
2950
- const git = new GitBridgeState(root);
2951
- const configuration = [
2952
- ...diagnoseConfiguration({
2953
- root,
2954
- git,
2955
- cli: commandInvocation
2956
- }),
2957
- ...diagnoseMcp({
2958
- root,
2959
- git,
2960
- cli: commandInvocation
2961
- }),
2962
- ...diagnoseNonstandard({
2963
- root,
2964
- cli: commandInvocation
2965
- })
2966
- ];
2967
- const { layers: rows } = listReferences$1(await referenceLayers({
3063
+ git,
3064
+ cli: commandInvocation
3065
+ }),
3066
+ ...diagnoseMcp({
2968
3067
  root,
2969
- home,
2970
- platform: process.platform,
2971
- env: process.env
2972
- }));
2973
- const held = rows.filter(({ tier }) => tier !== "plugin").map(({ name, tier, path, outcome }) => ({
2974
- name,
2975
- tier,
2976
- path: collapseHome(home, path),
2977
- status: outcome
2978
- }));
2979
- return result(buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held));
2980
- } catch (error) {
2981
- return fail(error, "Harness diagnosis failed.");
2982
- }
3068
+ git,
3069
+ cli: commandInvocation
3070
+ }),
3071
+ ...diagnoseNonstandard({
3072
+ root,
3073
+ cli: commandInvocation
3074
+ }),
3075
+ ...diagnoseWorktrees({
3076
+ root,
3077
+ git,
3078
+ cli: commandInvocation
3079
+ })
3080
+ ];
3081
+ const { layers: rows } = listReferences$1(await referenceLayers({
3082
+ root,
3083
+ home,
3084
+ platform: process.platform,
3085
+ env: process.env
3086
+ }));
3087
+ const held = rows.filter(({ tier }) => tier !== "plugin").map(({ name, tier, path, outcome }) => ({
3088
+ name,
3089
+ tier,
3090
+ path: collapseHome(home, path),
3091
+ status: outcome
3092
+ }));
3093
+ return buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held);
3094
+ } catch (error) {
3095
+ return fail(error, "Harness diagnosis failed.");
3096
+ }
3097
+ }
3098
+ const doctorCommand = command({
3099
+ name: "doctor",
3100
+ description: doctorDescription,
3101
+ options: doctorOptions,
3102
+ examples: [{
3103
+ command: "buddy-agent-harness doctor",
3104
+ description: "Check the current repository"
3105
+ }, {
3106
+ command: "buddy-agent-harness doctor --harness codex,gemini-cli",
3107
+ description: "Also check Codex and Gemini CLI"
3108
+ }],
3109
+ async run(args) {
3110
+ return result(await diagnoseReport(args));
2983
3111
  }
2984
3112
  });
2985
3113
  //#endregion
@@ -3010,6 +3138,10 @@ const governanceListCommand = command({
3010
3138
  root: rootOption,
3011
3139
  "overrides-only": overridesOnlyOption
3012
3140
  },
3141
+ examples: [{
3142
+ command: "buddy-agent-harness governance list",
3143
+ description: "List the governances the layers hold"
3144
+ }],
3013
3145
  run(args) {
3014
3146
  process.stderr.write(GOVERNANCE_DEPRECATED);
3015
3147
  try {
@@ -3046,16 +3178,21 @@ const governanceShowCommand = command({
3046
3178
  root: rootOption,
3047
3179
  "overrides-only": overridesOnlyOption
3048
3180
  },
3181
+ examples: [{
3182
+ command: "buddy-agent-harness governance show skill-design",
3183
+ description: "Print one governance document"
3184
+ }],
3049
3185
  run(args) {
3050
3186
  const format = this.format;
3051
3187
  process.stderr.write(GOVERNANCE_DEPRECATED);
3052
3188
  try {
3053
3189
  const name = parseGovernanceName(args.name);
3054
3190
  const found = resolveGovernance(name, layersFor(args));
3055
- if (!found) {
3056
- process.stderr.write(args["overrides-only"] ? `error: no override for governance "${name}" in the project, user, or machine-wide layer.\n` : `error: no governance named "${name}" in any layer.\n`);
3057
- throw CliError.reported(exitCodes.error);
3058
- }
3191
+ if (!found) throw new CliError(args["overrides-only"] ? `no override for governance "${name}" in the project, user, or machine-wide layer.` : `no governance named "${name}" in any layer.`, {
3192
+ code: "governance-not-found",
3193
+ details: { name },
3194
+ help: ref("reference list", {}, "see what is available")
3195
+ });
3059
3196
  const report = {
3060
3197
  name: found.name,
3061
3198
  scope: found.scope,
@@ -3136,9 +3273,16 @@ const initCommand = command({
3136
3273
  type: t.string().optional()
3137
3274
  }
3138
3275
  },
3276
+ examples: [{
3277
+ command: "buddy-agent-harness init",
3278
+ description: "Set up skills for Claude Code and Cursor"
3279
+ }, {
3280
+ command: "buddy-agent-harness init --harness codex --copy",
3281
+ description: "Also enable Codex, copying skills instead of linking"
3282
+ }],
3139
3283
  run(args) {
3140
3284
  try {
3141
- const harnesses = parseHarnesses(args.harness);
3285
+ const harnesses = parseHarnessOption(args.harness);
3142
3286
  return result(initializeHarnesses({
3143
3287
  root: args.root ?? process.cwd(),
3144
3288
  ...harnesses.length ? { harnesses } : {},
@@ -3594,6 +3738,13 @@ const mcpReconcileCommand = command({
3594
3738
  type: t.array(t.string()).optional()
3595
3739
  }
3596
3740
  },
3741
+ examples: [{
3742
+ command: "buddy-agent-harness mcp reconcile",
3743
+ description: "List harness-side changes the golden set lacks"
3744
+ }, {
3745
+ command: "buddy-agent-harness mcp reconcile --accept <path>",
3746
+ description: "Import one approved field"
3747
+ }],
3597
3748
  run(args) {
3598
3749
  try {
3599
3750
  const root = args.root ?? process.cwd();
@@ -3605,19 +3756,17 @@ const mcpReconcileCommand = command({
3605
3756
  });
3606
3757
  if (plan.kind === "unreadable") {
3607
3758
  const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
3608
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`);
3609
- throw CliError.reported(exitCodes.error);
3610
- }
3611
- if (plan.kind === "rejected") {
3612
- process.stderr.write(`error: nothing written.\n${plan.reasons.map((reason) => `- ${reason}\n`).join("")}`);
3613
- throw CliError.reported(exitCodes.error);
3759
+ throw new CliError(`${goldenSetPath} does not parse${at}.`, {
3760
+ code: "golden-set-unreadable",
3761
+ help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
3762
+ });
3614
3763
  }
3615
- if (plan.kind === "absent") return result({
3616
- golden: `0 servers — no golden set at ${goldenSetPath}`,
3617
- mode: "nothing to reconcile into",
3618
- fields: "0 fields",
3619
- record: "not written"
3764
+ if (plan.kind === "rejected") throw new CliError("nothing written.", {
3765
+ code: "accept-rejected",
3766
+ details: { reasons: plan.reasons },
3767
+ help: ref("mcp reconcile", {}, "list the fields that can be approved")
3620
3768
  });
3769
+ if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: `Create ${goldenSetPath} first; there is nothing to reconcile into.` });
3621
3770
  const report = {
3622
3771
  golden: goldenSetPath,
3623
3772
  mode: plan.written ? "written — only the fields named by --accept" : "dry run — nothing written; re-run with --accept <path> for each approved field",
@@ -3882,6 +4031,13 @@ const mcpProjectCommand = command({
3882
4031
  type: t.boolean().optional()
3883
4032
  }
3884
4033
  },
4034
+ examples: [{
4035
+ command: "buddy-agent-harness mcp project",
4036
+ description: "Preview what would be projected"
4037
+ }, {
4038
+ command: "buddy-agent-harness mcp project --write",
4039
+ description: "Project the golden set into each harness"
4040
+ }],
3885
4041
  run(args) {
3886
4042
  try {
3887
4043
  const root = args.root ?? process.cwd();
@@ -3893,15 +4049,12 @@ const mcpProjectCommand = command({
3893
4049
  });
3894
4050
  if (plan.kind === "unreadable") {
3895
4051
  const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
3896
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`);
3897
- throw CliError.reported(exitCodes.error);
4052
+ throw new CliError(`${goldenSetPath} does not parse${at}.`, {
4053
+ code: "golden-set-unreadable",
4054
+ help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
4055
+ });
3898
4056
  }
3899
- if (plan.kind === "absent") return result({
3900
- golden: `0 servers — no golden set at ${goldenSetPath}`,
3901
- mode: "nothing to project",
3902
- actions: "0 changes",
3903
- record: "not written"
3904
- });
4057
+ if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: [`Create ${goldenSetPath} with the MCP servers to project.`, ref("mcp project", {}, "preview the projection")] });
3905
4058
  const report = {
3906
4059
  golden: goldenSetPath,
3907
4060
  mode: write ? "written" : "dry run — nothing written; re-run with --write to apply",
@@ -3943,6 +4096,19 @@ function app(format) {
3943
4096
  version,
3944
4097
  description: rootCommand.description,
3945
4098
  format
4099
+ }).default({
4100
+ description: "Report whether this repository's harness bridges resolve (the same as `doctor`).",
4101
+ options: doctorCommand.options,
4102
+ async run(args) {
4103
+ const { bin: _bin, ...report } = await diagnoseReport({
4104
+ root: args["root"],
4105
+ harness: args["harness"]
4106
+ });
4107
+ return result({
4108
+ ...homeHeader({ description: rootCommand.description }),
4109
+ ...report
4110
+ });
4111
+ }
3946
4112
  }).command(first));
3947
4113
  }
3948
4114
  const textByDefault = {