buddy-agent-harness 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +9 -1
  5. package/bin/buddy-agent-harness.mjs +8 -3
  6. package/dist/cli-4ekejn5W.mjs +2735 -0
  7. package/dist/cli-4ekejn5W.mjs.map +1 -0
  8. package/dist/cli.d.mts +12 -2
  9. package/dist/cli.d.mts.map +1 -1
  10. package/dist/cli.mjs +2 -20
  11. package/dist/index.d.mts +389 -54
  12. package/dist/index.d.mts.map +1 -1
  13. package/dist/index.mjs +2 -2
  14. package/package.json +6 -5
  15. package/plugin.json +1 -1
  16. package/skills/doctor/SKILL.md +20 -34
  17. package/skills/doctor/references/bridges.md +23 -0
  18. package/skills/doctor/references/configuration.md +12 -0
  19. package/skills/doctor/references/harnesses/claude-code.md +39 -0
  20. package/skills/doctor/references/harnesses/codex.md +37 -0
  21. package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
  22. package/skills/doctor/references/harnesses/cursor.md +40 -0
  23. package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
  24. package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
  25. package/skills/doctor/references/harnesses/windsurf.md +27 -0
  26. package/skills/doctor/references/instructions.md +14 -0
  27. package/skills/doctor/references/mcp.md +31 -0
  28. package/skills/doctor/references/nonstandard.md +27 -0
  29. package/skills/doctor/scripts/doctor.mjs +7 -3
  30. package/skills/enhance/SKILL.md +4 -0
  31. package/skills/enhance/references/delegation.md +3 -1
  32. package/skills/init/README.md +1 -1
  33. package/skills/init/SKILL.md +19 -8
  34. package/skills/init/references/detection.md +5 -3
  35. package/skills/init/references/harnesses/cursor.md +9 -2
  36. package/skills/init/scripts/doctor.mjs +14 -0
  37. package/skills/init/scripts/init.mjs +7 -3
  38. package/skills/repair/README.md +21 -0
  39. package/skills/repair/SKILL.md +74 -0
  40. package/skills/repair/references/classes.md +49 -0
  41. package/skills/repair/scripts/doctor.mjs +14 -0
  42. package/src/cli.ts +42 -7
  43. package/src/command-output/command-output.ts +1 -4
  44. package/src/dep-plugins/dep-plugins.command.ts +168 -0
  45. package/src/dep-plugins/dep-plugins.ts +131 -0
  46. package/src/dep-plugins/generate.ts +115 -0
  47. package/src/dep-plugins/harness-plugins.ts +232 -0
  48. package/src/dep-plugins/reconcile.ts +132 -0
  49. package/src/dep-plugins/resolve.ts +117 -0
  50. package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
  51. package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
  52. package/src/diagnose-bridges/doctor-guidance.ts +636 -90
  53. package/src/diagnose-bridges/doctor.command.ts +55 -28
  54. package/src/diagnose-bridges/git-bridge-state.ts +30 -0
  55. package/src/diagnose-bridges/locator.ts +36 -0
  56. package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
  57. package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
  58. package/src/diagnose-mcp/mcp-baseline.ts +122 -0
  59. package/src/diagnose-mcp/mcp-model.ts +88 -0
  60. package/src/diagnose-mcp/mcp-secrets.ts +101 -0
  61. package/src/diagnose-mcp/mcp-sources.ts +131 -0
  62. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
  63. package/src/harness-registry/harness-registry.ts +97 -4
  64. package/src/harness-registry/mcp-config.ts +37 -0
  65. package/src/harness-registry/nonstandard-artifact.ts +39 -0
  66. package/src/index.ts +18 -2
  67. package/src/initialize-harnesses/init.command.ts +13 -18
  68. package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
  69. package/src/is-record/is-record.ts +12 -0
  70. package/src/skill-projection/skill-projection.ts +102 -16
  71. package/dist/cli.mjs.map +0 -1
  72. package/dist/init.command-zbntulIq.mjs +0 -1016
  73. package/dist/init.command-zbntulIq.mjs.map +0 -1
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Locating a declared dependency on disk, and reading the plugin manifest it may ship.
3
+ *
4
+ * Resolution rather than scanning. Walking `node_modules` finds transitive dependencies nobody
5
+ * vetted, and a skill or plugin is instruction text that steers an agent — an unvetted transitive
6
+ * dependency is a supply-chain surface, not a convenience. Asking the module resolver where a
7
+ * *declared* dependency lives answers the question that was actually asked, and it is correct under
8
+ * npm, pnpm, yarn with the node-modules linker, and bun alike, because all four put a real directory
9
+ * on disk. Only Yarn PnP does not, and it is reported rather than guessed at.
10
+ */
11
+
12
+ import { existsSync, readFileSync } from 'node:fs'
13
+ import { createRequire } from 'node:module'
14
+ import { dirname, join } from 'node:path'
15
+ import type { DepPlugin } from './dep-plugins.ts'
16
+
17
+ /** Why a declared dependency yielded no plugin, when the caller needs to explain itself. */
18
+ export type SkipReason = 'unresolved' | 'no-manifest' | 'unnamed'
19
+
20
+ export type ResolveOutcome = { ok: true; plugin: DepPlugin } | { ok: false; package: string; reason: SkipReason }
21
+
22
+ function readJson(path: string): unknown {
23
+ try {
24
+ return JSON.parse(readFileSync(path, 'utf8'))
25
+ } catch {
26
+ return undefined
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Every ancestor `node_modules/<pkg>` from `startDir` upward. The fallback path only — see
32
+ * {@link packageDir}.
33
+ */
34
+ function ancestorCandidates(startDir: string, pkg: string): string[] {
35
+ const segments = pkg.split('/')
36
+ const candidates: string[] = []
37
+ let dir = startDir
38
+ for (;;) {
39
+ candidates.push(join(dir, 'node_modules', ...segments))
40
+ const parent = dirname(dir)
41
+ if (parent === dir) break
42
+ dir = parent
43
+ }
44
+ return candidates
45
+ }
46
+
47
+ /**
48
+ * The directory a declared dependency was installed into, or `undefined`.
49
+ *
50
+ * Two mechanisms, in order, because neither alone is sufficient:
51
+ *
52
+ * 1. **`require.resolve('<pkg>/package.json')`, anchored at the declaring manifest.** The anchor
53
+ * matters: under pnpm's isolated store only the package that declares a dependency can resolve it,
54
+ * so resolving from a workspace root would miss what a workspace member declared.
55
+ * 2. **A walk up `node_modules`.** Roughly one package in seven declares an `exports` map without a
56
+ * `./package.json` entry, and those refuse the first mechanism with `ERR_PACKAGE_PATH_NOT_EXPORTED`
57
+ * however plainly they are installed. Resolving a deeper file instead is not an escape — the same
58
+ * map blocks that too.
59
+ */
60
+ export function packageDir(pkg: string, fromManifest: string): string | undefined {
61
+ try {
62
+ const require = createRequire(fromManifest)
63
+ return dirname(require.resolve(`${pkg}/package.json`))
64
+ } catch {
65
+ // fall through to the walk
66
+ }
67
+ for (const candidate of ancestorCandidates(dirname(fromManifest), pkg)) {
68
+ if (existsSync(join(candidate, 'package.json'))) return candidate
69
+ }
70
+ return undefined
71
+ }
72
+
73
+ /**
74
+ * Reads the plugin a dependency ships, if it ships one.
75
+ *
76
+ * The version comes from the dependency's own `package.json` rather than from a lockfile. It is the
77
+ * same number — the version actually installed — and reading it needs no knowledge of which of the
78
+ * four lockfile formats this repository uses.
79
+ *
80
+ * A manifest at the package root is what the Agent Plugins Specification pins, and it is the form a
81
+ * harness accepts as a marketplace source without any vendor-specific manifest alongside it.
82
+ */
83
+ export function readDepPlugin(pkg: string, fromManifest: string): ResolveOutcome {
84
+ const dir = packageDir(pkg, fromManifest)
85
+ if (!dir) return { ok: false, package: pkg, reason: 'unresolved' }
86
+
87
+ const manifest = readJson(join(dir, 'plugin.json'))
88
+ if (!manifest || typeof manifest !== 'object') return { ok: false, package: pkg, reason: 'no-manifest' }
89
+
90
+ const record = manifest as Record<string, unknown>
91
+ const name = typeof record['name'] === 'string' ? record['name'] : undefined
92
+ if (!name) return { ok: false, package: pkg, reason: 'unnamed' }
93
+
94
+ // The installed version, not the manifest's: the catalog installs the package from npm, so the
95
+ // number that must match is the one npm published the tree under.
96
+ const installed = readJson(join(dir, 'package.json')) as { version?: unknown } | undefined
97
+ const version = typeof installed?.version === 'string' ? installed.version : undefined
98
+ if (!version) return { ok: false, package: pkg, reason: 'no-manifest' }
99
+
100
+ const description = typeof record['description'] === 'string' ? record['description'] : undefined
101
+ return {
102
+ ok: true,
103
+ plugin: description
104
+ ? { package: pkg, plugin: name, version, description }
105
+ : { package: pkg, plugin: name, version },
106
+ }
107
+ }
108
+
109
+ /**
110
+ * True when the process is running under Yarn's Plug'n'Play resolver, where packages live inside zip
111
+ * archives rather than as directories. A marketplace source must be a real directory and an npm
112
+ * source must be installable, so a PnP project needs `nodeLinker: node-modules` or the affected
113
+ * dependencies unplugged. Detected rather than worked around, so the report can say so.
114
+ */
115
+ export function isPnp(): boolean {
116
+ return Boolean((process.versions as Record<string, string | undefined>)['pnp'])
117
+ }
@@ -1,9 +1,10 @@
1
- import { lstatSync, readFileSync, readlinkSync, statSync } from 'node:fs'
2
- import { dirname, join, relative } from 'node:path'
1
+ import { lstatSync, readFileSync, statSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
3
  import { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
4
+ import { linksTo } from '../skill-projection/skill-projection.ts'
4
5
  import { diagnoseInstructions, type InstructionReport } from './diagnose-instructions.ts'
5
6
  import { filesUnder } from './directory-files.ts'
6
- import { type BridgeProblem, repairFor } from './doctor-guidance.ts'
7
+ import { type BridgeProblem, type DoctorProblem, type RepairAction, repairFor } from './doctor-guidance.ts'
7
8
  import { type DivergenceDirection, GitBridgeState } from './git-bridge-state.ts'
8
9
 
9
10
  export type BridgeKind = 'symlink' | 'copy' | 'file' | 'none'
@@ -19,9 +20,14 @@ export type BridgeReport = {
19
20
 
20
21
  export type BridgeFinding = {
21
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
22
28
  detail: string
23
- /** The command that repairs this finding, already carrying the bridge path. */
24
- repair: string
29
+ /** What repairs this finding, already carrying the bridge path. Empty `command` means judgment. */
30
+ repair: RepairAction
25
31
  }
26
32
 
27
33
  export type DivergenceReport = {
@@ -67,11 +73,6 @@ function isDirectory(path: string): boolean {
67
73
  }
68
74
  }
69
75
 
70
- /** The same test `init` uses to decide a projection is already correct, plus that it still resolves. */
71
- function resolvesToCanonical(target: string, canonical: string): boolean {
72
- return readlinkSync(target) === relative(dirname(target), canonical) && isDirectory(target)
73
- }
74
-
75
76
  function sameContent(left: string, right: string): boolean {
76
77
  const leftFiles = filesUnder(left)
77
78
  const rightFiles = filesUnder(right)
@@ -99,8 +100,10 @@ function inspect(target: string, path: string, canonical: string, git: GitBridge
99
100
  // regular file holding the target path, and the harness silently loads nothing.
100
101
  case 'file':
101
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.
102
105
  case 'symlink':
103
- return resolvesToCanonical(target, canonical)
106
+ return linksTo(target, canonical)
104
107
  ? { kind: 'symlink', status: 'ok' }
105
108
  : { kind: 'symlink', status: 'stale', problem: 'stale' }
106
109
  default: {
@@ -136,7 +139,12 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
136
139
 
137
140
  if (!isDirectory(canonical)) {
138
141
  const { detail, repair } = repairFor('no-canonical')
139
- 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
+ })
140
148
  }
141
149
 
142
150
  for (const harness of bridged) {
@@ -146,7 +154,7 @@ export function diagnoseBridges({ root, harnesses: preferred = [], cli }: Diagno
146
154
  if (inspection.direction) divergence.push({ path, direction: inspection.direction })
147
155
  if (inspection.problem) {
148
156
  const { detail, repair } = repairFor(inspection.problem)
149
- findings.push({ path, detail, repair: repair(path, cli) })
157
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
150
158
  }
151
159
  }
152
160
 
@@ -122,7 +122,12 @@ export function diagnoseInstructions(
122
122
  // lies, so its absence is a repository that has not run `init` rather than a broken bridge.
123
123
  if (!agentsFileDirectories(root).includes('')) {
124
124
  const { detail, repair } = repairFor('no-instructions')
125
- findings.push({ path: canonicalInstructions, detail, repair: repair(canonicalInstructions, cli) })
125
+ findings.push({
126
+ path: canonicalInstructions,
127
+ problem: 'no-instructions',
128
+ detail,
129
+ repair: repair({ file: canonicalInstructions }, cli),
130
+ })
126
131
  }
127
132
 
128
133
  for (const { name, bridge } of bridged) {
@@ -131,7 +136,7 @@ export function diagnoseInstructions(
131
136
  instructions.push({ harness: name, path, kind: inspection.kind, status: inspection.status })
132
137
  if (inspection.problem) {
133
138
  const { detail, repair } = repairFor(inspection.problem)
134
- findings.push({ path, detail, repair: repair(path, cli) })
139
+ findings.push({ path, problem: inspection.problem, detail, repair: repair({ file: path }, cli) })
135
140
  }
136
141
  }
137
142
  }