buddy-agent-harness 0.4.1 → 0.6.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.
@@ -2,7 +2,7 @@ import { command, z } from "clibuilder";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, join, posix, relative, sep } from "node:path";
4
4
  import { encode } from "@toon-format/toon";
5
- import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs";
5
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs";
6
6
  import { execFileSync } from "node:child_process";
7
7
  //#region src/command-output/command-output.ts
8
8
  const formats = [
@@ -61,8 +61,18 @@ function binPath(home, executable) {
61
61
  //#endregion
62
62
  //#region src/harness-registry/harness-registry.ts
63
63
  /**
64
- * Codex, Cursor, Copilot CLI, and Devin Desktop read `.agents/skills` directly, so they need no
65
- * projection. Only Claude Code and Gemini CLI read solely their own directory.
64
+ * The two scopes disagree in general, and a single answer per harness could only record one of them:
65
+ * Copilot CLI is `.github/skills` in a repository and `~/.copilot` for the user. Claude Code is the
66
+ * only harness that needs a skills projection, and it needs one at both scopes. Codex, Cursor,
67
+ * Copilot CLI, Gemini CLI, and Devin Desktop read `.agents/skills` themselves and are never
68
+ * projected into.
69
+ *
70
+ * Gemini CLI carried a `.gemini/skills` projection until E-GEM-02: it reads the `.agents/skills`
71
+ * alias at project scope too, where that alias takes precedence over `.gemini/skills`. It still
72
+ * needs an instruction bridge, which is a separate axis and unaffected.
73
+ *
74
+ * Instruction bridges are recorded at project scope only. The user-scope equivalents exist, but
75
+ * nothing writes or reads them yet: `init` works inside a repository, and so does `doctor`.
66
76
  *
67
77
  * `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a
68
78
  * deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps
@@ -73,34 +83,56 @@ function binPath(home, executable) {
73
83
  const harnessRegistry = [
74
84
  {
75
85
  name: "claude-code",
76
- detect: ".claude",
77
- skillsDirectory: ".claude/skills"
86
+ project: {
87
+ detect: ".claude",
88
+ skillsDirectory: ".claude/skills",
89
+ instructionBridge: {
90
+ kind: "import",
91
+ path: "CLAUDE.md"
92
+ }
93
+ },
94
+ user: {
95
+ detect: ".claude",
96
+ skillsDirectory: ".claude/skills"
97
+ }
78
98
  },
79
99
  {
80
100
  name: "cursor",
81
- detect: ".cursor"
101
+ project: { detect: ".cursor" },
102
+ user: { detect: ".cursor" }
82
103
  },
83
104
  {
84
105
  name: "codex",
85
- detect: ".codex"
106
+ project: { detect: ".codex" },
107
+ user: { detect: ".codex" }
86
108
  },
87
109
  {
88
110
  name: "copilot-cli",
89
- detect: ".github/skills"
111
+ project: { detect: ".github/skills" },
112
+ user: { detect: ".copilot" }
90
113
  },
91
114
  {
92
115
  name: "gemini-cli",
93
- detect: ".gemini",
94
- skillsDirectory: ".gemini/skills"
116
+ project: {
117
+ detect: ".gemini",
118
+ instructionBridge: {
119
+ kind: "settings-entry",
120
+ path: ".gemini/settings.json",
121
+ key: "context.fileName"
122
+ }
123
+ },
124
+ user: { detect: ".gemini" }
95
125
  },
96
126
  {
97
127
  name: "devin-desktop",
98
- detect: ".devin"
128
+ project: { detect: ".devin" }
99
129
  },
100
130
  {
101
131
  name: "windsurf",
102
- detect: ".windsurf",
103
- skillsDirectory: ".windsurf/skills",
132
+ project: {
133
+ detect: ".windsurf",
134
+ skillsDirectory: ".windsurf/skills"
135
+ },
104
136
  deprecated: "devin-desktop"
105
137
  }
106
138
  ];
@@ -116,23 +148,37 @@ function directoryExists(path) {
116
148
  /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
117
149
  function selectHarnesses(root, preferred) {
118
150
  const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
119
- return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.detect)));
151
+ return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
120
152
  }
121
153
  //#endregion
122
- //#region src/diagnose-bridges/directory-files.ts
154
+ //#region src/diagnose-bridges/agents-files.ts
155
+ /** Directories that never hold instructions this repository is responsible for bridging. */
156
+ const pruned = /* @__PURE__ */ new Set(["node_modules"]);
123
157
  /**
124
- * Every file below `directory`, as paths relative to it, POSIX-separated and sorted. A directory
125
- * that does not exist reads as empty: `doctor` never creates one to look at it.
158
+ * Every directory below `root` holding an `AGENTS.md`, repository-relative and POSIX-separated,
159
+ * with the root itself as `''`. Sorted, root first.
160
+ *
161
+ * Dot-directories are pruned along with `node_modules`. That is not only a speed measure:
162
+ * `.agents/AGENTS.md` is canonical shared instructions rather than a subtree-scoped file, and
163
+ * bridging it would claim a scope it does not have.
126
164
  */
127
- function filesUnder(directory) {
128
- try {
129
- return readdirSync(directory, {
130
- recursive: true,
131
- withFileTypes: true
132
- }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
133
- } catch {
134
- return [];
135
- }
165
+ function agentsFileDirectories(root) {
166
+ const found = [];
167
+ const walk = (relative) => {
168
+ if (existsSync(join(root, relative, "AGENTS.md"))) found.push(relative);
169
+ let entries;
170
+ try {
171
+ entries = readdirSync(join(root, relative), { withFileTypes: true });
172
+ } catch {
173
+ return;
174
+ }
175
+ for (const entry of entries) {
176
+ if (!entry.isDirectory() || entry.name.startsWith(".") || pruned.has(entry.name)) continue;
177
+ walk(relative ? posix.join(relative, entry.name) : entry.name);
178
+ }
179
+ };
180
+ walk("");
181
+ return found.sort();
136
182
  }
137
183
  //#endregion
138
184
  //#region src/diagnose-bridges/doctor-guidance.ts
@@ -163,7 +209,8 @@ const launcherFor = (subcommand) => `scripts/${subcommand}.mjs`;
163
209
  const launcherInvocation = (subcommand) => `node ${launcherFor(subcommand)}`;
164
210
  /** How the skill hands a repair back to `init`. */
165
211
  const initSkillInvocation = "/buddy-agent-harness:init";
166
- const doctorRepairs = [
212
+ /** The skills bridges, reported in `bridges`. */
213
+ const bridgeRepairs = [
167
214
  {
168
215
  problem: "no-canonical",
169
216
  detail: "the canonical skill directory does not exist, so no bridge can resolve",
@@ -219,13 +266,47 @@ const doctorRepairs = [
219
266
  skillRepair: (path) => `run \`git ls-files -z ${path} | xargs -0 git update-index --skip-worktree\``
220
267
  }
221
268
  ];
269
+ /**
270
+ * The instruction bridges, reported in `instructions`. Every repair is the `init` skill: these are
271
+ * files a person wrote, or files carrying content beside the bridge, and deciding what to preserve
272
+ * while restoring the bridge is judgment no flag carries. `repair` therefore names the skill in
273
+ * both places rather than pretending a shell command exists.
274
+ */
275
+ const instructionRepairs = [
276
+ {
277
+ problem: "no-instructions",
278
+ detail: "no AGENTS.md at the repository root, so every instruction bridge points at nothing",
279
+ repair: () => initSkillInvocation,
280
+ skillRepair: () => `run \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`
281
+ },
282
+ {
283
+ problem: "instructions-missing",
284
+ detail: "no instruction bridge at this path — the harness reads none of AGENTS.md",
285
+ repair: () => initSkillInvocation,
286
+ skillRepair: () => `run \`${initSkillInvocation}\``
287
+ },
288
+ {
289
+ problem: "instructions-unbridged",
290
+ detail: "the file is present but names AGENTS.md nowhere — the harness reads none of it",
291
+ repair: () => initSkillInvocation,
292
+ skillRepair: () => `run \`${initSkillInvocation}\`, which adds the bridge without discarding what the file already says`
293
+ },
294
+ {
295
+ problem: "instructions-unreadable",
296
+ detail: "the settings file does not parse, so the harness reads none of it",
297
+ repair: () => initSkillInvocation,
298
+ skillRepair: () => `fix the JSON by hand, then run \`${initSkillInvocation}\``
299
+ }
300
+ ];
301
+ /** Both sections, in the order `doctor` reports them. */
302
+ const doctorRepairs = [...bridgeRepairs, ...instructionRepairs];
222
303
  const repairsByProblem = new Map(doctorRepairs.map((entry) => [entry.problem, entry]));
223
304
  function repairFor(problem) {
224
305
  return repairsByProblem.get(problem);
225
306
  }
226
307
  const doctorSkill = {
227
308
  name: "doctor",
228
- description: "Use this skill when a repository loads no project skills, when skills are missing after a clone, or when checking whether the agent configuration bridges into .claude/skills and the other harness directories still resolve."
309
+ 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."
229
310
  };
230
311
  /** Written into the generated skill so a reader knows not to edit it in place. */
231
312
  const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand. -->";
@@ -236,7 +317,7 @@ const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-g
236
317
  */
237
318
  function renderDoctorSkill(version) {
238
319
  const cell = (value) => value.replaceAll("|", "\\|");
239
- const rows = doctorRepairs.map((entry) => `| \`${entry.problem}\` | ${entry.detail} | ${cell(entry.skillRepair("<path>"))} |`).join("\n");
320
+ const rows = (repairs) => repairs.map((entry) => `| \`${entry.problem}\` | ${entry.detail} | ${cell(entry.skillRepair("<path>"))} |`).join("\n");
240
321
  return `---
241
322
  name: ${doctorSkill.name}
242
323
  description: ${doctorSkill.description}
@@ -246,7 +327,7 @@ ${generatedSkillWarning}
246
327
 
247
328
  # Harness Doctor
248
329
 
249
- \`.agents/skills\` is the canonical skill directory. Harnesses that cannot read it — Claude Code and Gemini CLI — get a bridge pointing at it, normally a directory symlink. A bridge that stops resolving is silent: the harness finds nothing and loads zero project skills, with no warning anywhere.
330
+ A repository keeps one canonical configuration: \`.agents/skills\` for its skills and \`AGENTS.md\` for its instructions. Harnesses that cannot read those get bridges pointing at them: Claude Code needs both, and Gemini CLI needs the instruction bridge only — it reads \`.agents/skills\` itself. A bridge that stops resolving is silent: the harness finds nothing and loads zero project skills, with no warning anywhere. An instruction bridge fails the same way and costs more, because the harness then reads none of the repository's instructions at all.
250
331
 
251
332
  Diagnose it:
252
333
 
@@ -260,22 +341,36 @@ The command is read-only. It never repairs anything, so it is safe to run at any
260
341
 
261
342
  ## Reading the report
262
343
 
263
- \`bridges\` lists every bridge \`init\` would create for this repository, each with a \`status\` of \`ok\`, \`missing\`, \`degraded\`, \`stale\`, or \`diverged\`. \`findings\` explains each problem and \`help\` names its repair. Apply the repair from the table below, then re-run \`doctor\`.
344
+ \`bridges\` lists every skills bridge \`init\` would create for this repository, each with a \`status\` of \`ok\`, \`missing\`, \`degraded\`, \`stale\`, or \`diverged\`.
345
+
346
+ \`instructions\` lists every instruction bridge into \`AGENTS.md\`, with a \`status\` of \`ok\`, \`missing\`, \`unbridged\`, or \`unreadable\`. They are a separate section because nothing about them is shared: a different \`kind\`, a different status vocabulary, and a repair that is never a command.
347
+
348
+ \`findings\` explains each problem from either section and \`help\` names its repair. Apply the repair from the tables below, then re-run \`doctor\`.
264
349
 
265
- \`help\` names the \`init\` command for a person at a shell. Do not run it yourself. Rebuilding a bridge can move skills a user wrote, and that judgment belongs to the \`init\` skill — hand the repair to \`${initSkillInvocation}\` instead.
350
+ Do not run an \`init\` command yourself. Rebuilding a skills bridge can move skills a user wrote, and rewriting an instruction file touches prose a person authored — both are the \`init\` skill's judgment, so hand the repair to \`${initSkillInvocation}\` instead. A \`help\` line naming that skill has no shell equivalent at all.
266
351
 
267
352
  When every bridge resolves, \`findings\` says so outright rather than being empty.
268
353
 
269
354
  The default output is TOON, which is what you parse. Add \`--format text\` when you need to show the same report to a person, or \`--format json\`.
270
355
 
271
- ## Findings and their repairs
356
+ ## Skills bridge findings
272
357
 
273
358
  | Finding | What it means | Repair |
274
359
  | --- | --- | --- |
275
- ${rows}
360
+ ${rows(bridgeRepairs)}
276
361
 
277
362
  Substitute the reported bridge path for \`<path>\`.
278
363
 
364
+ ## Instruction bridge findings
365
+
366
+ | Finding | What it means | Repair |
367
+ | --- | --- | --- |
368
+ ${rows(instructionRepairs)}
369
+
370
+ \`unbridged\` is the one to read carefully. The file is there and looks fine, and it names \`AGENTS.md\` nowhere — a \`CLAUDE.md\` someone overwrote with real content, or a \`.gemini/settings.json\` another tool rewrote without \`AGENTS.md\` in \`context.fileName\`. Never fix it by replacing the file: the content that displaced the bridge may be the only copy of something.
371
+
372
+ An instruction bridge is reported per file, so a monorepo gets one row per \`AGENTS.md\` in the tree. Each nested \`AGENTS.md\` needs its own stub — an import bridges the file beside it and nothing deeper.
373
+
279
374
  ## The Windows case
280
375
 
281
376
  The common failure is \`degraded\`. Creating a symlink on Windows needs a privilege most accounts do not have, and Git for Windows gates it separately with \`core.symlinks\`, which its installer leaves off. With \`core.symlinks=false\` git does not error — it writes the symlink out as a regular file whose contents are the target path. \`${initSkillInvocation} --copy --force\` rebuilds the bridges as real directories on that machine.
@@ -287,9 +382,207 @@ A copy is a snapshot rather than a live projection, and an agent that edits a sk
287
382
  - Never repair a \`diverged-both\` or \`diverged-unknown\` bridge by re-running \`init\`. Rebuilding overwrites whichever side holds the newer edit. Reconcile the two directories first.
288
383
  - Edit skills at \`.agents/skills/<name>/SKILL.md\`. Editing through a bridge is only safe when that bridge is a symlink.
289
384
  - Do not add bridges to \`.gitignore\`. An untracked bridge swallows a real edit silently.
385
+ - Write instructions in \`AGENTS.md\`, never in \`CLAUDE.md\`. A bridge file holds the import and any harness-specific notes; content written there reaches one harness and drifts from the canonical file.
290
386
  `;
291
387
  }
292
388
  //#endregion
389
+ //#region src/diagnose-bridges/json-with-comments.ts
390
+ /**
391
+ * `.gemini/settings.json` may legally carry comments — the Gemini CLI loader strips them before
392
+ * parsing. A plain `JSON.parse` of a valid settings file therefore throws, and `doctor` would report
393
+ * a working bridge as unreadable. Comments are removed before parsing for that reason.
394
+ *
395
+ * Only comments. A trailing comma stays a parse error, because nothing documents it as accepted.
396
+ */
397
+ function stripJsonComments(source) {
398
+ let output = "";
399
+ let index = 0;
400
+ let inString = false;
401
+ let escaped = false;
402
+ while (index < source.length) {
403
+ const character = source[index];
404
+ if (inString) {
405
+ output += character;
406
+ if (escaped) escaped = false;
407
+ else if (character === "\\") escaped = true;
408
+ else if (character === "\"") inString = false;
409
+ index += 1;
410
+ continue;
411
+ }
412
+ if (character === "\"") {
413
+ inString = true;
414
+ output += character;
415
+ index += 1;
416
+ continue;
417
+ }
418
+ if (character === "/" && source[index + 1] === "/") {
419
+ while (index < source.length && source[index] !== "\n") index += 1;
420
+ continue;
421
+ }
422
+ if (character === "/" && source[index + 1] === "*") {
423
+ index += 2;
424
+ while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) index += 1;
425
+ index += 2;
426
+ continue;
427
+ }
428
+ output += character;
429
+ index += 1;
430
+ }
431
+ return output;
432
+ }
433
+ /** The parsed value, or `undefined` when the file is not JSON even once its comments are gone. */
434
+ function parseJsonWithComments(source) {
435
+ try {
436
+ return JSON.parse(stripJsonComments(source));
437
+ } catch {
438
+ return;
439
+ }
440
+ }
441
+ //#endregion
442
+ //#region src/diagnose-bridges/diagnose-instructions.ts
443
+ const canonicalInstructions = "AGENTS.md";
444
+ /**
445
+ * An import bridges the `AGENTS.md` beside it and nothing else, so the check is per directory
446
+ * holding one rather than per harness. A line of its own is enough: the harness reference allows
447
+ * Claude-specific notes below the import, and a file that carries the import plus notes still
448
+ * bridges.
449
+ */
450
+ function inspectImport(target) {
451
+ let stats;
452
+ try {
453
+ stats = lstatSync(target);
454
+ } catch {
455
+ return {
456
+ kind: "none",
457
+ status: "missing",
458
+ problem: "instructions-missing"
459
+ };
460
+ }
461
+ if (stats.isSymbolicLink()) return readlinkSync(target) === "AGENTS.md" ? {
462
+ kind: "symlink",
463
+ status: "ok"
464
+ } : {
465
+ kind: "symlink",
466
+ status: "unbridged",
467
+ problem: "instructions-unbridged"
468
+ };
469
+ return readFileSync(target, "utf8").split("\n").some((line) => line.trim() === `@AGENTS.md`) ? {
470
+ kind: "import",
471
+ status: "ok"
472
+ } : {
473
+ kind: "file",
474
+ status: "unbridged",
475
+ problem: "instructions-unbridged"
476
+ };
477
+ }
478
+ /** Reads a dotted key out of parsed JSON without asserting anything about the rest of the file. */
479
+ function valueAt(settings, key) {
480
+ return key.split(".").reduce((value, segment) => {
481
+ if (typeof value !== "object" || value === null) return void 0;
482
+ return value[segment];
483
+ }, settings);
484
+ }
485
+ function inspectSettingsEntry(target, key) {
486
+ let source;
487
+ try {
488
+ source = readFileSync(target, "utf8");
489
+ } catch {
490
+ return {
491
+ kind: "none",
492
+ status: "missing",
493
+ problem: "instructions-missing"
494
+ };
495
+ }
496
+ const settings = parseJsonWithComments(source);
497
+ if (settings === void 0) return {
498
+ kind: "file",
499
+ status: "unreadable",
500
+ problem: "instructions-unreadable"
501
+ };
502
+ const entry = valueAt(settings, key);
503
+ return Array.isArray(entry) && entry.includes("AGENTS.md") ? {
504
+ kind: "settings-entry",
505
+ status: "ok"
506
+ } : {
507
+ kind: "file",
508
+ status: "unbridged",
509
+ problem: "instructions-unbridged"
510
+ };
511
+ }
512
+ /** Every path an instruction bridge of this kind belongs at, in report order. */
513
+ function pathsFor(root, bridge) {
514
+ if (bridge.kind !== "import") return [bridge.path];
515
+ return agentsFileDirectories(root).map((directory) => directory ? posix.join(directory, bridge.path) : bridge.path);
516
+ }
517
+ function inspect$1(target, bridge) {
518
+ return bridge.kind === "import" ? inspectImport(target) : inspectSettingsEntry(target, bridge.key);
519
+ }
520
+ /**
521
+ * Reports whether every enabled harness can still read `AGENTS.md`.
522
+ *
523
+ * Losing one of these is quieter than losing a skills bridge and costs more: a harness with no
524
+ * instruction bridge reads none of the repository's instructions, and says nothing about it. The
525
+ * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
526
+ * skill's judgment.
527
+ */
528
+ function diagnoseInstructions(root, harnesses, cli) {
529
+ const bridged = harnesses.map((harness) => ({
530
+ name: harness.name,
531
+ bridge: harness.project.instructionBridge
532
+ })).filter((harness) => Boolean(harness.bridge));
533
+ const instructions = [];
534
+ const findings = [];
535
+ if (!bridged.length) return {
536
+ instructions,
537
+ findings
538
+ };
539
+ if (!agentsFileDirectories(root).includes("")) {
540
+ const { detail, repair } = repairFor("no-instructions");
541
+ findings.push({
542
+ path: canonicalInstructions,
543
+ detail,
544
+ repair: repair(canonicalInstructions, cli)
545
+ });
546
+ }
547
+ for (const { name, bridge } of bridged) for (const path of pathsFor(root, bridge)) {
548
+ const inspection = inspect$1(join(root, path), bridge);
549
+ instructions.push({
550
+ harness: name,
551
+ path,
552
+ kind: inspection.kind,
553
+ status: inspection.status
554
+ });
555
+ if (inspection.problem) {
556
+ const { detail, repair } = repairFor(inspection.problem);
557
+ findings.push({
558
+ path,
559
+ detail,
560
+ repair: repair(path, cli)
561
+ });
562
+ }
563
+ }
564
+ return {
565
+ instructions,
566
+ findings
567
+ };
568
+ }
569
+ //#endregion
570
+ //#region src/diagnose-bridges/directory-files.ts
571
+ /**
572
+ * Every file below `directory`, as paths relative to it, POSIX-separated and sorted. A directory
573
+ * that does not exist reads as empty: `doctor` never creates one to look at it.
574
+ */
575
+ function filesUnder(directory) {
576
+ try {
577
+ return readdirSync(directory, {
578
+ recursive: true,
579
+ withFileTypes: true
580
+ }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
581
+ } catch {
582
+ return [];
583
+ }
584
+ }
585
+ //#endregion
293
586
  //#region src/diagnose-bridges/git-bridge-state.ts
294
587
  function git(root, args, input) {
295
588
  try {
@@ -467,14 +760,18 @@ function inspect(target, path, canonical, git) {
467
760
  }
468
761
  }
469
762
  /**
470
- * Reports whether every bridge `init` would create for this repository still resolves into
471
- * `.agents/skills`. Read-only: nothing is created, moved, or repaired, so the caller decides what
472
- * to run from the repair each finding carries.
763
+ * Reports whether every bridge this repository needs still resolves: the skills bridges into
764
+ * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
765
+ * moved, or repaired, so the caller decides what to run from the repair each finding carries.
473
766
  */
474
767
  function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
475
768
  const canonical = join(root, ".agents", "skills");
476
769
  const git = new GitBridgeState(root);
477
- const bridged = selectHarnesses(root, preferred).filter((harness) => Boolean(harness.skillsDirectory));
770
+ const selected = selectHarnesses(root, preferred);
771
+ const bridged = selected.map((harness) => ({
772
+ name: harness.name,
773
+ skillsDirectory: harness.project.skillsDirectory
774
+ })).filter((harness) => Boolean(harness.skillsDirectory));
478
775
  const bridges = [];
479
776
  const divergence = [];
480
777
  const findings = [];
@@ -508,8 +805,11 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
508
805
  });
509
806
  }
510
807
  }
808
+ const instructions = diagnoseInstructions(root, selected, cli);
809
+ findings.push(...instructions.findings);
511
810
  return {
512
811
  bridges,
812
+ instructions: instructions.instructions,
513
813
  divergence,
514
814
  findings
515
815
  };
@@ -522,16 +822,18 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
522
822
  */
523
823
  function buildReport(bin, result) {
524
824
  if (!result.findings.length) {
525
- const count = result.bridges.length;
825
+ const count = result.bridges.length + result.instructions.length;
526
826
  return {
527
827
  bin,
528
828
  bridges: result.bridges,
829
+ instructions: result.instructions,
529
830
  findings: count === 1 ? "0 problems found — the 1 bridge resolves" : `0 problems found — all ${count} bridges resolve`
530
831
  };
531
832
  }
532
833
  return {
533
834
  bin,
534
835
  bridges: result.bridges,
836
+ instructions: result.instructions,
535
837
  ...result.divergence.length ? { divergence: result.divergence } : {},
536
838
  findings: result.findings.map(({ path, detail }) => ({
537
839
  path,
@@ -542,7 +844,7 @@ function buildReport(bin, result) {
542
844
  }
543
845
  const doctorCommand = command({
544
846
  name: "doctor",
545
- description: "Report whether the harness skill bridges in this repository still resolve. Read-only.",
847
+ description: "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md. Read-only.",
546
848
  options: {
547
849
  root: {
548
850
  description: "Repository or package directory. Defaults to the current directory.",
@@ -602,9 +904,9 @@ function countSkills(canonicalSkills) {
602
904
  * preferred; a platform that refuses the link falls back to a copy. Returns the projected harnesses.
603
905
  */
604
906
  function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
605
- const projections = harnesses.filter((harness) => harness.skillsDirectory).map((harness) => ({
907
+ const projections = harnesses.filter((harness) => harness.project.skillsDirectory).map((harness) => ({
606
908
  harness,
607
- target: join(root, harness.skillsDirectory)
909
+ target: join(root, harness.project.skillsDirectory)
608
910
  }));
609
911
  const conflicts = projections.filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills)).map(({ target }) => target);
610
912
  if (conflicts.length && !force) throw new Error(`Refusing to replace existing skill targets:\n${conflicts.map((target) => `- ${target}`).join("\n")}`);
@@ -644,7 +946,7 @@ function initializeHarnesses({ root, harnesses: preferred = [], copy = false, fo
644
946
  return {
645
947
  root,
646
948
  harnesses: harnesses.map((harness) => harness.name),
647
- native: harnesses.filter((harness) => !harness.skillsDirectory).map((harness) => harness.name),
949
+ native: harnesses.filter((harness) => !harness.project.skillsDirectory).map((harness) => harness.name),
648
950
  linked,
649
951
  deprecated: harnesses.filter((harness) => harness.deprecated).map((harness) => ({
650
952
  name: harness.name,
@@ -709,6 +1011,6 @@ function activate({ addCommand }) {
709
1011
  addCommand(harnessCommand);
710
1012
  }
711
1013
  //#endregion
712
- export { doctorCommand as a, doctorSkill as c, initializeHarnesses as i, renderDoctorSkill as l, harnessCommand as n, diagnoseBridges as o, initCommand as r, doctorRepairs as s, activate as t, harnessRegistry as u };
1014
+ export { doctorCommand as a, bridgeRepairs as c, instructionRepairs as d, renderDoctorSkill as f, initializeHarnesses as i, doctorRepairs as l, harnessCommand as n, diagnoseBridges as o, harnessRegistry as p, initCommand as r, diagnoseInstructions as s, activate as t, doctorSkill as u };
713
1015
 
714
- //# sourceMappingURL=init.command-c0fBv15T.mjs.map
1016
+ //# sourceMappingURL=init.command-zbntulIq.mjs.map