buddy-agent-harness 0.5.0 → 0.7.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 (70) 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/bin/buddy-agent-harness.mjs +8 -3
  5. package/dist/cli-Bvnq8zOL.mjs +2122 -0
  6. package/dist/cli-Bvnq8zOL.mjs.map +1 -0
  7. package/dist/cli.d.mts +13 -2
  8. package/dist/cli.d.mts.map +1 -1
  9. package/dist/cli.mjs +2 -20
  10. package/dist/index.d.mts +491 -40
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +2 -2
  13. package/package.json +3 -2
  14. package/plugin.json +1 -1
  15. package/skills/doctor/SKILL.md +27 -23
  16. package/skills/doctor/references/bridges.md +23 -0
  17. package/skills/doctor/references/configuration.md +12 -0
  18. package/skills/doctor/references/harnesses/claude-code.md +39 -0
  19. package/skills/doctor/references/harnesses/codex.md +37 -0
  20. package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
  21. package/skills/doctor/references/harnesses/cursor.md +40 -0
  22. package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
  23. package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
  24. package/skills/doctor/references/harnesses/windsurf.md +27 -0
  25. package/skills/doctor/references/instructions.md +14 -0
  26. package/skills/doctor/references/mcp.md +31 -0
  27. package/skills/doctor/references/nonstandard.md +27 -0
  28. package/skills/doctor/scripts/doctor.mjs +7 -3
  29. package/skills/enhance/SKILL.md +4 -0
  30. package/skills/init/README.md +1 -1
  31. package/skills/init/SKILL.md +24 -10
  32. package/skills/init/references/detection.md +5 -3
  33. package/skills/init/references/harnesses/claude-code.md +2 -0
  34. package/skills/init/references/harnesses/cursor.md +9 -2
  35. package/skills/init/references/harnesses/gemini-cli.md +6 -9
  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 +40 -7
  43. package/src/command-output/command-output.ts +1 -4
  44. package/src/diagnose-bridges/agents-files.ts +34 -0
  45. package/src/diagnose-bridges/diagnose-bridges.ts +39 -21
  46. package/src/diagnose-bridges/diagnose-instructions.ts +145 -0
  47. package/src/diagnose-bridges/doctor-guidance.ts +683 -69
  48. package/src/diagnose-bridges/doctor.command.ts +63 -30
  49. package/src/diagnose-bridges/git-bridge-state.ts +30 -0
  50. package/src/diagnose-bridges/json-with-comments.ts +60 -0
  51. package/src/diagnose-bridges/locator.ts +36 -0
  52. package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
  53. package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
  54. package/src/diagnose-mcp/mcp-baseline.ts +122 -0
  55. package/src/diagnose-mcp/mcp-model.ts +88 -0
  56. package/src/diagnose-mcp/mcp-secrets.ts +101 -0
  57. package/src/diagnose-mcp/mcp-sources.ts +131 -0
  58. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
  59. package/src/harness-registry/harness-registry.ts +164 -14
  60. package/src/harness-registry/instruction-bridge.ts +31 -0
  61. package/src/harness-registry/mcp-config.ts +37 -0
  62. package/src/harness-registry/nonstandard-artifact.ts +39 -0
  63. package/src/index.ts +33 -4
  64. package/src/initialize-harnesses/init.command.ts +13 -18
  65. package/src/initialize-harnesses/initialize-harnesses.ts +7 -4
  66. package/src/is-record/is-record.ts +12 -0
  67. package/src/skill-projection/skill-projection.ts +104 -18
  68. package/dist/cli.mjs.map +0 -1
  69. package/dist/init.command-c0fBv15T.mjs +0 -714
  70. package/dist/init.command-c0fBv15T.mjs.map +0 -1
@@ -1,10 +1,15 @@
1
+ import type { Harness, HarnessScope } from '../harness-registry/harness-registry.ts'
2
+ import { harnessRegistry } from '../harness-registry/harness-registry.ts'
3
+ import type { NonstandardKind } from '../harness-registry/nonstandard-artifact.ts'
4
+ import { type Locator, locatorText } from './locator.ts'
5
+
1
6
  /**
2
7
  * The one place `doctor`'s guidance is written. The command reads `detail` and `repair` to fill its
3
8
  * `findings` and `help` sections; `skills/doctor/SKILL.md` is generated from the same table by
4
9
  * `scripts/generate-skills.ts`, so the shipped skill cannot drift from what the command says.
5
10
  */
6
11
 
7
- /** Every way a bridge can fail, in the order `doctor` reports them. */
12
+ /** Every way a skills bridge can fail, in the order `doctor` reports them. */
8
13
  export type BridgeProblem =
9
14
  | 'no-canonical'
10
15
  | 'missing'
@@ -16,21 +21,125 @@ export type BridgeProblem =
16
21
  | 'diverged-unknown'
17
22
  | 'unpinned-copy'
18
23
 
24
+ /**
25
+ * Every way an instruction bridge can fail. Separate from `BridgeProblem` because the two share no
26
+ * repair: a skills bridge is rebuilt with `init` flags, and an instruction bridge is a file whose
27
+ * content is the user's, so every repair here goes back to the `init` skill.
28
+ */
29
+ /**
30
+ * Configuration that is present and **wrong**, as against a bridge that does not resolve. These are
31
+ * the faults the `repair` skill owns: none is expressible as an `init` flag, because `init`
32
+ * consolidates and creates but never corrects a file the user already wrote.
33
+ */
34
+ export type ConfigurationFault = 'deprecated-harness' | 'ignored-bridge' | 'unread-local-override' | 'unloadable-skill'
35
+
36
+ /**
37
+ * Every way the golden MCP server set and a harness's copy of it can disagree, plus the two
38
+ * credential findings.
39
+ *
40
+ * A family of its own because nothing it says is shared with the others. A bridge either resolves
41
+ * or does not; two MCP files never share a byte, so every finding here is the result of a semantic
42
+ * comparison, and each one names a **server** and usually a **field** rather than a file.
43
+ */
44
+ export type McpProblem =
45
+ | 'mcp-golden-unreadable'
46
+ | 'mcp-target-unreadable'
47
+ | 'mcp-unprojected'
48
+ | 'mcp-undeclared'
49
+ | 'mcp-diverged-target'
50
+ | 'mcp-diverged-golden'
51
+ | 'mcp-diverged-both'
52
+ | 'mcp-diverged-unknown'
53
+ | 'mcp-literal-secret'
54
+ | 'mcp-committed-secret'
55
+
56
+ /**
57
+ * Configuration that works, for exactly one harness. Not wrong and not missing — the two states the
58
+ * other families cover — which is why it is a family of its own: every other finding says something
59
+ * is broken, and these say something reaches less far than the repository meant.
60
+ *
61
+ * Each one names the canonical form it converts to, because surfacing without a destination is just
62
+ * a list of files somebody already knows about.
63
+ */
64
+ export type NonstandardProblem =
65
+ | 'nonstandard-instructions'
66
+ | 'nonstandard-rule'
67
+ | 'nonstandard-command'
68
+ | 'nonstandard-skill'
69
+ | 'nonstandard-subagent'
70
+
71
+ export type InstructionProblem =
72
+ | 'no-instructions'
73
+ | 'instructions-missing'
74
+ | 'instructions-unbridged'
75
+ | 'instructions-unreadable'
76
+
77
+ /** Everything `doctor` can report against, across every section. */
78
+ export type DoctorProblem = BridgeProblem | InstructionProblem | ConfigurationFault | McpProblem | NonstandardProblem
79
+
80
+ /** Alias kept for callers that name the whole set rather than one section. */
81
+ export type ConfigurationProblem = DoctorProblem
82
+
83
+ /**
84
+ * What resolves one finding, for `doctor`'s own output. Two fields rather than one string, because
85
+ * the caller `doctor` is written for is an agent parsing TOON, and the question it has to answer is
86
+ * "can I execute this, or is this judgment I hand to a skill?" A single string leaves that
87
+ * answerable only by parsing English.
88
+ *
89
+ * The contract is exact, and it is what makes the field safe to act on blindly:
90
+ *
91
+ * - `command` non-empty — a shell invocation that, run verbatim, **completes** the repair.
92
+ * - `command` empty — no single invocation does; act on `instruction` and do not synthesize one.
93
+ *
94
+ * So a diagnostic worth running is not a `command`. `diverged-both` carries none even though
95
+ * `git diff --no-index …` is perfectly runnable: the diff shows what differs, it does not reconcile
96
+ * anything. That is what stops an agent executing every `command` it is handed from rebuilding a
97
+ * diverged bridge over whichever side holds the newer edit. A skill invocation is not a `command`
98
+ * either — nothing in a shell runs `/buddy-agent-harness:init`.
99
+ */
100
+ export type RepairAction = {
101
+ /** A shell invocation that completes the repair, or empty when none does. */
102
+ command: string
103
+ /** The imperative, in prose, always present and complete on its own. */
104
+ instruction: string
105
+ }
106
+
19
107
  export type Repair = {
20
- problem: BridgeProblem
108
+ problem: DoctorProblem
21
109
  /** What `doctor` prints in the `findings` row for this problem. */
22
110
  detail: string
23
111
  /**
24
- * The command that fixes it, for `doctor`'s own output. `path` is the repository-relative bridge
25
- * path; `cli` is how to invoke this tool.
112
+ * What fixes it, for `doctor`'s own output. `at` is where the finding is, in parts — the file
113
+ * alone for a bridge, the file, server, and field for an MCP finding; `cli` is how to invoke this
114
+ * tool. A repair that names only part of a locator reads it off `at` rather than splitting the
115
+ * rendered string, which no separator survives: a server may be named `io.github.foo`.
26
116
  */
27
- repair(path: string, cli: string): string
117
+ repair(at: Locator, cli: string): RepairAction
28
118
  /**
29
119
  * What the shipped skill tells an agent to do instead. A repair that rebuilds a bridge delegates
30
120
  * to the `init` skill rather than calling the `init` command, because rebuilding can move
31
121
  * user-authored skills and that judgment is the `init` skill's, not `doctor`'s.
32
122
  */
33
- skillRepair(path: string): string
123
+ skillRepair(at: Locator): string
124
+ }
125
+
126
+ /**
127
+ * One row of a repair table. The problem is the key it is filed under rather than a field on it, so
128
+ * a table typed `Record<…Problem, RepairRow>` cannot be written with a row missing: adding a
129
+ * variant to one of the unions above fails to compile until its row exists. That is the only place
130
+ * the invariant can be held — a union is not enumerable at runtime, so no test can walk it, and a
131
+ * lookup that asserts its own completeness reads `.detail` off `undefined` the first time it is
132
+ * wrong.
133
+ */
134
+ export type RepairRow = Omit<Repair, 'problem'>
135
+
136
+ /**
137
+ * A table as the list `doctor` reports, in the order it was written. The keys of a
138
+ * `Record<P, RepairRow>` are `P` by construction, which is what the assertion says; nothing here
139
+ * claims a row exists.
140
+ */
141
+ function repairsOf<Problem extends DoctorProblem>(table: Record<Problem, RepairRow>): readonly Repair[] {
142
+ return (Object.entries(table) as [Problem, RepairRow][]).map(([problem, row]) => ({ problem, ...row }))
34
143
  }
35
144
 
36
145
  /** How the command names itself. */
@@ -70,108 +179,403 @@ export function renderSkillLauncher(subcommand: string): string {
70
179
  return `#!/usr/bin/env node
71
180
  // Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand.
72
181
  import { dirname, join } from 'node:path'
73
- import { fileURLToPath } from 'node:url'
182
+ import { fileURLToPath, pathToFileURL } from 'node:url'
74
183
 
75
184
  // <package>/skills/<skill>/scripts/${subcommand}.mjs: four levels up is the package root.
76
185
  const packageRoot = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
186
+ const { run } = await import(pathToFileURL(join(packageRoot, 'dist', 'cli.mjs')).href)
77
187
 
78
- process.argv.splice(2, 0, '${subcommand}')
79
- await import(join(packageRoot, 'bin', '${commandInvocation}.mjs'))
188
+ // The subcommand is composed into a fresh argv rather than spliced into the global one, so nothing
189
+ // outside this file observes the rewrite. Applied only when non-zero, so a usage code clibuilder
190
+ // recorded itself is not overwritten by the zero \`run\` returns on that path.
191
+ const code = await run([...process.argv.slice(0, 2), '${subcommand}', ...process.argv.slice(2)])
192
+ if (code !== 0) process.exitCode = code
80
193
  `
81
194
  }
82
195
 
196
+ /**
197
+ * How the skill hands a repair to `repair`. Bridge and instruction repairs go to `init`, which
198
+ * writes both kinds of bridge in the first place; configuration that is present and wrong goes
199
+ * here, because no `init` flag corrects a file the user already wrote.
200
+ */
201
+ export const repairSkillInvocation = '/buddy-agent-harness:repair'
202
+
83
203
  /** How the skill hands a repair back to `init`. */
84
204
  export const initSkillInvocation = '/buddy-agent-harness:init'
85
205
 
86
- export const doctorRepairs: readonly Repair[] = [
87
- {
88
- problem: 'no-canonical',
206
+ /** Where the user authors the golden MCP server set, named in every MCP repair that points at it. */
207
+ const goldenSet = '.agents/buddy-agent-harness/mcp.toml'
208
+
209
+ /** The skills bridges, reported in `bridges`. */
210
+ const bridgeTable: Record<BridgeProblem, RepairRow> = {
211
+ 'no-canonical': {
89
212
  detail: 'the canonical skill directory does not exist, so no bridge can resolve',
90
- repair: (_path, cli) => `${cli} init`,
213
+ repair: (_at, cli) => ({
214
+ command: `${cli} init`,
215
+ instruction: `run \`${cli} init\` to create .agents/skills and the bridges into it`,
216
+ }),
91
217
  skillRepair: () => `run \`${initSkillInvocation}\`, which creates \`.agents/skills\` and the bridges`,
92
218
  },
93
- {
94
- problem: 'missing',
219
+ missing: {
95
220
  detail: 'no bridge at this path — the harness sees zero project skills',
96
- repair: (_path, cli) => `${cli} init`,
221
+ repair: ({ file }, cli) => ({
222
+ command: `${cli} init`,
223
+ instruction: `run \`${cli} init\` to create the bridge at ${file}`,
224
+ }),
97
225
  skillRepair: () => `run \`${initSkillInvocation}\``,
98
226
  },
99
- {
100
- problem: 'degraded',
227
+ degraded: {
101
228
  detail: 'expected a directory but found a regular file — checkout without core.symlinks',
102
- repair: (_path, cli) => `${cli} init --copy --force`,
103
- skillRepair: () => `run \`${initSkillInvocation} --copy --force\``,
229
+ repair: ({ file }, cli) => ({
230
+ command: `${cli} init --copy --force ${file}`,
231
+ instruction: `run \`${cli} init --copy --force ${file}\` to rebuild ${file} as a real directory`,
232
+ }),
233
+ skillRepair: ({ file }) => `run \`${initSkillInvocation} --copy --force ${file}\``,
104
234
  },
105
- {
106
- problem: 'stale',
235
+ stale: {
107
236
  detail: 'symlink does not resolve to .agents/skills',
108
- repair: (_path, cli) => `${cli} init --force`,
109
- skillRepair: () => `run \`${initSkillInvocation} --force\``,
237
+ repair: ({ file }, cli) => ({
238
+ command: `${cli} init --force ${file}`,
239
+ instruction: `run \`${cli} init --force ${file}\` to repoint ${file} at .agents/skills`,
240
+ }),
241
+ skillRepair: ({ file }) => `run \`${initSkillInvocation} --force ${file}\``,
110
242
  },
111
- {
112
- problem: 'diverged-bridge',
243
+ 'diverged-bridge': {
113
244
  detail: 'only the bridge changed since the two last agreed — an agent wrote through the copy',
114
- repair: (path, cli) =>
115
- `replace .agents/skills with ${path} to keep the newer edit and then run ${cli} init --force`,
116
- skillRepair: (path) =>
117
- `replace .agents/skills with ${path} to keep the newer edit, then run \`${initSkillInvocation} --force\``,
245
+ // No command, unlike `diverged-canonical`, and the asymmetry is real: `init` only ever builds a
246
+ // bridge *from* the canonical directory, so no flag promotes the bridge's newer content back
247
+ // into it. Deciding to keep that side is the caller's, and the rebuild is what follows.
248
+ repair: ({ file }, cli) => ({
249
+ command: '',
250
+ instruction: `replace .agents/skills with ${file} to keep the newer edit, then run \`${cli} init --force ${file}\``,
251
+ }),
252
+ skillRepair: ({ file }) =>
253
+ `replace .agents/skills with ${file} to keep the newer edit, then run \`${initSkillInvocation} --force ${file}\``,
118
254
  },
119
- {
120
- problem: 'diverged-canonical',
255
+ 'diverged-canonical': {
121
256
  detail: 'only .agents/skills changed since the two last agreed — the copy is stale',
122
- repair: (_path, cli) => `${cli} init --copy --force`,
123
- skillRepair: () => `run \`${initSkillInvocation} --copy --force\``,
257
+ repair: ({ file }, cli) => ({
258
+ command: `${cli} init --copy --force ${file}`,
259
+ instruction: `run \`${cli} init --copy --force ${file}\` to rebuild ${file} from the newer .agents/skills`,
260
+ }),
261
+ skillRepair: ({ file }) => `run \`${initSkillInvocation} --copy --force ${file}\``,
124
262
  },
125
- {
126
- problem: 'diverged-both',
263
+ 'diverged-both': {
127
264
  detail: 'both sides changed since they last agreed — rebuilding would discard one of them',
128
- repair: (path) => `git diff --no-index .agents/skills ${path} and reconcile by hand`,
129
- skillRepair: (path) => `run \`git diff --no-index .agents/skills ${path}\` and reconcile by hand`,
265
+ repair: ({ file }) => ({
266
+ command: '',
267
+ instruction: `reconcile .agents/skills with ${file} by hand — rebuilding would discard one of them; \`git diff --no-index .agents/skills ${file}\` shows what differs`,
268
+ }),
269
+ skillRepair: ({ file }) => `run \`git diff --no-index .agents/skills ${file}\` and reconcile by hand`,
130
270
  },
131
- {
132
- problem: 'diverged-unknown',
271
+ 'diverged-unknown': {
133
272
  detail: 'contents differ and no commit where they agreed was found — which side moved is unknown',
134
- repair: (path) => `git diff --no-index .agents/skills ${path} and reconcile by hand`,
135
- skillRepair: (path) => `run \`git diff --no-index .agents/skills ${path}\` and reconcile by hand`,
273
+ repair: ({ file }) => ({
274
+ command: '',
275
+ instruction: `reconcile .agents/skills with ${file} by hand — which side moved is unknown; \`git diff --no-index .agents/skills ${file}\` shows what differs`,
276
+ }),
277
+ skillRepair: ({ file }) => `run \`git diff --no-index .agents/skills ${file}\` and reconcile by hand`,
136
278
  },
137
- {
138
- problem: 'unpinned-copy',
279
+ 'unpinned-copy': {
139
280
  detail: 'tracked copy without the skip-worktree bit — the tree is dirty with content that must not be committed',
140
281
  // The index entry is the tracked symlink on a Windows checkout but the individual files in a
141
282
  // committed copy, so the paths are read back from git rather than assumed.
142
- repair: (path) => `git ls-files -z ${path} | xargs -0 git update-index --skip-worktree`,
143
- skillRepair: (path) => `run \`git ls-files -z ${path} | xargs -0 git update-index --skip-worktree\``,
283
+ repair: ({ file }) => ({
284
+ command: `git ls-files -z ${file} | xargs -0 git update-index --skip-worktree`,
285
+ instruction: `run \`git ls-files -z ${file} | xargs -0 git update-index --skip-worktree\` to restore the skip-worktree bit`,
286
+ }),
287
+ skillRepair: ({ file }) => `run \`git ls-files -z ${file} | xargs -0 git update-index --skip-worktree\``,
288
+ },
289
+ }
290
+
291
+ export const bridgeRepairs: readonly Repair[] = repairsOf(bridgeTable)
292
+
293
+ /**
294
+ * The instruction bridges, reported in `instructions`. Every repair is the `init` skill: these are
295
+ * files a person wrote, or files carrying content beside the bridge, and deciding what to preserve
296
+ * while restoring the bridge is judgment no flag carries. `repair` therefore names the skill in
297
+ * both places rather than pretending a shell command exists.
298
+ */
299
+ const instructionTable: Record<InstructionProblem, RepairRow> = {
300
+ 'no-instructions': {
301
+ detail: 'no AGENTS.md at the repository root, so every instruction bridge points at nothing',
302
+ repair: () => ({
303
+ command: '',
304
+ instruction: `hand this to \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`,
305
+ }),
306
+ skillRepair: () => `run \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`,
144
307
  },
145
- ]
308
+ 'instructions-missing': {
309
+ detail: 'no instruction bridge at this path — the harness reads none of AGENTS.md',
310
+ repair: ({ file }) => ({
311
+ command: '',
312
+ instruction: `hand ${file} to \`${initSkillInvocation}\`, which writes the bridge into it`,
313
+ }),
314
+ skillRepair: () => `run \`${initSkillInvocation}\``,
315
+ },
316
+ 'instructions-unbridged': {
317
+ detail: 'the file is present but names AGENTS.md nowhere — the harness reads none of it',
318
+ repair: ({ file }) => ({
319
+ command: '',
320
+ instruction: `hand ${file} to \`${initSkillInvocation}\`, which adds the bridge without discarding what the file already says`,
321
+ }),
322
+ skillRepair: () =>
323
+ `run \`${initSkillInvocation}\`, which adds the bridge without discarding what the file already says`,
324
+ },
325
+ 'instructions-unreadable': {
326
+ detail: 'the settings file does not parse, so the harness reads none of it',
327
+ repair: ({ file }) => ({
328
+ command: '',
329
+ instruction: `fix the JSON in ${file} by hand, then hand it to \`${initSkillInvocation}\``,
330
+ }),
331
+ skillRepair: () => `fix the JSON by hand, then run \`${initSkillInvocation}\``,
332
+ },
333
+ }
334
+
335
+ export const instructionRepairs: readonly Repair[] = repairsOf(instructionTable)
336
+
337
+ /**
338
+ * Configuration that is present and wrong. Detected here like everything else, but repaired by the
339
+ * `repair` skill rather than by `init` — which is why it is a section of its own.
340
+ */
341
+ const configurationTable: Record<ConfigurationFault, RepairRow> = {
342
+ 'deprecated-harness': {
343
+ detail:
344
+ 'a projection under a harness name that has been superseded — the replacement reads .agents/skills natively and needs no projection at all',
345
+ repair: ({ file }) => ({
346
+ command: '',
347
+ instruction: `remove ${file} and enable the harness that replaced it — \`${repairSkillInvocation}\` offers the correction`,
348
+ }),
349
+ skillRepair: () => `run \`${repairSkillInvocation}\``,
350
+ },
351
+ 'ignored-bridge': {
352
+ detail: 'a .gitignore rule matches this bridge — an untracked bridge swallows a real edit silently',
353
+ repair: ({ file }) => ({
354
+ command: '',
355
+ instruction: `narrow or remove the .gitignore rule matching ${file} — \`${repairSkillInvocation}\` offers the correction`,
356
+ }),
357
+ skillRepair: () => `run \`${repairSkillInvocation}\``,
358
+ },
359
+ 'unread-local-override': {
360
+ detail: 'no harness reads this filename, so everything in it is invisible to every agent',
361
+ repair: ({ file }) => ({
362
+ command: '',
363
+ instruction: `move ${file} to CLAUDE.local.md, or consolidate it into AGENTS.md — \`${repairSkillInvocation}\` offers the correction`,
364
+ }),
365
+ skillRepair: () => `run \`${repairSkillInvocation}\``,
366
+ },
367
+ 'unloadable-skill': {
368
+ detail: 'frontmatter that does not parse, or no description — either one makes a harness skip the skill outright',
369
+ repair: ({ file }) => ({
370
+ command: '',
371
+ instruction: `quote the description in ${file}, or add one — \`${repairSkillInvocation}\` offers the correction`,
372
+ }),
373
+ skillRepair: () => `run \`${repairSkillInvocation}\``,
374
+ },
375
+ }
376
+
377
+ const configurationRepairs: readonly Repair[] = repairsOf(configurationTable)
378
+
379
+ /**
380
+ * The golden MCP server set against the harness copies of it.
381
+ *
382
+ * Every repair names a **locator** rather than a file — `.cursor/mcp.json#servers.linear.command` —
383
+ * because a file holding twenty servers is not an address. The locator never carries a value: not
384
+ * a whole one, and not a prefix. `doctor` is safe to run from a session-start hook, so a value it
385
+ * echoes lands in agent context on every session and from there into transcripts, and `sk-ab…`
386
+ * leaks into exactly the same place the whole string would.
387
+ */
388
+ const mcpTable: Record<McpProblem, RepairRow> = {
389
+ 'mcp-golden-unreadable': {
390
+ detail:
391
+ 'the golden MCP set does not parse — the locator gives the line and column, and nothing else can be said about it',
392
+ repair: (at) => ({ command: '', instruction: `fix the TOML at ${locatorText(at)}` }),
393
+ skillRepair: (at) =>
394
+ `fix the TOML at ${locatorText(at)} by hand — the reported line and column are all that can be quoted, because the parser's own message repeats the offending line and that line is the one holding the credential`,
395
+ },
396
+ 'mcp-target-unreadable': {
397
+ detail:
398
+ 'this harness config does not parse, so the harness starts none of its servers and nothing in it can be compared',
399
+ repair: (at) => ({ command: '', instruction: `fix the syntax of ${locatorText(at)}` }),
400
+ skillRepair: (at) => `fix the syntax of ${locatorText(at)} by hand`,
401
+ },
402
+ 'mcp-unprojected': {
403
+ detail: 'the golden set declares this server and the harness config does not carry it',
404
+ repair: ({ file, server }) => ({
405
+ command: '',
406
+ instruction: `add the server ${server} to ${file}, or drop it from the golden set`,
407
+ }),
408
+ skillRepair: (at) =>
409
+ `add the server at ${locatorText(at)} to that file from its golden entry, or drop it from the golden set`,
410
+ },
411
+ 'mcp-undeclared': {
412
+ detail: 'the harness config carries this server and the golden set does not declare it',
413
+ repair: ({ file, server }) => ({
414
+ command: '',
415
+ instruction: `add the server ${server} to ${goldenSet}, or drop it from ${file}`,
416
+ }),
417
+ skillRepair: (at) =>
418
+ `copy the server at ${locatorText(at)} into ${goldenSet}, refusing any literal credential it carries, or drop it from ${at.file}`,
419
+ },
420
+ 'mcp-diverged-target': {
421
+ detail: 'only the harness config changed since the two last agreed — the edit was made through the copy',
422
+ repair: (at) => ({ command: '', instruction: `reconcile the value at ${locatorText(at)} back into ${goldenSet}` }),
423
+ skillRepair: (at) => `reconcile the value at ${locatorText(at)} back into ${goldenSet}, field by field`,
424
+ },
425
+ 'mcp-diverged-golden': {
426
+ detail: 'only the golden set changed since the two last agreed — the harness copy is stale',
427
+ repair: (at) => ({ command: '', instruction: `update ${locatorText(at)} from the golden entry` }),
428
+ skillRepair: (at) => `update ${locatorText(at)} from the golden entry`,
429
+ },
430
+ 'mcp-diverged-both': {
431
+ detail: 'both sides changed since they last agreed — merging either way would discard the other',
432
+ repair: (at) => ({ command: '', instruction: `reconcile ${locatorText(at)} against ${goldenSet} by hand` }),
433
+ skillRepair: (at) =>
434
+ `reconcile ${locatorText(at)} against ${goldenSet} by hand — never merge a three-way conflict automatically`,
435
+ },
436
+ 'mcp-diverged-unknown': {
437
+ detail:
438
+ 'the two disagree and no baseline says which side moved — neither history nor a last-projected record covers this server',
439
+ repair: (at) => ({ command: '', instruction: `compare ${locatorText(at)} against ${goldenSet} by hand` }),
440
+ skillRepair: (at) => `compare ${locatorText(at)} against ${goldenSet} by hand`,
441
+ },
442
+ 'mcp-literal-secret': {
443
+ detail: 'a credential-bearing field holds a literal rather than a reference to an environment variable',
444
+ repair: (at) => ({
445
+ command: '',
446
+ instruction: `move the value at ${locatorText(at)} into an environment variable and reference it`,
447
+ }),
448
+ skillRepair: (at) =>
449
+ `move the value at ${locatorText(at)} into an environment variable and reference it — read the value from the file, never from this report, and never repeat it back`,
450
+ },
451
+ 'mcp-committed-secret': {
452
+ detail:
453
+ 'a credential-bearing field holds a literal in a git-tracked file — the credential is committed, and moving it does not un-commit it',
454
+ repair: (at) => ({
455
+ command: '',
456
+ instruction: `rotate the credential behind ${locatorText(at)}, then reference it from an environment variable`,
457
+ }),
458
+ skillRepair: (at) =>
459
+ `rotate the credential behind ${locatorText(at)} at its issuer, then reference it from an environment variable — it is in the repository's history, so moving it is not enough`,
460
+ },
461
+ }
462
+
463
+ const mcpRepairs: readonly Repair[] = repairsOf(mcpTable)
464
+
465
+ /**
466
+ * Configuration only one harness can read. Every repair here is a **conversion**, and every one of
467
+ * them is the `init` skill's: consolidating what a repository already has is exactly its remit, and
468
+ * the content is the user's, so it is offered and never written on sight. `repair` owns none of
469
+ * them — it corrects configuration that is wrong, and none of this is wrong.
470
+ *
471
+ * `nonstandard-subagent` is the exception that names no owner. There is no cross-harness subagent
472
+ * format to convert to, so the honest report is that the gap exists, not a repair that cannot run.
473
+ */
474
+ const nonstandardTable: Record<NonstandardProblem, RepairRow> = {
475
+ 'nonstandard-instructions': {
476
+ detail: 'instruction content only one harness reads — AGENTS.md carries the same prose to all of them',
477
+ repair: ({ file }) => ({
478
+ command: '',
479
+ instruction: `consolidate ${file} into AGENTS.md and leave a generated bridge in its place — \`${initSkillInvocation}\` offers the consolidation`,
480
+ }),
481
+ skillRepair: ({ file }) => `hand ${file} to \`${initSkillInvocation}\`, which consolidates it into AGENTS.md`,
482
+ },
483
+ 'nonstandard-rule': {
484
+ detail: 'a path-scoped rule only one harness reads — a skill reaches every harness where the scoping is incidental',
485
+ repair: ({ file }) => ({
486
+ command: '',
487
+ instruction: `convert ${file} into a skill under .agents/skills where its guidance is not tied to the paths it names — \`${initSkillInvocation}\` offers the conversion; keep it where the path scoping is the point`,
488
+ }),
489
+ skillRepair: ({ file }) =>
490
+ `hand ${file} to \`${initSkillInvocation}\` to convert into a skill, unless the path scoping is load-bearing`,
491
+ },
492
+ 'nonstandard-command': {
493
+ detail: 'a harness command file — a skill is the portable form, and this harness reads skills too',
494
+ repair: ({ file }) => ({
495
+ command: '',
496
+ instruction: `move ${file} to .agents/skills/<name>/SKILL.md — \`${initSkillInvocation}\` offers the move`,
497
+ }),
498
+ skillRepair: ({ file }) => `hand ${file} to \`${initSkillInvocation}\`, which moves it to .agents/skills`,
499
+ },
500
+ 'nonstandard-skill': {
501
+ detail: 'a skill under a harness directory rather than the canonical one, where only that harness finds it',
502
+ repair: ({ file }) => ({
503
+ command: '',
504
+ instruction: `move ${file} to .agents/skills and project it back where the harness needs one — \`${initSkillInvocation}\` offers the move`,
505
+ }),
506
+ skillRepair: ({ file }) => `hand ${file} to \`${initSkillInvocation}\`, which moves it to .agents/skills`,
507
+ },
508
+ 'nonstandard-subagent': {
509
+ detail:
510
+ 'a subagent definition with no cross-harness form — nothing outside this harness can read it, and no canonical form exists yet',
511
+ repair: ({ file }) => ({
512
+ command: '',
513
+ instruction: `leave ${file} where it is and record that it reaches one harness only — no portable form exists to convert it to`,
514
+ }),
515
+ skillRepair: ({ file }) =>
516
+ `leave ${file} in place — no portable form exists yet, so this is work for a person rather than a skill`,
517
+ },
518
+ }
146
519
 
147
- const repairsByProblem = new Map(doctorRepairs.map((entry) => [entry.problem, entry]))
520
+ export const nonstandardRepairs: readonly Repair[] = repairsOf(nonstandardTable)
148
521
 
149
- export function repairFor(problem: BridgeProblem): Repair {
150
- return repairsByProblem.get(problem) as Repair
522
+ /**
523
+ * All five sections, in the order `doctor` reports them. Annotated over the whole union rather than
524
+ * inferred, so a problem added to any of the four fails here too if its section was left alone.
525
+ */
526
+ const doctorTable: Record<DoctorProblem, RepairRow> = {
527
+ ...bridgeTable,
528
+ ...instructionTable,
529
+ ...configurationTable,
530
+ ...mcpTable,
531
+ ...nonstandardTable,
532
+ }
533
+
534
+ export const doctorRepairs: readonly Repair[] = repairsOf(doctorTable)
535
+
536
+ /** The repair for one problem. Total by construction: the table is keyed by the union. */
537
+ export function repairFor(problem: DoctorProblem): Repair {
538
+ return { problem, ...doctorTable[problem] }
151
539
  }
152
540
 
153
541
  export const doctorSkill = {
154
542
  name: 'doctor',
155
543
  description:
156
- '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.',
544
+ '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.',
157
545
  } as const
158
546
 
159
547
  /** Written into the generated skill so a reader knows not to edit it in place. */
160
548
  export const generatedSkillWarning =
161
549
  '<!-- Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand. -->'
162
550
 
551
+ /**
552
+ * A file the generator writes under `skills/doctor/`, path relative to that directory. The skill is
553
+ * split the way `init`'s is: a lean `SKILL.md` an agent always reads, and reference pages it loads
554
+ * only for the family it is acting on. One flat file made every reader of one finding pay for the
555
+ * prose behind all five.
556
+ */
557
+ export type GeneratedDoc = { path: string; content: string }
558
+
559
+ /** A repair table, rendered for the skill's reader. */
560
+ function repairTable(repairs: readonly Repair[]): string {
561
+ // A repair may itself contain a pipe, which would otherwise end the table cell early.
562
+ const cell = (value: string) => value.replaceAll('|', '\\|')
563
+ return `| Finding | What it means | Repair |
564
+ | --- | --- | --- |
565
+ ${repairs
566
+ .map((entry) => `| \`${entry.problem}\` | ${entry.detail} | ${cell(entry.skillRepair({ file: '<path>' }))} |`)
567
+ .join('\n')}`
568
+ }
569
+
163
570
  /**
164
571
  * The shipped `doctor` skill, rendered from the same table the command prints. `version` is the
165
572
  * package version the skill ships with; it pins the `npx` invocation so the table and the CLI that
166
573
  * produced it stay on the same breaking line.
574
+ *
575
+ * The finding tables are NOT here. They live in the reference pages below, and this file carries
576
+ * only what every reader needs: how to run it, how to read the report, and where to go next.
167
577
  */
168
578
  export function renderDoctorSkill(version: string): string {
169
- // A repair may itself contain a pipe, which would otherwise end the table cell early.
170
- const cell = (value: string) => value.replaceAll('|', '\\|')
171
- const rows = doctorRepairs
172
- .map((entry) => `| \`${entry.problem}\` | ${entry.detail} | ${cell(entry.skillRepair('<path>'))} |`)
173
- .join('\n')
174
-
175
579
  return `---
176
580
  name: ${doctorSkill.name}
177
581
  description: ${doctorSkill.description}
@@ -181,7 +585,7 @@ ${generatedSkillWarning}
181
585
 
182
586
  # Harness Doctor
183
587
 
184
- \`.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.
588
+ 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.
185
589
 
186
590
  Diagnose it:
187
591
 
@@ -195,32 +599,242 @@ The command is read-only. It never repairs anything, so it is safe to run at any
195
599
 
196
600
  ## Reading the report
197
601
 
198
- \`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\`.
602
+ \`bridges\` lists every skills bridge \`init\` would create for this repository, each with a \`status\` of \`ok\`, \`missing\`, \`degraded\`, \`stale\`, or \`diverged\`.
603
+
604
+ \`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.
605
+
606
+ \`findings\` explains each problem and carries more than the two sections above: the configuration, MCP, and non-standard findings have no section of their own, because they are about files rather than about bridges. \`help\` carries each repair, one row per distinct repair, with two columns:
607
+
608
+ - \`command\` — a shell invocation that, run exactly as given, **completes** the repair.
609
+ - \`instruction\` — the same repair in the imperative, always present and complete on its own.
610
+
611
+ \`command\` is empty whenever no single invocation does the job, and that emptiness is the signal: act on \`instruction\` and do not assemble a command out of it. A runnable invocation quoted *inside* an \`instruction\` is not the repair either — \`diverged-both\` names \`git diff --no-index\` because the diff shows you what differs, not because running it reconciles anything. Apply the repair, then re-run \`doctor\`.
199
612
 
200
- \`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.
613
+ Nothing in \`help\` is wrapped. An earlier version prefixed every repair with \`Run\`, which read as an instruction to paste prose into a shell.
614
+
615
+ 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. Every such repair carries an empty \`command\`: a skill invocation has no shell equivalent at all.
201
616
 
202
617
  When every bridge resolves, \`findings\` says so outright rather than being empty.
203
618
 
204
619
  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\`.
205
620
 
206
- ## Findings and their repairs
621
+ ## Where the detail is
622
+
623
+ Every \`problem\` name routes to exactly one page. Load the page for the finding in front of you and leave the rest unread — the tables are long, and reading four families to act on one is what this split exists to stop.
624
+
625
+ | Page | Load it for |
626
+ | --- | --- |
627
+ | \`references/bridges.md\` | any \`bridges\` row that is not \`ok\` |
628
+ | \`references/instructions.md\` | any \`instructions\` row that is not \`ok\` |
629
+ | \`references/configuration.md\` | a finding about the configuration around the bridges rather than a bridge |
630
+ | \`references/mcp.md\` | any finding whose path is an MCP locator — **always** before acting on a credential finding |
631
+ | \`references/nonstandard.md\` | a finding about configuration only one harness can read |
632
+ | \`references/harnesses/<name>.md\` | the paths and files one named harness uses |
633
+
634
+ ## Rules
635
+
636
+ - 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.
637
+ - Edit skills at \`.agents/skills/<name>/SKILL.md\`. Editing through a bridge is only safe when that bridge is a symlink.
638
+ - Do not add bridges to \`.gitignore\`. An untracked bridge swallows a real edit silently.
639
+ - 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.
640
+ - 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.
641
+ `
642
+ }
643
+
644
+ /** Where each kind of harness-exclusive artifact is meant to end up, for the per-harness tables. */
645
+ function conversionOf(kind: NonstandardKind): string {
646
+ switch (kind) {
647
+ case 'instructions':
648
+ return '`AGENTS.md`, with a generated bridge left behind'
649
+ case 'rule':
650
+ return 'a skill, where the path scoping is incidental'
651
+ case 'command':
652
+ return '`.agents/skills/<name>/SKILL.md`'
653
+ case 'skill':
654
+ return '`.agents/skills`, projected back if needed'
655
+ case 'subagent':
656
+ return 'nothing yet — no cross-harness format exists'
657
+ }
658
+ }
659
+
660
+ /** How one harness's scope reads, for its generated reference page. */
661
+ function scopeRows(scope: HarnessScope): string {
662
+ const bridge = scope.instructionBridge
663
+ const mcp = scope.mcpConfig
664
+ return [
665
+ `| detection directory | \`${scope.detect}\` |`,
666
+ `| skills projection | ${scope.skillsDirectory ? `\`${scope.skillsDirectory}\` — written by \`init\`` : 'none — reads `.agents/skills` natively'} |`,
667
+ `| instruction bridge | ${
668
+ bridge === undefined
669
+ ? 'none'
670
+ : bridge.kind === 'import'
671
+ ? `\`${bridge.path}\` — an import of \`AGENTS.md\``
672
+ : `\`${bridge.path}\` — \`AGENTS.md\` in the \`${bridge.key}\` entry`
673
+ } |`,
674
+ `| MCP configuration | ${mcp === undefined ? 'none' : `\`${mcp.path}\` — the \`${mcp.key}\` key, ${mcp.format}${mcp.shared ? ', shared with other settings' : ''}`} |`,
675
+ ].join('\n')
676
+ }
677
+
678
+ /**
679
+ * One page per harness, generated from the registry rather than written by hand. These are the
680
+ * paths the detectors actually use, so a page written beside them would drift the first time a
681
+ * harness moved a file. Editorial judgment about a harness — what is contested, what not to
682
+ * generate — stays in the `init` skill's own reference pages, which these link to where one exists.
683
+ */
684
+ function harnessPage(harness: Harness, hasInitReference: boolean): GeneratedDoc {
685
+ const deprecated =
686
+ harness.deprecated === undefined
687
+ ? ''
688
+ : `\n> **Superseded by \`${harness.deprecated}\`.** The legacy paths still work, so a projection here keeps resolving and \`doctor\` reports the name as deprecated rather than broken. New repositories should enable \`${harness.deprecated}\`.\n`
689
+
690
+ const user =
691
+ harness.user === undefined
692
+ ? '\nNo user-scope paths are primary-sourced for this harness, so `doctor` describes none.\n'
693
+ : `\n## User scope
694
+
695
+ Described, never written: \`init\` and \`doctor\` both work inside a repository.
696
+
697
+ | What | Path |
698
+ | --- | --- |
699
+ ${scopeRows(harness.user)}
700
+ `
701
+
702
+ const artifacts = harness.project.nonstandard ?? []
703
+ const nonstandard =
704
+ artifacts.length === 0
705
+ ? ''
706
+ : `\n## Configuration only this harness reads
207
707
 
208
- | Finding | What it means | Repair |
708
+ Reported by \`doctor\` so it can be converted; see \`../nonstandard.md\` for what each conversion is.
709
+
710
+ | Path | Kind | Converts to |
209
711
  | --- | --- | --- |
210
- ${rows}
712
+ ${artifacts
713
+ .map(
714
+ (artifact) =>
715
+ `| \`${artifact.path}${artifact.shape === 'directory' ? '/' : ''}\` | ${artifact.kind} | ${conversionOf(artifact.kind)} |`,
716
+ )
717
+ .join('\n')}
718
+ `
719
+
720
+ const editorial = hasInitReference
721
+ ? `\n## Judgment about this harness\n\nWhat to generate for it, what to leave alone, and which claims are contested: \`../../../init/references/harnesses/${harness.name}.md\`. That page is hand-written and is the one to read before writing anything for this harness.\n`
722
+ : ''
723
+
724
+ return {
725
+ path: `references/harnesses/${harness.name}.md`,
726
+ content: `${generatedSkillWarning}
727
+
728
+ # ${harness.name}
729
+ ${deprecated}
730
+ ## Project scope
731
+
732
+ Where \`doctor\` looks inside a repository.
733
+
734
+ | What | Path |
735
+ | --- | --- |
736
+ ${scopeRows(harness.project)}
737
+ ${user}${nonstandard}${editorial}`,
738
+ }
739
+ }
740
+
741
+ /**
742
+ * The reference pages, split by finding family plus one per harness. `initReferences` names the
743
+ * harnesses the `init` skill has a hand-written page for; it is read off the filesystem by the
744
+ * generator rather than written down here, so a page added there is linked without a second edit.
745
+ */
746
+ export function renderDoctorReferences(initReferences: ReadonlySet<string>): GeneratedDoc[] {
747
+ return [
748
+ {
749
+ path: 'references/bridges.md',
750
+ content: `${generatedSkillWarning}
751
+
752
+ # Skills bridge findings
753
+
754
+ ${repairTable(bridgeRepairs)}
211
755
 
212
756
  Substitute the reported bridge path for \`<path>\`.
213
757
 
214
758
  ## The Windows case
215
759
 
216
- 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.
760
+ 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 every bridge as a real directory on that machine; name one bridge to rebuild only that one.
217
761
 
218
762
  A copy is a snapshot rather than a live projection, and an agent that edits a skill through it writes into the copy instead of into \`.agents/skills\`. That is what the \`diverged\` findings catch.
763
+ `,
764
+ },
765
+ {
766
+ path: 'references/instructions.md',
767
+ content: `${generatedSkillWarning}
219
768
 
220
- ## Rules
769
+ # Instruction bridge findings
221
770
 
222
- - 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.
223
- - Edit skills at \`.agents/skills/<name>/SKILL.md\`. Editing through a bridge is only safe when that bridge is a symlink.
224
- - Do not add bridges to \`.gitignore\`. An untracked bridge swallows a real edit silently.
225
- `
771
+ ${repairTable(instructionRepairs)}
772
+
773
+ \`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.
774
+
775
+ 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.
776
+ `,
777
+ },
778
+ {
779
+ path: 'references/configuration.md',
780
+ content: `${generatedSkillWarning}
781
+
782
+ # Configuration findings
783
+
784
+ The bridges resolve, and the configuration around them is still wrong: a superseded harness name, a git-ignored bridge, a local-override file nothing reads, a skill whose frontmatter makes every harness skip it. None of these is an \`init\` flag — \`init\` consolidates and creates, and will not correct a file the user already wrote. They go to the \`repair\` skill, which offers each correction with its before and after and writes only what is approved.
785
+
786
+ ${repairTable(configurationRepairs)}
787
+ `,
788
+ },
789
+ {
790
+ path: 'references/mcp.md',
791
+ content: `${generatedSkillWarning}
792
+
793
+ # MCP findings
794
+
795
+ A repository may keep a **golden MCP server set** at \`${goldenSet}\` — one canonical entry per server, in the superset of fields the supported hosts accept, written by the user. Where it exists, \`doctor\` compares it against each harness's own MCP configuration and reports how the two have drifted. **No golden set means no MCP drift findings at all**, and a harness with no MCP file yet has nothing that could have drifted.
796
+
797
+ Comparison is semantic. Six config keys across three file formats means no two of these files are ever byte-equal, so each side is parsed into one model and the models are compared. A field the golden set leaves unset is never a difference, however a harness fills it in: a host restating its own default and a user's deliberate edit are indistinguishable there, and treating both as changes is what makes a golden set accumulate noise.
798
+
799
+ Each finding names a **locator**, not a file: \`.cursor/mcp.json#servers.linear.command\` is the server and field, and that is what you route on.
800
+
801
+ ${repairTable(mcpRepairs)}
802
+
803
+ ## Credentials
804
+
805
+ The two secret findings are the ones to handle carefully.
806
+
807
+ - **The report never contains the value.** It gives you the locator and stops. Read the value out of the file named in the locator, and do not repeat it into your reply, into a commit message, or into any other file. There is no truncated preview to work from because a truncated credential is a leaked credential in the same transcript.
808
+ - **\`mcp-committed-secret\` is not \`mcp-literal-secret\` with worse wording.** The file is tracked, so the value is in the repository's history and every clone already has it. Moving it into an environment variable fixes the working tree and changes nothing about that. Rotate it at its issuer first.
809
+ - **A reference passes.** \`\${LINEAR_TOKEN}\` and \`Bearer \${LINEAR_TOKEN}\` are the documented ways to write these fields and are never reported. The test is that shape, not how random the value looks.
810
+ - **An unreadable golden set is reported by position only.** The parser's own message quotes the line it failed on, and in this file that line is the one holding the credential — so neither the message nor the offending line is ever carried into the report. Open the file at the reported line and column.
811
+ `,
812
+ },
813
+ {
814
+ path: 'references/nonstandard.md',
815
+ content: `${generatedSkillWarning}
816
+
817
+ # Non-standard configuration
818
+
819
+ Configuration only one harness can read. Nothing here is broken — a Cursor rule does exactly what it says — which is why none of it is in the other four families. What these findings report is **reach**: guidance that lands in one tool and nowhere else, where nobody finds out except by noticing an agent behave differently somewhere else.
820
+
821
+ Each finding names the canonical form it converts to. The direction is always the same: move the content to a canonical source, and let the harness file be **generated** from it rather than authored beside it. A repository with none of these is the target; it is a direction to walk, not a gate to pass.
822
+
823
+ ${repairTable(nonstandardRepairs)}
824
+
825
+ ## Two of them need judgment, not a move
826
+
827
+ **A path-scoped rule may have nowhere to go.** \`AGENTS.md\` scopes by directory nesting and a skill is loaded on relevance, so neither reproduces "these globs and no others". Where the scoping is incidental — a rule that happens to name paths but says something generally true — a skill carries it everywhere. Where the scoping is the point, leave it, and the finding stays as the record of why.
828
+
829
+ **A subagent has no portable form at all.** No cross-harness format exists, so the finding is the gap rather than a repair. It names no skill, and it is work for a person if it is work at all.
830
+
831
+ ## What is not reported
832
+
833
+ A harness directory that is a **symlink** is a projection someone already made, not configuration authored here. An artifact is reported whether or not its harness is enabled: a \`.cursorrules\` in a repository nobody opens in Cursor is still instruction content \`AGENTS.md\` does not carry, and filtering by the enabled set would hide exactly the drift worth converting.
834
+
835
+ Hooks, LSP settings, and output styles are not covered yet. Their event names and shapes differ by harness with no safe projection, and half-reporting them would be worse than the silence.
836
+ `,
837
+ },
838
+ ...harnessRegistry.map((harness) => harnessPage(harness, initReferences.has(harness.name))),
839
+ ]
226
840
  }