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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/bin/buddy-agent-harness.mjs +8 -3
- package/dist/cli-Bvnq8zOL.mjs +2122 -0
- package/dist/cli-Bvnq8zOL.mjs.map +1 -0
- package/dist/cli.d.mts +13 -2
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +2 -20
- package/dist/index.d.mts +491 -40
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/doctor/SKILL.md +27 -23
- 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/init/README.md +1 -1
- package/skills/init/SKILL.md +24 -10
- package/skills/init/references/detection.md +5 -3
- package/skills/init/references/harnesses/claude-code.md +2 -0
- package/skills/init/references/harnesses/cursor.md +9 -2
- package/skills/init/references/harnesses/gemini-cli.md +6 -9
- 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 +40 -7
- package/src/command-output/command-output.ts +1 -4
- package/src/diagnose-bridges/agents-files.ts +34 -0
- package/src/diagnose-bridges/diagnose-bridges.ts +39 -21
- package/src/diagnose-bridges/diagnose-instructions.ts +145 -0
- package/src/diagnose-bridges/doctor-guidance.ts +683 -69
- package/src/diagnose-bridges/doctor.command.ts +63 -30
- package/src/diagnose-bridges/git-bridge-state.ts +30 -0
- package/src/diagnose-bridges/json-with-comments.ts +60 -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 +164 -14
- package/src/harness-registry/instruction-bridge.ts +31 -0
- package/src/harness-registry/mcp-config.ts +37 -0
- package/src/harness-registry/nonstandard-artifact.ts +39 -0
- package/src/index.ts +33 -4
- package/src/initialize-harnesses/init.command.ts +13 -18
- package/src/initialize-harnesses/initialize-harnesses.ts +7 -4
- package/src/is-record/is-record.ts +12 -0
- package/src/skill-projection/skill-projection.ts +104 -18
- package/dist/cli.mjs.map +0 -1
- package/dist/init.command-c0fBv15T.mjs +0 -714
- package/dist/init.command-c0fBv15T.mjs.map +0 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { lstatSync, readFileSync,
|
|
2
|
-
import {
|
|
3
|
-
import { type
|
|
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
|
-
/**
|
|
23
|
-
repair:
|
|
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
|
|
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
|
|
116
|
-
* `.agents/skills`. Read-only: nothing is created,
|
|
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
|
|
123
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
+
}
|