buddy-agent-harness 0.7.0 → 0.8.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/dist/index.d.mts CHANGED
@@ -163,7 +163,7 @@ type Harness = {
163
163
  *
164
164
  * See `.research/agentic-configuration-standards/` for the per-harness sources.
165
165
  */
166
- declare const harnessRegistry: readonly Harness[];
166
+ export declare const harnessRegistry: readonly Harness[];
167
167
  //#endregion
168
168
  //#region src/diagnose-bridges/diagnose-instructions.d.ts
169
169
  /** What is on disk where an instruction bridge belongs. */
@@ -189,7 +189,7 @@ type InstructionReport = {
189
189
  * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
190
190
  * skill's judgment.
191
191
  */
192
- declare function diagnoseInstructions(root: string, harnesses: readonly Harness[], cli: string): {
192
+ export declare function diagnoseInstructions(root: string, harnesses: readonly Harness[], cli: string): {
193
193
  instructions: InstructionReport[];
194
194
  findings: BridgeFinding[];
195
195
  };
@@ -309,10 +309,10 @@ type Repair = {
309
309
  */
310
310
  skillRepair(at: Locator): string;
311
311
  };
312
- declare const bridgeRepairs: readonly Repair[];
313
- declare const instructionRepairs: readonly Repair[];
314
- declare const doctorRepairs: readonly Repair[];
315
- declare const doctorSkill: {
312
+ export declare const bridgeRepairs: readonly Repair[];
313
+ export declare const instructionRepairs: readonly Repair[];
314
+ export declare const doctorRepairs: readonly Repair[];
315
+ export declare const doctorSkill: {
316
316
  readonly name: "doctor";
317
317
  readonly 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.";
318
318
  };
@@ -324,7 +324,7 @@ declare const doctorSkill: {
324
324
  * The finding tables are NOT here. They live in the reference pages below, and this file carries
325
325
  * only what every reader needs: how to run it, how to read the report, and where to go next.
326
326
  */
327
- declare function renderDoctorSkill(version: string): string;
327
+ export declare function renderDoctorSkill(version: string): string;
328
328
  //#endregion
329
329
  //#region src/diagnose-bridges/git-bridge-state.d.ts
330
330
  /** Which side of a diverged bridge moved since the last commit where the two agreed. */
@@ -425,7 +425,7 @@ type DiagnoseResult = {
425
425
  * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
426
426
  * moved, or repaired, so the caller decides what to run from the repair each finding carries.
427
427
  */
428
- declare function diagnoseBridges({ root, harnesses: preferred, cli }: DiagnoseOptions): DiagnoseResult;
428
+ export declare function diagnoseBridges({ root, harnesses: preferred, cli }: DiagnoseOptions): DiagnoseResult;
429
429
  //#endregion
430
430
  //#region src/diagnose-configuration/diagnose-configuration.d.ts
431
431
  /**
@@ -480,8 +480,8 @@ type DoctorReport = {
480
480
  * AXI §5: the healthy answer states the zero with context, so an agent does not re-run with other
481
481
  * flags to confirm that an empty section really meant "nothing wrong".
482
482
  */
483
- declare function buildDoctorReport(bin: string, result: DiagnoseResult, configuration?: ConfigurationFinding[]): DoctorReport;
484
- declare const doctorCommand: cli$1.Command;
483
+ export declare function buildDoctorReport(bin: string, result: DiagnoseResult, configuration?: ConfigurationFinding[]): DoctorReport;
484
+ export declare const doctorCommand: cli$1.Command;
485
485
  //#endregion
486
486
  //#region src/diagnose-mcp/diagnose-mcp.d.ts
487
487
  /**
@@ -505,7 +505,7 @@ type DiagnoseMcpOptions = {
505
505
  /** How to name this tool in the repair commands. */
506
506
  cli: string;
507
507
  };
508
- declare function diagnoseMcp({ root, git, cli }: DiagnoseMcpOptions): ConfigurationFinding[];
508
+ export declare function diagnoseMcp({ root, git, cli }: DiagnoseMcpOptions): ConfigurationFinding[];
509
509
  //#endregion
510
510
  //#region src/diagnose-mcp/mcp-model.d.ts
511
511
  /**
@@ -544,12 +544,12 @@ type McpDirection = "target" | "golden" | "both" | "unknown";
544
544
  //#endregion
545
545
  //#region src/diagnose-mcp/mcp-sources.d.ts
546
546
  /** Where the golden set lives, and why it is namespaced rather than sitting at `.agents/mcp.json`. */
547
- declare const goldenSetPath = ".agents/buddy-agent-harness/mcp.toml";
547
+ export declare const goldenSetPath = ".agents/buddy-agent-harness/mcp.toml";
548
548
  //#endregion
549
549
  //#region src/initialize-harnesses/init.command.d.ts
550
- declare const initCommand: cli$1.Command;
551
- declare const harnessCommand: cli$1.Command;
552
- declare function activate({ addCommand }: {
550
+ export declare const initCommand: cli$1.Command;
551
+ export declare const harnessCommand: cli$1.Command;
552
+ export declare function activate({ addCommand }: {
553
553
  addCommand(command: typeof harnessCommand): void;
554
554
  }): void;
555
555
  //#endregion
@@ -583,7 +583,7 @@ type InitializeResult = {
583
583
  skills: number;
584
584
  copied: boolean;
585
585
  };
586
- declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
586
+ export declare function initializeHarnesses({ root, harnesses: preferred, copy, force }: InitializeOptions): InitializeResult;
587
587
  //#endregion
588
- export { type BridgeFinding, type BridgeKind, type BridgeProblem, type BridgeReport, type BridgeStatus, type ConfigurationFault, type ConfigurationProblem, type DiagnoseMcpOptions, type DiagnoseOptions, type DiagnoseResult, type DivergenceDirection, type DivergenceReport, type DoctorProblem, type DoctorReport, type Harness, type HarnessName, type HarnessScope, type HarnessScopeName, type InitializeOptions, type InitializeResult, type InstructionBridge, type InstructionKind, type InstructionProblem, type InstructionReport, type InstructionStatus, type McpConfig, type McpDirection, type McpField, type McpProblem, type McpServer, type McpTransport, type Repair, activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill, run };
588
+ export { type BridgeFinding, type BridgeKind, type BridgeProblem, type BridgeReport, type BridgeStatus, type ConfigurationFault, type ConfigurationProblem, type DiagnoseMcpOptions, type DiagnoseOptions, type DiagnoseResult, type DivergenceDirection, type DivergenceReport, type DoctorProblem, type DoctorReport, type Harness, type HarnessName, type HarnessScope, type HarnessScopeName, type InitializeOptions, type InitializeResult, type InstructionBridge, type InstructionKind, type InstructionProblem, type InstructionReport, type InstructionStatus, type McpConfig, type McpDirection, type McpField, type McpProblem, type McpServer, type McpTransport, type Repair, run };
589
589
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;;;;;;;;;;;;;;KAYY;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;KCbS;;EAEX;;;;;;EAMA;;;;;EAKA;;;;;;EAMA;;;;;;;;;;;;;;;;;KCtBW;KAgBA;;;;;EAKX;;EAEA;EACA,MAAM;;;;KC/BK;;KAUA;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA;EACX;EACA;EACA,oBAAoB;EACpB,YAAY;EACZ,uBAAuB;;KAGZ;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;cAuBD,0BAA0B;;;;KCzE3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;iBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;;;;;;;;;;;;;;;;KC5FtC;EAAa;EAAc;;KAE3B;;EAEX;;EAEA;;EAEA;;EAEA,WAAW;;;;;;;;;;KCfA;;;;;;;;;;;KAqBA;;;;;;;;;KAUA;;;;;;;;;KAoBA;KAOA;;KAOA,gBAAgB,gBAAgB,qBAAqB,qBAAqB,aAAa;;KAGvF,uBAAuB;;;;;;;;;;;;;;;;;;KAmBvB;;EAEX;;EAEA;;KAGW;EACX,SAAS;;EAET;;;;;;;EAOA,OAAO,IAAI,SAAS,cAAc;;;;;;EAMlC,YAAY,IAAI;;cAwKJ,wBAAwB;cA4CxB,6BAA6B;cAuM7B,wBAAwB;cAOxB;WACZ;WACA;;;;;;;;;;iBAmCe,kBAAkB;;;;KC5jBtB;;KAGA;;;;;;cAwBC;mBAIA;;mBAFK;EAEjB,YAAY;;;;;;EASZ,WAAW,eAAe;;;;;;;EAa1B,UAAU;;;;;;;;;EAaV,gBAAgB;;EAMhB,UAAU,gBAAgB;;;;;EAS1B,YAAY,gBAAgB,oBAAoB;;UAYxC;;;;;UAmBA;;;;KC5GG;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;;;;;EAKA,SAAS;EACT;;EAEA,QAAQ;;KAGG;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;iBA2EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;;;;;;;;;;;;;;;KC1GhF;;EAEX;EACA,SAAS;EACT;;EAEA,QAAQ;;;;KCfG;EACX;EACA,SAAS;EACT,cAAc;EACd,aAAa;;;;;EAKb;IAAY;IAAc,SAAS;IAAe;;;;;;;;;;EASlD,OAAO;;;;;;iBAOQ,kBACf,aACA,QAAQ,gBACR,gBAAe,yBACb;cAgCU,eAAe,MAAI;;;;;;;;;;;;;;;;;;KC7CpB;EACX;EACA,KAAK;;EAEL;;iBAgCe,cAAc,MAAM,KAAK,OAAO,qBAAqB;;;;;;;;;;;;;;;;KClDzD;KAEA;EACX,YAAY;EACZ;EACA;EACA,MAAM,SAAS;EACf;EACA,UAAU,SAAS;EACnB;EACA;EACA;EACA;;;cAIY;KAaD,mBAAmB;;;;KCnCnB;;;;cCAC;;;cCAA,aAAa,MAAI;cAoDjB,gBAAgB,MAAI;iBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;;;;;;KCV1D;;;KCpDA;EACX;EACA,YAAY;EACZ;EACA,QAAQ;;KAEG;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER,SAAS;;EAET;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;iBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/harness-registry/instruction-bridge.ts","../src/harness-registry/mcp-config.ts","../src/harness-registry/nonstandard-artifact.ts","../src/harness-registry/harness-registry.ts","../src/diagnose-bridges/diagnose-instructions.ts","../src/diagnose-bridges/locator.ts","../src/diagnose-bridges/doctor-guidance.ts","../src/diagnose-bridges/git-bridge-state.ts","../src/diagnose-bridges/diagnose-bridges.ts","../src/diagnose-configuration/diagnose-configuration.ts","../src/diagnose-bridges/doctor.command.ts","../src/diagnose-mcp/diagnose-mcp.ts","../src/diagnose-mcp/mcp-model.ts","../src/diagnose-mcp/mcp-baseline.ts","../src/diagnose-mcp/mcp-sources.ts","../src/initialize-harnesses/init.command.ts","../src/skill-projection/skill-projection.ts","../src/initialize-harnesses/initialize-harnesses.ts"],"mappings":";;;;;;;;;;;;;;;KAYY;;;;;;EAOT;;EAEA;;;EAIA;;EAEA;;EAEA;;;;;;;;;;;;;;;;;;;;KCbS;;EAEX;;;;;;EAMA;;;;;EAKA;;;;;;EAMA;;;;;;;;;;;;;;;;;KCtBW;KAgBA;;;;;EAKX;;EAEA;EACA,MAAM;;;;KC/BK;;KAUA;;;;;;;;;;;;;;;;;;;;;;;;;KA0BA;EACX;EACA;EACA,oBAAoB;EACpB,YAAY;EACZ,uBAAuB;;KAGZ;EACX,MAAM;;EAEN,SAAS;;;;;EAKT,OAAO;;EAEP,aAAa;;;;;;;;;;;;;;;;;;;;;;qBAuBD,0BAA0B;;;;KCzE3B;;;;;;KAMA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;;;;;;;;;wBAkFO,qBACf,cACA,oBAAoB,WACpB;EACI,cAAc;EAAqB,UAAU;;;;;;;;;;;;;;;;;;;;KC5FtC;EAAa;EAAc;;KAE3B;;EAEX;;EAEA;;EAEA;;EAEA,WAAW;;;;;;;;;;KCfA;;;;;;;;;;;KAqBA;;;;;;;;;KAUA;;;;;;;;;KAoBA;KAOA;;KAOA,gBAAgB,gBAAgB,qBAAqB,qBAAqB,aAAa;;KAGvF,uBAAuB;;;;;;;;;;;;;;;;;;KAmBvB;;EAEX;;EAEA;;KAGW;EACX,SAAS;;EAET;;;;;;;EAOA,OAAO,IAAI,SAAS,cAAc;;;;;;EAMlC,YAAY,IAAI;;qBAwKJ,wBAAwB;qBA4CxB,6BAA6B;qBAuM7B,wBAAwB;qBAOxB;WACZ;WACA;;;;;;;;;;wBAmCe,kBAAkB;;;;KC5jBtB;;KAGA;;;;;;cAwBC;mBAIA;;mBAFK;EAEjB,YAAY;;;;;;EASZ,WAAW,eAAe;;;;;;;EAa1B,UAAU;;;;;;;;;EAaV,gBAAgB;;EAMhB,UAAU,gBAAgB;;;;;EAS1B,YAAY,gBAAgB,oBAAoB;;UAYxC;;;;;UAmBA;;;;KC5GG;KACA;KAEA;EACX,SAAS;;EAET;EACA,MAAM;EACN,QAAQ;;KAGG;EACX;;;;;EAKA,SAAS;EACT;;EAEA,QAAQ;;KAGG;EACX;EACA,WAAW;;KAGA;EACX;EACA,YAAY;;EAEZ;;KAGW;EACX,SAAS;;;;;EAKT,cAAc;EACd,YAAY;EACZ,UAAU;;;;;;;wBA2EK,kBAAkB,MAAM,WAAW,WAAgB,OAAO,kBAAkB;;;;;;;;;;;;;;;;;KC1GhF;;EAEX;EACA,SAAS;EACT;;EAEA,QAAQ;;;;KCfG;EACX;EACA,SAAS;EACT,cAAc;EACd,aAAa;;;;;EAKb;IAAY;IAAc,SAAS;IAAe;;;;;;;;;;EASlD,OAAO;;;;;;wBAOQ,kBACf,aACA,QAAQ,gBACR,gBAAe,yBACb;qBAgCU,eAAe,MAAI;;;;;;;;;;;;;;;;;;KC7CpB;EACX;EACA,KAAK;;EAEL;;wBAgCe,cAAc,MAAM,KAAK,OAAO,qBAAqB;;;;;;;;;;;;;;;;KClDzD;KAEA;EACX,YAAY;EACZ;EACA;EACA,MAAM,SAAS;EACf;EACA,UAAU,SAAS;EACnB;EACA;EACA;EACA;;;cAIY;KAaD,mBAAmB;;;;KCnCnB;;;;qBCAC;;;qBCAA,aAAa,MAAI;qBAoDjB,gBAAgB,MAAI;wBAMjB,WAAW;EAAgB,WAAW,gBAAgB;;;;;;;;;KCV1D;;;KCpDA;EACX;EACA,YAAY;EACZ;EACA,QAAQ;;KAEG;EACX;EACA,WAAW;EACX,QAAQ;EACR,QAAQ;;EAER,SAAS;;EAET;IAAc,MAAM;IAAa,YAAY;;EAC7C;EACA;;wBAGe,sBACf,MACA,WAAW,WACX,MACA,SACE,oBAAoB"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as harnessRegistry, a as initializeHarnesses, c as diagnoseBridges, d as goldenSetPath, f as bridgeRepairs, g as renderDoctorSkill, h as instructionRepairs, i as initCommand, l as diagnoseInstructions, m as doctorSkill, n as activate, o as buildDoctorReport, p as doctorRepairs, r as harnessCommand, s as doctorCommand, t as run, u as diagnoseMcp } from "./cli-Bvnq8zOL.mjs";
1
+ import { _ as harnessRegistry, a as initializeHarnesses, c as diagnoseBridges, d as goldenSetPath, f as bridgeRepairs, g as renderDoctorSkill, h as instructionRepairs, i as initCommand, l as diagnoseInstructions, m as doctorSkill, n as activate, o as buildDoctorReport, p as doctorRepairs, r as harnessCommand, s as doctorCommand, t as run, u as diagnoseMcp } from "./cli-4ekejn5W.mjs";
2
2
  export { activate, bridgeRepairs, buildDoctorReport, diagnoseBridges, diagnoseInstructions, diagnoseMcp, doctorCommand, doctorRepairs, doctorSkill, goldenSetPath, harnessCommand, harnessRegistry, initCommand, initializeHarnesses, instructionRepairs, renderDoctorSkill, run };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-agent-harness",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Initialize standards-based agent configuration across agent harnesses",
5
5
  "keywords": [
6
6
  "repobuddy",
@@ -51,11 +51,11 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/node": "^24.12.4",
54
- "@vitest/coverage-v8": "^4.1.0",
55
- "tsdown": "^0.22.0",
54
+ "@vitest/coverage-v8": "^5.0.0",
55
+ "tsdown": "^0.23.0",
56
56
  "tsx": "^4.22.3",
57
57
  "typescript": "^7.0.0",
58
- "vitest": "^4.1.0"
58
+ "vitest": "^5.0.0"
59
59
  },
60
60
  "scripts": {
61
61
  "build": "tsdown",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "buddy-agent-harness",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "description": "Initialize standards-based agent configuration across agent harnesses",
6
6
  "author": {
7
7
  "name": "repobuddy"
@@ -15,7 +15,7 @@ Diagnose it:
15
15
  node scripts/doctor.mjs
16
16
  ```
17
17
 
18
- That path is relative to this skill's own directory. The launcher runs the CLI that shipped beside it against the current working directory, so nothing is downloaded. Fall back to `npx -y buddy-agent-harness@^0.7.0 doctor` when the launcher cannot be resolved or run, which is the case when the plugin was installed from git rather than npm and its dependencies were never installed.
18
+ That path is relative to this skill's own directory. The launcher runs the CLI that shipped beside it against the current working directory, so nothing is downloaded. Fall back to `npx -y buddy-agent-harness@^0.8.0 doctor` when the launcher cannot be resolved or run, which is the case when the plugin was installed from git rather than npm and its dependencies were never installed.
19
19
 
20
20
  The command is read-only. It never repairs anything, so it is safe to run at any point, including from a session-start hook.
21
21
 
@@ -16,7 +16,9 @@ rule that appears only in a skill body — this addition is for the always-loade
16
16
  ```markdown
17
17
  ## Delegation
18
18
 
19
- If this harness can spawn subagents, delegate bulk-mechanical work and research whose answer is far smaller than the reading behind it. Do it yourself when the brief would cost more than the task — a one-line edit is not worth a subagent. Keep the judgment calls and final decisions; delegate the fact-gathering that feeds them. The cheaper the subagent, the less should break if it gets the answer wrong. Brief every one you spawn: it inherits no context, so give it the context, the why, and what done looks like.
19
+ If this harness can spawn subagents, delegate the mechanical work and the research whose answer is far smaller than the reading behind it. Keep the judgment calls and the decisions; anything you would finish in less time than briefing it takes, do yourself.
20
+
21
+ A subagent inherits your model if you do not pick one, and none of your context either way. Pick the cheapest, unless you cannot say what a right answer looks like or could not cheaply tell a wrong one. Give it the context, the why, and what done looks like.
20
22
  ```
21
23
 
22
24
  ## Do not edit it
@@ -75,7 +75,7 @@ One case does need asking even though it only creates a file: a nested `AGENTS.m
75
75
  2. Move approved skills and commands to `.agents/skills/<name>/SKILL.md`, preferring `git mv` so history follows. Fix frontmatter per `references/frontmatter.md` — this is where cross-harness portability is won or lost.
76
76
  3. Merge approved instruction content into `AGENTS.md`, preserving the author's wording. Append; do not restructure. Replace a harness file with a pointer only where approved. Content only some tasks need belongs in a skill, not in `AGENTS.md`.
77
77
  4. Apply the conversions that were approved, one at a time, and nothing else from the list. A rule approved as a skill moves to `.agents/skills/<name>/SKILL.md` with frontmatter per `references/frontmatter.md`; instruction content approved for consolidation is appended as in the step above and its harness file left as a generated copy. Everything declined, and everything with no candidate, stays exactly where it is.
78
- 5. Run `node scripts/init.mjs` at the repository root, resolving that path against this skill's own directory. The launcher runs the CLI that shipped beside it, so nothing is downloaded; fall back to `npx -y buddy-agent-harness@^0.7.0 init` when the path cannot be resolved. It links `.agents/skills` into the harnesses that need it and skips those that read it natively. Add harnesses the user names with `--harness codex,gemini-cli`.
78
+ 5. Run `node scripts/init.mjs` at the repository root, resolving that path against this skill's own directory. The launcher runs the CLI that shipped beside it, so nothing is downloaded; fall back to `npx -y buddy-agent-harness@^0.8.0 init` when the path cannot be resolved. It links `.agents/skills` into the harnesses that need it and skips those that read it natively. Add harnesses the user names with `--harness codex,gemini-cli`.
79
79
  6. If the command reports a conflict, resolve the named target and retry. Pass the target to `--force` — `--force .claude/skills` replaces that projection and reports any other conflict untouched, where a bare `--force` replaces every conflicting target at once. Reach for the bare form only when replacing all of them is what you came for. Use `--copy` only where links are unavailable — a copy is a snapshot, not a live projection, so say so when you fall back.
80
80
  7. Apply the instruction bridges `init` does not write. Claude Code reads `CLAUDE.md`, not `AGENTS.md` — create a `CLAUDE.md` containing `@AGENTS.md`. Gemini CLI needs `AGENTS.md` added to `context.fileName` in `.gemini/settings.json`. Both are detailed in the matching `references/harnesses/<harness>.md`; apply only the ones you enabled. Where nested `AGENTS.md` files exist, write the same stub in each directory holding one — `references/agents-md.md` covers the one case that stops for approval first.
81
81
  8. If any bridge now exists, write or restore the non-material region in `AGENTS.md` per `references/agents-md.md`, unless the markers are present and empty. Without it, an agent asked to change a skill edits `.claude/skills/<name>/SKILL.md` — which a link resolves back to canonical, but a copy silently forks. Skip this when every enabled harness reads `.agents/skills/` natively; with no bridge there is nothing to warn about.
package/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { cli, exitCodes } from 'clibuilder'
3
+ import { depPluginsCommand } from './dep-plugins/dep-plugins.command.ts'
3
4
  import { doctorCommand } from './diagnose-bridges/doctor.command.ts'
4
5
  import { initCommand } from './initialize-harnesses/init.command.ts'
5
6
 
@@ -24,6 +25,7 @@ function app() {
24
25
  })
25
26
  .command(initCommand)
26
27
  .command(doctorCommand)
28
+ .command(depPluginsCommand)
27
29
  }
28
30
 
29
31
  /**
@@ -0,0 +1,168 @@
1
+ import { homedir } from 'node:os'
2
+ import { resolve } from 'node:path'
3
+ import type { cli } from 'clibuilder'
4
+ import { command, exitCodes, z } from 'clibuilder'
5
+ import { parseFormat, writeResult } from '../command-output/command-output.ts'
6
+ import { CATALOG_DIR, CATALOG_PATH, catalogStatus, deriveCatalog, type WriteOutcome, writeCatalog } from './generate.ts'
7
+ import { COPILOT_LIMITATION, type PluginRuntime, runtimes } from './harness-plugins.ts'
8
+ import { type Action, reconcile } from './reconcile.ts'
9
+
10
+ export type DepPluginsReport = {
11
+ catalog: string
12
+ marketplace: string
13
+ outcome: WriteOutcome | 'would-change' | 'current'
14
+ /** Always emitted, so a healthy run states its zero rather than leaving a reader to infer it. */
15
+ plugins: { name: string; package: string; version: string }[] | string
16
+ /** Every runtime detected on this machine, and whether it matches the catalog. */
17
+ harness: { runtime: string; status: string }[] | string
18
+ /**
19
+ * What each runtime needs in order to match the catalog, and the command that does it. Reported
20
+ * rather than run: this writes outside the repository, and a generation step should not reach into
21
+ * a developer's harness state without being asked.
22
+ *
23
+ * One flat row per action rather than actions nested under each runtime: a uniform record renders
24
+ * as a table in both TOON and text, where a nested array collapses into a JSON blob.
25
+ */
26
+ actions: { runtime: string; do: string; subject: string; why: string }[] | string
27
+ notes?: string[]
28
+ help?: string[]
29
+ }
30
+
31
+ /** The command that performs one action, so a reader is not left to look the verb up per runtime. */
32
+ function commandFor(runtime: PluginRuntime, action: Action, marketplace: string): string {
33
+ switch (action.kind) {
34
+ case 'register':
35
+ return runtime.register(CATALOG_DIR)
36
+ case 'install':
37
+ return runtime.install(action.subject)
38
+ case 'update': {
39
+ // Where a runtime caches the catalog it has to be refreshed first, or the update reinstalls
40
+ // the version already cached rather than the one the catalog now pins. Codex re-reads a local
41
+ // catalog on every add and needs no such step, so it declares no refresh command.
42
+ const refresh = runtime.refreshMarketplace(marketplace)
43
+ const update = runtime.update(action.subject)
44
+ return refresh ? `${refresh} && ${update}` : update
45
+ }
46
+ case 'uninstall':
47
+ return runtime.uninstall(action.subject)
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Notes name the conditions a caller cannot see from the entry list and would otherwise discover as a
53
+ * failure later — a dependency that is declared but absent, a marketplace name two repositories are
54
+ * likely to share, a resolver under which no dependency has a directory at all.
55
+ */
56
+ function notesFor(derived: ReturnType<typeof deriveCatalog>): string[] {
57
+ const notes: string[] = []
58
+ if (derived.pnp) {
59
+ notes.push(
60
+ '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.',
61
+ )
62
+ }
63
+ if (derived.genericName) {
64
+ notes.push(
65
+ `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.`,
66
+ )
67
+ }
68
+ for (const { package: pkg } of derived.unresolved) {
69
+ notes.push(`Declared dependency "${pkg}" is not installed; install dependencies before generating.`)
70
+ }
71
+ return notes
72
+ }
73
+
74
+ export const depPluginsCommand: cli.Command = command({
75
+ name: 'dep-plugins',
76
+ description:
77
+ '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.',
78
+ options: {
79
+ root: {
80
+ description: 'Repository directory. Defaults to the current directory.',
81
+ type: z.optional(z.string()),
82
+ },
83
+ check: {
84
+ description: 'Report whether the catalog is current without writing it. For CI and postinstall.',
85
+ type: z.optional(z.boolean()),
86
+ },
87
+ format: {
88
+ description: 'Output format: toon (default), json, or text for a human-readable report.',
89
+ type: z.optional(z.string()),
90
+ default: 'toon',
91
+ },
92
+ },
93
+ run(args) {
94
+ try {
95
+ const format = parseFormat(args.format)
96
+ const root = args.root ?? process.cwd()
97
+ const derived = deriveCatalog(root)
98
+
99
+ // `--check` compares without touching the disk, so it is safe from a postinstall hook and from
100
+ // CI, where the question is only whether the committed catalog still matches the declared
101
+ // dependencies.
102
+ const outcome = args.check
103
+ ? catalogStatus(root, derived.catalog) === 'unchanged'
104
+ ? ('current' as const)
105
+ : ('would-change' as const)
106
+ : writeCatalog(root, derived.catalog)
107
+
108
+ // Read-only, and against each harness's own state files rather than its CLI: asking a runtime
109
+ // what it has should not depend on that runtime being launchable from here. Only runtimes
110
+ // actually set up on this machine are reported, so a report names work a reader can do.
111
+ const home = homedir()
112
+ const notes = notesFor(derived)
113
+ const detected = runtimes.filter((candidate) => candidate.present(home))
114
+ const harness: { runtime: string; status: string }[] = []
115
+ const actions: { runtime: string; do: string; subject: string; why: string }[] = []
116
+
117
+ for (const runtime of detected) {
118
+ const state = runtime.state(home)
119
+ const delta = reconcile({
120
+ catalog: derived.catalog,
121
+ catalogDir: CATALOG_DIR,
122
+ root: resolve(root),
123
+ installed: state.installed,
124
+ marketplaces: state.marketplaces,
125
+ scoped: runtime.scoped,
126
+ })
127
+ if (delta.collision) {
128
+ notes.push(
129
+ `${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.`,
130
+ )
131
+ }
132
+ harness.push({
133
+ runtime: runtime.name,
134
+ status: delta.actions.length ? `${delta.actions.length} to apply` : 'in sync',
135
+ })
136
+ for (const action of delta.actions) {
137
+ actions.push({
138
+ runtime: runtime.name,
139
+ do: commandFor(runtime, action, derived.catalog.name),
140
+ subject: action.subject,
141
+ why: action.detail,
142
+ })
143
+ }
144
+ }
145
+ if (derived.plugins.length) notes.push(COPILOT_LIMITATION)
146
+
147
+ const report: DepPluginsReport = {
148
+ catalog: CATALOG_PATH,
149
+ marketplace: derived.catalog.name,
150
+ outcome,
151
+ plugins: derived.plugins.length
152
+ ? derived.plugins.map(({ plugin, package: pkg, version }) => ({ name: plugin, package: pkg, version }))
153
+ : '0 dependencies ship a plugin — nothing to install',
154
+ harness: harness.length ? harness : 'no supported harness detected on this machine',
155
+ actions: actions.length ? actions : '0 actions — every detected harness matches the catalog',
156
+ ...(notes.length ? { notes } : {}),
157
+ }
158
+ writeResult(report, format)
159
+
160
+ // A stale catalog under `--check` is the one failure this command reports through its exit
161
+ // code, because that is the whole question the flag asks. Everything else the report carries.
162
+ return outcome === 'would-change' ? exitCodes.error : exitCodes.success
163
+ } catch (error) {
164
+ process.stderr.write(`error: ${error instanceof Error ? error.message : 'Catalog generation failed.'}\n`)
165
+ return exitCodes.error
166
+ }
167
+ },
168
+ })
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Dependency-provided plugins: the catalog half.
3
+ *
4
+ * A dependency that ships an Agent Plugins manifest at its package root is a plugin the consuming
5
+ * repository can install through its harness. This module turns the set of such dependencies into a
6
+ * marketplace catalog; installing from that catalog is the runtime's own job, and the lifecycle half
7
+ * drives it.
8
+ *
9
+ * Everything here is pure. Resolution against `node_modules` lives in `resolve.ts`.
10
+ */
11
+
12
+ /** One dependency that ships a plugin manifest. */
13
+ export type DepPlugin = {
14
+ /** The npm package name — what a consumer declared, and what the catalog installs by. */
15
+ package: string
16
+ /** The plugin's own name from its manifest, which need not match the package name. */
17
+ plugin: string
18
+ /** The version actually installed, read from the dependency's own `package.json`. */
19
+ version: string
20
+ description?: string
21
+ }
22
+
23
+ /**
24
+ * A catalog entry's `npm` source. Deliberately not a path: a path source is resolved against the
25
+ * marketplace root and may not escape it, so a path-sourced catalog would have to sit at the
26
+ * repository root — the one place the repository's own public catalog already occupies. An `npm`
27
+ * source carries no path, so this catalog can live in its own directory and stay separate from
28
+ * whatever the repository publishes for its own consumers.
29
+ */
30
+ export type NpmSource = {
31
+ source: 'npm'
32
+ package: string
33
+ version: string
34
+ }
35
+
36
+ export type CatalogEntry = {
37
+ name: string
38
+ source: NpmSource
39
+ description?: string
40
+ }
41
+
42
+ /**
43
+ * The Claude-shaped catalog, which Codex, Copilot CLI, and Cursor also read. `owner` is an object
44
+ * whose `name` is required — a string there is a schema error rather than a tolerated shorthand.
45
+ */
46
+ export type Catalog = {
47
+ name: string
48
+ owner: { name: string }
49
+ description: string
50
+ plugins: CatalogEntry[]
51
+ }
52
+
53
+ /**
54
+ * Marketplace names generic enough that two repositories are likely to pick the same one. The
55
+ * collision is silent and costly: registering a second marketplace under a name already in use
56
+ * replaces the first, and the install that follows reports "already installed" and does nothing —
57
+ * so the repository silently runs the other one's plugin versions.
58
+ */
59
+ const GENERIC_NAMES = new Set(['deps', 'dependencies', 'local', 'plugins', 'marketplace', 'plugin', 'default'])
60
+
61
+ export function isGenericName(name: string): boolean {
62
+ return GENERIC_NAMES.has(name.toLowerCase())
63
+ }
64
+
65
+ /**
66
+ * A marketplace name derived from the consuming package's own name, so two repositories do not
67
+ * collide by default. A scope is folded into the name rather than dropped (`@acme/web` → `acme-web`)
68
+ * because the unscoped half is the part most likely to repeat across organizations.
69
+ */
70
+ export function marketplaceName(packageName: string): string {
71
+ const folded = packageName.replace(/^@/, '').replace(/\//g, '-')
72
+ const cleaned = folded
73
+ .replace(/[^a-zA-Z0-9-_.]/g, '-')
74
+ .replace(/-+/g, '-')
75
+ .replace(/^-|-$/g, '')
76
+ return cleaned || 'agent-plugins'
77
+ }
78
+
79
+ /**
80
+ * Dependency names a repository declares, both runtime and development.
81
+ *
82
+ * Development dependencies are included on purpose: a plugin that supplies a project's testing or
83
+ * release workflow is a development dependency, and excluding them would drop most of the cases this
84
+ * exists for. Transitive dependencies are never included — see `resolve.ts`.
85
+ */
86
+ export function declaredDependencies(manifest: unknown): string[] {
87
+ if (!manifest || typeof manifest !== 'object') return []
88
+ const record = manifest as Record<string, unknown>
89
+ const names = new Set<string>()
90
+ for (const field of ['dependencies', 'devDependencies']) {
91
+ const group = record[field]
92
+ if (!group || typeof group !== 'object') continue
93
+ for (const name of Object.keys(group as Record<string, unknown>)) names.add(name)
94
+ }
95
+ return [...names].sort()
96
+ }
97
+
98
+ export type BuildCatalogOptions = {
99
+ name: string
100
+ owner: string
101
+ plugins: readonly DepPlugin[]
102
+ }
103
+
104
+ /**
105
+ * The catalog is derived in full on every run and never merged into what is already on disk. That is
106
+ * what makes the three cases a consumer cares about — a plugin added, a plugin removed, a version
107
+ * moved — fall out of regeneration instead of needing reconciliation logic of their own.
108
+ *
109
+ * Entries are sorted by name so that a regeneration whose inputs did not change produces a
110
+ * byte-identical file, which is what lets a check mode compare rather than interpret.
111
+ */
112
+ export function buildCatalog({ name, owner, plugins }: BuildCatalogOptions): Catalog {
113
+ const entries = [...plugins]
114
+ .sort((a, b) => a.plugin.localeCompare(b.plugin))
115
+ .map(({ plugin, package: pkg, version, description }): CatalogEntry => {
116
+ const source: NpmSource = { source: 'npm', package: pkg, version }
117
+ return description ? { name: plugin, source, description } : { name: plugin, source }
118
+ })
119
+
120
+ return {
121
+ name,
122
+ owner: { name: owner },
123
+ description: 'Plugins provided by this repository’s dependencies. Generated — do not edit.',
124
+ plugins: entries,
125
+ }
126
+ }
127
+
128
+ /** Serialized the way the repository formats JSON, with the trailing newline a text file needs. */
129
+ export function serializeCatalog(catalog: Catalog): string {
130
+ return `${JSON.stringify(catalog, null, '\t')}\n`
131
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Deriving a repository's dependency-plugin catalog, and deciding whether what is on disk matches.
3
+ */
4
+
5
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
6
+ import { dirname, join } from 'node:path'
7
+ import {
8
+ buildCatalog,
9
+ type Catalog,
10
+ type DepPlugin,
11
+ declaredDependencies,
12
+ isGenericName,
13
+ marketplaceName,
14
+ serializeCatalog,
15
+ } from './dep-plugins.ts'
16
+ import { isPnp, readDepPlugin, type SkipReason } from './resolve.ts'
17
+
18
+ /**
19
+ * The catalog's home, relative to the repository root.
20
+ *
21
+ * Under this tool's own directory, beside the other files it owns there — the same place as
22
+ * `mcp.toml` and `mcp.projected.json`, and the same shape as `.agents/repobuddy/` and
23
+ * `.agents/cyberlegion/`. A directory named for what it holds rather than for who writes it would be
24
+ * a second convention in a tree that already has one.
25
+ *
26
+ * Not the repository root's `.claude-plugin/`, which is where a repository publishes its *own*
27
+ * plugins for its *own* consumers. Merging the two would leak every dependency-provided plugin into
28
+ * what a consumer sees on adding the published marketplace, and no harness has a notion of a private
29
+ * catalog entry to prevent that. Being a separate directory also makes this one a marketplace root
30
+ * in its own right, which is what lets its entries be resolved without reaching outside it.
31
+ */
32
+ export const CATALOG_DIR: string = join('.agents', 'buddy-agent-harness')
33
+ export const CATALOG_PATH: string = join(CATALOG_DIR, '.claude-plugin', 'marketplace.json')
34
+
35
+ /** A declared dependency that yielded no plugin, kept so a report can say why rather than go quiet. */
36
+ export type Skipped = { package: string; reason: SkipReason }
37
+
38
+ export type DerivedCatalog = {
39
+ catalog: Catalog
40
+ plugins: DepPlugin[]
41
+ /** Only the dependencies that failed to resolve at all; a dependency with no plugin is not news. */
42
+ unresolved: Skipped[]
43
+ /** Set when the marketplace name is one two repositories are likely to share. */
44
+ genericName: boolean
45
+ /** Set under Yarn PnP, where no dependency has a directory for a harness to install from. */
46
+ pnp: boolean
47
+ }
48
+
49
+ function readManifest(path: string): Record<string, unknown> {
50
+ return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>
51
+ }
52
+
53
+ /**
54
+ * Derives the catalog from the repository's declared dependencies.
55
+ *
56
+ * Derived in full every time. A plugin newly added to `package.json` appears, one removed disappears,
57
+ * and a version that moved follows — all three without a line of reconciliation logic, because there
58
+ * is nothing to reconcile against. That is the same rule the vendor manifests already follow: the
59
+ * artifact is derived, never authored.
60
+ */
61
+ export function deriveCatalog(root: string): DerivedCatalog {
62
+ const manifestPath = join(root, 'package.json')
63
+ const manifest = readManifest(manifestPath)
64
+
65
+ const plugins: DepPlugin[] = []
66
+ const unresolved: Skipped[] = []
67
+ for (const pkg of declaredDependencies(manifest)) {
68
+ const outcome = readDepPlugin(pkg, manifestPath)
69
+ if (outcome.ok) plugins.push(outcome.plugin)
70
+ else if (outcome.reason === 'unresolved') unresolved.push({ package: pkg, reason: outcome.reason })
71
+ }
72
+
73
+ const owner = typeof manifest['name'] === 'string' ? manifest['name'] : 'unknown'
74
+ const name = marketplaceName(owner)
75
+
76
+ return {
77
+ catalog: buildCatalog({ name, owner, plugins }),
78
+ plugins,
79
+ unresolved,
80
+ genericName: isGenericName(name),
81
+ pnp: isPnp(),
82
+ }
83
+ }
84
+
85
+ /** What a write did, or would do, so a caller can report a no-op as a no-op rather than as a change. */
86
+ export type WriteOutcome = 'created' | 'updated' | 'unchanged'
87
+
88
+ /**
89
+ * What writing the catalog would do, without touching the disk.
90
+ *
91
+ * Comparing the serialized form rather than the parsed one is deliberate: a byte comparison is what
92
+ * lets a check mode be a comparison instead of an interpretation, and `buildCatalog` sorts its
93
+ * entries so that unchanged inputs serialize identically.
94
+ */
95
+ export function catalogStatus(root: string, catalog: Catalog): WriteOutcome {
96
+ let current: string | undefined
97
+ try {
98
+ current = readFileSync(join(root, CATALOG_PATH), 'utf8')
99
+ } catch {
100
+ current = undefined
101
+ }
102
+ if (current === serializeCatalog(catalog)) return 'unchanged'
103
+ return current === undefined ? 'created' : 'updated'
104
+ }
105
+
106
+ /** Writes the catalog, reporting whether it moved. A write that would change nothing writes nothing. */
107
+ export function writeCatalog(root: string, catalog: Catalog): WriteOutcome {
108
+ const outcome = catalogStatus(root, catalog)
109
+ if (outcome === 'unchanged') return outcome
110
+
111
+ const path = join(root, CATALOG_PATH)
112
+ mkdirSync(dirname(path), { recursive: true })
113
+ writeFileSync(path, serializeCatalog(catalog))
114
+ return outcome
115
+ }