rulesync 16.1.0 → 16.3.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.
@@ -61,6 +61,13 @@ const ALL_FEATURES = [
61
61
  ...ACTIVE_FEATURES_AFTER_IGNORE
62
62
  ];
63
63
  const ALL_FEATURES_WITH_WILDCARD = [...ALL_FEATURES, "*"];
64
+ /**
65
+ * Features that remain accepted for compatibility but are superseded by a
66
+ * newer feature. Maps each deprecated feature to its replacement; consumers
67
+ * (e.g. `rulesync doctor`) derive their deprecation warnings from this map so
68
+ * the set cannot drift from the schema above.
69
+ */
70
+ const DEPRECATED_FEATURE_REPLACEMENTS = { ignore: "permissions" };
64
71
  const ACTIVE_FEATURES = [...ACTIVE_FEATURES_BEFORE_IGNORE, ...ACTIVE_FEATURES_AFTER_IGNORE];
65
72
  const DeprecatedIgnoreFeatureSchema = z.literal("ignore").check(meta({
66
73
  deprecated: true,
@@ -192,6 +199,7 @@ const ignoreProcessorToolTargetTuple = [
192
199
  "kiro-cli",
193
200
  "kiro-ide",
194
201
  "qwencode",
202
+ "reasonix",
195
203
  "roo",
196
204
  "devin",
197
205
  "vibe",
@@ -249,6 +257,7 @@ const commandsProcessorToolTargetTuple = [
249
257
  "cursor",
250
258
  "factorydroid",
251
259
  "goose",
260
+ "grokcli",
252
261
  "hermesagent",
253
262
  "junie",
254
263
  "kilo",
@@ -404,6 +413,7 @@ const checksProcessorToolTargetTuple = [
404
413
  "amp",
405
414
  "cursor",
406
415
  "hermesagent",
416
+ "rovodev",
407
417
  "takt"
408
418
  ];
409
419
  //#endregion
@@ -807,6 +817,7 @@ const ErrorCodes = {
807
817
  GITIGNORE_FAILED: "GITIGNORE_FAILED",
808
818
  INIT_FAILED: "INIT_FAILED",
809
819
  MCP_FAILED: "MCP_FAILED",
820
+ DOCTOR_FAILED: "DOCTOR_FAILED",
810
821
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
811
822
  };
812
823
  /**
@@ -815,10 +826,12 @@ const ErrorCodes = {
815
826
  var CLIError = class extends Error {
816
827
  code;
817
828
  exitCode;
818
- constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1) {
829
+ details;
830
+ constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1, details) {
819
831
  super(message);
820
832
  this.code = code;
821
833
  this.exitCode = exitCode;
834
+ this.details = details;
822
835
  this.name = "CLIError";
823
836
  }
824
837
  };
@@ -942,6 +955,7 @@ var JsonLogger = class extends BaseLogger {
942
955
  message: errorMessage
943
956
  };
944
957
  if (this._verbose && message instanceof Error && message.stack) errorInfo.stack = message.stack;
958
+ if (message instanceof CLIError && message.details !== void 0) errorInfo.details = message.details;
945
959
  this.outputJson(false, errorInfo);
946
960
  }
947
961
  debug(_message, ..._args) {}
@@ -1002,6 +1016,10 @@ function hasControlCharacters(value) {
1002
1016
  }
1003
1017
  //#endregion
1004
1018
  //#region src/config/config.ts
1019
+ /**
1020
+ * Key accepted alongside feature names in the per-feature object form of
1021
+ * `targets`. Exported so `rulesync doctor` treats the same key as valid.
1022
+ */
1005
1023
  const GITIGNORE_DESTINATION_KEY = "gitignoreDestination";
1006
1024
  /**
1007
1025
  * Schema for a single source entry in the sources array.
@@ -1057,7 +1075,20 @@ const ConfigFileSchema = z.object({
1057
1075
  });
1058
1076
  z.required(ConfigParamsSchema);
1059
1077
  /**
1060
- * Conflicting target pairs that cannot be used together
1078
+ * Normalizes the configuration file location to an absolute path.
1079
+ *
1080
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1081
+ * only covers direct programmatic construction, where the conventional
1082
+ * location next to the input root is the best guess.
1083
+ */
1084
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1085
+ if (configFilePath === void 0) return join(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1086
+ return isAbsolute(configFilePath) ? configFilePath : resolve(configFilePath);
1087
+ }
1088
+ /**
1089
+ * Conflicting target pairs that cannot be used together.
1090
+ * Exported so `rulesync doctor` can report the same conflicts as diagnostics
1091
+ * without duplicating the list.
1061
1092
  */
1062
1093
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
1063
1094
  /**
@@ -1127,8 +1158,9 @@ var Config = class Config {
1127
1158
  dryRun;
1128
1159
  check;
1129
1160
  inputRoot;
1161
+ configFilePath;
1130
1162
  sources;
1131
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1163
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1132
1164
  assertTargetsFeaturesExclusive({
1133
1165
  targets,
1134
1166
  features
@@ -1161,6 +1193,10 @@ var Config = class Config {
1161
1193
  this.dryRun = dryRun ?? false;
1162
1194
  this.check = check ?? false;
1163
1195
  this.inputRoot = inputRoot === void 0 ? process.cwd() : isAbsolute(inputRoot) ? inputRoot : resolve(inputRoot);
1196
+ this.configFilePath = normalizeConfigFilePath({
1197
+ configFilePath,
1198
+ inputRoot: this.inputRoot
1199
+ });
1164
1200
  this.sources = sources ?? [];
1165
1201
  }
1166
1202
  /**
@@ -1354,6 +1390,14 @@ var Config = class Config {
1354
1390
  getInputRoot() {
1355
1391
  return this.inputRoot;
1356
1392
  }
1393
+ /**
1394
+ * Returns the absolute path of the configuration file this config was
1395
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1396
+ * without one — so callers must treat this as a location, not a guarantee.
1397
+ */
1398
+ getConfigFilePath() {
1399
+ return this.configFilePath;
1400
+ }
1357
1401
  getSources() {
1358
1402
  return this.sources;
1359
1403
  }
@@ -1576,6 +1620,7 @@ var ConfigResolver = class {
1576
1620
  fallback: getDefaults().check
1577
1621
  }),
1578
1622
  inputRoot: resolvedInputRoot !== void 0 ? resolve(resolvedInputRoot) : cwd,
1623
+ configFilePath: validatedConfigPath,
1579
1624
  sources: configByFile.sources ?? getDefaults().sources,
1580
1625
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1581
1626
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -3986,10 +4031,30 @@ const ClinePermissionsOverrideSchema = z.looseObject({
3986
4031
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
3987
4032
  * override; each value may be a bare action string or a pattern map.
3988
4033
  *
4034
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4035
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4036
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4037
+ * `writable_paths`. It has no canonical permission category, so it is authored
4038
+ * here and emitted only for Kilo.
4039
+ *
4040
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4041
+ * `writable_paths` are honored from the global config only, and a project
4042
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4043
+ * project-level network denial even clears the global destination exceptions.
4044
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4045
+ * written, and the rest are dropped with a warning rather than emitted into a
4046
+ * file Kilo would ignore.
4047
+ *
3989
4048
  * @example
3990
4049
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4050
+ * @example
4051
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4052
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
3991
4053
  */
3992
- const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
4054
+ const KiloPermissionsOverrideSchema = z.looseObject({
4055
+ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)),
4056
+ sandbox: z.optional(z.looseObject({}))
4057
+ });
3993
4058
  /**
3994
4059
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
3995
4060
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -5298,6 +5363,26 @@ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5298
5363
  const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5299
5364
  const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5300
5365
  //#endregion
5366
+ //#region src/constants/rovodev-paths.ts
5367
+ const ROVODEV_DIR = ".rovodev";
5368
+ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
5369
+ const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
5370
+ const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
5371
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5372
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5373
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5374
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5375
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
5376
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5377
+ const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
5378
+ /**
5379
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5380
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5381
+ * dot in the file name.
5382
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5383
+ */
5384
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5385
+ //#endregion
5301
5386
  //#region src/constants/takt-paths.ts
5302
5387
  const TAKT_DIR = ".takt";
5303
5388
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5759,12 +5844,19 @@ function slugifyCheckName(value) {
5759
5844
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5760
5845
  }
5761
5846
  //#endregion
5762
- //#region src/features/checks/cursor-check.ts
5847
+ //#region src/features/checks/aggregated-check-file.ts
5763
5848
  /**
5764
- * Marks where one check starts inside the single instruction file. Bugbot reads
5765
- * the file as prose, and an HTML comment is invisible in rendered Markdown, so
5766
- * the marker carries the check identity without changing what Bugbot is told.
5849
+ * Shared machinery for the tools whose checks surface is **one aggregated
5850
+ * instruction file** rather than a file per check Cursor Bugbot's
5851
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5852
+ *
5853
+ * Both read the file as free prose, so the check identities have to be carried
5854
+ * in something invisible to the reader: an HTML-comment marker per section.
5855
+ * That marker convention, the escaping that keeps a check body from splitting
5856
+ * itself, and the import-side split are the same for both files, so they live
5857
+ * here once.
5767
5858
  */
5859
+ /** Marks where one check starts inside the single instruction file. */
5768
5860
  const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5769
5861
  /**
5770
5862
  * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
@@ -5775,17 +5867,16 @@ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5775
5867
  */
5776
5868
  const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5777
5869
  const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5778
- const FALLBACK_CHECK_NAME = "bugbot";
5779
- function renderMarker(name) {
5870
+ function renderCheckMarker(name) {
5780
5871
  return `<!-- rulesync:check:${name} -->`;
5781
5872
  }
5782
- function escapeMarkers(content) {
5873
+ function escapeCheckMarkers(content) {
5783
5874
  return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5784
5875
  }
5785
- function unescapeMarkers(content) {
5876
+ function unescapeCheckMarkers(content) {
5786
5877
  return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5787
5878
  }
5788
- function findMarkers(fileContent) {
5879
+ function findCheckMarkers(fileContent) {
5789
5880
  CHECK_MARKER_PATTERN.lastIndex = 0;
5790
5881
  const markers = [];
5791
5882
  let match = CHECK_MARKER_PATTERN.exec(fileContent);
@@ -5800,23 +5891,47 @@ function findMarkers(fileContent) {
5800
5891
  return markers;
5801
5892
  }
5802
5893
  /**
5803
- * The instruction text one check contributes. Bugbot has no field to put a
5804
- * summary in, so `description` is used only when there is no body — the same
5805
- * fallback the file-stem heading above it gets.
5894
+ * Whether the file holds instruction text ahead of the first marker the
5895
+ * question the "generating replaces this" warning asks. A file with no marker
5896
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5897
+ * since there is nothing to replace.
5898
+ */
5899
+ function hasHandWrittenPreamble(fileContent) {
5900
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5901
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5902
+ }
5903
+ /**
5904
+ * Whether the file is nothing but sections rulesync generated — the question
5905
+ * the deletion guard asks, and a stricter one than
5906
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5907
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5908
+ * empty one is somebody's placeholder rather than our leftover.
5909
+ */
5910
+ function isOnlyGeneratedSections(fileContent) {
5911
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5912
+ if (firstMarkerStart === void 0) return false;
5913
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5914
+ }
5915
+ /**
5916
+ * The instruction text one check contributes. Neither file has a field to put a
5917
+ * summary in, so `description` is used only when there is no body.
5806
5918
  */
5807
5919
  function toInstruction(rulesyncCheck) {
5808
5920
  const body = rulesyncCheck.getBody().trim();
5809
5921
  if (body.length > 0) return body;
5810
5922
  return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5811
5923
  }
5812
- function renderSection(rulesyncCheck) {
5924
+ function renderCheckSection(rulesyncCheck) {
5813
5925
  const name = basename(rulesyncCheck.getRelativeFilePath(), ".md");
5814
5926
  const heading = `## ${name}`;
5815
5927
  const instruction = toInstruction(rulesyncCheck);
5816
- const lines = [renderMarker(name), heading];
5817
- if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5928
+ const lines = [renderCheckMarker(name), heading];
5929
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5818
5930
  return lines.join("\n");
5819
5931
  }
5932
+ function renderCheckFile(rulesyncChecks) {
5933
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5934
+ }
5820
5935
  /** Drop the heading generate writes, so a round trip does not stack headings. */
5821
5936
  function stripGeneratedHeading(section, name) {
5822
5937
  const [firstLine, ...rest] = section.split("\n");
@@ -5824,6 +5939,53 @@ function stripGeneratedHeading(section, name) {
5824
5939
  return section.trim();
5825
5940
  }
5826
5941
  /**
5942
+ * Split an aggregated instruction file back into one check per section.
5943
+ *
5944
+ * Content ahead of the first marker — and a hand-written file with no markers
5945
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5946
+ * is dropped.
5947
+ */
5948
+ function splitCheckFile({ fileContent, fallbackName }) {
5949
+ const sections = [];
5950
+ const markers = findCheckMarkers(fileContent);
5951
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5952
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5953
+ if (preamble.length > 0) sections.push({
5954
+ name: fallbackName,
5955
+ content: unescapeCheckMarkers(preamble)
5956
+ });
5957
+ for (const [index, marker] of markers.entries()) {
5958
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5959
+ const markerName = marker.name.trim();
5960
+ const name = slugifyCheckName(markerName) || fallbackName;
5961
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5962
+ sections.push({
5963
+ name,
5964
+ content: unescapeCheckMarkers(content)
5965
+ });
5966
+ }
5967
+ const used = /* @__PURE__ */ new Set();
5968
+ return sections.map(({ name, content }) => {
5969
+ let uniqueName = name;
5970
+ let suffix = 2;
5971
+ while (used.has(uniqueName)) {
5972
+ uniqueName = `${name}-${suffix}`;
5973
+ suffix += 1;
5974
+ }
5975
+ used.add(uniqueName);
5976
+ return new RulesyncCheck({
5977
+ outputRoot: ".",
5978
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5979
+ relativeFilePath: `${uniqueName}.md`,
5980
+ frontmatter: { targets: ["*"] },
5981
+ body: content
5982
+ });
5983
+ });
5984
+ }
5985
+ //#endregion
5986
+ //#region src/features/checks/cursor-check.ts
5987
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
5988
+ /**
5827
5989
  * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5828
5990
  *
5829
5991
  * Bugbot takes one aggregated instruction file per directory rather than a file
@@ -5880,9 +6042,7 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5880
6042
  const paths = CursorCheck.getSettablePaths();
5881
6043
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5882
6044
  if (fileContent === null) return true;
5883
- const firstMarkerStart = findMarkers(fileContent)[0]?.start;
5884
- if (firstMarkerStart === void 0) return false;
5885
- return fileContent.slice(0, firstMarkerStart).trim().length === 0;
6045
+ return isOnlyGeneratedSections(fileContent);
5886
6046
  }
5887
6047
  static fromRulesyncCheck(_params) {
5888
6048
  throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
@@ -5892,10 +6052,8 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5892
6052
  const paths = CursorCheck.getSettablePaths({ global });
5893
6053
  const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5894
6054
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
5895
- const existingContent = await readFileContentOrNull(filePath) ?? "";
5896
- const firstMarkerStart = findMarkers(existingContent)[0]?.start ?? existingContent.length;
5897
- if (existingContent.slice(0, firstMarkerStart).trim().length > 0) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
5898
- const fileContent = `${rulesyncChecks.map(renderSection).join("\n\n")}\n`;
6055
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
6056
+ const fileContent = renderCheckFile(rulesyncChecks);
5899
6057
  return [new CursorCheck({
5900
6058
  outputRoot,
5901
6059
  relativeDirPath: paths.relativeDirPath,
@@ -5938,41 +6096,9 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5938
6096
  return first;
5939
6097
  }
5940
6098
  toRulesyncChecks() {
5941
- const fileContent = this.getFileContent();
5942
- const sections = [];
5943
- const markers = findMarkers(fileContent);
5944
- const preambleEnd = markers[0]?.start ?? fileContent.length;
5945
- const preamble = fileContent.slice(0, preambleEnd).trim();
5946
- if (preamble.length > 0) sections.push({
5947
- name: FALLBACK_CHECK_NAME,
5948
- content: unescapeMarkers(preamble)
5949
- });
5950
- for (const [index, marker] of markers.entries()) {
5951
- const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5952
- const markerName = marker.name.trim();
5953
- const name = slugifyCheckName(markerName) || FALLBACK_CHECK_NAME;
5954
- const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5955
- sections.push({
5956
- name,
5957
- content: unescapeMarkers(content)
5958
- });
5959
- }
5960
- const used = /* @__PURE__ */ new Set();
5961
- return sections.map(({ name, content }) => {
5962
- let uniqueName = name;
5963
- let suffix = 2;
5964
- while (used.has(uniqueName)) {
5965
- uniqueName = `${name}-${suffix}`;
5966
- suffix += 1;
5967
- }
5968
- used.add(uniqueName);
5969
- return new RulesyncCheck({
5970
- outputRoot: ".",
5971
- relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5972
- relativeFilePath: `${uniqueName}.md`,
5973
- frontmatter: { targets: ["*"] },
5974
- body: content
5975
- });
6099
+ return splitCheckFile({
6100
+ fileContent: this.getFileContent(),
6101
+ fallbackName: FALLBACK_CHECK_NAME$1
5976
6102
  });
5977
6103
  }
5978
6104
  };
@@ -5994,8 +6120,22 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5994
6120
  */
5995
6121
  /** Project-root instruction file auto-injected by Hermes Agent. */
5996
6122
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
5997
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6123
+ /**
6124
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6125
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6126
+ */
5998
6127
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6128
+ /**
6129
+ * Home-relative global profile root on Windows: upstream defaults to
6130
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6131
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6132
+ *
6133
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6134
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6135
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6136
+ * followed; those users should set `HERMES_HOME` explicitly.
6137
+ */
6138
+ const HERMESAGENT_GLOBAL_WIN32_DIR = join("AppData", "Local", "hermes");
5999
6139
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
6000
6140
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
6001
6141
  const HERMESAGENT_CONFIG_FILE_PATH = join(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -6218,6 +6358,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
6218
6358
  }
6219
6359
  };
6220
6360
  //#endregion
6361
+ //#region src/features/checks/rovodev-check.ts
6362
+ const FALLBACK_CHECK_NAME = "review-agent";
6363
+ /**
6364
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6365
+ * (`.rovodev/.review-agent.md`).
6366
+ *
6367
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6368
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6369
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6370
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6371
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6372
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6373
+ * adapters share).
6374
+ *
6375
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6376
+ * have no equivalent there: they are not written and do not come back on
6377
+ * import. Neither does `description` whenever the check also has a body.
6378
+ *
6379
+ * Project scope only — these are per-repository review instructions, and Rovo
6380
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6381
+ * same tool is the opposite: global only.)
6382
+ *
6383
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6384
+ */
6385
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6386
+ static getSettablePaths(_options = {}) {
6387
+ return {
6388
+ relativeDirPath: ROVODEV_DIR,
6389
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6390
+ };
6391
+ }
6392
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6393
+ return this.isTargetedByRulesyncCheckDefault({
6394
+ rulesyncCheck,
6395
+ toolTarget: "rovodev"
6396
+ });
6397
+ }
6398
+ /**
6399
+ * Ownership guard the processor consults before it deletes anything for this
6400
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6401
+ * to hand-write, so anything in it that rulesync did not write is not
6402
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6403
+ * take somebody's hand-written review instructions with it.
6404
+ */
6405
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6406
+ const paths = RovodevCheck.getSettablePaths();
6407
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6408
+ if (fileContent === null) return true;
6409
+ return isOnlyGeneratedSections(fileContent);
6410
+ }
6411
+ static fromRulesyncCheck(_params) {
6412
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6413
+ }
6414
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6415
+ if (rulesyncChecks.length === 0) return [];
6416
+ const paths = RovodevCheck.getSettablePaths({ global });
6417
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6418
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6419
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Rovo Dev checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets rovodev --features checks\` first to keep them.`);
6420
+ return [new RovodevCheck({
6421
+ outputRoot,
6422
+ relativeDirPath: paths.relativeDirPath,
6423
+ relativeFilePath,
6424
+ fileContent: renderCheckFile(rulesyncChecks),
6425
+ global
6426
+ })];
6427
+ }
6428
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6429
+ const paths = RovodevCheck.getSettablePaths({ global });
6430
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6431
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6432
+ return new RovodevCheck({
6433
+ outputRoot,
6434
+ relativeDirPath: paths.relativeDirPath,
6435
+ relativeFilePath,
6436
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6437
+ global
6438
+ });
6439
+ }
6440
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6441
+ return new RovodevCheck({
6442
+ outputRoot,
6443
+ relativeDirPath,
6444
+ relativeFilePath,
6445
+ fileContent: "",
6446
+ validate: false,
6447
+ global
6448
+ });
6449
+ }
6450
+ validate() {
6451
+ return {
6452
+ success: true,
6453
+ error: null
6454
+ };
6455
+ }
6456
+ toRulesyncCheck() {
6457
+ const first = this.toRulesyncChecks()[0];
6458
+ if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6459
+ return first;
6460
+ }
6461
+ toRulesyncChecks() {
6462
+ return splitCheckFile({
6463
+ fileContent: this.getFileContent(),
6464
+ fallbackName: FALLBACK_CHECK_NAME
6465
+ });
6466
+ }
6467
+ };
6468
+ //#endregion
6221
6469
  //#region src/constants/codexcli-paths.ts
6222
6470
  const CODEXCLI_DIR = ".codex";
6223
6471
  const CODEXCLI_PROMPTS_DIR_PATH = join(CODEXCLI_DIR, "prompts");
@@ -6330,11 +6578,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6330
6578
  }
6331
6579
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6332
6580
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6581
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6582
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6333
6583
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6334
6584
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6335
6585
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6336
6586
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6337
6587
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6588
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6338
6589
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6339
6590
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6340
6591
  /**
@@ -6358,6 +6609,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6358
6609
  * lock-step with the writers derived from the processor registry, so an
6359
6610
  * undeclared writer fails CI instead of merging by accident.
6360
6611
  */
6612
+ /**
6613
+ * Hermes writes one `config.yaml`, but its global profile root has three
6614
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6615
+ * itself). They are the same file with the same owners, so the declaration is
6616
+ * written once and shared — a policy edit cannot land on one spelling only.
6617
+ */
6618
+ const HERMES_CONFIG_DECLARATION = {
6619
+ format: "yaml",
6620
+ features: {
6621
+ commands: {
6622
+ kind: "replace-owned-keys",
6623
+ ownedKeys: ["plugins"]
6624
+ },
6625
+ subagents: {
6626
+ kind: "replace-owned-keys",
6627
+ ownedKeys: ["plugins"]
6628
+ },
6629
+ mcp: {
6630
+ kind: "replace-owned-keys",
6631
+ ownedKeys: ["mcp_servers"]
6632
+ },
6633
+ hooks: {
6634
+ kind: "replace-owned-keys",
6635
+ ownedKeys: ["hooks"]
6636
+ },
6637
+ permissions: {
6638
+ kind: "deep-merge",
6639
+ replaceKeys: ["permissions"]
6640
+ }
6641
+ }
6642
+ };
6643
+ /**
6644
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6645
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6646
+ * name the profile directory itself, so the file has two spellings that share
6647
+ * one declaration — a policy edit cannot land on only one of them.
6648
+ */
6649
+ const KIMI_CODE_CONFIG_DECLARATION = {
6650
+ format: "toml",
6651
+ invalidRootPolicy: "error",
6652
+ features: {
6653
+ hooks: {
6654
+ kind: "replace-owned-keys",
6655
+ ownedKeys: ["hooks"]
6656
+ },
6657
+ mcp: {
6658
+ kind: "replace-owned-keys",
6659
+ ownedKeys: ["mcp"]
6660
+ },
6661
+ permissions: {
6662
+ kind: "replace-owned-keys",
6663
+ ownedKeys: [
6664
+ "permission",
6665
+ "default_permission_mode",
6666
+ "tools"
6667
+ ]
6668
+ }
6669
+ }
6670
+ };
6361
6671
  const SHARED_CONFIG_OWNERSHIP = {
6362
6672
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6363
6673
  format: "json",
@@ -6376,31 +6686,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6376
6686
  }
6377
6687
  }
6378
6688
  },
6379
- [HERMES_CONFIG_SHARED_FILE_KEY]: {
6380
- format: "yaml",
6381
- features: {
6382
- commands: {
6383
- kind: "replace-owned-keys",
6384
- ownedKeys: ["plugins"]
6385
- },
6386
- subagents: {
6387
- kind: "replace-owned-keys",
6388
- ownedKeys: ["plugins"]
6389
- },
6390
- mcp: {
6391
- kind: "replace-owned-keys",
6392
- ownedKeys: ["mcp_servers"]
6393
- },
6394
- hooks: {
6395
- kind: "replace-owned-keys",
6396
- ownedKeys: ["hooks"]
6397
- },
6398
- permissions: {
6399
- kind: "deep-merge",
6400
- replaceKeys: ["permissions"]
6401
- }
6402
- }
6403
- },
6689
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6690
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6691
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6404
6692
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6405
6693
  format: "yaml",
6406
6694
  invalidRootPolicy: "error",
@@ -6446,6 +6734,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6446
6734
  ".config/zed/settings.json": {
6447
6735
  format: "json",
6448
6736
  features: {
6737
+ ignore: {
6738
+ kind: "replace-owned-keys",
6739
+ ownedKeys: ["private_files"]
6740
+ },
6449
6741
  mcp: {
6450
6742
  kind: "replace-owned-keys",
6451
6743
  ownedKeys: ["context_servers"]
@@ -6459,6 +6751,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6459
6751
  "AppData/Roaming/Zed/settings.json": {
6460
6752
  format: "json",
6461
6753
  features: {
6754
+ ignore: {
6755
+ kind: "replace-owned-keys",
6756
+ ownedKeys: ["private_files"]
6757
+ },
6462
6758
  mcp: {
6463
6759
  kind: "replace-owned-keys",
6464
6760
  ownedKeys: ["context_servers"]
@@ -6715,31 +7011,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6715
7011
  }
6716
7012
  }
6717
7013
  },
6718
- [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: {
6719
- format: "toml",
6720
- invalidRootPolicy: "error",
6721
- features: {
6722
- hooks: {
6723
- kind: "replace-owned-keys",
6724
- ownedKeys: ["hooks"]
6725
- },
6726
- mcp: {
6727
- kind: "replace-owned-keys",
6728
- ownedKeys: ["mcp"]
6729
- },
6730
- permissions: {
6731
- kind: "replace-owned-keys",
6732
- ownedKeys: [
6733
- "permission",
6734
- "default_permission_mode",
6735
- "tools"
6736
- ]
6737
- }
6738
- }
6739
- },
7014
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
7015
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6740
7016
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6741
7017
  format: "toml",
6742
7018
  features: {
7019
+ ignore: {
7020
+ kind: "custom",
7021
+ policyFunction: "applyIgnoreReadDenies"
7022
+ },
6743
7023
  mcp: {
6744
7024
  kind: "replace-owned-keys",
6745
7025
  ownedKeys: ["plugins"]
@@ -6757,6 +7037,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6757
7037
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6758
7038
  format: "toml",
6759
7039
  features: {
7040
+ ignore: {
7041
+ kind: "custom",
7042
+ policyFunction: "applyIgnoreReadDenies"
7043
+ },
6760
7044
  mcp: {
6761
7045
  kind: "replace-owned-keys",
6762
7046
  ownedKeys: ["plugins"]
@@ -7201,6 +7485,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7201
7485
  filePattern: "*.json"
7202
7486
  }
7203
7487
  }],
7488
+ ["rovodev", {
7489
+ class: RovodevCheck,
7490
+ meta: {
7491
+ supportsGlobal: false,
7492
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7493
+ }
7494
+ }],
7204
7495
  ["takt", {
7205
7496
  class: TaktCheck,
7206
7497
  meta: {
@@ -7356,19 +7647,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7356
7647
  }
7357
7648
  };
7358
7649
  //#endregion
7650
+ //#region src/utils/tool-home.ts
7651
+ /**
7652
+ * Where the rulesync-side source files of a tool with a home override belong.
7653
+ *
7654
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7655
+ * imported back out of it are not part of the tool's profile — they stay under
7656
+ * the rulesync home. When no override is set, the native output root already is
7657
+ * that place.
7658
+ */
7659
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7660
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7661
+ }
7662
+ //#endregion
7359
7663
  //#region src/utils/hermesagent.ts
7360
7664
  function getHermesagentHome() {
7361
7665
  const configuredHome = process.env.HERMES_HOME?.trim();
7362
7666
  return configuredHome ? resolve(configuredHome) : void 0;
7363
7667
  }
7668
+ /**
7669
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7670
+ *
7671
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7672
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7673
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7674
+ * would land where Hermes never reads.
7675
+ *
7676
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7677
+ */
7678
+ function getHermesagentGlobalDir() {
7679
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7680
+ }
7364
7681
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7365
7682
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7366
7683
  }
7684
+ /**
7685
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7686
+ * actually writes in the requested scope.
7687
+ *
7688
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7689
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7690
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7691
+ * platform default directory takes its place.
7692
+ */
7367
7693
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7368
- if (!global || !getHermesagentHome()) return relativeDirPath;
7694
+ if (!global) return relativeDirPath;
7369
7695
  const relativePath = relative(HERMESAGENT_GLOBAL_DIR, relativeDirPath);
7370
- if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7371
- return relativePath;
7696
+ try {
7697
+ checkPathTraversal({
7698
+ relativePath: relativeDirPath,
7699
+ intendedRootDir: "."
7700
+ });
7701
+ checkPathTraversal({
7702
+ relativePath,
7703
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7704
+ });
7705
+ } catch {
7706
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7707
+ }
7708
+ return getHermesagentHome() ? relativePath || "." : join(getHermesagentGlobalDir(), relativePath);
7372
7709
  }
7373
7710
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7374
7711
  return join(getHermesagentRelativeDirPath({
@@ -7376,8 +7713,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7376
7713
  relativeDirPath: dirname(relativeFilePath)
7377
7714
  }), basename(relativeFilePath));
7378
7715
  }
7716
+ /**
7717
+ * Every spelling `config.yaml` can take in global scope, so that the
7718
+ * shared-write derivation and the gateway ownership table it is checked against
7719
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7720
+ *
7721
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7722
+ * which would otherwise make the derived shared-file key depend on the ambient
7723
+ * environment — the drift guards would then go blind in precisely the
7724
+ * configuration this feature exists for.
7725
+ */
7726
+ function getHermesagentSharedConfigWritePaths() {
7727
+ return [
7728
+ {
7729
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7730
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7731
+ },
7732
+ {
7733
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7734
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7735
+ },
7736
+ {
7737
+ relativeDirPath: ".",
7738
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7739
+ }
7740
+ ];
7741
+ }
7742
+ /**
7743
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7744
+ * writes. All three spellings carry the same declaration, but passing the key of
7745
+ * the file being written keeps the write path and the drift guards reading the
7746
+ * same entry.
7747
+ */
7748
+ function getHermesagentConfigSharedFileKey({ global }) {
7749
+ return sharedConfigFileKey({
7750
+ relativeDirPath: getHermesagentRelativeDirPath({
7751
+ global,
7752
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7753
+ }),
7754
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7755
+ });
7756
+ }
7379
7757
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7380
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7758
+ return getToolRulesyncOutputRoot({
7759
+ nativeOutputRoot,
7760
+ global,
7761
+ toolHome: getHermesagentHome
7762
+ });
7381
7763
  }
7382
7764
  //#endregion
7383
7765
  //#region src/constants/agentsmd-paths.ts
@@ -9081,6 +9463,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
9081
9463
  }
9082
9464
  };
9083
9465
  //#endregion
9466
+ //#region src/constants/grokcli-paths.ts
9467
+ /**
9468
+ * Grok Build CLI (xAI) configuration-layout conventions.
9469
+ *
9470
+ * Single source of truth for where Grok Build expects its files. Grok Build
9471
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9472
+ * with project/global scopes resolved by the directory the CLI runs in
9473
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9474
+ *
9475
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9476
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9477
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9478
+ * @see https://docs.x.ai/build/overview
9479
+ */
9480
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9481
+ const GROKCLI_DIR = ".grok";
9482
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9483
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9484
+ /**
9485
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9486
+ * permission mode, and other settings all live here; permissions reuse the same
9487
+ * file name as MCP since Grok consolidates everything into one config.
9488
+ */
9489
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9490
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9491
+ const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
9492
+ /**
9493
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9494
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9495
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9496
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9497
+ * @see https://docs.x.ai/build/features/hooks
9498
+ */
9499
+ const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
9500
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9501
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9502
+ /**
9503
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9504
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9505
+ * (global), each a Markdown file with YAML frontmatter (verified via
9506
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9507
+ */
9508
+ const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
9509
+ /**
9510
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9511
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9512
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9513
+ * MCP/skills/subagents adapters).
9514
+ */
9515
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9516
+ /**
9517
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9518
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9519
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9520
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9521
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9522
+ * for Claude Code.
9523
+ *
9524
+ * Skills are collected before commands and win name collisions, so a
9525
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9526
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9527
+ */
9528
+ const GROKCLI_COMMANDS_DIR_PATH = join(GROKCLI_DIR, "commands");
9529
+ /**
9530
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9531
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9532
+ * walks, and `~/.grok/rules/` in the home scope.
9533
+ * @see https://docs.x.ai/build/overview
9534
+ */
9535
+ const GROKCLI_RULES_DIR_PATH = join(GROKCLI_DIR, "rules");
9536
+ //#endregion
9537
+ //#region src/features/commands/grokcli-command.ts
9538
+ /**
9539
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9540
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9541
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9542
+ *
9543
+ * Two upstream constraints shape this adapter:
9544
+ *
9545
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9546
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9547
+ * onto their basename).
9548
+ * - Skills are collected before commands and win name collisions, so a
9549
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9550
+ *
9551
+ * `description` and `argument-hint` describe the command, while
9552
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9553
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9554
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9555
+ */
9556
+ const GrokcliCommandFrontmatterSchema = z.looseObject({
9557
+ description: z.optional(z.string()),
9558
+ "argument-hint": z.optional(z.string()),
9559
+ "user-invocable": z.optional(z.boolean()),
9560
+ "disable-model-invocation": z.optional(z.boolean())
9561
+ });
9562
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9563
+ frontmatter;
9564
+ body;
9565
+ constructor({ frontmatter, body, ...rest }) {
9566
+ if (rest.validate) {
9567
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9568
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9569
+ }
9570
+ super({
9571
+ ...rest,
9572
+ fileContent: stringifyFrontmatter(body, frontmatter)
9573
+ });
9574
+ this.frontmatter = frontmatter;
9575
+ this.body = body;
9576
+ }
9577
+ static getSettablePaths(_options = {}) {
9578
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9579
+ }
9580
+ getBody() {
9581
+ return this.body;
9582
+ }
9583
+ getFrontmatter() {
9584
+ return this.frontmatter;
9585
+ }
9586
+ toRulesyncCommand() {
9587
+ const { description, ...restFields } = this.frontmatter;
9588
+ const rulesyncFrontmatter = {
9589
+ targets: ["*"],
9590
+ description,
9591
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9592
+ };
9593
+ return new RulesyncCommand({
9594
+ outputRoot: ".",
9595
+ frontmatter: rulesyncFrontmatter,
9596
+ body: this.body,
9597
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9598
+ relativeFilePath: this.relativeFilePath,
9599
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9600
+ validate: true
9601
+ });
9602
+ }
9603
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9604
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9605
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9606
+ const grokcliFrontmatter = {
9607
+ description: rulesyncFrontmatter.description,
9608
+ ...grokcliFields
9609
+ };
9610
+ const paths = this.getSettablePaths({ global });
9611
+ return new GrokcliCommand({
9612
+ outputRoot,
9613
+ frontmatter: grokcliFrontmatter,
9614
+ body: rulesyncCommand.getBody(),
9615
+ relativeDirPath: paths.relativeDirPath,
9616
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9617
+ validate
9618
+ });
9619
+ }
9620
+ validate() {
9621
+ if (!this.frontmatter) return {
9622
+ success: true,
9623
+ error: null
9624
+ };
9625
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9626
+ if (result.success) return {
9627
+ success: true,
9628
+ error: null
9629
+ };
9630
+ return {
9631
+ success: false,
9632
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9633
+ };
9634
+ }
9635
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9636
+ return this.isTargetedByRulesyncCommandDefault({
9637
+ rulesyncCommand,
9638
+ toolTarget: "grokcli"
9639
+ });
9640
+ }
9641
+ /**
9642
+ * Warn when a rulesync skill would shadow a rulesync command.
9643
+ *
9644
+ * Grok collects skills before commands and lets skills win name collisions,
9645
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9646
+ * Both files are still written correctly — nothing is overwritten and no
9647
+ * output is lost — so this warns rather than failing the run the way the
9648
+ * Hermes check does, where the two surfaces really do write the same path.
9649
+ */
9650
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9651
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
9652
+ if (commandNames.size === 0) return;
9653
+ const shadowed = (await findFilesByGlobs(join(join(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
9654
+ if (shadowed.length > 0) logger.warn(`Grok CLI resolves skills before commands, so these skills shadow the same-named commands, which will never be reachable: ${[...new Set(shadowed)].toSorted().join(", ")}. Rename either side to make both invocable.`);
9655
+ }
9656
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9657
+ const paths = this.getSettablePaths({ global });
9658
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
9659
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9660
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9661
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9662
+ return new GrokcliCommand({
9663
+ outputRoot,
9664
+ relativeDirPath: paths.relativeDirPath,
9665
+ relativeFilePath,
9666
+ frontmatter: result.data,
9667
+ body: content.trim(),
9668
+ validate
9669
+ });
9670
+ }
9671
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9672
+ return new GrokcliCommand({
9673
+ outputRoot,
9674
+ relativeDirPath,
9675
+ relativeFilePath,
9676
+ frontmatter: { description: "" },
9677
+ body: "",
9678
+ validate: false
9679
+ });
9680
+ }
9681
+ };
9682
+ //#endregion
9084
9683
  //#region src/features/skills/tool-skill.ts
9085
9684
  /** Ordered skill directory roots: primary first. */
9086
9685
  function toolSkillSearchRoots(paths) {
@@ -9715,7 +10314,7 @@ def register(ctx):
9715
10314
  _register_command(ctx, command)
9716
10315
  `;
9717
10316
  }
9718
- function getEnabledPluginConfigContent$1(currentContent) {
10317
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9719
10318
  const config = parseSharedConfig({
9720
10319
  format: "yaml",
9721
10320
  fileContent: currentContent
@@ -9723,7 +10322,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9723
10322
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9724
10323
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9725
10324
  return applySharedConfigPatch({
9726
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10325
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9727
10326
  feature: "commands",
9728
10327
  existingContent: currentContent,
9729
10328
  patch: { plugins: {
@@ -9732,7 +10331,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9732
10331
  } }
9733
10332
  });
9734
10333
  }
9735
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10334
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9736
10335
  const config = parseSharedConfig({
9737
10336
  format: "yaml",
9738
10337
  fileContent: currentContent
@@ -9740,7 +10339,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9740
10339
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9741
10340
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9742
10341
  return applySharedConfigPatch({
9743
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10342
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9744
10343
  feature: "commands",
9745
10344
  existingContent: currentContent,
9746
10345
  patch: { plugins: {
@@ -9756,35 +10355,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9756
10355
  error: null
9757
10356
  };
9758
10357
  }
9759
- shouldMergeExistingFileContent() {
10358
+ /**
10359
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10360
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10361
+ */
10362
+ matchesPath(relativeFilePath) {
9760
10363
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9761
10364
  global: this.global,
9762
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10365
+ relativeFilePath
9763
10366
  }));
9764
10367
  }
10368
+ shouldMergeExistingFileContent() {
10369
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10370
+ }
9765
10371
  setFileContent(newFileContent) {
9766
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9767
- global: this.global,
9768
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9769
- }))) {
9770
- super.setFileContent(getEnabledPluginConfigContent$1(newFileContent));
10372
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10373
+ super.setFileContent(getEnabledPluginConfigContent$1({
10374
+ currentContent: newFileContent,
10375
+ global: this.global
10376
+ }));
9771
10377
  return;
9772
10378
  }
9773
10379
  super.setFileContent(newFileContent);
9774
10380
  }
9775
10381
  getFileContent() {
9776
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9777
- global: this.global,
9778
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH
9779
- }))) return getPluginManifestContent$2();
9780
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9781
- global: this.global,
9782
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH
9783
- }))) return getPluginInitContent$2();
9784
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9785
- global: this.global,
9786
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9787
- }))) return getEnabledPluginConfigContent$1(super.getFileContent());
10382
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10383
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10384
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10385
+ currentContent: super.getFileContent(),
10386
+ global: this.global
10387
+ });
9788
10388
  return super.getFileContent();
9789
10389
  }
9790
10390
  };
@@ -9801,14 +10401,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9801
10401
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9802
10402
  }) };
9803
10403
  }
9804
- static getExtraSharedWritePaths({ global = false } = {}) {
9805
- return [{
9806
- relativeDirPath: getHermesagentRelativeDirPath({
9807
- global,
9808
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
9809
- }),
9810
- relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
9811
- }];
10404
+ /**
10405
+ * `config.yaml` under every spelling the global profile root can take.
10406
+ * @see getHermesagentSharedConfigWritePaths
10407
+ */
10408
+ static getExtraSharedWritePaths() {
10409
+ return getHermesagentSharedConfigWritePaths();
9812
10410
  }
9813
10411
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9814
10412
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9833,33 +10431,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9833
10431
  }
9834
10432
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9835
10433
  if (toolCommands.length === 0 && !forDeletion) return [];
10434
+ const pluginDirPath = getHermesagentRelativeDirPath({
10435
+ global,
10436
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10437
+ });
9836
10438
  const pluginFiles = [
9837
10439
  new HermesagentCommandAuxiliaryFile({
9838
10440
  outputRoot,
9839
- relativeDirPath: getHermesagentRelativeDirPath({
9840
- global,
9841
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9842
- }),
10441
+ relativeDirPath: pluginDirPath,
9843
10442
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9844
10443
  fileContent: "",
9845
10444
  global
9846
10445
  }),
9847
10446
  new HermesagentCommandAuxiliaryFile({
9848
10447
  outputRoot,
9849
- relativeDirPath: getHermesagentRelativeDirPath({
9850
- global,
9851
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9852
- }),
10448
+ relativeDirPath: pluginDirPath,
9853
10449
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9854
10450
  fileContent: "Generated and owned by RuleSync.\n",
9855
10451
  global
9856
10452
  }),
9857
10453
  new HermesagentCommandAuxiliaryFile({
9858
10454
  outputRoot,
9859
- relativeDirPath: getHermesagentRelativeDirPath({
9860
- global,
9861
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9862
- }),
10455
+ relativeDirPath: pluginDirPath,
9863
10456
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9864
10457
  fileContent: "",
9865
10458
  global
@@ -11094,19 +11687,6 @@ var RooCommand = class RooCommand extends ToolCommand {
11094
11687
  }
11095
11688
  };
11096
11689
  //#endregion
11097
- //#region src/constants/rovodev-paths.ts
11098
- const ROVODEV_DIR = ".rovodev";
11099
- const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
11100
- const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
11101
- const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
11102
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
11103
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
11104
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
11105
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
11106
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
11107
- const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
11108
- const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
11109
- //#endregion
11110
11690
  //#region src/features/commands/rovodev-command.ts
11111
11691
  /**
11112
11692
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11690,6 +12270,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11690
12270
  supportsSubdirectory: false
11691
12271
  }
11692
12272
  }],
12273
+ ["grokcli", {
12274
+ class: GrokcliCommand,
12275
+ meta: {
12276
+ extension: "md",
12277
+ supportsProject: true,
12278
+ supportsGlobal: true,
12279
+ isSimulated: false,
12280
+ supportsSubdirectory: false
12281
+ }
12282
+ }],
11693
12283
  ["hermesagent", {
11694
12284
  class: HermesagentCommand,
11695
12285
  meta: {
@@ -11883,7 +12473,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11883
12473
  const factory = this.getFactory(this.toolTarget);
11884
12474
  await factory.class.validateRulesyncCommands?.({
11885
12475
  inputRoot: this.inputRoot,
11886
- rulesyncCommands
12476
+ rulesyncCommands,
12477
+ logger: this.logger
11887
12478
  });
11888
12479
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11889
12480
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -12021,7 +12612,10 @@ var CommandsProcessor = class extends FeatureProcessor {
12021
12612
  }));
12022
12613
  const currentContent = await readFileContentOrNull(configPath);
12023
12614
  if (currentContent === null) return changedCount;
12024
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12615
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12616
+ currentContent,
12617
+ global: this.global
12618
+ });
12025
12619
  if (nextContent === currentContent) return changedCount;
12026
12620
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
12027
12621
  else await writeFileContent(configPath, nextContent);
@@ -14483,64 +15077,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14483
15077
  }
14484
15078
  };
14485
15079
  //#endregion
14486
- //#region src/constants/grokcli-paths.ts
14487
- /**
14488
- * Grok Build CLI (xAI) configuration-layout conventions.
14489
- *
14490
- * Single source of truth for where Grok Build expects its files. Grok Build
14491
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14492
- * with project/global scopes resolved by the directory the CLI runs in
14493
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14494
- *
14495
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14496
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14497
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14498
- * @see https://docs.x.ai/build/overview
14499
- */
14500
- /** Root directory for Grok Build configuration, relative to the scope root. */
14501
- const GROKCLI_DIR = ".grok";
14502
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14503
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14504
- /**
14505
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14506
- * permission mode, and other settings all live here; permissions reuse the same
14507
- * file name as MCP since Grok consolidates everything into one config.
14508
- */
14509
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14510
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14511
- const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
14512
- /**
14513
- * Hooks directory under `.grok/`. Grok Build discovers hook config files from
14514
- * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
14515
- * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
14516
- * shape. rulesync writes all its hooks into a single `rulesync.json`.
14517
- * @see https://docs.x.ai/build/features/hooks
14518
- */
14519
- const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
14520
- /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
14521
- const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
14522
- /**
14523
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14524
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14525
- * (global), each a Markdown file with YAML frontmatter (verified via
14526
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14527
- */
14528
- const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
14529
- /**
14530
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14531
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14532
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14533
- * MCP/skills/subagents adapters).
14534
- */
14535
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14536
- /**
14537
- * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
14538
- * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
14539
- * walks, and `~/.grok/rules/` in the home scope.
14540
- * @see https://docs.x.ai/build/overview
14541
- */
14542
- const GROKCLI_RULES_DIR_PATH = join(GROKCLI_DIR, "rules");
14543
- //#endregion
14544
15080
  //#region src/features/hooks/grokcli-hooks.ts
14545
15081
  const GROKCLI_CONVERTER_CONFIG = {
14546
15082
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14786,6 +15322,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14786
15322
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14787
15323
  };
14788
15324
  }
15325
+ /**
15326
+ * `config.yaml` under every spelling the global profile root can take.
15327
+ * @see getHermesagentSharedConfigWritePaths
15328
+ */
15329
+ static getExtraSharedWritePaths() {
15330
+ return getHermesagentSharedConfigWritePaths();
15331
+ }
14789
15332
  constructor(params) {
14790
15333
  super({
14791
15334
  ...params,
@@ -14823,7 +15366,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14823
15366
  }
14824
15367
  setFileContent(fileContent) {
14825
15368
  this.fileContent = applySharedConfigPatch({
14826
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15369
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14827
15370
  feature: "hooks",
14828
15371
  existingContent: fileContent,
14829
15372
  patch: parseSharedConfig({
@@ -15199,8 +15742,38 @@ function getKimiCodeHome() {
15199
15742
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
15200
15743
  return global && getKimiCodeHome() ? relativeDirPath : join(KIMI_CODE_DIR, relativeDirPath);
15201
15744
  }
15745
+ /**
15746
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15747
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15748
+ * Declared unconditionally so the derived shared-file keys — and the drift
15749
+ * guards checked against them — do not depend on the ambient environment.
15750
+ */
15751
+ function getKimiCodeSharedConfigWritePaths() {
15752
+ return [{
15753
+ relativeDirPath: KIMI_CODE_DIR,
15754
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15755
+ }, {
15756
+ relativeDirPath: ".",
15757
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15758
+ }];
15759
+ }
15760
+ /**
15761
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15762
+ * Both spellings carry the same declaration, but passing the key of the file
15763
+ * being written keeps the write path and the drift guards on the same entry.
15764
+ */
15765
+ function getKimiCodeConfigSharedFileKey({ global }) {
15766
+ return sharedConfigFileKey({
15767
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15768
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15769
+ });
15770
+ }
15202
15771
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
15203
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15772
+ return getToolRulesyncOutputRoot({
15773
+ nativeOutputRoot,
15774
+ global,
15775
+ toolHome: getKimiCodeHome
15776
+ });
15204
15777
  }
15205
15778
  //#endregion
15206
15779
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -15298,13 +15871,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
15298
15871
  isDeletable() {
15299
15872
  return false;
15300
15873
  }
15874
+ /**
15875
+ * `config.toml` under both spellings its directory can take.
15876
+ * @see getKimiCodeSharedConfigWritePaths
15877
+ */
15878
+ static getExtraSharedWritePaths() {
15879
+ return getKimiCodeSharedConfigWritePaths();
15880
+ }
15301
15881
  shouldMergeExistingFileContent() {
15302
15882
  return true;
15303
15883
  }
15304
15884
  setFileContent(fileContent) {
15305
15885
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
15306
15886
  this.fileContent = applySharedConfigPatch({
15307
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15887
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
15308
15888
  feature: "hooks",
15309
15889
  existingContent: fileContent,
15310
15890
  patch: parseSharedConfig({
@@ -17995,6 +18575,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
17995
18575
  }
17996
18576
  };
17997
18577
  //#endregion
18578
+ //#region src/features/shared/reasonix-config-table.ts
18579
+ /**
18580
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18581
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18582
+ *
18583
+ * TOML is only structurally validated on parse, so a hand-edited config can
18584
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18585
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18586
+ * before merging, so the narrowing lives here once rather than being re-spelled
18587
+ * (and re-diverging) per feature.
18588
+ */
18589
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18590
+ function toReasonixStringArray(value) {
18591
+ if (!Array.isArray(value)) return [];
18592
+ return value.filter((entry) => typeof entry === "string");
18593
+ }
18594
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18595
+ function toReasonixTable(value) {
18596
+ if (!isPlainObject$1(value)) return {};
18597
+ return { ...value };
18598
+ }
18599
+ //#endregion
18600
+ //#region src/features/ignore/reasonix-ignore.ts
18601
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18602
+ /**
18603
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18604
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18605
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18606
+ * gateway already implements applies verbatim; only the surrounding file
18607
+ * format differs. Sibling keys such as `mode` pass through untouched.
18608
+ */
18609
+ const asClaudeStyleSettings = (document) => {
18610
+ const table = permissionsTableOf(document);
18611
+ return {
18612
+ ...document,
18613
+ permissions: {
18614
+ ...table,
18615
+ allow: toReasonixStringArray(table.allow),
18616
+ ask: toReasonixStringArray(table.ask),
18617
+ deny: toReasonixStringArray(table.deny)
18618
+ }
18619
+ };
18620
+ };
18621
+ /**
18622
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18623
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18624
+ * one.
18625
+ */
18626
+ const withoutEmptyPermissions = (settings) => {
18627
+ const document = { ...settings };
18628
+ const permissions = document.permissions;
18629
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18630
+ return document;
18631
+ };
18632
+ /**
18633
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18634
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18635
+ * `~/.reasonix/config.toml` (global).
18636
+ *
18637
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18638
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18639
+ * while `forbid_read` is documented as absolute paths with no glob support.
18640
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18641
+ */
18642
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18643
+ constructor(params) {
18644
+ super(params);
18645
+ const document = parseSharedConfig({
18646
+ format: "toml",
18647
+ fileContent: this.fileContent
18648
+ });
18649
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18650
+ }
18651
+ static getSettablePaths({ global = false } = {}) {
18652
+ return {
18653
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18654
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18655
+ };
18656
+ }
18657
+ /**
18658
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18659
+ * permissions feature and user-authored tables, so rulesync must never
18660
+ * delete it.
18661
+ */
18662
+ isDeletable() {
18663
+ return false;
18664
+ }
18665
+ toRulesyncIgnore() {
18666
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18667
+ return new RulesyncIgnore({
18668
+ outputRoot: this.outputRoot,
18669
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18670
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18671
+ fileContent: rulesyncPatterns.join("\n")
18672
+ });
18673
+ }
18674
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18675
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18676
+ const paths = this.getSettablePaths({ global });
18677
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18678
+ const existingDocument = parseSharedConfig({
18679
+ format: "toml",
18680
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18681
+ filePath
18682
+ });
18683
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18684
+ settings: asClaudeStyleSettings(existingDocument),
18685
+ readDenies
18686
+ }));
18687
+ return new ReasonixIgnore({
18688
+ outputRoot,
18689
+ relativeDirPath: paths.relativeDirPath,
18690
+ relativeFilePath: paths.relativeFilePath,
18691
+ fileContent: stringifySharedConfig({
18692
+ format: "toml",
18693
+ document
18694
+ }),
18695
+ validate: true,
18696
+ global
18697
+ });
18698
+ }
18699
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18700
+ const paths = this.getSettablePaths({ global });
18701
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18702
+ return new ReasonixIgnore({
18703
+ outputRoot,
18704
+ relativeDirPath: paths.relativeDirPath,
18705
+ relativeFilePath: paths.relativeFilePath,
18706
+ fileContent,
18707
+ validate,
18708
+ global
18709
+ });
18710
+ }
18711
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18712
+ return new ReasonixIgnore({
18713
+ outputRoot,
18714
+ relativeDirPath,
18715
+ relativeFilePath,
18716
+ fileContent: "",
18717
+ validate: false,
18718
+ global
18719
+ });
18720
+ }
18721
+ };
18722
+ //#endregion
17998
18723
  //#region src/features/ignore/roo-ignore.ts
17999
18724
  /**
18000
18725
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -18152,6 +18877,14 @@ const ZED_GLOBAL_WIN32_DIR = join("AppData", "Roaming", "Zed");
18152
18877
  function getZedGlobalDir() {
18153
18878
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
18154
18879
  }
18880
+ /**
18881
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18882
+ * spelling per platform, but the shared-write derivation (and the gateway
18883
+ * ownership table it is checked against) must know both on every platform.
18884
+ */
18885
+ function getZedOtherPlatformGlobalDir() {
18886
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18887
+ }
18155
18888
  const ZED_SETTINGS_FILE_NAME = "settings.json";
18156
18889
  const ZED_RULE_FILE_NAME = ".rules";
18157
18890
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -18164,12 +18897,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18164
18897
  const jsonValue = JSON.parse(this.fileContent);
18165
18898
  this.patterns = jsonValue.private_files ?? [];
18166
18899
  }
18167
- static getSettablePaths() {
18900
+ static getSettablePaths({ global = false } = {}) {
18168
18901
  return {
18169
- relativeDirPath: ZED_DIR,
18902
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
18170
18903
  relativeFilePath: ZED_SETTINGS_FILE_NAME
18171
18904
  };
18172
18905
  }
18906
+ /** @see getZedOtherPlatformGlobalDir */
18907
+ static getExtraSharedWritePaths({ global = false } = {}) {
18908
+ if (!global) return [];
18909
+ return [{
18910
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18911
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18912
+ }];
18913
+ }
18173
18914
  /**
18174
18915
  * ZedIgnore uses settings.json which is a user-managed config file.
18175
18916
  * It should not be deleted by rulesync.
@@ -18180,48 +18921,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18180
18921
  toRulesyncIgnore() {
18181
18922
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
18182
18923
  return new RulesyncIgnore({
18183
- outputRoot: this.outputRoot,
18924
+ outputRoot: ".",
18184
18925
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18185
18926
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18186
18927
  fileContent
18187
18928
  });
18188
18929
  }
18189
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18930
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18190
18931
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
18191
- const filePath = join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
18932
+ const paths = this.getSettablePaths({ global });
18933
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18192
18934
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
18193
- const mergedPatterns = uniq([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18935
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
18194
18936
  return new ZedIgnore({
18195
18937
  outputRoot,
18196
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18197
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18938
+ relativeDirPath: paths.relativeDirPath,
18939
+ relativeFilePath: paths.relativeFilePath,
18198
18940
  fileContent: applySharedConfigPatch({
18199
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18941
+ fileKey: sharedConfigFileKey(paths),
18200
18942
  feature: "ignore",
18201
18943
  existingContent: existingFileContent,
18202
- patch: { private_files: mergedPatterns },
18944
+ patch: { private_files: managedPatterns },
18203
18945
  filePath
18204
18946
  }),
18205
- validate: true
18947
+ validate: true,
18948
+ global
18206
18949
  });
18207
18950
  }
18208
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
18209
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
18951
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18952
+ const paths = this.getSettablePaths({ global });
18953
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
18210
18954
  return new ZedIgnore({
18211
18955
  outputRoot,
18212
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18213
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18956
+ relativeDirPath: paths.relativeDirPath,
18957
+ relativeFilePath: paths.relativeFilePath,
18214
18958
  fileContent,
18215
- validate
18959
+ validate,
18960
+ global
18216
18961
  });
18217
18962
  }
18218
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18963
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18219
18964
  return new ZedIgnore({
18220
18965
  outputRoot,
18221
18966
  relativeDirPath,
18222
18967
  relativeFilePath,
18223
18968
  fileContent: "{}",
18224
- validate: false
18969
+ validate: false,
18970
+ global
18225
18971
  });
18226
18972
  }
18227
18973
  };
@@ -18243,6 +18989,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
18243
18989
  ["kiro-cli", { class: KiroIgnore }],
18244
18990
  ["kiro-ide", { class: KiroIgnore }],
18245
18991
  ["qwencode", { class: QwencodeIgnore }],
18992
+ ["reasonix", { class: ReasonixIgnore }],
18246
18993
  ["roo", { class: RooIgnore }],
18247
18994
  ["devin", { class: DevinIgnore }],
18248
18995
  ["vibe", { class: VibeIgnore }],
@@ -18253,7 +19000,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
18253
19000
  const ignoreProcessorGlobalToolTargets = [
18254
19001
  "kiro",
18255
19002
  "kiro-cli",
18256
- "kiro-ide"
19003
+ "kiro-ide",
19004
+ "reasonix",
19005
+ "zed"
18257
19006
  ];
18258
19007
  const defaultGetFactory$4 = (target) => {
18259
19008
  const factory = toolIgnoreFactories.get(target);
@@ -20849,7 +21598,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20849
21598
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20850
21599
  this.config = merged;
20851
21600
  super.setFileContent(applySharedConfigPatch({
20852
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21601
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20853
21602
  feature: "mcp",
20854
21603
  existingContent: fileContent,
20855
21604
  patch: { mcp_servers: merged.mcp_servers }
@@ -20867,6 +21616,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20867
21616
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20868
21617
  };
20869
21618
  }
21619
+ /**
21620
+ * `config.yaml` under every spelling the global profile root can take.
21621
+ * @see getHermesagentSharedConfigWritePaths
21622
+ */
21623
+ static getExtraSharedWritePaths() {
21624
+ return getHermesagentSharedConfigWritePaths();
21625
+ }
20870
21626
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20871
21627
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20872
21628
  const paths = this.getSettablePaths({ global });
@@ -20893,7 +21649,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20893
21649
  relativeDirPath: paths.relativeDirPath,
20894
21650
  relativeFilePath: paths.relativeFilePath,
20895
21651
  fileContent: applySharedConfigPatch({
20896
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21652
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20897
21653
  feature: "mcp",
20898
21654
  existingContent: fileContent,
20899
21655
  patch: { mcp_servers: merged.mcp_servers }
@@ -21354,9 +22110,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
21354
22110
  * Merge a list of project rule file globs into the `instructions` array of the
21355
22111
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
21356
22112
  * (notably `mcp`/`tools` written by the MCP feature). In Kilo v7, files under
21357
- * `.kilo/rules/` are NOT auto-loaded; they are only picked up when listed in
21358
- * the `instructions` key. The resulting `instructions` list is deduped and
21359
- * sorted for a stable output.
22113
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22114
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22115
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22116
+ * config load — which is why `KiloRule` registers instructions in project
22117
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22118
+ * stable output.
21360
22119
  *
21361
22120
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
21362
22121
  */
@@ -21569,7 +22328,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21569
22328
  const existingContent = existing.content;
21570
22329
  const existingSection = existing.mcp;
21571
22330
  const fileContent = applySharedConfigPatch({
21572
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22331
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21573
22332
  feature: "mcp",
21574
22333
  existingContent,
21575
22334
  patch: { mcp: {
@@ -21656,11 +22415,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21656
22415
  * derivation sees this feature as one of that file's writers — it is not a
21657
22416
  * settable path, since the servers themselves live in `mcp.json`.
21658
22417
  */
21659
- static getExtraSharedWritePaths({ global = false } = {}) {
21660
- return global ? [{
21661
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21662
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21663
- }] : [];
22418
+ static getExtraSharedWritePaths() {
22419
+ return getKimiCodeSharedConfigWritePaths();
21664
22420
  }
21665
22421
  /**
21666
22422
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -23296,16 +24052,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
23296
24052
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23297
24053
  };
23298
24054
  }
23299
- /**
23300
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
23301
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
23302
- * derivation (and the gateway ownership table it is checked against) must
23303
- * know both spellings on every platform.
23304
- */
24055
+ /** @see getZedOtherPlatformGlobalDir */
23305
24056
  static getExtraSharedWritePaths({ global = false } = {}) {
23306
24057
  if (!global) return [];
23307
24058
  return [{
23308
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24059
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
23309
24060
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23310
24061
  }];
23311
24062
  }
@@ -27735,6 +28486,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27735
28486
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27736
28487
  };
27737
28488
  }
28489
+ /**
28490
+ * `config.yaml` under every spelling the global profile root can take.
28491
+ * @see getHermesagentSharedConfigWritePaths
28492
+ */
28493
+ static getExtraSharedWritePaths() {
28494
+ return getHermesagentSharedConfigWritePaths();
28495
+ }
27738
28496
  constructor(params) {
27739
28497
  super({
27740
28498
  ...params,
@@ -27772,7 +28530,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27772
28530
  }
27773
28531
  setFileContent(fileContent) {
27774
28532
  this.fileContent = applySharedConfigPatch({
27775
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28533
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27776
28534
  feature: "permissions",
27777
28535
  existingContent: fileContent,
27778
28536
  patch: parseSharedConfig({
@@ -28185,7 +28943,10 @@ const KiloPermissionSchema = z.union([z.enum([
28185
28943
  "ask",
28186
28944
  "deny"
28187
28945
  ]))]);
28188
- const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
28946
+ const KiloPermissionsConfigSchema = z.looseObject({
28947
+ permission: z.optional(z.record(z.string(), KiloPermissionSchema)),
28948
+ sandbox: z.optional(z.unknown())
28949
+ });
28189
28950
  /**
28190
28951
  * Kilo permission keys that share a name with a canonical rulesync category and
28191
28952
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -28242,6 +29003,25 @@ function collectKiloDenyPatterns(value) {
28242
29003
  }
28243
29004
  return [];
28244
29005
  }
29006
+ function asKiloRecord(value) {
29007
+ return isPlainObject$1(value) ? { ...value } : {};
29008
+ }
29009
+ /**
29010
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
29011
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
29012
+ * project config merely tighten — so writing the wider keys into a project file
29013
+ * would produce config Kilo ignores.
29014
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
29015
+ */
29016
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29017
+ function narrowSandboxToProjectScope({ authored, logger }) {
29018
+ const emitted = {};
29019
+ const dropped = [];
29020
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29021
+ else dropped.push(key);
29022
+ if (dropped.length > 0) logger?.warn(`Kilo honors these 'sandbox' keys from the global config only, so they were dropped from the project config: ${dropped.toSorted().join(", ")}. A project config may only tighten the sandbox ('enabled', 'network'); generate with --global to author the rest.`);
29023
+ return emitted;
29024
+ }
28245
29025
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
28246
29026
  json;
28247
29027
  constructor(params) {
@@ -28321,8 +29101,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28321
29101
  const basePaths = KiloPermissions.getSettablePaths({ global });
28322
29102
  const filePath = join(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
28323
29103
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
28324
- const parsedPermission = parsed.permission;
28325
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29104
+ const existingPermission = asKiloRecord(parsed.permission);
28326
29105
  const rulesyncJson = rulesyncPermissions.getJson();
28327
29106
  const kiloOverride = rulesyncJson.kilo;
28328
29107
  const incomingPermission = {
@@ -28348,6 +29127,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28348
29127
  ...parsed,
28349
29128
  permission: mergedPermission
28350
29129
  };
29130
+ if (kiloOverride?.sandbox !== void 0) {
29131
+ const authored = asKiloRecord(kiloOverride.sandbox);
29132
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29133
+ authored,
29134
+ logger
29135
+ });
29136
+ const merged = {
29137
+ ...asKiloRecord(parsed.sandbox),
29138
+ ...emitted
29139
+ };
29140
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29141
+ }
28351
29142
  return new KiloPermissions({
28352
29143
  outputRoot,
28353
29144
  relativeDirPath: basePaths.relativeDirPath,
@@ -28362,9 +29153,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28362
29153
  const overrideOnly = {};
28363
29154
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
28364
29155
  else overrideOnly[key] = value;
28365
- const json = Object.keys(overrideOnly).length > 0 ? {
29156
+ const sandbox = this.json.sandbox;
29157
+ const override = {
29158
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29159
+ ...isPlainObject$1(sandbox) && { sandbox }
29160
+ };
29161
+ const json = Object.keys(override).length > 0 ? {
28366
29162
  permission: shared,
28367
- kilo: { permission: overrideOnly }
29163
+ kilo: override
28368
29164
  } : { permission: shared };
28369
29165
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
28370
29166
  }
@@ -28605,6 +29401,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28605
29401
  isDeletable() {
28606
29402
  return false;
28607
29403
  }
29404
+ /**
29405
+ * `config.toml` under both spellings its directory can take.
29406
+ * @see getKimiCodeSharedConfigWritePaths
29407
+ */
29408
+ static getExtraSharedWritePaths() {
29409
+ return getKimiCodeSharedConfigWritePaths();
29410
+ }
28608
29411
  shouldMergeExistingFileContent() {
28609
29412
  return true;
28610
29413
  }
@@ -28619,7 +29422,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28619
29422
  patch
28620
29423
  });
28621
29424
  this.fileContent = applySharedConfigPatch({
28622
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29425
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
28623
29426
  feature: "permissions",
28624
29427
  existingContent: fileContent,
28625
29428
  patch: {
@@ -29543,14 +30346,6 @@ function parseReasonixConfig(fileContent) {
29543
30346
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
29544
30347
  return { ...parsed };
29545
30348
  }
29546
- function toStringArray$1(value) {
29547
- if (!Array.isArray(value)) return [];
29548
- return value.filter((entry) => typeof entry === "string");
29549
- }
29550
- function toPermissionsTable(value) {
29551
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
29552
- return { ...value };
29553
- }
29554
30349
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
29555
30350
  /**
29556
30351
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -29607,12 +30402,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29607
30402
  const config = rulesyncPermissions.getJson();
29608
30403
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
29609
30404
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
29610
- const existingPermissions = toPermissionsTable(parsed.permissions);
29611
- const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29612
- const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29613
- const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30405
+ const existingPermissions = toReasonixTable(parsed.permissions);
30406
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30407
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30408
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29614
30409
  if (logger && managedToolNames.has("Read")) {
29615
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30410
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
29616
30411
  const { toolName } = parseReasonixPermissionEntry(entry);
29617
30412
  return toolName === "Read";
29618
30413
  });
@@ -29659,11 +30454,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29659
30454
  });
29660
30455
  }
29661
30456
  toRulesyncPermissions() {
29662
- const permissions = toPermissionsTable(this.toml.permissions);
30457
+ const permissions = toReasonixTable(this.toml.permissions);
29663
30458
  const config = convertReasonixToRulesyncPermissions({
29664
- allow: toStringArray$1(permissions.allow),
29665
- ask: toStringArray$1(permissions.ask),
29666
- deny: toStringArray$1(permissions.deny)
30459
+ allow: toReasonixStringArray(permissions.allow),
30460
+ ask: toReasonixStringArray(permissions.ask),
30461
+ deny: toReasonixStringArray(permissions.deny)
29667
30462
  });
29668
30463
  const sandbox = asReasonixRecord(this.toml.sandbox);
29669
30464
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29755,19 +30550,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29755
30550
  "open_files",
29756
30551
  "expand_code_chunks",
29757
30552
  "expand_folder",
29758
- "grep"
30553
+ "grep",
30554
+ "getJiraIssue",
30555
+ "getConfluencePage"
29759
30556
  ],
29760
30557
  edit: [
29761
30558
  "find_and_replace_code",
29762
30559
  "create_file",
29763
30560
  "delete_file",
29764
- "move_file"
30561
+ "move_file",
30562
+ "createTechnicalPlan",
30563
+ "createJiraIssue",
30564
+ "updateJiraIssue",
30565
+ "createConfluencePage",
30566
+ "updateConfluencePage"
29765
30567
  ],
29766
30568
  write: [
29767
30569
  "create_file",
29768
30570
  "delete_file",
29769
30571
  "move_file",
29770
- "find_and_replace_code"
30572
+ "find_and_replace_code",
30573
+ "createTechnicalPlan",
30574
+ "createJiraIssue",
30575
+ "updateJiraIssue",
30576
+ "createConfluencePage",
30577
+ "updateConfluencePage"
29771
30578
  ]
29772
30579
  };
29773
30580
  const TOOL_KEY_TO_CATEGORY = {
@@ -29775,13 +30582,24 @@ const TOOL_KEY_TO_CATEGORY = {
29775
30582
  expand_code_chunks: "read",
29776
30583
  expand_folder: "read",
29777
30584
  grep: "read",
30585
+ getJiraIssue: "read",
30586
+ getConfluencePage: "read",
29778
30587
  find_and_replace_code: "edit",
29779
30588
  create_file: "edit",
29780
30589
  delete_file: "edit",
29781
- move_file: "edit"
30590
+ move_file: "edit",
30591
+ createTechnicalPlan: "edit",
30592
+ createJiraIssue: "edit",
30593
+ updateJiraIssue: "edit",
30594
+ createConfluencePage: "edit",
30595
+ updateConfluencePage: "edit"
29782
30596
  };
29783
30597
  const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
29784
- const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
30598
+ const OWNED_TOOL_PERMISSION_KEYS = [
30599
+ "bash",
30600
+ "allowedExternalPaths",
30601
+ "default"
30602
+ ];
29785
30603
  /**
29786
30604
  * Permissions adapter for Rovo Dev CLI.
29787
30605
  *
@@ -29795,9 +30613,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29795
30613
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29796
30614
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29797
30615
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30616
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30617
+ * the level Rovo Dev falls back to for any tool with no more specific
30618
+ * setting (Rovo Dev's own default is `ask`).
29798
30619
  * - `read` -> the inspection tools (`open_files`, `expand_code_chunks`,
29799
- * `expand_folder`, `grep`); `edit`/`write` -> the mutation tools
29800
- * (`find_and_replace_code`, `create_file`, `delete_file`, `move_file`).
30620
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30621
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30622
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30623
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30624
+ * `updateConfluencePage`) — so these two categories reach Jira and
30625
+ * Confluence, not just the working tree.
29801
30626
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29802
30627
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29803
30628
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -29977,6 +30802,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
29977
30802
  delete toolPermissions.allowedExternalPaths;
29978
30803
  strippedKeys.push("allowedExternalPaths");
29979
30804
  }
30805
+ if (toolPermissions.default === "allow") {
30806
+ delete toolPermissions.default;
30807
+ strippedKeys.push("default");
30808
+ }
29980
30809
  const bash = toolPermissions.bash;
29981
30810
  if (isRecord(bash)) {
29982
30811
  const stripped = { ...bash };
@@ -30001,10 +30830,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
30001
30830
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30002
30831
  const toolPermissions = {};
30003
30832
  const allowedExternalPaths = [];
30004
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30005
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30006
- if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30833
+ warnOnEditWriteConflict({
30834
+ config,
30835
+ logger
30836
+ });
30007
30837
  for (const [category, rules] of Object.entries(config.permission)) {
30838
+ if (category === CATCH_ALL_PATTERN$1) {
30839
+ const toolWideDefault = convertAllToolsRules({
30840
+ rules,
30841
+ logger
30842
+ });
30843
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30844
+ continue;
30845
+ }
30008
30846
  if (category === "bash") {
30009
30847
  const bash = convertBashRules(rules);
30010
30848
  if (bash) toolPermissions.bash = bash;
@@ -30031,6 +30869,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30031
30869
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
30032
30870
  return toolPermissions;
30033
30871
  }
30872
+ /**
30873
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30874
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30875
+ * is happening; the conversion keeps the stricter of the two — the same
30876
+ * fail-closed rule the import direction uses when those tools disagree, so the
30877
+ * resolution never grants more than the author asked for.
30878
+ */
30879
+ function warnOnEditWriteConflict({ config, logger }) {
30880
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30881
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30882
+ if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30883
+ }
30884
+ /**
30885
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30886
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30887
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30888
+ * under `*` have no counterpart (the default is a single level), so they are
30889
+ * reported and skipped like any other rule Rovo Dev cannot express.
30890
+ */
30891
+ function convertAllToolsRules({ rules, logger }) {
30892
+ let toolWideDefault;
30893
+ for (const [pattern, action] of Object.entries(rules)) {
30894
+ if (pattern === CATCH_ALL_PATTERN$1) {
30895
+ toolWideDefault = action;
30896
+ continue;
30897
+ }
30898
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30899
+ }
30900
+ return toolWideDefault;
30901
+ }
30034
30902
  function convertBashRules(rules) {
30035
30903
  const bash = {};
30036
30904
  const commands = [];
@@ -30052,6 +30920,7 @@ function convertBashRules(rules) {
30052
30920
  */
30053
30921
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30054
30922
  const permission = {};
30923
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
30055
30924
  const bash = toolPermissions.bash;
30056
30925
  if (isRecord(bash)) {
30057
30926
  const bashRules = {};
@@ -30062,12 +30931,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30062
30931
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
30063
30932
  }
30064
30933
  const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
30065
- for (const [toolKey, category] of Object.entries(TOOL_KEY_TO_CATEGORY)) {
30066
- const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30067
- if (!isPermissionAction(value)) continue;
30934
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30935
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30936
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30937
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30938
+ return isPermissionAction(value) ? value : void 0;
30939
+ });
30940
+ if (levels.every((level) => level === void 0)) continue;
30068
30941
  permission[category] ??= {};
30069
- const current = permission[category][CATCH_ALL_PATTERN$1];
30070
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30942
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
30071
30943
  }
30072
30944
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
30073
30945
  permission.read ??= {};
@@ -30899,16 +31771,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30899
31771
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30900
31772
  };
30901
31773
  }
30902
- /**
30903
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
30904
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
30905
- * derivation (and the gateway ownership table it is checked against) must
30906
- * know both spellings on every platform.
30907
- */
31774
+ /** @see getZedOtherPlatformGlobalDir */
30908
31775
  static getExtraSharedWritePaths({ global = false } = {}) {
30909
31776
  if (!global) return [];
30910
31777
  return [{
30911
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31778
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30912
31779
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30913
31780
  }];
30914
31781
  }
@@ -39480,7 +40347,7 @@ def register(ctx):
39480
40347
  _register_subagent(ctx, subagent)
39481
40348
  `;
39482
40349
  }
39483
- function getEnabledPluginConfigContent(currentContent) {
40350
+ function getEnabledPluginConfigContent({ currentContent, global }) {
39484
40351
  const config = parseSharedConfig({
39485
40352
  format: "yaml",
39486
40353
  fileContent: currentContent
@@ -39488,7 +40355,7 @@ function getEnabledPluginConfigContent(currentContent) {
39488
40355
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
39489
40356
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
39490
40357
  return applySharedConfigPatch({
39491
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40358
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
39492
40359
  feature: "subagents",
39493
40360
  existingContent: currentContent,
39494
40361
  patch: { plugins: {
@@ -39539,6 +40406,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39539
40406
  return !targets || targets.includes("*") || targets.includes("hermesagent");
39540
40407
  }
39541
40408
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40409
+ const pluginDirPath = getHermesagentRelativeDirPath({
40410
+ global,
40411
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40412
+ });
39542
40413
  return [
39543
40414
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
39544
40415
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -39547,20 +40418,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39547
40418
  global
39548
40419
  })),
39549
40420
  new HermesagentSubagent({
39550
- relativeDirPath: getHermesagentRelativeDirPath({
39551
- global,
39552
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39553
- }),
40421
+ relativeDirPath: pluginDirPath,
39554
40422
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
39555
40423
  fileContent: "",
39556
40424
  outputRoot,
39557
40425
  global
39558
40426
  }),
39559
40427
  new HermesagentSubagent({
39560
- relativeDirPath: getHermesagentRelativeDirPath({
39561
- global,
39562
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39563
- }),
40428
+ relativeDirPath: pluginDirPath,
39564
40429
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
39565
40430
  fileContent: "",
39566
40431
  outputRoot,
@@ -39600,14 +40465,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39600
40465
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
39601
40466
  * so the write must be declared for the shared-file order derivation.
39602
40467
  */
39603
- static getExtraSharedWritePaths({ global = false } = {}) {
39604
- return global ? [{
39605
- relativeDirPath: getHermesagentRelativeDirPath({
39606
- global,
39607
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
39608
- }),
39609
- relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
39610
- }] : [];
40468
+ /**
40469
+ * `config.yaml` under every spelling the global profile root can take.
40470
+ * @see getHermesagentSharedConfigWritePaths
40471
+ */
40472
+ static getExtraSharedWritePaths() {
40473
+ return getHermesagentSharedConfigWritePaths();
39611
40474
  }
39612
40475
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
39613
40476
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -39640,7 +40503,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39640
40503
  }
39641
40504
  setFileContent(newFileContent) {
39642
40505
  if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) {
39643
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40506
+ super.setFileContent(getEnabledPluginConfigContent({
40507
+ currentContent: newFileContent,
40508
+ global: this.global
40509
+ }));
39644
40510
  return;
39645
40511
  }
39646
40512
  super.setFileContent(newFileContent);
@@ -39648,7 +40514,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39648
40514
  getFileContent() {
39649
40515
  if (this.getRelativeFilePath() === basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
39650
40516
  if (this.getRelativeFilePath() === basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH)) return getPluginInitContent();
39651
- if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent(super.getFileContent());
40517
+ if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40518
+ currentContent: super.getFileContent(),
40519
+ global: this.global
40520
+ });
39652
40521
  return super.getFileContent();
39653
40522
  }
39654
40523
  };
@@ -44727,10 +45596,13 @@ var JunieRule = class JunieRule extends ToolRule {
44727
45596
  //#region src/features/rules/kilo-rule.ts
44728
45597
  var KiloRule = class KiloRule extends ToolRule {
44729
45598
  static getSettablePaths({ global, excludeToolDir } = {}) {
44730
- if (global) return { root: {
44731
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
44732
- relativeFilePath: KILO_RULE_FILE_NAME
44733
- } };
45599
+ if (global) return {
45600
+ root: {
45601
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45602
+ relativeFilePath: KILO_RULE_FILE_NAME
45603
+ },
45604
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45605
+ };
44734
45606
  return {
44735
45607
  root: {
44736
45608
  relativeDirPath: ".",
@@ -46315,6 +47187,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46315
47187
  extension: "md",
46316
47188
  supportsGlobal: false,
46317
47189
  ruleDiscoveryMode: "toon",
47190
+ collisionPolicy: "compose",
46318
47191
  additionalConventions: {
46319
47192
  commands: { commandClass: AgentsmdCommand },
46320
47193
  subagents: { subagentClass: AgentsmdSubagent },
@@ -46335,7 +47208,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46335
47208
  meta: {
46336
47209
  extension: "md",
46337
47210
  supportsGlobal: true,
46338
- ruleDiscoveryMode: "toon"
47211
+ ruleDiscoveryMode: "toon",
47212
+ collisionPolicy: "compose"
46339
47213
  }
46340
47214
  }],
46341
47215
  ["antigravity-cli", {
@@ -46412,7 +47286,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46412
47286
  extension: "md",
46413
47287
  supportsGlobal: true,
46414
47288
  ruleDiscoveryMode: "auto",
46415
- foldsNonRootIntoRoot: true
47289
+ collisionPolicy: "fold"
46416
47290
  }
46417
47291
  }],
46418
47292
  ["copilot", {
@@ -46445,7 +47319,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46445
47319
  extension: "md",
46446
47320
  supportsGlobal: true,
46447
47321
  ruleDiscoveryMode: "auto",
46448
- foldsNonRootIntoRoot: true
47322
+ collisionPolicy: "fold"
46449
47323
  }
46450
47324
  }],
46451
47325
  ["factorydroid", {
@@ -46453,7 +47327,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46453
47327
  meta: {
46454
47328
  extension: "md",
46455
47329
  supportsGlobal: true,
46456
- ruleDiscoveryMode: "toon"
47330
+ ruleDiscoveryMode: "toon",
47331
+ collisionPolicy: "compose"
46457
47332
  }
46458
47333
  }],
46459
47334
  ["goose", {
@@ -46462,7 +47337,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46462
47337
  extension: "md",
46463
47338
  supportsGlobal: true,
46464
47339
  ruleDiscoveryMode: "auto",
46465
- foldsNonRootIntoRoot: true
47340
+ collisionPolicy: "fold"
46466
47341
  }
46467
47342
  }],
46468
47343
  ["hermesagent", {
@@ -46471,7 +47346,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46471
47346
  extension: "md",
46472
47347
  supportsGlobal: false,
46473
47348
  ruleDiscoveryMode: "auto",
46474
- foldsNonRootIntoRoot: true
47349
+ collisionPolicy: "fold"
46475
47350
  }
46476
47351
  }],
46477
47352
  ["grokcli", {
@@ -46488,7 +47363,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46488
47363
  extension: "md",
46489
47364
  supportsGlobal: true,
46490
47365
  ruleDiscoveryMode: "auto",
46491
- foldsNonRootIntoRoot: true
47366
+ collisionPolicy: "fold"
46492
47367
  }
46493
47368
  }],
46494
47369
  ["kilo", {
@@ -46497,7 +47372,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46497
47372
  extension: "md",
46498
47373
  supportsGlobal: true,
46499
47374
  ruleDiscoveryMode: "auto",
46500
- mcpInstructionsRegistrar: KiloMcp
47375
+ mcpInstructionsRegistrar: KiloMcp,
47376
+ collisionPolicy: "compose"
46501
47377
  }
46502
47378
  }],
46503
47379
  ["kimi-code", {
@@ -46506,7 +47382,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46506
47382
  extension: "md",
46507
47383
  supportsGlobal: true,
46508
47384
  ruleDiscoveryMode: "auto",
46509
- foldsNonRootIntoRoot: true
47385
+ collisionPolicy: "fold"
46510
47386
  }
46511
47387
  }],
46512
47388
  ["kiro", {
@@ -46539,7 +47415,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46539
47415
  extension: "md",
46540
47416
  supportsGlobal: true,
46541
47417
  ruleDiscoveryMode: "toon",
46542
- mcpInstructionsRegistrar: OpencodeMcp
47418
+ mcpInstructionsRegistrar: OpencodeMcp,
47419
+ collisionPolicy: "compose"
46543
47420
  }
46544
47421
  }],
46545
47422
  ["pi", {
@@ -46548,7 +47425,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46548
47425
  extension: "md",
46549
47426
  supportsGlobal: true,
46550
47427
  ruleDiscoveryMode: "auto",
46551
- foldsNonRootIntoRoot: true
47428
+ collisionPolicy: "fold"
46552
47429
  }
46553
47430
  }],
46554
47431
  ["qwencode", {
@@ -46566,7 +47443,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46566
47443
  extension: "md",
46567
47444
  supportsGlobal: true,
46568
47445
  ruleDiscoveryMode: "auto",
46569
- foldsNonRootIntoRoot: true
47446
+ collisionPolicy: "fold"
46570
47447
  }
46571
47448
  }],
46572
47449
  ["replit", {
@@ -46623,7 +47500,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46623
47500
  extension: "md",
46624
47501
  supportsGlobal: false,
46625
47502
  ruleDiscoveryMode: "toon",
46626
- foldsNonRootIntoRoot: true
47503
+ collisionPolicy: "fold"
46627
47504
  }
46628
47505
  }],
46629
47506
  ["devin", {
@@ -46692,16 +47569,23 @@ var RulesProcessor = class extends FeatureProcessor {
46692
47569
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
46693
47570
  const factory = this.getFactory(this.toolTarget);
46694
47571
  const { meta } = factory;
46695
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47572
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
46696
47573
  if (!factory.class.isTargetedByRulesyncRule(rulesyncRule)) return null;
46697
- return factory.class.fromRulesyncRule({
46698
- outputRoot: this.outputRoot,
46699
- rulesyncRule,
46700
- validate: true,
46701
- global: this.global
46702
- });
47574
+ return {
47575
+ toolRule: factory.class.fromRulesyncRule({
47576
+ outputRoot: this.outputRoot,
47577
+ rulesyncRule,
47578
+ validate: true,
47579
+ global: this.global
47580
+ }),
47581
+ rulesyncRule
47582
+ };
46703
47583
  }).filter((rule) => rule !== null);
46704
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47584
+ this.mergeRulesByOutputPath({
47585
+ convertedRules,
47586
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47587
+ });
47588
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
46705
47589
  this.applyLocalRootRules({
46706
47590
  toolRules,
46707
47591
  localRootRules,
@@ -46719,7 +47603,12 @@ var RulesProcessor = class extends FeatureProcessor {
46719
47603
  toolRules,
46720
47604
  factory
46721
47605
  });
46722
- return [...toolRules, ...extraFiles];
47606
+ const outputFiles = [...toolRules, ...extraFiles];
47607
+ this.warnForOutputPathCollisions({
47608
+ outputFiles,
47609
+ convertedRules
47610
+ });
47611
+ return outputFiles;
46723
47612
  }
46724
47613
  /**
46725
47614
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -46806,39 +47695,80 @@ var RulesProcessor = class extends FeatureProcessor {
46806
47695
  });
46807
47696
  }
46808
47697
  /**
46809
- * Fold every non-root rule body into the single root rule file.
47698
+ * Reconcile rules that resolve to the same output path.
46810
47699
  *
46811
- * Used for tools whose rules engine reads only one root `AGENTS.md` and neither
46812
- * scans a `memories/` directory nor follows references (deepagents' dcode reads
46813
- * `.deepagents/AGENTS.md`; Warp reads root/subdir `AGENTS.md` but never
46814
- * `.warp/memories/`). Those rule classes emit both root and non-root rules to
46815
- * the same root path, so all bodies must be merged into one instance to avoid
46816
- * colliding on that path (last-writer-wins would silently drop content).
47700
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47701
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47702
+ * neither scans a modular rules directory nor follows references. For example,
47703
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47704
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47705
+ * body into one instance because last-writer-wins would silently drop content.
47706
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
46817
47707
  *
46818
- * The root rule (if any) becomes the merge target and leads the merged content;
46819
- * otherwise the first rule is used so a rule set without a root overview still
46820
- * produces a single, complete file. Mutates `toolRules` in place.
47708
+ * A generated root rule becomes the merge target when present. A `fold` group
47709
+ * without one uses its first rule. A group only composes when every rendered
47710
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47711
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47712
+ * Root-involved collisions that cannot be composed safely fail; other
47713
+ * collisions remain separate and are reported by the final output-path check.
47714
+ * Mutates `convertedRules` in place.
46821
47715
  */
46822
- foldNonRootRulesIntoRootRule(toolRules) {
46823
- if (toolRules.length <= 1) return;
47716
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47717
+ if (convertedRules.length <= 1) return;
46824
47718
  const groups = /* @__PURE__ */ new Map();
46825
- for (const rule of toolRules) {
46826
- const path = join(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47719
+ for (const conversion of convertedRules) {
47720
+ const path = join(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
46827
47721
  const group = groups.get(path);
46828
- if (group) group.push(rule);
46829
- else groups.set(path, [rule]);
47722
+ if (group) group.push(conversion);
47723
+ else groups.set(path, [conversion]);
46830
47724
  }
46831
47725
  const survivors = /* @__PURE__ */ new Set();
46832
- for (const group of groups.values()) {
46833
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47726
+ for (const [path, group] of groups) {
47727
+ if (group.length === 1) {
47728
+ const conversion = group[0];
47729
+ if (conversion) {
47730
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47731
+ survivors.add(conversion);
47732
+ }
47733
+ continue;
47734
+ }
47735
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47736
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47737
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47738
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47739
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47740
+ if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
47741
+ if (!shouldCompose) {
47742
+ for (const conversion of group) survivors.add(conversion);
47743
+ continue;
47744
+ }
47745
+ const target = rootConversion ?? group[0];
46834
47746
  if (!target) continue;
46835
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map((rule) => rule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
46836
- target.setFileContent(mergedContent);
47747
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47748
+ target.toolRule.setFileContent(mergedContent);
46837
47749
  survivors.add(target);
46838
47750
  }
46839
- for (let i = toolRules.length - 1; i >= 0; i--) {
46840
- const rule = toolRules[i];
46841
- if (rule && !survivors.has(rule)) toolRules.splice(i, 1);
47751
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47752
+ const conversion = convertedRules[i];
47753
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47754
+ }
47755
+ }
47756
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47757
+ const seen = /* @__PURE__ */ new Map();
47758
+ const describeSource = (file) => {
47759
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47760
+ return source ? formatRulePaths([source]) : join(file.getRelativeDirPath(), file.getRelativeFilePath());
47761
+ };
47762
+ for (const file of outputFiles) {
47763
+ const path = join(file.getRelativeDirPath(), file.getRelativeFilePath());
47764
+ const key = path.toLowerCase();
47765
+ const previous = seen.get(key);
47766
+ if (previous) {
47767
+ const previousPath = join(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47768
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47769
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47770
+ }
47771
+ seen.set(key, file);
46842
47772
  }
46843
47773
  }
46844
47774
  /**
@@ -46998,14 +47928,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
46998
47928
  }));
46999
47929
  const factory = this.getFactory(this.toolTarget);
47000
47930
  const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47001
- if (targetedRootRules.length > 1) throw new Error(`Multiple root rulesync rules found for target '${this.toolTarget}': ${formatRulePaths(targetedRootRules)}`);
47002
47931
  if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
47003
47932
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47004
47933
  if (targetedLocalRootRules.length > 1) throw new Error(`Multiple localRoot rules found for target '${this.toolTarget}': ${formatRulePaths(targetedLocalRootRules)}. Only one rule can have localRoot: true`);
47005
47934
  if (targetedLocalRootRules.length > 0 && targetedRootRules.length === 0) throw new Error(`localRoot: true requires a root: true rule to exist for target '${this.toolTarget}' (found in ${formatRulePaths(targetedLocalRootRules)})`);
47006
47935
  if (this.global) {
47007
47936
  const globalPaths = factory.class.getSettablePaths({ global: true });
47008
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47937
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
47009
47938
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
47010
47939
  if (nonRootRules.length > 0 && !supportsGlobalNonRoot) this.logger.warn(`${nonRootRules.length} non-root rulesync rules found, but it's in global mode, so ignoring them: ${formatRulePaths(nonRootRules)}`);
47011
47940
  if (targetedLocalRootRules.length > 0) this.logger.warn(`${targetedLocalRootRules.length} localRoot rules found, but localRoot is not supported in global mode, ignoring them: ${formatRulePaths(targetedLocalRootRules)}`);
@@ -47248,14 +48177,36 @@ async function assertPluginRootSafe(params) {
47248
48177
  }
47249
48178
  //#endregion
47250
48179
  //#region src/utils/tool-output-root.ts
48180
+ /** The environment variable each tool reads for its profile root. */
48181
+ const TOOL_HOME_ENV_VARS = {
48182
+ hermesagent: "HERMES_HOME",
48183
+ "kimi-code": "KIMI_CODE_HOME"
48184
+ };
48185
+ /**
48186
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48187
+ * output root in global scope.
48188
+ *
48189
+ * The override wins over `--output-roots`: it names where the tool itself reads
48190
+ * its profile, so writing anywhere else would produce files the tool ignores.
48191
+ *
48192
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48193
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48194
+ * instead of silently becoming the output root. The rejection is re-thrown
48195
+ * naming the variable, since the user never passed an `--output-roots` flag.
48196
+ */
47251
48197
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
47252
48198
  if (!global) return outputRoot;
47253
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48199
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
47254
48200
  outputRoot,
47255
48201
  global
47256
- });
47257
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
47258
- return outputRoot;
48202
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48203
+ if (resolved === outputRoot) return resolved;
48204
+ try {
48205
+ validateOutputRoot(resolved);
48206
+ } catch (error) {
48207
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48208
+ }
48209
+ return resolved;
47259
48210
  }
47260
48211
  //#endregion
47261
48212
  //#region src/lib/convert.ts
@@ -48656,7 +49607,11 @@ async function generateChecksCore(params) {
48656
49607
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
48657
49608
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
48658
49609
  const processor = new ChecksProcessor({
48659
- outputRoot,
49610
+ outputRoot: resolveToolOutputRoot({
49611
+ outputRoot,
49612
+ toolTarget,
49613
+ global: config.getGlobal()
49614
+ }),
48660
49615
  inputRoot: config.getInputRoot(),
48661
49616
  toolTarget,
48662
49617
  global: config.getGlobal(),
@@ -49029,6 +49984,6 @@ async function importChecksCore(params) {
49029
49984
  return writtenCount;
49030
49985
  }
49031
49986
  //#endregion
49032
- export { ErrorCodes as $, RULESYNC_SKILLS_RELATIVE_DIR_PATH as $t, RulesyncMcp as A, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as At, stringifyFrontmatter as B, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Bt, RulesyncSubagent as C, writeFileContent as Ct, RulesyncRule as D, ToolTargetSchema as Dt, RulesyncSkillFrontmatterSchema as E, PACKAGING_TOOL_TARGETS as Et, parseJsonc as F, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Ft, ConfigFileSchema as G, RULESYNC_MCP_SCHEMA_URL as Gt, SHARED_USER_MANAGED_CONFIG_PATHS as H, RULESYNC_MCP_FILE_NAME as Ht, RulesyncCommand as I, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as It, ConsoleLogger as J, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Jt, SourceEntrySchema as K, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Kt, RulesyncCommandFrontmatterSchema as L, RULESYNC_HOOKS_FILE_NAME as Lt, RulesyncHooks as M, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Mt, getRulesyncSourceCandidates as N, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Nt, RulesyncRuleFrontmatterSchema as O, MAX_FILE_SIZE as Ot, resolveRulesyncSourceWritePath as P, RULESYNC_CONFIG_SCHEMA_URL as Pt, CLIError as Q, RULESYNC_RULES_RELATIVE_DIR_PATH as Qt, RulesyncCheck as R, RULESYNC_HOOKS_LEGACY_FILE_NAME as Rt, getLocalSkillDirNames as S, toPosixPath as St, RulesyncSkill as T, ALL_TOOL_TARGETS_WITH_WILDCARD as Tt, SKILL_FILE_NAME as U, RULESYNC_MCP_LEGACY_FILE_NAME as Ut, loadYaml as V, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Vt, ConfigResolver as W, RULESYNC_MCP_RELATIVE_FILE_PATH as Wt, fallbackLogger as X, RULESYNC_PERMISSIONS_SCHEMA_URL as Xt, JsonLogger as Y, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Yt, warnOnConflictingFlags as Z, RULESYNC_RELATIVE_DIR_PATH as Zt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as _, removeFile as _t, convertFromTool as a, directoryExists as at, CODEXCLI_BASH_RULES_FILE_NAME as b, resolvePath as bt, SubagentsProcessor as c, findFilesByGlobs as ct, IgnoreProcessor as d, isSymlink as dt, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as en, assertDirectoryIfExists as et, HooksProcessor as f, listDirectoryFiles as ft, CLAUDECODE_MEMORIES_DIR_NAME as g, removeDirectoryStrict as gt, CLAUDECODE_LOCAL_RULE_FILE_NAME as h, removeDirectory as ht, getProcessorRegistryEntry as i, formatError as in, createTempDirectory as it, RulesyncIgnore as j, RULESYNC_CHECKS_RELATIVE_DIR_PATH as jt, RulesyncPermissions as k, RULESYNC_AIIGNORE_FILE_NAME as kt, SkillsProcessor as l, getFileSize as lt, CLAUDECODE_DIR as m, readFileContentOrNull as mt, checkRulesyncDirExists as n, ALL_FEATURES as nn, assertWritablePathInsideRoot as nt, isPackagingToolTarget as o, ensureDir as ot, CommandsProcessor as p, readFileContent as pt, findControlCharacter as q, RULESYNC_PERMISSIONS_FILE_NAME as qt, generate as r, ALL_FEATURES_WITH_WILDCARD as rn, checkPathTraversal as rt, RulesProcessor as s, fileExists as st, importFromTool as t, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as tn, assertTreeContainsNoSymlinks as tt, McpProcessor as u, getHomeDirectory as ut, CLAUDECODE_SKILLS_DIR_PATH as v, removeFileStrict as vt, RulesyncSubagentFrontmatterSchema as w, ALL_TOOL_TARGETS as wt, CODEXCLI_DIR as x, runWithDirectoryRollback as xt, ChecksProcessor as y, removeTempDirectory as yt, RulesyncCheckFrontmatterSchema as z, RULESYNC_HOOKS_RELATIVE_FILE_PATH as zt };
49987
+ export { warnOnConflictingFlags as $, RULESYNC_RELATIVE_DIR_PATH as $t, RulesyncMcp as A, MAX_FILE_SIZE as At, stringifyFrontmatter as B, RULESYNC_HOOKS_LEGACY_FILE_NAME as Bt, RulesyncSubagent as C, runWithDirectoryRollback as Ct, RulesyncRule as D, ALL_TOOL_TARGETS_WITH_WILDCARD as Dt, RulesyncSkillFrontmatterSchema as E, ALL_TOOL_TARGETS as Et, parseJsonc as F, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Ft, CONFLICTING_TARGET_PAIRS as G, RULESYNC_MCP_LEGACY_FILE_NAME as Gt, SHARED_USER_MANAGED_CONFIG_PATHS as H, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Ht, RulesyncCommand as I, RULESYNC_CONFIG_SCHEMA_URL as It, SourceEntrySchema as J, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Jt, ConfigFileSchema as K, RULESYNC_MCP_RELATIVE_FILE_PATH as Kt, RulesyncCommandFrontmatterSchema as L, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Lt, RulesyncHooks as M, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Mt, getRulesyncSourceCandidates as N, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Nt, RulesyncRuleFrontmatterSchema as O, PACKAGING_TOOL_TARGETS as Ot, resolveRulesyncSourceWritePath as P, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Pt, fallbackLogger as Q, RULESYNC_PERMISSIONS_SCHEMA_URL as Qt, RulesyncCheck as R, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Rt, getLocalSkillDirNames as S, resolvePath as St, RulesyncSkill as T, writeFileContent as Tt, SKILL_FILE_NAME as U, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Ut, loadYaml as V, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Vt, ConfigResolver as W, RULESYNC_MCP_FILE_NAME as Wt, ConsoleLogger as X, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Xt, findControlCharacter as Y, RULESYNC_PERMISSIONS_FILE_NAME as Yt, JsonLogger as Z, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Zt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as _, removeDirectory as _t, convertFromTool as a, ALL_FEATURES_WITH_WILDCARD as an, checkPathTraversal as at, CODEXCLI_BASH_RULES_FILE_NAME as b, removeFileStrict as bt, SubagentsProcessor as c, ensureDir as ct, IgnoreProcessor as d, getFileSize as dt, RULESYNC_RULES_RELATIVE_DIR_PATH as en, CLIError as et, HooksProcessor as f, getHomeDirectory as ft, CLAUDECODE_MEMORIES_DIR_NAME as g, readFileContentOrNull as gt, CLAUDECODE_LOCAL_RULE_FILE_NAME as h, readFileContent as ht, getProcessorRegistryEntry as i, ALL_FEATURES as in, assertWritablePathInsideRoot as it, RulesyncIgnore as j, RULESYNC_AIIGNORE_FILE_NAME as jt, RulesyncPermissions as k, ToolTargetSchema as kt, SkillsProcessor as l, fileExists as lt, CLAUDECODE_DIR as m, listDirectoryFiles as mt, checkRulesyncDirExists as n, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as nn, assertDirectoryIfExists as nt, isPackagingToolTarget as o, DEPRECATED_FEATURE_REPLACEMENTS as on, createTempDirectory as ot, CommandsProcessor as p, isSymlink as pt, GITIGNORE_DESTINATION_KEY as q, RULESYNC_MCP_SCHEMA_URL as qt, generate as r, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as rn, assertTreeContainsNoSymlinks as rt, RulesProcessor as s, formatError as sn, directoryExists as st, importFromTool as t, RULESYNC_SKILLS_RELATIVE_DIR_PATH as tn, ErrorCodes as tt, McpProcessor as u, findFilesByGlobs as ut, CLAUDECODE_SKILLS_DIR_PATH as v, removeDirectoryStrict as vt, RulesyncSubagentFrontmatterSchema as w, toPosixPath as wt, CODEXCLI_DIR as x, removeTempDirectory as xt, ChecksProcessor as y, removeFile as yt, RulesyncCheckFrontmatterSchema as z, RULESYNC_HOOKS_FILE_NAME as zt };
49033
49988
 
49034
- //# sourceMappingURL=import-BmXT7mRS.js.map
49989
+ //# sourceMappingURL=import-Bj5HxY9p.js.map