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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +9 -1
- package/bin/buddy-agent-harness.mjs +8 -3
- package/dist/cli-4ekejn5W.mjs +2735 -0
- package/dist/cli-4ekejn5W.mjs.map +1 -0
- package/dist/cli.d.mts +12 -2
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +2 -20
- package/dist/index.d.mts +389 -54
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +6 -5
- package/plugin.json +1 -1
- package/skills/doctor/SKILL.md +20 -34
- package/skills/doctor/references/bridges.md +23 -0
- package/skills/doctor/references/configuration.md +12 -0
- package/skills/doctor/references/harnesses/claude-code.md +39 -0
- package/skills/doctor/references/harnesses/codex.md +37 -0
- package/skills/doctor/references/harnesses/copilot-cli.md +39 -0
- package/skills/doctor/references/harnesses/cursor.md +40 -0
- package/skills/doctor/references/harnesses/devin-desktop.md +20 -0
- package/skills/doctor/references/harnesses/gemini-cli.md +38 -0
- package/skills/doctor/references/harnesses/windsurf.md +27 -0
- package/skills/doctor/references/instructions.md +14 -0
- package/skills/doctor/references/mcp.md +31 -0
- package/skills/doctor/references/nonstandard.md +27 -0
- package/skills/doctor/scripts/doctor.mjs +7 -3
- package/skills/enhance/SKILL.md +4 -0
- package/skills/enhance/references/delegation.md +3 -1
- package/skills/init/README.md +1 -1
- package/skills/init/SKILL.md +19 -8
- package/skills/init/references/detection.md +5 -3
- package/skills/init/references/harnesses/cursor.md +9 -2
- package/skills/init/scripts/doctor.mjs +14 -0
- package/skills/init/scripts/init.mjs +7 -3
- package/skills/repair/README.md +21 -0
- package/skills/repair/SKILL.md +74 -0
- package/skills/repair/references/classes.md +49 -0
- package/skills/repair/scripts/doctor.mjs +14 -0
- package/src/cli.ts +42 -7
- package/src/command-output/command-output.ts +1 -4
- package/src/dep-plugins/dep-plugins.command.ts +168 -0
- package/src/dep-plugins/dep-plugins.ts +131 -0
- package/src/dep-plugins/generate.ts +115 -0
- package/src/dep-plugins/harness-plugins.ts +232 -0
- package/src/dep-plugins/reconcile.ts +132 -0
- package/src/dep-plugins/resolve.ts +117 -0
- package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
- package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
- package/src/diagnose-bridges/doctor-guidance.ts +636 -90
- package/src/diagnose-bridges/doctor.command.ts +55 -28
- package/src/diagnose-bridges/git-bridge-state.ts +30 -0
- package/src/diagnose-bridges/locator.ts +36 -0
- package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
- package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
- package/src/diagnose-mcp/mcp-baseline.ts +122 -0
- package/src/diagnose-mcp/mcp-model.ts +88 -0
- package/src/diagnose-mcp/mcp-secrets.ts +101 -0
- package/src/diagnose-mcp/mcp-sources.ts +131 -0
- package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
- package/src/harness-registry/harness-registry.ts +97 -4
- package/src/harness-registry/mcp-config.ts +37 -0
- package/src/harness-registry/nonstandard-artifact.ts +39 -0
- package/src/index.ts +18 -2
- package/src/initialize-harnesses/init.command.ts +13 -18
- package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
- package/src/is-record/is-record.ts +12 -0
- package/src/skill-projection/skill-projection.ts +102 -16
- package/dist/cli.mjs.map +0 -1
- package/dist/init.command-zbntulIq.mjs +0 -1016
- 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,
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
24
|
-
repair:
|
|
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
|
|
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({
|
|
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({
|
|
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
|
}
|