buddy-agent-harness 0.1.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 (36) hide show
  1. package/.claude-plugin/plugin.json +12 -0
  2. package/.codex-plugin/plugin.json +16 -0
  3. package/.cursor-plugin/plugin.json +14 -0
  4. package/.plugin/plugin.json +30 -0
  5. package/README.md +37 -0
  6. package/bin/buddy-agent-harness.mjs +7 -0
  7. package/dist/cli.d.mts +5 -0
  8. package/dist/cli.d.mts.map +1 -0
  9. package/dist/cli.mjs +20 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/index.d.mts +57 -0
  12. package/dist/index.d.mts.map +1 -0
  13. package/dist/index.mjs +2 -0
  14. package/dist/init.command-wxC4cgUc.mjs +203 -0
  15. package/dist/init.command-wxC4cgUc.mjs.map +1 -0
  16. package/package.json +69 -0
  17. package/plugin.json +14 -0
  18. package/skills/init/README.md +29 -0
  19. package/skills/init/SKILL.md +75 -0
  20. package/skills/init/agents/openai.yaml +4 -0
  21. package/skills/init/references/agents-md.md +131 -0
  22. package/skills/init/references/detection.md +34 -0
  23. package/skills/init/references/frontmatter.md +63 -0
  24. package/skills/init/references/harnesses/claude-code.md +51 -0
  25. package/skills/init/references/harnesses/codex.md +13 -0
  26. package/skills/init/references/harnesses/copilot-cli.md +16 -0
  27. package/skills/init/references/harnesses/cursor.md +23 -0
  28. package/skills/init/references/harnesses/devin-desktop.md +19 -0
  29. package/skills/init/references/harnesses/gemini-cli.md +33 -0
  30. package/skills/init/references/standard.md +26 -0
  31. package/src/cli.ts +16 -0
  32. package/src/harness-registry/harness-registry.ts +60 -0
  33. package/src/index.ts +5 -0
  34. package/src/initialize-harnesses/init.command.ts +76 -0
  35. package/src/initialize-harnesses/initialize-harnesses.ts +44 -0
  36. package/src/skill-projection/skill-projection.ts +78 -0
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "buddy-agent-harness",
3
+ "version": "0.1.0",
4
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
5
+ "author": {
6
+ "name": "repobuddy"
7
+ },
8
+ "repository": "https://github.com/repobuddy/buddy-agent-harness",
9
+ "license": "MIT",
10
+ "keywords": ["agent", "skills", "harness", "claude-code", "codex"],
11
+ "skills": "./skills/"
12
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "buddy-agent-harness",
3
+ "version": "0.1.0",
4
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
5
+ "author": {
6
+ "name": "repobuddy"
7
+ },
8
+ "repository": "https://github.com/repobuddy/buddy-agent-harness",
9
+ "license": "MIT",
10
+ "keywords": ["agent", "skills", "harness", "claude-code", "codex"],
11
+ "skills": "./skills/",
12
+ "interface": {
13
+ "displayName": "Buddy Agent Harness",
14
+ "category": "productivity"
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "buddy-agent-harness",
3
+ "version": "0.1.0",
4
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
5
+ "author": {
6
+ "name": "repobuddy"
7
+ },
8
+ "repository": "https://github.com/repobuddy/buddy-agent-harness",
9
+ "license": "MIT",
10
+ "keywords": ["agent", "skills", "harness", "claude-code", "codex"],
11
+ "skills": "./skills/",
12
+ "category": "productivity",
13
+ "tags": ["agent", "skills"]
14
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/cyberuni/universal-plugin/refs/heads/main/schema/v1.json",
3
+ "name": "buddy-agent-harness",
4
+ "version": "0.1.0",
5
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
6
+ "author": {
7
+ "name": "repobuddy"
8
+ },
9
+ "repository": "https://github.com/repobuddy/buddy-agent-harness",
10
+ "license": "MIT",
11
+ "keywords": ["agent", "skills", "harness", "claude-code", "codex"],
12
+ "skills": "./skills/",
13
+ "vendorExtensions": {
14
+ "claude-code": {},
15
+ "cursor": {
16
+ "category": "productivity",
17
+ "tags": ["agent", "skills"]
18
+ },
19
+ "codex": {
20
+ "interface": {
21
+ "displayName": "Buddy Agent Harness",
22
+ "category": "productivity"
23
+ }
24
+ },
25
+ "copilot-cli": {
26
+ "category": "productivity",
27
+ "tags": ["agent", "skills"]
28
+ }
29
+ }
30
+ }
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Buddy Agent Harness
2
+
3
+ [![CI](https://github.com/repobuddy/buddy-agent-harness/actions/workflows/release.yml/badge.svg)](https://github.com/repobuddy/buddy-agent-harness/actions/workflows/release.yml)
4
+ [![Codecov](https://codecov.io/gh/repobuddy/buddy-agent-harness/graph/badge.svg)](https://codecov.io/gh/repobuddy/buddy-agent-harness)
5
+ [![npm](https://img.shields.io/npm/v/buddy-agent-harness)](https://www.npmjs.com/package/buddy-agent-harness)
6
+ [![License](https://img.shields.io/npm/l/buddy-agent-harness)](LICENSE)
7
+
8
+ An agent plugin and CLI for initializing or updating a consumer repository's standards-based agent configuration across coding-agent harnesses.
9
+
10
+ ## Install the plugin
11
+
12
+ Install the portable plugin and its `init` skill with `npx skills`:
13
+
14
+ ```sh
15
+ npx skills add repobuddy/buddy-agent-harness --plugin
16
+ ```
17
+
18
+ In Claude Code, add the marketplace and install the plugin:
19
+
20
+ ```text
21
+ /plugin marketplace add repobuddy/buddy-agent-harness
22
+ /plugin install buddy-agent-harness@repobuddy
23
+ ```
24
+
25
+ Other agent clients can install the same repository through their plugin marketplace or use the `npx skills` command above.
26
+
27
+ ## CLI
28
+
29
+ After the npm package is published, run the CLI without a global installation:
30
+
31
+ ```sh
32
+ npx -y buddy-agent-harness init
33
+ ```
34
+
35
+ The repository root's `AGENTS.md` and `.agents/` tree are canonical: `.agents/AGENTS.md` holds shared behavior, `.agents/skills/**/SKILL.md` holds capabilities, and separately named files hold tool settings. The active harness is enabled by default; explicit user preferences add others. The command preserves user-authored configuration and projects only supported mappings; it records nothing on disk.
36
+
37
+ Use `buddy-agent-harness init --help` to see `--root`, `--copy`, `--force`, and `--format`.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const dir = dirname(fileURLToPath(import.meta.url))
6
+ const { main } = await import(join(dir, '..', 'dist', 'cli.mjs'))
7
+ await main()
package/dist/cli.d.mts ADDED
@@ -0,0 +1,5 @@
1
+ //#region src/cli.d.ts
2
+ declare function main(): Promise<void>;
3
+ //#endregion
4
+ export { main };
5
+ //# sourceMappingURL=cli.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.mts","names":[],"sources":["../src/cli.ts"],"mappings":";iBAGsB,QAAQ"}
package/dist/cli.mjs ADDED
@@ -0,0 +1,20 @@
1
+ import { r as initCommand } from "./init.command-wxC4cgUc.mjs";
2
+ import { cli } from "clibuilder";
3
+ //#region src/cli.ts
4
+ async function main() {
5
+ const app = cli({
6
+ name: "buddy-agent-harness",
7
+ version: "0.1.0",
8
+ description: "Initialize agent harness skill compatibility in consumer repositories."
9
+ });
10
+ try {
11
+ await app.command(initCommand).parse(process.argv);
12
+ } catch (error) {
13
+ process.stdout.write(`error: ${error instanceof Error ? error.message : "Invalid command."}\n`);
14
+ process.exitCode = 2;
15
+ }
16
+ }
17
+ //#endregion
18
+ export { main };
19
+
20
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { cli } from 'clibuilder'\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).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":";;;AAGA,eAAsB,OAAsB;CAC3C,MAAM,MAAM,IAAI;EACf,MAAM;EACN,SAAS;EACT,aAAa;CACd,CAAC;CACD,IAAI;EACH,MAAM,IAAI,QAAQ,WAAW,CAAC,CAAC,MAAM,QAAQ,IAAI;CAClD,SAAS,OAAO;EACf,QAAQ,OAAO,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,mBAAmB,GAAG;EAC9F,QAAQ,WAAW;CACpB;AACD"}
@@ -0,0 +1,57 @@
1
+ import { cli } from "clibuilder";
2
+ //#region src/harness-registry/harness-registry.d.ts
3
+ type HarnessName = "claude-code" | "cursor" | "codex" | "copilot-cli" | "gemini-cli" | "devin-desktop" | "windsurf";
4
+ /**
5
+ * `skillsDirectory` is the projection target a harness needs to see `.agents/skills`.
6
+ * A harness that reads `.agents/skills` natively has none, and is never projected into.
7
+ */
8
+ type Harness = {
9
+ name: HarnessName;
10
+ detect: string;
11
+ skillsDirectory?: string;
12
+ /** Set when this name has been superseded; the value is the name that replaces it. */
13
+ deprecated?: HarnessName;
14
+ };
15
+ /**
16
+ * Codex, Cursor, Copilot CLI, and Devin Desktop read `.agents/skills` directly, so they need no
17
+ * projection. Only Claude Code and Gemini CLI read solely their own directory.
18
+ *
19
+ * `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a
20
+ * deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps
21
+ * working, but new repositories should enable `devin-desktop` and have nothing written for them.
22
+ *
23
+ * See `.research/agentic-configuration-standards/` for the per-harness sources.
24
+ */
25
+ declare const harnessRegistry: readonly Harness[];
26
+ //#endregion
27
+ //#region src/initialize-harnesses/init.command.d.ts
28
+ declare const initCommand: cli.Command;
29
+ declare const harnessCommand: cli.Command;
30
+ declare function activate({ addCommand }: {
31
+ addCommand(command: typeof harnessCommand): void;
32
+ }): void;
33
+ //#endregion
34
+ //#region src/initialize-harnesses/initialize-harnesses.d.ts
35
+ type InitializeOptions = {
36
+ root: string;
37
+ harnesses?: HarnessName[];
38
+ copy?: boolean;
39
+ force?: boolean;
40
+ };
41
+ type InitializeResult = {
42
+ root: string;
43
+ harnesses: HarnessName[];
44
+ native: HarnessName[];
45
+ linked: HarnessName[];
46
+ /** Enabled harnesses whose name has been superseded, as `{ name, replacedBy }`. */
47
+ deprecated: {
48
+ name: HarnessName;
49
+ replacedBy: HarnessName;
50
+ }[];
51
+ skills: number;
52
+ copied: boolean;
53
+ };
54
+ declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
55
+ //#endregion
56
+ export { type HarnessName, type InitializeOptions, type InitializeResult, activate, harnessCommand, harnessRegistry, initCommand, initializeHarnesses };
57
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/harness-registry/harness-registry.ts","../src/initialize-harnesses/init.command.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;KAGY;;;;;KAaA;EACX,MAAM;EACN;EACA;;EAEA,aAAa;;;;;;;;;;;;cAaD,0BAA0B;;;cCxB1B,aAAa,IAAI;cAyDjB,gBAAgB,IAAI;iBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;KCrE1D;EACX;EACA,YAAY;EACZ;EACA;;KAEW;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;iBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as harnessRegistry, i as initializeHarnesses, n as harnessCommand, r as initCommand, t as activate } from "./init.command-wxC4cgUc.mjs";
2
+ export { activate, harnessCommand, harnessRegistry, initCommand, initializeHarnesses };
@@ -0,0 +1,203 @@
1
+ import { command, z } from "clibuilder";
2
+ import { encode } from "@toon-format/toon";
3
+ import { cpSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs";
4
+ import { dirname, join, relative } from "node:path";
5
+ //#region src/harness-registry/harness-registry.ts
6
+ /**
7
+ * Codex, Cursor, Copilot CLI, and Devin Desktop read `.agents/skills` directly, so they need no
8
+ * projection. Only Claude Code and Gemini CLI read solely their own directory.
9
+ *
10
+ * `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a
11
+ * deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps
12
+ * working, but new repositories should enable `devin-desktop` and have nothing written for them.
13
+ *
14
+ * See `.research/agentic-configuration-standards/` for the per-harness sources.
15
+ */
16
+ const harnessRegistry = [
17
+ {
18
+ name: "claude-code",
19
+ detect: ".claude",
20
+ skillsDirectory: ".claude/skills"
21
+ },
22
+ {
23
+ name: "cursor",
24
+ detect: ".cursor"
25
+ },
26
+ {
27
+ name: "codex",
28
+ detect: ".codex"
29
+ },
30
+ {
31
+ name: "copilot-cli",
32
+ detect: ".github/skills"
33
+ },
34
+ {
35
+ name: "gemini-cli",
36
+ detect: ".gemini",
37
+ skillsDirectory: ".gemini/skills"
38
+ },
39
+ {
40
+ name: "devin-desktop",
41
+ detect: ".devin"
42
+ },
43
+ {
44
+ name: "windsurf",
45
+ detect: ".windsurf",
46
+ skillsDirectory: ".windsurf/skills",
47
+ deprecated: "devin-desktop"
48
+ }
49
+ ];
50
+ /** Enabled whether or not the repository already contains their directories. */
51
+ const defaultHarnesses = ["claude-code", "cursor"];
52
+ function directoryExists(path) {
53
+ try {
54
+ return lstatSync(path).isDirectory();
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
60
+ function selectHarnesses(root, preferred) {
61
+ const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
62
+ return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.detect)));
63
+ }
64
+ //#endregion
65
+ //#region src/skill-projection/skill-projection.ts
66
+ function skillNames(skillsDirectory) {
67
+ return readdirSync(skillsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
68
+ }
69
+ function pathOccupied(path) {
70
+ try {
71
+ lstatSync(path);
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ function isCorrectLink(target, source) {
78
+ return lstatSync(target).isSymbolicLink() && readlinkSync(target) === relative(dirname(target), source);
79
+ }
80
+ /** Creates the canonical directory when absent, so a fresh repository reports zero skills. */
81
+ function countSkills(canonicalSkills) {
82
+ mkdirSync(canonicalSkills, { recursive: true });
83
+ return skillNames(canonicalSkills).length;
84
+ }
85
+ /**
86
+ * Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is
87
+ * preferred; a platform that refuses the link falls back to a copy. Returns the projected harnesses.
88
+ */
89
+ function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
90
+ const projections = harnesses.filter((harness) => harness.skillsDirectory).map((harness) => ({
91
+ harness,
92
+ target: join(root, harness.skillsDirectory)
93
+ }));
94
+ const conflicts = projections.filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills)).map(({ target }) => target);
95
+ if (conflicts.length && !force) throw new Error(`Refusing to replace existing skill targets:\n${conflicts.map((target) => `- ${target}`).join("\n")}`);
96
+ for (const { target } of projections) {
97
+ mkdirSync(dirname(target), { recursive: true });
98
+ if (pathOccupied(target) && isCorrectLink(target, canonicalSkills)) continue;
99
+ if (pathOccupied(target)) rmSync(target, {
100
+ recursive: true,
101
+ force: true
102
+ });
103
+ if (copy) {
104
+ cpSync(canonicalSkills, target, { recursive: true });
105
+ continue;
106
+ }
107
+ try {
108
+ symlinkSync(relative(dirname(target), canonicalSkills), target, "junction");
109
+ } catch (error) {
110
+ if (pathOccupied(target)) throw error;
111
+ cpSync(canonicalSkills, target, { recursive: true });
112
+ }
113
+ }
114
+ return projections.map(({ harness }) => harness.name);
115
+ }
116
+ //#endregion
117
+ //#region src/initialize-harnesses/initialize-harnesses.ts
118
+ function initializeHarnesses({ root, harnesses: preferred = [], copy = false, force = false }) {
119
+ const canonicalSkills = join(root, ".agents", "skills");
120
+ const skills = countSkills(canonicalSkills);
121
+ const harnesses = selectHarnesses(root, preferred);
122
+ const linked = projectSkills({
123
+ root,
124
+ canonicalSkills,
125
+ harnesses,
126
+ copy,
127
+ force
128
+ });
129
+ return {
130
+ root,
131
+ harnesses: harnesses.map((harness) => harness.name),
132
+ native: harnesses.filter((harness) => !harness.skillsDirectory).map((harness) => harness.name),
133
+ linked,
134
+ deprecated: harnesses.filter((harness) => harness.deprecated).map((harness) => ({
135
+ name: harness.name,
136
+ replacedBy: harness.deprecated
137
+ })),
138
+ skills,
139
+ copied: copy
140
+ };
141
+ }
142
+ //#endregion
143
+ //#region src/initialize-harnesses/init.command.ts
144
+ function write(value, format) {
145
+ process.stdout.write(`${format === "json" ? JSON.stringify(value) : encode(value)}\n`);
146
+ }
147
+ const initCommand = command({
148
+ name: "init",
149
+ description: "Set up canonical agent skills for the harnesses already enabled in a repository.",
150
+ options: {
151
+ root: {
152
+ description: "Repository or package directory. Defaults to the current directory.",
153
+ type: z.optional(z.string())
154
+ },
155
+ harness: {
156
+ description: "Comma-separated harnesses to enable in addition to Claude Code and Cursor, e.g. codex,windsurf.",
157
+ type: z.optional(z.string())
158
+ },
159
+ copy: {
160
+ description: "Copy skills instead of linking them.",
161
+ type: z.optional(z.boolean())
162
+ },
163
+ force: {
164
+ description: "Replace conflicting target skill directories.",
165
+ type: z.optional(z.boolean())
166
+ },
167
+ format: {
168
+ description: "Output format: toon (default) or json.",
169
+ type: z.optional(z.string()),
170
+ default: "toon"
171
+ }
172
+ },
173
+ run(args) {
174
+ try {
175
+ const format = args.format;
176
+ if (format !== "toon" && format !== "json") throw new Error("--format must be toon or json.");
177
+ const requested = args.harness?.split(",").map((name) => name.trim()).filter(Boolean);
178
+ const unsupported = requested?.filter((name) => !harnessRegistry.some((harness) => harness.name === name));
179
+ if (unsupported?.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
180
+ write(initializeHarnesses({
181
+ root: args.root ?? process.cwd(),
182
+ ...requested?.length ? { harnesses: requested } : {},
183
+ ...args.copy === void 0 ? {} : { copy: args.copy },
184
+ ...args.force === void 0 ? {} : { force: args.force }
185
+ }), format);
186
+ } catch (error) {
187
+ process.stderr.write(`error: ${error instanceof Error ? error.message : "Harness initialization failed."}\n`);
188
+ process.exitCode = 1;
189
+ }
190
+ }
191
+ });
192
+ const harnessCommand = command({
193
+ name: "harness",
194
+ description: "Commands for configuring agent harness compatibility.",
195
+ commands: [initCommand]
196
+ });
197
+ function activate({ addCommand }) {
198
+ addCommand(harnessCommand);
199
+ }
200
+ //#endregion
201
+ export { harnessRegistry as a, initializeHarnesses as i, harnessCommand as n, initCommand as r, activate as t };
202
+
203
+ //# sourceMappingURL=init.command-wxC4cgUc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.command-wxC4cgUc.mjs","names":[],"sources":["../src/harness-registry/harness-registry.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts","../src/initialize-harnesses/init.command.ts"],"sourcesContent":["import { lstatSync } from 'node:fs'\nimport { join } from 'node:path'\n\nexport type HarnessName =\n\t| 'claude-code'\n\t| 'cursor'\n\t| 'codex'\n\t| 'copilot-cli'\n\t| 'gemini-cli'\n\t| 'devin-desktop'\n\t| 'windsurf'\n\n/**\n * `skillsDirectory` is the projection target a harness needs to see `.agents/skills`.\n * A harness that reads `.agents/skills` natively has none, and is never projected into.\n */\nexport type Harness = {\n\tname: HarnessName\n\tdetect: string\n\tskillsDirectory?: string\n\t/** Set when this name has been superseded; the value is the name that replaces it. */\n\tdeprecated?: HarnessName\n}\n\n/**\n * Codex, Cursor, Copilot CLI, and Devin Desktop read `.agents/skills` directly, so they need no\n * projection. Only Claude Code and Gemini CLI read solely their own directory.\n *\n * `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a\n * deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps\n * working, but new repositories should enable `devin-desktop` and have nothing written for them.\n *\n * See `.research/agentic-configuration-standards/` for the per-harness sources.\n */\nexport const harnessRegistry: readonly Harness[] = [\n\t{ name: 'claude-code', detect: '.claude', skillsDirectory: '.claude/skills' },\n\t{ name: 'cursor', detect: '.cursor' },\n\t{ name: 'codex', detect: '.codex' },\n\t{ name: 'copilot-cli', detect: '.github/skills' },\n\t{ name: 'gemini-cli', detect: '.gemini', skillsDirectory: '.gemini/skills' },\n\t{ name: 'devin-desktop', detect: '.devin' },\n\t{ name: 'windsurf', detect: '.windsurf', skillsDirectory: '.windsurf/skills', deprecated: 'devin-desktop' },\n]\n\n/** Enabled whether or not the repository already contains their directories. */\nconst defaultHarnesses: readonly HarnessName[] = ['claude-code', 'cursor']\n\nfunction directoryExists(path: string): boolean {\n\ttry {\n\t\treturn lstatSync(path).isDirectory()\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */\nexport function selectHarnesses(root: string, preferred: readonly HarnessName[]): Harness[] {\n\tconst enabled = new Set<HarnessName>([...defaultHarnesses, ...preferred])\n\treturn harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.detect)))\n}\n","import { cpSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync } from 'node:fs'\nimport { dirname, join, relative } from 'node:path'\nimport type { Harness, HarnessName } from '../harness-registry/harness-registry.ts'\n\nfunction skillNames(skillsDirectory: string): string[] {\n\treturn readdirSync(skillsDirectory, { withFileTypes: true })\n\t\t.filter((entry) => entry.isDirectory())\n\t\t.map((entry) => entry.name)\n\t\t.sort()\n}\n\nfunction pathOccupied(path: string): boolean {\n\ttry {\n\t\tlstatSync(path)\n\t\treturn true\n\t} catch {\n\t\treturn false\n\t}\n}\n\nfunction isCorrectLink(target: string, source: string): boolean {\n\treturn lstatSync(target).isSymbolicLink() && readlinkSync(target) === relative(dirname(target), source)\n}\n\n/** Creates the canonical directory when absent, so a fresh repository reports zero skills. */\nexport function countSkills(canonicalSkills: string): number {\n\tmkdirSync(canonicalSkills, { recursive: true })\n\treturn skillNames(canonicalSkills).length\n}\n\nexport type ProjectSkillsOptions = {\n\troot: string\n\tcanonicalSkills: string\n\tharnesses: readonly Harness[]\n\tcopy: boolean\n\tforce: boolean\n}\n\n/**\n * Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is\n * preferred; a platform that refuses the link falls back to a copy. Returns the projected harnesses.\n */\nexport function projectSkills({ root, canonicalSkills, harnesses, copy, force }: ProjectSkillsOptions): HarnessName[] {\n\tconst projections = harnesses\n\t\t.filter((harness) => harness.skillsDirectory)\n\t\t.map((harness) => ({\n\t\t\tharness,\n\t\t\ttarget: join(root, harness.skillsDirectory as string),\n\t\t}))\n\n\tconst conflicts = projections\n\t\t.filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills))\n\t\t.map(({ target }) => target)\n\n\tif (conflicts.length && !force) {\n\t\tthrow new Error(\n\t\t\t`Refusing to replace existing skill targets:\\n${conflicts.map((target) => `- ${target}`).join('\\n')}`,\n\t\t)\n\t}\n\n\tfor (const { target } of projections) {\n\t\tmkdirSync(dirname(target), { recursive: true })\n\t\tif (pathOccupied(target) && isCorrectLink(target, canonicalSkills)) continue\n\t\tif (pathOccupied(target)) rmSync(target, { recursive: true, force: true })\n\t\tif (copy) {\n\t\t\tcpSync(canonicalSkills, target, { recursive: true })\n\t\t\tcontinue\n\t\t}\n\t\ttry {\n\t\t\tsymlinkSync(relative(dirname(target), canonicalSkills), target, 'junction')\n\t\t} catch (error) {\n\t\t\tif (pathOccupied(target)) throw error\n\t\t\tcpSync(canonicalSkills, target, { recursive: true })\n\t\t}\n\t}\n\n\treturn projections.map(({ harness }) => harness.name)\n}\n","import { join } from 'node:path'\nimport { type HarnessName, selectHarnesses } from '../harness-registry/harness-registry.ts'\nimport { countSkills, projectSkills } from '../skill-projection/skill-projection.ts'\n\nexport type InitializeOptions = {\n\troot: string\n\tharnesses?: HarnessName[]\n\tcopy?: boolean\n\tforce?: boolean\n}\nexport type InitializeResult = {\n\troot: string\n\tharnesses: HarnessName[]\n\tnative: HarnessName[]\n\tlinked: HarnessName[]\n\t/** Enabled harnesses whose name has been superseded, as `{ name, replacedBy }`. */\n\tdeprecated: { name: HarnessName; replacedBy: HarnessName }[]\n\tskills: number\n\tcopied: boolean\n}\n\nexport function initializeHarnesses({\n\troot,\n\tharnesses: preferred = [],\n\tcopy = false,\n\tforce = false,\n}: InitializeOptions): InitializeResult {\n\tconst canonicalSkills = join(root, '.agents', 'skills')\n\tconst skills = countSkills(canonicalSkills)\n\tconst harnesses = selectHarnesses(root, preferred)\n\tconst linked = projectSkills({ root, canonicalSkills, harnesses, copy, force })\n\n\treturn {\n\t\troot,\n\t\tharnesses: harnesses.map((harness) => harness.name),\n\t\tnative: harnesses.filter((harness) => !harness.skillsDirectory).map((harness) => harness.name),\n\t\tlinked,\n\t\tdeprecated: harnesses\n\t\t\t.filter((harness) => harness.deprecated)\n\t\t\t.map((harness) => ({ name: harness.name, replacedBy: harness.deprecated as HarnessName })),\n\t\tskills,\n\t\tcopied: copy,\n\t}\n}\n","import { encode } from '@toon-format/toon'\nimport type { cli } from 'clibuilder'\nimport { command, z } from 'clibuilder'\nimport { type HarnessName, harnessRegistry } from '../harness-registry/harness-registry.ts'\nimport { initializeHarnesses } from './initialize-harnesses.ts'\n\nfunction write(value: object, format: 'json' | 'toon' | undefined): void {\n\tprocess.stdout.write(`${format === 'json' ? JSON.stringify(value) : encode(value)}\\n`)\n}\n\nexport const initCommand: cli.Command = command({\n\tname: 'init',\n\tdescription: 'Set up canonical agent skills for the harnesses already enabled in a repository.',\n\toptions: {\n\t\troot: {\n\t\t\tdescription: 'Repository or package directory. Defaults to the current directory.',\n\t\t\ttype: z.optional(z.string()),\n\t\t},\n\t\tharness: {\n\t\t\tdescription: 'Comma-separated harnesses to enable in addition to Claude Code and Cursor, e.g. codex,windsurf.',\n\t\t\ttype: z.optional(z.string()),\n\t\t},\n\t\tcopy: {\n\t\t\tdescription: 'Copy skills instead of linking them.',\n\t\t\ttype: z.optional(z.boolean()),\n\t\t},\n\t\tforce: {\n\t\t\tdescription: 'Replace conflicting target skill directories.',\n\t\t\ttype: z.optional(z.boolean()),\n\t\t},\n\t\tformat: {\n\t\t\tdescription: 'Output format: toon (default) or json.',\n\t\t\ttype: z.optional(z.string()),\n\t\t\tdefault: 'toon',\n\t\t},\n\t},\n\trun(args) {\n\t\ttry {\n\t\t\tconst format = args.format\n\t\t\tif (format !== 'toon' && format !== 'json') throw new Error('--format must be toon or json.')\n\t\t\tconst requested = args.harness\n\t\t\t\t?.split(',')\n\t\t\t\t.map((name) => name.trim())\n\t\t\t\t.filter(Boolean)\n\t\t\tconst unsupported = requested?.filter((name) => !harnessRegistry.some((harness) => harness.name === name))\n\t\t\tif (unsupported?.length)\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unsupported harness: ${unsupported.join(', ')}. Supported: ${harnessRegistry\n\t\t\t\t\t\t.map((harness) => harness.name)\n\t\t\t\t\t\t.join(', ')}.`,\n\t\t\t\t)\n\t\t\twrite(\n\t\t\t\tinitializeHarnesses({\n\t\t\t\t\troot: args.root ?? process.cwd(),\n\t\t\t\t\t...(requested?.length ? { harnesses: requested as HarnessName[] } : {}),\n\t\t\t\t\t...(args.copy === undefined ? {} : { copy: args.copy }),\n\t\t\t\t\t...(args.force === undefined ? {} : { force: args.force }),\n\t\t\t\t}),\n\t\t\t\tformat,\n\t\t\t)\n\t\t} catch (error) {\n\t\t\tprocess.stderr.write(`error: ${error instanceof Error ? error.message : 'Harness initialization failed.'}\\n`)\n\t\t\tprocess.exitCode = 1\n\t\t}\n\t},\n})\n\nexport const harnessCommand: cli.Command = command({\n\tname: 'harness',\n\tdescription: 'Commands for configuring agent harness compatibility.',\n\tcommands: [initCommand],\n})\n\nexport function activate({ addCommand }: { addCommand(command: typeof harnessCommand): void }): void {\n\taddCommand(harnessCommand)\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkCA,MAAa,kBAAsC;CAClD;EAAE,MAAM;EAAe,QAAQ;EAAW,iBAAiB;CAAiB;CAC5E;EAAE,MAAM;EAAU,QAAQ;CAAU;CACpC;EAAE,MAAM;EAAS,QAAQ;CAAS;CAClC;EAAE,MAAM;EAAe,QAAQ;CAAiB;CAChD;EAAE,MAAM;EAAc,QAAQ;EAAW,iBAAiB;CAAiB;CAC3E;EAAE,MAAM;EAAiB,QAAQ;CAAS;CAC1C;EAAE,MAAM;EAAY,QAAQ;EAAa,iBAAiB;EAAoB,YAAY;CAAgB;AAC3G;;AAGA,MAAM,mBAA2C,CAAC,eAAe,QAAQ;AAEzE,SAAS,gBAAgB,MAAuB;CAC/C,IAAI;EACH,OAAO,UAAU,IAAI,CAAC,CAAC,YAAY;CACpC,QAAQ;EACP,OAAO;CACR;AACD;;AAGA,SAAgB,gBAAgB,MAAc,WAA8C;CAC3F,MAAM,0BAAU,IAAI,IAAiB,CAAC,GAAG,kBAAkB,GAAG,SAAS,CAAC;CACxE,OAAO,gBAAgB,QAAQ,YAAY,QAAQ,IAAI,QAAQ,IAAI,KAAK,gBAAgB,KAAK,MAAM,QAAQ,MAAM,CAAC,CAAC;AACpH;;;ACvDA,SAAS,WAAW,iBAAmC;CACtD,OAAO,YAAY,iBAAiB,EAAE,eAAe,KAAK,CAAC,CAAC,CAC1D,QAAQ,UAAU,MAAM,YAAY,CAAC,CAAC,CACtC,KAAK,UAAU,MAAM,IAAI,CAAC,CAC1B,KAAK;AACR;AAEA,SAAS,aAAa,MAAuB;CAC5C,IAAI;EACH,UAAU,IAAI;EACd,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD;AAEA,SAAS,cAAc,QAAgB,QAAyB;CAC/D,OAAO,UAAU,MAAM,CAAC,CAAC,eAAe,KAAK,aAAa,MAAM,MAAM,SAAS,QAAQ,MAAM,GAAG,MAAM;AACvG;;AAGA,SAAgB,YAAY,iBAAiC;CAC5D,UAAU,iBAAiB,EAAE,WAAW,KAAK,CAAC;CAC9C,OAAO,WAAW,eAAe,CAAC,CAAC;AACpC;;;;;AAcA,SAAgB,cAAc,EAAE,MAAM,iBAAiB,WAAW,MAAM,SAA8C;CACrH,MAAM,cAAc,UAClB,QAAQ,YAAY,QAAQ,eAAe,CAAC,CAC5C,KAAK,aAAa;EAClB;EACA,QAAQ,KAAK,MAAM,QAAQ,eAAyB;CACrD,EAAE;CAEH,MAAM,YAAY,YAChB,QAAQ,EAAE,aAAa,aAAa,MAAM,KAAK,CAAC,cAAc,QAAQ,eAAe,CAAC,CAAC,CACvF,KAAK,EAAE,aAAa,MAAM;CAE5B,IAAI,UAAU,UAAU,CAAC,OACxB,MAAM,IAAI,MACT,gDAAgD,UAAU,KAAK,WAAW,KAAK,QAAQ,CAAC,CAAC,KAAK,IAAI,GACnG;CAGD,KAAK,MAAM,EAAE,YAAY,aAAa;EACrC,UAAU,QAAQ,MAAM,GAAG,EAAE,WAAW,KAAK,CAAC;EAC9C,IAAI,aAAa,MAAM,KAAK,cAAc,QAAQ,eAAe,GAAG;EACpE,IAAI,aAAa,MAAM,GAAG,OAAO,QAAQ;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EACzE,IAAI,MAAM;GACT,OAAO,iBAAiB,QAAQ,EAAE,WAAW,KAAK,CAAC;GACnD;EACD;EACA,IAAI;GACH,YAAY,SAAS,QAAQ,MAAM,GAAG,eAAe,GAAG,QAAQ,UAAU;EAC3E,SAAS,OAAO;GACf,IAAI,aAAa,MAAM,GAAG,MAAM;GAChC,OAAO,iBAAiB,QAAQ,EAAE,WAAW,KAAK,CAAC;EACpD;CACD;CAEA,OAAO,YAAY,KAAK,EAAE,cAAc,QAAQ,IAAI;AACrD;;;ACxDA,SAAgB,oBAAoB,EACnC,MACA,WAAW,YAAY,CAAC,GACxB,OAAO,OACP,QAAQ,SAC+B;CACvC,MAAM,kBAAkB,KAAK,MAAM,WAAW,QAAQ;CACtD,MAAM,SAAS,YAAY,eAAe;CAC1C,MAAM,YAAY,gBAAgB,MAAM,SAAS;CACjD,MAAM,SAAS,cAAc;EAAE;EAAM;EAAiB;EAAW;EAAM;CAAM,CAAC;CAE9E,OAAO;EACN;EACA,WAAW,UAAU,KAAK,YAAY,QAAQ,IAAI;EAClD,QAAQ,UAAU,QAAQ,YAAY,CAAC,QAAQ,eAAe,CAAC,CAAC,KAAK,YAAY,QAAQ,IAAI;EAC7F;EACA,YAAY,UACV,QAAQ,YAAY,QAAQ,UAAU,CAAC,CACvC,KAAK,aAAa;GAAE,MAAM,QAAQ;GAAM,YAAY,QAAQ;EAA0B,EAAE;EAC1F;EACA,QAAQ;CACT;AACD;;;ACrCA,SAAS,MAAM,OAAe,QAA2C;CACxE,QAAQ,OAAO,MAAM,GAAG,WAAW,SAAS,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK,EAAE,GAAG;AACtF;AAEA,MAAa,cAA2B,QAAQ;CAC/C,MAAM;CACN,aAAa;CACb,SAAS;EACR,MAAM;GACL,aAAa;GACb,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;EAC5B;EACA,SAAS;GACR,aAAa;GACb,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;EAC5B;EACA,MAAM;GACL,aAAa;GACb,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC7B;EACA,OAAO;GACN,aAAa;GACb,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC;EAC7B;EACA,QAAQ;GACP,aAAa;GACb,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC;GAC3B,SAAS;EACV;CACD;CACA,IAAI,MAAM;EACT,IAAI;GACH,MAAM,SAAS,KAAK;GACpB,IAAI,WAAW,UAAU,WAAW,QAAQ,MAAM,IAAI,MAAM,gCAAgC;GAC5F,MAAM,YAAY,KAAK,SACpB,MAAM,GAAG,CAAC,CACX,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,OAAO,OAAO;GAChB,MAAM,cAAc,WAAW,QAAQ,SAAS,CAAC,gBAAgB,MAAM,YAAY,QAAQ,SAAS,IAAI,CAAC;GACzG,IAAI,aAAa,QAChB,MAAM,IAAI,MACT,wBAAwB,YAAY,KAAK,IAAI,EAAE,eAAe,gBAC5D,KAAK,YAAY,QAAQ,IAAI,CAAC,CAC9B,KAAK,IAAI,EAAE,EACd;GACD,MACC,oBAAoB;IACnB,MAAM,KAAK,QAAQ,QAAQ,IAAI;IAC/B,GAAI,WAAW,SAAS,EAAE,WAAW,UAA2B,IAAI,CAAC;IACrE,GAAI,KAAK,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,KAAK,KAAK;IACrD,GAAI,KAAK,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM;GACzD,CAAC,GACD,MACD;EACD,SAAS,OAAO;GACf,QAAQ,OAAO,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,iCAAiC,GAAG;GAC5G,QAAQ,WAAW;EACpB;CACD;AACD,CAAC;AAED,MAAa,iBAA8B,QAAQ;CAClD,MAAM;CACN,aAAa;CACb,UAAU,CAAC,WAAW;AACvB,CAAC;AAED,SAAgB,SAAS,EAAE,cAA0E;CACpG,WAAW,cAAc;AAC1B"}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "buddy-agent-harness",
3
+ "version": "0.1.0",
4
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
5
+ "keywords": [
6
+ "repobuddy",
7
+ "agent",
8
+ "skills",
9
+ "claude-code",
10
+ "codex",
11
+ "cursor"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/repobuddy/buddy-agent-harness.git"
17
+ },
18
+ "bugs": "https://github.com/repobuddy/buddy-agent-harness/issues",
19
+ "homepage": "https://github.com/repobuddy/buddy-agent-harness#readme",
20
+ "author": {
21
+ "name": "repobuddy"
22
+ },
23
+ "type": "module",
24
+ "bin": {
25
+ "buddy-agent-harness": "./bin/buddy-agent-harness.mjs"
26
+ },
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.mts",
30
+ "default": "./dist/index.mjs"
31
+ }
32
+ },
33
+ "files": [
34
+ "bin",
35
+ "dist",
36
+ "src",
37
+ "!**/*.{test,spec}.ts",
38
+ "skills",
39
+ "plugin.json",
40
+ ".plugin",
41
+ ".claude-plugin",
42
+ ".cursor-plugin",
43
+ ".codex-plugin"
44
+ ],
45
+ "scripts": {
46
+ "build": "tsdown",
47
+ "coverage": "vitest run --coverage",
48
+ "dev": "node bin/buddy-agent-harness.mjs",
49
+ "prepack": "pnpm build",
50
+ "test": "vitest run",
51
+ "test:dist": "tsc --noEmit -p tsconfig.dist.json && vitest run --config vitest.dist.config.ts",
52
+ "typecheck": "tsc --noEmit"
53
+ },
54
+ "engines": {
55
+ "node": ">=22"
56
+ },
57
+ "dependencies": {
58
+ "@toon-format/toon": "^4.1.1",
59
+ "clibuilder": "^10.0.0"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^24.12.4",
63
+ "@vitest/coverage-v8": "^4.1.0",
64
+ "tsdown": "^0.22.0",
65
+ "tsx": "^4.22.3",
66
+ "typescript": "^7.0.0",
67
+ "vitest": "^4.1.0"
68
+ }
69
+ }
package/plugin.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "buddy-agent-harness",
3
+ "version": "0.1.0",
4
+ "description": "Initialize standards-based agent configuration across coding-agent harnesses",
5
+ "author": {
6
+ "name": "repobuddy"
7
+ },
8
+ "repository": "https://github.com/repobuddy/buddy-agent-harness",
9
+ "license": "MIT",
10
+ "keywords": ["agent", "skills", "harness", "claude-code", "codex"],
11
+ "skills": "./skills/",
12
+ "category": "productivity",
13
+ "tags": ["agent", "skills"]
14
+ }
@@ -0,0 +1,29 @@
1
+ # Harness Init
2
+
3
+ Give a repository one canonical agent configuration — a root `AGENTS.md` and an `.agents/` tree — and bridge the harnesses that cannot read it directly.
4
+
5
+ ## What it does
6
+
7
+ The skill runs a five-phase workflow: survey the repository, classify what it finds, confirm the plan with you, apply it, then verify.
8
+
9
+ It detects agent configuration you already have — instruction files, skills, commands, subagents, rules, MCP servers, hooks — and either consolidates it into the canonical source, links it back out, or reports it as canonical-only where no safe cross-harness mapping exists. It never removes or rewrites a file you authored without asking first.
10
+
11
+ ## Why the projection step is small
12
+
13
+ Codex, Cursor, GitHub Copilot CLI, and Devin Desktop read `.agents/skills/` natively, so they need nothing. Only Claude Code and Gemini CLI need a link:
14
+
15
+ ```sh
16
+ npx -y buddy-agent-harness init
17
+ ```
18
+
19
+ Claude Code also reads `CLAUDE.md` rather than `AGENTS.md`, so the skill sets up a `CLAUDE.md` that imports `@AGENTS.md`.
20
+
21
+ `references/standard.md` defines the baseline every repository gets from the open standards. `references/harnesses/<harness>.md` covers what each harness needs on top of that baseline — and how well-sourced each claim is. `SKILL.md` routes to them directly.
22
+
23
+ ## The part that needs care
24
+
25
+ Linking is easy; frontmatter is not. Each harness recognizes a different set of `SKILL.md` fields and silently drops the rest, so anything that must hold everywhere belongs in the Markdown body rather than in a harness-specific field. See `references/frontmatter.md`.
26
+
27
+ ## Boundaries
28
+
29
+ Local agent configuration only. The skill does not invent instructions, convert tool settings without a documented mapping, or change CI, GitHub settings, security scanning, or branch rules.