buddy-agent-harness 0.6.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 +382 -47
- 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 +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/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 +40 -7
- package/src/command-output/command-output.ts +1 -4
- 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
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'
|
|
3
|
-
import { countSkills, projectSkills } from '../skill-projection/skill-projection.ts'
|
|
3
|
+
import { countSkills, type ForceSelection, projectSkills } from '../skill-projection/skill-projection.ts'
|
|
4
4
|
|
|
5
5
|
export type InitializeOptions = {
|
|
6
6
|
root: string
|
|
7
7
|
harnesses?: HarnessName[]
|
|
8
8
|
copy?: boolean
|
|
9
|
-
force?:
|
|
9
|
+
force?: ForceSelection
|
|
10
10
|
}
|
|
11
11
|
export type InitializeResult = {
|
|
12
12
|
root: string
|
|
13
13
|
harnesses: HarnessName[]
|
|
14
14
|
native: HarnessName[]
|
|
15
15
|
linked: HarnessName[]
|
|
16
|
+
/** Enabled harnesses left untouched because their target conflicts and `--force` did not name it. */
|
|
17
|
+
skipped: HarnessName[]
|
|
16
18
|
/** Enabled harnesses whose name has been superseded, as `{ name, replacedBy }`. */
|
|
17
19
|
deprecated: { name: HarnessName; replacedBy: HarnessName }[]
|
|
18
20
|
skills: number
|
|
@@ -28,13 +30,14 @@ export function initializeHarnesses({
|
|
|
28
30
|
const canonicalSkills = join(root, '.agents', 'skills')
|
|
29
31
|
const skills = countSkills(canonicalSkills)
|
|
30
32
|
const harnesses = selectHarnesses(root, preferred)
|
|
31
|
-
const linked = projectSkills({ root, canonicalSkills, harnesses, copy, force })
|
|
33
|
+
const { linked, skipped } = projectSkills({ root, canonicalSkills, harnesses, copy, force })
|
|
32
34
|
|
|
33
35
|
return {
|
|
34
36
|
root,
|
|
35
37
|
harnesses: harnesses.map((harness) => harness.name),
|
|
36
38
|
native: harnesses.filter((harness) => !harness.project.skillsDirectory).map((harness) => harness.name),
|
|
37
39
|
linked,
|
|
40
|
+
skipped,
|
|
38
41
|
deprecated: harnesses
|
|
39
42
|
.filter((harness) => harness.deprecated)
|
|
40
43
|
.map((harness) => ({ name: harness.name, replacedBy: harness.deprecated as HarnessName })),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a value is a record: an object with named keys, and not an array.
|
|
3
|
+
*
|
|
4
|
+
* Everything this package reads is a file someone else wrote — a golden MCP set, a harness config,
|
|
5
|
+
* a projection record — so every parse lands on `unknown` and every reader narrows it here before
|
|
6
|
+
* asking for a key. Four modules had their own copy of the same three clauses; a predicate that
|
|
7
|
+
* decides what counts as a parsed object is one fact, and a copy of it that drifts changes what a
|
|
8
|
+
* file is read to say.
|
|
9
|
+
*/
|
|
10
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
12
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { cpSync, lstatSync, mkdirSync, readdirSync,
|
|
2
|
-
import {
|
|
1
|
+
import { cpSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
2
|
+
import { platform } from 'node:os'
|
|
3
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
4
|
import type { Harness, HarnessName } from '../harness-registry/harness-registry.ts'
|
|
4
5
|
|
|
5
6
|
function skillNames(skillsDirectory: string): string[] {
|
|
@@ -18,8 +19,19 @@ function pathOccupied(path: string): boolean {
|
|
|
18
19
|
}
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Whether `target` is a symbolic link that resolves to `canonical`. Resolved paths are compared
|
|
24
|
+
* rather than link text, so a link a user wrote as an absolute path, one this tool wrote as a
|
|
25
|
+
* Windows junction, and one reached through a symbolic link in a parent directory all read as the
|
|
26
|
+
* correct bridge. A link that no longer resolves is not.
|
|
27
|
+
*/
|
|
28
|
+
export function linksTo(target: string, canonical: string): boolean {
|
|
29
|
+
try {
|
|
30
|
+
if (!lstatSync(target).isSymbolicLink()) return false
|
|
31
|
+
return realpathSync(target) === realpathSync(canonical)
|
|
32
|
+
} catch {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
/** Creates the canonical directory when absent, so a fresh repository reports zero skills. */
|
|
@@ -28,19 +40,72 @@ export function countSkills(canonicalSkills: string): number {
|
|
|
28
40
|
return skillNames(canonicalSkills).length
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* A junction is the only link Windows grants an unprivileged process, and Node resolves a relative
|
|
45
|
+
* junction target against the process directory rather than the link's own, so Windows is given the
|
|
46
|
+
* absolute path. Every other platform keeps the link relative, so it survives the repository moving.
|
|
47
|
+
*/
|
|
48
|
+
function linkTarget(target: string, canonicalSkills: string): string {
|
|
49
|
+
return platform() === 'win32' ? resolve(canonicalSkills) : relative(dirname(target), canonicalSkills)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Which conflicting targets a run may replace. `true` is every one of them; a list names them
|
|
54
|
+
* individually, so a run invoked to fix one bridge cannot reach past it to another. `false` and an
|
|
55
|
+
* empty list both mean none, which is what makes a conflict stop the run.
|
|
56
|
+
*/
|
|
57
|
+
export type ForceSelection = boolean | readonly string[]
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Translates what the option parser hands back into a selection. A valueless `--force` arrives as
|
|
61
|
+
* the literal string `true` — that is how the parser encodes a flag given no value — and means
|
|
62
|
+
* every conflicting target, which is what keeps an existing `init --force` doing what it always did.
|
|
63
|
+
* Anything else is a comma-separated list of targets.
|
|
64
|
+
*/
|
|
65
|
+
export function parseForce(value: string): ForceSelection {
|
|
66
|
+
if (value === 'true') return true
|
|
67
|
+
if (value === 'false') return false
|
|
68
|
+
return value
|
|
69
|
+
.split(',')
|
|
70
|
+
.map((target) => target.trim())
|
|
71
|
+
.filter((target) => target.length > 0)
|
|
72
|
+
}
|
|
73
|
+
|
|
31
74
|
export type ProjectSkillsOptions = {
|
|
32
75
|
root: string
|
|
33
76
|
canonicalSkills: string
|
|
34
77
|
harnesses: readonly Harness[]
|
|
35
78
|
copy: boolean
|
|
36
|
-
force:
|
|
79
|
+
force: ForceSelection
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type ProjectSkillsResult = {
|
|
83
|
+
/** Harnesses whose target now points at the canonical directory. */
|
|
84
|
+
linked: HarnessName[]
|
|
85
|
+
/** Harnesses left untouched because their target conflicts and `--force` did not name it. */
|
|
86
|
+
skipped: HarnessName[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Targets are matched against what the caller typed, resolved against the repository root, so the
|
|
91
|
+
* repo-relative form the conflict message prints is the form that selects it back — and an absolute
|
|
92
|
+
* path naming the same directory selects it too.
|
|
93
|
+
*/
|
|
94
|
+
function namesTarget(root: string, target: string, given: string): boolean {
|
|
95
|
+
return resolve(root, given) === resolve(target)
|
|
37
96
|
}
|
|
38
97
|
|
|
39
98
|
/**
|
|
40
99
|
* Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is
|
|
41
|
-
* preferred; a platform that refuses the link falls back to a copy.
|
|
100
|
+
* preferred; a platform that refuses the link falls back to a copy.
|
|
42
101
|
*/
|
|
43
|
-
export function projectSkills({
|
|
102
|
+
export function projectSkills({
|
|
103
|
+
root,
|
|
104
|
+
canonicalSkills,
|
|
105
|
+
harnesses,
|
|
106
|
+
copy,
|
|
107
|
+
force,
|
|
108
|
+
}: ProjectSkillsOptions): ProjectSkillsResult {
|
|
44
109
|
const projections = harnesses
|
|
45
110
|
.filter((harness) => harness.project.skillsDirectory)
|
|
46
111
|
.map((harness) => ({
|
|
@@ -48,31 +113,52 @@ export function projectSkills({ root, canonicalSkills, harnesses, copy, force }:
|
|
|
48
113
|
target: join(root, harness.project.skillsDirectory as string),
|
|
49
114
|
}))
|
|
50
115
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
116
|
+
const named = Array.isArray(force) ? force : []
|
|
117
|
+
// A name matching no projection target at all is a typo, not a narrower run: silently forcing
|
|
118
|
+
// nothing would surface as the conflict error the flag was reached for, blaming the wrong thing.
|
|
119
|
+
const unknown = named.filter((given) => !projections.some(({ target }) => namesTarget(root, target, given)))
|
|
120
|
+
if (unknown.length) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`No skill target matches:\n${unknown.map((given) => `- ${given}`).join('\n')}\n` +
|
|
123
|
+
`Targets for the enabled harnesses:\n${projections.map(({ target }) => `- ${relative(root, target)}`).join('\n')}`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const forces = (target: string): boolean => force === true || named.some((given) => namesTarget(root, target, given))
|
|
54
128
|
|
|
55
|
-
|
|
129
|
+
const conflicts = projections.filter(({ target }) => pathOccupied(target) && !linksTo(target, canonicalSkills))
|
|
130
|
+
const blocked = conflicts.filter(({ target }) => !forces(target))
|
|
131
|
+
|
|
132
|
+
// Nothing was named, so nothing distinguishes one conflict from another: stop before writing.
|
|
133
|
+
// Once a target IS named, the unnamed conflicts are the ones this run was told to leave alone,
|
|
134
|
+
// so they are skipped and reported rather than turned into a refusal that writes nothing at all.
|
|
135
|
+
if (blocked.length && named.length === 0) {
|
|
56
136
|
throw new Error(
|
|
57
|
-
`Refusing to replace existing skill targets:\n${
|
|
137
|
+
`Refusing to replace existing skill targets:\n${blocked.map(({ target }) => `- ${relative(root, target)}`).join('\n')}`,
|
|
58
138
|
)
|
|
59
139
|
}
|
|
60
140
|
|
|
61
|
-
|
|
141
|
+
const skipped = new Set(blocked.map(({ harness }) => harness.name))
|
|
142
|
+
|
|
143
|
+
for (const { harness, target } of projections) {
|
|
144
|
+
if (skipped.has(harness.name)) continue
|
|
62
145
|
mkdirSync(dirname(target), { recursive: true })
|
|
63
|
-
if (
|
|
146
|
+
if (linksTo(target, canonicalSkills)) continue
|
|
64
147
|
if (pathOccupied(target)) rmSync(target, { recursive: true, force: true })
|
|
65
148
|
if (copy) {
|
|
66
149
|
cpSync(canonicalSkills, target, { recursive: true })
|
|
67
150
|
continue
|
|
68
151
|
}
|
|
69
152
|
try {
|
|
70
|
-
symlinkSync(
|
|
153
|
+
symlinkSync(linkTarget(target, canonicalSkills), target, platform() === 'win32' ? 'junction' : undefined)
|
|
71
154
|
} catch (error) {
|
|
72
155
|
if (pathOccupied(target)) throw error
|
|
73
156
|
cpSync(canonicalSkills, target, { recursive: true })
|
|
74
157
|
}
|
|
75
158
|
}
|
|
76
159
|
|
|
77
|
-
return
|
|
160
|
+
return {
|
|
161
|
+
linked: projections.filter(({ harness }) => !skipped.has(harness.name)).map(({ harness }) => harness.name),
|
|
162
|
+
skipped: projections.filter(({ harness }) => skipped.has(harness.name)).map(({ harness }) => harness.name),
|
|
163
|
+
}
|
|
78
164
|
}
|
package/dist/cli.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { cli } from 'clibuilder'\nimport { doctorCommand } from './diagnose-bridges/doctor.command.ts'\nimport { initCommand } from './initialize-harnesses/init.command.ts'\n\nexport async function main(): Promise<void> {\n\tconst app = cli({\n\t\tname: 'buddy-agent-harness',\n\t\tversion: '0.1.0',\n\t\tdescription: 'Initialize agent harness skill compatibility in consumer repositories.',\n\t})\n\ttry {\n\t\tawait app.command(initCommand).command(doctorCommand).parse(process.argv)\n\t} catch (error) {\n\t\tprocess.stdout.write(`error: ${error instanceof Error ? error.message : 'Invalid command.'}\\n`)\n\t\tprocess.exitCode = 2\n\t}\n}\n"],"mappings":";;;AAIA,eAAsB,OAAsB;CAC3C,MAAM,MAAM,IAAI;EACf,MAAM;EACN,SAAS;EACT,aAAa;CACd,CAAC;CACD,IAAI;EACH,MAAM,IAAI,QAAQ,WAAW,CAAC,CAAC,QAAQ,aAAa,CAAC,CAAC,MAAM,QAAQ,IAAI;CACzE,SAAS,OAAO;EACf,QAAQ,OAAO,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,mBAAmB,GAAG;EAC9F,QAAQ,WAAW;CACpB;AACD"}
|