buddy-agent-harness 1.0.1 → 1.2.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/CHANGELOG.md +44 -0
- package/dist/cli.mjs +2166 -702
- package/dist/index.d.mts +10 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +597 -431
- package/dist/index.mjs.map +1 -1
- package/dist/skill-scripts/doctor.mjs +74 -67
- package/dist/skill-scripts/init.mjs +74 -67
- package/dist/skill-scripts/mcp.mjs +74 -67
- package/llms.txt +8 -6
- package/package.json +5 -4
- package/plugin.json +1 -1
- package/skills/doctor-buddy-agent-harness/README.md +1 -1
- package/skills/doctor-buddy-agent-harness/SKILL.md +9 -2
- package/skills/doctor-buddy-agent-harness/references/worktrees.md +17 -0
- package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +74 -67
- package/skills/enhance/SKILL.md +18 -10
- package/skills/enhance/references/agent-worktrees.md +59 -0
- package/skills/enhance/references/list-identifiers.history.md +9 -1
- package/skills/enhance/references/list-identifiers.md +6 -9
- package/skills/enhance/references/references-as-links.history.md +9 -0
- package/skills/enhance/references/references-as-links.md +76 -0
- package/skills/enhance/scripts/doctor.mjs +74 -67
- package/skills/init-buddy-agent-harness/SKILL.md +8 -5
- package/skills/init-buddy-agent-harness/references/agent-worktrees.md +59 -0
- package/skills/init-buddy-agent-harness/scripts/doctor.mjs +74 -67
- package/skills/init-buddy-agent-harness/scripts/init.mjs +74 -67
- package/skills/repair/README.md +2 -2
- package/skills/repair/SKILL.md +3 -3
- package/skills/repair/scripts/doctor.mjs +74 -67
- package/skills/repair/scripts/mcp.mjs +74 -67
- package/src/cli.ts +13 -3
- package/src/command-output/command-output.ts +28 -5
- package/src/dep-plugins/dep-plugins.command.ts +4 -0
- package/src/diagnose-bridges/doctor-guidance.ts +66 -1
- package/src/diagnose-bridges/doctor.command.ts +69 -49
- package/src/diagnose-bridges/git-bridge-state.ts +5 -0
- package/src/diagnose-worktrees/diagnose-worktrees.ts +69 -0
- package/src/governance-overrides/governance.command.ts +13 -8
- package/src/index.ts +1 -0
- package/src/initialize-harnesses/init.command.ts +9 -3
- package/src/project-mcp/mcp.command.ts +15 -9
- package/src/reconcile-mcp/reconcile.command.ts +17 -11
- package/src/references/references.ts +1 -0
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { declaredDependencies, listReferences, listReferences as listReferences$1, packageDir, parseReferenceName, referenceLayers as referenceLayers$1, referenceNames, repoReferencesDir, resolveReference, searchReferences, skillsDirectories } from "cyber-agentkit";
|
|
2
2
|
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { cli, command, t } from "clibuilder";
|
|
4
|
-
import { CliError, document, exitCodes, isCliError, printAnswer, result } from "clibuilder/axi";
|
|
4
|
+
import { CliError, document, empty, exitCodes, homeHeader, isCliError, printAnswer, ref, result } from "clibuilder/axi";
|
|
5
5
|
import { homedir, platform } from "node:os";
|
|
6
6
|
import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { parse, stringify } from "smol-toml";
|
|
@@ -10,15 +10,276 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { parseTOML } from "toml-eslint-parser";
|
|
12
12
|
import { findNodeAtLocation, parseTree } from "jsonc-parser";
|
|
13
|
+
//#region src/harness-registry/harness-registry.ts
|
|
14
|
+
/**
|
|
15
|
+
* Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
|
|
16
|
+
* drop a projection it may need.
|
|
17
|
+
*/
|
|
18
|
+
function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
|
|
19
|
+
const listed = directories ?? [];
|
|
20
|
+
const [first] = listed;
|
|
21
|
+
if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
|
|
22
|
+
return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
|
|
26
|
+
* reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
|
|
27
|
+
* E-GEM-03, E-COPILOT-04, E-CUR-08).
|
|
28
|
+
*/
|
|
29
|
+
const globalInstructions = ".agents/AGENTS.md";
|
|
30
|
+
/**
|
|
31
|
+
* Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
|
|
32
|
+
* (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
|
|
33
|
+
*/
|
|
34
|
+
const harnessRegistry = [
|
|
35
|
+
{
|
|
36
|
+
name: "claude-code",
|
|
37
|
+
project: {
|
|
38
|
+
detect: ".claude",
|
|
39
|
+
...skillsProjection("claude-code", "project"),
|
|
40
|
+
shadowedBy: [
|
|
41
|
+
"CLAUDE.md",
|
|
42
|
+
".claude/CLAUDE.md",
|
|
43
|
+
"CLAUDE.local.md"
|
|
44
|
+
],
|
|
45
|
+
mcpConfig: {
|
|
46
|
+
path: ".mcp.json",
|
|
47
|
+
key: "mcpServers",
|
|
48
|
+
format: "json",
|
|
49
|
+
dialect: "claude-code"
|
|
50
|
+
},
|
|
51
|
+
nonstandard: [
|
|
52
|
+
{
|
|
53
|
+
path: ".claude/commands",
|
|
54
|
+
shape: "directory",
|
|
55
|
+
kind: "command"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
path: ".claude/rules",
|
|
59
|
+
shape: "directory",
|
|
60
|
+
kind: "rule"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
path: ".claude/agents",
|
|
64
|
+
shape: "directory",
|
|
65
|
+
kind: "subagent"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
user: {
|
|
70
|
+
detect: ".claude",
|
|
71
|
+
...skillsProjection("claude-code", "user"),
|
|
72
|
+
relocatedBy: "CLAUDE_CONFIG_DIR",
|
|
73
|
+
emptyRelocatesToStart: true,
|
|
74
|
+
instructionBridge: {
|
|
75
|
+
kind: "import",
|
|
76
|
+
path: ".claude/CLAUDE.md",
|
|
77
|
+
line: `@~/${globalInstructions}`
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "cursor",
|
|
83
|
+
project: {
|
|
84
|
+
detect: ".cursor",
|
|
85
|
+
...skillsProjection("cursor", "project"),
|
|
86
|
+
mcpConfig: {
|
|
87
|
+
path: ".cursor/mcp.json",
|
|
88
|
+
key: "mcpServers",
|
|
89
|
+
format: "json",
|
|
90
|
+
dialect: "cursor"
|
|
91
|
+
},
|
|
92
|
+
nonstandard: [
|
|
93
|
+
{
|
|
94
|
+
path: ".cursorrules",
|
|
95
|
+
shape: "file",
|
|
96
|
+
kind: "instructions"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
path: ".cursor/rules",
|
|
100
|
+
shape: "directory",
|
|
101
|
+
kind: "rule"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: ".cursor/commands",
|
|
105
|
+
shape: "directory",
|
|
106
|
+
kind: "command"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: ".cursor/skills",
|
|
110
|
+
shape: "directory",
|
|
111
|
+
kind: "skill"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
115
|
+
user: {
|
|
116
|
+
detect: ".cursor",
|
|
117
|
+
...skillsProjection("cursor", "user")
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "codex",
|
|
122
|
+
project: {
|
|
123
|
+
detect: ".codex",
|
|
124
|
+
...skillsProjection("codex", "project"),
|
|
125
|
+
mcpConfig: {
|
|
126
|
+
path: ".codex/config.toml",
|
|
127
|
+
key: "mcp_servers",
|
|
128
|
+
format: "toml",
|
|
129
|
+
dialect: "codex",
|
|
130
|
+
shared: true
|
|
131
|
+
},
|
|
132
|
+
nonstandard: [{
|
|
133
|
+
path: ".codex/skills",
|
|
134
|
+
shape: "directory",
|
|
135
|
+
kind: "skill"
|
|
136
|
+
}]
|
|
137
|
+
},
|
|
138
|
+
user: {
|
|
139
|
+
detect: ".codex",
|
|
140
|
+
...skillsProjection("codex", "user"),
|
|
141
|
+
instructionBridge: {
|
|
142
|
+
kind: "symlink",
|
|
143
|
+
path: ".codex/AGENTS.md"
|
|
144
|
+
},
|
|
145
|
+
shadowedBy: [".codex/AGENTS.override.md"],
|
|
146
|
+
relocatedBy: "CODEX_HOME"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "copilot-cli",
|
|
151
|
+
project: {
|
|
152
|
+
detect: ".github/skills",
|
|
153
|
+
...skillsProjection("copilot-cli", "project"),
|
|
154
|
+
nonstandard: [
|
|
155
|
+
{
|
|
156
|
+
path: ".github/copilot-instructions.md",
|
|
157
|
+
shape: "file",
|
|
158
|
+
kind: "instructions"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
path: ".github/instructions",
|
|
162
|
+
shape: "directory",
|
|
163
|
+
kind: "instructions"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
path: ".github/skills",
|
|
167
|
+
shape: "directory",
|
|
168
|
+
kind: "skill"
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
user: {
|
|
173
|
+
detect: ".copilot",
|
|
174
|
+
...skillsProjection("copilot-cli", "user"),
|
|
175
|
+
relocatedBy: "COPILOT_HOME",
|
|
176
|
+
instructionBridge: {
|
|
177
|
+
kind: "symlink",
|
|
178
|
+
path: ".copilot/copilot-instructions.md"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "gemini-cli",
|
|
184
|
+
project: {
|
|
185
|
+
detect: ".gemini",
|
|
186
|
+
...skillsProjection("gemini-cli", "project"),
|
|
187
|
+
instructionBridge: {
|
|
188
|
+
kind: "settings-entry",
|
|
189
|
+
path: ".gemini/settings.json",
|
|
190
|
+
key: "context.fileName"
|
|
191
|
+
},
|
|
192
|
+
mcpConfig: {
|
|
193
|
+
path: ".gemini/settings.json",
|
|
194
|
+
key: "mcpServers",
|
|
195
|
+
format: "json",
|
|
196
|
+
dialect: "gemini-cli",
|
|
197
|
+
shared: true
|
|
198
|
+
},
|
|
199
|
+
nonstandard: [{
|
|
200
|
+
path: "GEMINI.md",
|
|
201
|
+
shape: "file",
|
|
202
|
+
kind: "instructions"
|
|
203
|
+
}, {
|
|
204
|
+
path: ".gemini/skills",
|
|
205
|
+
shape: "directory",
|
|
206
|
+
kind: "skill"
|
|
207
|
+
}]
|
|
208
|
+
},
|
|
209
|
+
user: {
|
|
210
|
+
detect: ".gemini",
|
|
211
|
+
...skillsProjection("gemini-cli", "user"),
|
|
212
|
+
instructionBridge: {
|
|
213
|
+
kind: "symlink",
|
|
214
|
+
path: ".gemini/GEMINI.md"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "devin-desktop",
|
|
220
|
+
project: { detect: ".devin" }
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "windsurf",
|
|
224
|
+
project: {
|
|
225
|
+
detect: ".windsurf",
|
|
226
|
+
skillsDirectory: ".windsurf/skills",
|
|
227
|
+
nonstandard: [{
|
|
228
|
+
path: ".windsurfrules",
|
|
229
|
+
shape: "file",
|
|
230
|
+
kind: "instructions"
|
|
231
|
+
}, {
|
|
232
|
+
path: ".windsurf/rules",
|
|
233
|
+
shape: "directory",
|
|
234
|
+
kind: "rule"
|
|
235
|
+
}]
|
|
236
|
+
},
|
|
237
|
+
deprecated: "devin-desktop"
|
|
238
|
+
}
|
|
239
|
+
];
|
|
240
|
+
const harnessNames = new Set(harnessRegistry.map((harness) => harness.name));
|
|
241
|
+
function isHarnessName(value) {
|
|
242
|
+
return harnessNames.has(value);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* An unknown name throws rather than being dropped — silently falling back to the defaults would
|
|
246
|
+
* misreport what was diagnosed.
|
|
247
|
+
*/
|
|
248
|
+
function parseHarnesses(value) {
|
|
249
|
+
const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
250
|
+
const unsupported = requested.filter((name) => !isHarnessName(name));
|
|
251
|
+
if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
|
|
252
|
+
return requested.filter(isHarnessName);
|
|
253
|
+
}
|
|
254
|
+
/** Enabled whether or not the repository already contains their directories. */
|
|
255
|
+
const defaultHarnesses = ["claude-code", "cursor"];
|
|
256
|
+
function directoryExists(path) {
|
|
257
|
+
try {
|
|
258
|
+
return lstatSync(path).isDirectory();
|
|
259
|
+
} catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
|
|
264
|
+
function selectHarnesses(root, preferred) {
|
|
265
|
+
const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
|
|
266
|
+
return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
|
|
267
|
+
}
|
|
268
|
+
//#endregion
|
|
13
269
|
//#region src/command-output/command-output.ts
|
|
14
270
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
271
|
+
* AXI §6: a failure is a coded error on stdout, in the caller's format, with a `help` line saying
|
|
272
|
+
* what to do next. A `CliError` the command already built is rethrown untouched; anything else
|
|
273
|
+
* becomes `command-failed`, exit code 1.
|
|
17
274
|
*/
|
|
18
|
-
function fail(error, fallback) {
|
|
275
|
+
function fail(error, fallback, help) {
|
|
19
276
|
if (isCliError(error)) throw error;
|
|
20
|
-
|
|
21
|
-
|
|
277
|
+
throw new CliError(error instanceof Error ? error.message : fallback, {
|
|
278
|
+
code: "command-failed",
|
|
279
|
+
exitCode: exitCodes.error,
|
|
280
|
+
...help ? { help } : {},
|
|
281
|
+
cause: error
|
|
282
|
+
});
|
|
22
283
|
}
|
|
23
284
|
/**
|
|
24
285
|
* An answer that comes with a failing exit code. The cli prints only an answer a command returns, and
|
|
@@ -39,6 +300,22 @@ function collapseHome(home, path) {
|
|
|
39
300
|
function displayBinPath(home, executable) {
|
|
40
301
|
return executable ? collapseHome(home, executable) : "buddy-agent-harness";
|
|
41
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* `parseHarnesses` for a command's `--harness`: an unsupported name is a usage error, exit code 2.
|
|
305
|
+
* The registry stays free of clibuilder, which the library entry treats as an optional peer.
|
|
306
|
+
*/
|
|
307
|
+
function parseHarnessOption(value) {
|
|
308
|
+
try {
|
|
309
|
+
return parseHarnesses(value);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
throw new CliError(error.message, {
|
|
312
|
+
code: "unsupported-harness",
|
|
313
|
+
exitCode: exitCodes.usage,
|
|
314
|
+
help: "Pass --harness as a comma-separated list of the supported names.",
|
|
315
|
+
cause: error
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
42
319
|
//#endregion
|
|
43
320
|
//#region src/dep-plugins/dep-plugins.ts
|
|
44
321
|
/** Generic marketplace names: a second registration under one of these replaces the first, silently. */
|
|
@@ -94,7 +371,7 @@ function serializeCatalog(catalog) {
|
|
|
94
371
|
}
|
|
95
372
|
//#endregion
|
|
96
373
|
//#region src/dep-plugins/resolve.ts
|
|
97
|
-
function readJson$
|
|
374
|
+
function readJson$2(path) {
|
|
98
375
|
try {
|
|
99
376
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
100
377
|
} catch {
|
|
@@ -109,7 +386,7 @@ function readDepPlugin(pkg, fromManifest) {
|
|
|
109
386
|
package: pkg,
|
|
110
387
|
reason: "unresolved"
|
|
111
388
|
};
|
|
112
|
-
const manifest = readJson$
|
|
389
|
+
const manifest = readJson$2(join(dir, "plugin.json"));
|
|
113
390
|
if (!manifest || typeof manifest !== "object") return {
|
|
114
391
|
ok: false,
|
|
115
392
|
package: pkg,
|
|
@@ -122,7 +399,7 @@ function readDepPlugin(pkg, fromManifest) {
|
|
|
122
399
|
package: pkg,
|
|
123
400
|
reason: "unnamed"
|
|
124
401
|
};
|
|
125
|
-
const installed = readJson$
|
|
402
|
+
const installed = readJson$2(join(dir, "package.json"));
|
|
126
403
|
const version = typeof installed?.version === "string" ? installed.version : void 0;
|
|
127
404
|
if (!version) return {
|
|
128
405
|
ok: false,
|
|
@@ -215,7 +492,7 @@ function writeCatalog(root, catalog) {
|
|
|
215
492
|
//#endregion
|
|
216
493
|
//#region src/dep-plugins/harness-plugins.ts
|
|
217
494
|
const COPILOT_LIMITATION = "Copilot CLI rejects an npm marketplace source, so it cannot install from this catalog. It needs a path-sourced catalog, which cannot reach node_modules from a subdirectory.";
|
|
218
|
-
function readJson(path) {
|
|
495
|
+
function readJson$1(path) {
|
|
219
496
|
try {
|
|
220
497
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
221
498
|
} catch {
|
|
@@ -233,7 +510,7 @@ function claudeConfigDir(home, env = process.env) {
|
|
|
233
510
|
return env["CLAUDE_CONFIG_DIR"] || join(home, ".claude");
|
|
234
511
|
}
|
|
235
512
|
function readClaudeState(configDir) {
|
|
236
|
-
const installedFile = readJson(join(configDir, "plugins", "installed_plugins.json"));
|
|
513
|
+
const installedFile = readJson$1(join(configDir, "plugins", "installed_plugins.json"));
|
|
237
514
|
const installed = [];
|
|
238
515
|
for (const [id, entries] of Object.entries(installedFile?.plugins ?? {})) {
|
|
239
516
|
if (!Array.isArray(entries)) continue;
|
|
@@ -256,7 +533,7 @@ function readClaudeState(configDir) {
|
|
|
256
533
|
});
|
|
257
534
|
}
|
|
258
535
|
}
|
|
259
|
-
const knownFile = readJson(join(configDir, "plugins", "known_marketplaces.json"));
|
|
536
|
+
const knownFile = readJson$1(join(configDir, "plugins", "known_marketplaces.json"));
|
|
260
537
|
const marketplaces = [];
|
|
261
538
|
for (const [name, value] of Object.entries(knownFile ?? {})) marketplaces.push({
|
|
262
539
|
name,
|
|
@@ -394,350 +671,101 @@ function commandFor(runtime, action, marketplace) {
|
|
|
394
671
|
return refresh ? `${refresh} && ${update}` : update;
|
|
395
672
|
}
|
|
396
673
|
case "uninstall": return runtime.uninstall(action.subject);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
function notesFor(derived) {
|
|
400
|
-
const notes = [];
|
|
401
|
-
if (derived.pnp) notes.push("Yarn Plug’n’Play is active: dependencies live inside zip archives, so no harness can install from them. Set nodeLinker: node-modules, or unplug the plugin dependencies.");
|
|
402
|
-
if (derived.genericName) notes.push(`Marketplace name "${derived.catalog.name}" is generic. Registering a second marketplace under a name already in use replaces the first silently, and the install that follows reports "already installed" — the repository then runs the other one's versions.`);
|
|
403
|
-
for (const { package: pkg } of derived.unresolved) notes.push(`Declared dependency "${pkg}" is not installed; install dependencies before generating.`);
|
|
404
|
-
return notes;
|
|
405
|
-
}
|
|
406
|
-
const depPluginsCommand = command({
|
|
407
|
-
name: "dep-plugins",
|
|
408
|
-
description: "Derive the marketplace catalog for plugins shipped by this repository’s dependencies, so a harness can install them. Writes the catalog; installing is the harness’s own job.",
|
|
409
|
-
options: {
|
|
410
|
-
root: {
|
|
411
|
-
description: "Repository directory. Defaults to the current directory.",
|
|
412
|
-
type: t.string().optional()
|
|
413
|
-
},
|
|
414
|
-
check: {
|
|
415
|
-
description: "Report whether the catalog is current without writing it. For CI and postinstall.",
|
|
416
|
-
type: t.boolean().optional()
|
|
417
|
-
}
|
|
418
|
-
},
|
|
419
|
-
run(args) {
|
|
420
|
-
try {
|
|
421
|
-
const root = args.root ?? process.cwd();
|
|
422
|
-
const derived = deriveCatalog(root);
|
|
423
|
-
const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
|
|
424
|
-
const home = homedir();
|
|
425
|
-
const notes = notesFor(derived);
|
|
426
|
-
const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
|
|
427
|
-
const harness = [];
|
|
428
|
-
const actions = [];
|
|
429
|
-
for (const runtime of detected) {
|
|
430
|
-
const state = runtime.state(home);
|
|
431
|
-
const delta = reconcile({
|
|
432
|
-
catalog: derived.catalog,
|
|
433
|
-
catalogDir: CATALOG_DIR,
|
|
434
|
-
root: resolve(root),
|
|
435
|
-
installed: state.installed,
|
|
436
|
-
marketplaces: state.marketplaces,
|
|
437
|
-
scoped: runtime.scoped
|
|
438
|
-
});
|
|
439
|
-
if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
|
|
440
|
-
harness.push({
|
|
441
|
-
runtime: runtime.name,
|
|
442
|
-
status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
|
|
443
|
-
});
|
|
444
|
-
for (const action of delta.actions) actions.push({
|
|
445
|
-
runtime: runtime.name,
|
|
446
|
-
do: commandFor(runtime, action, derived.catalog.name),
|
|
447
|
-
subject: action.subject,
|
|
448
|
-
why: action.detail
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
|
|
452
|
-
const report = {
|
|
453
|
-
catalog: CATALOG_PATH,
|
|
454
|
-
marketplace: derived.catalog.name,
|
|
455
|
-
outcome,
|
|
456
|
-
plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
|
|
457
|
-
name: plugin,
|
|
458
|
-
package: pkg,
|
|
459
|
-
version
|
|
460
|
-
})) : "0 dependencies ship a plugin — nothing to install",
|
|
461
|
-
harness: harness.length ? harness : "no supported harness detected on this machine",
|
|
462
|
-
actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
|
|
463
|
-
...notes.length ? { notes } : {}
|
|
464
|
-
};
|
|
465
|
-
const answer = result(report);
|
|
466
|
-
return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
|
|
467
|
-
} catch (error) {
|
|
468
|
-
return fail(error, "Catalog generation failed.");
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
//#endregion
|
|
473
|
-
//#region src/diagnose-bridges/directory-files.ts
|
|
474
|
-
/** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
|
|
475
|
-
function filesUnder(directory) {
|
|
476
|
-
try {
|
|
477
|
-
return readdirSync(directory, {
|
|
478
|
-
recursive: true,
|
|
479
|
-
withFileTypes: true
|
|
480
|
-
}).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
|
|
481
|
-
} catch {
|
|
482
|
-
return [];
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
//#endregion
|
|
486
|
-
//#region src/harness-registry/harness-registry.ts
|
|
487
|
-
/**
|
|
488
|
-
* Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
|
|
489
|
-
* drop a projection it may need.
|
|
490
|
-
*/
|
|
491
|
-
function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
|
|
492
|
-
const listed = directories ?? [];
|
|
493
|
-
const [first] = listed;
|
|
494
|
-
if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
|
|
495
|
-
return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
|
|
499
|
-
* reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
|
|
500
|
-
* E-GEM-03, E-COPILOT-04, E-CUR-08).
|
|
501
|
-
*/
|
|
502
|
-
const globalInstructions = ".agents/AGENTS.md";
|
|
503
|
-
/**
|
|
504
|
-
* Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
|
|
505
|
-
* (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
|
|
506
|
-
*/
|
|
507
|
-
const harnessRegistry = [
|
|
508
|
-
{
|
|
509
|
-
name: "claude-code",
|
|
510
|
-
project: {
|
|
511
|
-
detect: ".claude",
|
|
512
|
-
...skillsProjection("claude-code", "project"),
|
|
513
|
-
shadowedBy: [
|
|
514
|
-
"CLAUDE.md",
|
|
515
|
-
".claude/CLAUDE.md",
|
|
516
|
-
"CLAUDE.local.md"
|
|
517
|
-
],
|
|
518
|
-
mcpConfig: {
|
|
519
|
-
path: ".mcp.json",
|
|
520
|
-
key: "mcpServers",
|
|
521
|
-
format: "json",
|
|
522
|
-
dialect: "claude-code"
|
|
523
|
-
},
|
|
524
|
-
nonstandard: [
|
|
525
|
-
{
|
|
526
|
-
path: ".claude/commands",
|
|
527
|
-
shape: "directory",
|
|
528
|
-
kind: "command"
|
|
529
|
-
},
|
|
530
|
-
{
|
|
531
|
-
path: ".claude/rules",
|
|
532
|
-
shape: "directory",
|
|
533
|
-
kind: "rule"
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
path: ".claude/agents",
|
|
537
|
-
shape: "directory",
|
|
538
|
-
kind: "subagent"
|
|
539
|
-
}
|
|
540
|
-
]
|
|
541
|
-
},
|
|
542
|
-
user: {
|
|
543
|
-
detect: ".claude",
|
|
544
|
-
...skillsProjection("claude-code", "user"),
|
|
545
|
-
relocatedBy: "CLAUDE_CONFIG_DIR",
|
|
546
|
-
emptyRelocatesToStart: true,
|
|
547
|
-
instructionBridge: {
|
|
548
|
-
kind: "import",
|
|
549
|
-
path: ".claude/CLAUDE.md",
|
|
550
|
-
line: `@~/${globalInstructions}`
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
},
|
|
554
|
-
{
|
|
555
|
-
name: "cursor",
|
|
556
|
-
project: {
|
|
557
|
-
detect: ".cursor",
|
|
558
|
-
...skillsProjection("cursor", "project"),
|
|
559
|
-
mcpConfig: {
|
|
560
|
-
path: ".cursor/mcp.json",
|
|
561
|
-
key: "mcpServers",
|
|
562
|
-
format: "json",
|
|
563
|
-
dialect: "cursor"
|
|
564
|
-
},
|
|
565
|
-
nonstandard: [
|
|
566
|
-
{
|
|
567
|
-
path: ".cursorrules",
|
|
568
|
-
shape: "file",
|
|
569
|
-
kind: "instructions"
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
path: ".cursor/rules",
|
|
573
|
-
shape: "directory",
|
|
574
|
-
kind: "rule"
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
path: ".cursor/commands",
|
|
578
|
-
shape: "directory",
|
|
579
|
-
kind: "command"
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
path: ".cursor/skills",
|
|
583
|
-
shape: "directory",
|
|
584
|
-
kind: "skill"
|
|
585
|
-
}
|
|
586
|
-
]
|
|
587
|
-
},
|
|
588
|
-
user: {
|
|
589
|
-
detect: ".cursor",
|
|
590
|
-
...skillsProjection("cursor", "user")
|
|
591
|
-
}
|
|
592
|
-
},
|
|
593
|
-
{
|
|
594
|
-
name: "codex",
|
|
595
|
-
project: {
|
|
596
|
-
detect: ".codex",
|
|
597
|
-
...skillsProjection("codex", "project"),
|
|
598
|
-
mcpConfig: {
|
|
599
|
-
path: ".codex/config.toml",
|
|
600
|
-
key: "mcp_servers",
|
|
601
|
-
format: "toml",
|
|
602
|
-
dialect: "codex",
|
|
603
|
-
shared: true
|
|
604
|
-
},
|
|
605
|
-
nonstandard: [{
|
|
606
|
-
path: ".codex/skills",
|
|
607
|
-
shape: "directory",
|
|
608
|
-
kind: "skill"
|
|
609
|
-
}]
|
|
610
|
-
},
|
|
611
|
-
user: {
|
|
612
|
-
detect: ".codex",
|
|
613
|
-
...skillsProjection("codex", "user"),
|
|
614
|
-
instructionBridge: {
|
|
615
|
-
kind: "symlink",
|
|
616
|
-
path: ".codex/AGENTS.md"
|
|
617
|
-
},
|
|
618
|
-
shadowedBy: [".codex/AGENTS.override.md"],
|
|
619
|
-
relocatedBy: "CODEX_HOME"
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
name: "copilot-cli",
|
|
624
|
-
project: {
|
|
625
|
-
detect: ".github/skills",
|
|
626
|
-
...skillsProjection("copilot-cli", "project"),
|
|
627
|
-
nonstandard: [
|
|
628
|
-
{
|
|
629
|
-
path: ".github/copilot-instructions.md",
|
|
630
|
-
shape: "file",
|
|
631
|
-
kind: "instructions"
|
|
632
|
-
},
|
|
633
|
-
{
|
|
634
|
-
path: ".github/instructions",
|
|
635
|
-
shape: "directory",
|
|
636
|
-
kind: "instructions"
|
|
637
|
-
},
|
|
638
|
-
{
|
|
639
|
-
path: ".github/skills",
|
|
640
|
-
shape: "directory",
|
|
641
|
-
kind: "skill"
|
|
642
|
-
}
|
|
643
|
-
]
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function notesFor(derived) {
|
|
677
|
+
const notes = [];
|
|
678
|
+
if (derived.pnp) notes.push("Yarn Plug’n’Play is active: dependencies live inside zip archives, so no harness can install from them. Set nodeLinker: node-modules, or unplug the plugin dependencies.");
|
|
679
|
+
if (derived.genericName) notes.push(`Marketplace name "${derived.catalog.name}" is generic. Registering a second marketplace under a name already in use replaces the first silently, and the install that follows reports "already installed" — the repository then runs the other one's versions.`);
|
|
680
|
+
for (const { package: pkg } of derived.unresolved) notes.push(`Declared dependency "${pkg}" is not installed; install dependencies before generating.`);
|
|
681
|
+
return notes;
|
|
682
|
+
}
|
|
683
|
+
const depPluginsCommand = command({
|
|
684
|
+
name: "dep-plugins",
|
|
685
|
+
description: "Derive the marketplace catalog for plugins shipped by this repository’s dependencies, so a harness can install them. Writes the catalog; installing is the harness’s own job.",
|
|
686
|
+
options: {
|
|
687
|
+
root: {
|
|
688
|
+
description: "Repository directory. Defaults to the current directory.",
|
|
689
|
+
type: t.string().optional()
|
|
644
690
|
},
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
relocatedBy: "COPILOT_HOME",
|
|
649
|
-
instructionBridge: {
|
|
650
|
-
kind: "symlink",
|
|
651
|
-
path: ".copilot/copilot-instructions.md"
|
|
652
|
-
}
|
|
691
|
+
check: {
|
|
692
|
+
description: "Report whether the catalog is current without writing it. For CI and postinstall.",
|
|
693
|
+
type: t.boolean().optional()
|
|
653
694
|
}
|
|
654
695
|
},
|
|
655
|
-
{
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
696
|
+
examples: [{
|
|
697
|
+
command: "buddy-agent-harness dep-plugins",
|
|
698
|
+
description: "Write the marketplace catalog for dependency plugins"
|
|
699
|
+
}, {
|
|
700
|
+
command: "buddy-agent-harness dep-plugins --check",
|
|
701
|
+
description: "Fail when the catalog is stale (CI)"
|
|
702
|
+
}],
|
|
703
|
+
run(args) {
|
|
704
|
+
try {
|
|
705
|
+
const root = args.root ?? process.cwd();
|
|
706
|
+
const derived = deriveCatalog(root);
|
|
707
|
+
const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
|
|
708
|
+
const home = homedir();
|
|
709
|
+
const notes = notesFor(derived);
|
|
710
|
+
const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
|
|
711
|
+
const harness = [];
|
|
712
|
+
const actions = [];
|
|
713
|
+
for (const runtime of detected) {
|
|
714
|
+
const state = runtime.state(home);
|
|
715
|
+
const delta = reconcile({
|
|
716
|
+
catalog: derived.catalog,
|
|
717
|
+
catalogDir: CATALOG_DIR,
|
|
718
|
+
root: resolve(root),
|
|
719
|
+
installed: state.installed,
|
|
720
|
+
marketplaces: state.marketplaces,
|
|
721
|
+
scoped: runtime.scoped
|
|
722
|
+
});
|
|
723
|
+
if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
|
|
724
|
+
harness.push({
|
|
725
|
+
runtime: runtime.name,
|
|
726
|
+
status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
|
|
727
|
+
});
|
|
728
|
+
for (const action of delta.actions) actions.push({
|
|
729
|
+
runtime: runtime.name,
|
|
730
|
+
do: commandFor(runtime, action, derived.catalog.name),
|
|
731
|
+
subject: action.subject,
|
|
732
|
+
why: action.detail
|
|
733
|
+
});
|
|
688
734
|
}
|
|
735
|
+
if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
|
|
736
|
+
const report = {
|
|
737
|
+
catalog: CATALOG_PATH,
|
|
738
|
+
marketplace: derived.catalog.name,
|
|
739
|
+
outcome,
|
|
740
|
+
plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
|
|
741
|
+
name: plugin,
|
|
742
|
+
package: pkg,
|
|
743
|
+
version
|
|
744
|
+
})) : "0 dependencies ship a plugin — nothing to install",
|
|
745
|
+
harness: harness.length ? harness : "no supported harness detected on this machine",
|
|
746
|
+
actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
|
|
747
|
+
...notes.length ? { notes } : {}
|
|
748
|
+
};
|
|
749
|
+
const answer = result(report);
|
|
750
|
+
return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
|
|
751
|
+
} catch (error) {
|
|
752
|
+
return fail(error, "Catalog generation failed.");
|
|
689
753
|
}
|
|
690
|
-
},
|
|
691
|
-
{
|
|
692
|
-
name: "devin-desktop",
|
|
693
|
-
project: { detect: ".devin" }
|
|
694
|
-
},
|
|
695
|
-
{
|
|
696
|
-
name: "windsurf",
|
|
697
|
-
project: {
|
|
698
|
-
detect: ".windsurf",
|
|
699
|
-
skillsDirectory: ".windsurf/skills",
|
|
700
|
-
nonstandard: [{
|
|
701
|
-
path: ".windsurfrules",
|
|
702
|
-
shape: "file",
|
|
703
|
-
kind: "instructions"
|
|
704
|
-
}, {
|
|
705
|
-
path: ".windsurf/rules",
|
|
706
|
-
shape: "directory",
|
|
707
|
-
kind: "rule"
|
|
708
|
-
}]
|
|
709
|
-
},
|
|
710
|
-
deprecated: "devin-desktop"
|
|
711
754
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* An unknown name throws rather than being dropped — silently falling back to the defaults would
|
|
719
|
-
* misreport what was diagnosed.
|
|
720
|
-
*/
|
|
721
|
-
function parseHarnesses(value) {
|
|
722
|
-
const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
|
|
723
|
-
const unsupported = requested.filter((name) => !isHarnessName(name));
|
|
724
|
-
if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
|
|
725
|
-
return requested.filter(isHarnessName);
|
|
726
|
-
}
|
|
727
|
-
/** Enabled whether or not the repository already contains their directories. */
|
|
728
|
-
const defaultHarnesses = ["claude-code", "cursor"];
|
|
729
|
-
function directoryExists(path) {
|
|
755
|
+
});
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/diagnose-bridges/directory-files.ts
|
|
758
|
+
/** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
|
|
759
|
+
function filesUnder(directory) {
|
|
730
760
|
try {
|
|
731
|
-
return
|
|
761
|
+
return readdirSync(directory, {
|
|
762
|
+
recursive: true,
|
|
763
|
+
withFileTypes: true
|
|
764
|
+
}).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
|
|
732
765
|
} catch {
|
|
733
|
-
return
|
|
766
|
+
return [];
|
|
734
767
|
}
|
|
735
768
|
}
|
|
736
|
-
/** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
|
|
737
|
-
function selectHarnesses(root, preferred) {
|
|
738
|
-
const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
|
|
739
|
-
return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
|
|
740
|
-
}
|
|
741
769
|
//#endregion
|
|
742
770
|
//#region src/diagnose-bridges/locator.ts
|
|
743
771
|
/** The locator as one string: the finding's `path`, and what a repair names. */
|
|
@@ -776,6 +804,7 @@ const initSkillInvocation = `/buddy-agent-harness:init-buddy-agent-harness`;
|
|
|
776
804
|
*/
|
|
777
805
|
const pnpmSkillRef = (invocation) => `\`${invocation}\` (pnpm install: \`pnpm-${commandInvocation}-${invocation.slice(invocation.indexOf(":") + 1)}\`)`;
|
|
778
806
|
const initSkillRef = pnpmSkillRef(initSkillInvocation);
|
|
807
|
+
const enhanceSkillRef = pnpmSkillRef("/buddy-agent-harness:enhance");
|
|
779
808
|
const repairSkillRef = pnpmSkillRef(repairSkillInvocation);
|
|
780
809
|
/** Where the user authors the golden MCP server set, named in every MCP repair that points at it. */
|
|
781
810
|
const goldenSet = ".agents/buddy-agent-harness/mcp.toml";
|
|
@@ -1141,6 +1170,35 @@ const nonstandardTable = {
|
|
|
1141
1170
|
}
|
|
1142
1171
|
};
|
|
1143
1172
|
repairsOf(nonstandardTable);
|
|
1173
|
+
/** Who offers every worktree repair, named the same way in each row. */
|
|
1174
|
+
const worktreeOffer = `${initSkillRef} offers it, or ${enhanceSkillRef} where the repository already has an AGENTS.md`;
|
|
1175
|
+
const worktreeTable = {
|
|
1176
|
+
"worktrees-untracked": {
|
|
1177
|
+
detail: ".claude/worktrees/ is not git-ignored, so every agent worktree Claude Code creates there shows up as untracked in git status",
|
|
1178
|
+
repair: ({ file }) => ({
|
|
1179
|
+
command: "",
|
|
1180
|
+
instruction: `add \`.claude/worktrees/\` to ${file} — ${worktreeOffer}`
|
|
1181
|
+
}),
|
|
1182
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the .gitignore line`
|
|
1183
|
+
},
|
|
1184
|
+
"worktrees-scanned": {
|
|
1185
|
+
detail: "this config does not exclude .claude/worktrees, so the tool walks into each agent worktree, a whole checkout with its own config, and Biome stops on the nested root configuration",
|
|
1186
|
+
repair: ({ file }) => ({
|
|
1187
|
+
command: "",
|
|
1188
|
+
instruction: `add \`"!**/.claude/worktrees"\` to \`files.includes\` in ${file} — ${worktreeOffer}`
|
|
1189
|
+
}),
|
|
1190
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which offers the exclusion`
|
|
1191
|
+
},
|
|
1192
|
+
"worktree-base-unset": {
|
|
1193
|
+
detail: "worktree.baseRef is unset, so agent worktrees branch from the default branch rather than the current HEAD — work on a feature branch starts without its commits",
|
|
1194
|
+
repair: ({ file }) => ({
|
|
1195
|
+
command: "",
|
|
1196
|
+
instruction: `decide whether agent worktrees should branch from HEAD, and if so set \`"worktree": { "baseRef": "head" }\` in ${file} — ${worktreeOffer}`
|
|
1197
|
+
}),
|
|
1198
|
+
skillRepair: () => `run ${initSkillRef} or ${enhanceSkillRef}, which explains the trade-off and offers the setting`
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
repairsOf(worktreeTable);
|
|
1144
1202
|
/**
|
|
1145
1203
|
* Annotated over the whole union rather than inferred, so an added problem fails to compile until
|
|
1146
1204
|
* its section carries it.
|
|
@@ -1151,7 +1209,8 @@ const doctorTable = {
|
|
|
1151
1209
|
...globalInstructionTable,
|
|
1152
1210
|
...configurationTable,
|
|
1153
1211
|
...mcpTable,
|
|
1154
|
-
...nonstandardTable
|
|
1212
|
+
...nonstandardTable,
|
|
1213
|
+
...worktreeTable
|
|
1155
1214
|
};
|
|
1156
1215
|
const doctorRepairs = repairsOf(doctorTable);
|
|
1157
1216
|
/** The repair for one problem. Total by construction: the table is keyed by the union. */
|
|
@@ -1163,7 +1222,7 @@ function repairFor(problem) {
|
|
|
1163
1222
|
}
|
|
1164
1223
|
const doctorSkill = {
|
|
1165
1224
|
name: "doctor-buddy-agent-harness",
|
|
1166
|
-
description: "
|
|
1225
|
+
description: "Checks whether a repository's agent configuration bridges into .claude/skills and the other harness files still resolve — when a repository loads no project skills, when skills are missing after a clone, or when a harness appears to be ignoring AGENTS.md."
|
|
1167
1226
|
};
|
|
1168
1227
|
/** Written into the generated skill so a reader knows not to edit it in place. */
|
|
1169
1228
|
const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand. -->";
|
|
@@ -1230,6 +1289,7 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
|
|
|
1230
1289
|
| \`references/configuration.md\` | a finding about the configuration around the bridges rather than a bridge |
|
|
1231
1290
|
| \`references/mcp.md\` | any finding whose path is an MCP locator — **always** before acting on a credential finding |
|
|
1232
1291
|
| \`references/nonstandard.md\` | a finding about configuration only one harness can read |
|
|
1292
|
+
| \`references/worktrees.md\` | a finding about the agent worktrees Claude Code creates under \`.claude/worktrees/\` |
|
|
1233
1293
|
| \`references/harnesses/<name>.md\` | the paths and files one named harness uses |
|
|
1234
1294
|
|
|
1235
1295
|
## Rules
|
|
@@ -1240,6 +1300,12 @@ Every \`problem\` name routes to exactly one page. Load the page for the finding
|
|
|
1240
1300
|
- Never repeat a value from a file an \`mcp-literal-secret\` or \`mcp-committed-secret\` finding points at. The report withheld it on purpose, and quoting it back puts it in the transcript anyway.
|
|
1241
1301
|
- Never write outside the repository. A \`globalInstructions\` repair is a step for the user to take in their own home directory; hand it over exactly as reported.
|
|
1242
1302
|
- Write instructions in \`AGENTS.md\`, never in \`CLAUDE.md\`. Content written there reaches one harness, drifts from the canonical file, and — because Claude Code prefers it — takes \`AGENTS.md\` out of that harness's context entirely.
|
|
1303
|
+
|
|
1304
|
+
## Done when
|
|
1305
|
+
|
|
1306
|
+
- **C1.** The reply names each problem the report found and its cause, or, when the report found none, says the configuration is healthy and calls for no change.
|
|
1307
|
+
- **C2.** No bridge, skill, or instruction file changed: no \`init\` ran, and nothing was written to \`CLAUDE.md\`.
|
|
1308
|
+
- **C3.** The reply repeats no value that a secret finding withheld.
|
|
1243
1309
|
`;
|
|
1244
1310
|
}
|
|
1245
1311
|
//#endregion
|
|
@@ -2052,6 +2118,43 @@ function diagnoseNonstandard({ root, cli }) {
|
|
|
2052
2118
|
}
|
|
2053
2119
|
return findings.sort((left, right) => left.path.localeCompare(right.path));
|
|
2054
2120
|
}
|
|
2121
|
+
const biomeConfigs = ["biome.json", "biome.jsonc"];
|
|
2122
|
+
const claudeSettings = ".claude/settings.json";
|
|
2123
|
+
/** `undefined` when the file is missing or does not parse; either way Claude Code reads no base from it. */
|
|
2124
|
+
function readJson(path) {
|
|
2125
|
+
try {
|
|
2126
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
2127
|
+
} catch {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Checked only where `.claude/` exists, the sign the repository is worked in with Claude Code.
|
|
2133
|
+
* Biome is the one tool checked: it is the confirmed failure, and the others are offered by the
|
|
2134
|
+
* skills without a finding.
|
|
2135
|
+
*/
|
|
2136
|
+
function diagnoseWorktrees({ root, git, cli }) {
|
|
2137
|
+
if (!existsSync(join(root, ".claude"))) return [];
|
|
2138
|
+
const findings = [];
|
|
2139
|
+
const add = (path, problem) => {
|
|
2140
|
+
const { detail, repair } = repairFor(problem);
|
|
2141
|
+
findings.push({
|
|
2142
|
+
path,
|
|
2143
|
+
problem,
|
|
2144
|
+
detail,
|
|
2145
|
+
repair: repair({ file: path }, cli)
|
|
2146
|
+
});
|
|
2147
|
+
};
|
|
2148
|
+
if (git.isRepository() && !git.isIgnored(".claude/worktrees/")) add(".gitignore", "worktrees-untracked");
|
|
2149
|
+
for (const config of biomeConfigs) {
|
|
2150
|
+
const path = join(root, config);
|
|
2151
|
+
if (existsSync(path) && !readFileSync(path, "utf8").includes(".claude/worktrees")) add(config, "worktrees-scanned");
|
|
2152
|
+
}
|
|
2153
|
+
const settings = readJson(join(root, claudeSettings));
|
|
2154
|
+
const worktree = isRecord(settings) ? settings["worktree"] : void 0;
|
|
2155
|
+
if (!isRecord(worktree) || worktree["baseRef"] === void 0) add(claudeSettings, "worktree-base-unset");
|
|
2156
|
+
return findings;
|
|
2157
|
+
}
|
|
2055
2158
|
//#endregion
|
|
2056
2159
|
//#region src/governance-overrides/governance-overrides.ts
|
|
2057
2160
|
function machineWideDir(platform, programData, names) {
|
|
@@ -2204,10 +2307,13 @@ function referenceLayers({ packageRoot = dirname(packageGovernancesDir()), ...op
|
|
|
2204
2307
|
}
|
|
2205
2308
|
});
|
|
2206
2309
|
}
|
|
2207
|
-
const referenceCommand = createReferenceCommand({
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2310
|
+
const referenceCommand = createReferenceCommand({
|
|
2311
|
+
plugin: {
|
|
2312
|
+
name: PACKAGE_PLUGIN,
|
|
2313
|
+
root: dirname(packageGovernancesDir())
|
|
2314
|
+
},
|
|
2315
|
+
path: "buddy-agent-harness reference"
|
|
2316
|
+
});
|
|
2211
2317
|
//#endregion
|
|
2212
2318
|
//#region src/skill-projection/skill-projection.ts
|
|
2213
2319
|
function skillNames(skillsDirectory) {
|
|
@@ -2660,6 +2766,10 @@ var GitBridgeState = class {
|
|
|
2660
2766
|
this.root = root;
|
|
2661
2767
|
this.repoRootPrefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
|
|
2662
2768
|
}
|
|
2769
|
+
/** Whether `root` is inside a git work tree at all; a check that needs git's answer skips when not. */
|
|
2770
|
+
isRepository() {
|
|
2771
|
+
return this.repoRootPrefix !== void 0;
|
|
2772
|
+
}
|
|
2663
2773
|
/** The skip-worktree bit is a hint some checkouts clear, so it is verified rather than assumed. */
|
|
2664
2774
|
trackingOf(path) {
|
|
2665
2775
|
if (this.repoRootPrefix === void 0) return "untracked";
|
|
@@ -2922,64 +3032,82 @@ function buildDoctorReport(bin, result, configuration = [], held = []) {
|
|
|
2922
3032
|
help: [...new Map(findings.map((finding) => [`${finding.repair.command}\u0000${finding.repair.instruction}`, finding.repair])).values()]
|
|
2923
3033
|
};
|
|
2924
3034
|
}
|
|
2925
|
-
const
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
root: {
|
|
2930
|
-
description: "Repository or package directory. Defaults to the current directory.",
|
|
2931
|
-
type: t.string().optional()
|
|
2932
|
-
},
|
|
2933
|
-
harness: {
|
|
2934
|
-
description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
|
|
2935
|
-
type: t.string().optional()
|
|
2936
|
-
}
|
|
3035
|
+
const doctorOptions = {
|
|
3036
|
+
root: {
|
|
3037
|
+
description: "Repository or package directory. Defaults to the current directory.",
|
|
3038
|
+
type: t.string().optional()
|
|
2937
3039
|
},
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3040
|
+
harness: {
|
|
3041
|
+
description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
|
|
3042
|
+
type: t.string().optional()
|
|
3043
|
+
}
|
|
3044
|
+
};
|
|
3045
|
+
const doctorDescription = "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only.";
|
|
3046
|
+
/** The live report `doctor` answers with, shared with the bare invocation of the CLI. */
|
|
3047
|
+
async function diagnoseReport(args) {
|
|
3048
|
+
try {
|
|
3049
|
+
const harnesses = parseHarnessOption(args.harness);
|
|
3050
|
+
const root = args.root ?? process.cwd();
|
|
3051
|
+
const home = homedir();
|
|
3052
|
+
const diagnosis = diagnoseBridges({
|
|
3053
|
+
root,
|
|
3054
|
+
...harnesses.length ? { harnesses } : {},
|
|
3055
|
+
cli: commandInvocation,
|
|
3056
|
+
home,
|
|
3057
|
+
env: process.env
|
|
3058
|
+
});
|
|
3059
|
+
const git = new GitBridgeState(root);
|
|
3060
|
+
const configuration = [
|
|
3061
|
+
...diagnoseConfiguration({
|
|
2944
3062
|
root,
|
|
2945
|
-
|
|
2946
|
-
cli: commandInvocation
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
});
|
|
2950
|
-
const git = new GitBridgeState(root);
|
|
2951
|
-
const configuration = [
|
|
2952
|
-
...diagnoseConfiguration({
|
|
2953
|
-
root,
|
|
2954
|
-
git,
|
|
2955
|
-
cli: commandInvocation
|
|
2956
|
-
}),
|
|
2957
|
-
...diagnoseMcp({
|
|
2958
|
-
root,
|
|
2959
|
-
git,
|
|
2960
|
-
cli: commandInvocation
|
|
2961
|
-
}),
|
|
2962
|
-
...diagnoseNonstandard({
|
|
2963
|
-
root,
|
|
2964
|
-
cli: commandInvocation
|
|
2965
|
-
})
|
|
2966
|
-
];
|
|
2967
|
-
const { layers: rows } = listReferences$1(await referenceLayers({
|
|
3063
|
+
git,
|
|
3064
|
+
cli: commandInvocation
|
|
3065
|
+
}),
|
|
3066
|
+
...diagnoseMcp({
|
|
2968
3067
|
root,
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
}
|
|
3068
|
+
git,
|
|
3069
|
+
cli: commandInvocation
|
|
3070
|
+
}),
|
|
3071
|
+
...diagnoseNonstandard({
|
|
3072
|
+
root,
|
|
3073
|
+
cli: commandInvocation
|
|
3074
|
+
}),
|
|
3075
|
+
...diagnoseWorktrees({
|
|
3076
|
+
root,
|
|
3077
|
+
git,
|
|
3078
|
+
cli: commandInvocation
|
|
3079
|
+
})
|
|
3080
|
+
];
|
|
3081
|
+
const { layers: rows } = listReferences$1(await referenceLayers({
|
|
3082
|
+
root,
|
|
3083
|
+
home,
|
|
3084
|
+
platform: process.platform,
|
|
3085
|
+
env: process.env
|
|
3086
|
+
}));
|
|
3087
|
+
const held = rows.filter(({ tier }) => tier !== "plugin").map(({ name, tier, path, outcome }) => ({
|
|
3088
|
+
name,
|
|
3089
|
+
tier,
|
|
3090
|
+
path: collapseHome(home, path),
|
|
3091
|
+
status: outcome
|
|
3092
|
+
}));
|
|
3093
|
+
return buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held);
|
|
3094
|
+
} catch (error) {
|
|
3095
|
+
return fail(error, "Harness diagnosis failed.");
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
const doctorCommand = command({
|
|
3099
|
+
name: "doctor",
|
|
3100
|
+
description: doctorDescription,
|
|
3101
|
+
options: doctorOptions,
|
|
3102
|
+
examples: [{
|
|
3103
|
+
command: "buddy-agent-harness doctor",
|
|
3104
|
+
description: "Check the current repository"
|
|
3105
|
+
}, {
|
|
3106
|
+
command: "buddy-agent-harness doctor --harness codex,gemini-cli",
|
|
3107
|
+
description: "Also check Codex and Gemini CLI"
|
|
3108
|
+
}],
|
|
3109
|
+
async run(args) {
|
|
3110
|
+
return result(await diagnoseReport(args));
|
|
2983
3111
|
}
|
|
2984
3112
|
});
|
|
2985
3113
|
//#endregion
|
|
@@ -3010,6 +3138,10 @@ const governanceListCommand = command({
|
|
|
3010
3138
|
root: rootOption,
|
|
3011
3139
|
"overrides-only": overridesOnlyOption
|
|
3012
3140
|
},
|
|
3141
|
+
examples: [{
|
|
3142
|
+
command: "buddy-agent-harness governance list",
|
|
3143
|
+
description: "List the governances the layers hold"
|
|
3144
|
+
}],
|
|
3013
3145
|
run(args) {
|
|
3014
3146
|
process.stderr.write(GOVERNANCE_DEPRECATED);
|
|
3015
3147
|
try {
|
|
@@ -3046,16 +3178,21 @@ const governanceShowCommand = command({
|
|
|
3046
3178
|
root: rootOption,
|
|
3047
3179
|
"overrides-only": overridesOnlyOption
|
|
3048
3180
|
},
|
|
3181
|
+
examples: [{
|
|
3182
|
+
command: "buddy-agent-harness governance show skill-design",
|
|
3183
|
+
description: "Print one governance document"
|
|
3184
|
+
}],
|
|
3049
3185
|
run(args) {
|
|
3050
3186
|
const format = this.format;
|
|
3051
3187
|
process.stderr.write(GOVERNANCE_DEPRECATED);
|
|
3052
3188
|
try {
|
|
3053
3189
|
const name = parseGovernanceName(args.name);
|
|
3054
3190
|
const found = resolveGovernance(name, layersFor(args));
|
|
3055
|
-
if (!found) {
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3191
|
+
if (!found) throw new CliError(args["overrides-only"] ? `no override for governance "${name}" in the project, user, or machine-wide layer.` : `no governance named "${name}" in any layer.`, {
|
|
3192
|
+
code: "governance-not-found",
|
|
3193
|
+
details: { name },
|
|
3194
|
+
help: ref("reference list", {}, "see what is available")
|
|
3195
|
+
});
|
|
3059
3196
|
const report = {
|
|
3060
3197
|
name: found.name,
|
|
3061
3198
|
scope: found.scope,
|
|
@@ -3136,9 +3273,16 @@ const initCommand = command({
|
|
|
3136
3273
|
type: t.string().optional()
|
|
3137
3274
|
}
|
|
3138
3275
|
},
|
|
3276
|
+
examples: [{
|
|
3277
|
+
command: "buddy-agent-harness init",
|
|
3278
|
+
description: "Set up skills for Claude Code and Cursor"
|
|
3279
|
+
}, {
|
|
3280
|
+
command: "buddy-agent-harness init --harness codex --copy",
|
|
3281
|
+
description: "Also enable Codex, copying skills instead of linking"
|
|
3282
|
+
}],
|
|
3139
3283
|
run(args) {
|
|
3140
3284
|
try {
|
|
3141
|
-
const harnesses =
|
|
3285
|
+
const harnesses = parseHarnessOption(args.harness);
|
|
3142
3286
|
return result(initializeHarnesses({
|
|
3143
3287
|
root: args.root ?? process.cwd(),
|
|
3144
3288
|
...harnesses.length ? { harnesses } : {},
|
|
@@ -3594,6 +3738,13 @@ const mcpReconcileCommand = command({
|
|
|
3594
3738
|
type: t.array(t.string()).optional()
|
|
3595
3739
|
}
|
|
3596
3740
|
},
|
|
3741
|
+
examples: [{
|
|
3742
|
+
command: "buddy-agent-harness mcp reconcile",
|
|
3743
|
+
description: "List harness-side changes the golden set lacks"
|
|
3744
|
+
}, {
|
|
3745
|
+
command: "buddy-agent-harness mcp reconcile --accept <path>",
|
|
3746
|
+
description: "Import one approved field"
|
|
3747
|
+
}],
|
|
3597
3748
|
run(args) {
|
|
3598
3749
|
try {
|
|
3599
3750
|
const root = args.root ?? process.cwd();
|
|
@@ -3605,19 +3756,17 @@ const mcpReconcileCommand = command({
|
|
|
3605
3756
|
});
|
|
3606
3757
|
if (plan.kind === "unreadable") {
|
|
3607
3758
|
const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
process.stderr.write(`error: nothing written.\n${plan.reasons.map((reason) => `- ${reason}\n`).join("")}`);
|
|
3613
|
-
throw CliError.reported(exitCodes.error);
|
|
3759
|
+
throw new CliError(`${goldenSetPath} does not parse${at}.`, {
|
|
3760
|
+
code: "golden-set-unreadable",
|
|
3761
|
+
help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
|
|
3762
|
+
});
|
|
3614
3763
|
}
|
|
3615
|
-
if (plan.kind === "
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
record: "not written"
|
|
3764
|
+
if (plan.kind === "rejected") throw new CliError("nothing written.", {
|
|
3765
|
+
code: "accept-rejected",
|
|
3766
|
+
details: { reasons: plan.reasons },
|
|
3767
|
+
help: ref("mcp reconcile", {}, "list the fields that can be approved")
|
|
3620
3768
|
});
|
|
3769
|
+
if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: `Create ${goldenSetPath} first; there is nothing to reconcile into.` });
|
|
3621
3770
|
const report = {
|
|
3622
3771
|
golden: goldenSetPath,
|
|
3623
3772
|
mode: plan.written ? "written — only the fields named by --accept" : "dry run — nothing written; re-run with --accept <path> for each approved field",
|
|
@@ -3882,6 +4031,13 @@ const mcpProjectCommand = command({
|
|
|
3882
4031
|
type: t.boolean().optional()
|
|
3883
4032
|
}
|
|
3884
4033
|
},
|
|
4034
|
+
examples: [{
|
|
4035
|
+
command: "buddy-agent-harness mcp project",
|
|
4036
|
+
description: "Preview what would be projected"
|
|
4037
|
+
}, {
|
|
4038
|
+
command: "buddy-agent-harness mcp project --write",
|
|
4039
|
+
description: "Project the golden set into each harness"
|
|
4040
|
+
}],
|
|
3885
4041
|
run(args) {
|
|
3886
4042
|
try {
|
|
3887
4043
|
const root = args.root ?? process.cwd();
|
|
@@ -3893,15 +4049,12 @@ const mcpProjectCommand = command({
|
|
|
3893
4049
|
});
|
|
3894
4050
|
if (plan.kind === "unreadable") {
|
|
3895
4051
|
const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
|
|
3896
|
-
|
|
3897
|
-
|
|
4052
|
+
throw new CliError(`${goldenSetPath} does not parse${at}.`, {
|
|
4053
|
+
code: "golden-set-unreadable",
|
|
4054
|
+
help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
|
|
4055
|
+
});
|
|
3898
4056
|
}
|
|
3899
|
-
if (plan.kind === "absent") return
|
|
3900
|
-
golden: `0 servers — no golden set at ${goldenSetPath}`,
|
|
3901
|
-
mode: "nothing to project",
|
|
3902
|
-
actions: "0 changes",
|
|
3903
|
-
record: "not written"
|
|
3904
|
-
});
|
|
4057
|
+
if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: [`Create ${goldenSetPath} with the MCP servers to project.`, ref("mcp project", {}, "preview the projection")] });
|
|
3905
4058
|
const report = {
|
|
3906
4059
|
golden: goldenSetPath,
|
|
3907
4060
|
mode: write ? "written" : "dry run — nothing written; re-run with --write to apply",
|
|
@@ -3943,6 +4096,19 @@ function app(format) {
|
|
|
3943
4096
|
version,
|
|
3944
4097
|
description: rootCommand.description,
|
|
3945
4098
|
format
|
|
4099
|
+
}).default({
|
|
4100
|
+
description: "Report whether this repository's harness bridges resolve (the same as `doctor`).",
|
|
4101
|
+
options: doctorCommand.options,
|
|
4102
|
+
async run(args) {
|
|
4103
|
+
const { bin: _bin, ...report } = await diagnoseReport({
|
|
4104
|
+
root: args["root"],
|
|
4105
|
+
harness: args["harness"]
|
|
4106
|
+
});
|
|
4107
|
+
return result({
|
|
4108
|
+
...homeHeader({ description: rootCommand.description }),
|
|
4109
|
+
...report
|
|
4110
|
+
});
|
|
4111
|
+
}
|
|
3946
4112
|
}).command(first));
|
|
3947
4113
|
}
|
|
3948
4114
|
const textByDefault = {
|