buddy-agent-harness 0.4.1 → 0.6.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/dist/cli.mjs +1 -1
- package/dist/index.d.mts +129 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{init.command-c0fBv15T.mjs → init.command-zbntulIq.mjs} +347 -45
- package/dist/init.command-zbntulIq.mjs.map +1 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/doctor/SKILL.md +24 -6
- package/skills/enhance/SKILL.md +63 -0
- package/skills/enhance/references/delegation.md +28 -0
- package/skills/init/SKILL.md +8 -3
- package/skills/init/references/harnesses/claude-code.md +2 -0
- package/skills/init/references/harnesses/gemini-cli.md +6 -9
- package/src/diagnose-bridges/agents-files.ts +34 -0
- package/src/diagnose-bridges/diagnose-bridges.ts +18 -8
- package/src/diagnose-bridges/diagnose-instructions.ts +140 -0
- package/src/diagnose-bridges/doctor-guidance.ts +81 -13
- package/src/diagnose-bridges/doctor.command.ts +8 -2
- package/src/diagnose-bridges/json-with-comments.ts +60 -0
- package/src/harness-registry/harness-registry.ts +71 -14
- package/src/harness-registry/instruction-bridge.ts +31 -0
- package/src/index.ts +16 -3
- package/src/initialize-harnesses/initialize-harnesses.ts +1 -1
- package/src/skill-projection/skill-projection.ts +2 -2
- package/dist/init.command-c0fBv15T.mjs.map +0 -1
|
@@ -2,7 +2,7 @@ import { command, z } from "clibuilder";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join, posix, relative, sep } from "node:path";
|
|
4
4
|
import { encode } from "@toon-format/toon";
|
|
5
|
-
import { cpSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
5
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs";
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
//#region src/command-output/command-output.ts
|
|
8
8
|
const formats = [
|
|
@@ -61,8 +61,18 @@ function binPath(home, executable) {
|
|
|
61
61
|
//#endregion
|
|
62
62
|
//#region src/harness-registry/harness-registry.ts
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* The two scopes disagree in general, and a single answer per harness could only record one of them:
|
|
65
|
+
* Copilot CLI is `.github/skills` in a repository and `~/.copilot` for the user. Claude Code is the
|
|
66
|
+
* only harness that needs a skills projection, and it needs one at both scopes. Codex, Cursor,
|
|
67
|
+
* Copilot CLI, Gemini CLI, and Devin Desktop read `.agents/skills` themselves and are never
|
|
68
|
+
* projected into.
|
|
69
|
+
*
|
|
70
|
+
* Gemini CLI carried a `.gemini/skills` projection until E-GEM-02: it reads the `.agents/skills`
|
|
71
|
+
* alias at project scope too, where that alias takes precedence over `.gemini/skills`. It still
|
|
72
|
+
* needs an instruction bridge, which is a separate axis and unaffected.
|
|
73
|
+
*
|
|
74
|
+
* Instruction bridges are recorded at project scope only. The user-scope equivalents exist, but
|
|
75
|
+
* nothing writes or reads them yet: `init` works inside a repository, and so does `doctor`.
|
|
66
76
|
*
|
|
67
77
|
* `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a
|
|
68
78
|
* deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps
|
|
@@ -73,34 +83,56 @@ function binPath(home, executable) {
|
|
|
73
83
|
const harnessRegistry = [
|
|
74
84
|
{
|
|
75
85
|
name: "claude-code",
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
project: {
|
|
87
|
+
detect: ".claude",
|
|
88
|
+
skillsDirectory: ".claude/skills",
|
|
89
|
+
instructionBridge: {
|
|
90
|
+
kind: "import",
|
|
91
|
+
path: "CLAUDE.md"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
user: {
|
|
95
|
+
detect: ".claude",
|
|
96
|
+
skillsDirectory: ".claude/skills"
|
|
97
|
+
}
|
|
78
98
|
},
|
|
79
99
|
{
|
|
80
100
|
name: "cursor",
|
|
81
|
-
detect: ".cursor"
|
|
101
|
+
project: { detect: ".cursor" },
|
|
102
|
+
user: { detect: ".cursor" }
|
|
82
103
|
},
|
|
83
104
|
{
|
|
84
105
|
name: "codex",
|
|
85
|
-
detect: ".codex"
|
|
106
|
+
project: { detect: ".codex" },
|
|
107
|
+
user: { detect: ".codex" }
|
|
86
108
|
},
|
|
87
109
|
{
|
|
88
110
|
name: "copilot-cli",
|
|
89
|
-
detect: ".github/skills"
|
|
111
|
+
project: { detect: ".github/skills" },
|
|
112
|
+
user: { detect: ".copilot" }
|
|
90
113
|
},
|
|
91
114
|
{
|
|
92
115
|
name: "gemini-cli",
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
project: {
|
|
117
|
+
detect: ".gemini",
|
|
118
|
+
instructionBridge: {
|
|
119
|
+
kind: "settings-entry",
|
|
120
|
+
path: ".gemini/settings.json",
|
|
121
|
+
key: "context.fileName"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
user: { detect: ".gemini" }
|
|
95
125
|
},
|
|
96
126
|
{
|
|
97
127
|
name: "devin-desktop",
|
|
98
|
-
detect: ".devin"
|
|
128
|
+
project: { detect: ".devin" }
|
|
99
129
|
},
|
|
100
130
|
{
|
|
101
131
|
name: "windsurf",
|
|
102
|
-
|
|
103
|
-
|
|
132
|
+
project: {
|
|
133
|
+
detect: ".windsurf",
|
|
134
|
+
skillsDirectory: ".windsurf/skills"
|
|
135
|
+
},
|
|
104
136
|
deprecated: "devin-desktop"
|
|
105
137
|
}
|
|
106
138
|
];
|
|
@@ -116,23 +148,37 @@ function directoryExists(path) {
|
|
|
116
148
|
/** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
|
|
117
149
|
function selectHarnesses(root, preferred) {
|
|
118
150
|
const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
|
|
119
|
-
return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.detect)));
|
|
151
|
+
return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
|
|
120
152
|
}
|
|
121
153
|
//#endregion
|
|
122
|
-
//#region src/diagnose-bridges/
|
|
154
|
+
//#region src/diagnose-bridges/agents-files.ts
|
|
155
|
+
/** Directories that never hold instructions this repository is responsible for bridging. */
|
|
156
|
+
const pruned = /* @__PURE__ */ new Set(["node_modules"]);
|
|
123
157
|
/**
|
|
124
|
-
* Every
|
|
125
|
-
*
|
|
158
|
+
* Every directory below `root` holding an `AGENTS.md`, repository-relative and POSIX-separated,
|
|
159
|
+
* with the root itself as `''`. Sorted, root first.
|
|
160
|
+
*
|
|
161
|
+
* Dot-directories are pruned along with `node_modules`. That is not only a speed measure:
|
|
162
|
+
* `.agents/AGENTS.md` is canonical shared instructions rather than a subtree-scoped file, and
|
|
163
|
+
* bridging it would claim a scope it does not have.
|
|
126
164
|
*/
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
165
|
+
function agentsFileDirectories(root) {
|
|
166
|
+
const found = [];
|
|
167
|
+
const walk = (relative) => {
|
|
168
|
+
if (existsSync(join(root, relative, "AGENTS.md"))) found.push(relative);
|
|
169
|
+
let entries;
|
|
170
|
+
try {
|
|
171
|
+
entries = readdirSync(join(root, relative), { withFileTypes: true });
|
|
172
|
+
} catch {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || pruned.has(entry.name)) continue;
|
|
177
|
+
walk(relative ? posix.join(relative, entry.name) : entry.name);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
walk("");
|
|
181
|
+
return found.sort();
|
|
136
182
|
}
|
|
137
183
|
//#endregion
|
|
138
184
|
//#region src/diagnose-bridges/doctor-guidance.ts
|
|
@@ -163,7 +209,8 @@ const launcherFor = (subcommand) => `scripts/${subcommand}.mjs`;
|
|
|
163
209
|
const launcherInvocation = (subcommand) => `node ${launcherFor(subcommand)}`;
|
|
164
210
|
/** How the skill hands a repair back to `init`. */
|
|
165
211
|
const initSkillInvocation = "/buddy-agent-harness:init";
|
|
166
|
-
|
|
212
|
+
/** The skills bridges, reported in `bridges`. */
|
|
213
|
+
const bridgeRepairs = [
|
|
167
214
|
{
|
|
168
215
|
problem: "no-canonical",
|
|
169
216
|
detail: "the canonical skill directory does not exist, so no bridge can resolve",
|
|
@@ -219,13 +266,47 @@ const doctorRepairs = [
|
|
|
219
266
|
skillRepair: (path) => `run \`git ls-files -z ${path} | xargs -0 git update-index --skip-worktree\``
|
|
220
267
|
}
|
|
221
268
|
];
|
|
269
|
+
/**
|
|
270
|
+
* The instruction bridges, reported in `instructions`. Every repair is the `init` skill: these are
|
|
271
|
+
* files a person wrote, or files carrying content beside the bridge, and deciding what to preserve
|
|
272
|
+
* while restoring the bridge is judgment no flag carries. `repair` therefore names the skill in
|
|
273
|
+
* both places rather than pretending a shell command exists.
|
|
274
|
+
*/
|
|
275
|
+
const instructionRepairs = [
|
|
276
|
+
{
|
|
277
|
+
problem: "no-instructions",
|
|
278
|
+
detail: "no AGENTS.md at the repository root, so every instruction bridge points at nothing",
|
|
279
|
+
repair: () => initSkillInvocation,
|
|
280
|
+
skillRepair: () => `run \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
problem: "instructions-missing",
|
|
284
|
+
detail: "no instruction bridge at this path — the harness reads none of AGENTS.md",
|
|
285
|
+
repair: () => initSkillInvocation,
|
|
286
|
+
skillRepair: () => `run \`${initSkillInvocation}\``
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
problem: "instructions-unbridged",
|
|
290
|
+
detail: "the file is present but names AGENTS.md nowhere — the harness reads none of it",
|
|
291
|
+
repair: () => initSkillInvocation,
|
|
292
|
+
skillRepair: () => `run \`${initSkillInvocation}\`, which adds the bridge without discarding what the file already says`
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
problem: "instructions-unreadable",
|
|
296
|
+
detail: "the settings file does not parse, so the harness reads none of it",
|
|
297
|
+
repair: () => initSkillInvocation,
|
|
298
|
+
skillRepair: () => `fix the JSON by hand, then run \`${initSkillInvocation}\``
|
|
299
|
+
}
|
|
300
|
+
];
|
|
301
|
+
/** Both sections, in the order `doctor` reports them. */
|
|
302
|
+
const doctorRepairs = [...bridgeRepairs, ...instructionRepairs];
|
|
222
303
|
const repairsByProblem = new Map(doctorRepairs.map((entry) => [entry.problem, entry]));
|
|
223
304
|
function repairFor(problem) {
|
|
224
305
|
return repairsByProblem.get(problem);
|
|
225
306
|
}
|
|
226
307
|
const doctorSkill = {
|
|
227
308
|
name: "doctor",
|
|
228
|
-
description: "Use this skill when a repository loads no project skills, when skills are missing after a clone, or when checking whether the agent configuration bridges into .claude/skills and the other harness
|
|
309
|
+
description: "Use this skill when a repository loads no project skills, when skills are missing after a clone, when a harness appears to be ignoring AGENTS.md, or when checking whether the agent configuration bridges into .claude/skills, CLAUDE.md, and the other harness files still resolve."
|
|
229
310
|
};
|
|
230
311
|
/** Written into the generated skill so a reader knows not to edit it in place. */
|
|
231
312
|
const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-guidance.ts by scripts/generate-skills.ts. Do not edit by hand. -->";
|
|
@@ -236,7 +317,7 @@ const generatedSkillWarning = "<!-- Generated from src/diagnose-bridges/doctor-g
|
|
|
236
317
|
*/
|
|
237
318
|
function renderDoctorSkill(version) {
|
|
238
319
|
const cell = (value) => value.replaceAll("|", "\\|");
|
|
239
|
-
const rows =
|
|
320
|
+
const rows = (repairs) => repairs.map((entry) => `| \`${entry.problem}\` | ${entry.detail} | ${cell(entry.skillRepair("<path>"))} |`).join("\n");
|
|
240
321
|
return `---
|
|
241
322
|
name: ${doctorSkill.name}
|
|
242
323
|
description: ${doctorSkill.description}
|
|
@@ -246,7 +327,7 @@ ${generatedSkillWarning}
|
|
|
246
327
|
|
|
247
328
|
# Harness Doctor
|
|
248
329
|
|
|
249
|
-
\`.agents/skills\`
|
|
330
|
+
A repository keeps one canonical configuration: \`.agents/skills\` for its skills and \`AGENTS.md\` for its instructions. Harnesses that cannot read those get bridges pointing at them: Claude Code needs both, and Gemini CLI needs the instruction bridge only — it reads \`.agents/skills\` itself. A bridge that stops resolving is silent: the harness finds nothing and loads zero project skills, with no warning anywhere. An instruction bridge fails the same way and costs more, because the harness then reads none of the repository's instructions at all.
|
|
250
331
|
|
|
251
332
|
Diagnose it:
|
|
252
333
|
|
|
@@ -260,22 +341,36 @@ The command is read-only. It never repairs anything, so it is safe to run at any
|
|
|
260
341
|
|
|
261
342
|
## Reading the report
|
|
262
343
|
|
|
263
|
-
\`bridges\` lists every bridge \`init\` would create for this repository, each with a \`status\` of \`ok\`, \`missing\`, \`degraded\`, \`stale\`, or \`diverged\`.
|
|
344
|
+
\`bridges\` lists every skills bridge \`init\` would create for this repository, each with a \`status\` of \`ok\`, \`missing\`, \`degraded\`, \`stale\`, or \`diverged\`.
|
|
345
|
+
|
|
346
|
+
\`instructions\` lists every instruction bridge into \`AGENTS.md\`, with a \`status\` of \`ok\`, \`missing\`, \`unbridged\`, or \`unreadable\`. They are a separate section because nothing about them is shared: a different \`kind\`, a different status vocabulary, and a repair that is never a command.
|
|
347
|
+
|
|
348
|
+
\`findings\` explains each problem from either section and \`help\` names its repair. Apply the repair from the tables below, then re-run \`doctor\`.
|
|
264
349
|
|
|
265
|
-
|
|
350
|
+
Do not run an \`init\` command yourself. Rebuilding a skills bridge can move skills a user wrote, and rewriting an instruction file touches prose a person authored — both are the \`init\` skill's judgment, so hand the repair to \`${initSkillInvocation}\` instead. A \`help\` line naming that skill has no shell equivalent at all.
|
|
266
351
|
|
|
267
352
|
When every bridge resolves, \`findings\` says so outright rather than being empty.
|
|
268
353
|
|
|
269
354
|
The default output is TOON, which is what you parse. Add \`--format text\` when you need to show the same report to a person, or \`--format json\`.
|
|
270
355
|
|
|
271
|
-
##
|
|
356
|
+
## Skills bridge findings
|
|
272
357
|
|
|
273
358
|
| Finding | What it means | Repair |
|
|
274
359
|
| --- | --- | --- |
|
|
275
|
-
${rows}
|
|
360
|
+
${rows(bridgeRepairs)}
|
|
276
361
|
|
|
277
362
|
Substitute the reported bridge path for \`<path>\`.
|
|
278
363
|
|
|
364
|
+
## Instruction bridge findings
|
|
365
|
+
|
|
366
|
+
| Finding | What it means | Repair |
|
|
367
|
+
| --- | --- | --- |
|
|
368
|
+
${rows(instructionRepairs)}
|
|
369
|
+
|
|
370
|
+
\`unbridged\` is the one to read carefully. The file is there and looks fine, and it names \`AGENTS.md\` nowhere — a \`CLAUDE.md\` someone overwrote with real content, or a \`.gemini/settings.json\` another tool rewrote without \`AGENTS.md\` in \`context.fileName\`. Never fix it by replacing the file: the content that displaced the bridge may be the only copy of something.
|
|
371
|
+
|
|
372
|
+
An instruction bridge is reported per file, so a monorepo gets one row per \`AGENTS.md\` in the tree. Each nested \`AGENTS.md\` needs its own stub — an import bridges the file beside it and nothing deeper.
|
|
373
|
+
|
|
279
374
|
## The Windows case
|
|
280
375
|
|
|
281
376
|
The common failure is \`degraded\`. Creating a symlink on Windows needs a privilege most accounts do not have, and Git for Windows gates it separately with \`core.symlinks\`, which its installer leaves off. With \`core.symlinks=false\` git does not error — it writes the symlink out as a regular file whose contents are the target path. \`${initSkillInvocation} --copy --force\` rebuilds the bridges as real directories on that machine.
|
|
@@ -287,9 +382,207 @@ A copy is a snapshot rather than a live projection, and an agent that edits a sk
|
|
|
287
382
|
- Never repair a \`diverged-both\` or \`diverged-unknown\` bridge by re-running \`init\`. Rebuilding overwrites whichever side holds the newer edit. Reconcile the two directories first.
|
|
288
383
|
- Edit skills at \`.agents/skills/<name>/SKILL.md\`. Editing through a bridge is only safe when that bridge is a symlink.
|
|
289
384
|
- Do not add bridges to \`.gitignore\`. An untracked bridge swallows a real edit silently.
|
|
385
|
+
- Write instructions in \`AGENTS.md\`, never in \`CLAUDE.md\`. A bridge file holds the import and any harness-specific notes; content written there reaches one harness and drifts from the canonical file.
|
|
290
386
|
`;
|
|
291
387
|
}
|
|
292
388
|
//#endregion
|
|
389
|
+
//#region src/diagnose-bridges/json-with-comments.ts
|
|
390
|
+
/**
|
|
391
|
+
* `.gemini/settings.json` may legally carry comments — the Gemini CLI loader strips them before
|
|
392
|
+
* parsing. A plain `JSON.parse` of a valid settings file therefore throws, and `doctor` would report
|
|
393
|
+
* a working bridge as unreadable. Comments are removed before parsing for that reason.
|
|
394
|
+
*
|
|
395
|
+
* Only comments. A trailing comma stays a parse error, because nothing documents it as accepted.
|
|
396
|
+
*/
|
|
397
|
+
function stripJsonComments(source) {
|
|
398
|
+
let output = "";
|
|
399
|
+
let index = 0;
|
|
400
|
+
let inString = false;
|
|
401
|
+
let escaped = false;
|
|
402
|
+
while (index < source.length) {
|
|
403
|
+
const character = source[index];
|
|
404
|
+
if (inString) {
|
|
405
|
+
output += character;
|
|
406
|
+
if (escaped) escaped = false;
|
|
407
|
+
else if (character === "\\") escaped = true;
|
|
408
|
+
else if (character === "\"") inString = false;
|
|
409
|
+
index += 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (character === "\"") {
|
|
413
|
+
inString = true;
|
|
414
|
+
output += character;
|
|
415
|
+
index += 1;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (character === "/" && source[index + 1] === "/") {
|
|
419
|
+
while (index < source.length && source[index] !== "\n") index += 1;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (character === "/" && source[index + 1] === "*") {
|
|
423
|
+
index += 2;
|
|
424
|
+
while (index < source.length && !(source[index] === "*" && source[index + 1] === "/")) index += 1;
|
|
425
|
+
index += 2;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
output += character;
|
|
429
|
+
index += 1;
|
|
430
|
+
}
|
|
431
|
+
return output;
|
|
432
|
+
}
|
|
433
|
+
/** The parsed value, or `undefined` when the file is not JSON even once its comments are gone. */
|
|
434
|
+
function parseJsonWithComments(source) {
|
|
435
|
+
try {
|
|
436
|
+
return JSON.parse(stripJsonComments(source));
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/diagnose-bridges/diagnose-instructions.ts
|
|
443
|
+
const canonicalInstructions = "AGENTS.md";
|
|
444
|
+
/**
|
|
445
|
+
* An import bridges the `AGENTS.md` beside it and nothing else, so the check is per directory
|
|
446
|
+
* holding one rather than per harness. A line of its own is enough: the harness reference allows
|
|
447
|
+
* Claude-specific notes below the import, and a file that carries the import plus notes still
|
|
448
|
+
* bridges.
|
|
449
|
+
*/
|
|
450
|
+
function inspectImport(target) {
|
|
451
|
+
let stats;
|
|
452
|
+
try {
|
|
453
|
+
stats = lstatSync(target);
|
|
454
|
+
} catch {
|
|
455
|
+
return {
|
|
456
|
+
kind: "none",
|
|
457
|
+
status: "missing",
|
|
458
|
+
problem: "instructions-missing"
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
if (stats.isSymbolicLink()) return readlinkSync(target) === "AGENTS.md" ? {
|
|
462
|
+
kind: "symlink",
|
|
463
|
+
status: "ok"
|
|
464
|
+
} : {
|
|
465
|
+
kind: "symlink",
|
|
466
|
+
status: "unbridged",
|
|
467
|
+
problem: "instructions-unbridged"
|
|
468
|
+
};
|
|
469
|
+
return readFileSync(target, "utf8").split("\n").some((line) => line.trim() === `@AGENTS.md`) ? {
|
|
470
|
+
kind: "import",
|
|
471
|
+
status: "ok"
|
|
472
|
+
} : {
|
|
473
|
+
kind: "file",
|
|
474
|
+
status: "unbridged",
|
|
475
|
+
problem: "instructions-unbridged"
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
/** Reads a dotted key out of parsed JSON without asserting anything about the rest of the file. */
|
|
479
|
+
function valueAt(settings, key) {
|
|
480
|
+
return key.split(".").reduce((value, segment) => {
|
|
481
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
482
|
+
return value[segment];
|
|
483
|
+
}, settings);
|
|
484
|
+
}
|
|
485
|
+
function inspectSettingsEntry(target, key) {
|
|
486
|
+
let source;
|
|
487
|
+
try {
|
|
488
|
+
source = readFileSync(target, "utf8");
|
|
489
|
+
} catch {
|
|
490
|
+
return {
|
|
491
|
+
kind: "none",
|
|
492
|
+
status: "missing",
|
|
493
|
+
problem: "instructions-missing"
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const settings = parseJsonWithComments(source);
|
|
497
|
+
if (settings === void 0) return {
|
|
498
|
+
kind: "file",
|
|
499
|
+
status: "unreadable",
|
|
500
|
+
problem: "instructions-unreadable"
|
|
501
|
+
};
|
|
502
|
+
const entry = valueAt(settings, key);
|
|
503
|
+
return Array.isArray(entry) && entry.includes("AGENTS.md") ? {
|
|
504
|
+
kind: "settings-entry",
|
|
505
|
+
status: "ok"
|
|
506
|
+
} : {
|
|
507
|
+
kind: "file",
|
|
508
|
+
status: "unbridged",
|
|
509
|
+
problem: "instructions-unbridged"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
/** Every path an instruction bridge of this kind belongs at, in report order. */
|
|
513
|
+
function pathsFor(root, bridge) {
|
|
514
|
+
if (bridge.kind !== "import") return [bridge.path];
|
|
515
|
+
return agentsFileDirectories(root).map((directory) => directory ? posix.join(directory, bridge.path) : bridge.path);
|
|
516
|
+
}
|
|
517
|
+
function inspect$1(target, bridge) {
|
|
518
|
+
return bridge.kind === "import" ? inspectImport(target) : inspectSettingsEntry(target, bridge.key);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Reports whether every enabled harness can still read `AGENTS.md`.
|
|
522
|
+
*
|
|
523
|
+
* Losing one of these is quieter than losing a skills bridge and costs more: a harness with no
|
|
524
|
+
* instruction bridge reads none of the repository's instructions, and says nothing about it. The
|
|
525
|
+
* checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
|
|
526
|
+
* skill's judgment.
|
|
527
|
+
*/
|
|
528
|
+
function diagnoseInstructions(root, harnesses, cli) {
|
|
529
|
+
const bridged = harnesses.map((harness) => ({
|
|
530
|
+
name: harness.name,
|
|
531
|
+
bridge: harness.project.instructionBridge
|
|
532
|
+
})).filter((harness) => Boolean(harness.bridge));
|
|
533
|
+
const instructions = [];
|
|
534
|
+
const findings = [];
|
|
535
|
+
if (!bridged.length) return {
|
|
536
|
+
instructions,
|
|
537
|
+
findings
|
|
538
|
+
};
|
|
539
|
+
if (!agentsFileDirectories(root).includes("")) {
|
|
540
|
+
const { detail, repair } = repairFor("no-instructions");
|
|
541
|
+
findings.push({
|
|
542
|
+
path: canonicalInstructions,
|
|
543
|
+
detail,
|
|
544
|
+
repair: repair(canonicalInstructions, cli)
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
for (const { name, bridge } of bridged) for (const path of pathsFor(root, bridge)) {
|
|
548
|
+
const inspection = inspect$1(join(root, path), bridge);
|
|
549
|
+
instructions.push({
|
|
550
|
+
harness: name,
|
|
551
|
+
path,
|
|
552
|
+
kind: inspection.kind,
|
|
553
|
+
status: inspection.status
|
|
554
|
+
});
|
|
555
|
+
if (inspection.problem) {
|
|
556
|
+
const { detail, repair } = repairFor(inspection.problem);
|
|
557
|
+
findings.push({
|
|
558
|
+
path,
|
|
559
|
+
detail,
|
|
560
|
+
repair: repair(path, cli)
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
instructions,
|
|
566
|
+
findings
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/diagnose-bridges/directory-files.ts
|
|
571
|
+
/**
|
|
572
|
+
* Every file below `directory`, as paths relative to it, POSIX-separated and sorted. A directory
|
|
573
|
+
* that does not exist reads as empty: `doctor` never creates one to look at it.
|
|
574
|
+
*/
|
|
575
|
+
function filesUnder(directory) {
|
|
576
|
+
try {
|
|
577
|
+
return readdirSync(directory, {
|
|
578
|
+
recursive: true,
|
|
579
|
+
withFileTypes: true
|
|
580
|
+
}).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
|
|
581
|
+
} catch {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
293
586
|
//#region src/diagnose-bridges/git-bridge-state.ts
|
|
294
587
|
function git(root, args, input) {
|
|
295
588
|
try {
|
|
@@ -467,14 +760,18 @@ function inspect(target, path, canonical, git) {
|
|
|
467
760
|
}
|
|
468
761
|
}
|
|
469
762
|
/**
|
|
470
|
-
* Reports whether every bridge
|
|
471
|
-
* `.agents/skills`. Read-only: nothing is created,
|
|
472
|
-
* to run from the repair each finding carries.
|
|
763
|
+
* Reports whether every bridge this repository needs still resolves: the skills bridges into
|
|
764
|
+
* `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
|
|
765
|
+
* moved, or repaired, so the caller decides what to run from the repair each finding carries.
|
|
473
766
|
*/
|
|
474
767
|
function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
|
|
475
768
|
const canonical = join(root, ".agents", "skills");
|
|
476
769
|
const git = new GitBridgeState(root);
|
|
477
|
-
const
|
|
770
|
+
const selected = selectHarnesses(root, preferred);
|
|
771
|
+
const bridged = selected.map((harness) => ({
|
|
772
|
+
name: harness.name,
|
|
773
|
+
skillsDirectory: harness.project.skillsDirectory
|
|
774
|
+
})).filter((harness) => Boolean(harness.skillsDirectory));
|
|
478
775
|
const bridges = [];
|
|
479
776
|
const divergence = [];
|
|
480
777
|
const findings = [];
|
|
@@ -508,8 +805,11 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
|
|
|
508
805
|
});
|
|
509
806
|
}
|
|
510
807
|
}
|
|
808
|
+
const instructions = diagnoseInstructions(root, selected, cli);
|
|
809
|
+
findings.push(...instructions.findings);
|
|
511
810
|
return {
|
|
512
811
|
bridges,
|
|
812
|
+
instructions: instructions.instructions,
|
|
513
813
|
divergence,
|
|
514
814
|
findings
|
|
515
815
|
};
|
|
@@ -522,16 +822,18 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
|
|
|
522
822
|
*/
|
|
523
823
|
function buildReport(bin, result) {
|
|
524
824
|
if (!result.findings.length) {
|
|
525
|
-
const count = result.bridges.length;
|
|
825
|
+
const count = result.bridges.length + result.instructions.length;
|
|
526
826
|
return {
|
|
527
827
|
bin,
|
|
528
828
|
bridges: result.bridges,
|
|
829
|
+
instructions: result.instructions,
|
|
529
830
|
findings: count === 1 ? "0 problems found — the 1 bridge resolves" : `0 problems found — all ${count} bridges resolve`
|
|
530
831
|
};
|
|
531
832
|
}
|
|
532
833
|
return {
|
|
533
834
|
bin,
|
|
534
835
|
bridges: result.bridges,
|
|
836
|
+
instructions: result.instructions,
|
|
535
837
|
...result.divergence.length ? { divergence: result.divergence } : {},
|
|
536
838
|
findings: result.findings.map(({ path, detail }) => ({
|
|
537
839
|
path,
|
|
@@ -542,7 +844,7 @@ function buildReport(bin, result) {
|
|
|
542
844
|
}
|
|
543
845
|
const doctorCommand = command({
|
|
544
846
|
name: "doctor",
|
|
545
|
-
description: "Report whether
|
|
847
|
+
description: "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md. Read-only.",
|
|
546
848
|
options: {
|
|
547
849
|
root: {
|
|
548
850
|
description: "Repository or package directory. Defaults to the current directory.",
|
|
@@ -602,9 +904,9 @@ function countSkills(canonicalSkills) {
|
|
|
602
904
|
* preferred; a platform that refuses the link falls back to a copy. Returns the projected harnesses.
|
|
603
905
|
*/
|
|
604
906
|
function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
|
|
605
|
-
const projections = harnesses.filter((harness) => harness.skillsDirectory).map((harness) => ({
|
|
907
|
+
const projections = harnesses.filter((harness) => harness.project.skillsDirectory).map((harness) => ({
|
|
606
908
|
harness,
|
|
607
|
-
target: join(root, harness.skillsDirectory)
|
|
909
|
+
target: join(root, harness.project.skillsDirectory)
|
|
608
910
|
}));
|
|
609
911
|
const conflicts = projections.filter(({ target }) => pathOccupied(target) && !isCorrectLink(target, canonicalSkills)).map(({ target }) => target);
|
|
610
912
|
if (conflicts.length && !force) throw new Error(`Refusing to replace existing skill targets:\n${conflicts.map((target) => `- ${target}`).join("\n")}`);
|
|
@@ -644,7 +946,7 @@ function initializeHarnesses({ root, harnesses: preferred = [], copy = false, fo
|
|
|
644
946
|
return {
|
|
645
947
|
root,
|
|
646
948
|
harnesses: harnesses.map((harness) => harness.name),
|
|
647
|
-
native: harnesses.filter((harness) => !harness.skillsDirectory).map((harness) => harness.name),
|
|
949
|
+
native: harnesses.filter((harness) => !harness.project.skillsDirectory).map((harness) => harness.name),
|
|
648
950
|
linked,
|
|
649
951
|
deprecated: harnesses.filter((harness) => harness.deprecated).map((harness) => ({
|
|
650
952
|
name: harness.name,
|
|
@@ -709,6 +1011,6 @@ function activate({ addCommand }) {
|
|
|
709
1011
|
addCommand(harnessCommand);
|
|
710
1012
|
}
|
|
711
1013
|
//#endregion
|
|
712
|
-
export { doctorCommand as a,
|
|
1014
|
+
export { doctorCommand as a, bridgeRepairs as c, instructionRepairs as d, renderDoctorSkill as f, initializeHarnesses as i, doctorRepairs as l, harnessCommand as n, diagnoseBridges as o, harnessRegistry as p, initCommand as r, diagnoseInstructions as s, activate as t, doctorSkill as u };
|
|
713
1015
|
|
|
714
|
-
//# sourceMappingURL=init.command-
|
|
1016
|
+
//# sourceMappingURL=init.command-zbntulIq.mjs.map
|