buddy-agent-harness 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/.cursor-plugin/plugin.json +1 -1
  4. package/README.md +2 -2
  5. package/bin/buddy-agent-harness.mjs +4 -3
  6. package/dist/cli.mjs +480 -546
  7. package/dist/index.d.mts +205 -298
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +682 -621
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/skill-scripts/doctor.mjs +45 -42
  12. package/dist/skill-scripts/init.mjs +45 -42
  13. package/package.json +1 -1
  14. package/plugin.json +1 -1
  15. package/skills/{doctor → doctor-buddy-agent-harness}/README.md +1 -1
  16. package/skills/{doctor → doctor-buddy-agent-harness}/SKILL.md +9 -7
  17. package/skills/{doctor → doctor-buddy-agent-harness}/references/bridges.md +7 -7
  18. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/claude-code.md +3 -2
  19. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/codex.md +1 -1
  20. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/copilot-cli.md +1 -1
  21. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/cursor.md +1 -1
  22. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/devin-desktop.md +1 -1
  23. package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/gemini-cli.md +1 -1
  24. package/skills/doctor-buddy-agent-harness/references/instructions.md +22 -0
  25. package/skills/{doctor → doctor-buddy-agent-harness}/references/nonstandard.md +4 -4
  26. package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +303 -0
  27. package/skills/enhance/SKILL.md +12 -3
  28. package/skills/enhance/references/delegation.history.md +11 -1
  29. package/skills/enhance/references/delegation.md +23 -2
  30. package/skills/{init → init-buddy-agent-harness}/README.md +1 -1
  31. package/skills/{init → init-buddy-agent-harness}/SKILL.md +22 -14
  32. package/skills/{init → init-buddy-agent-harness}/agents/openai.yaml +1 -1
  33. package/skills/{init → init-buddy-agent-harness}/references/agents-md.md +38 -19
  34. package/skills/{init → init-buddy-agent-harness}/references/detection.md +2 -2
  35. package/skills/init-buddy-agent-harness/references/harnesses/claude-code.md +64 -0
  36. package/skills/init-buddy-agent-harness/scripts/doctor.mjs +303 -0
  37. package/skills/init-buddy-agent-harness/scripts/init.mjs +303 -0
  38. package/skills/repair/SKILL.md +2 -2
  39. package/skills/repair/references/classes.md +2 -2
  40. package/skills/repair/scripts/doctor.mjs +45 -42
  41. package/src/cli.ts +15 -24
  42. package/src/command-output/command-output.ts +21 -12
  43. package/src/dep-plugins/dep-plugins.command.ts +10 -26
  44. package/src/dep-plugins/dep-plugins.ts +13 -46
  45. package/src/dep-plugins/generate.ts +6 -34
  46. package/src/dep-plugins/harness-plugins.ts +16 -78
  47. package/src/dep-plugins/reconcile.ts +10 -27
  48. package/src/dep-plugins/resolve.ts +7 -46
  49. package/src/diagnose-bridges/agents-files.ts +2 -6
  50. package/src/diagnose-bridges/diagnose-bridges.ts +9 -13
  51. package/src/diagnose-bridges/diagnose-instructions.ts +73 -57
  52. package/src/diagnose-bridges/directory-files.ts +1 -4
  53. package/src/diagnose-bridges/doctor-guidance.ts +97 -148
  54. package/src/diagnose-bridges/doctor.command.ts +45 -21
  55. package/src/diagnose-bridges/git-bridge-state.ts +25 -35
  56. package/src/diagnose-bridges/json-with-comments.ts +2 -7
  57. package/src/diagnose-bridges/locator.ts +5 -14
  58. package/src/diagnose-configuration/diagnose-configuration.ts +17 -27
  59. package/src/diagnose-mcp/diagnose-mcp.ts +8 -34
  60. package/src/diagnose-mcp/mcp-baseline.ts +9 -33
  61. package/src/diagnose-mcp/mcp-model.ts +2 -29
  62. package/src/diagnose-mcp/mcp-secrets.ts +18 -45
  63. package/src/diagnose-mcp/mcp-sources.ts +13 -32
  64. package/src/diagnose-nonstandard/diagnose-nonstandard.ts +11 -22
  65. package/src/governance-overrides/governance-overrides.ts +148 -0
  66. package/src/governance-overrides/governance.command.ts +147 -0
  67. package/src/harness-registry/harness-registry.ts +24 -54
  68. package/src/harness-registry/instruction-bridge.ts +10 -29
  69. package/src/harness-registry/mcp-config.ts +4 -28
  70. package/src/harness-registry/nonstandard-artifact.ts +6 -18
  71. package/src/index.ts +18 -0
  72. package/src/initialize-harnesses/init.command.ts +6 -5
  73. package/src/initialize-harnesses/initialize-harnesses.ts +12 -1
  74. package/src/is-record/is-record.ts +0 -9
  75. package/src/mcp-inventory/mcp-inventory.ts +200 -40
  76. package/src/skill-projection/skill-projection.ts +16 -28
  77. package/src/skill-scripts/doctor.ts +4 -6
  78. package/src/skill-scripts/init.ts +4 -5
  79. package/src/skill-scripts/launchers.ts +5 -10
  80. package/skills/doctor/references/instructions.md +0 -14
  81. package/skills/doctor/scripts/doctor.mjs +0 -300
  82. package/skills/init/references/harnesses/claude-code.md +0 -54
  83. package/skills/init/scripts/doctor.mjs +0 -300
  84. package/skills/init/scripts/init.mjs +0 -300
  85. /package/skills/{doctor → doctor-buddy-agent-harness}/references/configuration.md +0 -0
  86. /package/skills/{doctor → doctor-buddy-agent-harness}/references/harnesses/windsurf.md +0 -0
  87. /package/skills/{doctor → doctor-buddy-agent-harness}/references/mcp.md +0 -0
  88. /package/skills/{init → init-buddy-agent-harness}/references/frontmatter.md +0 -0
  89. /package/skills/{init → init-buddy-agent-harness}/references/harnesses/codex.md +0 -0
  90. /package/skills/{init → init-buddy-agent-harness}/references/harnesses/copilot-cli.md +0 -0
  91. /package/skills/{init → init-buddy-agent-harness}/references/harnesses/cursor.md +0 -0
  92. /package/skills/{init → init-buddy-agent-harness}/references/harnesses/devin-desktop.md +0 -0
  93. /package/skills/{init → init-buddy-agent-harness}/references/harnesses/gemini-cli.md +0 -0
  94. /package/skills/{init → init-buddy-agent-harness}/references/standard.md +0 -0
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@ import { a as __toESM, i as __require, n as unpartial, r as __commonJSMin, t as
2
2
  import { createRequire } from "node:module";
3
3
  import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
4
4
  import { dirname, extname, join, parse, posix, relative, resolve, sep } from "node:path";
5
- import { pathToFileURL } from "node:url";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { homedir, platform } from "node:os";
7
7
  import { execFileSync } from "node:child_process";
8
8
  //#region ../../node_modules/.pnpm/clibuilder@10.1.0/node_modules/clibuilder/esm/argv.js
@@ -15651,15 +15651,6 @@ function resolveOptions(options) {
15651
15651
  }
15652
15652
  //#endregion
15653
15653
  //#region src/is-record/is-record.ts
15654
- /**
15655
- * Whether a value is a record: an object with named keys, and not an array.
15656
- *
15657
- * Everything this package reads is a file someone else wrote — a golden MCP set, a harness config,
15658
- * a projection record — so every parse lands on `unknown` and every reader narrows it here before
15659
- * asking for a key. Four modules had their own copy of the same three clauses; a predicate that
15660
- * decides what counts as a parsed object is one fact, and a copy of it that drifts changes what a
15661
- * file is read to say.
15662
- */
15663
15654
  function isRecord(value) {
15664
15655
  return typeof value === "object" && value !== null && !Array.isArray(value);
15665
15656
  }
@@ -15679,6 +15670,13 @@ function parseFormat(value) {
15679
15670
  function writeResult(value, format) {
15680
15671
  process.stdout.write(`${encodeResult(value, format)}\n`);
15681
15672
  }
15673
+ /**
15674
+ * Writes a document exactly as read, bypassing the encoders — a Markdown body run through TOON or
15675
+ * the text renderer comes back as one escaped line. A command writes one or the other, never both.
15676
+ */
15677
+ function writeDocument(content) {
15678
+ process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
15679
+ }
15682
15680
  function encodeResult(value, format) {
15683
15681
  if (format === "json") return JSON.stringify(value);
15684
15682
  return format === "text" ? renderText(value) : encode(value);
@@ -15686,18 +15684,13 @@ function encodeResult(value, format) {
15686
15684
  function cell(value) {
15687
15685
  return value === void 0 ? "" : String(value);
15688
15686
  }
15689
- /** A header row plus one row per record, every column padded to its widest cell. */
15690
15687
  function table(rows) {
15691
15688
  const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
15692
15689
  const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));
15693
15690
  const line = (values) => ` ${values.map((value, index) => value.padEnd(widths[index])).join(" ")}`.trimEnd();
15694
15691
  return [line(columns), ...rows.map((row) => line(columns.map((column) => cell(row[column]))))];
15695
15692
  }
15696
- /**
15697
- * The same result rendered for a person rather than for an agent: scalars as `key: value`, records
15698
- * as an aligned table, and everything else as a bulleted list. TOON stays the default because it is
15699
- * what an agent parses; this is for reading over someone's shoulder.
15700
- */
15693
+ /** Renders for a person, not an agent — TOON stays the default since that's what an agent parses. */
15701
15694
  function renderText(value) {
15702
15695
  const blocks = Object.entries(value).map(([key, item]) => {
15703
15696
  if (!Array.isArray(item)) return [`${key}: ${isRecord(item) ? JSON.stringify(item) : String(item)}`];
@@ -15710,19 +15703,19 @@ function renderText(value) {
15710
15703
  return previous && (previous.length > 1 || block.length > 1) ? ["", ...block] : block;
15711
15704
  }).join("\n");
15712
15705
  }
15713
- /** AXI §10: the executable's absolute path, with the user's home directory collapsed to `~`. */
15714
- function binPath(home, executable) {
15715
- if (!executable) return "buddy-agent-harness";
15716
- return home && executable.startsWith(home + sep) ? `~${executable.slice(home.length)}` : executable;
15706
+ /**
15707
+ * AXI §10: collapses the home directory to `~` so a path in a report is portable to another
15708
+ * machine.
15709
+ */
15710
+ function collapseHome(home, path) {
15711
+ return home && path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
15712
+ }
15713
+ function displayBinPath(home, executable) {
15714
+ return executable ? collapseHome(home, executable) : "buddy-agent-harness";
15717
15715
  }
15718
15716
  //#endregion
15719
15717
  //#region src/dep-plugins/dep-plugins.ts
15720
- /**
15721
- * Marketplace names generic enough that two repositories are likely to pick the same one. The
15722
- * collision is silent and costly: registering a second marketplace under a name already in use
15723
- * replaces the first, and the install that follows reports "already installed" and does nothing —
15724
- * so the repository silently runs the other one's plugin versions.
15725
- */
15718
+ /** Generic marketplace names: a second registration under one of these replaces the first, silently. */
15726
15719
  const GENERIC_NAMES = /* @__PURE__ */ new Set([
15727
15720
  "deps",
15728
15721
  "dependencies",
@@ -15736,19 +15729,15 @@ function isGenericName(name) {
15736
15729
  return GENERIC_NAMES.has(name.toLowerCase());
15737
15730
  }
15738
15731
  /**
15739
- * A marketplace name derived from the consuming package's own name, so two repositories do not
15740
- * collide by default. A scope is folded into the name rather than dropped (`@acme/web` → `acme-web`)
15741
- * because the unscoped half is the part most likely to repeat across organizations.
15732
+ * Folds a scope into the name (`@acme/web` → `acme-web`) rather than dropping it — the unscoped
15733
+ * part collides more often.
15742
15734
  */
15743
15735
  function marketplaceName(packageName) {
15744
15736
  return packageName.replace(/^@/, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-_.]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "agent-plugins";
15745
15737
  }
15746
15738
  /**
15747
- * Dependency names a repository declares, both runtime and development.
15748
- *
15749
- * Development dependencies are included on purpose: a plugin that supplies a project's testing or
15750
- * release workflow is a development dependency, and excluding them would drop most of the cases this
15751
- * exists for. Transitive dependencies are never included — see `resolve.ts`.
15739
+ * Includes `devDependencies` — a plugin can supply a project's testing or release workflow.
15740
+ * Transitive dependencies are excluded; see `resolve.ts`.
15752
15741
  */
15753
15742
  function declaredDependencies(manifest) {
15754
15743
  if (!manifest || typeof manifest !== "object") return [];
@@ -15762,12 +15751,8 @@ function declaredDependencies(manifest) {
15762
15751
  return [...names].sort();
15763
15752
  }
15764
15753
  /**
15765
- * The catalog is derived in full on every run and never merged into what is already on disk. That is
15766
- * what makes the three cases a consumer cares about — a plugin added, a plugin removed, a version
15767
- * moved — fall out of regeneration instead of needing reconciliation logic of their own.
15768
- *
15769
- * Entries are sorted by name so that a regeneration whose inputs did not change produces a
15770
- * byte-identical file, which is what lets a check mode compare rather than interpret.
15754
+ * Derived in full every run, never merged with what's on disk. Entries are sorted so unchanged
15755
+ * inputs serialize identically, which `--check` relies on.
15771
15756
  */
15772
15757
  function buildCatalog({ name, owner, plugins }) {
15773
15758
  const entries = [...plugins].sort((a, b) => a.plugin.localeCompare(b.plugin)).map(({ plugin, package: pkg, version, description }) => {
@@ -15792,22 +15777,12 @@ function buildCatalog({ name, owner, plugins }) {
15792
15777
  plugins: entries
15793
15778
  };
15794
15779
  }
15795
- /** Serialized the way the repository formats JSON, with the trailing newline a text file needs. */
15780
+ /** Includes the trailing newline. */
15796
15781
  function serializeCatalog(catalog) {
15797
15782
  return `${JSON.stringify(catalog, null, " ")}\n`;
15798
15783
  }
15799
15784
  //#endregion
15800
15785
  //#region src/dep-plugins/resolve.ts
15801
- /**
15802
- * Locating a declared dependency on disk, and reading the plugin manifest it may ship.
15803
- *
15804
- * Resolution rather than scanning. Walking `node_modules` finds transitive dependencies nobody
15805
- * vetted, and a skill or plugin is instruction text that steers an agent — an unvetted transitive
15806
- * dependency is a supply-chain surface, not a convenience. Asking the module resolver where a
15807
- * *declared* dependency lives answers the question that was actually asked, and it is correct under
15808
- * npm, pnpm, yarn with the node-modules linker, and bun alike, because all four put a real directory
15809
- * on disk. Only Yarn PnP does not, and it is reported rather than guessed at.
15810
- */
15811
15786
  function readJson$1(path) {
15812
15787
  try {
15813
15788
  return JSON.parse(readFileSync(path, "utf8"));
@@ -15815,10 +15790,6 @@ function readJson$1(path) {
15815
15790
  return;
15816
15791
  }
15817
15792
  }
15818
- /**
15819
- * Every ancestor `node_modules/<pkg>` from `startDir` upward. The fallback path only — see
15820
- * {@link packageDir}.
15821
- */
15822
15793
  function ancestorCandidates(startDir, pkg) {
15823
15794
  const segments = pkg.split("/");
15824
15795
  const candidates = [];
@@ -15832,17 +15803,8 @@ function ancestorCandidates(startDir, pkg) {
15832
15803
  return candidates;
15833
15804
  }
15834
15805
  /**
15835
- * The directory a declared dependency was installed into, or `undefined`.
15836
- *
15837
- * Two mechanisms, in order, because neither alone is sufficient:
15838
- *
15839
- * 1. **`require.resolve('<pkg>/package.json')`, anchored at the declaring manifest.** The anchor
15840
- * matters: under pnpm's isolated store only the package that declares a dependency can resolve it,
15841
- * so resolving from a workspace root would miss what a workspace member declared.
15842
- * 2. **A walk up `node_modules`.** Roughly one package in seven declares an `exports` map without a
15843
- * `./package.json` entry, and those refuse the first mechanism with `ERR_PACKAGE_PATH_NOT_EXPORTED`
15844
- * however plainly they are installed. Resolving a deeper file instead is not an escape — the same
15845
- * map blocks that too.
15806
+ * Anchored at the declaring manifest (required for pnpm's isolation), then falls back to a
15807
+ * `node_modules` walk — an `exports` map without a `./package.json` entry blocks the first.
15846
15808
  */
15847
15809
  function packageDir(pkg, fromManifest) {
15848
15810
  try {
@@ -15851,16 +15813,7 @@ function packageDir(pkg, fromManifest) {
15851
15813
  } catch {}
15852
15814
  for (const candidate of ancestorCandidates(dirname(fromManifest), pkg)) if (existsSync(join(candidate, "package.json"))) return candidate;
15853
15815
  }
15854
- /**
15855
- * Reads the plugin a dependency ships, if it ships one.
15856
- *
15857
- * The version comes from the dependency's own `package.json` rather than from a lockfile. It is the
15858
- * same number — the version actually installed — and reading it needs no knowledge of which of the
15859
- * four lockfile formats this repository uses.
15860
- *
15861
- * A manifest at the package root is what the Agent Plugins Specification pins, and it is the form a
15862
- * harness accepts as a marketplace source without any vendor-specific manifest alongside it.
15863
- */
15816
+ /** A manifest lives at the package root — the Agent Plugins Specification's pinned location. */
15864
15817
  function readDepPlugin(pkg, fromManifest) {
15865
15818
  const dir = packageDir(pkg, fromManifest);
15866
15819
  if (!dir) return {
@@ -15904,10 +15857,8 @@ function readDepPlugin(pkg, fromManifest) {
15904
15857
  };
15905
15858
  }
15906
15859
  /**
15907
- * True when the process is running under Yarn's Plug'n'Play resolver, where packages live inside zip
15908
- * archives rather than as directories. A marketplace source must be a real directory and an npm
15909
- * source must be installable, so a PnP project needs `nodeLinker: node-modules` or the affected
15910
- * dependencies unplugged. Detected rather than worked around, so the report can say so.
15860
+ * True under Yarn PnP, where packages live in zip archives rather than directories — nothing here
15861
+ * has an installable directory.
15911
15862
  */
15912
15863
  function isPnp() {
15913
15864
  return Boolean(process.versions["pnp"]);
@@ -15915,35 +15866,14 @@ function isPnp() {
15915
15866
  //#endregion
15916
15867
  //#region src/dep-plugins/generate.ts
15917
15868
  /**
15918
- * Deriving a repository's dependency-plugin catalog, and deciding whether what is on disk matches.
15919
- */
15920
- /**
15921
- * The catalog's home, relative to the repository root.
15922
- *
15923
- * Under this tool's own directory, beside the other files it owns there — the same place as
15924
- * `mcp.toml` and `mcp.projected.json`, and the same shape as `.agents/repobuddy/` and
15925
- * `.agents/cyberlegion/`. A directory named for what it holds rather than for who writes it would be
15926
- * a second convention in a tree that already has one.
15927
- *
15928
- * Not the repository root's `.claude-plugin/`, which is where a repository publishes its *own*
15929
- * plugins for its *own* consumers. Merging the two would leak every dependency-provided plugin into
15930
- * what a consumer sees on adding the published marketplace, and no harness has a notion of a private
15931
- * catalog entry to prevent that. Being a separate directory also makes this one a marketplace root
15932
- * in its own right, which is what lets its entries be resolved without reaching outside it.
15869
+ * Not the repository root's `.claude-plugin/` — that's the repo's own published catalog; merging
15870
+ * would leak dependency plugins into it.
15933
15871
  */
15934
15872
  const CATALOG_DIR = join(".agents", "buddy-agent-harness");
15935
15873
  const CATALOG_PATH = join(CATALOG_DIR, ".claude-plugin", "marketplace.json");
15936
15874
  function readManifest(path) {
15937
15875
  return JSON.parse(readFileSync(path, "utf8"));
15938
15876
  }
15939
- /**
15940
- * Derives the catalog from the repository's declared dependencies.
15941
- *
15942
- * Derived in full every time. A plugin newly added to `package.json` appears, one removed disappears,
15943
- * and a version that moved follows — all three without a line of reconciliation logic, because there
15944
- * is nothing to reconcile against. That is the same rule the vendor manifests already follow: the
15945
- * artifact is derived, never authored.
15946
- */
15947
15877
  function deriveCatalog(root) {
15948
15878
  const manifestPath = join(root, "package.json");
15949
15879
  const manifest = readManifest(manifestPath);
@@ -15972,11 +15902,8 @@ function deriveCatalog(root) {
15972
15902
  };
15973
15903
  }
15974
15904
  /**
15975
- * What writing the catalog would do, without touching the disk.
15976
- *
15977
- * Comparing the serialized form rather than the parsed one is deliberate: a byte comparison is what
15978
- * lets a check mode be a comparison instead of an interpretation, and `buildCatalog` sorts its
15979
- * entries so that unchanged inputs serialize identically.
15905
+ * Compares serialized bytes, not parsed values — that's what makes `--check` a comparison rather
15906
+ * than an interpretation.
15980
15907
  */
15981
15908
  function catalogStatus(root, catalog) {
15982
15909
  let current;
@@ -15988,7 +15915,7 @@ function catalogStatus(root, catalog) {
15988
15915
  if (current === serializeCatalog(catalog)) return "unchanged";
15989
15916
  return current === void 0 ? "created" : "updated";
15990
15917
  }
15991
- /** Writes the catalog, reporting whether it moved. A write that would change nothing writes nothing. */
15918
+ /** A write that would change nothing writes nothing. */
15992
15919
  function writeCatalog(root, catalog) {
15993
15920
  const outcome = catalogStatus(root, catalog);
15994
15921
  if (outcome === "unchanged") return outcome;
@@ -15999,31 +15926,6 @@ function writeCatalog(root, catalog) {
15999
15926
  }
16000
15927
  //#endregion
16001
15928
  //#region src/dep-plugins/harness-plugins.ts
16002
- /**
16003
- * What each harness does with a marketplace catalog, and how to read what it currently has.
16004
- *
16005
- * The runtimes diverge more than they look, and every difference below was established by running
16006
- * the shipped CLI rather than by reading a document:
16007
- *
16008
- * - **Claude Code** refreshes with `plugin update`; re-running `plugin install` is a no-op that
16009
- * reports "already installed" and leaves the stale copy in place. Every verb defaults to user
16010
- * scope, so each one has to name `--scope project` explicitly.
16011
- * - **Codex** has no update verb and no scope. Re-running `plugin add` re-reads the catalog and
16012
- * replaces the cached copy, so the same command installs and refreshes. It records no version in
16013
- * its config — the installed version is only in the cache directory's name.
16014
- * - **Copilot CLI** accepts a path source but **rejects an `npm` source**, so it cannot consume this
16015
- * catalog at all. See {@link COPILOT_LIMITATION}.
16016
- * - **Cursor** exposes no plugin subcommand from a terminal.
16017
- *
16018
- * A consumer should not have to know any of that, which is why it is here rather than in a document.
16019
- */
16020
- /**
16021
- * Why Copilot CLI is absent from the table. Recorded rather than dropped, so the reason travels with
16022
- * the code: `copilot plugin marketplace add` rejects this catalog with
16023
- * `Invalid marketplace.json: plugins.0.source: Invalid input`, while the same catalog with a
16024
- * `./path` source is accepted. Supporting it needs a second, path-sourced catalog — and a path source
16025
- * cannot escape its marketplace root, which is the constraint the `npm` source was chosen to avoid.
16026
- */
16027
15929
  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.";
16028
15930
  function readJson(path) {
16029
15931
  try {
@@ -16039,19 +15941,9 @@ function directories(path) {
16039
15941
  return [];
16040
15942
  }
16041
15943
  }
16042
- /**
16043
- * `CLAUDE_CONFIG_DIR` wins where set, which is also what keeps a test off a developer's real
16044
- * configuration.
16045
- */
16046
15944
  function claudeConfigDir(home, env = process.env) {
16047
15945
  return env["CLAUDE_CONFIG_DIR"] || join(home, ".claude");
16048
15946
  }
16049
- /**
16050
- * Reads what Claude Code has installed.
16051
- *
16052
- * The file keys each plugin by `<plugin>@<marketplace>` and holds an *array* per key, because the
16053
- * same plugin can be installed at more than one scope and, at project scope, once per project.
16054
- */
16055
15947
  function readClaudeState(configDir) {
16056
15948
  const installedFile = readJson(join(configDir, "plugins", "installed_plugins.json"));
16057
15949
  const installed = [];
@@ -16090,16 +15982,10 @@ function readClaudeState(configDir) {
16090
15982
  const claudeCode = {
16091
15983
  name: "claude-code",
16092
15984
  scoped: true,
16093
- present: (home, env = process.env) => existsSync(claudeConfigDir(home, env)),
15985
+ installedOnMachine: (home, env = process.env) => existsSync(claudeConfigDir(home, env)),
16094
15986
  state: (home, env = process.env) => readClaudeState(claudeConfigDir(home, env)),
16095
15987
  register: (catalogDir) => `claude plugin marketplace add ./${catalogDir} --scope project`,
16096
15988
  install: (id) => `claude plugin install ${id} --scope project`,
16097
- /**
16098
- * Not `install`: re-running that on an already-installed plugin changes nothing and says so, which
16099
- * reads as success. `--scope project` is required rather than cosmetic — every verb defaults to
16100
- * user scope, so an update omitting it fails with "not installed at scope user" against a plugin
16101
- * that is plainly installed.
16102
- */
16103
15989
  update: (id) => `claude plugin update ${id} --scope project`,
16104
15990
  uninstall: (id) => `claude plugin uninstall ${id} --scope project`,
16105
15991
  refreshMarketplace: (marketplace) => `claude plugin marketplace update ${marketplace}`
@@ -16107,16 +15993,6 @@ const claudeCode = {
16107
15993
  function codexHome(home, env = process.env) {
16108
15994
  return env["CODEX_HOME"] || join(home, ".codex");
16109
15995
  }
16110
- /**
16111
- * Reads what Codex has installed.
16112
- *
16113
- * The installed version comes from the cache layout, `plugins/cache/<marketplace>/<plugin>/<version>`,
16114
- * because Codex's `config.toml` records only that a plugin is enabled — never which version.
16115
- *
16116
- * Registered marketplaces are read from `config.toml` by scanning for its `[marketplaces.<name>]`
16117
- * tables. A deliberately narrow read rather than a TOML parser: only two keys are needed, and the
16118
- * cost of being wrong is a report that suggests re-registering, which is harmless to re-run.
16119
- */
16120
15996
  function readCodexState(home) {
16121
15997
  const cacheRoot = join(home, "plugins", "cache");
16122
15998
  const installed = [];
@@ -16157,30 +16033,20 @@ function readCodexState(home) {
16157
16033
  }
16158
16034
  const runtimes = [claudeCode, {
16159
16035
  name: "codex",
16160
- /**
16161
- * Codex installs into `CODEX_HOME` with no notion of scope, so one install is shared by every
16162
- * repository on the machine. Two repositories whose catalogs share a marketplace name cannot be
16163
- * told apart here, which is what makes the marketplace name derived from the package name matter.
16164
- */
16165
16036
  scoped: false,
16166
- present: (home, env = process.env) => existsSync(codexHome(home, env)),
16037
+ installedOnMachine: (home, env = process.env) => existsSync(codexHome(home, env)),
16167
16038
  state: (home, env = process.env) => readCodexState(codexHome(home, env)),
16168
16039
  register: (catalogDir) => `codex plugin marketplace add ./${catalogDir}`,
16169
16040
  install: (id) => `codex plugin add ${id}`,
16170
- /** No update verb: re-running `add` re-reads the catalog and replaces the cached copy. */
16171
16041
  update: (id) => `codex plugin add ${id}`,
16172
16042
  uninstall: (id) => `codex plugin remove ${id}`,
16173
- /** `marketplace upgrade` refreshes git snapshots only; `add` already re-reads a local catalog. */
16174
16043
  refreshMarketplace: () => ""
16175
16044
  }];
16176
16045
  //#endregion
16177
16046
  //#region src/dep-plugins/reconcile.ts
16178
16047
  /**
16179
- * Installs belonging to this repository and this catalog.
16180
- *
16181
- * On a scoped runtime a user-scoped install is deliberately excluded rather than counted: it is
16182
- * shared with every other repository on the machine, so treating it as satisfying this repository's
16183
- * catalog would let one project's pin silently stand in for another's.
16048
+ * Excludes a user-scoped install on a scoped runtime — it's shared machine-wide, so it can't
16049
+ * satisfy this repository's catalog alone.
16184
16050
  */
16185
16051
  function ours(installed, marketplace, root, scoped) {
16186
16052
  const suffix = `@${marketplace}`;
@@ -16230,7 +16096,6 @@ function reconcile({ catalog, catalogDir, root, installed, marketplaces, scoped
16230
16096
  }
16231
16097
  //#endregion
16232
16098
  //#region src/dep-plugins/dep-plugins.command.ts
16233
- /** The command that performs one action, so a reader is not left to look the verb up per runtime. */
16234
16099
  function commandFor(runtime, action, marketplace) {
16235
16100
  switch (action.kind) {
16236
16101
  case "register": return runtime.register(CATALOG_DIR);
@@ -16243,11 +16108,6 @@ function commandFor(runtime, action, marketplace) {
16243
16108
  case "uninstall": return runtime.uninstall(action.subject);
16244
16109
  }
16245
16110
  }
16246
- /**
16247
- * Notes name the conditions a caller cannot see from the entry list and would otherwise discover as a
16248
- * failure later — a dependency that is declared but absent, a marketplace name two repositories are
16249
- * likely to share, a resolver under which no dependency has a directory at all.
16250
- */
16251
16111
  function notesFor(derived) {
16252
16112
  const notes = [];
16253
16113
  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.");
@@ -16281,7 +16141,7 @@ const depPluginsCommand = command({
16281
16141
  const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
16282
16142
  const home = homedir();
16283
16143
  const notes = notesFor(derived);
16284
- const detected = runtimes.filter((candidate) => candidate.present(home));
16144
+ const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
16285
16145
  const harness = [];
16286
16146
  const actions = [];
16287
16147
  for (const runtime of detected) {
@@ -16329,10 +16189,7 @@ const depPluginsCommand = command({
16329
16189
  });
16330
16190
  //#endregion
16331
16191
  //#region src/diagnose-bridges/directory-files.ts
16332
- /**
16333
- * Every file below `directory`, as paths relative to it, POSIX-separated and sorted. A directory
16334
- * that does not exist reads as empty: `doctor` never creates one to look at it.
16335
- */
16192
+ /** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
16336
16193
  function filesUnder(directory) {
16337
16194
  try {
16338
16195
  return readdirSync(directory, {
@@ -16345,36 +16202,18 @@ function filesUnder(directory) {
16345
16202
  }
16346
16203
  //#endregion
16347
16204
  //#region src/harness-registry/harness-registry.ts
16348
- /**
16349
- * The two scopes disagree in general, and a single answer per harness could only record one of them:
16350
- * Copilot CLI is `.github/skills` in a repository and `~/.copilot` for the user. Claude Code is the
16351
- * only harness that needs a skills projection, and it needs one at both scopes. Codex, Cursor,
16352
- * Copilot CLI, Gemini CLI, and Devin Desktop read `.agents/skills` themselves and are never
16353
- * projected into.
16354
- *
16355
- * Gemini CLI carried a `.gemini/skills` projection until E-GEM-02: it reads the `.agents/skills`
16356
- * alias at project scope too, where that alias takes precedence over `.gemini/skills`. It still
16357
- * needs an instruction bridge, which is a separate axis and unaffected.
16358
- *
16359
- * Instruction bridges are recorded at project scope only. The user-scope equivalents exist, but
16360
- * nothing writes or reads them yet: `init` works inside a repository, and so does `doctor`.
16361
- *
16362
- * `windsurf` is the former name of Devin Desktop, rebranded 2026-06-02. It is retained as a
16363
- * deprecated alias: Devin still scans the legacy `.windsurf/skills` path, so its projection keeps
16364
- * working, but new repositories should enable `devin-desktop` and have nothing written for them.
16365
- *
16366
- * See `.research/agentic-configuration-standards/` for the per-harness sources.
16367
- */
16205
+ /** See `.research/agentic-configuration-standards/` for the per-harness sources. */
16368
16206
  const harnessRegistry = [
16369
16207
  {
16370
16208
  name: "claude-code",
16371
16209
  project: {
16372
16210
  detect: ".claude",
16373
16211
  skillsDirectory: ".claude/skills",
16374
- instructionBridge: {
16375
- kind: "import",
16376
- path: "CLAUDE.md"
16377
- },
16212
+ shadowedBy: [
16213
+ "CLAUDE.md",
16214
+ ".claude/CLAUDE.md",
16215
+ "CLAUDE.local.md"
16216
+ ],
16378
16217
  mcpConfig: {
16379
16218
  path: ".mcp.json",
16380
16219
  key: "mcpServers",
@@ -16532,13 +16371,8 @@ function isHarnessName(value) {
16532
16371
  return harnessNames.has(value);
16533
16372
  }
16534
16373
  /**
16535
- * The `--harness` option, as names this registry knows. Both commands take the option and neither
16536
- * owns what a name means, so the parse lives beside the list it validates against: a harness added
16537
- * to the registry is accepted by both commands with nothing else to change, and the rejection reads
16538
- * the same either way.
16539
- *
16540
- * An unknown name throws rather than being dropped. A caller that asked for a harness by name and
16541
- * got a report about the two default ones has been told the wrong thing about their repository.
16374
+ * An unknown name throws rather than being dropped — silently falling back to the defaults would
16375
+ * misreport what was diagnosed.
16542
16376
  */
16543
16377
  function parseHarnesses(value) {
16544
16378
  const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
@@ -16571,9 +16405,8 @@ function locatorText({ file, server, field, position }) {
16571
16405
  //#endregion
16572
16406
  //#region src/diagnose-bridges/doctor-guidance.ts
16573
16407
  /**
16574
- * A table as the list `doctor` reports, in the order it was written. The keys of a
16575
- * `Record<P, RepairRow>` are `P` by construction, which is what the assertion says; nothing here
16576
- * claims a row exists.
16408
+ * Turns a table into the list `doctor` reports; the cast is safe because a `Record<P, RepairRow>`'s
16409
+ * keys are `P` by construction.
16577
16410
  */
16578
16411
  function repairsOf(table) {
16579
16412
  return Object.entries(table).map(([problem, row]) => ({
@@ -16581,19 +16414,11 @@ function repairsOf(table) {
16581
16414
  ...row
16582
16415
  }));
16583
16416
  }
16584
- /** How the command names itself. */
16585
16417
  const commandInvocation = "buddy-agent-harness";
16586
- /**
16587
- * How the skill hands a repair to `repair`. Bridge and instruction repairs go to `init`, which
16588
- * writes both kinds of bridge in the first place; configuration that is present and wrong goes
16589
- * here, because no `init` flag corrects a file the user already wrote.
16590
- */
16591
16418
  const repairSkillInvocation = "/buddy-agent-harness:repair";
16592
- /** How the skill hands a repair back to `init`. */
16593
- const initSkillInvocation = "/buddy-agent-harness:init";
16419
+ const initSkillInvocation = `/buddy-agent-harness:init-buddy-agent-harness`;
16594
16420
  /** Where the user authors the golden MCP server set, named in every MCP repair that points at it. */
16595
16421
  const goldenSet = ".agents/buddy-agent-harness/mcp.toml";
16596
- /** The skills bridges, reported in `bridges`. */
16597
16422
  const bridgeTable = {
16598
16423
  "no-canonical": {
16599
16424
  detail: "the canonical skill directory does not exist, so no bridge can resolve",
@@ -16670,19 +16495,17 @@ const bridgeTable = {
16670
16495
  };
16671
16496
  repairsOf(bridgeTable);
16672
16497
  /**
16673
- * The instruction bridges, reported in `instructions`. Every repair is the `init` skill: these are
16674
- * files a person wrote, or files carrying content beside the bridge, and deciding what to preserve
16675
- * while restoring the bridge is judgment no flag carries. `repair` therefore names the skill in
16676
- * both places rather than pretending a shell command exists.
16498
+ * Everything reported in `instructions`: bridges into `AGENTS.md` and the files that suppress it.
16499
+ * Every repair is the `init-buddy-agent-harness` skill's judgment, never a shell command.
16677
16500
  */
16678
16501
  const instructionTable = {
16679
16502
  "no-instructions": {
16680
- detail: "no AGENTS.md at the repository root, so every instruction bridge points at nothing",
16503
+ detail: "no AGENTS.md at the repository root, so the instructions this repository has reach one harness at most",
16681
16504
  repair: () => ({
16682
16505
  command: "",
16683
- instruction: `hand this to \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`
16506
+ instruction: `hand this to \`${initSkillInvocation}\`, which consolidates what the repository has into AGENTS.md, or derives it`
16684
16507
  }),
16685
- skillRepair: () => `run \`${initSkillInvocation}\`, which derives AGENTS.md and the bridges to it`
16508
+ skillRepair: () => `run \`${initSkillInvocation}\`, which consolidates or derives AGENTS.md`
16686
16509
  },
16687
16510
  "instructions-missing": {
16688
16511
  detail: "no instruction bridge at this path — the harness reads none of AGENTS.md",
@@ -16700,6 +16523,22 @@ const instructionTable = {
16700
16523
  }),
16701
16524
  skillRepair: () => `run \`${initSkillInvocation}\`, which adds the bridge without discarding what the file already says`
16702
16525
  },
16526
+ "instructions-shadowing": {
16527
+ detail: "this file suppresses the AGENTS.md beside it — the harness reads this instead, and none of AGENTS.md",
16528
+ repair: ({ file }) => ({
16529
+ command: "",
16530
+ instruction: `hand ${file} to \`${initSkillInvocation}\`, which consolidates what it says into AGENTS.md, or adds an @AGENTS.md import above it where the file has to stay`
16531
+ }),
16532
+ skillRepair: () => `run \`${initSkillInvocation}\`, which consolidates the file into AGENTS.md or imports AGENTS.md from it`
16533
+ },
16534
+ "instructions-superseded": {
16535
+ detail: "a bridge from before the harness read AGENTS.md itself — it still works, and nothing needs it",
16536
+ repair: ({ file }) => ({
16537
+ command: "",
16538
+ instruction: `remove ${file}, or keep it for sessions that cannot read AGENTS.md directly — \`${initSkillInvocation}\` offers the choice`
16539
+ }),
16540
+ skillRepair: () => `run \`${initSkillInvocation}\`, which offers to remove it`
16541
+ },
16703
16542
  "instructions-unreadable": {
16704
16543
  detail: "the settings file does not parse, so the harness reads none of it",
16705
16544
  repair: ({ file }) => ({
@@ -16710,10 +16549,6 @@ const instructionTable = {
16710
16549
  }
16711
16550
  };
16712
16551
  repairsOf(instructionTable);
16713
- /**
16714
- * Configuration that is present and wrong. Detected here like everything else, but repaired by the
16715
- * `repair` skill rather than by `init` — which is why it is a section of its own.
16716
- */
16717
16552
  const configurationTable = {
16718
16553
  "deprecated-harness": {
16719
16554
  detail: "a projection under a harness name that has been superseded — the replacement reads .agents/skills natively and needs no projection at all",
@@ -16750,13 +16585,8 @@ const configurationTable = {
16750
16585
  };
16751
16586
  repairsOf(configurationTable);
16752
16587
  /**
16753
- * The golden MCP server set against the harness copies of it.
16754
- *
16755
- * Every repair names a **locator** rather than a file — `.cursor/mcp.json#servers.linear.command` —
16756
- * because a file holding twenty servers is not an address. The locator never carries a value: not
16757
- * a whole one, and not a prefix. `doctor` is safe to run from a session-start hook, so a value it
16758
- * echoes lands in agent context on every session and from there into transcripts, and `sk-ab…`
16759
- * leaks into exactly the same place the whole string would.
16588
+ * Every repair names a locator rather than a file, and the locator never carries a credential
16589
+ * value.
16760
16590
  */
16761
16591
  const mcpTable = {
16762
16592
  "mcp-golden-unreadable": {
@@ -16842,13 +16672,8 @@ const mcpTable = {
16842
16672
  };
16843
16673
  repairsOf(mcpTable);
16844
16674
  /**
16845
- * Configuration only one harness can read. Every repair here is a **conversion**, and every one of
16846
- * them is the `init` skill's: consolidating what a repository already has is exactly its remit, and
16847
- * the content is the user's, so it is offered and never written on sight. `repair` owns none of
16848
- * them — it corrects configuration that is wrong, and none of this is wrong.
16849
- *
16850
- * `nonstandard-subagent` is the exception that names no owner. There is no cross-harness subagent
16851
- * format to convert to, so the honest report is that the gap exists, not a repair that cannot run.
16675
+ * Every repair here is a conversion owned by the `init-buddy-agent-harness` skill, except
16676
+ * `nonstandard-subagent`, which names no owner since no cross-harness format exists to convert to.
16852
16677
  */
16853
16678
  const nonstandardTable = {
16854
16679
  "nonstandard-instructions": {
@@ -16894,8 +16719,8 @@ const nonstandardTable = {
16894
16719
  };
16895
16720
  repairsOf(nonstandardTable);
16896
16721
  /**
16897
- * All five sections, in the order `doctor` reports them. Annotated over the whole union rather than
16898
- * inferred, so a problem added to any of the four fails here too if its section was left alone.
16722
+ * Annotated over the whole union rather than inferred, so an added problem fails to compile until
16723
+ * its section carries it.
16899
16724
  */
16900
16725
  const doctorTable = {
16901
16726
  ...bridgeTable,
@@ -16917,12 +16742,10 @@ function repairFor(problem) {
16917
16742
  const canonicalSkills = ".agents/skills";
16918
16743
  const localOverride = "AGENTS.local.md";
16919
16744
  /**
16920
- * The two frontmatter faults that make a harness skip a skill outright, per
16921
- * `skills/init/references/frontmatter.md`: YAML that does not parse, and a missing `description`.
16922
- * A `name` that mismatches the directory is a warning and still loads, so it is not reported here.
16923
- *
16924
- * The unquoted colon is the documented cause of the first, and is checked directly rather than by
16925
- * parsing YAML: the package ships no parser, and a targeted check names the actual fault.
16745
+ * The two frontmatter faults that make a harness skip a skill outright (see
16746
+ * `skills/init-buddy-agent-harness/references/frontmatter.md`): unparseable YAML, and a missing
16747
+ * `description`. A name/directory mismatch only warns and still loads, so it's not checked here.
16748
+ * Checked directly for an unquoted colon rather than by parsing YAML — the package ships no parser.
16926
16749
  */
16927
16750
  function frontmatterFault(body) {
16928
16751
  const block = /^---\n([\s\S]*?)\n---/.exec(body);
@@ -16932,10 +16755,8 @@ function frontmatterFault(body) {
16932
16755
  if (!/^(['"]).*\1$/.test(value) && /:\s/.test(value)) return "unparseable";
16933
16756
  }
16934
16757
  /**
16935
- * Unlike `diagnoseBridges`, this takes no harness preference. Every check here requires the
16936
- * projection to exist on disk, and a projection cannot exist without its harness's detection
16937
- * directory — which selects that harness anyway. A preference could therefore never add a finding,
16938
- * so accepting one would be surface that does nothing.
16758
+ * Takes no harness preference, unlike `diagnoseBridges`: every check here needs the projection to
16759
+ * already exist on disk, which already selects the harness — a preference could never add a finding.
16939
16760
  */
16940
16761
  function diagnoseConfiguration({ root, git, cli }) {
16941
16762
  const findings = [];
@@ -16963,13 +16784,6 @@ function diagnoseConfiguration({ root, git, cli }) {
16963
16784
  }
16964
16785
  //#endregion
16965
16786
  //#region src/diagnose-bridges/json-with-comments.ts
16966
- /**
16967
- * `.gemini/settings.json` may legally carry comments — the Gemini CLI loader strips them before
16968
- * parsing. A plain `JSON.parse` of a valid settings file therefore throws, and `doctor` would report
16969
- * a working bridge as unreadable. Comments are removed before parsing for that reason.
16970
- *
16971
- * Only comments. A trailing comma stays a parse error, because nothing documents it as accepted.
16972
- */
16973
16787
  function stripJsonComments(source) {
16974
16788
  let output = "";
16975
16789
  let index = 0;
@@ -17031,10 +16845,7 @@ const mcpFields = [
17031
16845
  ];
17032
16846
  /** The two fields holding one value per name rather than a single value. */
17033
16847
  const mapFields = /* @__PURE__ */ new Set(["env", "headers"]);
17034
- /**
17035
- * Whether two values of the same field agree. Arrays are ordered — `args` is a command line, and
17036
- * reordering it changes what runs. Maps are not, so they are compared by their sorted entries.
17037
- */
16848
+ /** Arrays compare ordered (`args` is a command line); maps compare by sorted entries. */
17038
16849
  function sameValue(field, left, right) {
17039
16850
  if (mapFields.has(field) && isRecord(left) && isRecord(right)) {
17040
16851
  const keys = Object.keys(left);
@@ -17042,18 +16853,6 @@ function sameValue(field, left, right) {
17042
16853
  }
17043
16854
  return JSON.stringify(left) === JSON.stringify(right);
17044
16855
  }
17045
- /**
17046
- * The fields on which a target disagrees with the golden set.
17047
- *
17048
- * Asymmetric by design, and this is the rule that keeps a golden set from accumulating noise. A
17049
- * field the golden set leaves unset is never a difference, however the target fills it: a host
17050
- * restating its own default and a user's deliberate edit are indistinguishable in that position,
17051
- * and pulling both back would grow the golden set on every round-trip. The golden set speaks only
17052
- * about what it says.
17053
- *
17054
- * `env` and `headers` are compared per name for the same reason — a variable only the target sets
17055
- * is that target's business, and one the golden set names must match.
17056
- */
17057
16856
  function divergingFields(golden, target) {
17058
16857
  return mcpFields.filter((field) => {
17059
16858
  const declared = golden[field];
@@ -17843,7 +17642,6 @@ function parse$1(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
17843
17642
  //#region src/diagnose-mcp/mcp-sources.ts
17844
17643
  /** Where the golden set lives, and why it is namespaced rather than sitting at `.agents/mcp.json`. */
17845
17644
  const goldenSetPath = ".agents/buddy-agent-harness/mcp.toml";
17846
- /** The table the golden set keeps its servers under. */
17847
17645
  const goldenKey = "servers";
17848
17646
  function stringMap(value) {
17849
17647
  if (!isRecord(value)) return void 0;
@@ -17856,10 +17654,8 @@ const transports = /* @__PURE__ */ new Set([
17856
17654
  "sse"
17857
17655
  ]);
17858
17656
  /**
17859
- * The transport a host entry describes. Hosts spell it `type` (Claude Code, VS Code) or
17860
- * `transport`, and several state it nowhere and leave it implied by which of `url` and `command`
17861
- * is present. Inferring it is what lets a `url` entry in one file compare equal to a `type: http`
17862
- * entry in another.
17657
+ * Infers transport when unstated (`url` → `http`, `command` → `stdio`), so a `url` entry in one
17658
+ * file compares equal to a `type: http` entry in another.
17863
17659
  */
17864
17660
  function transportOf(entry) {
17865
17661
  const declared = entry["type"] ?? entry["transport"];
@@ -17868,13 +17664,10 @@ function transportOf(entry) {
17868
17664
  if (typeof entry["command"] === "string") return "stdio";
17869
17665
  }
17870
17666
  /**
17871
- * One host entry, or one golden entry, in the canonical model. The two formats carry the same
17872
- * field names — the divergence between hosts is the file, the top-level key, and the serialization,
17873
- * not the entry — so one reader serves both directions of the comparison.
17874
- *
17875
- * A field whose value is the wrong type is dropped rather than carried through. A `timeout` that
17876
- * is a string is not a timeout, and letting it into the model would report a divergence whose real
17877
- * cause is a typo the finding does not name.
17667
+ * One host or golden entry, converted into the shared model; a field of the wrong type is dropped
17668
+ * rather than carried through, so a `timeout` that is a string does not report as a divergence.
17669
+ * Also used by `mcp-inventory.ts`, which normalizes its own raw shapes into these same fields
17670
+ * first.
17878
17671
  */
17879
17672
  function serverFrom(entry) {
17880
17673
  const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
@@ -17900,16 +17693,8 @@ function serversUnder(document, key) {
17900
17693
  return new Map(Object.entries(table).filter(([, entry]) => isRecord(entry)).map(([name, entry]) => [name, serverFrom(entry)]));
17901
17694
  }
17902
17695
  /**
17903
- * The position out of a thrown parse error, and nothing else out of it.
17904
- *
17905
- * `smol-toml` reports `line` and `column` on the error alongside a `codeblock` quoting the source,
17906
- * and its `message` embeds that same code block. Both are unusable: in a file of MCP configuration
17907
- * the line a parser failed on is exactly the line holding the credential. Only the two numbers are
17908
- * read, and anything thrown that does not carry them is reported without a position rather than by
17909
- * reaching for its message.
17910
- *
17911
- * Exported because that restriction is the point of the function rather than an implementation
17912
- * detail of it, and it is verified directly.
17696
+ * Only line and column — never `message` or `codeblock`, which quote the source line and could echo
17697
+ * a credential.
17913
17698
  */
17914
17699
  function positionOf(error) {
17915
17700
  if (!isRecord(error)) return void 0;
@@ -17935,12 +17720,8 @@ function parseGoldenSet(source) {
17935
17720
  }
17936
17721
  }
17937
17722
  /**
17938
- * One harness's MCP configuration, parsed. The file is read for its own key and nothing else:
17939
- * `.gemini/settings.json` also carries the instruction bridge, and a reader that treated the whole
17940
- * file as MCP would report the rest of a user's settings as servers it does not recognize.
17941
- *
17942
- * JSON is parsed with comments stripped, because Gemini CLI's loader strips them too and a
17943
- * settings file may legally carry them.
17723
+ * One harness's config, parsed under its own key only — `.gemini/settings.json` also carries the
17724
+ * instruction bridge. JSON is parsed with comments stripped, matching Gemini CLI's own loader.
17944
17725
  */
17945
17726
  function parseTarget(config, source) {
17946
17727
  if (source === void 0) return { kind: "absent" };
@@ -17961,49 +17742,26 @@ function parseTarget(config, source) {
17961
17742
  }
17962
17743
  //#endregion
17963
17744
  //#region src/diagnose-mcp/mcp-baseline.ts
17964
- /** Where the record of what was last written to each target lives, beside the golden set it came from. */
17965
- const projectionRecordPath = ".agents/buddy-agent-harness/mcp.projected.json";
17966
17745
  /**
17967
- * The last-projected record: per target path, per server name, the model that was written there.
17968
- *
17969
- * It exists because git can only answer for a file it can see. Harnesses write their MCP
17970
- * configuration themselves, at user scope, into files that are often untracked or ignored outright,
17971
- * and for those "which side moved since they last agreed" has no answer in history. A record
17972
- * written at projection time has one, exactly, because it *is* what was projected.
17973
- *
17974
- * A malformed record answers nothing rather than throwing. It is a cache of an answer, not the
17975
- * answer, and a diagnosis that dies because its cache is corrupt is worse than one that says
17976
- * `unknown`.
17746
+ * Where the record of what was last written to each target lives, beside the golden set it came
17747
+ * from.
17977
17748
  */
17749
+ const projectionRecordPath = ".agents/buddy-agent-harness/mcp.projected.json";
17978
17750
  function parseProjectionRecord(source) {
17979
17751
  const document = source === void 0 ? void 0 : parseJsonWithComments(source);
17980
17752
  const targets = isRecord(document) ? document["targets"] : void 0;
17981
17753
  if (!isRecord(targets)) return /* @__PURE__ */ new Map();
17982
17754
  return new Map(Object.entries(targets).filter(([, servers]) => isRecord(servers)).map(([path, servers]) => [path, new Map(Object.entries(servers).filter(([, server]) => isRecord(server)).map(([name, server]) => [name, server]))]));
17983
17755
  }
17984
- /**
17985
- * Which side moved, for one server and one field.
17986
- *
17987
- * The record is asked first, because it records exactly what was written and therefore answers
17988
- * exactly. Where it holds nothing, a **tracked** target falls back to git: the newest commit where
17989
- * the golden set and the target agreed on that field, then which working tree still matches it.
17990
- * Where neither can answer, the direction is `unknown` and the finding says so — naming a side on a
17991
- * guess would send a reconcile at the wrong file.
17992
- */
17993
17756
  var McpBaseline = class {
17994
17757
  options;
17995
17758
  constructor(options) {
17996
17759
  this.options = options;
17997
17760
  }
17998
17761
  /**
17999
- * One parse per commit per file, and one commit walk per target.
18000
- *
18001
- * `lastAgreed` runs per (config, server, field) that diverges without a projection record, and
18002
- * every one of those walks reads the same two files at the same commits. Unmemoized, three
18003
- * targets against five servers with two diverged fields each over fifty commits of history is
18004
- * three thousand `git show` calls and as many parses — on the command the `doctor` skill says is
18005
- * cheap enough for a session-start hook. One instance serves one diagnosis, so the memo lives and
18006
- * dies with it and can never answer for a working tree that has moved on.
17762
+ * Memoized per instance: unmemoized, `lastAgreed` would repeat the same `git show` and parse
17763
+ * for every diverged field. The memo dies with the instance, so it never answers for a working
17764
+ * tree that has since moved on.
18007
17765
  */
18008
17766
  parsed = /* @__PURE__ */ new Map();
18009
17767
  walked = /* @__PURE__ */ new Map();
@@ -18021,9 +17779,8 @@ var McpBaseline = class {
18021
17779
  return this.lastAgreed(config, name, field);
18022
17780
  }
18023
17781
  /**
18024
- * The newest committed state where both files described this field the same way. Read at model
18025
- * granularity rather than by comparing blobs: the two files never share bytes, and a commit that
18026
- * reformatted one of them did not change what it says.
17782
+ * Compared at model granularity, not blob bytes — the two files never share bytes, so a
17783
+ * reformat wouldn't register as agreement lost.
18027
17784
  */
18028
17785
  lastAgreed(config, name, field) {
18029
17786
  for (const commit of this.commitsTouching(config)) {
@@ -18054,28 +17811,18 @@ var McpBaseline = class {
18054
17811
  //#endregion
18055
17812
  //#region src/diagnose-mcp/mcp-secrets.ts
18056
17813
  /**
18057
- * Finding a literal credential in MCP configuration, without ever handling one.
18058
- *
18059
- * `doctor` is safe to run from a session-start hook, so its output lands in agent context on every
18060
- * session and from there into transcripts. A value it echoes is amplified far past the file it came
18061
- * from. Every function here answers **where**, never **what**: the return is a field path, and the
18062
- * value never leaves this module.
18063
- *
18064
- * That is also why the redaction lives here rather than in a formatter. `doctor` renders three ways
18065
- * — TOON, `--format text`, and `--format json` — and trusting each of them to mask is three chances
18066
- * to drift. A value that never enters the finding cannot be printed by any of them.
17814
+ * Finding a literal credential in MCP configuration, without ever handling one: every function
17815
+ * here answers where, never what, and the value itself never leaves this module — redaction lives
17816
+ * here, once, rather than being trusted to each of `doctor`'s three output formats.
18067
17817
  */
18068
17818
  /**
18069
- * Words that mark a field as credential-bearing wherever they appear in its name. Each is one that
18070
- * effectively never turns up inside an innocent word, so a bare substring test is safe and catches
18071
- * the run-together spellings a segment test would miss — `MYTOKEN`, `githubsecret`.
17819
+ * Marks a field as credential-bearing anywhere in its name — safe as a substring, since none of
17820
+ * these turn up inside an innocent word.
18072
17821
  */
18073
17822
  const credentialWord = /token|secret|password|passwd|credential/i;
18074
17823
  /**
18075
- * Words that mark a field only when they are a **segment** of its name. `key` and `auth` are the
18076
- * two that matter, and both are common inside words that carry nothing — `MONKEY`, `AUTHOR`,
18077
- * `KEYSTONE_URL`. Matching them as substrings reports those, and a scanner that cries wolf on a
18078
- * hook-run command is one people stop reading.
17824
+ * Marks a field only as a whole name segment: `key`/`auth` are common inside innocent words like
17825
+ * `MONKEY` or `AUTHOR`.
18079
17826
  */
18080
17827
  const credentialSegment = /* @__PURE__ */ new Set([
18081
17828
  "key",
@@ -18089,19 +17836,15 @@ function segmentsOf(key) {
18089
17836
  return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((segment) => segment.toLowerCase());
18090
17837
  }
18091
17838
  /**
18092
- * A value that **names** a secret instead of holding one, in the forms the supported hosts
18093
- * document: `${VAR}`, `$VAR`, and the `${env:VAR}` / `${input:id}` variants.
18094
- *
18095
- * Classification is by shape, not by content. An entropy test on the value would be a guess about
18096
- * a string this module has already decided not to look at, and it would fail in both directions —
18097
- * a short password is low-entropy and a base64 configuration blob is high.
17839
+ * A value that names a secret rather than holding one: `${VAR}`, `$VAR`, `${env:VAR}`,
17840
+ * `${input:id}`. Classification is by shape, not entropy — a short password and a base64 blob look
17841
+ * identical to an entropy test.
18098
17842
  */
18099
17843
  const reference = /\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/;
18100
17844
  /**
18101
- * A credential-bearing field passes when it carries a reference anywhere in it, because the common
18102
- * legitimate form is a template rather than a bare variable — `Bearer ${LINEAR_TOKEN}` is the
18103
- * documented way to write an authorization header, and requiring the whole value to be a reference
18104
- * would report every correct one.
17845
+ * True unless a reference appears anywhere in the value — `Bearer ${LINEAR_TOKEN}` is the
17846
+ * documented template form, so requiring the whole value to be a reference would flag it as
17847
+ * literal.
18105
17848
  */
18106
17849
  function holdsLiteral(value) {
18107
17850
  return !reference.test(value);
@@ -18110,11 +17853,8 @@ function credentialKey(key) {
18110
17853
  return credentialWord.test(key) || segmentsOf(key).some((segment) => credentialSegment.has(segment));
18111
17854
  }
18112
17855
  /**
18113
- * Whether a URL carries a credential in itself. Two shapes: a password in the userinfo component,
18114
- * and a credential-named query parameter holding a literal. Neither value is read past the test.
18115
- *
18116
- * A URL that does not parse is not reported. It is malformed rather than leaky, and guessing at its
18117
- * structure with a regular expression is how a scanner starts matching the thing it must not touch.
17856
+ * A password in the URL's userinfo, or a credential-named query parameter holding a literal. An
17857
+ * unparseable URL is not reported — malformed, not leaky.
18118
17858
  */
18119
17859
  function urlHoldsCredential(value) {
18120
17860
  let url;
@@ -18130,12 +17870,7 @@ function urlHoldsCredential(value) {
18130
17870
  function literalsIn(field, values) {
18131
17871
  return Object.entries(values ?? {}).filter(([key, value]) => credentialKey(key) && holdsLiteral(value)).map(([key]) => `${field}.${key}`);
18132
17872
  }
18133
- /**
18134
- * Every field of one server holding a literal credential, as a path within that server.
18135
- *
18136
- * The path is the whole answer. It is never a truncated value: `sk-ab…` is a leak into the same
18137
- * transcript and buys a reader nothing the field name does not already give them.
18138
- */
17873
+ /** Field paths only — never a truncated value; `sk-ab…` is still a leak into the same transcript. */
18139
17874
  function credentialFields(server) {
18140
17875
  return [
18141
17876
  ...literalsIn("env", server.env),
@@ -18150,12 +17885,8 @@ function read(root, path) {
18150
17885
  return existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
18151
17886
  }
18152
17887
  /**
18153
- * The distinct MCP files the enabled harnesses read. Two harnesses may name the same file.
18154
- *
18155
- * No harness preference is accepted, for the same reason the configuration half accepts none: every
18156
- * harness documenting an MCP file is already selected without one. Claude Code and Cursor are
18157
- * selected unconditionally; Codex and Gemini CLI keep their file inside their own detection
18158
- * directory, so it cannot exist without that directory selecting them. A preference could never add
17888
+ * The distinct MCP files the enabled harnesses read. No `--harness` preference is accepted: every
17889
+ * harness documenting an MCP file is already selected without one, so a preference could never add
18159
17890
  * a finding.
18160
17891
  */
18161
17892
  function targetsOf(root) {
@@ -18179,12 +17910,6 @@ function diagnoseMcp({ root, git, cli }) {
18179
17910
  repair: repair(at, cli)
18180
17911
  });
18181
17912
  };
18182
- /**
18183
- * Credentials are reported for every file that holds servers, the golden set included — a user
18184
- * pastes a token into whichever file is open, and the golden set is a file. Severity splits on
18185
- * tracking, because a literal in a tracked file is already a committed credential and moving it
18186
- * to an environment variable does not un-commit it.
18187
- */
18188
17913
  const reportSecrets = (path, servers) => {
18189
17914
  const committed = git.trackingOf(path) !== "untracked";
18190
17915
  for (const [name, server] of servers) for (const field of credentialFields(server)) add({
@@ -18254,11 +17979,9 @@ const problemOf = {
18254
17979
  subagent: "nonstandard-subagent"
18255
17980
  };
18256
17981
  /**
18257
- * A `.mdc` rule splits on whether its scoping is load-bearing. `globs:` with a value binds the rule
18258
- * to paths, and `AGENTS.md` has no equivalent — it scopes by directory nesting only — so that one
18259
- * converts to a skill, or stays. Without it the rule is always-on prose, which `AGENTS.md` holds
18260
- * verbatim. Read from frontmatter rather than assumed, because the two need different conversions
18261
- * and guessing wrong sends prose to the wrong destination.
17982
+ * `globs:` with a value binds a `.mdc` rule to paths — `AGENTS.md` has no equivalent, so that rule
17983
+ * converts to a skill; without it, the rule is always-on prose that `AGENTS.md` holds verbatim.
17984
+ * Read from frontmatter rather than assumed, since guessing wrong sends prose to the wrong destination.
18262
17985
  */
18263
17986
  function scopedByGlobs(body) {
18264
17987
  const block = /^---\n([\s\S]*?)\n---/.exec(body);
@@ -18271,8 +17994,8 @@ function skillFiles(directory) {
18271
17994
  return filesUnder(directory).filter((file) => file.endsWith("SKILL.md"));
18272
17995
  }
18273
17996
  /**
18274
- * A harness directory that is a symlink is a projection someone already made, not configuration
18275
- * authored here — reporting it would tell a repository to convert what it already converted.
17997
+ * A symlinked path is already a projection, not authored config — reporting it would tell a repo to
17998
+ * convert what it already converted.
18276
17999
  */
18277
18000
  function authoredHere(path) {
18278
18001
  try {
@@ -18288,10 +18011,8 @@ function artifactFiles(root, artifact) {
18288
18011
  return (artifact.kind === "skill" ? skillFiles(absolute) : filesUnder(absolute)).map((file) => posix.join(artifact.path, file));
18289
18012
  }
18290
18013
  /**
18291
- * Unlike the other detectors this takes no harness preference and no enabled set. An artifact is
18292
- * reachable by one harness whether or not that harness is enabled here — a `.cursorrules` in a
18293
- * repository nobody opens in Cursor is still instruction content `AGENTS.md` does not carry — so
18294
- * filtering by the enabled set would hide exactly the drift worth converting.
18014
+ * Takes no harness preference or enabled set, unlike the other detectors — an artifact matters
18015
+ * whether or not its harness is enabled here, so filtering by the enabled set would hide it.
18295
18016
  */
18296
18017
  function diagnoseNonstandard({ root, cli }) {
18297
18018
  const findings = [];
@@ -18309,6 +18030,129 @@ function diagnoseNonstandard({ root, cli }) {
18309
18030
  return findings.sort((left, right) => left.path.localeCompare(right.path));
18310
18031
  }
18311
18032
  //#endregion
18033
+ //#region src/governance-overrides/governance-overrides.ts
18034
+ function managedGovernancesDir(platform, programData) {
18035
+ if (platform === "darwin") return "/Library/Application Support/BuddyAgentHarness/governances";
18036
+ if (platform === "win32") return join(programData || "C:\\ProgramData", "BuddyAgentHarness", "governances");
18037
+ return "/etc/buddy-agent-harness/governances";
18038
+ }
18039
+ /**
18040
+ * Still read below the layer above, so a machine already using it keeps working; reported as
18041
+ * deprecated so an admin knows to move it.
18042
+ */
18043
+ function deprecatedManagedGovernancesDir(platform, programData) {
18044
+ if (platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
18045
+ if (platform === "win32") return join(programData || "C:\\ProgramData", "UniPlugin", "governances");
18046
+ return "/etc/universal-plugin/governances";
18047
+ }
18048
+ function projectGovernancesDir(root) {
18049
+ return join(root, ".agents", "governances");
18050
+ }
18051
+ function userGovernancesDir(home) {
18052
+ return join(home, ".agents", "governances");
18053
+ }
18054
+ /**
18055
+ * Walks up to the nearest `package.json` rather than a relative literal — this module sits at a
18056
+ * different depth in `src/` vs the bundle.
18057
+ */
18058
+ function packageGovernancesDir(from = fileURLToPath(import.meta.url)) {
18059
+ let dir = dirname(from);
18060
+ for (;;) {
18061
+ if (existsSync(join(dir, "package.json"))) return join(dir, "governances");
18062
+ const parent = dirname(dir);
18063
+ if (parent === dir) return join(dir, "governances");
18064
+ dir = parent;
18065
+ }
18066
+ }
18067
+ function governanceLayers({ root, home, platform, programData }) {
18068
+ return [
18069
+ {
18070
+ scope: "project",
18071
+ dir: projectGovernancesDir(root)
18072
+ },
18073
+ {
18074
+ scope: "user",
18075
+ dir: userGovernancesDir(home)
18076
+ },
18077
+ {
18078
+ scope: "managed",
18079
+ dir: managedGovernancesDir(platform, programData)
18080
+ },
18081
+ {
18082
+ scope: "managed-deprecated",
18083
+ dir: deprecatedManagedGovernancesDir(platform, programData)
18084
+ },
18085
+ {
18086
+ scope: "package",
18087
+ dir: packageGovernancesDir()
18088
+ }
18089
+ ];
18090
+ }
18091
+ /**
18092
+ * Filters by scope rather than slicing the list, so a layer added later can't quietly become an
18093
+ * override by position.
18094
+ */
18095
+ function overrideLayers(layers) {
18096
+ return layers.filter((layer) => layer.scope !== "package");
18097
+ }
18098
+ const namePattern = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/i;
18099
+ /**
18100
+ * Rejects rather than sanitizes — silently answering a different question is how untrusted input
18101
+ * reads a file outside the layer.
18102
+ */
18103
+ function parseGovernanceName(value) {
18104
+ if (value.endsWith(".md")) throw new Error(`"${value}" names a file. Ask for the governance by name, without the .md extension.`);
18105
+ if (!namePattern.test(value)) throw new Error(`"${value}" is not a governance name. A name is letters, digits, hyphens, and dots — not a path.`);
18106
+ return value;
18107
+ }
18108
+ function tryReadDocument(path) {
18109
+ try {
18110
+ return readFileSync(path, "utf8");
18111
+ } catch {
18112
+ return;
18113
+ }
18114
+ }
18115
+ /** Empty for a layer that's missing, unreadable, or not a directory — never throws. */
18116
+ function documentNames(dir) {
18117
+ try {
18118
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name.slice(0, -3));
18119
+ } catch {
18120
+ return [];
18121
+ }
18122
+ }
18123
+ /** The first layer that holds the name wins; the rest are not read. */
18124
+ function resolveGovernance(name, layers) {
18125
+ for (const { scope, dir } of layers) {
18126
+ const path = join(dir, `${name}.md`);
18127
+ const content = tryReadDocument(path);
18128
+ if (content !== void 0) return {
18129
+ name,
18130
+ scope,
18131
+ path,
18132
+ content
18133
+ };
18134
+ }
18135
+ }
18136
+ /** Every name any layer holds, each reported at the layer that would win, sorted by name. */
18137
+ function listGovernances(layers) {
18138
+ const found = /* @__PURE__ */ new Map();
18139
+ for (const { scope, dir } of layers) for (const name of documentNames(dir)) if (!found.has(name)) found.set(name, {
18140
+ name,
18141
+ scope,
18142
+ path: join(dir, `${name}.md`)
18143
+ });
18144
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
18145
+ }
18146
+ /**
18147
+ * Creates the directory if absent — a fresh repository gets one obvious place to put an override,
18148
+ * and a count of zero.
18149
+ */
18150
+ function countProjectGovernances(root) {
18151
+ const dir = projectGovernancesDir(root);
18152
+ mkdirSync(dir, { recursive: true });
18153
+ return documentNames(dir).length;
18154
+ }
18155
+ //#endregion
18312
18156
  //#region src/skill-projection/skill-projection.ts
18313
18157
  function skillNames(skillsDirectory) {
18314
18158
  return readdirSync(skillsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -18322,10 +18166,8 @@ function pathOccupied(path) {
18322
18166
  }
18323
18167
  }
18324
18168
  /**
18325
- * Whether `target` is a symbolic link that resolves to `canonical`. Resolved paths are compared
18326
- * rather than link text, so a link a user wrote as an absolute path, one this tool wrote as a
18327
- * Windows junction, and one reached through a symbolic link in a parent directory all read as the
18328
- * correct bridge. A link that no longer resolves is not.
18169
+ * Compares resolved paths, not link text, so an absolute link, a Windows junction, or a link
18170
+ * reached through a parent symlink all read as correct.
18329
18171
  */
18330
18172
  function linksTo(target, canonical) {
18331
18173
  try {
@@ -18341,18 +18183,15 @@ function countSkills(canonicalSkills) {
18341
18183
  return skillNames(canonicalSkills).length;
18342
18184
  }
18343
18185
  /**
18344
- * A junction is the only link Windows grants an unprivileged process, and Node resolves a relative
18345
- * junction target against the process directory rather than the link's own, so Windows is given the
18346
- * absolute path. Every other platform keeps the link relative, so it survives the repository moving.
18186
+ * Windows junctions resolve a relative target against the process directory, not the link's own, so
18187
+ * Windows gets an absolute path; everywhere else stays relative to survive the repository moving.
18347
18188
  */
18348
18189
  function linkTarget(target, canonicalSkills) {
18349
18190
  return platform() === "win32" ? resolve(canonicalSkills) : relative(dirname(target), canonicalSkills);
18350
18191
  }
18351
18192
  /**
18352
- * Translates what the option parser hands back into a selection. A valueless `--force` arrives as
18353
- * the literal string `true` — that is how the parser encodes a flag given no value — and means
18354
- * every conflicting target, which is what keeps an existing `init --force` doing what it always did.
18355
- * Anything else is a comma-separated list of targets.
18193
+ * A valueless `--force` arrives as the literal string `"true"` — the parser's flag encoding — and
18194
+ * means every target; anything else is a comma-separated list.
18356
18195
  */
18357
18196
  function parseForce(value) {
18358
18197
  if (value === "true") return true;
@@ -18360,17 +18199,13 @@ function parseForce(value) {
18360
18199
  return value.split(",").map((target) => target.trim()).filter((target) => target.length > 0);
18361
18200
  }
18362
18201
  /**
18363
- * Targets are matched against what the caller typed, resolved against the repository root, so the
18364
- * repo-relative form the conflict message prints is the form that selects it back — and an absolute
18365
- * path naming the same directory selects it too.
18202
+ * Resolves both sides so the repo-relative path a conflict message prints also selects it back, and
18203
+ * so does an absolute path naming the same directory.
18366
18204
  */
18367
18205
  function namesTarget(root, target, given) {
18368
18206
  return resolve(root, given) === resolve(target);
18369
18207
  }
18370
- /**
18371
- * Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is
18372
- * preferred; a platform that refuses the link falls back to a copy.
18373
- */
18208
+ /** Symlinks by default; falls back to a copy where the platform refuses the link. */
18374
18209
  function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
18375
18210
  const projections = harnesses.filter((harness) => harness.project.skillsDirectory).map((harness) => ({
18376
18211
  harness,
@@ -18412,12 +18247,8 @@ function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
18412
18247
  /** Directories that never hold instructions this repository is responsible for bridging. */
18413
18248
  const pruned = /* @__PURE__ */ new Set(["node_modules"]);
18414
18249
  /**
18415
- * Every directory below `root` holding an `AGENTS.md`, repository-relative and POSIX-separated,
18416
- * with the root itself as `''`. Sorted, root first.
18417
- *
18418
- * Dot-directories are pruned along with `node_modules`. That is not only a speed measure:
18419
- * `.agents/AGENTS.md` is canonical shared instructions rather than a subtree-scoped file, and
18420
- * bridging it would claim a scope it does not have.
18250
+ * Dot-directories are pruned along with `node_modules`: `.agents/AGENTS.md` is canonical shared
18251
+ * instructions, not a subtree-scoped file.
18421
18252
  */
18422
18253
  function agentsFileDirectories(root) {
18423
18254
  const found = [];
@@ -18440,40 +18271,6 @@ function agentsFileDirectories(root) {
18440
18271
  //#endregion
18441
18272
  //#region src/diagnose-bridges/diagnose-instructions.ts
18442
18273
  const canonicalInstructions = "AGENTS.md";
18443
- /**
18444
- * An import bridges the `AGENTS.md` beside it and nothing else, so the check is per directory
18445
- * holding one rather than per harness. A line of its own is enough: the harness reference allows
18446
- * Claude-specific notes below the import, and a file that carries the import plus notes still
18447
- * bridges.
18448
- */
18449
- function inspectImport(target) {
18450
- let stats;
18451
- try {
18452
- stats = lstatSync(target);
18453
- } catch {
18454
- return {
18455
- kind: "none",
18456
- status: "missing",
18457
- problem: "instructions-missing"
18458
- };
18459
- }
18460
- if (stats.isSymbolicLink()) return readlinkSync(target) === "AGENTS.md" ? {
18461
- kind: "symlink",
18462
- status: "ok"
18463
- } : {
18464
- kind: "symlink",
18465
- status: "unbridged",
18466
- problem: "instructions-unbridged"
18467
- };
18468
- return readFileSync(target, "utf8").split("\n").some((line) => line.trim() === `@AGENTS.md`) ? {
18469
- kind: "import",
18470
- status: "ok"
18471
- } : {
18472
- kind: "file",
18473
- status: "unbridged",
18474
- problem: "instructions-unbridged"
18475
- };
18476
- }
18477
18274
  /** Reads a dotted key out of parsed JSON without asserting anything about the rest of the file. */
18478
18275
  function valueAt(settings, key) {
18479
18276
  return key.split(".").reduce((value, segment) => {
@@ -18481,6 +18278,14 @@ function valueAt(settings, key) {
18481
18278
  return value[segment];
18482
18279
  }, settings);
18483
18280
  }
18281
+ function exists(target) {
18282
+ try {
18283
+ lstatSync(target);
18284
+ return true;
18285
+ } catch {
18286
+ return false;
18287
+ }
18288
+ }
18484
18289
  function inspectSettingsEntry(target, key) {
18485
18290
  let source;
18486
18291
  try {
@@ -18508,21 +18313,45 @@ function inspectSettingsEntry(target, key) {
18508
18313
  problem: "instructions-unbridged"
18509
18314
  };
18510
18315
  }
18511
- /** Every path an instruction bridge of this kind belongs at, in report order. */
18512
- function pathsFor(root, bridge) {
18513
- if (bridge.kind !== "import") return [bridge.path];
18514
- return agentsFileDirectories(root).map((directory) => directory ? posix.join(directory, bridge.path) : bridge.path);
18515
- }
18516
- function inspect$1(target, bridge) {
18517
- return bridge.kind === "import" ? inspectImport(target) : inspectSettingsEntry(target, bridge.key);
18316
+ function inspectShadow(target) {
18317
+ let stats;
18318
+ try {
18319
+ stats = lstatSync(target);
18320
+ } catch {
18321
+ return;
18322
+ }
18323
+ if (stats.isSymbolicLink()) return readlinkSync(target) === "AGENTS.md" ? {
18324
+ kind: "symlink",
18325
+ status: "superseded",
18326
+ problem: "instructions-superseded"
18327
+ } : {
18328
+ kind: "symlink",
18329
+ status: "shadowing",
18330
+ problem: "instructions-shadowing"
18331
+ };
18332
+ let body;
18333
+ try {
18334
+ body = readFileSync(target, "utf8");
18335
+ } catch {
18336
+ return {
18337
+ kind: "file",
18338
+ status: "unreadable",
18339
+ problem: "instructions-unreadable"
18340
+ };
18341
+ }
18342
+ return body.split("\n").some((line) => line.trim() === `@AGENTS.md`) ? {
18343
+ kind: "import",
18344
+ status: "superseded",
18345
+ problem: "instructions-superseded"
18346
+ } : {
18347
+ kind: "file",
18348
+ status: "shadowing",
18349
+ problem: "instructions-shadowing"
18350
+ };
18518
18351
  }
18519
18352
  /**
18520
- * Reports whether every enabled harness can still read `AGENTS.md`.
18521
- *
18522
- * Losing one of these is quieter than losing a skills bridge and costs more: a harness with no
18523
- * instruction bridge reads none of the repository's instructions, and says nothing about it. The
18524
- * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
18525
- * skill's judgment.
18353
+ * Checks two things: whether each instruction bridge resolves, and whether anything suppresses a
18354
+ * harness that reads `AGENTS.md` natively.
18526
18355
  */
18527
18356
  function diagnoseInstructions(root, harnesses, cli) {
18528
18357
  const bridged = harnesses.map((harness) => ({
@@ -18531,11 +18360,9 @@ function diagnoseInstructions(root, harnesses, cli) {
18531
18360
  })).filter((harness) => Boolean(harness.bridge));
18532
18361
  const instructions = [];
18533
18362
  const findings = [];
18534
- if (!bridged.length) return {
18535
- instructions,
18536
- findings
18537
- };
18538
- if (!agentsFileDirectories(root).includes("")) {
18363
+ const directories = agentsFileDirectories(root);
18364
+ const shadowsAtRoot = harnesses.some((harness) => (harness.project.shadowedBy ?? []).some((shadow) => exists(join(root, shadow))));
18365
+ if ((bridged.length || shadowsAtRoot) && !directories.includes("")) {
18539
18366
  const { detail, repair } = repairFor("no-instructions");
18540
18367
  findings.push({
18541
18368
  path: canonicalInstructions,
@@ -18544,24 +18371,42 @@ function diagnoseInstructions(root, harnesses, cli) {
18544
18371
  repair: repair({ file: canonicalInstructions }, cli)
18545
18372
  });
18546
18373
  }
18547
- for (const { name, bridge } of bridged) for (const path of pathsFor(root, bridge)) {
18548
- const inspection = inspect$1(join(root, path), bridge);
18374
+ for (const { name, bridge } of bridged) {
18375
+ const inspection = inspectSettingsEntry(join(root, bridge.path), bridge.key);
18549
18376
  instructions.push({
18550
18377
  harness: name,
18551
- path,
18378
+ path: bridge.path,
18552
18379
  kind: inspection.kind,
18553
18380
  status: inspection.status
18554
18381
  });
18555
18382
  if (inspection.problem) {
18556
18383
  const { detail, repair } = repairFor(inspection.problem);
18557
18384
  findings.push({
18558
- path,
18385
+ path: bridge.path,
18559
18386
  problem: inspection.problem,
18560
18387
  detail,
18561
- repair: repair({ file: path }, cli)
18388
+ repair: repair({ file: bridge.path }, cli)
18562
18389
  });
18563
18390
  }
18564
18391
  }
18392
+ for (const harness of harnesses) for (const shadow of harness.project.shadowedBy ?? []) for (const directory of directories) {
18393
+ const path = directory ? posix.join(directory, shadow) : shadow;
18394
+ const inspection = inspectShadow(join(root, path));
18395
+ if (!inspection) continue;
18396
+ instructions.push({
18397
+ harness: harness.name,
18398
+ path,
18399
+ kind: inspection.kind,
18400
+ status: inspection.status
18401
+ });
18402
+ const { detail, repair } = repairFor(inspection.problem);
18403
+ findings.push({
18404
+ path,
18405
+ problem: inspection.problem,
18406
+ detail,
18407
+ repair: repair({ file: path }, cli)
18408
+ });
18409
+ }
18565
18410
  return {
18566
18411
  instructions,
18567
18412
  findings
@@ -18589,25 +18434,20 @@ function lines(output) {
18589
18434
  return (output ?? "").split("\n").filter(Boolean);
18590
18435
  }
18591
18436
  /**
18592
- * Reads git state for the two sides of a bridge. Every method degrades to "cannot tell" rather than
18593
- * throwing, so `doctor` still reports on a tarball, a worktree without git, or a repository with no
18594
- * commits.
18437
+ * Every method degrades to "cannot tell" rather than throwing, so `doctor` still reports on a
18438
+ * tarball or a repository with no commits.
18595
18439
  */
18596
18440
  var GitBridgeState = class {
18597
18441
  root;
18598
- /** Path of `root` relative to the repository root, `''` at the top, `undefined` outside a repository. */
18599
- prefix;
18442
+ /** `''` at the top, `undefined` outside a repository. */
18443
+ repoRootPrefix;
18600
18444
  constructor(root) {
18601
18445
  this.root = root;
18602
- this.prefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
18446
+ this.repoRootPrefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
18603
18447
  }
18604
- /**
18605
- * `init --copy` marks a bridge skip-worktree so a machine-local copy over a tracked symlink stops
18606
- * showing as a pending change. The bit is a hint that some checkouts clear, so it is verified
18607
- * rather than assumed.
18608
- */
18448
+ /** The skip-worktree bit is a hint some checkouts clear, so it is verified rather than assumed. */
18609
18449
  trackingOf(path) {
18610
- if (this.prefix === void 0) return "untracked";
18450
+ if (this.repoRootPrefix === void 0) return "untracked";
18611
18451
  const entries = lines(git(this.root, [
18612
18452
  "ls-files",
18613
18453
  "-v",
@@ -18618,13 +18458,11 @@ var GitBridgeState = class {
18618
18458
  return entries.every((entry) => entry.startsWith("S")) ? "skip-worktree" : "tracked";
18619
18459
  }
18620
18460
  /**
18621
- * Whether a `.gitignore` rule matches this path. Asked of git rather than matched against the
18622
- * file, so a rule on a parent directory — the common way a bridge gets ignored — is caught.
18623
- * Outside a repository nothing is ignored, which is the same "cannot tell" degradation the rest
18624
- * of this class uses.
18461
+ * Asked of git rather than matched against the file, so a rule on a parent directory is caught
18462
+ * too.
18625
18463
  */
18626
18464
  isIgnored(path) {
18627
- if (this.prefix === void 0) return false;
18465
+ if (this.repoRootPrefix === void 0) return false;
18628
18466
  return git(this.root, [
18629
18467
  "check-ignore",
18630
18468
  "-q",
@@ -18633,15 +18471,11 @@ var GitBridgeState = class {
18633
18471
  ]) !== void 0;
18634
18472
  }
18635
18473
  /**
18636
- * The newest commits that touched any of these paths, newest first. Empty outside a repository,
18637
- * which is the same "cannot tell" degradation as everything else here.
18638
- *
18639
- * Bounded like `lastAgreedTree` is: a baseline that is not in the recent history of the two files
18640
- * is not a baseline anyone can act on, and walking a whole repository to prove its absence costs
18641
- * more than the answer is worth.
18474
+ * Bounded to the last 200 commits: a baseline outside that range costs more to find than it's
18475
+ * worth acting on.
18642
18476
  */
18643
18477
  commitsTouching(paths) {
18644
- if (this.prefix === void 0) return [];
18478
+ if (this.repoRootPrefix === void 0) return [];
18645
18479
  return lines(git(this.root, [
18646
18480
  "rev-list",
18647
18481
  "-n",
@@ -18653,15 +18487,11 @@ var GitBridgeState = class {
18653
18487
  }
18654
18488
  /** One file's content at one commit, or `undefined` when it did not exist there. */
18655
18489
  contentAt(commit, path) {
18656
- if (this.prefix === void 0) return void 0;
18657
- return git(this.root, ["show", `${commit}:${this.prefix}${path}`]);
18490
+ if (this.repoRootPrefix === void 0) return void 0;
18491
+ return git(this.root, ["show", `${commit}:${this.repoRootPrefix}${path}`]);
18658
18492
  }
18659
- /**
18660
- * Names the side that moved by finding the newest commit whose two trees agreed, then asking which
18661
- * of the working directories still matches it.
18662
- */
18663
18493
  directionOf(bridge, canonical) {
18664
- if (this.prefix === void 0) return "unknown";
18494
+ if (this.repoRootPrefix === void 0) return "unknown";
18665
18495
  const base = this.lastAgreedTree(bridge, canonical);
18666
18496
  if (base === void 0) return "unknown";
18667
18497
  const bridgeMoved = !this.matchesTree(bridge, base);
@@ -18685,20 +18515,20 @@ var GitBridgeState = class {
18685
18515
  "rev-parse",
18686
18516
  "-q",
18687
18517
  "--verify",
18688
- `${commit}:${this.prefix}${bridge}`
18518
+ `${commit}:${this.repoRootPrefix}${bridge}`
18689
18519
  ])?.trim();
18690
18520
  if (!bridgeObject) continue;
18691
18521
  if (bridgeObject === git(this.root, [
18692
18522
  "rev-parse",
18693
18523
  "-q",
18694
18524
  "--verify",
18695
- `${commit}:${this.prefix}${canonical}`
18525
+ `${commit}:${this.repoRootPrefix}${canonical}`
18696
18526
  ])?.trim()) return bridgeObject;
18697
18527
  }
18698
18528
  }
18699
18529
  /**
18700
- * Compares a working directory against a git tree by object id. `hash-object` is used rather than
18701
- * hashing in-process so the comparison follows whatever object format the repository uses.
18530
+ * Compares by object id via `hash-object` rather than hashing in-process, so it follows
18531
+ * whatever object format the repository uses.
18702
18532
  */
18703
18533
  matchesTree(directory, tree) {
18704
18534
  const expected = new Map(lines(git(this.root, [
@@ -18780,9 +18610,8 @@ function inspect(target, path, canonical, git) {
18780
18610
  }
18781
18611
  }
18782
18612
  /**
18783
- * Reports whether every bridge this repository needs still resolves: the skills bridges into
18784
- * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
18785
- * moved, or repaired, so the caller decides what to run from the repair each finding carries.
18613
+ * Read-only: nothing is created, moved, or repaired — the caller decides what to run from the
18614
+ * repair each finding carries.
18786
18615
  */
18787
18616
  function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
18788
18617
  const canonical = join(root, ".agents", "skills");
@@ -18839,11 +18668,12 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
18839
18668
  //#endregion
18840
18669
  //#region src/diagnose-bridges/doctor.command.ts
18841
18670
  /**
18842
- * AXI §5: the healthy answer states the zero with context, so an agent does not re-run with other
18843
- * flags to confirm that an empty section really meant "nothing wrong".
18671
+ * AXI §5: the healthy answer states the zero with context, so an agent doesn't re-run to confirm
18672
+ * nothing was wrong.
18844
18673
  */
18845
- function buildDoctorReport(bin, result, configuration = []) {
18674
+ function buildDoctorReport(bin, result, configuration = [], overrides = []) {
18846
18675
  const findings = [...result.findings, ...configuration];
18676
+ const governances = overrides.length ? overrides : "0 governance overrides — no .agents/governances at project, user, or machine scope";
18847
18677
  if (!findings.length) {
18848
18678
  const count = result.bridges.length + result.instructions.length;
18849
18679
  const bridges = count === 1 ? "the 1 bridge resolves" : `all ${count} bridges resolve`;
@@ -18851,6 +18681,7 @@ function buildDoctorReport(bin, result, configuration = []) {
18851
18681
  bin,
18852
18682
  bridges: result.bridges,
18853
18683
  instructions: result.instructions,
18684
+ governances,
18854
18685
  findings: `0 problems found — ${bridges} and the configuration around them is current`
18855
18686
  };
18856
18687
  }
@@ -18858,6 +18689,7 @@ function buildDoctorReport(bin, result, configuration = []) {
18858
18689
  bin,
18859
18690
  bridges: result.bridges,
18860
18691
  instructions: result.instructions,
18692
+ governances,
18861
18693
  ...result.divergence.length ? { divergence: result.divergence } : {},
18862
18694
  findings: findings.map(({ path, problem, detail }) => ({
18863
18695
  path,
@@ -18890,6 +18722,7 @@ const doctorCommand = command({
18890
18722
  const format = parseFormat(args.format);
18891
18723
  const harnesses = parseHarnesses(args.harness);
18892
18724
  const root = args.root ?? process.cwd();
18725
+ const home = homedir();
18893
18726
  const result = diagnoseBridges({
18894
18727
  root,
18895
18728
  ...harnesses.length ? { harnesses } : {},
@@ -18912,7 +18745,17 @@ const doctorCommand = command({
18912
18745
  cli: commandInvocation
18913
18746
  })
18914
18747
  ];
18915
- writeResult(buildDoctorReport(binPath(homedir(), process.argv[1]), result, configuration), format);
18748
+ const overrides = listGovernances(overrideLayers(governanceLayers({
18749
+ root,
18750
+ home,
18751
+ platform: process.platform,
18752
+ programData: process.env["ProgramData"]
18753
+ }))).map(({ name, scope, path }) => ({
18754
+ name,
18755
+ scope,
18756
+ path: collapseHome(home, path)
18757
+ }));
18758
+ writeResult(buildDoctorReport(displayBinPath(home, process.argv[1]), result, configuration, overrides), format);
18916
18759
  return exitCodes.success;
18917
18760
  } catch (error) {
18918
18761
  process.stderr.write(`error: ${error instanceof Error ? error.message : "Harness diagnosis failed."}\n`);
@@ -18921,10 +18764,111 @@ const doctorCommand = command({
18921
18764
  }
18922
18765
  });
18923
18766
  //#endregion
18767
+ //#region src/governance-overrides/governance.command.ts
18768
+ function layersFor(args) {
18769
+ const layers = governanceLayers({
18770
+ root: args.root ?? process.cwd(),
18771
+ home: homedir(),
18772
+ platform: process.platform,
18773
+ programData: process.env["ProgramData"]
18774
+ });
18775
+ return args["overrides-only"] ? overrideLayers(layers) : layers;
18776
+ }
18777
+ const DEPRECATED_MANAGED = "deprecated — move these documents to the managed layer above";
18778
+ const rootOption = {
18779
+ description: "Repository or package directory. Defaults to the current directory.",
18780
+ type: optionalType(stringType())
18781
+ };
18782
+ const overridesOnlyOption = {
18783
+ description: "Resolve only the project, user, and machine-wide layers. Never returns a governance this package ships, and exits non-zero when no layer has one.",
18784
+ type: optionalType(booleanType())
18785
+ };
18786
+ const governanceCommand = command({
18787
+ name: "governance",
18788
+ description: "Read the governance documents the project, user, and machine-wide override layers hold. Read-only.",
18789
+ commands: [command({
18790
+ name: "list",
18791
+ description: "List every governance the override layers hold, each at the layer that would win.",
18792
+ options: {
18793
+ root: rootOption,
18794
+ "overrides-only": overridesOnlyOption,
18795
+ format: {
18796
+ description: "Output format: toon (default), json, or text for a human-readable report.",
18797
+ type: optionalType(stringType()),
18798
+ default: "toon"
18799
+ }
18800
+ },
18801
+ run(args) {
18802
+ try {
18803
+ const format = parseFormat(args.format);
18804
+ const home = homedir();
18805
+ const layers = layersFor(args);
18806
+ const entries = listGovernances(layers);
18807
+ writeResult({
18808
+ layers: layers.map(({ scope, dir }) => ({
18809
+ scope,
18810
+ path: collapseHome(home, dir),
18811
+ status: scope === "managed-deprecated" ? DEPRECATED_MANAGED : ""
18812
+ })),
18813
+ governances: entries.length ? entries.map(({ name, scope, path }) => ({
18814
+ name,
18815
+ scope,
18816
+ path: collapseHome(home, path)
18817
+ })) : "0 governances — no layer holds one"
18818
+ }, format);
18819
+ return exitCodes.success;
18820
+ } catch (error) {
18821
+ process.stderr.write(`error: ${error instanceof Error ? error.message : "Governance listing failed."}\n`);
18822
+ return exitCodes.error;
18823
+ }
18824
+ }
18825
+ }), command({
18826
+ name: "show",
18827
+ description: "Print one governance document, resolved through the layers in order.",
18828
+ arguments: [{
18829
+ name: "name",
18830
+ description: "The governance name, without the `.md` extension.",
18831
+ type: stringType()
18832
+ }],
18833
+ options: {
18834
+ root: rootOption,
18835
+ "overrides-only": overridesOnlyOption,
18836
+ format: {
18837
+ description: "Output format: text (default) writes the document itself; toon and json wrap it with the layer it came from.",
18838
+ type: optionalType(stringType()),
18839
+ default: "text"
18840
+ }
18841
+ },
18842
+ run(args) {
18843
+ try {
18844
+ const format = parseFormat(args.format);
18845
+ const name = parseGovernanceName(args.name);
18846
+ const found = resolveGovernance(name, layersFor(args));
18847
+ if (!found) {
18848
+ process.stderr.write(args["overrides-only"] ? `error: no override for governance "${name}" in the project, user, or machine-wide layer.\n` : `error: no governance named "${name}" in any layer.\n`);
18849
+ return exitCodes.error;
18850
+ }
18851
+ if (format === "text") writeDocument(found.content);
18852
+ else writeResult({
18853
+ name: found.name,
18854
+ scope: found.scope,
18855
+ path: collapseHome(homedir(), found.path),
18856
+ content: found.content
18857
+ }, format);
18858
+ return exitCodes.success;
18859
+ } catch (error) {
18860
+ process.stderr.write(`error: ${error instanceof Error ? error.message : "Governance lookup failed."}\n`);
18861
+ return exitCodes.error;
18862
+ }
18863
+ }
18864
+ })]
18865
+ });
18866
+ //#endregion
18924
18867
  //#region src/initialize-harnesses/initialize-harnesses.ts
18925
18868
  function initializeHarnesses({ root, harnesses: preferred = [], copy = false, force = false }) {
18926
18869
  const canonicalSkills = join(root, ".agents", "skills");
18927
18870
  const skills = countSkills(canonicalSkills);
18871
+ const governances = countProjectGovernances(root);
18928
18872
  const harnesses = selectHarnesses(root, preferred);
18929
18873
  const { linked, skipped } = projectSkills({
18930
18874
  root,
@@ -18944,6 +18888,7 @@ function initializeHarnesses({ root, harnesses: preferred = [], copy = false, fo
18944
18888
  replacedBy: harness.deprecated
18945
18889
  })),
18946
18890
  skills,
18891
+ governances,
18947
18892
  copied: copy
18948
18893
  };
18949
18894
  }
@@ -18995,27 +18940,16 @@ const initCommand = command({
18995
18940
  //#endregion
18996
18941
  //#region src/cli.ts
18997
18942
  const version = typeof __PACKAGE_VERSION__ !== "undefined" ? __PACKAGE_VERSION__ : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
18998
- /**
18999
- * A factory rather than a module-level constant: `cli()` builds state, and state built at import
19000
- * time is shared by every later call in the process.
19001
- */
19002
18943
  function app() {
19003
18944
  return cli({
19004
18945
  name: "buddy-agent-harness",
19005
18946
  version,
19006
18947
  description: "Initialize agent harness skill compatibility in consumer repositories."
19007
- }).command(initCommand).command(doctorCommand).command(depPluginsCommand);
18948
+ }).command(initCommand).command(doctorCommand).command(depPluginsCommand).command(governanceCommand);
19008
18949
  }
19009
18950
  /**
19010
- * The application boundary: argv in, exit code out, nothing read from or written to the process.
19011
- *
19012
- * The exit code is returned rather than written so that a caller which is not the process — a skill
19013
- * launcher, a test — can run a command and learn how it went. `bin/buddy-agent-harness.mjs` is the
19014
- * only place that turns the returned code back into a process outcome.
19015
- *
19016
- * One exit path this return cannot carry: `clibuilder` answers an unknown option or an unknown
19017
- * command by printing help and writing `process.exitCode` itself, returning nothing. `run` reports
19018
- * `0` there, which is why `bin` and the launchers apply the returned code only when it is non-zero.
18951
+ * The application boundary: argv in, exit code out — returned rather than written, so a caller that
18952
+ * is not the process (a skill launcher, a test) can act on it.
19019
18953
  */
19020
18954
  async function run(argv) {
19021
18955
  try {