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,8 +1,10 @@
1
- import { lstatSync, readFileSync, readlinkSync, statSync } from 'node:fs'
2
- import { dirname, join, relative } from 'node:path'
3
- import { type Harness, type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
1
+ import { lstatSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
4
+ import { linksTo } from '../skill-projection/skill-projection.ts'
5
+ import { diagnoseInstructions, type InstructionReport } from './diagnose-instructions.ts'
4
6
  import { filesUnder } from './directory-files.ts'
5
- import { type BridgeProblem, repairFor } from './doctor-guidance.ts'
7
+ import { type BridgeProblem, type DoctorProblem, type RepairAction, repairFor } from './doctor-guidance.ts'
6
8
  import { type DivergenceDirection, GitBridgeState } from './git-bridge-state.ts'
7
9
 
8
10
  export type BridgeKind = 'symlink' | 'copy' | 'file' | 'none'
@@ -18,9 +20,14 @@ export type BridgeReport = {
18
20
 
19
21
  export type BridgeFinding = {
20
22
  path: string
23
+ /**
24
+ * Which problem this is. Emitted so a caller routes on a name rather than by matching `detail`
25
+ * prose, which would break the moment the wording is improved.
26
+ */
27
+ problem: DoctorProblem
21
28
  detail: string
22
- /** The command that repairs this finding, already carrying the bridge path. */
23
- repair: string
29
+ /** What repairs this finding, already carrying the bridge path. Empty `command` means judgment. */
30
+ repair: RepairAction
24
31
  }
25
32
 
26
33
  export type DivergenceReport = {
@@ -37,6 +44,11 @@ export type DiagnoseOptions = {
37
44
 
38
45
  export type DiagnoseResult = {
39
46
  bridges: BridgeReport[]
47
+ /**
48
+ * The instruction bridges into `AGENTS.md`, kept out of `bridges` on purpose: a different `kind`
49
+ * and `status` vocabulary, and a repair that is the `init` skill rather than a command.
50
+ */
51
+ instructions: InstructionReport[]
40
52
  divergence: DivergenceReport[]
41
53
  findings: BridgeFinding[]
42
54
  }
@@ -61,11 +73,6 @@ function isDirectory(path: string): boolean {
61
73
  }
62
74
  }
63
75
 
64
- /** The same test `init` uses to decide a projection is already correct, plus that it still resolves. */
65
- function resolvesToCanonical(target: string, canonical: string): boolean {
66
- return readlinkSync(target) === relative(dirname(target), canonical) && isDirectory(target)
67
- }
68
-
69
76
  function sameContent(left: string, right: string): boolean {
70
77
  const leftFiles = filesUnder(left)
71
78
  const rightFiles = filesUnder(right)
@@ -93,8 +100,10 @@ function inspect(target: string, path: string, canonical: string, git: GitBridge
93
100
  // regular file holding the target path, and the harness silently loads nothing.
94
101
  case 'file':
95
102
  return { kind: 'file', status: 'degraded', problem: 'degraded' }
103
+ // The very test `init` uses to decide a projection is already correct, so a bridge `init` would
104
+ // leave alone never reports as one `doctor` wants rebuilt.
96
105
  case 'symlink':
97
- return resolvesToCanonical(target, canonical)
106
+ return linksTo(target, canonical)
98
107
  ? { kind: 'symlink', status: 'ok' }
99
108
  : { kind: 'symlink', status: 'stale', problem: 'stale' }
100
109
  default: {
@@ -112,16 +121,17 @@ function inspect(target: string, path: string, canonical: string, git: GitBridge
112
121
  }
113
122
 
114
123
  /**
115
- * Reports whether every bridge `init` would create for this repository still resolves into
116
- * `.agents/skills`. Read-only: nothing is created, moved, or repaired, so the caller decides what
117
- * to run from the repair each finding carries.
124
+ * Reports whether every bridge this repository needs still resolves: the skills bridges into
125
+ * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
126
+ * moved, or repaired, so the caller decides what to run from the repair each finding carries.
118
127
  */
119
128
  export function diagnoseBridges({ root, harnesses: preferred = [], cli }: DiagnoseOptions): DiagnoseResult {
120
129
  const canonical = join(root, '.agents', 'skills')
121
130
  const git = new GitBridgeState(root)
122
- const bridged = selectHarnesses(root, preferred).filter((harness): harness is Harness & { skillsDirectory: string } =>
123
- Boolean(harness.skillsDirectory),
124
- )
131
+ const selected = selectHarnesses(root, preferred)
132
+ const bridged = selected
133
+ .map((harness) => ({ name: harness.name, skillsDirectory: harness.project.skillsDirectory }))
134
+ .filter((harness): harness is { name: HarnessName; skillsDirectory: string } => Boolean(harness.skillsDirectory))
125
135
 
126
136
  const bridges: BridgeReport[] = []
127
137
  const divergence: DivergenceReport[] = []
@@ -129,7 +139,12 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
129
139
 
130
140
  if (!isDirectory(canonical)) {
131
141
  const { detail, repair } = repairFor('no-canonical')
132
- findings.push({ path: canonicalPath, detail, repair: repair(canonicalPath, cli) })
142
+ findings.push({
143
+ path: canonicalPath,
144
+ problem: 'no-canonical',
145
+ detail,
146
+ repair: repair({ file: canonicalPath }, cli),
147
+ })
133
148
  }
134
149
 
135
150
  for (const harness of bridged) {
@@ -139,9 +154,12 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
139
154
  if (inspection.direction) divergence.push({ path, direction: inspection.direction })
140
155
  if (inspection.problem) {
141
156
  const { detail, repair } = repairFor(inspection.problem)
142
- findings.push({ path, detail, repair: repair(path, cli) })
157
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
143
158
  }
144
159
  }
145
160
 
146
- return { bridges, divergence, findings }
161
+ const instructions = diagnoseInstructions(root, selected, cli)
162
+ findings.push(...instructions.findings)
163
+
164
+ return { bridges, instructions: instructions.instructions, divergence, findings }
147
165
  }
@@ -0,0 +1,145 @@
1
+ import { lstatSync, readFileSync, readlinkSync } from 'node:fs'
2
+ import { join, posix } from 'node:path'
3
+ import type { Harness, HarnessName } from '../harness-registry/harness-registry.ts'
4
+ import type { InstructionBridge } from '../harness-registry/instruction-bridge.ts'
5
+ import { agentsFileDirectories } from './agents-files.ts'
6
+ import type { BridgeFinding } from './diagnose-bridges.ts'
7
+ import { type InstructionProblem, repairFor } from './doctor-guidance.ts'
8
+ import { parseJsonWithComments } from './json-with-comments.ts'
9
+
10
+ /** What is on disk where an instruction bridge belongs. */
11
+ export type InstructionKind = 'import' | 'symlink' | 'settings-entry' | 'file' | 'none'
12
+ /**
13
+ * `unbridged` is the case a skills bridge has no equivalent of: the file is there, so nothing looks
14
+ * wrong, and it names `AGENTS.md` nowhere — a `CLAUDE.md` overwritten with real content, or a
15
+ * settings file another tool rewrote.
16
+ */
17
+ export type InstructionStatus = 'ok' | 'missing' | 'unbridged' | 'unreadable'
18
+
19
+ export type InstructionReport = {
20
+ harness: HarnessName
21
+ /** Repository-relative path of the file that carries the bridge. */
22
+ path: string
23
+ kind: InstructionKind
24
+ status: InstructionStatus
25
+ }
26
+
27
+ export const canonicalInstructions = 'AGENTS.md'
28
+
29
+ type Inspection = {
30
+ kind: InstructionKind
31
+ status: InstructionStatus
32
+ problem?: InstructionProblem
33
+ }
34
+
35
+ /**
36
+ * An import bridges the `AGENTS.md` beside it and nothing else, so the check is per directory
37
+ * holding one rather than per harness. A line of its own is enough: the harness reference allows
38
+ * Claude-specific notes below the import, and a file that carries the import plus notes still
39
+ * bridges.
40
+ */
41
+ function inspectImport(target: string): Inspection {
42
+ let stats: ReturnType<typeof lstatSync>
43
+ try {
44
+ stats = lstatSync(target)
45
+ } catch {
46
+ return { kind: 'none', status: 'missing', problem: 'instructions-missing' }
47
+ }
48
+
49
+ if (stats.isSymbolicLink()) {
50
+ return readlinkSync(target) === canonicalInstructions
51
+ ? { kind: 'symlink', status: 'ok' }
52
+ : { kind: 'symlink', status: 'unbridged', problem: 'instructions-unbridged' }
53
+ }
54
+
55
+ const imported = readFileSync(target, 'utf8')
56
+ .split('\n')
57
+ .some((line) => line.trim() === `@${canonicalInstructions}`)
58
+ return imported
59
+ ? { kind: 'import', status: 'ok' }
60
+ : { kind: 'file', status: 'unbridged', problem: 'instructions-unbridged' }
61
+ }
62
+
63
+ /** Reads a dotted key out of parsed JSON without asserting anything about the rest of the file. */
64
+ function valueAt(settings: unknown, key: string): unknown {
65
+ return key.split('.').reduce<unknown>((value, segment) => {
66
+ if (typeof value !== 'object' || value === null) return undefined
67
+ return (value as Record<string, unknown>)[segment]
68
+ }, settings)
69
+ }
70
+
71
+ function inspectSettingsEntry(target: string, key: string): Inspection {
72
+ let source: string
73
+ try {
74
+ source = readFileSync(target, 'utf8')
75
+ } catch {
76
+ return { kind: 'none', status: 'missing', problem: 'instructions-missing' }
77
+ }
78
+
79
+ const settings = parseJsonWithComments(source)
80
+ if (settings === undefined) return { kind: 'file', status: 'unreadable', problem: 'instructions-unreadable' }
81
+
82
+ const entry = valueAt(settings, key)
83
+ return Array.isArray(entry) && entry.includes(canonicalInstructions)
84
+ ? { kind: 'settings-entry', status: 'ok' }
85
+ : { kind: 'file', status: 'unbridged', problem: 'instructions-unbridged' }
86
+ }
87
+
88
+ /** Every path an instruction bridge of this kind belongs at, in report order. */
89
+ function pathsFor(root: string, bridge: InstructionBridge): string[] {
90
+ if (bridge.kind !== 'import') return [bridge.path]
91
+ return agentsFileDirectories(root).map((directory) => (directory ? posix.join(directory, bridge.path) : bridge.path))
92
+ }
93
+
94
+ function inspect(target: string, bridge: InstructionBridge): Inspection {
95
+ return bridge.kind === 'import' ? inspectImport(target) : inspectSettingsEntry(target, bridge.key)
96
+ }
97
+
98
+ /**
99
+ * Reports whether every enabled harness can still read `AGENTS.md`.
100
+ *
101
+ * Losing one of these is quieter than losing a skills bridge and costs more: a harness with no
102
+ * instruction bridge reads none of the repository's instructions, and says nothing about it. The
103
+ * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
104
+ * skill's judgment.
105
+ */
106
+ export function diagnoseInstructions(
107
+ root: string,
108
+ harnesses: readonly Harness[],
109
+ cli: string,
110
+ ): { instructions: InstructionReport[]; findings: BridgeFinding[] } {
111
+ // Project scope only: `doctor` diagnoses a repository, and the user-scope bridges are neither
112
+ // written nor read by this tool.
113
+ const bridged = harnesses
114
+ .map((harness) => ({ name: harness.name, bridge: harness.project.instructionBridge }))
115
+ .filter((harness): harness is { name: HarnessName; bridge: InstructionBridge } => Boolean(harness.bridge))
116
+
117
+ const instructions: InstructionReport[] = []
118
+ const findings: BridgeFinding[] = []
119
+ if (!bridged.length) return { instructions, findings }
120
+
121
+ // Only reported when something bridges into it. Every other harness reads `AGENTS.md` where it
122
+ // lies, so its absence is a repository that has not run `init` rather than a broken bridge.
123
+ if (!agentsFileDirectories(root).includes('')) {
124
+ const { detail, repair } = repairFor('no-instructions')
125
+ findings.push({
126
+ path: canonicalInstructions,
127
+ problem: 'no-instructions',
128
+ detail,
129
+ repair: repair({ file: canonicalInstructions }, cli),
130
+ })
131
+ }
132
+
133
+ for (const { name, bridge } of bridged) {
134
+ for (const path of pathsFor(root, bridge)) {
135
+ const inspection = inspect(join(root, path), bridge)
136
+ instructions.push({ harness: name, path, kind: inspection.kind, status: inspection.status })
137
+ if (inspection.problem) {
138
+ const { detail, repair } = repairFor(inspection.problem)
139
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
140
+ }
141
+ }
142
+ }
143
+
144
+ return { instructions, findings }
145
+ }