buddy-agent-harness 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2088,6 +2088,11 @@ function stripMergeComments(body) {
2088
2088
  const inFence = fenceTracker();
2089
2089
  return body.split("\n").filter((line) => inFence(line) || !mergeCommentPattern.test(line)).join("\n");
2090
2090
  }
2091
+ /** Each heading with a single `#` outside a code fence, as written. */
2092
+ function titleHeadings(body) {
2093
+ const inFence = fenceTracker();
2094
+ return body.split("\n").filter((line) => !inFence(line) && headingPattern.exec(line)?.[1] === "#");
2095
+ }
2091
2096
  function sectionKey(title) {
2092
2097
  return title.trim().replace(/\s+/g, " ").toLowerCase();
2093
2098
  }
@@ -2513,6 +2518,7 @@ function searchReferences(query, layers, options = {}) {
2513
2518
  }
2514
2519
  //#endregion
2515
2520
  //#region src/governance-overrides/governance-overrides.ts
2521
+ /** @deprecated Use `managedReferencesDir`. Removed in the next major version. */
2516
2522
  function managedGovernancesDir(platform, programData) {
2517
2523
  if (platform === "darwin") return "/Library/Application Support/BuddyAgentHarness/governances";
2518
2524
  if (platform === "win32") return join(programData || "C:\\ProgramData", "BuddyAgentHarness", "governances");
@@ -2521,6 +2527,8 @@ function managedGovernancesDir(platform, programData) {
2521
2527
  /**
2522
2528
  * Still read below the layer above, so a machine already using it keeps working; reported as
2523
2529
  * deprecated so an admin knows to move it.
2530
+ *
2531
+ * @deprecated Use `managedReferencesDir`. Removed in the next major version.
2524
2532
  */
2525
2533
  function deprecatedManagedGovernancesDir(platform, programData) {
2526
2534
  if (platform === "darwin") return "/Library/Application Support/UniPlugin/governances";
@@ -2546,6 +2554,7 @@ function packageGovernancesDir(from = fileURLToPath(import.meta.url)) {
2546
2554
  dir = parent;
2547
2555
  }
2548
2556
  }
2557
+ /** @deprecated Use `referenceLayers`. Removed in the next major version. */
2549
2558
  function governanceLayers({ root, home, platform, programData }) {
2550
2559
  return [
2551
2560
  {
@@ -2573,6 +2582,8 @@ function governanceLayers({ root, home, platform, programData }) {
2573
2582
  /**
2574
2583
  * Filters by scope rather than slicing the list, so a layer added later can't quietly become an
2575
2584
  * override by position.
2585
+ *
2586
+ * @deprecated Use `referenceLayers`. Removed in the next major version.
2576
2587
  */
2577
2588
  function overrideLayers(layers) {
2578
2589
  return layers.filter((layer) => layer.scope !== "package");
@@ -2581,6 +2592,8 @@ const namePattern = /^[a-z0-9]+(?:[-.][a-z0-9]+)*$/i;
2581
2592
  /**
2582
2593
  * Rejects rather than sanitizes — silently answering a different question is how untrusted input
2583
2594
  * reads a file outside the layer.
2595
+ *
2596
+ * @deprecated Use `parseReferenceName`. Removed in the next major version.
2584
2597
  */
2585
2598
  function parseGovernanceName(value) {
2586
2599
  if (value.endsWith(".md")) throw new Error(`"${value}" names a file. Ask for the governance by name, without the .md extension.`);
@@ -2602,7 +2615,11 @@ function documentNames(dir) {
2602
2615
  return [];
2603
2616
  }
2604
2617
  }
2605
- /** The first layer that holds the name wins; the rest are not read. */
2618
+ /**
2619
+ * The first layer that holds the name wins; the rest are not read.
2620
+ *
2621
+ * @deprecated Use `resolveReference`. Removed in the next major version.
2622
+ */
2606
2623
  function resolveGovernance(name, layers) {
2607
2624
  for (const { scope, dir } of layers) {
2608
2625
  const path = join(dir, `${name}.md`);
@@ -2615,7 +2632,11 @@ function resolveGovernance(name, layers) {
2615
2632
  };
2616
2633
  }
2617
2634
  }
2618
- /** Every name any layer holds, each reported at the layer that would win, sorted by name. */
2635
+ /**
2636
+ * Every name any layer holds, each reported at the layer that would win, sorted by name.
2637
+ *
2638
+ * @deprecated Use `listReferences`. Removed in the next major version.
2639
+ */
2619
2640
  function listGovernances(layers) {
2620
2641
  const found = /* @__PURE__ */ new Map();
2621
2642
  for (const { scope, dir } of layers) for (const name of documentNames(dir)) if (!found.has(name)) found.set(name, {
@@ -3580,12 +3601,61 @@ const governanceShowCommand = command({
3580
3601
  }
3581
3602
  }
3582
3603
  });
3604
+ /** @deprecated Use `referenceCommand`. Removed in the next major version. */
3583
3605
  const governanceCommand = command({
3584
3606
  name: "governance",
3585
3607
  description: "Deprecated: use `reference`. Read the governance documents the project, user, and machine-wide override layers hold. Read-only.",
3586
3608
  commands: [governanceListCommand, governanceShowCommand]
3587
3609
  });
3588
3610
  //#endregion
3611
+ //#region src/references/create-reference.ts
3612
+ /** No `#` title: `merge-sections` matches by heading path, so a title would hold every section under it. */
3613
+ const defaultTemplate = `---
3614
+ description: What this reference covers, and when an agent should read it.
3615
+ tags: [topic]
3616
+ ---
3617
+
3618
+ ## Overview
3619
+
3620
+ Write the guidance here. Give each topic its own top-level \`##\` section.
3621
+ `;
3622
+ function readTemplate(path) {
3623
+ if (path === void 0) return parseTemplate(defaultTemplate, "the default template");
3624
+ let content;
3625
+ try {
3626
+ content = readFileSync(path, "utf8");
3627
+ } catch (error) {
3628
+ throw new Error(`cannot read the template ${path}: ${error.message}`);
3629
+ }
3630
+ return parseTemplate(content, `the template ${path}`);
3631
+ }
3632
+ function parseTemplate(content, label) {
3633
+ let mapping = true;
3634
+ const { metadata, body } = parseDocument(content, () => {
3635
+ mapping = false;
3636
+ });
3637
+ if (!mapping) throw new Error(`${label} has frontmatter that is not a YAML mapping.`);
3638
+ return {
3639
+ content,
3640
+ metadata,
3641
+ body
3642
+ };
3643
+ }
3644
+ /** Adds one line and changes nothing else, so the result still reads as the template it came from. */
3645
+ function withMergeSections(content) {
3646
+ const match = frontmatterPattern.exec(content);
3647
+ if (!match) return `---\nmerge: merge-sections\n---\n\n${content}`;
3648
+ const open = match[0].indexOf("\n") + 1;
3649
+ const newline = match[0][open - 2] === "\r" ? "\r\n" : "\n";
3650
+ const end = open + match[1].length;
3651
+ return `${content.slice(0, end)}${newline}merge: merge-sections${content.slice(end)}`;
3652
+ }
3653
+ function templateWarnings({ metadata, body }) {
3654
+ const warnings = titleHeadings(body).map((heading) => `"${heading.trim()}" is a heading with one hash; merge-sections matches by heading path, so an override would replace or duplicate the whole document. Use top-level ## sections.`);
3655
+ if (typeof metadata["description"] !== "string" || !metadata["description"].trim()) warnings.push("the reference has no `description` in its frontmatter; `list` and `search` show it blank.");
3656
+ return warnings;
3657
+ }
3658
+ //#endregion
3589
3659
  //#region src/references/reference.command.ts
3590
3660
  const rootOption = {
3591
3661
  description: "Directory the project tier is read from, and each folder above it up to the repository root. Defaults to the current directory.",
@@ -3604,6 +3674,12 @@ function fail(error, fallback) {
3604
3674
  process.stderr.write(`error: ${error instanceof Error ? error.message : fallback}\n`);
3605
3675
  return exitCodes.error;
3606
3676
  }
3677
+ function traceOf(resolved, home) {
3678
+ return resolved.trace.map(({ description: _, ...step }) => ({
3679
+ ...step,
3680
+ path: collapseHome(home, step.path)
3681
+ }));
3682
+ }
3607
3683
  function showEntry(resolved, home, suggestions, trace) {
3608
3684
  const entry = {
3609
3685
  name: resolved.name,
@@ -3625,10 +3701,7 @@ function showEntry(resolved, home, suggestions, trace) {
3625
3701
  if (resolved.status === "missing") entry.suggestions = suggestions;
3626
3702
  if (resolved.status === "ambiguous") entry.plugins = resolved.plugins;
3627
3703
  entry.warnings = resolved.warnings;
3628
- if (trace) entry.trace = resolved.trace.map(({ description: _, ...step }) => ({
3629
- ...step,
3630
- path: collapseHome(home, step.path)
3631
- }));
3704
+ if (trace) entry.trace = traceOf(resolved, home);
3632
3705
  return entry;
3633
3706
  }
3634
3707
  function missReason(entry) {
@@ -3850,14 +3923,92 @@ const referenceWhereCommand = command({
3850
3923
  }
3851
3924
  }
3852
3925
  });
3926
+ function parseScope(value) {
3927
+ if (value !== "project" && value !== "user") throw new Error("--scope must be project or user.");
3928
+ return value;
3929
+ }
3930
+ const referenceCreateCommand = command({
3931
+ name: "create",
3932
+ description: "Start a new reference in the project or user tier from a template, marked merge-sections when it overrides a copy below. Never overwrites.",
3933
+ arguments: [{
3934
+ name: "name",
3935
+ description: "Reference name, without the `.md` extension.",
3936
+ type: z.string()
3937
+ }],
3938
+ options: {
3939
+ root: rootOption,
3940
+ template: {
3941
+ description: "File whose text is written as it is, frontmatter included. Defaults to a built-in template of top-level `##` sections with no `#` title.",
3942
+ type: z.optional(z.string())
3943
+ },
3944
+ scope: {
3945
+ description: "Tier to write: project (default), `.agents/references/` at the root, or user, `~/.agents/references/`.",
3946
+ type: z.optional(z.string()),
3947
+ default: "project"
3948
+ },
3949
+ "dry-run": {
3950
+ description: "Print the target path and the exact content, and write nothing.",
3951
+ type: z.optional(z.boolean())
3952
+ },
3953
+ format: {
3954
+ description: "Output format: text (default) writes the path, then the content or the trace; toon and json return an object.",
3955
+ type: z.optional(z.string()),
3956
+ default: "text"
3957
+ }
3958
+ },
3959
+ async run(args) {
3960
+ try {
3961
+ const format = parseFormat(args.format);
3962
+ const name = parseReferenceName(args.name);
3963
+ if (name.plugin !== void 0) throw new Error(`an override is written under the bare name, which every tier resolves; ask for "${name.name}".`);
3964
+ const scope = parseScope(args.scope);
3965
+ const template = readTemplate(args.template === void 0 ? void 0 : resolve(args.template));
3966
+ const home = homedir();
3967
+ const dir = scope === "project" ? projectReferencesDir(resolve(args.root ?? process.cwd())) : join(home, ".agents", "references");
3968
+ const target = join(dir, `${name.name}.md`);
3969
+ const display = (path) => collapseHome(home, path);
3970
+ const layers = await layersFor(args, home);
3971
+ const { trace } = resolveReference(name, layers, { display });
3972
+ const at = layers.findIndex((layer) => layer.dir === dir);
3973
+ if (trace[at]?.found) throw new Error(`${display(trace[at].path)} already holds "${name.name}"; change it with the reference skill's Update mode.`);
3974
+ const shadow = trace.slice(0, at).find((step) => step.found && step.merge === "first-wins");
3975
+ if (shadow) throw new Error(`the ${shadow.tier} copy ${display(shadow.path)} is first-wins, so nothing would read a new ${scope} file; change that copy instead.`);
3976
+ const content = trace.slice(at + 1).some((step) => step.found) && template.metadata["merge"] === void 0 ? withMergeSections(template.content) : template.content;
3977
+ const warnings = templateWarnings(template);
3978
+ const report = {
3979
+ name: name.name,
3980
+ scope,
3981
+ path: display(target),
3982
+ dryRun: Boolean(args["dry-run"]),
3983
+ content,
3984
+ warnings
3985
+ };
3986
+ if (!report.dryRun) {
3987
+ mkdirSync(dir, { recursive: true });
3988
+ writeFileSync(target, content, { flag: "wx" });
3989
+ report.trace = traceOf(resolveReference(name, layers, { display }), home);
3990
+ }
3991
+ if (format !== "text") writeResult(report, format);
3992
+ else {
3993
+ for (const warning of warnings) process.stderr.write(`warning: ${warning}\n`);
3994
+ if (report.trace) process.stdout.write(`${report.path}\n\n${renderText({ trace: report.trace })}\n`);
3995
+ else writeDocument(`${report.path}\n\n${content}`);
3996
+ }
3997
+ return exitCodes.success;
3998
+ } catch (error) {
3999
+ return fail(error, "Reference creation failed.");
4000
+ }
4001
+ }
4002
+ });
3853
4003
  const referenceCommand = command({
3854
4004
  name: "reference",
3855
- description: "Read on-demand reference documents by name, layered across the managed, project, user, and plugin tiers. Read-only.",
4005
+ description: "Read on-demand reference documents by name, layered across the managed, project, user, and plugin tiers, and start a new one in the project or user tier.",
3856
4006
  commands: [
3857
4007
  referenceShowCommand,
3858
4008
  referenceListCommand,
3859
4009
  referenceSearchCommand,
3860
- referenceWhereCommand
4010
+ referenceWhereCommand,
4011
+ referenceCreateCommand
3861
4012
  ]
3862
4013
  });
3863
4014
  //#endregion