buddy-agent-harness 1.0.1 → 1.1.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 (36) 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/CHANGELOG.md +25 -0
  5. package/dist/cli.mjs +2227 -840
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +521 -439
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/skill-scripts/doctor.mjs +74 -67
  10. package/dist/skill-scripts/init.mjs +74 -67
  11. package/dist/skill-scripts/mcp.mjs +74 -67
  12. package/llms.txt +2 -1
  13. package/package.json +4 -4
  14. package/plugin.json +1 -1
  15. package/skills/doctor-buddy-agent-harness/README.md +1 -1
  16. package/skills/doctor-buddy-agent-harness/SKILL.md +1 -1
  17. package/skills/doctor-buddy-agent-harness/scripts/doctor.mjs +74 -67
  18. package/skills/enhance/SKILL.md +1 -1
  19. package/skills/enhance/references/list-identifiers.history.md +9 -1
  20. package/skills/enhance/references/list-identifiers.md +6 -9
  21. package/skills/enhance/scripts/doctor.mjs +74 -67
  22. package/skills/init-buddy-agent-harness/SKILL.md +1 -1
  23. package/skills/init-buddy-agent-harness/scripts/doctor.mjs +74 -67
  24. package/skills/init-buddy-agent-harness/scripts/init.mjs +74 -67
  25. package/skills/repair/SKILL.md +1 -1
  26. package/skills/repair/scripts/doctor.mjs +74 -67
  27. package/skills/repair/scripts/mcp.mjs +74 -67
  28. package/src/cli.ts +13 -3
  29. package/src/command-output/command-output.ts +28 -5
  30. package/src/dep-plugins/dep-plugins.command.ts +4 -0
  31. package/src/diagnose-bridges/doctor.command.ts +67 -49
  32. package/src/governance-overrides/governance.command.ts +13 -8
  33. package/src/initialize-harnesses/init.command.ts +9 -3
  34. package/src/project-mcp/mcp.command.ts +15 -9
  35. package/src/reconcile-mcp/reconcile.command.ts +17 -11
  36. package/src/references/references.ts +1 -0
package/dist/cli.mjs CHANGED
@@ -1,18 +1,46 @@
1
1
  import { createRequire } from "node:module";
2
- import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, rmdirSync, statSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, symlinkSync, writeFileSync } from "node:fs";
3
3
  import { inspect, styleText } from "node:util";
4
4
  import { homedir, platform } from "node:os";
5
5
  import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
6
6
  import { readFile, readdir } from "node:fs/promises";
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/ref.js
9
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/ref.js
10
10
  /**
11
11
  * Next-step references: `ref()` names another command, and the runner renders it as a command line.
12
12
  * Imports nothing from the framework (axi-native RFC section 4.1; next-step-refs RFC).
13
13
  */
14
14
  const refKey = Symbol.for("clibuilder.ref");
15
15
  const placeholderKey = Symbol.for("clibuilder.placeholder");
16
+ /**
17
+ * A reference to another command, for a `help` list.
18
+ *
19
+ * The runner renders it as a complete command line for the channel it runs in:
20
+ * `<cli name> task get --gid 123` under `cli()`, or the sibling script, `pnpm exec` or pinned `npx` line under `runScript()`.
21
+ *
22
+ * Options render as `--name value` in the order given. `true` renders as `--name`, `false` and `undefined` are left out,
23
+ * an array repeats the flag, and a value is single-quoted when the shell would read it otherwise.
24
+ *
25
+ * @param path the command path as a user types it, without the cli name.
26
+ * @param options the options to pass, keyed by option name. Use {@link placeholder} for a value not known yet.
27
+ * @param why what running it does, as a verb phrase: `Run \`…\` to <why>`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * return result(task, { help: ref('task get', { gid: task.parent.gid }, 'show the parent task') })
32
+ * ```
33
+ */
34
+ function ref(path, options = {}, why) {
35
+ const words = path.trim().split(/\s+/).filter(Boolean);
36
+ if (words.length === 0) throw new TypeError("ref() needs a command path, such as \"task get\"");
37
+ return {
38
+ [refKey]: true,
39
+ path: words.join(" "),
40
+ options: { ...options },
41
+ why
42
+ };
43
+ }
16
44
  function isRef(value) {
17
45
  return typeof value === "object" && value !== null && value[refKey] === true;
18
46
  }
@@ -66,7 +94,7 @@ function shellQuote(value) {
66
94
  return /^[\w./:@%+=,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;
67
95
  }
68
96
  //#endregion
69
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/app/errors.js
97
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/app/errors.js
70
98
  var _a;
71
99
  /**
72
100
  * The exit codes a clibuilder cli uses, following the AXI convention so an agent
@@ -142,7 +170,7 @@ var CliError = class CliError extends Error {
142
170
  super(report.message);
143
171
  this.name = "CliError";
144
172
  this[_a] = true;
145
- this.exitCode = validateExitCode(report.exitCode ?? exitCodes.error);
173
+ this.exitCode = validateExitCode$1(report.exitCode ?? exitCodes.error);
146
174
  this.code = validateCode(report.code ?? "error");
147
175
  this.details = validateDetails(report.details ?? {});
148
176
  const lines = report.help === void 0 ? [] : Array.isArray(report.help) ? [...report.help] : [report.help];
@@ -163,7 +191,7 @@ function helpLinesOf(err) {
163
191
  function isReportedCliError(err) {
164
192
  return err[reportedBrand] === true;
165
193
  }
166
- function validateExitCode(exitCode) {
194
+ function validateExitCode$1(exitCode) {
167
195
  if (Number.isInteger(exitCode) && exitCode >= 1 && exitCode <= 255) return exitCode;
168
196
  throw new RangeError(`CliError exitCode must be an integer from 1 to 255, received ${exitCode}`);
169
197
  }
@@ -176,7 +204,7 @@ function validateDetails(details) {
176
204
  return { ...details };
177
205
  }
178
206
  //#endregion
179
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/command/define.js
207
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/command/define.js
180
208
  /**
181
209
  * Defines a command with its argument and option types inferred.
182
210
  *
@@ -198,7 +226,7 @@ function command(cmd) {
198
226
  return cmd;
199
227
  }
200
228
  //#endregion
201
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/descriptor.js
229
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/descriptor.js
202
230
  /** Whether a descriptor option takes a value, as `args-minus` decides: a boolean or boolean literal is a flag. */
203
231
  function takesValue$1(data) {
204
232
  const d = data.kind === "array" ? data.element : data;
@@ -228,7 +256,22 @@ function describeRange(min, max) {
228
256
  return "";
229
257
  }
230
258
  //#endregion
231
- //#region ../../node_modules/.pnpm/args-minus@0.2.2/node_modules/args-minus/esm/t.js
259
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/props.js
260
+ /** The property `key` of `object` itself, or `undefined` when only its prototype has one. */
261
+ function getProp$1(object, key) {
262
+ return Object.prototype.hasOwnProperty.call(object, key) ? object[key] : void 0;
263
+ }
264
+ /** Sets `key` on `object` itself, even when the key is `__proto__`. */
265
+ function setProp(object, key, value) {
266
+ Object.defineProperty(object, key, {
267
+ value,
268
+ enumerable: true,
269
+ writable: true,
270
+ configurable: true
271
+ });
272
+ }
273
+ //#endregion
274
+ //#region ../../node_modules/.pnpm/args-minus@0.2.3/node_modules/args-minus/esm/t.js
232
275
  const descriptorBrand = Symbol.for("args-minus.descriptor");
233
276
  const descriptorVersion = 1;
234
277
  const methods = {
@@ -400,7 +443,7 @@ Array.prototype.reduce;
400
443
  const ArraySlice = Array.prototype.slice;
401
444
  const ArrayPush = Array.prototype.push;
402
445
  //#endregion
403
- //#region ../../node_modules/.pnpm/args-minus@0.2.2/node_modules/args-minus/esm/tokenize_args.js
446
+ //#region ../../node_modules/.pnpm/args-minus@0.2.3/node_modules/args-minus/esm/tokenize_args.js
404
447
  const DEFAULT_GRAMMAR = {
405
448
  modifiers: "-",
406
449
  terminator: "--"
@@ -472,7 +515,7 @@ function tokenizeArgs(args, grammar) {
472
515
  return tokens;
473
516
  }
474
517
  //#endregion
475
- //#region ../../node_modules/.pnpm/args-minus@0.2.2/node_modules/args-minus/esm/parse_args.js
518
+ //#region ../../node_modules/.pnpm/args-minus@0.2.3/node_modules/args-minus/esm/parse_args.js
476
519
  /**
477
520
  * Thrown when the input does not fit the declarations: a usage error the caller can fix.
478
521
  */
@@ -602,7 +645,7 @@ function* parse$1(config) {
602
645
  const raw = names[i];
603
646
  const isLast = i === names.length - 1;
604
647
  const name = canonical.get(raw) ?? raw;
605
- const declaration = declarations?.[name];
648
+ const declaration = declarations && getProp(declarations, name);
606
649
  if (config.strict && !declaration) throw new ParseArgsError(`Unknown option '${raw}' (from '${token.raw}')`, raw, token.raw, { kind: "unknown-option" });
607
650
  if (!isLast && declaration && takesValue(declaration)) throw new ParseArgsError(`Invalid option group '${token.raw}': '${raw}' takes a value, so it can only be the last option in a group`, name, token.raw, {
608
651
  kind: "invalid-group",
@@ -666,6 +709,10 @@ function* parse$1(config) {
666
709
  if (config.tokens) result.tokens = tokens;
667
710
  return result;
668
711
  }
712
+ /** The property `key` of `object` itself, or `undefined` when only its prototype has one. */
713
+ function getProp(object, key) {
714
+ return Object.prototype.hasOwnProperty.call(object, key) ? object[key] : void 0;
715
+ }
669
716
  function checkDeclarations(declarations) {
670
717
  for (const [name, declaration] of Object.entries(declarations ?? {})) {
671
718
  const type = declaration.type;
@@ -901,10 +948,10 @@ function* checkDefault(declaration, name) {
901
948
  checkedDefaults.set(declaration, true);
902
949
  }
903
950
  //#endregion
904
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/argv_occurrences.js
951
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/argv_occurrences.js
905
952
  const optionOccurrences = /* @__PURE__ */ new WeakMap();
906
953
  //#endregion
907
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/lookup.js
954
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/lookup.js
908
955
  /**
909
956
  * @param fallback a command whose options apply to every command (the global options).
910
957
  * It decides how many following tokens such an option takes,
@@ -986,13 +1033,13 @@ function assignOptionValues(rawArgs, command, fallback) {
986
1033
  });
987
1034
  positionals.push(...o.following.slice(count));
988
1035
  const own = o.inline.length + taken.length > 0 ? [...o.inline, ...taken] : ["true"];
989
- values[o.key] = [...values[o.key] ?? [], ...own];
1036
+ setProp(values, o.key, [...getProp$1(values, o.key) ?? [], ...own]);
990
1037
  }
991
1038
  const result = {
992
1039
  ...rawArgs,
993
1040
  _: [...rawArgs._, ...positionals]
994
1041
  };
995
- for (const key of Object.keys(values)) if (rawArgs[key]) result[key] = values[key];
1042
+ for (const key of Object.keys(values)) if (getProp$1(rawArgs, key)) setProp(result, key, values[key]);
996
1043
  return [result, descriptorOccurrences];
997
1044
  }
998
1045
  function countFollowing(entry, o) {
@@ -1116,7 +1163,7 @@ function collectDescriptorOptions(state, occurrences) {
1116
1163
  function lookupOptions(command, key) {
1117
1164
  const opts = command.options;
1118
1165
  if (!opts) return [];
1119
- const options = opts[key];
1166
+ const options = getProp$1(opts, key);
1120
1167
  if (options) return [key, options];
1121
1168
  const optKey = Object.keys(opts).find((k) => {
1122
1169
  const opt = opts[k];
@@ -1180,6 +1227,25 @@ var RawString = class {
1180
1227
  this.value = value;
1181
1228
  }
1182
1229
  };
1230
+ /**
1231
+ * Wraps a pre-formatted string for verbatim emission, typically returned from
1232
+ * an encode `replacer`. Compose with `escapeString` to control quoting yourself.
1233
+ *
1234
+ * @param value The exact text to emit at the value position
1235
+ * @returns A `RawString` marker honored at primitive value positions
1236
+ *
1237
+ * @example
1238
+ * ```ts
1239
+ * encode({ name: 'Ada', age: 30 }, {
1240
+ * replacer: (key, value) => rawString(`"${escapeString(String(value))}"`)
1241
+ * })
1242
+ * // name: "Ada"
1243
+ * // age: "30"
1244
+ * ```
1245
+ */
1246
+ function rawString(value) {
1247
+ return new RawString(value);
1248
+ }
1183
1249
  function isRawString(value) {
1184
1250
  return value instanceof RawString;
1185
1251
  }
@@ -1674,7 +1740,7 @@ function resolveOptions(options) {
1674
1740
  };
1675
1741
  }
1676
1742
  //#endregion
1677
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/text.js
1743
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/text.js
1678
1744
  /**
1679
1745
  * The `text` encoder, the same rendering as `@clibuilder/axi`'s `renderText`.
1680
1746
  * Imports nothing from the framework (axi-native RFC section 4.1).
@@ -1682,14 +1748,14 @@ function resolveOptions(options) {
1682
1748
  function isRecord$3(value) {
1683
1749
  return typeof value === "object" && value !== null && !Array.isArray(value);
1684
1750
  }
1685
- function cell(value) {
1751
+ function cell$1(value) {
1686
1752
  return value === void 0 ? "" : String(value);
1687
1753
  }
1688
1754
  function table(rows) {
1689
1755
  const columns = [...new Set(rows.flatMap((row) => Object.keys(row)))];
1690
- const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell(row[column]).length)));
1756
+ const widths = columns.map((column) => Math.max(column.length, ...rows.map((row) => cell$1(row[column]).length)));
1691
1757
  const line = (values) => ` ${values.map((value, index) => value.padEnd(widths[index])).join(" ")}`.trimEnd();
1692
- return [line(columns), ...rows.map((row) => line(columns.map((column) => cell(row[column]))))];
1758
+ return [line(columns), ...rows.map((row) => line(columns.map((column) => cell$1(row[column]))))];
1693
1759
  }
1694
1760
  /**
1695
1761
  * Renders a value for a person: a key per line, a list of records as an aligned table, a list of values as bullets,
@@ -1708,7 +1774,7 @@ function renderText(value) {
1708
1774
  }).join("\n");
1709
1775
  }
1710
1776
  //#endregion
1711
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/encode.js
1777
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/encode.js
1712
1778
  /**
1713
1779
  * The single place an answer is turned into text.
1714
1780
  * Imports nothing from the framework (axi-native RFC section 4.1).
@@ -1718,11 +1784,40 @@ const formats = [
1718
1784
  "json",
1719
1785
  "text"
1720
1786
  ];
1721
- /** Encodes `value` in `format`, without a trailing newline. `toon` uses the official `@toon-format/toon` encoder. */
1787
+ /**
1788
+ * Encodes `value` in `format`, without a trailing newline.
1789
+ *
1790
+ * `toon` uses TOON only for collections (change 1 of the AXI conventions): an array, at any depth, is encoded
1791
+ * by the official `@toon-format/toon` encoder, and a string field outside any array is a plain `key: value` line.
1792
+ * The output is still TOON: a TOON decoder reads it back to the same value.
1793
+ */
1722
1794
  function encode(value, format) {
1723
1795
  if (format === "json") return JSON.stringify(value);
1724
1796
  if (format === "text") return renderText(value);
1725
- return encode$1(value);
1797
+ return encode$1(value, { replacer: plainLine });
1798
+ }
1799
+ function plainLine(_key, value, path) {
1800
+ if (typeof value !== "string" || path.length === 0 || path.some((p) => typeof p === "number")) return value;
1801
+ return readsBackAsIs(value) ? rawString(value) : value;
1802
+ }
1803
+ /**
1804
+ * Whether a TOON decoder reads `value`, written unquoted after `key: `, back as the same string.
1805
+ * Quoted are: the empty string, edge whitespace, control characters such as a newline, a leading `"`,
1806
+ * a leading `#` (the encoder keeps it for comments), and what reads as another type:
1807
+ * `true`, `false`, `null`, a number and `[]`.
1808
+ */
1809
+ function readsBackAsIs(value) {
1810
+ if (value === "" || value === "[]" || value.startsWith("\"") || value.startsWith("#")) return false;
1811
+ if (/^\s|\s$/.test(value) || hasControlCharacter(value)) return false;
1812
+ if (value === "true" || value === "false" || value === "null") return false;
1813
+ return !/^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value);
1814
+ }
1815
+ function hasControlCharacter(value) {
1816
+ for (let i = 0; i < value.length; i++) {
1817
+ const code = value.charCodeAt(i);
1818
+ if (code < 32 || code === 127) return true;
1819
+ }
1820
+ return false;
1726
1821
  }
1727
1822
  /** The error as a flat value: `error`, `code`, the `details` fields, then `help` (left out when empty). Keys match `@clibuilder/axi`'s `errorOutput` and axi-sdk-js. */
1728
1823
  function errorOutput({ message, code, details, help }) {
@@ -1735,7 +1830,7 @@ function errorOutput({ message, code, details, help }) {
1735
1830
  return output;
1736
1831
  }
1737
1832
  //#endregion
1738
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/builtin/base_command.js
1833
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/builtin/base_command.js
1739
1834
  const globalFormat = Symbol.for("clibuilder.global-format");
1740
1835
  /**
1741
1836
  * Marks a command's own `format` option that gives way to the global `--format`:
@@ -1807,7 +1902,7 @@ function yieldToGlobalFormat(command) {
1807
1902
  };
1808
1903
  }
1809
1904
  //#endregion
1810
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/plugins/registry.js
1905
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/plugins/registry.js
1811
1906
  /** Creates a key for a single value. `id` must be unique across extensions. */
1812
1907
  function defineKey(id) {
1813
1908
  return {
@@ -1857,7 +1952,7 @@ function createRegistry() {
1857
1952
  };
1858
1953
  }
1859
1954
  //#endregion
1860
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/config/key.js
1955
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/config/key.js
1861
1956
  /**
1862
1957
  * The shared-state key the `clibuilder/config` extension stores the resolved config under.
1863
1958
  *
@@ -1866,7 +1961,7 @@ function createRegistry() {
1866
1961
  */
1867
1962
  const configKey = defineKey("clibuilder.config");
1868
1963
  //#endregion
1869
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/argv.js
1964
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/argv.js
1870
1965
  function parseArgv(argv) {
1871
1966
  const occurrences = [];
1872
1967
  const result = argv.slice(2).reduce((result, v) => {
@@ -1941,9 +2036,10 @@ function startMultiCharacterOptions(result, value) {
1941
2036
  function endOption(state) {
1942
2037
  const { key, values } = state;
1943
2038
  const args = { ...state.args };
1944
- if (!args[key]) args[key] = [];
1945
- if (values.length === 0) args[key].push("true");
1946
- else args[key].push(...values);
2039
+ const list = getProp$1(args, key) ?? [];
2040
+ if (values.length === 0) list.push("true");
2041
+ else list.push(...values);
2042
+ setProp(args, key, list);
1947
2043
  return {
1948
2044
  args,
1949
2045
  key: "",
@@ -1951,7 +2047,7 @@ function endOption(state) {
1951
2047
  };
1952
2048
  }
1953
2049
  //#endregion
1954
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/descriptors.js
2050
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/invocation/descriptors.js
1955
2051
  const declarations = /* @__PURE__ */ new WeakMap();
1956
2052
  /**
1957
2053
  * Converts and checks every declaration of the matched command through `args-minus`,
@@ -2003,7 +2099,7 @@ function configOf(input) {
2003
2099
  };
2004
2100
  }
2005
2101
  function assign(result, input, { options }) {
2006
- if (options[input.name] !== void 0) result.args[input.name] = options[input.name];
2102
+ if (options[input.name] !== void 0) setProp(result.args, input.name, options[input.name]);
2007
2103
  }
2008
2104
  function report$1(result, input, e) {
2009
2105
  if (!(e instanceof ParseArgsError)) throw e;
@@ -2050,7 +2146,7 @@ function rawValues(input) {
2050
2146
  return input.occurrences.map((o) => o.tokens.join(" "));
2051
2147
  }
2052
2148
  //#endregion
2053
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/answer.js
2149
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/answer.js
2054
2150
  /**
2055
2151
  * The result channel: what `run()` returns, and how it is printed under `output: 'axi'`.
2056
2152
  * Imports nothing from the framework (axi-native RFC section 4.1).
@@ -2063,19 +2159,31 @@ const answerKey = Symbol.for("clibuilder.answer");
2063
2159
  * inside it. A renderer for the format replaces the encoder: its string is written verbatim, and the
2064
2160
  * `help` follows it, encoded. `parse()` resolves to `value`.
2065
2161
  *
2162
+ * The cli exits with `exitCode`, 0 by default, in either output mode.
2163
+ * A non-zero one returns an answer that is also a failure, printed as a result rather than as an error.
2164
+ *
2066
2165
  * @example
2067
2166
  * ```ts
2068
2167
  * run() {
2069
2168
  * return result({ items }, { help: 'Run `my-cli show <id>` for one', render: { text: (v) => table(v.items) } })
2070
2169
  * }
2071
2170
  * ```
2171
+ *
2172
+ * @example
2173
+ * ```ts
2174
+ * run({ name }) {
2175
+ * const exists = panes.has(name)
2176
+ * return result({ exists }, { exitCode: exists ? exitCodes.success : exitCodes.error })
2177
+ * }
2178
+ * ```
2072
2179
  */
2073
2180
  function result(value, options = {}) {
2074
2181
  return {
2075
2182
  [answerKey]: "result",
2076
2183
  value,
2077
2184
  help: helpLines(options.help),
2078
- render: options.render
2185
+ render: options.render,
2186
+ exitCode: validateExitCode(options.exitCode)
2079
2187
  };
2080
2188
  }
2081
2189
  /**
@@ -2083,11 +2191,13 @@ function result(value, options = {}) {
2083
2191
  * whatever the format, ending on a newline. `parse()` resolves to `content`.
2084
2192
  *
2085
2193
  * For output that is already the answer, such as Markdown, a diff or a file.
2194
+ * The cli exits with `exitCode`, 0 by default, in either output mode.
2086
2195
  */
2087
- function document(content) {
2196
+ function document(content, options = {}) {
2088
2197
  return {
2089
2198
  [answerKey]: "document",
2090
- value: String(content)
2199
+ value: String(content),
2200
+ exitCode: validateExitCode(options.exitCode)
2091
2201
  };
2092
2202
  }
2093
2203
  /**
@@ -2115,6 +2225,15 @@ function answerValue(returned) {
2115
2225
  return isAnswer(returned) ? returned.value : returned;
2116
2226
  }
2117
2227
  /**
2228
+ * The code the cli exits with for what `run()` returned: the `exitCode` of a `result()` or `document()`, otherwise 0.
2229
+ * Read field by field: an answer from an older copy of clibuilder has no `exitCode`.
2230
+ */
2231
+ function answerExitCode(returned) {
2232
+ if (!isAnswer(returned)) return 0;
2233
+ const code = returned.exitCode;
2234
+ return typeof code === "number" ? code : 0;
2235
+ }
2236
+ /**
2118
2237
  * The text written on stdout for what `run()` returned, ending on one newline, or `undefined` when nothing is printed.
2119
2238
  * A string is written verbatim, an object is encoded, and an array or scalar is encoded under the `result` key
2120
2239
  * so `help` has a place beside it.
@@ -2122,18 +2241,22 @@ function answerValue(returned) {
2122
2241
  function printAnswer(returned, format, options = {}) {
2123
2242
  if (returned === void 0) return void 0;
2124
2243
  const tail = (lines) => renderHelp(lines, options.resolve, options.refs);
2244
+ const withVerbs = (value, lines) => options.verbs && !hasOwnCommands(value) ? [...lines, options.verbs] : lines;
2125
2245
  if (!isAnswer(returned)) return print(returned, tail([]), format);
2126
2246
  switch (returned[answerKey]) {
2127
2247
  case "written": return;
2128
2248
  case "document": return line(returned.value);
2129
- case "empty": return line(encode(withHelp({ empty: returned.value }, tail(returned.help)), format));
2249
+ case "empty": return line(encode(withHelp({ empty: returned.value }, tail(withVerbs(returned.value, returned.help))), format));
2130
2250
  case "result": {
2131
2251
  const render = returned.render?.[format];
2132
- if (render) return withRenderedHelp(render(returned.value), tail(returned.help), format);
2133
- return print(returned.value, tail(returned.help), format);
2252
+ if (render) return withRenderedHelp(render(returned.value), tail(withVerbs(returned.value, returned.help)), format);
2253
+ return print(returned.value, tail(withVerbs(returned.value, returned.help)), format);
2134
2254
  }
2135
2255
  }
2136
2256
  }
2257
+ function hasOwnCommands(value) {
2258
+ return typeof value === "object" && value !== null && "commands" in value;
2259
+ }
2137
2260
  function print(value, tail, format) {
2138
2261
  if (typeof value === "string") return withRenderedHelp(value, tail, format);
2139
2262
  return line(encode(withHelp(toRecord(value), tail), format));
@@ -2164,11 +2287,16 @@ function helpLines(help) {
2164
2287
  if (help === void 0) return [];
2165
2288
  return Array.isArray(help) ? [...help] : [help];
2166
2289
  }
2290
+ function validateExitCode(exitCode) {
2291
+ if (exitCode === void 0) return 0;
2292
+ if (Number.isInteger(exitCode) && exitCode >= 0 && exitCode <= 255) return exitCode;
2293
+ throw new RangeError(`an answer's exitCode must be an integer from 0 to 255, received ${exitCode}`);
2294
+ }
2167
2295
  function line(text) {
2168
2296
  return text.endsWith("\n") ? text : `${text}\n`;
2169
2297
  }
2170
2298
  //#endregion
2171
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/error.js
2299
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/error.js
2172
2300
  /** Describes one `lookupCommand` error in the terms the caller used. `command` tells an argument name from an option name. */
2173
2301
  function formatLookupError(error, command) {
2174
2302
  switch (error.type) {
@@ -2196,7 +2324,7 @@ function toArray$1(value) {
2196
2324
  return Array.isArray(value) ? value : [value];
2197
2325
  }
2198
2326
  //#endregion
2199
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/wrap.js
2327
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/wrap.js
2200
2328
  /**
2201
2329
  * Wraps `text` at `width` columns, breaking only between words; a longer word stays whole.
2202
2330
  * A tab counts as four spaces.
@@ -2219,15 +2347,19 @@ function wrap(text, width) {
2219
2347
  return lines.join("\n");
2220
2348
  }
2221
2349
  //#endregion
2222
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/help.js
2350
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/help.js
2223
2351
  const RIGHT_PADDING = 2;
2224
2352
  const MIN_LHS_WIDTH = 25;
2225
- function generateHelpMessage(cliName, command) {
2353
+ /**
2354
+ * `signatures` lists a group's sub-commands with their full signatures (AXI change 2).
2355
+ * Left off, the list is names only, as `output: 'console'` has always printed it.
2356
+ */
2357
+ function generateHelpMessage(cliName, command, options) {
2226
2358
  return `
2227
2359
  ${[
2228
2360
  generateUsageSection(cliName, command),
2229
2361
  generateDescriptionSection(command),
2230
- generateCommandsSection(command),
2362
+ generateCommandsSection(command, options?.signatures ?? false),
2231
2363
  generateArgumentsSection(command),
2232
2364
  generateOptionsSection(command),
2233
2365
  generateAliasSection(command),
@@ -2241,12 +2373,23 @@ function generateUsageSection(cliName, command) {
2241
2373
  const hasCommand = command.commands && command.commands.length > 0;
2242
2374
  let message = `Usage: ${nameChain.join(" ")}${hasCommand ? " <command>" : ""}`;
2243
2375
  if (command.arguments) message += command.arguments.some((a) => isRequired(a, true)) ? " <arguments>" : " [arguments]";
2244
- if (command.options) {
2376
+ if (hasOptions(command)) {
2245
2377
  const options = command.options;
2246
2378
  message += Object.keys(options).some((k) => isRequired(options[k], false)) ? " <options>" : " [options]";
2247
2379
  }
2248
2380
  return message;
2249
2381
  }
2382
+ function hasOptions(command) {
2383
+ return !!command.options && Object.keys(command.options).length > 0;
2384
+ }
2385
+ /**
2386
+ * A sub-command as a bare group lists it: its name and its required arguments, such as `get <id>`
2387
+ * (AXI change 2: names and required arguments only).
2388
+ */
2389
+ function commandSignature(command) {
2390
+ const required = (command.arguments ?? []).filter((a) => isRequired(a, true)).map((a) => `<${a.name}>`);
2391
+ return [command.name, ...required].join(" ");
2392
+ }
2250
2393
  function isRequired({ type, default: d }, defaultValue) {
2251
2394
  if (!type) return defaultValue;
2252
2395
  return !type.isOptional && d === void 0;
@@ -2262,22 +2405,37 @@ function getCommandNameChain(cliName, command) {
2262
2405
  }
2263
2406
  return [cliName, ...commands.map((c) => c.name).filter((x) => x)];
2264
2407
  }
2265
- function generateCommandsSection(command) {
2266
- const commandNames = getCommandsNamesAndAlias(command.commands);
2267
- if (commandNames.length === 0) return "";
2408
+ /**
2409
+ * A sub-command as its group's `--help` lists it: name, alias, and the full signature of its
2410
+ * arguments and options, such as `add <name: string> [--force]` (AXI change 2: a node's --help is its schema).
2411
+ */
2412
+ function commandFullSignature(command) {
2413
+ const parts = [command.alias?.length ? `${command.name} (${command.alias.join("|")})` : command.name];
2414
+ if (command.commands && command.commands.length > 0) parts.push("<command>");
2415
+ for (const a of command.arguments ?? []) parts.push(a.type ? formatArgumentDescriptor(a.name, a.type) : `<${a.name}>`);
2416
+ if (hasOptions(command)) {
2417
+ const options = command.options;
2418
+ for (const key in options) parts.push(formatKeyValue(key, options[key]));
2419
+ }
2420
+ return parts.join(" ");
2421
+ }
2422
+ function generateCommandsSection(command, signatures) {
2423
+ const commands = command.commands ?? [];
2424
+ if (commands.length === 0) return "";
2425
+ if (!signatures) return `Commands:
2426
+ ${wrap(commands.map((c) => c.alias ? `${c.name} (${c.alias.join("|")})` : c.name).join(", "), 80)}
2427
+
2428
+ ${(command.name ? `${command.name} <command> -h` : " <command> -h").padEnd(MIN_LHS_WIDTH)}Get help for <command>`;
2429
+ const sigs = commands.map(commandFullSignature);
2430
+ const width = Math.max(23, ...sigs.map((x) => x.length + RIGHT_PADDING));
2268
2431
  return `Commands:
2269
- ${wrap(commandNames.join(", "), 80)}
2432
+ ${commands.map((c, i) => {
2433
+ const line = c.description?.split("\n")[0] ?? "";
2434
+ return ` ${sigs[i].padEnd(width)}${line}`.trimEnd();
2435
+ }).join("\n")}
2270
2436
 
2271
2437
  ${(command.name ? `${command.name} <command> -h` : " <command> -h").padEnd(MIN_LHS_WIDTH)}Get help for <command>`;
2272
2438
  }
2273
- function getCommandsNamesAndAlias(commands) {
2274
- const result = [];
2275
- if (commands) commands.forEach((c) => {
2276
- if (c.alias) result.push(`${c.name} (${c.alias.join("|")})`);
2277
- else result.push(c.name);
2278
- });
2279
- return result;
2280
- }
2281
2439
  function generateArgumentsSection(command) {
2282
2440
  if (!command.arguments) return "";
2283
2441
  let message = "Arguments:\n";
@@ -2293,14 +2451,15 @@ function generateArgumentsSection(command) {
2293
2451
  return message;
2294
2452
  }
2295
2453
  function generateOptionsSection(command) {
2296
- if (!command.options) return "";
2454
+ if (!hasOptions(command)) return "";
2297
2455
  let message = "Options:\n";
2298
2456
  const entries = [];
2299
2457
  let maxOptionStrWidth = 0;
2300
- for (const key in command.options) {
2301
- const value = command.options[key];
2458
+ const options = command.options;
2459
+ for (const key in options) {
2460
+ const value = options[key];
2302
2461
  const optionStr = formatKeyValue(key, value);
2303
- const description = formatDescription(value, conflictsOf(key, command.options));
2462
+ const description = formatDescription(value, conflictsOf(key, options));
2304
2463
  entries.push([optionStr, description]);
2305
2464
  maxOptionStrWidth = Math.max(maxOptionStrWidth, optionStr.length);
2306
2465
  }
@@ -2396,7 +2555,13 @@ function generateHelpModel(cliName, command, globalOptions) {
2396
2555
  if (command.description) model.description = command.description;
2397
2556
  if (command.alias && command.alias.length > 0) model.aliases = [...command.alias];
2398
2557
  if (command.commands && command.commands.length > 0) model.commands = command.commands.map((c) => {
2399
- const summary = { name: c.name };
2558
+ const summary = {
2559
+ name: c.name,
2560
+ usage: commandFullSignature({
2561
+ ...c,
2562
+ alias: void 0
2563
+ })
2564
+ };
2400
2565
  if (c.alias && c.alias.length > 0) summary.aliases = [...c.alias];
2401
2566
  const line = c.description?.split("\n")[0];
2402
2567
  if (line) summary.description = line;
@@ -2420,7 +2585,12 @@ function generateHelpModel(cliName, command, globalOptions) {
2420
2585
  command,
2421
2586
  description
2422
2587
  }));
2423
- model.globalOptions = optionModels(globalOptions, []);
2588
+ if (isRoot) model.globalOptions = optionModels(globalOptions, []);
2589
+ else {
2590
+ const others = Object.keys(globalOptions).filter((k) => k !== "help" && k !== "format");
2591
+ model.globalOptions = optionModels(globalOptions, others);
2592
+ if (others.length > 0) model.otherGlobalOptions = others;
2593
+ }
2424
2594
  return model;
2425
2595
  }
2426
2596
  function optionModels(options, exclude) {
@@ -2470,8 +2640,42 @@ function describeData(data) {
2470
2640
  default: return { type: data.kind };
2471
2641
  }
2472
2642
  }
2643
+ /**
2644
+ * The structured help shaped for `toon`: each collection of rows becomes a uniform table.
2645
+ *
2646
+ * TOON writes an array as a table only when every row has the same keys and every cell is a scalar.
2647
+ * A row of the help model leaves out a key with nothing to say, and some keys hold an array,
2648
+ * so the collections fell back to list form. Here every row of a collection carries the union of its keys,
2649
+ * a missing one as `null`, and an array of scalars as one comma-joined string.
2650
+ * The `json` shape is unchanged: this is applied for `toon` only.
2651
+ */
2652
+ function tabularHelpModel(model) {
2653
+ const shaped = { ...model };
2654
+ for (const key of [
2655
+ "commands",
2656
+ "arguments",
2657
+ "options",
2658
+ "examples",
2659
+ "globalOptions"
2660
+ ]) {
2661
+ const rows = model[key];
2662
+ if (rows) shaped[key] = uniformRows(rows);
2663
+ }
2664
+ return shaped;
2665
+ }
2666
+ function uniformRows(rows) {
2667
+ const keys = [];
2668
+ for (const row of rows) for (const key of Object.keys(row)) if (!keys.includes(key)) keys.push(key);
2669
+ return rows.map((row) => Object.fromEntries(keys.map((key) => [key, cell(row[key])])));
2670
+ }
2671
+ function cell(value) {
2672
+ if (value === void 0) return null;
2673
+ if (Array.isArray(value)) return value.map(String).join(",");
2674
+ if (typeof value === "object" && value !== null) return JSON.stringify(value);
2675
+ return value;
2676
+ }
2473
2677
  //#endregion
2474
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/render/usage_error.js
2678
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/render/usage_error.js
2475
2679
  const rank = {
2476
2680
  "invalid-key": 0,
2477
2681
  "conflicting-options": 1,
@@ -2564,7 +2768,7 @@ function describeUsageError(error, command, cliName, hasHelp = true) {
2564
2768
  /** A command group invoked without its sub-command (#609): `missing-command`, with the valid sub-commands in `help`. */
2565
2769
  function describeMissingCommand(command, cliName) {
2566
2770
  const name = commandName(command, cliName);
2567
- const commands = (command.commands ?? []).map((c) => c.name);
2771
+ const commands = (command.commands ?? []).map(commandSignature);
2568
2772
  return {
2569
2773
  message: `missing command for \`${name}\``,
2570
2774
  code: "missing-command",
@@ -2594,7 +2798,7 @@ function isOptional(type) {
2594
2798
  }
2595
2799
  function argumentsHelp(command, cliName) {
2596
2800
  const name = command.name || cliName;
2597
- const commands = (command.commands ?? []).map((c) => c.name);
2801
+ const commands = (command.commands ?? []).map(commandSignature);
2598
2802
  if (!command.arguments?.length && commands.length > 0) return `valid commands for \`${name}\`: ${commands.join(", ")}`;
2599
2803
  const args = (command.arguments ?? []).map((a) => isOptional(a.type) ? `[${a.name}]` : `<${a.name}>`);
2600
2804
  return args.length > 0 ? `\`${name}\` takes: ${args.join(" ")}` : `\`${name}\` takes no arguments`;
@@ -2622,7 +2826,7 @@ function toArray(value) {
2622
2826
  return Array.isArray(value) ? value : [value];
2623
2827
  }
2624
2828
  //#endregion
2625
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/app/state.js
2829
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/app/state.js
2626
2830
  function state(options) {
2627
2831
  const { name, description = "", version } = options;
2628
2832
  return {
@@ -2634,7 +2838,7 @@ function state(options) {
2634
2838
  };
2635
2839
  }
2636
2840
  //#endregion
2637
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/app/usage_errors.js
2841
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/app/usage_errors.js
2638
2842
  /**
2639
2843
  * Looks the invocation up on the base command, the single source of truth for the global options.
2640
2844
  * A bad `--format` value falls back to `format`, so its usage error is reported in a valid format and cannot loop.
@@ -2693,7 +2897,7 @@ function withAllowedFormats(baseCommand) {
2693
2897
  } : e;
2694
2898
  }
2695
2899
  //#endregion
2696
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/app/builder.js
2900
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/app/builder.js
2697
2901
  function builder(context, options) {
2698
2902
  const s = state(options);
2699
2903
  const format = options.format ?? "toon";
@@ -2881,7 +3085,7 @@ function builder(context, options) {
2881
3085
  for (const hook of beforeRunHooks) await hook(run);
2882
3086
  } catch (e) {
2883
3087
  if (!isCliError(e)) throw e;
2884
- reportCliError(e, globals.format, ui);
3088
+ reportCliError(e, globals.format, ui, command);
2885
3089
  return exit(e.exitCode);
2886
3090
  }
2887
3091
  s.config = run.config;
@@ -2897,16 +3101,16 @@ function builder(context, options) {
2897
3101
  commandInstance.ui.showVersion = () => writeVersion();
2898
3102
  }
2899
3103
  try {
2900
- return answer(await commandInstance.run(args), globals.format);
3104
+ return answer(await commandInstance.run(args), globals.format, command);
2901
3105
  } catch (e) {
2902
3106
  if (!isCliError(e)) throw e;
2903
- reportCliError(e, globals.format, commandInstance.ui);
3107
+ reportCliError(e, globals.format, commandInstance.ui, command);
2904
3108
  return exit(e.exitCode);
2905
3109
  }
2906
3110
  }
2907
- function reportCliError(e, format, ui) {
3111
+ function reportCliError(e, format, ui, command) {
2908
3112
  if (isReportedCliError(e)) return;
2909
- const { help } = renderHelp(helpLinesOf(e), resolveRef);
3113
+ const { help } = renderHelp(helpLinesOf(e), (r) => resolveRef(r, command));
2910
3114
  if (output === "axi") {
2911
3115
  const report = errorOutput({
2912
3116
  message: e.message,
@@ -2924,36 +3128,72 @@ function builder(context, options) {
2924
3128
  function writeReport(report, format) {
2925
3129
  context.writeStdout(`${encode(errorOutput(report), format)}\n`);
2926
3130
  }
2927
- function answer(returned, format) {
3131
+ function answer(returned, format, command) {
2928
3132
  if (output === "axi") {
3133
+ const verbs = command?.commands?.length ? `valid commands for \`${commandName(command, s.name)}\`: ${command.commands.map(commandSignature).join(", ")}` : void 0;
2929
3134
  const text = printAnswer(returned, format, {
2930
- resolve: resolveRef,
2931
- refs
3135
+ resolve: (r) => resolveRef(r, command),
3136
+ refs,
3137
+ verbs
2932
3138
  });
2933
3139
  if (text !== void 0) context.writeStdout(text);
2934
3140
  }
3141
+ const code = answerExitCode(returned);
3142
+ if (code !== exitCodes.success) context.exit(code);
2935
3143
  return answerValue(returned);
2936
3144
  }
2937
- function writeHelp(command, baseCommand, format) {
2938
- context.writeStdout(format === "text" ? `${generateHelpMessage(s.name, command)}\n` : `${encode(generateHelpModel(s.name, command, baseCommand.options), format)}\n`);
3145
+ function helpModel(command, baseCommand, format) {
3146
+ const model = generateHelpModel(s.name, command, baseCommand.options);
3147
+ return format === "toon" ? tabularHelpModel(model) : model;
2939
3148
  }
2940
- function resolveRef(ref) {
2941
- let command = s.command;
2942
- for (const word of ref.path.split(" ")) {
2943
- const next = command.commands?.find((c) => c.name === word || c.alias?.includes(word));
2944
- if (!next) throw new TypeError(`ref('${ref.path}'): ${s.name} has no command "${ref.path}"`);
2945
- command = next;
3149
+ function writeHelp(command, baseCommand, format) {
3150
+ context.writeStdout(format === "text" ? `${generateHelpMessage(s.name, command, { signatures: true })}\n` : `${encode(helpModel(command, baseCommand, format), format)}\n`);
3151
+ }
3152
+ function resolveRef(ref, from) {
3153
+ const words = ref.path.split(" ");
3154
+ let found;
3155
+ for (const scope of scopesOf(from)) {
3156
+ const command = walk(scope, words);
3157
+ if (command) {
3158
+ found = {
3159
+ command,
3160
+ scope
3161
+ };
3162
+ break;
3163
+ }
2946
3164
  }
3165
+ if (!found) throw new TypeError(`ref('${ref.path}'): ${s.name} has no command "${ref.path}"`);
3166
+ const { command, scope } = found;
2947
3167
  for (const name of Object.keys(ref.options)) {
2948
3168
  if (command.options?.[name] || s.command.options?.[name]) continue;
2949
3169
  throw new TypeError(`ref('${ref.path}'): the command has no option "${name}"`);
2950
3170
  }
2951
3171
  return [
2952
3172
  s.name,
3173
+ ...pathOf(scope),
2953
3174
  ref.path,
2954
3175
  ...refArgs(ref)
2955
3176
  ].join(" ");
2956
3177
  }
3178
+ function scopesOf(from) {
3179
+ const scopes = [];
3180
+ for (let c = from?.parent; c?.parent; c = c.parent) scopes.push(c);
3181
+ scopes.push(s.command);
3182
+ return scopes;
3183
+ }
3184
+ function walk(scope, words) {
3185
+ let command = scope;
3186
+ for (const word of words) {
3187
+ command = command.commands?.find((c) => c.name === word || c.alias?.includes(word));
3188
+ if (!command) return void 0;
3189
+ }
3190
+ return command;
3191
+ }
3192
+ function pathOf(command) {
3193
+ const words = [];
3194
+ for (let c = command; c?.parent; c = c.parent) words.unshift(c.name);
3195
+ return words;
3196
+ }
2957
3197
  function writeVersion() {
2958
3198
  context.writeStdout(`${s.version || "not versioned"}\n`);
2959
3199
  }
@@ -3018,7 +3258,7 @@ function adjustCommand(base, command) {
3018
3258
  return command;
3019
3259
  }
3020
3260
  //#endregion
3021
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/logger.js
3261
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/logger.js
3022
3262
  function createBuilderUI(ui) {
3023
3263
  let pending = true;
3024
3264
  const entries = [];
@@ -3042,7 +3282,7 @@ function createBuilderUI(ui) {
3042
3282
  };
3043
3283
  }
3044
3284
  //#endregion
3045
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/stream_ui.js
3285
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/stream_ui.js
3046
3286
  /**
3047
3287
  * Which stream each `ui` method writes to, per output mode. Internal: a cli picks a preset
3048
3288
  * with `cli({ output })`, never a table (axi-native RFC section 3.10).
@@ -3119,7 +3359,7 @@ function formatLine(args, colors) {
3119
3359
  return args.map((v) => typeof v === "object" && v !== null ? inspect(v, { colors }) : v).join(" ");
3120
3360
  }
3121
3361
  //#endregion
3122
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/streams.js
3362
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/streams.js
3123
3363
  /**
3124
3364
  * The stdout and stderr driver: where every byte the cli writes goes through.
3125
3365
  *
@@ -3173,7 +3413,7 @@ function handleEpipe(stream) {
3173
3413
  });
3174
3414
  }
3175
3415
  //#endregion
3176
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/context.js
3416
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/drivers/context.js
3177
3417
  /** Creates the app context: the built-in ui writing to `streams` as `output` routes it. */
3178
3418
  function context(options = {}) {
3179
3419
  const cwd = process.cwd();
@@ -3197,7 +3437,7 @@ function context(options = {}) {
3197
3437
  };
3198
3438
  }
3199
3439
  //#endregion
3200
- //#region ../../node_modules/.pnpm/clibuilder@12.1.1_js-yaml@4.3.2/node_modules/clibuilder/esm/cli.js
3440
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/cli.js
3201
3441
  function cli(options) {
3202
3442
  return builder(context({
3203
3443
  streams: options.streams,
@@ -3205,37 +3445,76 @@ function cli(options) {
3205
3445
  }), options);
3206
3446
  }
3207
3447
  //#endregion
3208
- //#region src/command-output/command-output.ts
3448
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/axi/home.js
3449
+ /** Shortens a path under the home directory to `~/...`: fewer tokens, and no user name in the output. */
3450
+ function collapseHome$2(path, home = homedir()) {
3451
+ const rest = path.slice(home.length);
3452
+ if (!home || !path.startsWith(home) || rest !== "" && rest[0] !== "/" && rest[0] !== "\\") return path;
3453
+ return `~${rest}`;
3454
+ }
3209
3455
  /**
3210
- * Reports a failure on stderr, not stdout — stdout carries what an agent parses — and fails the cli
3211
- * with the exit code. A `CliError` the cli already reports is rethrown untouched.
3456
+ * The first lines of a CLI's home view — what it prints when run with no arguments — so an agent
3457
+ * knows which tool answered: `bin: ~/.local/bin/my-cli` and `description: ...`.
3458
+ *
3459
+ * Spread it in front of the live content the home view shows:
3460
+ *
3461
+ * @example
3462
+ * ```ts
3463
+ * return result({ ...homeHeader({ description: 'Manage plugins' }), plugins })
3464
+ * ```
3212
3465
  */
3213
- function fail(error, fallback) {
3214
- if (isCliError(error)) throw error;
3215
- process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
3216
- throw CliError.reported(exitCodes.error);
3466
+ function homeHeader(options) {
3467
+ return {
3468
+ bin: collapseHome$2(options.bin ?? process.argv[1] ?? "", options.home ?? homedir()),
3469
+ description: options.description
3470
+ };
3217
3471
  }
3472
+ //#endregion
3473
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/axi/truncate.js
3218
3474
  /**
3219
- * An answer that comes with a failing exit code. The cli prints only an answer a command returns, and
3220
- * a thrown error replaces the return, so the answer is printed first.
3475
+ * Keeps the first `limit` items and counts the rest, so the reader knows whether it saw everything
3476
+ * without another call.
3477
+ *
3478
+ * `count` reads `30 of 847 total`. The kept list's own length (`items[30]` in TOON) stays the
3479
+ * number of rows shown, so a header never claims rows that are not there.
3221
3480
  */
3222
- function answerThenFail(answer, format) {
3223
- const text = printAnswer(answer, format);
3224
- if (text !== void 0) process.stdout.write(text);
3225
- throw CliError.reported(exitCodes.error);
3481
+ function truncateList(items, options) {
3482
+ const total = items.length;
3483
+ const kept = options.full ? [...items] : items.slice(0, options.limit);
3484
+ return {
3485
+ items: kept,
3486
+ count: `${kept.length} of ${total} total`,
3487
+ total,
3488
+ truncated: kept.length < total
3489
+ };
3226
3490
  }
3491
+ //#endregion
3492
+ //#region ../../node_modules/.pnpm/clibuilder@12.3.0_js-yaml@4.3.2/node_modules/clibuilder/esm/axi.js
3227
3493
  /**
3228
- * AXI §10: collapses the home directory to `~` so a path in a report is portable to another
3229
- * machine.
3494
+ * `clibuilder/axi`: helpers that shape the value a command returns for an agent.
3495
+ *
3496
+ * Core already renders errors, usage errors and results the AXI way.
3497
+ * This subpath adds truncation, counts, the empty state and the home header,
3498
+ * plus the result channel and exit codes so one import covers a command.
3499
+ *
3500
+ * It also works without `cli()`: `encode`, `errorOutput` and `printAnswer` are the output contract,
3501
+ * and `runScript` runs a command headless, for a script that is not a cli.
3230
3502
  */
3231
- function collapseHome$1(home, path) {
3232
- return home && path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
3233
- }
3234
- function displayBinPath(home, executable) {
3235
- return executable ? collapseHome$1(home, executable) : "buddy-agent-harness";
3236
- }
3503
+ /**
3504
+ * The `--full` option: the escape hatch from truncation. Add it to a command's options, and pass
3505
+ * `args.full` to {@link truncateText} and {@link truncateList}.
3506
+ *
3507
+ * @example
3508
+ * ```ts
3509
+ * options: { full: fullOption }
3510
+ * ```
3511
+ */
3512
+ const fullOption = {
3513
+ type: t.boolean().optional(),
3514
+ description: "Show the complete output instead of truncating it"
3515
+ };
3237
3516
  //#endregion
3238
- //#region ../../node_modules/.pnpm/cyber-agentkit@2.0.0_clibuilder@12.1.1_js-yaml@4.3.2_/node_modules/cyber-agentkit/dist/reference-where-CVmGFOwR.js
3517
+ //#region ../../node_modules/.pnpm/cyber-agentkit@2.2.0_clibuilder@12.3.0_js-yaml@4.3.2_/node_modules/cyber-agentkit/dist/reference-where-C93SAYP9.js
3239
3518
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
3240
3519
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
3241
3520
  /** Read a file, or `undefined` when it does not exist. Other read errors are thrown. */
@@ -11611,7 +11890,15 @@ const referenceCodes = {
11611
11890
  },
11612
11891
  "reference-renamed": {
11613
11892
  kind: "warning",
11614
- when: "a name or file under an older name, or an interface's older id (R96, R97)"
11893
+ when: "a name or file under an older name, an interface's older id, or a name a plugin's rename table renames (R96, R97, R120)"
11894
+ },
11895
+ "rename-invalid": {
11896
+ kind: "warning",
11897
+ when: "a plugin's rename table, or an entry of it, that is not a pair of bare reference names (R120)"
11898
+ },
11899
+ "reference-legacy-format": {
11900
+ kind: "warning",
11901
+ when: "a contribution in a form version 1 wrote and 2.0 retired, which `reference migrate` rewrites (R119)"
11615
11902
  },
11616
11903
  "alias-not-used": {
11617
11904
  kind: "warning",
@@ -11659,7 +11946,7 @@ const referenceCodes = {
11659
11946
  },
11660
11947
  "interface-unknown": {
11661
11948
  kind: "refused",
11662
- when: "`implementations` of an unknown interface (R35)"
11949
+ when: "`implementations` of an unknown interface (R35), or of a family matching none; `customize` of a family matching none (R135)"
11663
11950
  },
11664
11951
  "template-title": {
11665
11952
  kind: "refused",
@@ -11719,7 +12006,7 @@ const referenceCodes = {
11719
12006
  },
11720
12007
  "name-invalid": {
11721
12008
  kind: "usage",
11722
- when: "a write given a string that is not a name, such as `../x` (J1, R1)"
12009
+ when: "a write given a string that is not a name, such as `../x` (J1, R1); `*` anywhere but the last segment of a family (R136)"
11723
12010
  },
11724
12011
  "reference-qualified": {
11725
12012
  kind: "usage",
@@ -12042,6 +12329,19 @@ const namePattern$1 = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/i;
12042
12329
  const defaultSuffix = ".default";
12043
12330
  /** The suffix a metadata file's name carries before `.md` (K31); a name ending in it is reserved for that file. */
12044
12331
  const metadataSuffix = ".meta";
12332
+ /** Whether a value is written as a family: it ends in `.*`. Its prefix may still be invalid (R136). */
12333
+ function isFamilyName(value) {
12334
+ return value.endsWith(".*");
12335
+ }
12336
+ /**
12337
+ * The prefix of a family (K39), or why the value is not one. `*` anywhere but the last segment, a
12338
+ * qualified or file-like prefix, or an empty prefix makes the name invalid (R136).
12339
+ */
12340
+ function parseFamily(value) {
12341
+ const prefix = value.slice(0, -2);
12342
+ if (!isFamilyName(value) || !prefix || prefix.includes("*") || !namePattern$1.test(prefix)) return { invalid: `"${value}" is not a family. A family is \`<prefix>.*\`, with \`*\` only as the last segment and a prefix of letters, digits, hyphens, and dots.` };
12343
+ return { prefix };
12344
+ }
12045
12345
  /** No name at all is a usage error (`name-missing`), unlike a string that is not a name, which is `invalid`. */
12046
12346
  function requireName(value) {
12047
12347
  if (!value.trim()) throw new ReferenceFailure("name-missing", "name a reference.", { help: "Run `reference list` for every name." });
@@ -12060,6 +12360,7 @@ function parseReferenceName(value) {
12060
12360
  if (value.endsWith(".md")) return invalid(`"${value}" names a file. Ask for the reference by name, without the .md extension.`);
12061
12361
  const extension = dataExtensions.find((item) => value.endsWith(item.extension));
12062
12362
  if (extension) return invalid(`"${value}" names a file. Ask for the reference by name, without the ${extension.extension} extension.`);
12363
+ if (value.includes("*")) return invalid(`"${value}" is not a reference name: \`*\` makes a family, \`<prefix>.*\`, which only customize and implementations take.`);
12063
12364
  const parts = value.split("/");
12064
12365
  if (parts.length > 2 || value.includes("\\")) return invalid(`"${value}" is a path. A name is letters, digits, hyphens, and dots, optionally after "<plugin>/".`);
12065
12366
  const [first, second] = parts;
@@ -12112,11 +12413,12 @@ function parseDocument(raw, warn) {
12112
12413
  body
12113
12414
  };
12114
12415
  }
12115
- /** The contribution's own `merge`; `undefined` when it writes none. An unknown value reads as `patch`. */
12416
+ /** The contribution's own `merge`; `undefined` when it writes none. An unknown or version 1 value reads as `patch`. */
12116
12417
  function readMergeOperation(metadata, warn) {
12117
12418
  const value = metadata["merge"];
12118
12419
  if (value === void 0) return void 0;
12119
12420
  if (isMergeOperation(value)) return value;
12421
+ if (value === "merge-sections" || value === "first-wins") return "patch";
12120
12422
  warn("merge-unknown", `unknown merge "${String(value)}"; read as patch`);
12121
12423
  return "patch";
12122
12424
  }
@@ -12473,6 +12775,184 @@ function pickSections(body, picks) {
12473
12775
  missing
12474
12776
  };
12475
12777
  }
12778
+ const legacyMerges = {
12779
+ "merge-sections": void 0,
12780
+ "first-wins": "replace"
12781
+ };
12782
+ function isLegacyMerge(value) {
12783
+ return value === "merge-sections" || value === "first-wins";
12784
+ }
12785
+ /** The plugins `overrides` names, each qualified entry reduced to its plugin; `undefined` when it is not a list of them. */
12786
+ function overriddenPlugins(value) {
12787
+ const list = typeof value === "string" ? [value] : Array.isArray(value) ? value : void 0;
12788
+ if (!list?.length || !list.every((item) => typeof item === "string" && item.trim())) return void 0;
12789
+ return [...new Set(list.map((item) => item.split("/")[0].trim()))];
12790
+ }
12791
+ /** The plugin or `default` each `extends` step names, as written; `undefined` when it is not a list of steps. */
12792
+ function stepTargets(value) {
12793
+ const list = typeof value === "string" ? [value] : Array.isArray(value) ? value : void 0;
12794
+ if (!list) return void 0;
12795
+ const targets = [];
12796
+ for (const item of list) if (typeof item === "string") targets.push(item);
12797
+ else if (isRecord$1(item) && Object.keys(item).length === 1) targets.push(Object.keys(item)[0]);
12798
+ else return void 0;
12799
+ return targets;
12800
+ }
12801
+ const flow = (items) => `[${items.join(", ")}]`;
12802
+ /**
12803
+ * The legacy forms a Markdown contribution's frontmatter holds (K37), and the frontmatter that says
12804
+ * the same in 2.0 (R122). The body and every other key are left as written.
12805
+ */
12806
+ function legacyRewrite(metadata, body, place, name) {
12807
+ const findings = [];
12808
+ const keys = {};
12809
+ const merge = metadata["merge"];
12810
+ if (isLegacyMerge(merge)) {
12811
+ const into = legacyMerges[merge];
12812
+ keys["merge"] = place === "project" ? void 0 : into;
12813
+ findings.push({
12814
+ form: merge,
12815
+ from: `merge: ${merge}`,
12816
+ to: place === "project" ? "no merge: a project section carries none" : into === void 0 ? "no merge: patch is the default" : `merge: ${into}`,
12817
+ reads: "it reads as patch"
12818
+ });
12819
+ }
12820
+ if ("overrides" in metadata) findings.push(overridesFinding(metadata, place, keys));
12821
+ if (place === "project" && metadata["extends"] !== void 0 && !hasBody(body)) {
12822
+ const steps = stepTargets(metadata["extends"]);
12823
+ findings.push({
12824
+ form: "project-selection",
12825
+ from: `extends: ${steps ? flow(steps) : JSON.stringify(metadata["extends"])}`,
12826
+ manual: `a pick now applies to the whole repository: run \`reference customize ${name} --extends ${steps?.join(" ") ?? "<step>..."}\` at the root, then delete this file`,
12827
+ reads: "its extends is ignored"
12828
+ });
12829
+ }
12830
+ return {
12831
+ findings,
12832
+ keys
12833
+ };
12834
+ }
12835
+ function overridesFinding(metadata, place, keys) {
12836
+ const value = metadata["overrides"];
12837
+ const plugins = overriddenPlugins(value);
12838
+ const from = `overrides: ${plugins ? flow(Array.isArray(value) ? value : [String(value)]) : JSON.stringify(value)}`;
12839
+ const reads = "it is ignored";
12840
+ if (place === "project") {
12841
+ keys["overrides"] = void 0;
12842
+ return {
12843
+ form: "overrides",
12844
+ from,
12845
+ to: "removed: a project section carries no extends",
12846
+ reads
12847
+ };
12848
+ }
12849
+ if (!plugins) return {
12850
+ form: "overrides",
12851
+ from,
12852
+ manual: "`overrides` is not a list of plugin names",
12853
+ reads
12854
+ };
12855
+ const written = metadata["extends"];
12856
+ if (written === void 0) {
12857
+ keys["overrides"] = void 0;
12858
+ keys["extends"] = plugins;
12859
+ return {
12860
+ form: "overrides",
12861
+ from,
12862
+ to: `extends: ${flow(plugins)}`,
12863
+ reads
12864
+ };
12865
+ }
12866
+ const targets = stepTargets(written);
12867
+ const missing = plugins.filter((plugin) => !targets?.includes(plugin));
12868
+ if (targets && !missing.length) {
12869
+ keys["overrides"] = void 0;
12870
+ return {
12871
+ form: "overrides",
12872
+ from,
12873
+ to: "removed: extends already names each plugin",
12874
+ reads
12875
+ };
12876
+ }
12877
+ return {
12878
+ form: "overrides",
12879
+ from,
12880
+ manual: `\`extends\` is also written and does not name ${missing.join(", ")}; add ${missing.length > 1 ? "them" : "it"} to \`extends\` where it belongs, then remove \`overrides\``,
12881
+ reads
12882
+ };
12883
+ }
12884
+ /** The 2.0 form of a skill's default in the old layout (K37). */
12885
+ function defaultLayoutFinding(name) {
12886
+ return {
12887
+ form: "default-layout",
12888
+ from: `references/${name}.md`,
12889
+ to: `references/${name}.default.md`,
12890
+ reads: "it is not read as the default"
12891
+ };
12892
+ }
12893
+ /** The warning a legacy form raises at load (R119): the form, how it reads, its 2.0 form, and the fix. */
12894
+ function warnLegacy(finding, fix, warn) {
12895
+ const note = finding.note === void 0 ? "" : `, and ${finding.note}`;
12896
+ const into = finding.to === void 0 ? "" : `; its 2.0 form is ${finding.to}${note}`;
12897
+ const how = finding.manual === void 0 ? fix : `To fix it, ${finding.manual}${finding.note === void 0 ? "" : `; ${finding.note}`}.`;
12898
+ warn("reference-legacy-format", `\`${finding.from}\` is a version 1 form, and ${finding.reads}${into}. ${how}`);
12899
+ }
12900
+ function directoryEntries(dir) {
12901
+ try {
12902
+ return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
12903
+ } catch {
12904
+ return [];
12905
+ }
12906
+ }
12907
+ /**
12908
+ * The qualified-name overrides a tier or project folder holds, of `name` when given. A subfolder
12909
+ * holding `README.md` is a reference in folder form, not a plugin's, so its files are left alone.
12910
+ */
12911
+ function qualifiedOverrides(dir, name) {
12912
+ const found = [];
12913
+ for (const folder of directoryEntries(dir)) {
12914
+ const plugin = folder.name;
12915
+ if (!folder.isDirectory() || !namePattern$1.test(plugin)) continue;
12916
+ const files = directoryEntries(join(dir, plugin));
12917
+ if (files.some((file) => file.name === "README.md")) continue;
12918
+ for (const file of files) {
12919
+ if (!file.isFile() || !file.name.endsWith(".md")) continue;
12920
+ const held = file.name.slice(0, -3);
12921
+ if (!namePattern$1.test(held) || held.endsWith(".default") || held.endsWith(".meta")) continue;
12922
+ if (name !== void 0 && held !== name) continue;
12923
+ found.push({
12924
+ plugin,
12925
+ name: held,
12926
+ path: join(dir, plugin, file.name),
12927
+ file: `${plugin}/${file.name}`
12928
+ });
12929
+ }
12930
+ }
12931
+ return found;
12932
+ }
12933
+ const neverApplied = "no released version read it, so the override never applied";
12934
+ const turnsOn = "applying the plan turns it on";
12935
+ /**
12936
+ * The 2.0 form of a qualified-name override (K37): `<name>.md` beside the subfolder, with
12937
+ * `extends: [<plugin>]`. A project section carries no `extends` (K24), so there it is manual.
12938
+ */
12939
+ function qualifiedOverrideFinding(found, place) {
12940
+ const { plugin, name, file } = found;
12941
+ const said = {
12942
+ form: "qualified-override",
12943
+ from: file,
12944
+ reads: neverApplied,
12945
+ note: turnsOn
12946
+ };
12947
+ if (place === "project") return {
12948
+ ...said,
12949
+ manual: `pick plugin ${plugin} for the whole repository with \`reference customize ${name} --extends ${plugin}\` at the root, since a project section carries no extends; then move this file's body into ${name}.md in this folder and delete this file`
12950
+ };
12951
+ return {
12952
+ ...said,
12953
+ to: `${name}.md with extends: [${plugin}]`
12954
+ };
12955
+ }
12476
12956
  /** Keys a metadata file cannot carry: a non-Markdown contribution always replaces below (R68), and an interface's sections are Markdown. */
12477
12957
  const ignoredKeys$1 = ["merge", "implements"];
12478
12958
  /** The metadata file beside a contribution of `name` in a layer: `<name>.meta.md`, or a default's `<name>.default.meta.md`. */
@@ -12822,6 +13302,11 @@ function readContribution(layer, name, entry, { display, without, warnAt, older
12822
13302
  const stray = join(layer.dir, `${name}${defaultSuffix}.md`);
12823
13303
  if (stray !== without && tryRead$2(stray) !== void 0) warnAt(display(stray))("default-in-tier", "it is ignored: a default goes in a skill's references/ folder");
12824
13304
  }
13305
+ if (layer.tier === "repo" || layer.tier === "user") warnQualifiedOverrides$1(layer, name, {
13306
+ display,
13307
+ without,
13308
+ warnAt
13309
+ });
12825
13310
  const owner = layer.tier === "plugin" || layer.tier === "default";
12826
13311
  const [hit, ...ignored] = [name, ...owner ? [] : older].flatMap((as) => candidateFiles(layer, as).flatMap((file) => {
12827
13312
  const path = join(layer.dir, file);
@@ -12849,6 +13334,11 @@ function readContribution(layer, name, entry, { display, without, warnAt, older
12849
13334
  break;
12850
13335
  }
12851
13336
  if (hit && hit.as !== name) renamed(entry, hit.as, name, hit.path, display, warnAt);
13337
+ if (!hit && !sidecar && layer.tier === "default") warnDefaultLayout(layer, name, {
13338
+ display,
13339
+ without,
13340
+ warnAt
13341
+ });
12852
13342
  if (!hit) return sidecar ? sidecarSelection(layer, entry, sidecar, display, warnAt) : void 0;
12853
13343
  const label = contributionLabel(layer, hit.path, display);
12854
13344
  const warn = warnAt(label);
@@ -12878,6 +13368,8 @@ function readContribution(layer, name, entry, { display, without, warnAt, older
12878
13368
  };
12879
13369
  }
12880
13370
  const { metadata, body } = parseDocument(hit.raw, warn);
13371
+ const fix = legacyFix(layer, name, display);
13372
+ for (const finding of legacyRewrite(metadata, body, "contribution", name).findings) warnLegacy(finding, fix, warn);
12881
13373
  const merge = readMergeOperation(metadata, warn);
12882
13374
  const steps = readExtends(metadata["extends"], warn);
12883
13375
  Object.assign(entry, {
@@ -12904,6 +13396,26 @@ function readContribution(layer, name, entry, { display, without, warnAt, older
12904
13396
  steps
12905
13397
  };
12906
13398
  }
13399
+ /** Who rewrites a legacy form, and how (R119): only a file `migrate` may change gets the command itself. */
13400
+ function legacyFix(layer, name, display) {
13401
+ if (layer.tier === "plugin") return `Plugin ${layer.plugin}'s author rewrites it with \`reference migrate --plugin <its source folder>\`; until then, customize the name.`;
13402
+ if (layer.tier === "default") return `Its skill's author rewrites it with \`reference migrate ${name} --default ${display(dirname(layer.dir))}\`.`;
13403
+ if (layer.tier === "managed") return "An admin rewrites it; `reference migrate` never changes a managed file.";
13404
+ return "Run `reference migrate` to rewrite it.";
13405
+ }
13406
+ /** A qualified-name override of the name in a tier folder is not read; each warns (R119). */
13407
+ function warnQualifiedOverrides$1(layer, name, { display, without, warnAt }) {
13408
+ for (const found of qualifiedOverrides(layer.dir, name)) {
13409
+ if (found.path === without) continue;
13410
+ warnLegacy(qualifiedOverrideFinding(found, "contribution"), legacyFix(layer, name, display), warnAt(contributionLabel(layer, found.path, display)));
13411
+ }
13412
+ }
13413
+ /** A skill's default in the old layout, `references/<name>.md`, is not read; it warns when the skill's folder has no default of the name (R119). */
13414
+ function warnDefaultLayout(layer, name, { display, without, warnAt }) {
13415
+ const path = join(layer.dir, `${name}.md`);
13416
+ if (path === without || tryRead$2(path) === void 0) return;
13417
+ warnLegacy(defaultLayoutFinding(name), legacyFix(layer, name, display), warnAt(display(path)));
13418
+ }
12907
13419
  /** A file filed under an older name applies as the new one, with a warning naming the file to rename (R97). */
12908
13420
  function renamed(entry, as, name, path, display, warnAt) {
12909
13421
  entry.alias = as;
@@ -13147,7 +13659,7 @@ function checkImplementation(iface, { name, version, content, accept = iface.ver
13147
13659
  unsupported
13148
13660
  };
13149
13661
  }
13150
- const cache = /* @__PURE__ */ new WeakMap();
13662
+ const cache$1 = /* @__PURE__ */ new WeakMap();
13151
13663
  const quiet = () => () => {};
13152
13664
  function ownerLabel(owner) {
13153
13665
  return owner.tier === "plugin" ? `plugin ${owner.plugin}` : `default of ${owner.skill}`;
@@ -13169,7 +13681,7 @@ function listed(value) {
13169
13681
  */
13170
13682
  function aliasIndex(source) {
13171
13683
  const keyed = Array.isArray(source) ? void 0 : source;
13172
- const hit = keyed && cache.get(keyed);
13684
+ const hit = keyed && cache$1.get(keyed);
13173
13685
  if (hit) return hit;
13174
13686
  const layers = Array.isArray(source) ? source : source.layers;
13175
13687
  const byAlias = /* @__PURE__ */ new Map();
@@ -13209,16 +13721,15 @@ function aliasIndex(source) {
13209
13721
  });
13210
13722
  continue;
13211
13723
  }
13212
- if (id !== void 0) {
13213
- push(interfaces, id, {
13214
- alias,
13215
- owner
13216
- });
13217
- continue;
13724
+ if (id !== void 0) push(interfaces, id, {
13725
+ alias,
13726
+ owner
13727
+ });
13728
+ for (const older of id === void 0 ? [alias] : documentNames$1(alias, read.metadata)) {
13729
+ push(byAlias, older, owner);
13730
+ const known = byName.get(name) ?? [];
13731
+ if (!known.includes(older)) byName.set(name, [...known, older]);
13218
13732
  }
13219
- push(byAlias, alias, owner);
13220
- const older = byName.get(name) ?? [];
13221
- if (!older.includes(alias)) byName.set(name, [...older, alias]);
13222
13733
  }
13223
13734
  }
13224
13735
  }
@@ -13229,9 +13740,18 @@ function aliasIndex(source) {
13229
13740
  warnings,
13230
13741
  interfaces
13231
13742
  };
13232
- if (keyed) cache.set(keyed, index);
13743
+ if (keyed) cache$1.set(keyed, index);
13233
13744
  return index;
13234
13745
  }
13746
+ /**
13747
+ * The older names of an interface document that lists `alias` (R128): `<old id>.interface` always,
13748
+ * and `<old id>` itself when the interface is `many`, as no implementation of one is named `<old id>`.
13749
+ * For a `one` interface, `<old id>` is the implementation's older name (R96).
13750
+ */
13751
+ function documentNames$1(alias, metadata) {
13752
+ const block = metadata["interface"];
13753
+ return isRecord$1(block) && block["cardinality"] === "many" ? [alias, `${alias}${interfaceSuffix}`] : [`${alias}${interfaceSuffix}`];
13754
+ }
13235
13755
  function aliasFile(read) {
13236
13756
  return read.entry.frontmatter && read.whole ? read.path.replace(/\.[^./]+$/, ".meta.md") : read.path;
13237
13757
  }
@@ -13476,7 +13996,7 @@ function ignoredFolderOf(root, entry) {
13476
13996
  const folder = segments.join("/");
13477
13997
  return folder && isDirectory$3(join(root, ...segments, ...referencesPath)) ? folder : void 0;
13478
13998
  }
13479
- function entries(dir) {
13999
+ function entries$1(dir) {
13480
14000
  try {
13481
14001
  return readdirSync(dir, { withFileTypes: true });
13482
14002
  } catch {
@@ -13493,7 +14013,7 @@ function walk(root) {
13493
14013
  const found = [];
13494
14014
  const packages = [];
13495
14015
  const visit = (relative) => {
13496
- const listing = entries(join(root, relative));
14016
+ const listing = entries$1(join(root, relative));
13497
14017
  if (relative && !relative.split("/").includes("node_modules")) {
13498
14018
  if (listing.some((entry) => entry.isFile() && entry.name === "package.json")) packages.push(relative);
13499
14019
  }
@@ -13504,8 +14024,8 @@ function walk(root) {
13504
14024
  visit(child);
13505
14025
  continue;
13506
14026
  }
13507
- if (relative && entries(join(root, child, referencesPath[1])).length) found.push(relative);
13508
- for (const inner of entries(join(root, child))) if (inner.isDirectory() && inner.name !== referencesPath[1]) visit(`${child}/${inner.name}`);
14027
+ if (relative && entries$1(join(root, child, referencesPath[1])).length) found.push(relative);
14028
+ for (const inner of entries$1(join(root, child))) if (inner.isDirectory() && inner.name !== referencesPath[1]) visit(`${child}/${inner.name}`);
13509
14029
  }
13510
14030
  };
13511
14031
  visit("");
@@ -13978,10 +14498,26 @@ function readPluginSection(name, found, read, result) {
13978
14498
  plugin: plugin.plugin
13979
14499
  });
13980
14500
  }
14501
+ /** A qualified-name override of the name in a project folder is not read; each warns (R119). */
14502
+ function warnQualifiedOverrides(name, folder, read) {
14503
+ const { display, without, warnAt, listed } = read;
14504
+ for (const found of qualifiedOverrides(folder.dir, name)) {
14505
+ const hit = {
14506
+ folder,
14507
+ file: found.file,
14508
+ as: name,
14509
+ path: found.path,
14510
+ relative: `${folder.folder}/.agents/references/${found.file}`
14511
+ };
14512
+ if (found.path === without || !listed(hit)) continue;
14513
+ warnLegacy(qualifiedOverrideFinding(found, "project"), "", warnAt(`project ${display(found.path)}`));
14514
+ }
14515
+ }
13981
14516
  /** The folder's own `.agents/references/<name>.md` or `<name>/README.md`, when git does not ignore it. */
13982
14517
  function readOwnSection(name, folder, ignored, read, result) {
13983
14518
  const { trace, sections } = result;
13984
14519
  const { format, display, without, warnAt, catalog, older, listed, dropping, managed } = read;
14520
+ if (!ignored) warnQualifiedOverrides(name, folder, read);
13985
14521
  const filed = [name, ...older].map((as) => filesFor(folder, as, without));
13986
14522
  const markdown = filed.flatMap((files) => files.markdown);
13987
14523
  const data = filed.flatMap((files) => files.data);
@@ -14017,9 +14553,14 @@ function readOwnSection(name, folder, ignored, read, result) {
14017
14553
  const { metadata, body } = parseDocument(tryRead$2(hit.path) ?? "", warnAt(label));
14018
14554
  row.frontmatter = metadata;
14019
14555
  const kept = { ...metadata };
14556
+ const legacy = legacyRewrite(metadata, body, "project", name).findings;
14557
+ for (const finding of legacy) warnLegacy(finding, "Run `reference migrate` to rewrite it.", warnAt(label));
14558
+ const warnedAsLegacy = new Set(legacy.flatMap(({ form }) => form === "project-selection" ? ["extends"] : form === "overrides" ? ["overrides"] : ["merge"]));
14559
+ delete kept["overrides"];
14020
14560
  for (const key of ignoredKeys) {
14021
14561
  if (!(key in kept)) continue;
14022
14562
  delete kept[key];
14563
+ if (warnedAsLegacy.has(key)) continue;
14023
14564
  warnAt(label)("project-section-key-ignored", `\`${key}\` is ignored in the project section for ${folder.folder}: a project section states only what differs in its folder`);
14024
14565
  }
14025
14566
  if (dropping) {
@@ -14102,6 +14643,88 @@ function projectSectionNames(root, cache, plugins = /* @__PURE__ */ new Set()) {
14102
14643
  }
14103
14644
  return [...held];
14104
14645
  }
14646
+ /** Every name a qualified-name override (K37) in a project folder below the root names, in a file git does not ignore. */
14647
+ function projectQualifiedNames(root, cache) {
14648
+ const found = projectFolders(root, cache);
14649
+ return found.folders.flatMap((folder) => qualifiedOverrides(folder.dir).filter(({ file }) => !found.listed || found.listed.has(`${folder.folder}/.agents/references/${file}`)).map(({ name }) => name));
14650
+ }
14651
+ /**
14652
+ * A plugin's rename table, in its `references/` folder (K38). A leading `_` is not a reference name,
14653
+ * so the file never answers one.
14654
+ */
14655
+ const renameTableFile = "_renames.yaml";
14656
+ const cache = /* @__PURE__ */ new WeakMap();
14657
+ function bare(value) {
14658
+ if (typeof value !== "string") return void 0;
14659
+ const parsed = parseReferenceName(value.trim());
14660
+ return parsed.invalid || parsed.plugin !== void 0 ? void 0 : parsed.name;
14661
+ }
14662
+ /**
14663
+ * Every rename table the plugin tier's plugins ship (R120, R124), read once per set of layers. Only
14664
+ * a plugin renames its names, so no other tier is read.
14665
+ */
14666
+ function renameTable(source) {
14667
+ const keyed = Array.isArray(source) ? void 0 : source;
14668
+ const hit = keyed && cache.get(keyed);
14669
+ if (hit) return hit;
14670
+ const layers = Array.isArray(source) ? source : source.layers;
14671
+ const byOld = /* @__PURE__ */ new Map();
14672
+ const warnings = [];
14673
+ const seen = /* @__PURE__ */ new Set();
14674
+ for (const layer of layers) {
14675
+ if (layer.tier !== "plugin" || layer.skipped || layer.plugin === void 0) continue;
14676
+ const path = join(layer.dir, renameTableFile);
14677
+ if (seen.has(path)) continue;
14678
+ seen.add(path);
14679
+ const raw = tryRead$2(path);
14680
+ if (raw === void 0) continue;
14681
+ const plugin = layer.plugin;
14682
+ let value;
14683
+ try {
14684
+ value = (0, import_dist.parse)(raw);
14685
+ } catch {
14686
+ value = void 0;
14687
+ }
14688
+ if (value === null || value === void 0) {
14689
+ if (raw.trim()) warnings.push({
14690
+ code: "rename-invalid",
14691
+ message: `plugin ${plugin}: ${path} is not YAML; it is ignored`
14692
+ });
14693
+ continue;
14694
+ }
14695
+ if (!isRecord$1(value)) {
14696
+ warnings.push({
14697
+ code: "rename-invalid",
14698
+ message: `plugin ${plugin}: ${path} is not a mapping of older names to new names; it is ignored`
14699
+ });
14700
+ continue;
14701
+ }
14702
+ for (const [key, target] of Object.entries(value)) {
14703
+ const from = bare(key);
14704
+ const to = bare(target);
14705
+ if (from === void 0 || to === void 0 || from === to) {
14706
+ warnings.push({
14707
+ code: "rename-invalid",
14708
+ message: `plugin ${plugin}: the rename ${JSON.stringify(key)}: ${JSON.stringify(target)} in ${renameTableFile} is not a pair of bare reference names; it is ignored`
14709
+ });
14710
+ continue;
14711
+ }
14712
+ const list = byOld.get(from) ?? [];
14713
+ if (!list.some((entry) => entry.to === to && entry.plugin === plugin)) list.push({
14714
+ from,
14715
+ to,
14716
+ plugin
14717
+ });
14718
+ byOld.set(from, list);
14719
+ }
14720
+ }
14721
+ const table = {
14722
+ byOld,
14723
+ warnings
14724
+ };
14725
+ if (keyed) cache.set(keyed, table);
14726
+ return table;
14727
+ }
14105
14728
  function layersOf(source) {
14106
14729
  return Array.isArray(source) ? {
14107
14730
  layers: [...source],
@@ -14188,6 +14811,7 @@ function resolveAdjusted(input, source, { display = (path) => path, without, bel
14188
14811
  resolvedAs: ref.name
14189
14812
  };
14190
14813
  const older = olderNames(ref.name, aliases, warn);
14814
+ if (followed.kind === "own") warnTableRename(ref, source, aliases, warn);
14191
14815
  const cut = below === void 0 ? -1 : all.findIndex((layer) => layer.dir === below);
14192
14816
  const layers = cut === -1 ? all : all.slice(cut + 1);
14193
14817
  const contributions = [];
@@ -14475,8 +15099,20 @@ function resolveInterface(ref, contributions, holders, defaultContribution, resu
14475
15099
  function referenceNames(source) {
14476
15100
  return [...new Set(layersOf(source).layers.flatMap(layerNames))].sort((a, b) => a.localeCompare(b));
14477
15101
  }
14478
- /** Every name each installed skill holds a default of, by name then skill. */
14479
- function skillHoldings(source) {
15102
+ /**
15103
+ * A load of a name a plugin's rename table lists as older (R120) answers as itself, with a warning
15104
+ * naming the new name. A name a plugin or a default still holds as its own is not renamed.
15105
+ */
15106
+ function warnTableRename(ref, source, aliases, warn) {
15107
+ if (aliases.owned.has(ref.name)) return;
15108
+ const entries = (renameTable(source).byOld.get(ref.name) ?? []).filter((entry) => ref.plugin === void 0 || entry.plugin === ref.plugin);
15109
+ if (!entries.length) return;
15110
+ const by = [...new Set(entries.map(({ plugin }) => `plugin ${plugin}`))].join(" and ");
15111
+ const targets = [...new Set(entries.map(({ to }) => `"${to}"`))].join(" or ");
15112
+ warn("reference-renamed", `${by} renamed "${ref.name}" to ${targets}: load ${targets} instead, and run \`reference migrate\` to rename the files filed under "${ref.name}"`);
15113
+ }
15114
+ /** Every name each installed skill holds a default of, by name then skill. */
15115
+ function skillHoldings(source) {
14480
15116
  return (Array.isArray(source) ? [] : source.skills?.skills ?? []).flatMap((skill) => layerNames(defaultLayer(skill.folder)).map((name) => ({
14481
15117
  name,
14482
15118
  skill
@@ -14790,6 +15426,8 @@ const checkWarningClass = {
14790
15426
  "reference-stale": "environment",
14791
15427
  "reference-renamed": "environment",
14792
15428
  "alias-not-used": "environment",
15429
+ "rename-invalid": "environment",
15430
+ "reference-legacy-format": "defect",
14793
15431
  "alias-invalid": "defect",
14794
15432
  "project-section-not-markdown": "project-section",
14795
15433
  "reference-hidden": "environment",
@@ -14874,6 +15512,14 @@ function interfaceLookup(source, options = {}) {
14874
15512
  }
14875
15513
  function lookUp(id, source, options) {
14876
15514
  const resolved = resolveReference(`${id}${interfaceSuffix}`, source, options);
15515
+ const owner = interfaceIdOf(resolved.trace.alias?.resolvedAs ?? "");
15516
+ if (owner !== void 0) {
15517
+ const found = readFound(owner, resolved);
15518
+ return found.value ? {
15519
+ ...found,
15520
+ renamed: owner
15521
+ } : found;
15522
+ }
14877
15523
  if (resolved.status === "missing") {
14878
15524
  const owners = [...aliasIndex(source).interfaces].filter(([, aliases]) => aliases.some(({ alias }) => alias === id)).map(([owner]) => owner);
14879
15525
  if (owners.length > 1) return lookupFailure(id, `${id} is an older id of ${owners.join(" and ")}; implement one of them by its own id`);
@@ -14903,13 +15549,15 @@ function readFound(id, resolved) {
14903
15549
  const { value, problems } = readInterface(id, resolved.metadata ?? {});
14904
15550
  return value ? { value } : lookupFailure(id, `${name} is malformed: ${problems.join("; ")}`);
14905
15551
  }
14906
- function bareName$1(name) {
15552
+ /** The bare name a document answers as: the name an older one asked for resolves as (R96, R128), or the name itself. */
15553
+ function bareName$1(resolved) {
15554
+ const name = resolved.trace.alias?.resolvedAs ?? resolved.name;
14907
15555
  return name.slice(name.lastIndexOf("/") + 1);
14908
15556
  }
14909
15557
  /** What a composed document declares: an interface, an implementation, or neither. */
14910
15558
  function kindOf(resolved) {
14911
15559
  const metadata = resolved.metadata ?? {};
14912
- if (interfaceIdOf(bareName$1(resolved.name)) !== void 0 || metadata["interface"] !== void 0) return "interface";
15560
+ if (interfaceIdOf(bareName$1(resolved)) !== void 0 || metadata["interface"] !== void 0) return "interface";
14913
15561
  return metadata["implements"] === void 0 ? "none" : "implementation";
14914
15562
  }
14915
15563
  /** The `compose` failure for the defects composing a document raised (UC6.8), if any. */
@@ -14935,7 +15583,7 @@ function checkResolved(resolved, lookup, accept) {
14935
15583
  };
14936
15584
  if (resolved.status !== "found") return result;
14937
15585
  const { failures, warnings } = result;
14938
- const name = bareName$1(resolved.name);
15586
+ const name = bareName$1(resolved);
14939
15587
  const metadata = resolved.metadata ?? {};
14940
15588
  if (kindOf(resolved) === "interface") {
14941
15589
  const id = interfaceIdOf(name);
@@ -15052,7 +15700,7 @@ function checkReferences(source, options = {}) {
15052
15700
  };
15053
15701
  const lookup = interfaceLookup(source, rest);
15054
15702
  const layers = Array.isArray(source) ? void 0 : source;
15055
- const sectionNames = new Set(layers?.root === void 0 ? [] : projectSectionNames(layers.root, layers.projects, heldPlugins(layers)));
15703
+ const sectionNames = new Set(layers?.root === void 0 ? [] : [...projectSectionNames(layers.root, layers.projects, heldPlugins(layers)), ...projectQualifiedNames(layers.root, layers.projects)]);
15056
15704
  const resolve = (name) => {
15057
15705
  const ref = parseReferenceName(name);
15058
15706
  const main = resolveReference(ref, source, resolveOptions);
@@ -15260,6 +15908,198 @@ function generatorBase(resolved, iface) {
15260
15908
  if (stub !== void 0) base.stub = `${stub}\n`;
15261
15909
  return base;
15262
15910
  }
15911
+ function firstLine(value) {
15912
+ return typeof value === "string" ? value.split("\n")[0]?.trim() || "" : "";
15913
+ }
15914
+ /** Whether a name is in the interface's namespace but declares another interface that its name fits. */
15915
+ function belongsElsewhere(name, declared, id) {
15916
+ return declared !== void 0 && declared !== id && (name === declared || name.startsWith(`${declared}.`));
15917
+ }
15918
+ /** The highest `repo` or `user` file with a body the document holds: what makes the bare name its own. */
15919
+ function ownTier(resolved) {
15920
+ return resolved.trace.layers.find((layer) => (layer.tier === "repo" || layer.tier === "user") && layer.file && (layer.outcome === "used" || layer.outcome === "stale") && !layer.selection && !layer.empty)?.tier;
15921
+ }
15922
+ /** The plugins holding a file for the name, as read. */
15923
+ function holders(resolved) {
15924
+ return [...new Set(resolved.trace.layers.flatMap(({ tier, plugin, file }) => tier === "plugin" && plugin !== void 0 && file ? [plugin] : []))];
15925
+ }
15926
+ function referenceImplementations(id, source, options = {}) {
15927
+ return isFamilyName(id) || id.includes("*") ? familyImplementations(id, source, options) : interfaceImplementations(id, source, options);
15928
+ }
15929
+ /**
15930
+ * The interfaces a family names (K39): each interface document whose id starts with the prefix and a
15931
+ * dot, at any depth, never a plain document or an implementation (R130). Sorted by id. Throws
15932
+ * `name-invalid` for a malformed family (R136), and `interface-unknown` when none match (R135).
15933
+ */
15934
+ function familyInterfaces(family, source, options = {}) {
15935
+ const parsed = parseFamily(family);
15936
+ if (parsed.invalid !== void 0) throw new ReferenceFailure("name-invalid", parsed.invalid);
15937
+ const lookup = interfaceLookup(source, options);
15938
+ const aliases = aliasIndex(source);
15939
+ const ids = /* @__PURE__ */ new Set();
15940
+ for (const name of referenceNames(source)) {
15941
+ const id = interfaceIdOf(shownName(name, aliases));
15942
+ if (id === void 0 || !id.startsWith(`${parsed.prefix}.`)) continue;
15943
+ const value = lookup(id).value;
15944
+ if (value) ids.add(value.id);
15945
+ }
15946
+ if (!ids.size) throw new ReferenceFailure("interface-unknown", `no interface document matches ${family}.`, { help: "Run `reference list` for the names found." });
15947
+ return [...ids].sort((a, b) => a.localeCompare(b));
15948
+ }
15949
+ /** `implementations <prefix>.*`: each matched interface, in name order, as `implementations <id>` lists it (R134). */
15950
+ function familyImplementations(family, source, options = {}) {
15951
+ return {
15952
+ family,
15953
+ interfaces: familyInterfaces(family, source, options).map((id) => interfaceImplementations(id, source, options))
15954
+ };
15955
+ }
15956
+ function interfaceImplementations(id, source, options) {
15957
+ const { accept: acceptOption, ...rest } = options;
15958
+ const resolveOptions = {
15959
+ ...rest,
15960
+ projectSections: false
15961
+ };
15962
+ const ifaceName = parseReferenceName(`${id}${interfaceSuffix}`);
15963
+ if (ifaceName.invalid || ifaceName.plugin !== void 0) throw new ReferenceFailure("interface-unknown", `"${id}" is not an interface id.`);
15964
+ const lookup = interfaceLookup(source, rest);
15965
+ const iface = lookup(id);
15966
+ const value = iface.value;
15967
+ if (!value) throw new ReferenceFailure("interface-unknown", iface.failure?.message ?? `no interface ${id}`);
15968
+ const aliases = aliasIndex(source);
15969
+ const ids = [value.id, ...(aliases.interfaces.get(value.id) ?? []).map(({ alias }) => alias)];
15970
+ const accept = [...acceptOption?.length ? acceptOption : value.versions];
15971
+ const report = {
15972
+ interface: value.id,
15973
+ cardinality: value.cardinality,
15974
+ accept,
15975
+ implementations: [],
15976
+ excluded: [],
15977
+ warnings: []
15978
+ };
15979
+ const seen = /* @__PURE__ */ new Set();
15980
+ const warn = (warnings) => {
15981
+ for (const warning of warnings) {
15982
+ const key = `${warning.code} ${warning.message}`;
15983
+ if (!seen.has(key)) report.warnings.push(warning);
15984
+ seen.add(key);
15985
+ }
15986
+ };
15987
+ if (iface.renamed !== void 0) warn([{
15988
+ code: "reference-renamed",
15989
+ message: `${id} is an older id of ${value.id}; ask for ${value.id} instead`
15990
+ }]);
15991
+ const inNamespace = (name) => ids.some((each) => name === each || name.startsWith(`${each}.`));
15992
+ /** Whether a resolved document is a candidate at all: it declares the interface, or its name is in the namespace. */
15993
+ const relevant = (name, resolved) => {
15994
+ const declared = readImplements(resolved.metadata ?? {}).entry?.id;
15995
+ if (declared !== void 0 && ids.includes(declared)) return true;
15996
+ return inNamespace(name) && !ids.some((each) => belongsElsewhere(name, declared, each));
15997
+ };
15998
+ const consider = ({ pin, name, resolved, tier }) => {
15999
+ warn(resolved.warnings);
16000
+ const declared = readImplements(resolved.metadata ?? {}).entry;
16001
+ if (!declared || !ids.includes(declared.id)) {
16002
+ const shape = fitsNamespace(value.cardinality, ids, name) ? "" : ` and is not named ${value.cardinality === "one" ? value.id : `${value.id}.<key>`}`;
16003
+ report.excluded.push({
16004
+ name: pin,
16005
+ reasons: [`declares no implements ${value.id}@<version>${shape}`]
16006
+ });
16007
+ return;
16008
+ }
16009
+ const checked = checkResolved(resolved, lookup, {
16010
+ id: value.id,
16011
+ versions: accept
16012
+ });
16013
+ warn(checked.warnings);
16014
+ if (checked.failures.length) {
16015
+ report.excluded.push({
16016
+ name: pin,
16017
+ reasons: checked.failures.map(({ check, message }) => `${check}: ${message}`)
16018
+ });
16019
+ return;
16020
+ }
16021
+ report.implementations.push({
16022
+ pin,
16023
+ description: firstLine(resolved.metadata?.["description"]),
16024
+ unsupported: checked.unsupported,
16025
+ name,
16026
+ tier,
16027
+ version: declared.version
16028
+ });
16029
+ };
16030
+ const qualified = (plugin, name) => resolveReference(parseReferenceName(`${plugin}/${name}`), source, resolveOptions);
16031
+ const names = [...new Set(referenceNames(source).map((name) => shownName(name, aliases)))];
16032
+ for (const name of names) {
16033
+ if (interfaceIdOf(name) !== void 0) continue;
16034
+ const resolved = resolveReference(name, source, resolveOptions);
16035
+ if (resolved.status === "ambiguous") {
16036
+ for (const option of resolved.options ?? []) {
16037
+ if (option === "default") continue;
16038
+ const copy = resolveReference(parseReferenceName(option), source, resolveOptions);
16039
+ if (copy.status !== "found" || !relevant(name, copy)) continue;
16040
+ warn(copy.warnings);
16041
+ report.excluded.push({
16042
+ name: option,
16043
+ reasons: [`ambiguous: ${resolved.message}; each is left out until a customizer picks one`]
16044
+ });
16045
+ }
16046
+ continue;
16047
+ }
16048
+ if (resolved.status !== "found" || !relevant(name, resolved)) continue;
16049
+ const plugins = holders(resolved);
16050
+ const tier = value.cardinality === "many" ? ownTier(resolved) : void 0;
16051
+ if (tier !== void 0) {
16052
+ consider({
16053
+ pin: name,
16054
+ name,
16055
+ resolved,
16056
+ tier
16057
+ });
16058
+ for (const plugin of plugins) report.excluded.push({
16059
+ name: `${plugin}/${name}`,
16060
+ reasons: [`the ${tier} implementation answers ${name}; a consumer loads it by that name`]
16061
+ });
16062
+ continue;
16063
+ }
16064
+ const pick = resolved.trace.stack.filter((target) => target !== "default").at(-1);
16065
+ if (pick === void 0) continue;
16066
+ consider({
16067
+ pin: `${pick}/${name}`,
16068
+ name,
16069
+ resolved: qualified(pick, name),
16070
+ tier: "plugin"
16071
+ });
16072
+ for (const plugin of plugins) {
16073
+ if (plugin === pick) continue;
16074
+ report.excluded.push({
16075
+ name: `${plugin}/${name}`,
16076
+ reasons: [`${name} loads the stack of ${pick}, the pick; load ${pick}/${name}`]
16077
+ });
16078
+ }
16079
+ }
16080
+ if (value.cardinality === "one") {
16081
+ const inUse = implementationInUse(value.id, source, resolveOptions, report.implementations);
16082
+ if (inUse !== void 0) report.inUse = inUse;
16083
+ }
16084
+ return report;
16085
+ }
16086
+ /**
16087
+ * The implementation a load of a `one` interface's name builds on (R129), read from the stack the
16088
+ * load composes: the pin of the listed candidate for the plugin it builds on, or `default` when only
16089
+ * the skill's default is in the stack. None when the name is not `found`, or the plugin it builds on
16090
+ * is not listed.
16091
+ */
16092
+ function implementationInUse(id, source, options, listed) {
16093
+ const resolved = resolveReference(parseReferenceName(id), source, options);
16094
+ if (resolved.status !== "found") return void 0;
16095
+ const pick = resolved.trace.stack.filter((target) => target !== "default").at(-1);
16096
+ if (pick === void 0) return resolved.trace.stack.includes("default") ? "default" : void 0;
16097
+ return listed.find(({ pin }) => pin === `${pick}/${id}`)?.pin;
16098
+ }
16099
+ /** Whether a name fits the interface's cardinality under its id or one of its older ids. */
16100
+ function fitsNamespace(cardinality, ids, name) {
16101
+ return ids.some((id) => cardinality === "one" ? name === id : name.startsWith(`${id}.`) && name.length > id.length + 1);
16102
+ }
15263
16103
  /** A bare, valid name, or a refusal naming the bare name to use. */
15264
16104
  function bareName(value) {
15265
16105
  requireName(value);
@@ -16071,13 +16911,91 @@ function after(result, plan, layers, options) {
16071
16911
  }, options)
16072
16912
  };
16073
16913
  }
16914
+ /** Whether a step, at any depth, gives `sections`: they differ per interface, so a family takes none (UC24.4). */
16915
+ function givesSections(value) {
16916
+ if (Array.isArray(value)) return value.some(givesSections);
16917
+ if (typeof value !== "object" || value === null) return false;
16918
+ return Object.values(value).some((keys) => {
16919
+ if (typeof keys !== "object" || keys === null || Array.isArray(keys)) return false;
16920
+ const record = keys;
16921
+ return record["sections"] !== void 0 || givesSections(record["extends"]);
16922
+ });
16923
+ }
16074
16924
  /**
16075
- * Customizes a name for the `repo` or `user` tier or a folder's project section, running no model:
16076
- * writes the picks (a selection), or a body the generator wrote with its record, or, with `base`,
16077
- * returns what the generator reads and writes nothing. Every check runs first, in the order
16078
- * `validateWrite` gives; a file a contribution above hides is written all the same, with a warning.
16925
+ * Customizes every `one` interface of a family (K39) with the same steps (R131): for each, the file
16926
+ * `customize <id>` with those steps writes, in the tier asked. An interface no step can answer keeps
16927
+ * what it had (R132); a `many` interface is skipped (R133). Every interface is planned first, so a
16928
+ * refusal for any of them, a family matching none (R135), or a step giving `sections` leaves every
16929
+ * file as it was.
16079
16930
  */
16931
+ function customizeFamily(layers, args, options = {}) {
16932
+ const parsed = parseFamily(args.name);
16933
+ if (parsed.invalid !== void 0) throw new ReferenceFailure("name-invalid", parsed.invalid);
16934
+ const usage = validateWrite({
16935
+ command: "customize",
16936
+ ...args,
16937
+ name: parsed.prefix
16938
+ });
16939
+ if ([
16940
+ args.base,
16941
+ args.request,
16942
+ args.body,
16943
+ args.implements,
16944
+ args.merge
16945
+ ].some((value) => value !== void 0)) throw new ReferenceFailure("options-conflict", "a family takes only picks: --extends or --extends-file, with --scope and --dry-run; a body, a merge, or a base is per name.");
16946
+ const picks = args.extendsFile === void 0 ? usage.picks : readExtendsFile(args.extendsFile);
16947
+ if (picks !== void 0 && picks !== "none" && givesSections(picks.values)) throw new ReferenceFailure("step-invalid", `a step of ${args.name} gives sections, which differ per interface; pick for one interface at a time to change sections.`);
16948
+ const lookup = interfaceLookup(layers, options);
16949
+ const interfaces = familyInterfaces(args.name, layers, options).map((id) => {
16950
+ if (lookup(id).value?.cardinality === "many") return {
16951
+ interface: id,
16952
+ outcome: "skipped",
16953
+ reason: `${id} is a many interface, which has no single pick.`
16954
+ };
16955
+ const each = {
16956
+ ...args,
16957
+ name: id
16958
+ };
16959
+ try {
16960
+ const plan = validateWrite({
16961
+ command: "customize",
16962
+ ...each
16963
+ }, layers, options);
16964
+ if (picks !== "none" && plan.base.status !== "found") return {
16965
+ interface: id,
16966
+ outcome: "kept",
16967
+ reason: `no step holds ${id}; it keeps what it had.`
16968
+ };
16969
+ } catch (error) {
16970
+ if (error instanceof ReferenceFailure && error.code === "step-invalid") return {
16971
+ interface: id,
16972
+ outcome: "kept",
16973
+ reason: `${error.message} It keeps what it had.`
16974
+ };
16975
+ throw error;
16976
+ }
16977
+ return {
16978
+ interface: id,
16979
+ outcome: "written",
16980
+ answer: each
16981
+ };
16982
+ }).map(({ answer, ...entry }) => answer === void 0 ? entry : {
16983
+ ...entry,
16984
+ result: customizeSingle(layers, answer, options)
16985
+ });
16986
+ return {
16987
+ family: args.name,
16988
+ scope: args.scope ?? "repo",
16989
+ dryRun: Boolean(args.dryRun),
16990
+ interfaces
16991
+ };
16992
+ }
16080
16993
  function customizeContribution(layers, args, options = {}) {
16994
+ if (isFamilyName(args.name) || args.name.includes("*")) return customizeFamily(layers, args, options);
16995
+ return customizeSingle(layers, args, options);
16996
+ }
16997
+ /** Customizes one name that is not a family. */
16998
+ function customizeSingle(layers, args, options) {
16081
16999
  const plan = validateWrite({
16082
17000
  command: "customize",
16083
17001
  ...args
@@ -16099,160 +17017,324 @@ function customizeContribution(layers, args, options = {}) {
16099
17017
  if (plan.request !== void 0) return writeGenerated(plan, layers, result, options);
16100
17018
  return writePicks(plan, layers, result, options);
16101
17019
  }
16102
- function firstLine(value) {
16103
- return typeof value === "string" ? value.split("\n")[0]?.trim() || "" : "";
16104
- }
16105
- /** Whether a name is in the interface's namespace but declares another interface that its name fits. */
16106
- function belongsElsewhere(name, declared, id) {
16107
- return declared !== void 0 && declared !== id && (name === declared || name.startsWith(`${declared}.`));
17020
+ const dataSuffixes = dataExtensions.map(({ extension }) => extension);
17021
+ function entries(dir) {
17022
+ try {
17023
+ return readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
17024
+ } catch {
17025
+ return [];
17026
+ }
16108
17027
  }
16109
- /** The highest `repo` or `user` file with a body the document holds: what makes the bare name its own. */
16110
- function ownTier(resolved) {
16111
- return resolved.trace.layers.find((layer) => (layer.tier === "repo" || layer.tier === "user") && layer.file && (layer.outcome === "used" || layer.outcome === "stale") && !layer.selection && !layer.empty)?.tier;
17028
+ function valid(name) {
17029
+ return !parseReferenceName(name).invalid;
17030
+ }
17031
+ /** The files a place holds for each name, in the forms its tier answers. */
17032
+ function heldFiles(place) {
17033
+ const held = [];
17034
+ const isDefault = place.tier === "default";
17035
+ const own = isDefault ? defaultSuffix : "";
17036
+ for (const entry of entries(place.dir)) {
17037
+ const file = entry.name;
17038
+ const path = join(place.dir, file);
17039
+ if (entry.isDirectory()) {
17040
+ if (!isDefault && valid(file) && tryRead$2(join(path, "README.md")) !== void 0) held.push({
17041
+ name: file,
17042
+ path: join(path, "README.md"),
17043
+ suffix: "/README.md",
17044
+ kind: "markdown",
17045
+ folderForm: true
17046
+ });
17047
+ continue;
17048
+ }
17049
+ if (file === "_renames.yaml") continue;
17050
+ const metadata = `${own}${metadataSuffix}.md`;
17051
+ if (file.endsWith(metadata)) {
17052
+ const name = file.slice(0, -metadata.length);
17053
+ if (valid(name)) held.push({
17054
+ name,
17055
+ path,
17056
+ suffix: metadata,
17057
+ kind: "metadata",
17058
+ folderForm: false
17059
+ });
17060
+ continue;
17061
+ }
17062
+ const markdown = `${own}.md`;
17063
+ if (file.endsWith(markdown)) {
17064
+ const name = file.slice(0, -markdown.length);
17065
+ if (valid(name)) held.push({
17066
+ name,
17067
+ path,
17068
+ suffix: markdown,
17069
+ kind: "markdown",
17070
+ folderForm: false
17071
+ });
17072
+ continue;
17073
+ }
17074
+ const data = dataSuffixes.find((extension) => file.endsWith(`${own}${extension}`));
17075
+ if (data) {
17076
+ const name = file.slice(0, -`${own}${data}`.length);
17077
+ if (valid(name)) held.push({
17078
+ name,
17079
+ path,
17080
+ suffix: `${own}${data}`,
17081
+ kind: "data",
17082
+ folderForm: false
17083
+ });
17084
+ }
17085
+ }
17086
+ return held;
16112
17087
  }
16113
- /** The plugins holding a file for the name, as read. */
16114
- function holders(resolved) {
16115
- return [...new Set(resolved.trace.layers.flatMap(({ tier, plugin, file }) => tier === "plugin" && plugin !== void 0 && file ? [plugin] : []))];
17088
+ /** The places a run reads (R123): a source folder alone, or the `repo` and `user` tiers and every project section. */
17089
+ function placesOf(layers, root, options) {
17090
+ if (options.plugin !== void 0 && options.default !== void 0) throw new ReferenceFailure("options-conflict", "pass one of --plugin and --default.");
17091
+ if (options.plugin !== void 0) return [{
17092
+ tier: "plugin",
17093
+ dir: join(sourceFolder(options.plugin, root, "plugin"), "references")
17094
+ }];
17095
+ if (options.default !== void 0) return [{
17096
+ tier: "default",
17097
+ dir: join(sourceFolder(options.default, root, "skill"), "references")
17098
+ }];
17099
+ const places = [
17100
+ {
17101
+ tier: "repo",
17102
+ dir: repoReferencesDir(root)
17103
+ },
17104
+ {
17105
+ tier: "user",
17106
+ dir: userReferencesDir(options.home)
17107
+ },
17108
+ ...projectFolders(root, layers.projects).folders.map(({ folder, dir }) => ({
17109
+ tier: "project",
17110
+ dir,
17111
+ folder
17112
+ }))
17113
+ ];
17114
+ const seen = /* @__PURE__ */ new Set();
17115
+ return places.filter(({ dir }) => {
17116
+ const key = resolve(dir);
17117
+ if (seen.has(key)) return false;
17118
+ seen.add(key);
17119
+ return true;
17120
+ });
16116
17121
  }
16117
- /**
16118
- * The implementations of interface `id` a consumer may load, each checked as `check` would on its
16119
- * composed document, and every candidate left out with why (R35). A candidate is a name in the
16120
- * interface's namespace (`<id>` or `<id>.<key>`) or a reference that declares `implements: <id>@<n>`:
16121
- * a plugin's, pinned by its qualified name, or, for a `many` interface, a `repo` or `user` one,
16122
- * pinned by its bare name. When a `repo` or `user` implementation answers the bare name, a plugin's
16123
- * of that name is left out (C1, J3), since loading the bare name returns the other. Plugins holding
16124
- * a name no `extends` orders are all left out until a customizer picks one (R87). A skill's default
16125
- * is never listed: it has no qualified name to pin. An unknown interface throws.
16126
- */
16127
- function referenceImplementations(id, source, options = {}) {
16128
- const { accept: acceptOption, ...rest } = options;
16129
- const resolveOptions = {
16130
- ...rest,
16131
- projectSections: false
17122
+ /** The new names an older name takes: from each plugin's rename table, and from each owner's `aliases` (R124). */
17123
+ function renameSources(layers) {
17124
+ const table = renameTable(layers);
17125
+ const aliases = aliasIndex(layers);
17126
+ return (name) => {
17127
+ if (aliases.owned.has(name)) return [];
17128
+ const found = /* @__PURE__ */ new Map();
17129
+ const add = (to, by) => found.set(to, (found.get(to) ?? /* @__PURE__ */ new Set()).add(by));
17130
+ for (const entry of table.byOld.get(name) ?? []) add(entry.to, `plugin ${entry.plugin}'s ${renameTableFile}`);
17131
+ for (const owner of aliases.byAlias.get(name) ?? []) add(owner.name, `the aliases of ${owner.tier === "plugin" ? `plugin ${owner.plugin}` : `the default of ${owner.skill}`}`);
17132
+ return [...found].map(([to, by]) => ({
17133
+ to,
17134
+ by: [...by].join(" and ")
17135
+ }));
16132
17136
  };
16133
- const ifaceName = parseReferenceName(`${id}${interfaceSuffix}`);
16134
- if (ifaceName.invalid || ifaceName.plugin !== void 0) throw new ReferenceFailure("interface-unknown", `"${id}" is not an interface id.`);
16135
- const lookup = interfaceLookup(source, rest);
16136
- const iface = lookup(id);
16137
- const value = iface.value;
16138
- if (!value) throw new ReferenceFailure("interface-unknown", iface.failure?.message ?? `no interface ${id}`);
16139
- const aliases = aliasIndex(source);
16140
- const ids = [value.id, ...(aliases.interfaces.get(value.id) ?? []).map(({ alias }) => alias)];
16141
- const accept = [...acceptOption?.length ? acceptOption : value.versions];
16142
- const report = {
16143
- interface: value.id,
16144
- cardinality: value.cardinality,
16145
- accept,
16146
- implementations: [],
16147
- excluded: [],
16148
- warnings: []
16149
- };
16150
- const seen = /* @__PURE__ */ new Set();
16151
- const warn = (warnings) => {
16152
- for (const warning of warnings) {
16153
- const key = `${warning.code} ${warning.message}`;
16154
- if (!seen.has(key)) report.warnings.push(warning);
16155
- seen.add(key);
17137
+ }
17138
+ /** Whether a folder holds any file answering `name` in its tier (R60's forms). */
17139
+ function holdsName(place, name) {
17140
+ const own = place.tier === "default" ? defaultSuffix : "";
17141
+ return [
17142
+ `${name}${own}.md`,
17143
+ ...place.tier === "default" ? [] : [join(name, "README.md")],
17144
+ ...dataSuffixes.map((extension) => `${name}${own}${extension}`),
17145
+ `${name}${own}${metadataSuffix}.md`
17146
+ ].some((file) => existsSync(join(place.dir, file)));
17147
+ }
17148
+ /**
17149
+ * Finds every file in a legacy form (K37), or under a name a plugin renamed, and returns the plan
17150
+ * (R121). With `write`, it applies each change that is not manual. It reads the `repo` tier, the
17151
+ * `user` tier, and every project section below the root, or only a plugin's or a skill's source
17152
+ * folder in the repository (R123), and never changes a managed folder or an installed copy.
17153
+ */
17154
+ function migrateReferences(layers, options) {
17155
+ const root = resolve(options.root ?? layers.root ?? process.cwd());
17156
+ const names = options.names?.length ? new Set(options.names.map((name) => bareName(name).name)) : void 0;
17157
+ const places = placesOf(layers, root, options);
17158
+ const targetsOf = renameSources(layers);
17159
+ const installed = new Set(layers.layers.flatMap(({ tier, plugin, skipped }) => tier === "plugin" && plugin && !skipped ? [plugin] : []));
17160
+ const rows = [];
17161
+ const operations = [];
17162
+ for (const place of places) {
17163
+ const where = place.folder === void 0 ? {} : { folder: place.folder };
17164
+ const renames = place.tier === "repo" || place.tier === "user" || place.tier === "project";
17165
+ /** New names this run gives files in the folder, so a second file renamed to one is manual. */
17166
+ const claimed = /* @__PURE__ */ new Set();
17167
+ const held = heldFiles(place);
17168
+ if (place.tier === "default") held.push(...oldDefaults(place, names, held));
17169
+ for (const file of held) {
17170
+ const targets = renames ? targetsOf(file.name) : [];
17171
+ if (names && !names.has(file.name) && !targets.some(({ to }) => names.has(to))) continue;
17172
+ const base = {
17173
+ path: file.path,
17174
+ tier: place.tier,
17175
+ ...where,
17176
+ name: file.name
17177
+ };
17178
+ const operation = { path: file.path };
17179
+ if (file.kind === "markdown") {
17180
+ const raw = tryRead$2(file.path) ?? "";
17181
+ const { metadata, body } = parseDocument(raw, () => {});
17182
+ const { findings, keys } = legacyRewrite(metadata, body, place.tier === "project" ? "project" : "contribution", file.name);
17183
+ for (const finding of findings) rows.push(rowOf(base, finding));
17184
+ if (Object.keys(keys).length) operation.content = editFrontmatter(raw, keys);
17185
+ }
17186
+ if (file.suffix === ".md" && place.tier === "default") {
17187
+ const to = join(place.dir, `${file.name}${defaultSuffix}.md`);
17188
+ const finding = defaultLayoutFinding(file.name);
17189
+ if (existsSync(to)) {
17190
+ const { to: _, ...kept } = finding;
17191
+ rows.push(rowOf(base, {
17192
+ ...kept,
17193
+ manual: `${file.name}${defaultSuffix}.md is there too and answers the name; the old file is not read, so delete it by hand once nothing in it is needed`
17194
+ }));
17195
+ } else {
17196
+ rows.push(rowOf(base, finding));
17197
+ Object.assign(operation, {
17198
+ moveFrom: file.path,
17199
+ moveTo: to
17200
+ });
17201
+ }
17202
+ }
17203
+ if (targets.length) renameRow(place, file, targets, claimed, base, rows, operation);
17204
+ if (operation.content !== void 0 || operation.moveTo !== void 0) operations.push(operation);
17205
+ }
17206
+ if (renames) for (const found of qualifiedOverrides(place.dir)) {
17207
+ if (names && !names.has(found.name)) continue;
17208
+ const operation = qualifiedRow(place, found, installed, claimed, {
17209
+ path: found.path,
17210
+ tier: place.tier,
17211
+ ...where,
17212
+ name: found.name
17213
+ }, rows);
17214
+ if (operation) operations.push(operation);
16156
17215
  }
17216
+ }
17217
+ let written = false;
17218
+ if (options.write) for (const operation of operations) {
17219
+ if (operation.content !== void 0) writeFileSync(operation.path, operation.content);
17220
+ if (operation.moveFrom !== void 0 && operation.moveTo !== void 0) renameSync(operation.moveFrom, operation.moveTo);
17221
+ if (operation.prune !== void 0) pruneEmpty(operation.prune);
17222
+ written = true;
17223
+ }
17224
+ return {
17225
+ rows,
17226
+ written,
17227
+ warnings: [...renameTable(layers).warnings]
16157
17228
  };
16158
- if (iface.renamed !== void 0) warn([{
16159
- code: "reference-renamed",
16160
- message: `${id} is an older id of ${value.id}; ask for ${value.id} instead`
16161
- }]);
16162
- const inNamespace = (name) => ids.some((each) => name === each || name.startsWith(`${each}.`));
16163
- /** Whether a resolved document is a candidate at all: it declares the interface, or its name is in the namespace. */
16164
- const relevant = (name, resolved) => {
16165
- const declared = readImplements(resolved.metadata ?? {}).entry?.id;
16166
- if (declared !== void 0 && ids.includes(declared)) return true;
16167
- return inNamespace(name) && !ids.some((each) => belongsElsewhere(name, declared, each));
17229
+ }
17230
+ function rowOf(base, finding) {
17231
+ const row = {
17232
+ ...base,
17233
+ form: finding.form,
17234
+ from: finding.from
16168
17235
  };
16169
- const consider = ({ pin, name, resolved, tier }) => {
16170
- warn(resolved.warnings);
16171
- const declared = readImplements(resolved.metadata ?? {}).entry;
16172
- if (!declared || !ids.includes(declared.id)) {
16173
- const shape = fitsNamespace(value.cardinality, ids, name) ? "" : ` and is not named ${value.cardinality === "one" ? value.id : `${value.id}.<key>`}`;
16174
- report.excluded.push({
16175
- name: pin,
16176
- reasons: [`declares no implements ${value.id}@<version>${shape}`]
16177
- });
16178
- return;
16179
- }
16180
- const checked = checkResolved(resolved, lookup, {
16181
- id: value.id,
16182
- versions: accept
16183
- });
16184
- warn(checked.warnings);
16185
- if (checked.failures.length) {
16186
- report.excluded.push({
16187
- name: pin,
16188
- reasons: checked.failures.map(({ check, message }) => `${check}: ${message}`)
16189
- });
16190
- return;
16191
- }
16192
- report.implementations.push({
16193
- pin,
16194
- description: firstLine(resolved.metadata?.["description"]),
16195
- unsupported: checked.unsupported,
17236
+ const note = finding.note === void 0 ? "" : `; ${finding.reads}, and ${finding.note}`;
17237
+ if (finding.to !== void 0) row.to = `${finding.to}${note}`;
17238
+ if (finding.manual !== void 0) row.manual = `${finding.manual}${note}`;
17239
+ return row;
17240
+ }
17241
+ /**
17242
+ * A skill's defaults in the old layout, `references/<name>.md`, for the names given (UC22.7): the
17243
+ * folder's other files may be the skill's own documents, so no name given recognizes none.
17244
+ */
17245
+ function oldDefaults(place, names, held) {
17246
+ if (!names) return [];
17247
+ return [...names].flatMap((name) => {
17248
+ const path = join(place.dir, `${name}.md`);
17249
+ if (tryRead$2(path) === void 0 || held.some((file) => file.path === path)) return [];
17250
+ return [{
16196
17251
  name,
16197
- tier,
16198
- version: declared.version
16199
- });
17252
+ path,
17253
+ suffix: ".md",
17254
+ kind: "markdown",
17255
+ folderForm: false
17256
+ }];
17257
+ });
17258
+ }
17259
+ /** The rename row of a file under an older name (R124), and the move that applies it. */
17260
+ function renameRow(place, file, targets, claimed, base, rows, operation) {
17261
+ const from = file.folderForm ? `${file.name}/` : `${file.name}${file.suffix}`;
17262
+ const row = {
17263
+ ...base,
17264
+ form: "renamed",
17265
+ from,
17266
+ by: targets.map(({ by }) => by).join("; ")
16200
17267
  };
16201
- const qualified = (plugin, name) => resolveReference(parseReferenceName(`${plugin}/${name}`), source, resolveOptions);
16202
- const names = [...new Set(referenceNames(source).map((name) => shownName(name, aliases)))];
16203
- for (const name of names) {
16204
- if (interfaceIdOf(name) !== void 0) continue;
16205
- const resolved = resolveReference(name, source, resolveOptions);
16206
- if (resolved.status === "ambiguous") {
16207
- for (const option of resolved.options ?? []) {
16208
- if (option === "default") continue;
16209
- const copy = resolveReference(parseReferenceName(option), source, resolveOptions);
16210
- if (copy.status !== "found" || !relevant(name, copy)) continue;
16211
- warn(copy.warnings);
16212
- report.excluded.push({
16213
- name: option,
16214
- reasons: [`ambiguous: ${resolved.message}; each is left out until a customizer picks one`]
16215
- });
16216
- }
16217
- continue;
16218
- }
16219
- if (resolved.status !== "found" || !relevant(name, resolved)) continue;
16220
- const plugins = holders(resolved);
16221
- const tier = value.cardinality === "many" ? ownTier(resolved) : void 0;
16222
- if (tier !== void 0) {
16223
- consider({
16224
- pin: name,
16225
- name,
16226
- resolved,
16227
- tier
16228
- });
16229
- for (const plugin of plugins) report.excluded.push({
16230
- name: `${plugin}/${name}`,
16231
- reasons: [`the ${tier} implementation answers ${name}; a consumer loads it by that name`]
16232
- });
16233
- continue;
16234
- }
16235
- const pick = resolved.trace.stack.filter((target) => target !== "default").at(-1);
16236
- if (pick === void 0) continue;
16237
- consider({
16238
- pin: `${pick}/${name}`,
16239
- name,
16240
- resolved: qualified(pick, name),
16241
- tier: "plugin"
17268
+ if (targets.length > 1) {
17269
+ rows.push({
17270
+ ...row,
17271
+ manual: `it is renamed differently: ${targets.map(({ to, by }) => `"${to}" by ${by}`).join(", ")}; rename it by hand to the one that applies`
16242
17272
  });
16243
- for (const plugin of plugins) {
16244
- if (plugin === pick) continue;
16245
- report.excluded.push({
16246
- name: `${plugin}/${name}`,
16247
- reasons: [`${name} loads the stack of ${pick}, the pick; load ${pick}/${name}`]
16248
- });
16249
- }
17273
+ return;
16250
17274
  }
16251
- return report;
17275
+ const { to } = targets[0];
17276
+ const companion = file.kind === "metadata" || file.kind === "data";
17277
+ if (holdsName(place, to) && !claimed.has(to) || claimed.has(to) && !companion) {
17278
+ rows.push({
17279
+ ...row,
17280
+ manual: `"${to}" already has a file in that folder; merge the two by hand, then delete this one`
17281
+ });
17282
+ return;
17283
+ }
17284
+ claimed.add(to);
17285
+ const into = file.folderForm ? `${to}/` : `${to}${file.suffix}`;
17286
+ rows.push({
17287
+ ...row,
17288
+ to: into
17289
+ });
17290
+ const moveFrom = file.folderForm ? dirname(file.path) : file.path;
17291
+ Object.assign(operation, {
17292
+ moveFrom,
17293
+ moveTo: join(place.dir, file.folderForm ? to : `${to}${file.suffix}`)
17294
+ });
16252
17295
  }
16253
- /** Whether a name fits the interface's cardinality under its id or one of its older ids. */
16254
- function fitsNamespace(cardinality, ids, name) {
16255
- return ids.some((id) => cardinality === "one" ? name === id : name.startsWith(`${id}.`) && name.length > id.length + 1);
17296
+ function pruneEmpty(dir) {
17297
+ try {
17298
+ if (!readdirSync(dir).length) rmdirSync(dir);
17299
+ } catch {}
17300
+ }
17301
+ /**
17302
+ * The row of a qualified-name override (K37), and the move that applies it: `<plugin>/<name>.md`
17303
+ * becomes `<name>.md` beside the subfolder, extending the plugin. It is manual in a project folder,
17304
+ * for a plugin the plugin tier does not hold, beside a file that already answers the name, and for a
17305
+ * file that writes its own `extends` or `overrides`.
17306
+ */
17307
+ function qualifiedRow(place, found, installed, claimed, base, rows) {
17308
+ const { plugin, name } = found;
17309
+ const finding = qualifiedOverrideFinding(found, place.tier === "project" ? "project" : "contribution");
17310
+ const { to, ...kept } = finding;
17311
+ const manual = (why) => {
17312
+ rows.push(rowOf(base, {
17313
+ ...kept,
17314
+ manual: why
17315
+ }));
17316
+ };
17317
+ if (to === void 0) return manual(finding.manual);
17318
+ if (!installed.has(plugin)) return manual(`plugin ${plugin} is not installed; it would become ${to}, extending a plugin that is not there: install ${plugin} and run migrate again, or delete this file`);
17319
+ if (holdsName(place, name) || claimed.has(name)) return manual(`"${name}" already has a file in that folder; move this file's sections into it and add ${plugin} to its extends, then delete this one`);
17320
+ const raw = tryRead$2(found.path) ?? "";
17321
+ const { metadata, body } = parseDocument(raw, () => {});
17322
+ const own = ["extends", "overrides"].find((key) => key in metadata);
17323
+ if (own !== void 0) return manual(`it also writes \`${own}\`; move it to ${name}.md by hand, with an extends that names ${plugin}`);
17324
+ claimed.add(name);
17325
+ rows.push(rowOf(base, finding));
17326
+ const { findings, keys } = legacyRewrite(metadata, body, "contribution", name);
17327
+ for (const other of findings) rows.push(rowOf(base, other));
17328
+ return {
17329
+ path: found.path,
17330
+ content: editFrontmatter(raw, {
17331
+ ...keys,
17332
+ extends: [plugin]
17333
+ }),
17334
+ moveFrom: found.path,
17335
+ moveTo: join(place.dir, `${name}.md`),
17336
+ prune: dirname(found.path)
17337
+ };
16256
17338
  }
16257
17339
  /**
16258
17340
  * The files a customization of `name` is written to, `repo` and `user` and, with `in`, that folder's
@@ -16309,129 +17391,436 @@ function whereReference(input, source, { root, display = (path) => path, in: fol
16309
17391
  return report;
16310
17392
  }
16311
17393
  //#endregion
16312
- //#region src/dep-plugins/dep-plugins.ts
16313
- /** Generic marketplace names: a second registration under one of these replaces the first, silently. */
16314
- const GENERIC_NAMES = /* @__PURE__ */ new Set([
16315
- "deps",
16316
- "dependencies",
16317
- "local",
16318
- "plugins",
16319
- "marketplace",
16320
- "plugin",
16321
- "default"
16322
- ]);
16323
- function isGenericName(name) {
16324
- return GENERIC_NAMES.has(name.toLowerCase());
16325
- }
17394
+ //#region src/harness-registry/harness-registry.ts
16326
17395
  /**
16327
- * Folds a scope into the name (`@acme/web` → `acme-web`) rather than dropping it — the unscoped
16328
- * part collides more often.
17396
+ * Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
17397
+ * drop a projection it may need.
16329
17398
  */
16330
- function marketplaceName(packageName) {
16331
- return packageName.replace(/^@/, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-_.]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "agent-plugins";
17399
+ function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
17400
+ const listed = directories ?? [];
17401
+ const [first] = listed;
17402
+ if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
17403
+ return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
16332
17404
  }
16333
17405
  /**
16334
- * Derived in full every run, never merged with what's on disk. Entries are sorted so unchanged
16335
- * inputs serialize identically, which `--check` relies on.
17406
+ * The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
17407
+ * reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
17408
+ * E-GEM-03, E-COPILOT-04, E-CUR-08).
16336
17409
  */
16337
- function buildCatalog({ name, owner, plugins }) {
16338
- const entries = [...plugins].sort((a, b) => a.plugin.localeCompare(b.plugin)).map(({ plugin, package: pkg, version, description }) => {
16339
- const source = {
16340
- source: "npm",
16341
- package: pkg,
16342
- version
16343
- };
16344
- return description ? {
16345
- name: plugin,
16346
- source,
16347
- description
16348
- } : {
16349
- name: plugin,
16350
- source
16351
- };
16352
- });
16353
- return {
16354
- name,
16355
- owner: { name: owner },
16356
- description: "Plugins provided by this repository’s dependencies. Generated — do not edit.",
16357
- plugins: entries
16358
- };
16359
- }
16360
- /** Includes the trailing newline. */
16361
- function serializeCatalog(catalog) {
16362
- return `${JSON.stringify(catalog, null, " ")}\n`;
16363
- }
16364
- //#endregion
16365
- //#region src/dep-plugins/resolve.ts
16366
- function readJson$1(path) {
16367
- try {
16368
- return JSON.parse(readFileSync(path, "utf8"));
16369
- } catch {
16370
- return;
16371
- }
16372
- }
16373
- /** A manifest lives at the package root — the Agent Plugins Specification's pinned location. */
16374
- function readDepPlugin(pkg, fromManifest) {
16375
- const dir = packageDir(pkg, fromManifest);
16376
- if (!dir) return {
16377
- ok: false,
16378
- package: pkg,
16379
- reason: "unresolved"
16380
- };
16381
- const manifest = readJson$1(join(dir, "plugin.json"));
16382
- if (!manifest || typeof manifest !== "object") return {
16383
- ok: false,
16384
- package: pkg,
16385
- reason: "no-manifest"
16386
- };
16387
- const record = manifest;
16388
- const name = typeof record["name"] === "string" ? record["name"] : void 0;
16389
- if (!name) return {
16390
- ok: false,
16391
- package: pkg,
16392
- reason: "unnamed"
16393
- };
16394
- const installed = readJson$1(join(dir, "package.json"));
16395
- const version = typeof installed?.version === "string" ? installed.version : void 0;
16396
- if (!version) return {
16397
- ok: false,
16398
- package: pkg,
16399
- reason: "no-manifest"
16400
- };
16401
- const description = typeof record["description"] === "string" ? record["description"] : void 0;
16402
- return {
16403
- ok: true,
16404
- plugin: description ? {
16405
- package: pkg,
16406
- plugin: name,
16407
- version,
16408
- description
16409
- } : {
16410
- package: pkg,
16411
- plugin: name,
16412
- version
17410
+ const globalInstructions = ".agents/AGENTS.md";
17411
+ /**
17412
+ * Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
17413
+ * (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
17414
+ */
17415
+ const harnessRegistry = [
17416
+ {
17417
+ name: "claude-code",
17418
+ project: {
17419
+ detect: ".claude",
17420
+ ...skillsProjection("claude-code", "project"),
17421
+ shadowedBy: [
17422
+ "CLAUDE.md",
17423
+ ".claude/CLAUDE.md",
17424
+ "CLAUDE.local.md"
17425
+ ],
17426
+ mcpConfig: {
17427
+ path: ".mcp.json",
17428
+ key: "mcpServers",
17429
+ format: "json",
17430
+ dialect: "claude-code"
17431
+ },
17432
+ nonstandard: [
17433
+ {
17434
+ path: ".claude/commands",
17435
+ shape: "directory",
17436
+ kind: "command"
17437
+ },
17438
+ {
17439
+ path: ".claude/rules",
17440
+ shape: "directory",
17441
+ kind: "rule"
17442
+ },
17443
+ {
17444
+ path: ".claude/agents",
17445
+ shape: "directory",
17446
+ kind: "subagent"
17447
+ }
17448
+ ]
17449
+ },
17450
+ user: {
17451
+ detect: ".claude",
17452
+ ...skillsProjection("claude-code", "user"),
17453
+ relocatedBy: "CLAUDE_CONFIG_DIR",
17454
+ emptyRelocatesToStart: true,
17455
+ instructionBridge: {
17456
+ kind: "import",
17457
+ path: ".claude/CLAUDE.md",
17458
+ line: `@~/${globalInstructions}`
17459
+ }
16413
17460
  }
16414
- };
17461
+ },
17462
+ {
17463
+ name: "cursor",
17464
+ project: {
17465
+ detect: ".cursor",
17466
+ ...skillsProjection("cursor", "project"),
17467
+ mcpConfig: {
17468
+ path: ".cursor/mcp.json",
17469
+ key: "mcpServers",
17470
+ format: "json",
17471
+ dialect: "cursor"
17472
+ },
17473
+ nonstandard: [
17474
+ {
17475
+ path: ".cursorrules",
17476
+ shape: "file",
17477
+ kind: "instructions"
17478
+ },
17479
+ {
17480
+ path: ".cursor/rules",
17481
+ shape: "directory",
17482
+ kind: "rule"
17483
+ },
17484
+ {
17485
+ path: ".cursor/commands",
17486
+ shape: "directory",
17487
+ kind: "command"
17488
+ },
17489
+ {
17490
+ path: ".cursor/skills",
17491
+ shape: "directory",
17492
+ kind: "skill"
17493
+ }
17494
+ ]
17495
+ },
17496
+ user: {
17497
+ detect: ".cursor",
17498
+ ...skillsProjection("cursor", "user")
17499
+ }
17500
+ },
17501
+ {
17502
+ name: "codex",
17503
+ project: {
17504
+ detect: ".codex",
17505
+ ...skillsProjection("codex", "project"),
17506
+ mcpConfig: {
17507
+ path: ".codex/config.toml",
17508
+ key: "mcp_servers",
17509
+ format: "toml",
17510
+ dialect: "codex",
17511
+ shared: true
17512
+ },
17513
+ nonstandard: [{
17514
+ path: ".codex/skills",
17515
+ shape: "directory",
17516
+ kind: "skill"
17517
+ }]
17518
+ },
17519
+ user: {
17520
+ detect: ".codex",
17521
+ ...skillsProjection("codex", "user"),
17522
+ instructionBridge: {
17523
+ kind: "symlink",
17524
+ path: ".codex/AGENTS.md"
17525
+ },
17526
+ shadowedBy: [".codex/AGENTS.override.md"],
17527
+ relocatedBy: "CODEX_HOME"
17528
+ }
17529
+ },
17530
+ {
17531
+ name: "copilot-cli",
17532
+ project: {
17533
+ detect: ".github/skills",
17534
+ ...skillsProjection("copilot-cli", "project"),
17535
+ nonstandard: [
17536
+ {
17537
+ path: ".github/copilot-instructions.md",
17538
+ shape: "file",
17539
+ kind: "instructions"
17540
+ },
17541
+ {
17542
+ path: ".github/instructions",
17543
+ shape: "directory",
17544
+ kind: "instructions"
17545
+ },
17546
+ {
17547
+ path: ".github/skills",
17548
+ shape: "directory",
17549
+ kind: "skill"
17550
+ }
17551
+ ]
17552
+ },
17553
+ user: {
17554
+ detect: ".copilot",
17555
+ ...skillsProjection("copilot-cli", "user"),
17556
+ relocatedBy: "COPILOT_HOME",
17557
+ instructionBridge: {
17558
+ kind: "symlink",
17559
+ path: ".copilot/copilot-instructions.md"
17560
+ }
17561
+ }
17562
+ },
17563
+ {
17564
+ name: "gemini-cli",
17565
+ project: {
17566
+ detect: ".gemini",
17567
+ ...skillsProjection("gemini-cli", "project"),
17568
+ instructionBridge: {
17569
+ kind: "settings-entry",
17570
+ path: ".gemini/settings.json",
17571
+ key: "context.fileName"
17572
+ },
17573
+ mcpConfig: {
17574
+ path: ".gemini/settings.json",
17575
+ key: "mcpServers",
17576
+ format: "json",
17577
+ dialect: "gemini-cli",
17578
+ shared: true
17579
+ },
17580
+ nonstandard: [{
17581
+ path: "GEMINI.md",
17582
+ shape: "file",
17583
+ kind: "instructions"
17584
+ }, {
17585
+ path: ".gemini/skills",
17586
+ shape: "directory",
17587
+ kind: "skill"
17588
+ }]
17589
+ },
17590
+ user: {
17591
+ detect: ".gemini",
17592
+ ...skillsProjection("gemini-cli", "user"),
17593
+ instructionBridge: {
17594
+ kind: "symlink",
17595
+ path: ".gemini/GEMINI.md"
17596
+ }
17597
+ }
17598
+ },
17599
+ {
17600
+ name: "devin-desktop",
17601
+ project: { detect: ".devin" }
17602
+ },
17603
+ {
17604
+ name: "windsurf",
17605
+ project: {
17606
+ detect: ".windsurf",
17607
+ skillsDirectory: ".windsurf/skills",
17608
+ nonstandard: [{
17609
+ path: ".windsurfrules",
17610
+ shape: "file",
17611
+ kind: "instructions"
17612
+ }, {
17613
+ path: ".windsurf/rules",
17614
+ shape: "directory",
17615
+ kind: "rule"
17616
+ }]
17617
+ },
17618
+ deprecated: "devin-desktop"
17619
+ }
17620
+ ];
17621
+ const harnessNames = new Set(harnessRegistry.map((harness) => harness.name));
17622
+ function isHarnessName(value) {
17623
+ return harnessNames.has(value);
16415
17624
  }
16416
17625
  /**
16417
- * True under Yarn PnP, where packages live in zip archives rather than directories — nothing here
16418
- * has an installable directory.
17626
+ * An unknown name throws rather than being dropped — silently falling back to the defaults would
17627
+ * misreport what was diagnosed.
16419
17628
  */
16420
- function isPnp() {
16421
- return Boolean(process.versions["pnp"]);
17629
+ function parseHarnesses(value) {
17630
+ const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
17631
+ const unsupported = requested.filter((name) => !isHarnessName(name));
17632
+ if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
17633
+ return requested.filter(isHarnessName);
17634
+ }
17635
+ /** Enabled whether or not the repository already contains their directories. */
17636
+ const defaultHarnesses = ["claude-code", "cursor"];
17637
+ function directoryExists(path) {
17638
+ try {
17639
+ return lstatSync(path).isDirectory();
17640
+ } catch {
17641
+ return false;
17642
+ }
17643
+ }
17644
+ /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
17645
+ function selectHarnesses(root, preferred) {
17646
+ const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
17647
+ return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
16422
17648
  }
16423
17649
  //#endregion
16424
- //#region src/dep-plugins/generate.ts
17650
+ //#region src/command-output/command-output.ts
16425
17651
  /**
16426
- * Not the repository root's `.claude-plugin/` — that's the repo's own published catalog; merging
16427
- * would leak dependency plugins into it.
17652
+ * AXI §6: a failure is a coded error on stdout, in the caller's format, with a `help` line saying
17653
+ * what to do next. A `CliError` the command already built is rethrown untouched; anything else
17654
+ * becomes `command-failed`, exit code 1.
16428
17655
  */
16429
- const CATALOG_DIR = join(".agents", "buddy-agent-harness");
16430
- const CATALOG_PATH = join(CATALOG_DIR, ".claude-plugin", "marketplace.json");
16431
- function readManifest(path) {
16432
- return JSON.parse(readFileSync(path, "utf8"));
17656
+ function fail(error, fallback, help) {
17657
+ if (isCliError(error)) throw error;
17658
+ throw new CliError(error instanceof Error ? error.message : fallback, {
17659
+ code: "command-failed",
17660
+ exitCode: exitCodes.error,
17661
+ ...help ? { help } : {},
17662
+ cause: error
17663
+ });
16433
17664
  }
16434
- function deriveCatalog(root) {
17665
+ /**
17666
+ * An answer that comes with a failing exit code. The cli prints only an answer a command returns, and
17667
+ * a thrown error replaces the return, so the answer is printed first.
17668
+ */
17669
+ function answerThenFail(answer, format) {
17670
+ const text = printAnswer(answer, format);
17671
+ if (text !== void 0) process.stdout.write(text);
17672
+ throw CliError.reported(exitCodes.error);
17673
+ }
17674
+ /**
17675
+ * AXI §10: collapses the home directory to `~` so a path in a report is portable to another
17676
+ * machine.
17677
+ */
17678
+ function collapseHome$1(home, path) {
17679
+ return home && path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
17680
+ }
17681
+ function displayBinPath(home, executable) {
17682
+ return executable ? collapseHome$1(home, executable) : "buddy-agent-harness";
17683
+ }
17684
+ /**
17685
+ * `parseHarnesses` for a command's `--harness`: an unsupported name is a usage error, exit code 2.
17686
+ * The registry stays free of clibuilder, which the library entry treats as an optional peer.
17687
+ */
17688
+ function parseHarnessOption(value) {
17689
+ try {
17690
+ return parseHarnesses(value);
17691
+ } catch (error) {
17692
+ throw new CliError(error.message, {
17693
+ code: "unsupported-harness",
17694
+ exitCode: exitCodes.usage,
17695
+ help: "Pass --harness as a comma-separated list of the supported names.",
17696
+ cause: error
17697
+ });
17698
+ }
17699
+ }
17700
+ //#endregion
17701
+ //#region src/dep-plugins/dep-plugins.ts
17702
+ /** Generic marketplace names: a second registration under one of these replaces the first, silently. */
17703
+ const GENERIC_NAMES = /* @__PURE__ */ new Set([
17704
+ "deps",
17705
+ "dependencies",
17706
+ "local",
17707
+ "plugins",
17708
+ "marketplace",
17709
+ "plugin",
17710
+ "default"
17711
+ ]);
17712
+ function isGenericName(name) {
17713
+ return GENERIC_NAMES.has(name.toLowerCase());
17714
+ }
17715
+ /**
17716
+ * Folds a scope into the name (`@acme/web` → `acme-web`) rather than dropping it — the unscoped
17717
+ * part collides more often.
17718
+ */
17719
+ function marketplaceName(packageName) {
17720
+ return packageName.replace(/^@/, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-_.]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "agent-plugins";
17721
+ }
17722
+ /**
17723
+ * Derived in full every run, never merged with what's on disk. Entries are sorted so unchanged
17724
+ * inputs serialize identically, which `--check` relies on.
17725
+ */
17726
+ function buildCatalog({ name, owner, plugins }) {
17727
+ const entries = [...plugins].sort((a, b) => a.plugin.localeCompare(b.plugin)).map(({ plugin, package: pkg, version, description }) => {
17728
+ const source = {
17729
+ source: "npm",
17730
+ package: pkg,
17731
+ version
17732
+ };
17733
+ return description ? {
17734
+ name: plugin,
17735
+ source,
17736
+ description
17737
+ } : {
17738
+ name: plugin,
17739
+ source
17740
+ };
17741
+ });
17742
+ return {
17743
+ name,
17744
+ owner: { name: owner },
17745
+ description: "Plugins provided by this repository’s dependencies. Generated — do not edit.",
17746
+ plugins: entries
17747
+ };
17748
+ }
17749
+ /** Includes the trailing newline. */
17750
+ function serializeCatalog(catalog) {
17751
+ return `${JSON.stringify(catalog, null, " ")}\n`;
17752
+ }
17753
+ //#endregion
17754
+ //#region src/dep-plugins/resolve.ts
17755
+ function readJson$1(path) {
17756
+ try {
17757
+ return JSON.parse(readFileSync(path, "utf8"));
17758
+ } catch {
17759
+ return;
17760
+ }
17761
+ }
17762
+ /** A manifest lives at the package root — the Agent Plugins Specification's pinned location. */
17763
+ function readDepPlugin(pkg, fromManifest) {
17764
+ const dir = packageDir(pkg, fromManifest);
17765
+ if (!dir) return {
17766
+ ok: false,
17767
+ package: pkg,
17768
+ reason: "unresolved"
17769
+ };
17770
+ const manifest = readJson$1(join(dir, "plugin.json"));
17771
+ if (!manifest || typeof manifest !== "object") return {
17772
+ ok: false,
17773
+ package: pkg,
17774
+ reason: "no-manifest"
17775
+ };
17776
+ const record = manifest;
17777
+ const name = typeof record["name"] === "string" ? record["name"] : void 0;
17778
+ if (!name) return {
17779
+ ok: false,
17780
+ package: pkg,
17781
+ reason: "unnamed"
17782
+ };
17783
+ const installed = readJson$1(join(dir, "package.json"));
17784
+ const version = typeof installed?.version === "string" ? installed.version : void 0;
17785
+ if (!version) return {
17786
+ ok: false,
17787
+ package: pkg,
17788
+ reason: "no-manifest"
17789
+ };
17790
+ const description = typeof record["description"] === "string" ? record["description"] : void 0;
17791
+ return {
17792
+ ok: true,
17793
+ plugin: description ? {
17794
+ package: pkg,
17795
+ plugin: name,
17796
+ version,
17797
+ description
17798
+ } : {
17799
+ package: pkg,
17800
+ plugin: name,
17801
+ version
17802
+ }
17803
+ };
17804
+ }
17805
+ /**
17806
+ * True under Yarn PnP, where packages live in zip archives rather than directories — nothing here
17807
+ * has an installable directory.
17808
+ */
17809
+ function isPnp() {
17810
+ return Boolean(process.versions["pnp"]);
17811
+ }
17812
+ //#endregion
17813
+ //#region src/dep-plugins/generate.ts
17814
+ /**
17815
+ * Not the repository root's `.claude-plugin/` — that's the repo's own published catalog; merging
17816
+ * would leak dependency plugins into it.
17817
+ */
17818
+ const CATALOG_DIR = join(".agents", "buddy-agent-harness");
17819
+ const CATALOG_PATH = join(CATALOG_DIR, ".claude-plugin", "marketplace.json");
17820
+ function readManifest(path) {
17821
+ return JSON.parse(readFileSync(path, "utf8"));
17822
+ }
17823
+ function deriveCatalog(root) {
16435
17824
  const manifestPath = join(root, "package.json");
16436
17825
  const manifest = readManifest(manifestPath);
16437
17826
  const plugins = [];
@@ -16685,327 +18074,78 @@ const depPluginsCommand = command({
16685
18074
  type: t.boolean().optional()
16686
18075
  }
16687
18076
  },
18077
+ examples: [{
18078
+ command: "buddy-agent-harness dep-plugins",
18079
+ description: "Write the marketplace catalog for dependency plugins"
18080
+ }, {
18081
+ command: "buddy-agent-harness dep-plugins --check",
18082
+ description: "Fail when the catalog is stale (CI)"
18083
+ }],
16688
18084
  run(args) {
16689
18085
  try {
16690
18086
  const root = args.root ?? process.cwd();
16691
- const derived = deriveCatalog(root);
16692
- const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
16693
- const home = homedir();
16694
- const notes = notesFor(derived);
16695
- const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
16696
- const harness = [];
16697
- const actions = [];
16698
- for (const runtime of detected) {
16699
- const state = runtime.state(home);
16700
- const delta = reconcile({
16701
- catalog: derived.catalog,
16702
- catalogDir: CATALOG_DIR,
16703
- root: resolve(root),
16704
- installed: state.installed,
16705
- marketplaces: state.marketplaces,
16706
- scoped: runtime.scoped
16707
- });
16708
- if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
16709
- harness.push({
16710
- runtime: runtime.name,
16711
- status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
16712
- });
16713
- for (const action of delta.actions) actions.push({
16714
- runtime: runtime.name,
16715
- do: commandFor(runtime, action, derived.catalog.name),
16716
- subject: action.subject,
16717
- why: action.detail
16718
- });
16719
- }
16720
- if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
16721
- const answer = result({
16722
- catalog: CATALOG_PATH,
16723
- marketplace: derived.catalog.name,
16724
- outcome,
16725
- plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
16726
- name: plugin,
16727
- package: pkg,
16728
- version
16729
- })) : "0 dependencies ship a plugin — nothing to install",
16730
- harness: harness.length ? harness : "no supported harness detected on this machine",
16731
- actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
16732
- ...notes.length ? { notes } : {}
16733
- });
16734
- return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
16735
- } catch (error) {
16736
- return fail(error, "Catalog generation failed.");
16737
- }
16738
- }
16739
- });
16740
- //#endregion
16741
- //#region src/diagnose-bridges/directory-files.ts
16742
- /** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
16743
- function filesUnder(directory) {
16744
- try {
16745
- return readdirSync(directory, {
16746
- recursive: true,
16747
- withFileTypes: true
16748
- }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
16749
- } catch {
16750
- return [];
16751
- }
16752
- }
16753
- //#endregion
16754
- //#region src/harness-registry/harness-registry.ts
16755
- /**
16756
- * Throws on a harness `skillsDirectories` does not confirm: treating it as native would silently
16757
- * drop a projection it may need.
16758
- */
16759
- function skillsProjection(harness, scope, directories = skillsDirectories(harness)?.[scope]) {
16760
- const listed = directories ?? [];
16761
- const [first] = listed;
16762
- if (first === void 0) throw new Error(`cyber-agentkit records no ${scope} skills directories for ${harness}.`);
16763
- return listed.includes(".agents/skills") ? {} : { skillsDirectory: first };
16764
- }
16765
- /**
16766
- * The user-scope counterpart of the root `AGENTS.md`, relative to the home directory. No harness
16767
- * reads it natively; each loads it only through a user-scope `instructionBridge` (E-CC-18, E-CODEX-05,
16768
- * E-GEM-03, E-COPILOT-04, E-CUR-08).
16769
- */
16770
- const globalInstructions = ".agents/AGENTS.md";
16771
- /**
16772
- * Skills directories come from `cyber-agentkit`, except for the harnesses it does not know
16773
- * (`devin-desktop`, `windsurf`); see `.research/agentic-configuration-standards/` for the rest.
16774
- */
16775
- const harnessRegistry = [
16776
- {
16777
- name: "claude-code",
16778
- project: {
16779
- detect: ".claude",
16780
- ...skillsProjection("claude-code", "project"),
16781
- shadowedBy: [
16782
- "CLAUDE.md",
16783
- ".claude/CLAUDE.md",
16784
- "CLAUDE.local.md"
16785
- ],
16786
- mcpConfig: {
16787
- path: ".mcp.json",
16788
- key: "mcpServers",
16789
- format: "json",
16790
- dialect: "claude-code"
16791
- },
16792
- nonstandard: [
16793
- {
16794
- path: ".claude/commands",
16795
- shape: "directory",
16796
- kind: "command"
16797
- },
16798
- {
16799
- path: ".claude/rules",
16800
- shape: "directory",
16801
- kind: "rule"
16802
- },
16803
- {
16804
- path: ".claude/agents",
16805
- shape: "directory",
16806
- kind: "subagent"
16807
- }
16808
- ]
16809
- },
16810
- user: {
16811
- detect: ".claude",
16812
- ...skillsProjection("claude-code", "user"),
16813
- relocatedBy: "CLAUDE_CONFIG_DIR",
16814
- emptyRelocatesToStart: true,
16815
- instructionBridge: {
16816
- kind: "import",
16817
- path: ".claude/CLAUDE.md",
16818
- line: `@~/${globalInstructions}`
16819
- }
16820
- }
16821
- },
16822
- {
16823
- name: "cursor",
16824
- project: {
16825
- detect: ".cursor",
16826
- ...skillsProjection("cursor", "project"),
16827
- mcpConfig: {
16828
- path: ".cursor/mcp.json",
16829
- key: "mcpServers",
16830
- format: "json",
16831
- dialect: "cursor"
16832
- },
16833
- nonstandard: [
16834
- {
16835
- path: ".cursorrules",
16836
- shape: "file",
16837
- kind: "instructions"
16838
- },
16839
- {
16840
- path: ".cursor/rules",
16841
- shape: "directory",
16842
- kind: "rule"
16843
- },
16844
- {
16845
- path: ".cursor/commands",
16846
- shape: "directory",
16847
- kind: "command"
16848
- },
16849
- {
16850
- path: ".cursor/skills",
16851
- shape: "directory",
16852
- kind: "skill"
16853
- }
16854
- ]
16855
- },
16856
- user: {
16857
- detect: ".cursor",
16858
- ...skillsProjection("cursor", "user")
16859
- }
16860
- },
16861
- {
16862
- name: "codex",
16863
- project: {
16864
- detect: ".codex",
16865
- ...skillsProjection("codex", "project"),
16866
- mcpConfig: {
16867
- path: ".codex/config.toml",
16868
- key: "mcp_servers",
16869
- format: "toml",
16870
- dialect: "codex",
16871
- shared: true
16872
- },
16873
- nonstandard: [{
16874
- path: ".codex/skills",
16875
- shape: "directory",
16876
- kind: "skill"
16877
- }]
16878
- },
16879
- user: {
16880
- detect: ".codex",
16881
- ...skillsProjection("codex", "user"),
16882
- instructionBridge: {
16883
- kind: "symlink",
16884
- path: ".codex/AGENTS.md"
16885
- },
16886
- shadowedBy: [".codex/AGENTS.override.md"],
16887
- relocatedBy: "CODEX_HOME"
16888
- }
16889
- },
16890
- {
16891
- name: "copilot-cli",
16892
- project: {
16893
- detect: ".github/skills",
16894
- ...skillsProjection("copilot-cli", "project"),
16895
- nonstandard: [
16896
- {
16897
- path: ".github/copilot-instructions.md",
16898
- shape: "file",
16899
- kind: "instructions"
16900
- },
16901
- {
16902
- path: ".github/instructions",
16903
- shape: "directory",
16904
- kind: "instructions"
16905
- },
16906
- {
16907
- path: ".github/skills",
16908
- shape: "directory",
16909
- kind: "skill"
16910
- }
16911
- ]
16912
- },
16913
- user: {
16914
- detect: ".copilot",
16915
- ...skillsProjection("copilot-cli", "user"),
16916
- relocatedBy: "COPILOT_HOME",
16917
- instructionBridge: {
16918
- kind: "symlink",
16919
- path: ".copilot/copilot-instructions.md"
16920
- }
16921
- }
16922
- },
16923
- {
16924
- name: "gemini-cli",
16925
- project: {
16926
- detect: ".gemini",
16927
- ...skillsProjection("gemini-cli", "project"),
16928
- instructionBridge: {
16929
- kind: "settings-entry",
16930
- path: ".gemini/settings.json",
16931
- key: "context.fileName"
16932
- },
16933
- mcpConfig: {
16934
- path: ".gemini/settings.json",
16935
- key: "mcpServers",
16936
- format: "json",
16937
- dialect: "gemini-cli",
16938
- shared: true
16939
- },
16940
- nonstandard: [{
16941
- path: "GEMINI.md",
16942
- shape: "file",
16943
- kind: "instructions"
16944
- }, {
16945
- path: ".gemini/skills",
16946
- shape: "directory",
16947
- kind: "skill"
16948
- }]
16949
- },
16950
- user: {
16951
- detect: ".gemini",
16952
- ...skillsProjection("gemini-cli", "user"),
16953
- instructionBridge: {
16954
- kind: "symlink",
16955
- path: ".gemini/GEMINI.md"
18087
+ const derived = deriveCatalog(root);
18088
+ const outcome = args.check ? catalogStatus(root, derived.catalog) === "unchanged" ? "current" : "would-change" : writeCatalog(root, derived.catalog);
18089
+ const home = homedir();
18090
+ const notes = notesFor(derived);
18091
+ const detected = runtimes.filter((candidate) => candidate.installedOnMachine(home));
18092
+ const harness = [];
18093
+ const actions = [];
18094
+ for (const runtime of detected) {
18095
+ const state = runtime.state(home);
18096
+ const delta = reconcile({
18097
+ catalog: derived.catalog,
18098
+ catalogDir: CATALOG_DIR,
18099
+ root: resolve(root),
18100
+ installed: state.installed,
18101
+ marketplaces: state.marketplaces,
18102
+ scoped: runtime.scoped
18103
+ });
18104
+ if (delta.collision) notes.push(`${runtime.name}: marketplace "${derived.catalog.name}" is already registered against ${delta.collision}. Registering over it replaces the other repository's silently, and the install that follows reports "already installed" — rename this repository's marketplace instead.`);
18105
+ harness.push({
18106
+ runtime: runtime.name,
18107
+ status: delta.actions.length ? `${delta.actions.length} to apply` : "in sync"
18108
+ });
18109
+ for (const action of delta.actions) actions.push({
18110
+ runtime: runtime.name,
18111
+ do: commandFor(runtime, action, derived.catalog.name),
18112
+ subject: action.subject,
18113
+ why: action.detail
18114
+ });
16956
18115
  }
18116
+ if (derived.plugins.length) notes.push(COPILOT_LIMITATION);
18117
+ const answer = result({
18118
+ catalog: CATALOG_PATH,
18119
+ marketplace: derived.catalog.name,
18120
+ outcome,
18121
+ plugins: derived.plugins.length ? derived.plugins.map(({ plugin, package: pkg, version }) => ({
18122
+ name: plugin,
18123
+ package: pkg,
18124
+ version
18125
+ })) : "0 dependencies ship a plugin — nothing to install",
18126
+ harness: harness.length ? harness : "no supported harness detected on this machine",
18127
+ actions: actions.length ? actions : "0 actions — every detected harness matches the catalog",
18128
+ ...notes.length ? { notes } : {}
18129
+ });
18130
+ return outcome === "would-change" ? answerThenFail(answer, this.format) : answer;
18131
+ } catch (error) {
18132
+ return fail(error, "Catalog generation failed.");
16957
18133
  }
16958
- },
16959
- {
16960
- name: "devin-desktop",
16961
- project: { detect: ".devin" }
16962
- },
16963
- {
16964
- name: "windsurf",
16965
- project: {
16966
- detect: ".windsurf",
16967
- skillsDirectory: ".windsurf/skills",
16968
- nonstandard: [{
16969
- path: ".windsurfrules",
16970
- shape: "file",
16971
- kind: "instructions"
16972
- }, {
16973
- path: ".windsurf/rules",
16974
- shape: "directory",
16975
- kind: "rule"
16976
- }]
16977
- },
16978
- deprecated: "devin-desktop"
16979
18134
  }
16980
- ];
16981
- const harnessNames = new Set(harnessRegistry.map((harness) => harness.name));
16982
- function isHarnessName(value) {
16983
- return harnessNames.has(value);
16984
- }
16985
- /**
16986
- * An unknown name throws rather than being dropped — silently falling back to the defaults would
16987
- * misreport what was diagnosed.
16988
- */
16989
- function parseHarnesses(value) {
16990
- const requested = (value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
16991
- const unsupported = requested.filter((name) => !isHarnessName(name));
16992
- if (unsupported.length) throw new Error(`Unsupported harness: ${unsupported.join(", ")}. Supported: ${harnessRegistry.map((harness) => harness.name).join(", ")}.`);
16993
- return requested.filter(isHarnessName);
16994
- }
16995
- /** Enabled whether or not the repository already contains their directories. */
16996
- const defaultHarnesses = ["claude-code", "cursor"];
16997
- function directoryExists(path) {
18135
+ });
18136
+ //#endregion
18137
+ //#region src/diagnose-bridges/directory-files.ts
18138
+ /** A directory that does not exist reads as empty: `doctor` never creates one to look at it. */
18139
+ function filesUnder(directory) {
16998
18140
  try {
16999
- return lstatSync(path).isDirectory();
18141
+ return readdirSync(directory, {
18142
+ recursive: true,
18143
+ withFileTypes: true
18144
+ }).filter((entry) => entry.isFile()).map((entry) => relative(directory, join(entry.parentPath, entry.name)).split(sep).join(posix.sep)).sort();
17000
18145
  } catch {
17001
- return false;
18146
+ return [];
17002
18147
  }
17003
18148
  }
17004
- /** The default harnesses, plus the preferred ones, plus every harness whose directory is present. */
17005
- function selectHarnesses(root, preferred) {
17006
- const enabled = /* @__PURE__ */ new Set([...defaultHarnesses, ...preferred]);
17007
- return harnessRegistry.filter((harness) => enabled.has(harness.name) || directoryExists(join(root, harness.project.detect)));
17008
- }
17009
18149
  //#endregion
17010
18150
  //#region src/diagnose-bridges/locator.ts
17011
18151
  /** The locator as one string: the finding's `path`, and what a repair names. */
@@ -19201,7 +20341,7 @@ function diagnoseNonstandard({ root, cli }) {
19201
20341
  return findings.sort((left, right) => left.path.localeCompare(right.path));
19202
20342
  }
19203
20343
  //#endregion
19204
- //#region ../../node_modules/.pnpm/cyber-agentkit@2.0.0_clibuilder@12.1.1_js-yaml@4.3.2_/node_modules/cyber-agentkit/dist/commands.js
20344
+ //#region ../../node_modules/.pnpm/cyber-agentkit@2.2.0_clibuilder@12.3.0_js-yaml@4.3.2_/node_modules/cyber-agentkit/dist/commands.js
19205
20345
  /**
19206
20346
  * AXI §10: collapses the home directory to `~` so a path in a report is portable to another
19207
20347
  * machine.
@@ -19214,6 +20354,8 @@ t.boolean().optional();
19214
20354
  t.string().optional();
19215
20355
  t.string();
19216
20356
  t.string().optional();
20357
+ /** The command line that reaches the reference commands in `cyber-agentkit`'s own CLI. */
20358
+ const defaultReferencePath = "cyber-agentkit reference";
19217
20359
  const rootOption$3 = {
19218
20360
  description: "The repository root: its `.agents/references/` is the repo tier, and its `package.json` names the dependency plugins. Defaults to the nearest folder at or above the current directory holding `.git`, else the current directory.",
19219
20361
  type: t.string().optional()
@@ -19366,7 +20508,7 @@ const skillOption = {
19366
20508
  description: "A skill's source folder in this repository: acts on its `references/<name>.default.md`.",
19367
20509
  type: t.string().optional()
19368
20510
  };
19369
- function checkCommand(layersFor) {
20511
+ function checkCommand(layersFor, path = defaultReferencePath) {
19370
20512
  return command({
19371
20513
  name: "check",
19372
20514
  description: "Check references, each composed as `load` composes it: an interface document against its shape, an implementation against its interface, and every document for the defects composing it raises. With no names, checks every name found. Every warning `load` raises is reported. A failure is an entry in `failed`, not an error; a warning never fails a check.",
@@ -19382,6 +20524,10 @@ function checkCommand(layersFor) {
19382
20524
  description: "Add every entry checked, with its kind and unsupported sections."
19383
20525
  }
19384
20526
  },
20527
+ examples: [{
20528
+ command: `${path} check`,
20529
+ description: "Check every reference against its interface"
20530
+ }],
19385
20531
  async run(args) {
19386
20532
  try {
19387
20533
  const home = homedir();
@@ -19423,7 +20569,7 @@ function checkCommand(layersFor) {
19423
20569
  }
19424
20570
  });
19425
20571
  }
19426
- function createCommand(layersFor) {
20572
+ function createCommand(layersFor, path = defaultReferencePath) {
19427
20573
  return command({
19428
20574
  name: "create",
19429
20575
  description: "Start a contribution in the repo or user tier, a folder's project section, or a plugin's or a skill's source folder, from a template or an interface. Never overwrites a file with a body; writes a file a contribution above hides, with a warning.",
@@ -19459,6 +20605,10 @@ function createCommand(layersFor) {
19459
20605
  description: "After the write, add the trace, in which the new file applies."
19460
20606
  }
19461
20607
  },
20608
+ examples: [{
20609
+ command: `${path} create my-topic`,
20610
+ description: "Start a new reference named my-topic"
20611
+ }],
19462
20612
  async run(args) {
19463
20613
  const format = this.format;
19464
20614
  try {
@@ -19569,13 +20719,32 @@ function text(customized, shown) {
19569
20719
  const meta = shown.metadataFile ? ` Its record is in ${shown.metadataFile}.` : "";
19570
20720
  return `${shown.path}\n\n${shown.hint}${meta}\n`;
19571
20721
  }
19572
- function customizeCommand(layersFor) {
20722
+ function familyReport(customized, args, home) {
20723
+ return compact({
20724
+ family: customized.family,
20725
+ interfaces: customized.interfaces.map(({ interface: id, outcome, reason, result: each }) => compact({
20726
+ interface: id,
20727
+ outcome,
20728
+ reason,
20729
+ ...each ? report(each, args, home) : {}
20730
+ })),
20731
+ hint: customized.dryRun ? "Nothing was written. Run again without --dry-run to write it." : "Run `load <interface>` for each composed document."
20732
+ });
20733
+ }
20734
+ function familyText(customized, shown) {
20735
+ return `${customized.interfaces.map(({ interface: id, outcome, reason, result: each }, at) => {
20736
+ if (!each) return `${id}: ${outcome}. ${reason}\n`;
20737
+ const own = shown.interfaces[at];
20738
+ return `${id}: ${outcome}.\n${text(each, own)}`;
20739
+ }).join("\n")}`;
20740
+ }
20741
+ function customizeCommand(layersFor, path = defaultReferencePath) {
19573
20742
  return command({
19574
20743
  name: "customize",
19575
20744
  description: "Customize a name for the repo or user tier, or a folder's project section, running no model: pick what it builds on (`extends`), write a body generated from a request with its record, or print what the generator reads (--base). Writes a file a contribution above hides, with a warning.",
19576
20745
  arguments: [{
19577
20746
  name: "name",
19578
- description: "Reference name, without the `.md` extension.",
20747
+ description: "Reference name, without the `.md` extension; or a family, `<prefix>.*`, to pick for every interface under the prefix at once (takes --extends or --extends-file, no `sections`).",
19579
20748
  type: t.string()
19580
20749
  }, {
19581
20750
  name: "steps",
@@ -19634,6 +20803,13 @@ function customizeCommand(layersFor) {
19634
20803
  description: "After the write, add the trace, in which the written file applies."
19635
20804
  }
19636
20805
  },
20806
+ examples: [{
20807
+ command: `${path} customize my-topic`,
20808
+ description: "Customize a reference for this project"
20809
+ }, {
20810
+ command: `${path} customize my-skill.aspects.* --extends house-style`,
20811
+ description: "Pick one plugin for every interface under a prefix"
20812
+ }],
19637
20813
  async run(args) {
19638
20814
  const format = this.format;
19639
20815
  try {
@@ -19657,19 +20833,33 @@ function customizeCommand(layersFor) {
19657
20833
  base: args.base,
19658
20834
  dryRun: args["dry-run"]
19659
20835
  };
20836
+ const family = isFamilyName(args.name) || args.name.includes("*") ? parseFamily(args.name) : void 0;
19660
20837
  validateWrite({
19661
20838
  command: "customize",
19662
- ...request
20839
+ ...request,
20840
+ name: family === void 0 || family.invalid !== void 0 ? args.name : family.prefix
19663
20841
  });
19664
20842
  if (args.steps?.length && args.extends === void 0) throw new ReferenceFailure("options-missing", `${args.steps.join(" ")} follow no --extends; pass --extends before the steps.`);
19665
20843
  const layers = await layersFor({
19666
20844
  root: args.root,
19667
20845
  default: args.default
19668
20846
  }, home);
20847
+ if (family !== void 0) {
20848
+ const customized = customizeContribution(layers, {
20849
+ ...request,
20850
+ root: layers.root ?? request.root
20851
+ }, { display });
20852
+ if (!("family" in customized)) throw new Error(`${args.name} was customized as a single name, not a family.`);
20853
+ const shown = familyReport(customized, args, home);
20854
+ if (format !== "text") return result(shown);
20855
+ for (const { result: each } of customized.interfaces) writeWarnings(each?.warnings ?? []);
20856
+ return document(familyText(customized, shown));
20857
+ }
19669
20858
  const customized = customizeContribution(layers, {
19670
20859
  ...request,
19671
20860
  root: layers.root ?? request.root
19672
20861
  }, { display });
20862
+ if ("family" in customized) throw new Error(`${args.name} was customized as a family, not a single name.`);
19673
20863
  if (customized.action === "base") {
19674
20864
  if (format !== "text") return result(baseReport(customized));
19675
20865
  return document(baseText(customized));
@@ -19702,7 +20892,7 @@ function deleteReport(deleted, home) {
19702
20892
  options: deleted.after.options
19703
20893
  });
19704
20894
  }
19705
- function deleteCommand(layersFor) {
20895
+ function deleteCommand(layersFor, path = defaultReferencePath) {
19706
20896
  return command({
19707
20897
  name: "delete",
19708
20898
  description: "Delete a repo or user contribution, a folder's project section, or one in a plugin's or a skill's source folder, with a non-Markdown file's metadata file, and report what answers the name afterwards. Never deletes an installed or managed one.",
@@ -19725,6 +20915,10 @@ function deleteCommand(layersFor) {
19725
20915
  type: t.boolean().optional()
19726
20916
  }
19727
20917
  },
20918
+ examples: [{
20919
+ command: `${path} delete my-topic`,
20920
+ description: "Delete your contribution to my-topic"
20921
+ }],
19728
20922
  async run(args) {
19729
20923
  const format = this.format;
19730
20924
  try {
@@ -19772,13 +20966,40 @@ function parseAccept(value) {
19772
20966
  } });
19773
20967
  return versions.map(Number);
19774
20968
  }
19775
- function implementationsCommand(layersFor) {
20969
+ function rowsOf(found, trace) {
20970
+ return found.implementations.map(({ pin, description, unsupported, name, tier, version }) => compact(trace ? {
20971
+ pin,
20972
+ description,
20973
+ unsupported,
20974
+ name,
20975
+ tier,
20976
+ version
20977
+ } : {
20978
+ pin,
20979
+ ...tier === "plugin" ? {} : { tier },
20980
+ description,
20981
+ unsupported
20982
+ }));
20983
+ }
20984
+ function reportOf(found, trace) {
20985
+ const report = { implementations: rowsOf(found, trace) };
20986
+ if (found.inUse !== void 0) report.inUse = found.inUse;
20987
+ if (trace) Object.assign(report, compact({
20988
+ interface: found.interface,
20989
+ cardinality: found.cardinality,
20990
+ accept: found.accept,
20991
+ excluded: found.excluded,
20992
+ warnings: found.warnings
20993
+ }));
20994
+ return report;
20995
+ }
20996
+ function implementationsCommand(layersFor, path = defaultReferencePath) {
19776
20997
  return command({
19777
20998
  name: "implementations",
19778
20999
  description: "List the implementations of an interface a consumer can load, each with its pin, description, and unsupported sections.",
19779
21000
  arguments: [{
19780
21001
  name: "id",
19781
- description: "Interface id, the name of its `<id>.interface` document without the suffix.",
21002
+ description: "Interface id, the name of its `<id>.interface` document without the suffix; or a family, `<prefix>.*`, for every interface under the prefix.",
19782
21003
  type: t.string()
19783
21004
  }],
19784
21005
  options: {
@@ -19792,36 +21013,34 @@ function implementationsCommand(layersFor) {
19792
21013
  description: "Add each one's name, tier, and version, the interface's cardinality and accepted versions, and the ones left out with why."
19793
21014
  }
19794
21015
  },
21016
+ examples: [{
21017
+ command: `${path} implementations my-interface`,
21018
+ description: "List what implements an interface"
21019
+ }, {
21020
+ command: `${path} implementations my-skill.aspects.*`,
21021
+ description: "List, for every interface under a prefix, what implements it and which one is in use"
21022
+ }],
19795
21023
  async run(args) {
19796
21024
  try {
19797
21025
  const accept = parseAccept(args.accept);
19798
21026
  const home = homedir();
19799
- const found = referenceImplementations(args.id, await layersFor(args, home), {
21027
+ const options = {
19800
21028
  accept,
19801
21029
  display: (path) => collapseHome(home, path)
19802
- });
19803
- const rows = found.implementations.map(({ pin, description, unsupported, name, tier, version }) => compact(args.trace ? {
19804
- pin,
19805
- description,
19806
- unsupported,
19807
- name,
19808
- tier,
19809
- version
19810
- } : {
19811
- pin,
19812
- ...tier === "plugin" ? {} : { tier },
19813
- description,
19814
- unsupported
19815
- }));
19816
- if (!rows.length && !args.trace) return empty(`0 implementations of ${args.id}`, { help: `Run \`reference implementations ${args.id} --trace\` for the ones left out, with why.` });
19817
- const report = { implementations: rows };
19818
- if (args.trace) Object.assign(report, compact({
19819
- interface: found.interface,
19820
- cardinality: found.cardinality,
19821
- accept: found.accept,
19822
- excluded: found.excluded,
19823
- warnings: found.warnings
19824
- }));
21030
+ };
21031
+ const found = referenceImplementations(args.id, await layersFor(args, home), options);
21032
+ if ("family" in found) {
21033
+ const family = found;
21034
+ return result({
21035
+ family: family.family,
21036
+ interfaces: family.interfaces.map((each) => ({
21037
+ ...reportOf(each, args.trace),
21038
+ interface: each.interface
21039
+ }))
21040
+ });
21041
+ }
21042
+ const report = reportOf(found, args.trace);
21043
+ if (!report.implementations.length && report.inUse === void 0 && !args.trace) return empty(`0 implementations of ${args.id}`, { help: `Run \`reference implementations ${args.id} --trace\` for the ones left out, with why.` });
19825
21044
  return result(report);
19826
21045
  } catch (error) {
19827
21046
  return asCliError(error, "Implementation lookup failed.");
@@ -19829,7 +21048,7 @@ function implementationsCommand(layersFor) {
19829
21048
  }
19830
21049
  });
19831
21050
  }
19832
- function listCommand(layersFor) {
21051
+ function listCommand(layersFor, path = defaultReferencePath) {
19833
21052
  return command({
19834
21053
  name: "list",
19835
21054
  description: "List every reference name, one row each, with its description, and its status when not found. A pattern keeps only the names it matches exactly.",
@@ -19845,6 +21064,13 @@ function listCommand(layersFor) {
19845
21064
  description: "Add one row per name per layer holding it, with its outcome."
19846
21065
  }
19847
21066
  },
21067
+ examples: [{
21068
+ command: `${path} list`,
21069
+ description: "List every reference name"
21070
+ }, {
21071
+ command: `${path} list 'project-tracker.*'`,
21072
+ description: "List the names matching a pattern"
21073
+ }],
19848
21074
  async run(args) {
19849
21075
  try {
19850
21076
  const home = homedir();
@@ -19912,7 +21138,7 @@ function loadText(resolved) {
19912
21138
  }
19913
21139
  return document(resolved.map(tag).join("\n\n"));
19914
21140
  }
19915
- function loadCommand(layersFor) {
21141
+ function loadCommand(layersFor, path = defaultReferencePath) {
19916
21142
  return command({
19917
21143
  name: "load",
19918
21144
  description: "Print one or more references, each composed from its contributions as their extends and merge declare.",
@@ -19930,6 +21156,13 @@ function loadCommand(layersFor) {
19930
21156
  },
19931
21157
  trace: traceOption
19932
21158
  },
21159
+ examples: [{
21160
+ command: `${path} load my-topic`,
21161
+ description: "Print one reference"
21162
+ }, {
21163
+ command: `${path} load my-topic other-topic --format json`,
21164
+ description: "Print several as JSON"
21165
+ }],
19933
21166
  async run(args) {
19934
21167
  const format = this.format;
19935
21168
  try {
@@ -19970,7 +21203,87 @@ function loadCommand(layersFor) {
19970
21203
  }
19971
21204
  });
19972
21205
  }
19973
- function searchCommand(layersFor) {
21206
+ function rowLine(row) {
21207
+ const where = row.folder === void 0 ? row.tier : `${row.tier} ${row.folder}`;
21208
+ const change = row.manual === void 0 ? `${row.from} → ${row.to}` : `${row.from}: manual: ${row.manual}`;
21209
+ const by = row.by === void 0 ? "" : ` (by ${row.by})`;
21210
+ const user = row.tier === "user" ? "; a user file, which no git history restores" : "";
21211
+ return `${row.path} (${where}): ${row.form}: ${change}${by}${user}`;
21212
+ }
21213
+ function summary(rows, written) {
21214
+ const manual = rows.filter((row) => row.manual !== void 0).length;
21215
+ const applied = rows.length - manual;
21216
+ const left = manual ? ` ${manual} ${manual === 1 ? "needs" : "need"} a person, as each says.` : "";
21217
+ if (written) return `migrated ${applied} ${applied === 1 ? "change" : "changes"}.${left}`;
21218
+ if (!applied) return `nothing to apply.${left}`;
21219
+ return `${applied} ${applied === 1 ? "change" : "changes"} to make; run \`reference migrate --write\` with the same arguments to apply ${applied === 1 ? "it" : "them"}.${left}`;
21220
+ }
21221
+ function migrateCommand(layersFor, path = defaultReferencePath) {
21222
+ return command({
21223
+ name: "migrate",
21224
+ description: "Rewrite reference files still in a form version 1 wrote, and rename files under a name a plugin renamed, in the repo and user tiers and every project section, or in one plugin or skill source folder. Prints the plan; `--write` applies it. Never changes a managed or installed file.",
21225
+ arguments: [{
21226
+ name: "names",
21227
+ description: "Reference names to limit the run to, without the `.md` extension; a file under an older name of one is included.",
21228
+ type: t.array(t.string()).optional()
21229
+ }],
21230
+ options: {
21231
+ root: rootOption$3,
21232
+ plugin: {
21233
+ description: "A plugin's source folder in this repository: migrate only its `references/`, in place of the tiers.",
21234
+ type: t.string().optional()
21235
+ },
21236
+ default: {
21237
+ description: "A skill's source folder in this repository: migrate only its `references/`, in place of the tiers. A default at `references/<name>.md` is renamed only for a name given.",
21238
+ type: t.string().optional()
21239
+ },
21240
+ write: {
21241
+ description: "Apply the changes. It edits `user` files too, which no git history restores.",
21242
+ type: t.boolean().optional()
21243
+ }
21244
+ },
21245
+ examples: [{
21246
+ command: `${path} migrate`,
21247
+ description: "Migrate version 1 reference files"
21248
+ }],
21249
+ async run(args) {
21250
+ const format = this.format;
21251
+ try {
21252
+ const home = homedir();
21253
+ if (args.plugin !== void 0 && args.default !== void 0) throw new ReferenceFailure("options-conflict", "pass one of --plugin and --default.");
21254
+ const layers = await layersFor({ root: args.root }, home);
21255
+ const migrated = migrateReferences(layers, {
21256
+ root: layers.root ?? givenRoot(args.root),
21257
+ home,
21258
+ names: args.names,
21259
+ plugin: args.plugin,
21260
+ default: args.default,
21261
+ write: args.write
21262
+ });
21263
+ const changes = migrated.rows.map((row) => ({
21264
+ ...row,
21265
+ path: collapseHome(home, row.path)
21266
+ }));
21267
+ if (!changes.length && !migrated.warnings.length) return empty("0 changes — no reference file is in a version 1 form or under a renamed name", { help: ["Run `reference check` for every warning `load` raises."] });
21268
+ const report = compact({
21269
+ changes,
21270
+ written: migrated.written ? true : void 0,
21271
+ warnings: migrated.warnings
21272
+ });
21273
+ if (format !== "text") return result(report);
21274
+ writeWarnings(migrated.warnings);
21275
+ return document(`${[
21276
+ ...changes.map(rowLine),
21277
+ "",
21278
+ summary(changes, migrated.written)
21279
+ ].join("\n").trimStart()}\n`);
21280
+ } catch (error) {
21281
+ return asCliError(error, "Reference migration failed.");
21282
+ }
21283
+ }
21284
+ });
21285
+ }
21286
+ function searchCommand(layersFor, path = defaultReferencePath) {
19974
21287
  return command({
19975
21288
  name: "search",
19976
21289
  description: "Find references by name, description, tags, headings, or body, best match first.",
@@ -19984,16 +21297,29 @@ function searchCommand(layersFor) {
19984
21297
  trace: {
19985
21298
  ...traceOption,
19986
21299
  description: "Add the tier and plugin or skill of each match."
21300
+ },
21301
+ full: {
21302
+ ...fullOption,
21303
+ description: `Show every match, not only the best 10.`
19987
21304
  }
19988
21305
  },
21306
+ examples: [{
21307
+ command: `${path} search "worktree leases"`,
21308
+ description: "Find references by topic"
21309
+ }],
19989
21310
  async run(args) {
19990
21311
  const query = args.query.trim();
19991
21312
  try {
19992
21313
  const home = homedir();
19993
21314
  const { matches, defaults } = searchReferences(query, await layersFor(args, home), { display: (path) => collapseHome(home, path) });
19994
21315
  if (!matches.length) return empty(`0 references match "${query}"`, { help: [...defaults ? [defaults] : [], "Try other words, or run `reference list` for every name."] });
19995
- return result(compact({
19996
- references: matches.map(({ name, description, match, folder, tier, plugin, skill, loadedBy }) => compact(args.trace ? {
21316
+ const shown = truncateList(matches, {
21317
+ limit: 10,
21318
+ full: args.full
21319
+ });
21320
+ const report = compact({
21321
+ count: shown.truncated ? shown.count : void 0,
21322
+ references: shown.items.map(({ name, description, match, folder, tier, plugin, skill, loadedBy }) => compact(args.trace ? {
19997
21323
  name,
19998
21324
  description,
19999
21325
  match,
@@ -20010,14 +21336,15 @@ function searchCommand(layersFor) {
20010
21336
  loadedBy
20011
21337
  })),
20012
21338
  defaults
20013
- }));
21339
+ });
21340
+ return shown.truncated ? result(report, { help: [`Run \`reference search ${JSON.stringify(query)} --full\` for every match.`] }) : result(report);
20014
21341
  } catch (error) {
20015
21342
  return asCliError(error, "Reference search failed.");
20016
21343
  }
20017
21344
  }
20018
21345
  });
20019
21346
  }
20020
- function whereCommand(layersFor) {
21347
+ function whereCommand(layersFor, path = defaultReferencePath) {
20021
21348
  return command({
20022
21349
  name: "where",
20023
21350
  description: "Give the repo and user files, and with --in a folder's project section, a customization or a selection of a reference is written to, who each reaches and what hides it, the main document's heading paths, and the read-only plugins and default the stack uses.",
@@ -20038,6 +21365,10 @@ function whereCommand(layersFor) {
20038
21365
  description: "Add every layer checked, with its outcome."
20039
21366
  }
20040
21367
  },
21368
+ examples: [{
21369
+ command: `${path} where my-topic`,
21370
+ description: "Show which layers hold my-topic"
21371
+ }],
20041
21372
  async run(args) {
20042
21373
  const format = this.format;
20043
21374
  try {
@@ -20072,25 +21403,26 @@ function whereCommand(layersFor) {
20072
21403
  });
20073
21404
  }
20074
21405
  /** A factory, so each host CLI names itself as the plugin whose own references come first. */
20075
- function createReferenceCommands({ plugin } = {}) {
21406
+ function createReferenceCommands({ plugin, path = defaultReferencePath } = {}) {
20076
21407
  const layersFor = layerSource(plugin);
20077
21408
  return {
20078
- load: loadCommand(layersFor),
20079
- list: listCommand(layersFor),
20080
- search: searchCommand(layersFor),
20081
- where: whereCommand(layersFor),
20082
- create: createCommand(layersFor),
20083
- customize: customizeCommand(layersFor),
20084
- delete: deleteCommand(layersFor),
20085
- check: checkCommand(layersFor),
20086
- implementations: implementationsCommand(layersFor)
21409
+ load: loadCommand(layersFor, path),
21410
+ list: listCommand(layersFor, path),
21411
+ search: searchCommand(layersFor, path),
21412
+ where: whereCommand(layersFor, path),
21413
+ create: createCommand(layersFor, path),
21414
+ customize: customizeCommand(layersFor, path),
21415
+ delete: deleteCommand(layersFor, path),
21416
+ migrate: migrateCommand(layersFor, path),
21417
+ check: checkCommand(layersFor, path),
21418
+ implementations: implementationsCommand(layersFor, path)
20087
21419
  };
20088
21420
  }
20089
21421
  function createReferenceCommand(options = {}) {
20090
- const { load, list, search, where, create, customize, delete: remove, check, implementations } = createReferenceCommands(options);
21422
+ const { load, list, search, where, create, customize, delete: remove, migrate, check, implementations } = createReferenceCommands(options);
20091
21423
  return command({
20092
21424
  name: "reference",
20093
- description: "Read reference documents by name, composed from the managed, repo, user, plugin, and default tiers, and create, customize, or delete a contribution.",
21425
+ description: "Read reference documents by name, composed from the managed, repo, user, plugin, and default tiers, create, customize, or delete a contribution, and migrate version 1 files.",
20094
21426
  commands: [
20095
21427
  load,
20096
21428
  list,
@@ -20099,6 +21431,7 @@ function createReferenceCommand(options = {}) {
20099
21431
  create,
20100
21432
  customize,
20101
21433
  remove,
21434
+ migrate,
20102
21435
  implementations,
20103
21436
  check
20104
21437
  ]
@@ -20256,10 +21589,13 @@ function referenceLayers({ packageRoot = dirname(packageGovernancesDir()), ...op
20256
21589
  }
20257
21590
  });
20258
21591
  }
20259
- const referenceCommand = createReferenceCommand({ plugin: {
20260
- name: PACKAGE_PLUGIN,
20261
- root: dirname(packageGovernancesDir())
20262
- } });
21592
+ const referenceCommand = createReferenceCommand({
21593
+ plugin: {
21594
+ name: PACKAGE_PLUGIN,
21595
+ root: dirname(packageGovernancesDir())
21596
+ },
21597
+ path: "buddy-agent-harness reference"
21598
+ });
20263
21599
  //#endregion
20264
21600
  //#region src/skill-projection/skill-projection.ts
20265
21601
  function skillNames(skillsDirectory) {
@@ -20974,64 +22310,77 @@ function buildDoctorReport(bin, result, configuration = [], held = []) {
20974
22310
  help: [...new Map(findings.map((finding) => [`${finding.repair.command}\u0000${finding.repair.instruction}`, finding.repair])).values()]
20975
22311
  };
20976
22312
  }
20977
- const doctorCommand = command({
20978
- name: "doctor",
20979
- description: "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only.",
20980
- options: {
20981
- root: {
20982
- description: "Repository or package directory. Defaults to the current directory.",
20983
- type: t.string().optional()
20984
- },
20985
- harness: {
20986
- description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
20987
- type: t.string().optional()
20988
- }
22313
+ const doctorOptions = {
22314
+ root: {
22315
+ description: "Repository or package directory. Defaults to the current directory.",
22316
+ type: t.string().optional()
20989
22317
  },
20990
- async run(args) {
20991
- try {
20992
- const harnesses = parseHarnesses(args.harness);
20993
- const root = args.root ?? process.cwd();
20994
- const home = homedir();
20995
- const diagnosis = diagnoseBridges({
22318
+ harness: {
22319
+ description: "Comma-separated harnesses to check in addition to Claude Code and Cursor, e.g. codex,gemini-cli.",
22320
+ type: t.string().optional()
22321
+ }
22322
+ };
22323
+ const doctorDescription = "Report whether this repository's harness bridges still resolve — skills into .agents/skills, instructions into AGENTS.md — and whether each harness's user-scope file loads ~/.agents/AGENTS.md. Read-only.";
22324
+ /** The live report `doctor` answers with, shared with the bare invocation of the CLI. */
22325
+ async function diagnoseReport(args) {
22326
+ try {
22327
+ const harnesses = parseHarnessOption(args.harness);
22328
+ const root = args.root ?? process.cwd();
22329
+ const home = homedir();
22330
+ const diagnosis = diagnoseBridges({
22331
+ root,
22332
+ ...harnesses.length ? { harnesses } : {},
22333
+ cli: commandInvocation,
22334
+ home,
22335
+ env: process.env
22336
+ });
22337
+ const git = new GitBridgeState(root);
22338
+ const configuration = [
22339
+ ...diagnoseConfiguration({
20996
22340
  root,
20997
- ...harnesses.length ? { harnesses } : {},
20998
- cli: commandInvocation,
20999
- home,
21000
- env: process.env
21001
- });
21002
- const git = new GitBridgeState(root);
21003
- const configuration = [
21004
- ...diagnoseConfiguration({
21005
- root,
21006
- git,
21007
- cli: commandInvocation
21008
- }),
21009
- ...diagnoseMcp({
21010
- root,
21011
- git,
21012
- cli: commandInvocation
21013
- }),
21014
- ...diagnoseNonstandard({
21015
- root,
21016
- cli: commandInvocation
21017
- })
21018
- ];
21019
- const { layers: rows } = listReferences(await referenceLayers({
22341
+ git,
22342
+ cli: commandInvocation
22343
+ }),
22344
+ ...diagnoseMcp({
21020
22345
  root,
21021
- home,
21022
- platform: process.platform,
21023
- env: process.env
21024
- }));
21025
- const held = rows.filter(({ tier }) => tier !== "plugin").map(({ name, tier, path, outcome }) => ({
21026
- name,
21027
- tier,
21028
- path: collapseHome$1(home, path),
21029
- status: outcome
21030
- }));
21031
- return result(buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held));
21032
- } catch (error) {
21033
- return fail(error, "Harness diagnosis failed.");
21034
- }
22346
+ git,
22347
+ cli: commandInvocation
22348
+ }),
22349
+ ...diagnoseNonstandard({
22350
+ root,
22351
+ cli: commandInvocation
22352
+ })
22353
+ ];
22354
+ const { layers: rows } = listReferences(await referenceLayers({
22355
+ root,
22356
+ home,
22357
+ platform: process.platform,
22358
+ env: process.env
22359
+ }));
22360
+ const held = rows.filter(({ tier }) => tier !== "plugin").map(({ name, tier, path, outcome }) => ({
22361
+ name,
22362
+ tier,
22363
+ path: collapseHome$1(home, path),
22364
+ status: outcome
22365
+ }));
22366
+ return buildDoctorReport(displayBinPath(home, process.argv[1]), diagnosis, configuration, held);
22367
+ } catch (error) {
22368
+ return fail(error, "Harness diagnosis failed.");
22369
+ }
22370
+ }
22371
+ const doctorCommand = command({
22372
+ name: "doctor",
22373
+ description: doctorDescription,
22374
+ options: doctorOptions,
22375
+ examples: [{
22376
+ command: "buddy-agent-harness doctor",
22377
+ description: "Check the current repository"
22378
+ }, {
22379
+ command: "buddy-agent-harness doctor --harness codex,gemini-cli",
22380
+ description: "Also check Codex and Gemini CLI"
22381
+ }],
22382
+ async run(args) {
22383
+ return result(await diagnoseReport(args));
21035
22384
  }
21036
22385
  });
21037
22386
  //#endregion
@@ -21066,6 +22415,10 @@ const governanceCommand = command({
21066
22415
  root: rootOption,
21067
22416
  "overrides-only": overridesOnlyOption
21068
22417
  },
22418
+ examples: [{
22419
+ command: "buddy-agent-harness governance list",
22420
+ description: "List the governances the layers hold"
22421
+ }],
21069
22422
  run(args) {
21070
22423
  process.stderr.write(GOVERNANCE_DEPRECATED);
21071
22424
  try {
@@ -21100,16 +22453,21 @@ const governanceCommand = command({
21100
22453
  root: rootOption,
21101
22454
  "overrides-only": overridesOnlyOption
21102
22455
  },
22456
+ examples: [{
22457
+ command: "buddy-agent-harness governance show skill-design",
22458
+ description: "Print one governance document"
22459
+ }],
21103
22460
  run(args) {
21104
22461
  const format = this.format;
21105
22462
  process.stderr.write(GOVERNANCE_DEPRECATED);
21106
22463
  try {
21107
22464
  const name = parseGovernanceName(args.name);
21108
22465
  const found = resolveGovernance(name, layersFor(args));
21109
- if (!found) {
21110
- 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`);
21111
- throw CliError.reported(exitCodes.error);
21112
- }
22466
+ if (!found) throw new CliError(args["overrides-only"] ? `no override for governance "${name}" in the project, user, or machine-wide layer.` : `no governance named "${name}" in any layer.`, {
22467
+ code: "governance-not-found",
22468
+ details: { name },
22469
+ help: ref("reference list", {}, "see what is available")
22470
+ });
21113
22471
  const report = {
21114
22472
  name: found.name,
21115
22473
  scope: found.scope,
@@ -21185,9 +22543,16 @@ const initCommand = command({
21185
22543
  type: t.string().optional()
21186
22544
  }
21187
22545
  },
22546
+ examples: [{
22547
+ command: "buddy-agent-harness init",
22548
+ description: "Set up skills for Claude Code and Cursor"
22549
+ }, {
22550
+ command: "buddy-agent-harness init --harness codex --copy",
22551
+ description: "Also enable Codex, copying skills instead of linking"
22552
+ }],
21188
22553
  run(args) {
21189
22554
  try {
21190
- const harnesses = parseHarnesses(args.harness);
22555
+ const harnesses = parseHarnessOption(args.harness);
21191
22556
  return result(initializeHarnesses({
21192
22557
  root: args.root ?? process.cwd(),
21193
22558
  ...harnesses.length ? { harnesses } : {},
@@ -23763,6 +25128,13 @@ const mcpReconcileCommand = command({
23763
25128
  type: t.array(t.string()).optional()
23764
25129
  }
23765
25130
  },
25131
+ examples: [{
25132
+ command: "buddy-agent-harness mcp reconcile",
25133
+ description: "List harness-side changes the golden set lacks"
25134
+ }, {
25135
+ command: "buddy-agent-harness mcp reconcile --accept <path>",
25136
+ description: "Import one approved field"
25137
+ }],
23766
25138
  run(args) {
23767
25139
  try {
23768
25140
  const root = args.root ?? process.cwd();
@@ -23774,19 +25146,17 @@ const mcpReconcileCommand = command({
23774
25146
  });
23775
25147
  if (plan.kind === "unreadable") {
23776
25148
  const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
23777
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`);
23778
- throw CliError.reported(exitCodes.error);
23779
- }
23780
- if (plan.kind === "rejected") {
23781
- process.stderr.write(`error: nothing written.\n${plan.reasons.map((reason) => `- ${reason}\n`).join("")}`);
23782
- throw CliError.reported(exitCodes.error);
23783
- }
23784
- if (plan.kind === "absent") return result({
23785
- golden: `0 servers — no golden set at ${goldenSetPath}`,
23786
- mode: "nothing to reconcile into",
23787
- fields: "0 fields",
23788
- record: "not written"
25149
+ throw new CliError(`${goldenSetPath} does not parse${at}.`, {
25150
+ code: "golden-set-unreadable",
25151
+ help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
25152
+ });
25153
+ }
25154
+ if (plan.kind === "rejected") throw new CliError("nothing written.", {
25155
+ code: "accept-rejected",
25156
+ details: { reasons: plan.reasons },
25157
+ help: ref("mcp reconcile", {}, "list the fields that can be approved")
23789
25158
  });
25159
+ if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: `Create ${goldenSetPath} first; there is nothing to reconcile into.` });
23790
25160
  return result({
23791
25161
  golden: goldenSetPath,
23792
25162
  mode: plan.written ? "written — only the fields named by --accept" : "dry run — nothing written; re-run with --accept <path> for each approved field",
@@ -24836,6 +26206,13 @@ const mcpCommand = command({
24836
26206
  type: t.boolean().optional()
24837
26207
  }
24838
26208
  },
26209
+ examples: [{
26210
+ command: "buddy-agent-harness mcp project",
26211
+ description: "Preview what would be projected"
26212
+ }, {
26213
+ command: "buddy-agent-harness mcp project --write",
26214
+ description: "Project the golden set into each harness"
26215
+ }],
24839
26216
  run(args) {
24840
26217
  try {
24841
26218
  const root = args.root ?? process.cwd();
@@ -24847,15 +26224,12 @@ const mcpCommand = command({
24847
26224
  });
24848
26225
  if (plan.kind === "unreadable") {
24849
26226
  const at = plan.position ? ` at line ${plan.position.line}, column ${plan.position.column}` : "";
24850
- process.stderr.write(`error: ${goldenSetPath} does not parse${at}.\n`);
24851
- throw CliError.reported(exitCodes.error);
26227
+ throw new CliError(`${goldenSetPath} does not parse${at}.`, {
26228
+ code: "golden-set-unreadable",
26229
+ help: `Fix the TOML syntax in ${goldenSetPath}, then re-run.`
26230
+ });
24852
26231
  }
24853
- if (plan.kind === "absent") return result({
24854
- golden: `0 servers — no golden set at ${goldenSetPath}`,
24855
- mode: "nothing to project",
24856
- actions: "0 changes",
24857
- record: "not written"
24858
- });
26232
+ if (plan.kind === "absent") return empty(`0 servers — no golden set at ${goldenSetPath}`, { help: [`Create ${goldenSetPath} with the MCP servers to project.`, ref("mcp project", {}, "preview the projection")] });
24859
26233
  return result({
24860
26234
  golden: goldenSetPath,
24861
26235
  mode: write ? "written" : "dry run — nothing written; re-run with --write to apply",
@@ -24892,6 +26266,19 @@ function app(format) {
24892
26266
  version,
24893
26267
  description: rootCommand.description,
24894
26268
  format
26269
+ }).default({
26270
+ description: "Report whether this repository's harness bridges resolve (the same as `doctor`).",
26271
+ options: doctorCommand.options,
26272
+ async run(args) {
26273
+ const { bin: _bin, ...report } = await diagnoseReport({
26274
+ root: args["root"],
26275
+ harness: args["harness"]
26276
+ });
26277
+ return result({
26278
+ ...homeHeader({ description: rootCommand.description }),
26279
+ ...report
26280
+ });
26281
+ }
24895
26282
  }).command(first));
24896
26283
  }
24897
26284
  const textByDefault = {