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.
Files changed (65) 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 +382 -47
  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 +20 -34
  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 +19 -8
  32. package/skills/init/references/detection.md +5 -3
  33. package/skills/init/references/harnesses/cursor.md +9 -2
  34. package/skills/init/scripts/doctor.mjs +14 -0
  35. package/skills/init/scripts/init.mjs +7 -3
  36. package/skills/repair/README.md +21 -0
  37. package/skills/repair/SKILL.md +74 -0
  38. package/skills/repair/references/classes.md +49 -0
  39. package/skills/repair/scripts/doctor.mjs +14 -0
  40. package/src/cli.ts +40 -7
  41. package/src/command-output/command-output.ts +1 -4
  42. package/src/diagnose-bridges/diagnose-bridges.ts +21 -13
  43. package/src/diagnose-bridges/diagnose-instructions.ts +7 -2
  44. package/src/diagnose-bridges/doctor-guidance.ts +636 -90
  45. package/src/diagnose-bridges/doctor.command.ts +55 -28
  46. package/src/diagnose-bridges/git-bridge-state.ts +30 -0
  47. package/src/diagnose-bridges/locator.ts +36 -0
  48. package/src/diagnose-configuration/diagnose-configuration.ts +99 -0
  49. package/src/diagnose-mcp/diagnose-mcp.ts +135 -0
  50. package/src/diagnose-mcp/mcp-baseline.ts +122 -0
  51. package/src/diagnose-mcp/mcp-model.ts +88 -0
  52. package/src/diagnose-mcp/mcp-secrets.ts +101 -0
  53. package/src/diagnose-mcp/mcp-sources.ts +131 -0
  54. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +109 -0
  55. package/src/harness-registry/harness-registry.ts +97 -4
  56. package/src/harness-registry/mcp-config.ts +37 -0
  57. package/src/harness-registry/nonstandard-artifact.ts +39 -0
  58. package/src/index.ts +18 -2
  59. package/src/initialize-harnesses/init.command.ts +13 -18
  60. package/src/initialize-harnesses/initialize-harnesses.ts +6 -3
  61. package/src/is-record/is-record.ts +12 -0
  62. package/src/skill-projection/skill-projection.ts +102 -16
  63. package/dist/cli.mjs.map +0 -1
  64. package/dist/init.command-zbntulIq.mjs +0 -1016
  65. 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?: boolean
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, readlinkSync, rmSync, symlinkSync } from 'node:fs'
2
- import { dirname, join, relative } from 'node:path'
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
- function isCorrectLink(target: string, source: string): boolean {
22
- return lstatSync(target).isSymbolicLink() && readlinkSync(target) === relative(dirname(target), source)
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: boolean
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. Returns the projected harnesses.
100
+ * preferred; a platform that refuses the link falls back to a copy.
42
101
  */
43
- export function projectSkills({ root, canonicalSkills, harnesses, copy, force }: ProjectSkillsOptions): HarnessName[] {
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 conflicts = projections
52
- .filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills))
53
- .map(({ target }) => target)
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
- if (conflicts.length && !force) {
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${conflicts.map((target) => `- ${target}`).join('\n')}`,
137
+ `Refusing to replace existing skill targets:\n${blocked.map(({ target }) => `- ${relative(root, target)}`).join('\n')}`,
58
138
  )
59
139
  }
60
140
 
61
- for (const { target } of projections) {
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 (pathOccupied(target) && isCorrectLink(target, canonicalSkills)) continue
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(relative(dirname(target), canonicalSkills), target, 'junction')
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 projections.map(({ harness }) => harness.name)
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"}