buddy-agent-harness 0.11.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 -553
  7. package/dist/index.d.mts +196 -329
  8. package/dist/index.d.mts.map +1 -1
  9. package/dist/index.mjs +517 -631
  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 +12 -38
  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 +19 -60
  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 +22 -75
  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,20 +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.
17878
- */
17879
- /**
17880
- * Exported for `mcp-inventory.ts`, which reads a handful of sources this module's own `McpConfig`
17881
- * shape cannot describe — a plugin's flat `.mcp.json`, a project entry nested inside `~/.claude.json`,
17882
- * and hosts whose `command` is an array or an object rather than a string. Each of those normalizes
17883
- * its raw entry into this same field shape before calling this function, so one reader still decides
17884
- * what counts as `transport`, `args`, and the rest.
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.
17885
17671
  */
17886
17672
  function serverFrom(entry) {
17887
17673
  const args = Array.isArray(entry["args"]) && entry["args"].every((item) => typeof item === "string");
@@ -17907,16 +17693,8 @@ function serversUnder(document, key) {
17907
17693
  return new Map(Object.entries(table).filter(([, entry]) => isRecord(entry)).map(([name, entry]) => [name, serverFrom(entry)]));
17908
17694
  }
17909
17695
  /**
17910
- * The position out of a thrown parse error, and nothing else out of it.
17911
- *
17912
- * `smol-toml` reports `line` and `column` on the error alongside a `codeblock` quoting the source,
17913
- * and its `message` embeds that same code block. Both are unusable: in a file of MCP configuration
17914
- * the line a parser failed on is exactly the line holding the credential. Only the two numbers are
17915
- * read, and anything thrown that does not carry them is reported without a position rather than by
17916
- * reaching for its message.
17917
- *
17918
- * Exported because that restriction is the point of the function rather than an implementation
17919
- * 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.
17920
17698
  */
17921
17699
  function positionOf(error) {
17922
17700
  if (!isRecord(error)) return void 0;
@@ -17942,12 +17720,8 @@ function parseGoldenSet(source) {
17942
17720
  }
17943
17721
  }
17944
17722
  /**
17945
- * One harness's MCP configuration, parsed. The file is read for its own key and nothing else:
17946
- * `.gemini/settings.json` also carries the instruction bridge, and a reader that treated the whole
17947
- * file as MCP would report the rest of a user's settings as servers it does not recognize.
17948
- *
17949
- * JSON is parsed with comments stripped, because Gemini CLI's loader strips them too and a
17950
- * 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.
17951
17725
  */
17952
17726
  function parseTarget(config, source) {
17953
17727
  if (source === void 0) return { kind: "absent" };
@@ -17968,49 +17742,26 @@ function parseTarget(config, source) {
17968
17742
  }
17969
17743
  //#endregion
17970
17744
  //#region src/diagnose-mcp/mcp-baseline.ts
17971
- /** Where the record of what was last written to each target lives, beside the golden set it came from. */
17972
- const projectionRecordPath = ".agents/buddy-agent-harness/mcp.projected.json";
17973
17745
  /**
17974
- * The last-projected record: per target path, per server name, the model that was written there.
17975
- *
17976
- * It exists because git can only answer for a file it can see. Harnesses write their MCP
17977
- * configuration themselves, at user scope, into files that are often untracked or ignored outright,
17978
- * and for those "which side moved since they last agreed" has no answer in history. A record
17979
- * written at projection time has one, exactly, because it *is* what was projected.
17980
- *
17981
- * A malformed record answers nothing rather than throwing. It is a cache of an answer, not the
17982
- * answer, and a diagnosis that dies because its cache is corrupt is worse than one that says
17983
- * `unknown`.
17746
+ * Where the record of what was last written to each target lives, beside the golden set it came
17747
+ * from.
17984
17748
  */
17749
+ const projectionRecordPath = ".agents/buddy-agent-harness/mcp.projected.json";
17985
17750
  function parseProjectionRecord(source) {
17986
17751
  const document = source === void 0 ? void 0 : parseJsonWithComments(source);
17987
17752
  const targets = isRecord(document) ? document["targets"] : void 0;
17988
17753
  if (!isRecord(targets)) return /* @__PURE__ */ new Map();
17989
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]))]));
17990
17755
  }
17991
- /**
17992
- * Which side moved, for one server and one field.
17993
- *
17994
- * The record is asked first, because it records exactly what was written and therefore answers
17995
- * exactly. Where it holds nothing, a **tracked** target falls back to git: the newest commit where
17996
- * the golden set and the target agreed on that field, then which working tree still matches it.
17997
- * Where neither can answer, the direction is `unknown` and the finding says so — naming a side on a
17998
- * guess would send a reconcile at the wrong file.
17999
- */
18000
17756
  var McpBaseline = class {
18001
17757
  options;
18002
17758
  constructor(options) {
18003
17759
  this.options = options;
18004
17760
  }
18005
17761
  /**
18006
- * One parse per commit per file, and one commit walk per target.
18007
- *
18008
- * `lastAgreed` runs per (config, server, field) that diverges without a projection record, and
18009
- * every one of those walks reads the same two files at the same commits. Unmemoized, three
18010
- * targets against five servers with two diverged fields each over fifty commits of history is
18011
- * three thousand `git show` calls and as many parses — on the command the `doctor` skill says is
18012
- * cheap enough for a session-start hook. One instance serves one diagnosis, so the memo lives and
18013
- * 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.
18014
17765
  */
18015
17766
  parsed = /* @__PURE__ */ new Map();
18016
17767
  walked = /* @__PURE__ */ new Map();
@@ -18028,9 +17779,8 @@ var McpBaseline = class {
18028
17779
  return this.lastAgreed(config, name, field);
18029
17780
  }
18030
17781
  /**
18031
- * The newest committed state where both files described this field the same way. Read at model
18032
- * granularity rather than by comparing blobs: the two files never share bytes, and a commit that
18033
- * 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.
18034
17784
  */
18035
17785
  lastAgreed(config, name, field) {
18036
17786
  for (const commit of this.commitsTouching(config)) {
@@ -18061,28 +17811,18 @@ var McpBaseline = class {
18061
17811
  //#endregion
18062
17812
  //#region src/diagnose-mcp/mcp-secrets.ts
18063
17813
  /**
18064
- * Finding a literal credential in MCP configuration, without ever handling one.
18065
- *
18066
- * `doctor` is safe to run from a session-start hook, so its output lands in agent context on every
18067
- * session and from there into transcripts. A value it echoes is amplified far past the file it came
18068
- * from. Every function here answers **where**, never **what**: the return is a field path, and the
18069
- * value never leaves this module.
18070
- *
18071
- * That is also why the redaction lives here rather than in a formatter. `doctor` renders three ways
18072
- * — TOON, `--format text`, and `--format json` — and trusting each of them to mask is three chances
18073
- * 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.
18074
17817
  */
18075
17818
  /**
18076
- * Words that mark a field as credential-bearing wherever they appear in its name. Each is one that
18077
- * effectively never turns up inside an innocent word, so a bare substring test is safe and catches
18078
- * 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.
18079
17821
  */
18080
17822
  const credentialWord = /token|secret|password|passwd|credential/i;
18081
17823
  /**
18082
- * Words that mark a field only when they are a **segment** of its name. `key` and `auth` are the
18083
- * two that matter, and both are common inside words that carry nothing — `MONKEY`, `AUTHOR`,
18084
- * `KEYSTONE_URL`. Matching them as substrings reports those, and a scanner that cries wolf on a
18085
- * 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`.
18086
17826
  */
18087
17827
  const credentialSegment = /* @__PURE__ */ new Set([
18088
17828
  "key",
@@ -18096,19 +17836,15 @@ function segmentsOf(key) {
18096
17836
  return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).filter(Boolean).map((segment) => segment.toLowerCase());
18097
17837
  }
18098
17838
  /**
18099
- * A value that **names** a secret instead of holding one, in the forms the supported hosts
18100
- * document: `${VAR}`, `$VAR`, and the `${env:VAR}` / `${input:id}` variants.
18101
- *
18102
- * Classification is by shape, not by content. An entropy test on the value would be a guess about
18103
- * a string this module has already decided not to look at, and it would fail in both directions —
18104
- * 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.
18105
17842
  */
18106
17843
  const reference = /\$\{[^}]+\}|\$[A-Za-z_][A-Za-z0-9_]*/;
18107
17844
  /**
18108
- * A credential-bearing field passes when it carries a reference anywhere in it, because the common
18109
- * legitimate form is a template rather than a bare variable — `Bearer ${LINEAR_TOKEN}` is the
18110
- * documented way to write an authorization header, and requiring the whole value to be a reference
18111
- * 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.
18112
17848
  */
18113
17849
  function holdsLiteral(value) {
18114
17850
  return !reference.test(value);
@@ -18117,11 +17853,8 @@ function credentialKey(key) {
18117
17853
  return credentialWord.test(key) || segmentsOf(key).some((segment) => credentialSegment.has(segment));
18118
17854
  }
18119
17855
  /**
18120
- * Whether a URL carries a credential in itself. Two shapes: a password in the userinfo component,
18121
- * and a credential-named query parameter holding a literal. Neither value is read past the test.
18122
- *
18123
- * A URL that does not parse is not reported. It is malformed rather than leaky, and guessing at its
18124
- * 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.
18125
17858
  */
18126
17859
  function urlHoldsCredential(value) {
18127
17860
  let url;
@@ -18137,12 +17870,7 @@ function urlHoldsCredential(value) {
18137
17870
  function literalsIn(field, values) {
18138
17871
  return Object.entries(values ?? {}).filter(([key, value]) => credentialKey(key) && holdsLiteral(value)).map(([key]) => `${field}.${key}`);
18139
17872
  }
18140
- /**
18141
- * Every field of one server holding a literal credential, as a path within that server.
18142
- *
18143
- * The path is the whole answer. It is never a truncated value: `sk-ab…` is a leak into the same
18144
- * transcript and buys a reader nothing the field name does not already give them.
18145
- */
17873
+ /** Field paths only — never a truncated value; `sk-ab…` is still a leak into the same transcript. */
18146
17874
  function credentialFields(server) {
18147
17875
  return [
18148
17876
  ...literalsIn("env", server.env),
@@ -18157,12 +17885,8 @@ function read(root, path) {
18157
17885
  return existsSync(absolute) ? readFileSync(absolute, "utf8") : void 0;
18158
17886
  }
18159
17887
  /**
18160
- * The distinct MCP files the enabled harnesses read. Two harnesses may name the same file.
18161
- *
18162
- * No harness preference is accepted, for the same reason the configuration half accepts none: every
18163
- * harness documenting an MCP file is already selected without one. Claude Code and Cursor are
18164
- * selected unconditionally; Codex and Gemini CLI keep their file inside their own detection
18165
- * 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
18166
17890
  * a finding.
18167
17891
  */
18168
17892
  function targetsOf(root) {
@@ -18186,12 +17910,6 @@ function diagnoseMcp({ root, git, cli }) {
18186
17910
  repair: repair(at, cli)
18187
17911
  });
18188
17912
  };
18189
- /**
18190
- * Credentials are reported for every file that holds servers, the golden set included — a user
18191
- * pastes a token into whichever file is open, and the golden set is a file. Severity splits on
18192
- * tracking, because a literal in a tracked file is already a committed credential and moving it
18193
- * to an environment variable does not un-commit it.
18194
- */
18195
17913
  const reportSecrets = (path, servers) => {
18196
17914
  const committed = git.trackingOf(path) !== "untracked";
18197
17915
  for (const [name, server] of servers) for (const field of credentialFields(server)) add({
@@ -18261,11 +17979,9 @@ const problemOf = {
18261
17979
  subagent: "nonstandard-subagent"
18262
17980
  };
18263
17981
  /**
18264
- * A `.mdc` rule splits on whether its scoping is load-bearing. `globs:` with a value binds the rule
18265
- * to paths, and `AGENTS.md` has no equivalent — it scopes by directory nesting only — so that one
18266
- * converts to a skill, or stays. Without it the rule is always-on prose, which `AGENTS.md` holds
18267
- * verbatim. Read from frontmatter rather than assumed, because the two need different conversions
18268
- * 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.
18269
17985
  */
18270
17986
  function scopedByGlobs(body) {
18271
17987
  const block = /^---\n([\s\S]*?)\n---/.exec(body);
@@ -18278,8 +17994,8 @@ function skillFiles(directory) {
18278
17994
  return filesUnder(directory).filter((file) => file.endsWith("SKILL.md"));
18279
17995
  }
18280
17996
  /**
18281
- * A harness directory that is a symlink is a projection someone already made, not configuration
18282
- * 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.
18283
17999
  */
18284
18000
  function authoredHere(path) {
18285
18001
  try {
@@ -18295,10 +18011,8 @@ function artifactFiles(root, artifact) {
18295
18011
  return (artifact.kind === "skill" ? skillFiles(absolute) : filesUnder(absolute)).map((file) => posix.join(artifact.path, file));
18296
18012
  }
18297
18013
  /**
18298
- * Unlike the other detectors this takes no harness preference and no enabled set. An artifact is
18299
- * reachable by one harness whether or not that harness is enabled here — a `.cursorrules` in a
18300
- * repository nobody opens in Cursor is still instruction content `AGENTS.md` does not carry — so
18301
- * 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.
18302
18016
  */
18303
18017
  function diagnoseNonstandard({ root, cli }) {
18304
18018
  const findings = [];
@@ -18316,6 +18030,129 @@ function diagnoseNonstandard({ root, cli }) {
18316
18030
  return findings.sort((left, right) => left.path.localeCompare(right.path));
18317
18031
  }
18318
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
18319
18156
  //#region src/skill-projection/skill-projection.ts
18320
18157
  function skillNames(skillsDirectory) {
18321
18158
  return readdirSync(skillsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
@@ -18329,10 +18166,8 @@ function pathOccupied(path) {
18329
18166
  }
18330
18167
  }
18331
18168
  /**
18332
- * Whether `target` is a symbolic link that resolves to `canonical`. Resolved paths are compared
18333
- * rather than link text, so a link a user wrote as an absolute path, one this tool wrote as a
18334
- * Windows junction, and one reached through a symbolic link in a parent directory all read as the
18335
- * 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.
18336
18171
  */
18337
18172
  function linksTo(target, canonical) {
18338
18173
  try {
@@ -18348,18 +18183,15 @@ function countSkills(canonicalSkills) {
18348
18183
  return skillNames(canonicalSkills).length;
18349
18184
  }
18350
18185
  /**
18351
- * A junction is the only link Windows grants an unprivileged process, and Node resolves a relative
18352
- * junction target against the process directory rather than the link's own, so Windows is given the
18353
- * 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.
18354
18188
  */
18355
18189
  function linkTarget(target, canonicalSkills) {
18356
18190
  return platform() === "win32" ? resolve(canonicalSkills) : relative(dirname(target), canonicalSkills);
18357
18191
  }
18358
18192
  /**
18359
- * Translates what the option parser hands back into a selection. A valueless `--force` arrives as
18360
- * the literal string `true` — that is how the parser encodes a flag given no value — and means
18361
- * every conflicting target, which is what keeps an existing `init --force` doing what it always did.
18362
- * 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.
18363
18195
  */
18364
18196
  function parseForce(value) {
18365
18197
  if (value === "true") return true;
@@ -18367,17 +18199,13 @@ function parseForce(value) {
18367
18199
  return value.split(",").map((target) => target.trim()).filter((target) => target.length > 0);
18368
18200
  }
18369
18201
  /**
18370
- * Targets are matched against what the caller typed, resolved against the repository root, so the
18371
- * repo-relative form the conflict message prints is the form that selects it back — and an absolute
18372
- * 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.
18373
18204
  */
18374
18205
  function namesTarget(root, target, given) {
18375
18206
  return resolve(root, given) === resolve(target);
18376
18207
  }
18377
- /**
18378
- * Points every harness that cannot read `.agents/skills` at the canonical directory. Symlinking is
18379
- * preferred; a platform that refuses the link falls back to a copy.
18380
- */
18208
+ /** Symlinks by default; falls back to a copy where the platform refuses the link. */
18381
18209
  function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
18382
18210
  const projections = harnesses.filter((harness) => harness.project.skillsDirectory).map((harness) => ({
18383
18211
  harness,
@@ -18419,12 +18247,8 @@ function projectSkills({ root, canonicalSkills, harnesses, copy, force }) {
18419
18247
  /** Directories that never hold instructions this repository is responsible for bridging. */
18420
18248
  const pruned = /* @__PURE__ */ new Set(["node_modules"]);
18421
18249
  /**
18422
- * Every directory below `root` holding an `AGENTS.md`, repository-relative and POSIX-separated,
18423
- * with the root itself as `''`. Sorted, root first.
18424
- *
18425
- * Dot-directories are pruned along with `node_modules`. That is not only a speed measure:
18426
- * `.agents/AGENTS.md` is canonical shared instructions rather than a subtree-scoped file, and
18427
- * 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.
18428
18252
  */
18429
18253
  function agentsFileDirectories(root) {
18430
18254
  const found = [];
@@ -18447,40 +18271,6 @@ function agentsFileDirectories(root) {
18447
18271
  //#endregion
18448
18272
  //#region src/diagnose-bridges/diagnose-instructions.ts
18449
18273
  const canonicalInstructions = "AGENTS.md";
18450
- /**
18451
- * An import bridges the `AGENTS.md` beside it and nothing else, so the check is per directory
18452
- * holding one rather than per harness. A line of its own is enough: the harness reference allows
18453
- * Claude-specific notes below the import, and a file that carries the import plus notes still
18454
- * bridges.
18455
- */
18456
- function inspectImport(target) {
18457
- let stats;
18458
- try {
18459
- stats = lstatSync(target);
18460
- } catch {
18461
- return {
18462
- kind: "none",
18463
- status: "missing",
18464
- problem: "instructions-missing"
18465
- };
18466
- }
18467
- if (stats.isSymbolicLink()) return readlinkSync(target) === "AGENTS.md" ? {
18468
- kind: "symlink",
18469
- status: "ok"
18470
- } : {
18471
- kind: "symlink",
18472
- status: "unbridged",
18473
- problem: "instructions-unbridged"
18474
- };
18475
- return readFileSync(target, "utf8").split("\n").some((line) => line.trim() === `@AGENTS.md`) ? {
18476
- kind: "import",
18477
- status: "ok"
18478
- } : {
18479
- kind: "file",
18480
- status: "unbridged",
18481
- problem: "instructions-unbridged"
18482
- };
18483
- }
18484
18274
  /** Reads a dotted key out of parsed JSON without asserting anything about the rest of the file. */
18485
18275
  function valueAt(settings, key) {
18486
18276
  return key.split(".").reduce((value, segment) => {
@@ -18488,6 +18278,14 @@ function valueAt(settings, key) {
18488
18278
  return value[segment];
18489
18279
  }, settings);
18490
18280
  }
18281
+ function exists(target) {
18282
+ try {
18283
+ lstatSync(target);
18284
+ return true;
18285
+ } catch {
18286
+ return false;
18287
+ }
18288
+ }
18491
18289
  function inspectSettingsEntry(target, key) {
18492
18290
  let source;
18493
18291
  try {
@@ -18515,21 +18313,45 @@ function inspectSettingsEntry(target, key) {
18515
18313
  problem: "instructions-unbridged"
18516
18314
  };
18517
18315
  }
18518
- /** Every path an instruction bridge of this kind belongs at, in report order. */
18519
- function pathsFor(root, bridge) {
18520
- if (bridge.kind !== "import") return [bridge.path];
18521
- return agentsFileDirectories(root).map((directory) => directory ? posix.join(directory, bridge.path) : bridge.path);
18522
- }
18523
- function inspect$1(target, bridge) {
18524
- 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
+ };
18525
18351
  }
18526
18352
  /**
18527
- * Reports whether every enabled harness can still read `AGENTS.md`.
18528
- *
18529
- * Losing one of these is quieter than losing a skills bridge and costs more: a harness with no
18530
- * instruction bridge reads none of the repository's instructions, and says nothing about it. The
18531
- * checks are read-only, and no repair is a command — rewriting an instruction file is the `init`
18532
- * skill's judgment.
18353
+ * Checks two things: whether each instruction bridge resolves, and whether anything suppresses a
18354
+ * harness that reads `AGENTS.md` natively.
18533
18355
  */
18534
18356
  function diagnoseInstructions(root, harnesses, cli) {
18535
18357
  const bridged = harnesses.map((harness) => ({
@@ -18538,11 +18360,9 @@ function diagnoseInstructions(root, harnesses, cli) {
18538
18360
  })).filter((harness) => Boolean(harness.bridge));
18539
18361
  const instructions = [];
18540
18362
  const findings = [];
18541
- if (!bridged.length) return {
18542
- instructions,
18543
- findings
18544
- };
18545
- 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("")) {
18546
18366
  const { detail, repair } = repairFor("no-instructions");
18547
18367
  findings.push({
18548
18368
  path: canonicalInstructions,
@@ -18551,24 +18371,42 @@ function diagnoseInstructions(root, harnesses, cli) {
18551
18371
  repair: repair({ file: canonicalInstructions }, cli)
18552
18372
  });
18553
18373
  }
18554
- for (const { name, bridge } of bridged) for (const path of pathsFor(root, bridge)) {
18555
- 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);
18556
18376
  instructions.push({
18557
18377
  harness: name,
18558
- path,
18378
+ path: bridge.path,
18559
18379
  kind: inspection.kind,
18560
18380
  status: inspection.status
18561
18381
  });
18562
18382
  if (inspection.problem) {
18563
18383
  const { detail, repair } = repairFor(inspection.problem);
18564
18384
  findings.push({
18565
- path,
18385
+ path: bridge.path,
18566
18386
  problem: inspection.problem,
18567
18387
  detail,
18568
- repair: repair({ file: path }, cli)
18388
+ repair: repair({ file: bridge.path }, cli)
18569
18389
  });
18570
18390
  }
18571
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
+ }
18572
18410
  return {
18573
18411
  instructions,
18574
18412
  findings
@@ -18596,25 +18434,20 @@ function lines(output) {
18596
18434
  return (output ?? "").split("\n").filter(Boolean);
18597
18435
  }
18598
18436
  /**
18599
- * Reads git state for the two sides of a bridge. Every method degrades to "cannot tell" rather than
18600
- * throwing, so `doctor` still reports on a tarball, a worktree without git, or a repository with no
18601
- * 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.
18602
18439
  */
18603
18440
  var GitBridgeState = class {
18604
18441
  root;
18605
- /** Path of `root` relative to the repository root, `''` at the top, `undefined` outside a repository. */
18606
- prefix;
18442
+ /** `''` at the top, `undefined` outside a repository. */
18443
+ repoRootPrefix;
18607
18444
  constructor(root) {
18608
18445
  this.root = root;
18609
- this.prefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
18446
+ this.repoRootPrefix = git(root, ["rev-parse", "--show-prefix"])?.trim();
18610
18447
  }
18611
- /**
18612
- * `init --copy` marks a bridge skip-worktree so a machine-local copy over a tracked symlink stops
18613
- * showing as a pending change. The bit is a hint that some checkouts clear, so it is verified
18614
- * rather than assumed.
18615
- */
18448
+ /** The skip-worktree bit is a hint some checkouts clear, so it is verified rather than assumed. */
18616
18449
  trackingOf(path) {
18617
- if (this.prefix === void 0) return "untracked";
18450
+ if (this.repoRootPrefix === void 0) return "untracked";
18618
18451
  const entries = lines(git(this.root, [
18619
18452
  "ls-files",
18620
18453
  "-v",
@@ -18625,13 +18458,11 @@ var GitBridgeState = class {
18625
18458
  return entries.every((entry) => entry.startsWith("S")) ? "skip-worktree" : "tracked";
18626
18459
  }
18627
18460
  /**
18628
- * Whether a `.gitignore` rule matches this path. Asked of git rather than matched against the
18629
- * file, so a rule on a parent directory — the common way a bridge gets ignored — is caught.
18630
- * Outside a repository nothing is ignored, which is the same "cannot tell" degradation the rest
18631
- * 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.
18632
18463
  */
18633
18464
  isIgnored(path) {
18634
- if (this.prefix === void 0) return false;
18465
+ if (this.repoRootPrefix === void 0) return false;
18635
18466
  return git(this.root, [
18636
18467
  "check-ignore",
18637
18468
  "-q",
@@ -18640,15 +18471,11 @@ var GitBridgeState = class {
18640
18471
  ]) !== void 0;
18641
18472
  }
18642
18473
  /**
18643
- * The newest commits that touched any of these paths, newest first. Empty outside a repository,
18644
- * which is the same "cannot tell" degradation as everything else here.
18645
- *
18646
- * Bounded like `lastAgreedTree` is: a baseline that is not in the recent history of the two files
18647
- * is not a baseline anyone can act on, and walking a whole repository to prove its absence costs
18648
- * 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.
18649
18476
  */
18650
18477
  commitsTouching(paths) {
18651
- if (this.prefix === void 0) return [];
18478
+ if (this.repoRootPrefix === void 0) return [];
18652
18479
  return lines(git(this.root, [
18653
18480
  "rev-list",
18654
18481
  "-n",
@@ -18660,15 +18487,11 @@ var GitBridgeState = class {
18660
18487
  }
18661
18488
  /** One file's content at one commit, or `undefined` when it did not exist there. */
18662
18489
  contentAt(commit, path) {
18663
- if (this.prefix === void 0) return void 0;
18664
- 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}`]);
18665
18492
  }
18666
- /**
18667
- * Names the side that moved by finding the newest commit whose two trees agreed, then asking which
18668
- * of the working directories still matches it.
18669
- */
18670
18493
  directionOf(bridge, canonical) {
18671
- if (this.prefix === void 0) return "unknown";
18494
+ if (this.repoRootPrefix === void 0) return "unknown";
18672
18495
  const base = this.lastAgreedTree(bridge, canonical);
18673
18496
  if (base === void 0) return "unknown";
18674
18497
  const bridgeMoved = !this.matchesTree(bridge, base);
@@ -18692,20 +18515,20 @@ var GitBridgeState = class {
18692
18515
  "rev-parse",
18693
18516
  "-q",
18694
18517
  "--verify",
18695
- `${commit}:${this.prefix}${bridge}`
18518
+ `${commit}:${this.repoRootPrefix}${bridge}`
18696
18519
  ])?.trim();
18697
18520
  if (!bridgeObject) continue;
18698
18521
  if (bridgeObject === git(this.root, [
18699
18522
  "rev-parse",
18700
18523
  "-q",
18701
18524
  "--verify",
18702
- `${commit}:${this.prefix}${canonical}`
18525
+ `${commit}:${this.repoRootPrefix}${canonical}`
18703
18526
  ])?.trim()) return bridgeObject;
18704
18527
  }
18705
18528
  }
18706
18529
  /**
18707
- * Compares a working directory against a git tree by object id. `hash-object` is used rather than
18708
- * 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.
18709
18532
  */
18710
18533
  matchesTree(directory, tree) {
18711
18534
  const expected = new Map(lines(git(this.root, [
@@ -18787,9 +18610,8 @@ function inspect(target, path, canonical, git) {
18787
18610
  }
18788
18611
  }
18789
18612
  /**
18790
- * Reports whether every bridge this repository needs still resolves: the skills bridges into
18791
- * `.agents/skills`, and the instruction bridges into `AGENTS.md`. Read-only: nothing is created,
18792
- * 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.
18793
18615
  */
18794
18616
  function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
18795
18617
  const canonical = join(root, ".agents", "skills");
@@ -18846,11 +18668,12 @@ function diagnoseBridges({ root, harnesses: preferred = [], cli }) {
18846
18668
  //#endregion
18847
18669
  //#region src/diagnose-bridges/doctor.command.ts
18848
18670
  /**
18849
- * AXI §5: the healthy answer states the zero with context, so an agent does not re-run with other
18850
- * 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.
18851
18673
  */
18852
- function buildDoctorReport(bin, result, configuration = []) {
18674
+ function buildDoctorReport(bin, result, configuration = [], overrides = []) {
18853
18675
  const findings = [...result.findings, ...configuration];
18676
+ const governances = overrides.length ? overrides : "0 governance overrides — no .agents/governances at project, user, or machine scope";
18854
18677
  if (!findings.length) {
18855
18678
  const count = result.bridges.length + result.instructions.length;
18856
18679
  const bridges = count === 1 ? "the 1 bridge resolves" : `all ${count} bridges resolve`;
@@ -18858,6 +18681,7 @@ function buildDoctorReport(bin, result, configuration = []) {
18858
18681
  bin,
18859
18682
  bridges: result.bridges,
18860
18683
  instructions: result.instructions,
18684
+ governances,
18861
18685
  findings: `0 problems found — ${bridges} and the configuration around them is current`
18862
18686
  };
18863
18687
  }
@@ -18865,6 +18689,7 @@ function buildDoctorReport(bin, result, configuration = []) {
18865
18689
  bin,
18866
18690
  bridges: result.bridges,
18867
18691
  instructions: result.instructions,
18692
+ governances,
18868
18693
  ...result.divergence.length ? { divergence: result.divergence } : {},
18869
18694
  findings: findings.map(({ path, problem, detail }) => ({
18870
18695
  path,
@@ -18897,6 +18722,7 @@ const doctorCommand = command({
18897
18722
  const format = parseFormat(args.format);
18898
18723
  const harnesses = parseHarnesses(args.harness);
18899
18724
  const root = args.root ?? process.cwd();
18725
+ const home = homedir();
18900
18726
  const result = diagnoseBridges({
18901
18727
  root,
18902
18728
  ...harnesses.length ? { harnesses } : {},
@@ -18919,7 +18745,17 @@ const doctorCommand = command({
18919
18745
  cli: commandInvocation
18920
18746
  })
18921
18747
  ];
18922
- 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);
18923
18759
  return exitCodes.success;
18924
18760
  } catch (error) {
18925
18761
  process.stderr.write(`error: ${error instanceof Error ? error.message : "Harness diagnosis failed."}\n`);
@@ -18928,10 +18764,111 @@ const doctorCommand = command({
18928
18764
  }
18929
18765
  });
18930
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
18931
18867
  //#region src/initialize-harnesses/initialize-harnesses.ts
18932
18868
  function initializeHarnesses({ root, harnesses: preferred = [], copy = false, force = false }) {
18933
18869
  const canonicalSkills = join(root, ".agents", "skills");
18934
18870
  const skills = countSkills(canonicalSkills);
18871
+ const governances = countProjectGovernances(root);
18935
18872
  const harnesses = selectHarnesses(root, preferred);
18936
18873
  const { linked, skipped } = projectSkills({
18937
18874
  root,
@@ -18951,6 +18888,7 @@ function initializeHarnesses({ root, harnesses: preferred = [], copy = false, fo
18951
18888
  replacedBy: harness.deprecated
18952
18889
  })),
18953
18890
  skills,
18891
+ governances,
18954
18892
  copied: copy
18955
18893
  };
18956
18894
  }
@@ -19002,27 +18940,16 @@ const initCommand = command({
19002
18940
  //#endregion
19003
18941
  //#region src/cli.ts
19004
18942
  const version = typeof __PACKAGE_VERSION__ !== "undefined" ? __PACKAGE_VERSION__ : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
19005
- /**
19006
- * A factory rather than a module-level constant: `cli()` builds state, and state built at import
19007
- * time is shared by every later call in the process.
19008
- */
19009
18943
  function app() {
19010
18944
  return cli({
19011
18945
  name: "buddy-agent-harness",
19012
18946
  version,
19013
18947
  description: "Initialize agent harness skill compatibility in consumer repositories."
19014
- }).command(initCommand).command(doctorCommand).command(depPluginsCommand);
18948
+ }).command(initCommand).command(doctorCommand).command(depPluginsCommand).command(governanceCommand);
19015
18949
  }
19016
18950
  /**
19017
- * The application boundary: argv in, exit code out, nothing read from or written to the process.
19018
- *
19019
- * The exit code is returned rather than written so that a caller which is not the process — a skill
19020
- * launcher, a test — can run a command and learn how it went. `bin/buddy-agent-harness.mjs` is the
19021
- * only place that turns the returned code back into a process outcome.
19022
- *
19023
- * One exit path this return cannot carry: `clibuilder` answers an unknown option or an unknown
19024
- * command by printing help and writing `process.exitCode` itself, returning nothing. `run` reports
19025
- * `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.
19026
18953
  */
19027
18954
  async function run(argv) {
19028
18955
  try {