rulesync 16.1.0 → 16.2.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.
@@ -192,6 +192,7 @@ const ignoreProcessorToolTargetTuple = [
192
192
  "kiro-cli",
193
193
  "kiro-ide",
194
194
  "qwencode",
195
+ "reasonix",
195
196
  "roo",
196
197
  "devin",
197
198
  "vibe",
@@ -249,6 +250,7 @@ const commandsProcessorToolTargetTuple = [
249
250
  "cursor",
250
251
  "factorydroid",
251
252
  "goose",
253
+ "grokcli",
252
254
  "hermesagent",
253
255
  "junie",
254
256
  "kilo",
@@ -404,6 +406,7 @@ const checksProcessorToolTargetTuple = [
404
406
  "amp",
405
407
  "cursor",
406
408
  "hermesagent",
409
+ "rovodev",
407
410
  "takt"
408
411
  ];
409
412
  //#endregion
@@ -1057,6 +1060,17 @@ const ConfigFileSchema = z.object({
1057
1060
  });
1058
1061
  z.required(ConfigParamsSchema);
1059
1062
  /**
1063
+ * Normalizes the configuration file location to an absolute path.
1064
+ *
1065
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1066
+ * only covers direct programmatic construction, where the conventional
1067
+ * location next to the input root is the best guess.
1068
+ */
1069
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1070
+ if (configFilePath === void 0) return join(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1071
+ return isAbsolute(configFilePath) ? configFilePath : resolve(configFilePath);
1072
+ }
1073
+ /**
1060
1074
  * Conflicting target pairs that cannot be used together
1061
1075
  */
1062
1076
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
@@ -1127,8 +1141,9 @@ var Config = class Config {
1127
1141
  dryRun;
1128
1142
  check;
1129
1143
  inputRoot;
1144
+ configFilePath;
1130
1145
  sources;
1131
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1146
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1132
1147
  assertTargetsFeaturesExclusive({
1133
1148
  targets,
1134
1149
  features
@@ -1161,6 +1176,10 @@ var Config = class Config {
1161
1176
  this.dryRun = dryRun ?? false;
1162
1177
  this.check = check ?? false;
1163
1178
  this.inputRoot = inputRoot === void 0 ? process.cwd() : isAbsolute(inputRoot) ? inputRoot : resolve(inputRoot);
1179
+ this.configFilePath = normalizeConfigFilePath({
1180
+ configFilePath,
1181
+ inputRoot: this.inputRoot
1182
+ });
1164
1183
  this.sources = sources ?? [];
1165
1184
  }
1166
1185
  /**
@@ -1354,6 +1373,14 @@ var Config = class Config {
1354
1373
  getInputRoot() {
1355
1374
  return this.inputRoot;
1356
1375
  }
1376
+ /**
1377
+ * Returns the absolute path of the configuration file this config was
1378
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1379
+ * without one — so callers must treat this as a location, not a guarantee.
1380
+ */
1381
+ getConfigFilePath() {
1382
+ return this.configFilePath;
1383
+ }
1357
1384
  getSources() {
1358
1385
  return this.sources;
1359
1386
  }
@@ -1576,6 +1603,7 @@ var ConfigResolver = class {
1576
1603
  fallback: getDefaults().check
1577
1604
  }),
1578
1605
  inputRoot: resolvedInputRoot !== void 0 ? resolve(resolvedInputRoot) : cwd,
1606
+ configFilePath: validatedConfigPath,
1579
1607
  sources: configByFile.sources ?? getDefaults().sources,
1580
1608
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1581
1609
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -3986,10 +4014,30 @@ const ClinePermissionsOverrideSchema = z.looseObject({
3986
4014
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
3987
4015
  * override; each value may be a bare action string or a pattern map.
3988
4016
  *
4017
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4018
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4019
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4020
+ * `writable_paths`. It has no canonical permission category, so it is authored
4021
+ * here and emitted only for Kilo.
4022
+ *
4023
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4024
+ * `writable_paths` are honored from the global config only, and a project
4025
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4026
+ * project-level network denial even clears the global destination exceptions.
4027
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4028
+ * written, and the rest are dropped with a warning rather than emitted into a
4029
+ * file Kilo would ignore.
4030
+ *
3989
4031
  * @example
3990
4032
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4033
+ * @example
4034
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4035
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
3991
4036
  */
3992
- const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
4037
+ const KiloPermissionsOverrideSchema = z.looseObject({
4038
+ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)),
4039
+ sandbox: z.optional(z.looseObject({}))
4040
+ });
3993
4041
  /**
3994
4042
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
3995
4043
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -5298,6 +5346,26 @@ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5298
5346
  const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5299
5347
  const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5300
5348
  //#endregion
5349
+ //#region src/constants/rovodev-paths.ts
5350
+ const ROVODEV_DIR = ".rovodev";
5351
+ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
5352
+ const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
5353
+ const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
5354
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5355
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5356
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5357
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5358
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
5359
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5360
+ const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
5361
+ /**
5362
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5363
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5364
+ * dot in the file name.
5365
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5366
+ */
5367
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5368
+ //#endregion
5301
5369
  //#region src/constants/takt-paths.ts
5302
5370
  const TAKT_DIR = ".takt";
5303
5371
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5759,12 +5827,19 @@ function slugifyCheckName(value) {
5759
5827
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5760
5828
  }
5761
5829
  //#endregion
5762
- //#region src/features/checks/cursor-check.ts
5830
+ //#region src/features/checks/aggregated-check-file.ts
5763
5831
  /**
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.
5832
+ * Shared machinery for the tools whose checks surface is **one aggregated
5833
+ * instruction file** rather than a file per check Cursor Bugbot's
5834
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5835
+ *
5836
+ * Both read the file as free prose, so the check identities have to be carried
5837
+ * in something invisible to the reader: an HTML-comment marker per section.
5838
+ * That marker convention, the escaping that keeps a check body from splitting
5839
+ * itself, and the import-side split are the same for both files, so they live
5840
+ * here once.
5767
5841
  */
5842
+ /** Marks where one check starts inside the single instruction file. */
5768
5843
  const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5769
5844
  /**
5770
5845
  * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
@@ -5775,17 +5850,16 @@ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5775
5850
  */
5776
5851
  const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5777
5852
  const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5778
- const FALLBACK_CHECK_NAME = "bugbot";
5779
- function renderMarker(name) {
5853
+ function renderCheckMarker(name) {
5780
5854
  return `<!-- rulesync:check:${name} -->`;
5781
5855
  }
5782
- function escapeMarkers(content) {
5856
+ function escapeCheckMarkers(content) {
5783
5857
  return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5784
5858
  }
5785
- function unescapeMarkers(content) {
5859
+ function unescapeCheckMarkers(content) {
5786
5860
  return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5787
5861
  }
5788
- function findMarkers(fileContent) {
5862
+ function findCheckMarkers(fileContent) {
5789
5863
  CHECK_MARKER_PATTERN.lastIndex = 0;
5790
5864
  const markers = [];
5791
5865
  let match = CHECK_MARKER_PATTERN.exec(fileContent);
@@ -5800,23 +5874,47 @@ function findMarkers(fileContent) {
5800
5874
  return markers;
5801
5875
  }
5802
5876
  /**
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.
5877
+ * Whether the file holds instruction text ahead of the first marker the
5878
+ * question the "generating replaces this" warning asks. A file with no marker
5879
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5880
+ * since there is nothing to replace.
5881
+ */
5882
+ function hasHandWrittenPreamble(fileContent) {
5883
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5884
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5885
+ }
5886
+ /**
5887
+ * Whether the file is nothing but sections rulesync generated — the question
5888
+ * the deletion guard asks, and a stricter one than
5889
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5890
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5891
+ * empty one is somebody's placeholder rather than our leftover.
5892
+ */
5893
+ function isOnlyGeneratedSections(fileContent) {
5894
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5895
+ if (firstMarkerStart === void 0) return false;
5896
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5897
+ }
5898
+ /**
5899
+ * The instruction text one check contributes. Neither file has a field to put a
5900
+ * summary in, so `description` is used only when there is no body.
5806
5901
  */
5807
5902
  function toInstruction(rulesyncCheck) {
5808
5903
  const body = rulesyncCheck.getBody().trim();
5809
5904
  if (body.length > 0) return body;
5810
5905
  return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5811
5906
  }
5812
- function renderSection(rulesyncCheck) {
5907
+ function renderCheckSection(rulesyncCheck) {
5813
5908
  const name = basename(rulesyncCheck.getRelativeFilePath(), ".md");
5814
5909
  const heading = `## ${name}`;
5815
5910
  const instruction = toInstruction(rulesyncCheck);
5816
- const lines = [renderMarker(name), heading];
5817
- if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5911
+ const lines = [renderCheckMarker(name), heading];
5912
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5818
5913
  return lines.join("\n");
5819
5914
  }
5915
+ function renderCheckFile(rulesyncChecks) {
5916
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5917
+ }
5820
5918
  /** Drop the heading generate writes, so a round trip does not stack headings. */
5821
5919
  function stripGeneratedHeading(section, name) {
5822
5920
  const [firstLine, ...rest] = section.split("\n");
@@ -5824,6 +5922,53 @@ function stripGeneratedHeading(section, name) {
5824
5922
  return section.trim();
5825
5923
  }
5826
5924
  /**
5925
+ * Split an aggregated instruction file back into one check per section.
5926
+ *
5927
+ * Content ahead of the first marker — and a hand-written file with no markers
5928
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5929
+ * is dropped.
5930
+ */
5931
+ function splitCheckFile({ fileContent, fallbackName }) {
5932
+ const sections = [];
5933
+ const markers = findCheckMarkers(fileContent);
5934
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5935
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5936
+ if (preamble.length > 0) sections.push({
5937
+ name: fallbackName,
5938
+ content: unescapeCheckMarkers(preamble)
5939
+ });
5940
+ for (const [index, marker] of markers.entries()) {
5941
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5942
+ const markerName = marker.name.trim();
5943
+ const name = slugifyCheckName(markerName) || fallbackName;
5944
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5945
+ sections.push({
5946
+ name,
5947
+ content: unescapeCheckMarkers(content)
5948
+ });
5949
+ }
5950
+ const used = /* @__PURE__ */ new Set();
5951
+ return sections.map(({ name, content }) => {
5952
+ let uniqueName = name;
5953
+ let suffix = 2;
5954
+ while (used.has(uniqueName)) {
5955
+ uniqueName = `${name}-${suffix}`;
5956
+ suffix += 1;
5957
+ }
5958
+ used.add(uniqueName);
5959
+ return new RulesyncCheck({
5960
+ outputRoot: ".",
5961
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5962
+ relativeFilePath: `${uniqueName}.md`,
5963
+ frontmatter: { targets: ["*"] },
5964
+ body: content
5965
+ });
5966
+ });
5967
+ }
5968
+ //#endregion
5969
+ //#region src/features/checks/cursor-check.ts
5970
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
5971
+ /**
5827
5972
  * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5828
5973
  *
5829
5974
  * Bugbot takes one aggregated instruction file per directory rather than a file
@@ -5880,9 +6025,7 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5880
6025
  const paths = CursorCheck.getSettablePaths();
5881
6026
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5882
6027
  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;
6028
+ return isOnlyGeneratedSections(fileContent);
5886
6029
  }
5887
6030
  static fromRulesyncCheck(_params) {
5888
6031
  throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
@@ -5892,10 +6035,8 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5892
6035
  const paths = CursorCheck.getSettablePaths({ global });
5893
6036
  const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5894
6037
  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`;
6038
+ 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.`);
6039
+ const fileContent = renderCheckFile(rulesyncChecks);
5899
6040
  return [new CursorCheck({
5900
6041
  outputRoot,
5901
6042
  relativeDirPath: paths.relativeDirPath,
@@ -5938,41 +6079,9 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5938
6079
  return first;
5939
6080
  }
5940
6081
  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
- });
6082
+ return splitCheckFile({
6083
+ fileContent: this.getFileContent(),
6084
+ fallbackName: FALLBACK_CHECK_NAME$1
5976
6085
  });
5977
6086
  }
5978
6087
  };
@@ -5994,8 +6103,22 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5994
6103
  */
5995
6104
  /** Project-root instruction file auto-injected by Hermes Agent. */
5996
6105
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
5997
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6106
+ /**
6107
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6108
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6109
+ */
5998
6110
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6111
+ /**
6112
+ * Home-relative global profile root on Windows: upstream defaults to
6113
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6114
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6115
+ *
6116
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6117
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6118
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6119
+ * followed; those users should set `HERMES_HOME` explicitly.
6120
+ */
6121
+ const HERMESAGENT_GLOBAL_WIN32_DIR = join("AppData", "Local", "hermes");
5999
6122
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
6000
6123
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
6001
6124
  const HERMESAGENT_CONFIG_FILE_PATH = join(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -6218,6 +6341,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
6218
6341
  }
6219
6342
  };
6220
6343
  //#endregion
6344
+ //#region src/features/checks/rovodev-check.ts
6345
+ const FALLBACK_CHECK_NAME = "review-agent";
6346
+ /**
6347
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6348
+ * (`.rovodev/.review-agent.md`).
6349
+ *
6350
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6351
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6352
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6353
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6354
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6355
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6356
+ * adapters share).
6357
+ *
6358
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6359
+ * have no equivalent there: they are not written and do not come back on
6360
+ * import. Neither does `description` whenever the check also has a body.
6361
+ *
6362
+ * Project scope only — these are per-repository review instructions, and Rovo
6363
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6364
+ * same tool is the opposite: global only.)
6365
+ *
6366
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6367
+ */
6368
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6369
+ static getSettablePaths(_options = {}) {
6370
+ return {
6371
+ relativeDirPath: ROVODEV_DIR,
6372
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6373
+ };
6374
+ }
6375
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6376
+ return this.isTargetedByRulesyncCheckDefault({
6377
+ rulesyncCheck,
6378
+ toolTarget: "rovodev"
6379
+ });
6380
+ }
6381
+ /**
6382
+ * Ownership guard the processor consults before it deletes anything for this
6383
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6384
+ * to hand-write, so anything in it that rulesync did not write is not
6385
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6386
+ * take somebody's hand-written review instructions with it.
6387
+ */
6388
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6389
+ const paths = RovodevCheck.getSettablePaths();
6390
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6391
+ if (fileContent === null) return true;
6392
+ return isOnlyGeneratedSections(fileContent);
6393
+ }
6394
+ static fromRulesyncCheck(_params) {
6395
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6396
+ }
6397
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6398
+ if (rulesyncChecks.length === 0) return [];
6399
+ const paths = RovodevCheck.getSettablePaths({ global });
6400
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6401
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6402
+ 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.`);
6403
+ return [new RovodevCheck({
6404
+ outputRoot,
6405
+ relativeDirPath: paths.relativeDirPath,
6406
+ relativeFilePath,
6407
+ fileContent: renderCheckFile(rulesyncChecks),
6408
+ global
6409
+ })];
6410
+ }
6411
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6412
+ const paths = RovodevCheck.getSettablePaths({ global });
6413
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6414
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6415
+ return new RovodevCheck({
6416
+ outputRoot,
6417
+ relativeDirPath: paths.relativeDirPath,
6418
+ relativeFilePath,
6419
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6420
+ global
6421
+ });
6422
+ }
6423
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6424
+ return new RovodevCheck({
6425
+ outputRoot,
6426
+ relativeDirPath,
6427
+ relativeFilePath,
6428
+ fileContent: "",
6429
+ validate: false,
6430
+ global
6431
+ });
6432
+ }
6433
+ validate() {
6434
+ return {
6435
+ success: true,
6436
+ error: null
6437
+ };
6438
+ }
6439
+ toRulesyncCheck() {
6440
+ const first = this.toRulesyncChecks()[0];
6441
+ if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6442
+ return first;
6443
+ }
6444
+ toRulesyncChecks() {
6445
+ return splitCheckFile({
6446
+ fileContent: this.getFileContent(),
6447
+ fallbackName: FALLBACK_CHECK_NAME
6448
+ });
6449
+ }
6450
+ };
6451
+ //#endregion
6221
6452
  //#region src/constants/codexcli-paths.ts
6222
6453
  const CODEXCLI_DIR = ".codex";
6223
6454
  const CODEXCLI_PROMPTS_DIR_PATH = join(CODEXCLI_DIR, "prompts");
@@ -6330,11 +6561,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6330
6561
  }
6331
6562
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6332
6563
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6564
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6565
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6333
6566
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6334
6567
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6335
6568
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6336
6569
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6337
6570
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6571
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6338
6572
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6339
6573
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6340
6574
  /**
@@ -6358,6 +6592,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6358
6592
  * lock-step with the writers derived from the processor registry, so an
6359
6593
  * undeclared writer fails CI instead of merging by accident.
6360
6594
  */
6595
+ /**
6596
+ * Hermes writes one `config.yaml`, but its global profile root has three
6597
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6598
+ * itself). They are the same file with the same owners, so the declaration is
6599
+ * written once and shared — a policy edit cannot land on one spelling only.
6600
+ */
6601
+ const HERMES_CONFIG_DECLARATION = {
6602
+ format: "yaml",
6603
+ features: {
6604
+ commands: {
6605
+ kind: "replace-owned-keys",
6606
+ ownedKeys: ["plugins"]
6607
+ },
6608
+ subagents: {
6609
+ kind: "replace-owned-keys",
6610
+ ownedKeys: ["plugins"]
6611
+ },
6612
+ mcp: {
6613
+ kind: "replace-owned-keys",
6614
+ ownedKeys: ["mcp_servers"]
6615
+ },
6616
+ hooks: {
6617
+ kind: "replace-owned-keys",
6618
+ ownedKeys: ["hooks"]
6619
+ },
6620
+ permissions: {
6621
+ kind: "deep-merge",
6622
+ replaceKeys: ["permissions"]
6623
+ }
6624
+ }
6625
+ };
6626
+ /**
6627
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6628
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6629
+ * name the profile directory itself, so the file has two spellings that share
6630
+ * one declaration — a policy edit cannot land on only one of them.
6631
+ */
6632
+ const KIMI_CODE_CONFIG_DECLARATION = {
6633
+ format: "toml",
6634
+ invalidRootPolicy: "error",
6635
+ features: {
6636
+ hooks: {
6637
+ kind: "replace-owned-keys",
6638
+ ownedKeys: ["hooks"]
6639
+ },
6640
+ mcp: {
6641
+ kind: "replace-owned-keys",
6642
+ ownedKeys: ["mcp"]
6643
+ },
6644
+ permissions: {
6645
+ kind: "replace-owned-keys",
6646
+ ownedKeys: [
6647
+ "permission",
6648
+ "default_permission_mode",
6649
+ "tools"
6650
+ ]
6651
+ }
6652
+ }
6653
+ };
6361
6654
  const SHARED_CONFIG_OWNERSHIP = {
6362
6655
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6363
6656
  format: "json",
@@ -6376,31 +6669,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6376
6669
  }
6377
6670
  }
6378
6671
  },
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
- },
6672
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6673
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6674
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6404
6675
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6405
6676
  format: "yaml",
6406
6677
  invalidRootPolicy: "error",
@@ -6446,6 +6717,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6446
6717
  ".config/zed/settings.json": {
6447
6718
  format: "json",
6448
6719
  features: {
6720
+ ignore: {
6721
+ kind: "replace-owned-keys",
6722
+ ownedKeys: ["private_files"]
6723
+ },
6449
6724
  mcp: {
6450
6725
  kind: "replace-owned-keys",
6451
6726
  ownedKeys: ["context_servers"]
@@ -6459,6 +6734,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6459
6734
  "AppData/Roaming/Zed/settings.json": {
6460
6735
  format: "json",
6461
6736
  features: {
6737
+ ignore: {
6738
+ kind: "replace-owned-keys",
6739
+ ownedKeys: ["private_files"]
6740
+ },
6462
6741
  mcp: {
6463
6742
  kind: "replace-owned-keys",
6464
6743
  ownedKeys: ["context_servers"]
@@ -6715,31 +6994,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6715
6994
  }
6716
6995
  }
6717
6996
  },
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
- },
6997
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6998
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6740
6999
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6741
7000
  format: "toml",
6742
7001
  features: {
7002
+ ignore: {
7003
+ kind: "custom",
7004
+ policyFunction: "applyIgnoreReadDenies"
7005
+ },
6743
7006
  mcp: {
6744
7007
  kind: "replace-owned-keys",
6745
7008
  ownedKeys: ["plugins"]
@@ -6757,6 +7020,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6757
7020
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6758
7021
  format: "toml",
6759
7022
  features: {
7023
+ ignore: {
7024
+ kind: "custom",
7025
+ policyFunction: "applyIgnoreReadDenies"
7026
+ },
6760
7027
  mcp: {
6761
7028
  kind: "replace-owned-keys",
6762
7029
  ownedKeys: ["plugins"]
@@ -7201,6 +7468,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7201
7468
  filePattern: "*.json"
7202
7469
  }
7203
7470
  }],
7471
+ ["rovodev", {
7472
+ class: RovodevCheck,
7473
+ meta: {
7474
+ supportsGlobal: false,
7475
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7476
+ }
7477
+ }],
7204
7478
  ["takt", {
7205
7479
  class: TaktCheck,
7206
7480
  meta: {
@@ -7356,19 +7630,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7356
7630
  }
7357
7631
  };
7358
7632
  //#endregion
7633
+ //#region src/utils/tool-home.ts
7634
+ /**
7635
+ * Where the rulesync-side source files of a tool with a home override belong.
7636
+ *
7637
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7638
+ * imported back out of it are not part of the tool's profile — they stay under
7639
+ * the rulesync home. When no override is set, the native output root already is
7640
+ * that place.
7641
+ */
7642
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7643
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7644
+ }
7645
+ //#endregion
7359
7646
  //#region src/utils/hermesagent.ts
7360
7647
  function getHermesagentHome() {
7361
7648
  const configuredHome = process.env.HERMES_HOME?.trim();
7362
7649
  return configuredHome ? resolve(configuredHome) : void 0;
7363
7650
  }
7651
+ /**
7652
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7653
+ *
7654
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7655
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7656
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7657
+ * would land where Hermes never reads.
7658
+ *
7659
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7660
+ */
7661
+ function getHermesagentGlobalDir() {
7662
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7663
+ }
7364
7664
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7365
7665
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7366
7666
  }
7667
+ /**
7668
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7669
+ * actually writes in the requested scope.
7670
+ *
7671
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7672
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7673
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7674
+ * platform default directory takes its place.
7675
+ */
7367
7676
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7368
- if (!global || !getHermesagentHome()) return relativeDirPath;
7677
+ if (!global) return relativeDirPath;
7369
7678
  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;
7679
+ try {
7680
+ checkPathTraversal({
7681
+ relativePath: relativeDirPath,
7682
+ intendedRootDir: "."
7683
+ });
7684
+ checkPathTraversal({
7685
+ relativePath,
7686
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7687
+ });
7688
+ } catch {
7689
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7690
+ }
7691
+ return getHermesagentHome() ? relativePath || "." : join(getHermesagentGlobalDir(), relativePath);
7372
7692
  }
7373
7693
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7374
7694
  return join(getHermesagentRelativeDirPath({
@@ -7376,8 +7696,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7376
7696
  relativeDirPath: dirname(relativeFilePath)
7377
7697
  }), basename(relativeFilePath));
7378
7698
  }
7699
+ /**
7700
+ * Every spelling `config.yaml` can take in global scope, so that the
7701
+ * shared-write derivation and the gateway ownership table it is checked against
7702
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7703
+ *
7704
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7705
+ * which would otherwise make the derived shared-file key depend on the ambient
7706
+ * environment — the drift guards would then go blind in precisely the
7707
+ * configuration this feature exists for.
7708
+ */
7709
+ function getHermesagentSharedConfigWritePaths() {
7710
+ return [
7711
+ {
7712
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7713
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7714
+ },
7715
+ {
7716
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7717
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7718
+ },
7719
+ {
7720
+ relativeDirPath: ".",
7721
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7722
+ }
7723
+ ];
7724
+ }
7725
+ /**
7726
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7727
+ * writes. All three spellings carry the same declaration, but passing the key of
7728
+ * the file being written keeps the write path and the drift guards reading the
7729
+ * same entry.
7730
+ */
7731
+ function getHermesagentConfigSharedFileKey({ global }) {
7732
+ return sharedConfigFileKey({
7733
+ relativeDirPath: getHermesagentRelativeDirPath({
7734
+ global,
7735
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7736
+ }),
7737
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7738
+ });
7739
+ }
7379
7740
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7380
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7741
+ return getToolRulesyncOutputRoot({
7742
+ nativeOutputRoot,
7743
+ global,
7744
+ toolHome: getHermesagentHome
7745
+ });
7381
7746
  }
7382
7747
  //#endregion
7383
7748
  //#region src/constants/agentsmd-paths.ts
@@ -9081,6 +9446,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
9081
9446
  }
9082
9447
  };
9083
9448
  //#endregion
9449
+ //#region src/constants/grokcli-paths.ts
9450
+ /**
9451
+ * Grok Build CLI (xAI) configuration-layout conventions.
9452
+ *
9453
+ * Single source of truth for where Grok Build expects its files. Grok Build
9454
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9455
+ * with project/global scopes resolved by the directory the CLI runs in
9456
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9457
+ *
9458
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9459
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9460
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9461
+ * @see https://docs.x.ai/build/overview
9462
+ */
9463
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9464
+ const GROKCLI_DIR = ".grok";
9465
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9466
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9467
+ /**
9468
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9469
+ * permission mode, and other settings all live here; permissions reuse the same
9470
+ * file name as MCP since Grok consolidates everything into one config.
9471
+ */
9472
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9473
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9474
+ const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
9475
+ /**
9476
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9477
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9478
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9479
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9480
+ * @see https://docs.x.ai/build/features/hooks
9481
+ */
9482
+ const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
9483
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9484
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9485
+ /**
9486
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9487
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9488
+ * (global), each a Markdown file with YAML frontmatter (verified via
9489
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9490
+ */
9491
+ const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
9492
+ /**
9493
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9494
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9495
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9496
+ * MCP/skills/subagents adapters).
9497
+ */
9498
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9499
+ /**
9500
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9501
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9502
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9503
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9504
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9505
+ * for Claude Code.
9506
+ *
9507
+ * Skills are collected before commands and win name collisions, so a
9508
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9509
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9510
+ */
9511
+ const GROKCLI_COMMANDS_DIR_PATH = join(GROKCLI_DIR, "commands");
9512
+ /**
9513
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9514
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9515
+ * walks, and `~/.grok/rules/` in the home scope.
9516
+ * @see https://docs.x.ai/build/overview
9517
+ */
9518
+ const GROKCLI_RULES_DIR_PATH = join(GROKCLI_DIR, "rules");
9519
+ //#endregion
9520
+ //#region src/features/commands/grokcli-command.ts
9521
+ /**
9522
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9523
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9524
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9525
+ *
9526
+ * Two upstream constraints shape this adapter:
9527
+ *
9528
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9529
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9530
+ * onto their basename).
9531
+ * - Skills are collected before commands and win name collisions, so a
9532
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9533
+ *
9534
+ * `description` and `argument-hint` describe the command, while
9535
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9536
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9537
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9538
+ */
9539
+ const GrokcliCommandFrontmatterSchema = z.looseObject({
9540
+ description: z.optional(z.string()),
9541
+ "argument-hint": z.optional(z.string()),
9542
+ "user-invocable": z.optional(z.boolean()),
9543
+ "disable-model-invocation": z.optional(z.boolean())
9544
+ });
9545
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9546
+ frontmatter;
9547
+ body;
9548
+ constructor({ frontmatter, body, ...rest }) {
9549
+ if (rest.validate) {
9550
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9551
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9552
+ }
9553
+ super({
9554
+ ...rest,
9555
+ fileContent: stringifyFrontmatter(body, frontmatter)
9556
+ });
9557
+ this.frontmatter = frontmatter;
9558
+ this.body = body;
9559
+ }
9560
+ static getSettablePaths(_options = {}) {
9561
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9562
+ }
9563
+ getBody() {
9564
+ return this.body;
9565
+ }
9566
+ getFrontmatter() {
9567
+ return this.frontmatter;
9568
+ }
9569
+ toRulesyncCommand() {
9570
+ const { description, ...restFields } = this.frontmatter;
9571
+ const rulesyncFrontmatter = {
9572
+ targets: ["*"],
9573
+ description,
9574
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9575
+ };
9576
+ return new RulesyncCommand({
9577
+ outputRoot: ".",
9578
+ frontmatter: rulesyncFrontmatter,
9579
+ body: this.body,
9580
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9581
+ relativeFilePath: this.relativeFilePath,
9582
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9583
+ validate: true
9584
+ });
9585
+ }
9586
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9587
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9588
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9589
+ const grokcliFrontmatter = {
9590
+ description: rulesyncFrontmatter.description,
9591
+ ...grokcliFields
9592
+ };
9593
+ const paths = this.getSettablePaths({ global });
9594
+ return new GrokcliCommand({
9595
+ outputRoot,
9596
+ frontmatter: grokcliFrontmatter,
9597
+ body: rulesyncCommand.getBody(),
9598
+ relativeDirPath: paths.relativeDirPath,
9599
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9600
+ validate
9601
+ });
9602
+ }
9603
+ validate() {
9604
+ if (!this.frontmatter) return {
9605
+ success: true,
9606
+ error: null
9607
+ };
9608
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9609
+ if (result.success) return {
9610
+ success: true,
9611
+ error: null
9612
+ };
9613
+ return {
9614
+ success: false,
9615
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9616
+ };
9617
+ }
9618
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9619
+ return this.isTargetedByRulesyncCommandDefault({
9620
+ rulesyncCommand,
9621
+ toolTarget: "grokcli"
9622
+ });
9623
+ }
9624
+ /**
9625
+ * Warn when a rulesync skill would shadow a rulesync command.
9626
+ *
9627
+ * Grok collects skills before commands and lets skills win name collisions,
9628
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9629
+ * Both files are still written correctly — nothing is overwritten and no
9630
+ * output is lost — so this warns rather than failing the run the way the
9631
+ * Hermes check does, where the two surfaces really do write the same path.
9632
+ */
9633
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9634
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
9635
+ if (commandNames.size === 0) return;
9636
+ const shadowed = (await findFilesByGlobs(join(join(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
9637
+ 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.`);
9638
+ }
9639
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9640
+ const paths = this.getSettablePaths({ global });
9641
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
9642
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9643
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9644
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9645
+ return new GrokcliCommand({
9646
+ outputRoot,
9647
+ relativeDirPath: paths.relativeDirPath,
9648
+ relativeFilePath,
9649
+ frontmatter: result.data,
9650
+ body: content.trim(),
9651
+ validate
9652
+ });
9653
+ }
9654
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9655
+ return new GrokcliCommand({
9656
+ outputRoot,
9657
+ relativeDirPath,
9658
+ relativeFilePath,
9659
+ frontmatter: { description: "" },
9660
+ body: "",
9661
+ validate: false
9662
+ });
9663
+ }
9664
+ };
9665
+ //#endregion
9084
9666
  //#region src/features/skills/tool-skill.ts
9085
9667
  /** Ordered skill directory roots: primary first. */
9086
9668
  function toolSkillSearchRoots(paths) {
@@ -9715,7 +10297,7 @@ def register(ctx):
9715
10297
  _register_command(ctx, command)
9716
10298
  `;
9717
10299
  }
9718
- function getEnabledPluginConfigContent$1(currentContent) {
10300
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9719
10301
  const config = parseSharedConfig({
9720
10302
  format: "yaml",
9721
10303
  fileContent: currentContent
@@ -9723,7 +10305,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9723
10305
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9724
10306
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9725
10307
  return applySharedConfigPatch({
9726
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10308
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9727
10309
  feature: "commands",
9728
10310
  existingContent: currentContent,
9729
10311
  patch: { plugins: {
@@ -9732,7 +10314,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9732
10314
  } }
9733
10315
  });
9734
10316
  }
9735
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10317
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9736
10318
  const config = parseSharedConfig({
9737
10319
  format: "yaml",
9738
10320
  fileContent: currentContent
@@ -9740,7 +10322,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9740
10322
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9741
10323
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9742
10324
  return applySharedConfigPatch({
9743
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10325
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9744
10326
  feature: "commands",
9745
10327
  existingContent: currentContent,
9746
10328
  patch: { plugins: {
@@ -9756,35 +10338,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9756
10338
  error: null
9757
10339
  };
9758
10340
  }
9759
- shouldMergeExistingFileContent() {
10341
+ /**
10342
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10343
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10344
+ */
10345
+ matchesPath(relativeFilePath) {
9760
10346
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9761
10347
  global: this.global,
9762
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10348
+ relativeFilePath
9763
10349
  }));
9764
10350
  }
10351
+ shouldMergeExistingFileContent() {
10352
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10353
+ }
9765
10354
  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));
10355
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10356
+ super.setFileContent(getEnabledPluginConfigContent$1({
10357
+ currentContent: newFileContent,
10358
+ global: this.global
10359
+ }));
9771
10360
  return;
9772
10361
  }
9773
10362
  super.setFileContent(newFileContent);
9774
10363
  }
9775
10364
  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());
10365
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10366
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10367
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10368
+ currentContent: super.getFileContent(),
10369
+ global: this.global
10370
+ });
9788
10371
  return super.getFileContent();
9789
10372
  }
9790
10373
  };
@@ -9801,14 +10384,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9801
10384
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9802
10385
  }) };
9803
10386
  }
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
- }];
10387
+ /**
10388
+ * `config.yaml` under every spelling the global profile root can take.
10389
+ * @see getHermesagentSharedConfigWritePaths
10390
+ */
10391
+ static getExtraSharedWritePaths() {
10392
+ return getHermesagentSharedConfigWritePaths();
9812
10393
  }
9813
10394
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9814
10395
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9833,33 +10414,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9833
10414
  }
9834
10415
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9835
10416
  if (toolCommands.length === 0 && !forDeletion) return [];
10417
+ const pluginDirPath = getHermesagentRelativeDirPath({
10418
+ global,
10419
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10420
+ });
9836
10421
  const pluginFiles = [
9837
10422
  new HermesagentCommandAuxiliaryFile({
9838
10423
  outputRoot,
9839
- relativeDirPath: getHermesagentRelativeDirPath({
9840
- global,
9841
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9842
- }),
10424
+ relativeDirPath: pluginDirPath,
9843
10425
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9844
10426
  fileContent: "",
9845
10427
  global
9846
10428
  }),
9847
10429
  new HermesagentCommandAuxiliaryFile({
9848
10430
  outputRoot,
9849
- relativeDirPath: getHermesagentRelativeDirPath({
9850
- global,
9851
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9852
- }),
10431
+ relativeDirPath: pluginDirPath,
9853
10432
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9854
10433
  fileContent: "Generated and owned by RuleSync.\n",
9855
10434
  global
9856
10435
  }),
9857
10436
  new HermesagentCommandAuxiliaryFile({
9858
10437
  outputRoot,
9859
- relativeDirPath: getHermesagentRelativeDirPath({
9860
- global,
9861
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9862
- }),
10438
+ relativeDirPath: pluginDirPath,
9863
10439
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9864
10440
  fileContent: "",
9865
10441
  global
@@ -11094,19 +11670,6 @@ var RooCommand = class RooCommand extends ToolCommand {
11094
11670
  }
11095
11671
  };
11096
11672
  //#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
11673
  //#region src/features/commands/rovodev-command.ts
11111
11674
  /**
11112
11675
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11690,6 +12253,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11690
12253
  supportsSubdirectory: false
11691
12254
  }
11692
12255
  }],
12256
+ ["grokcli", {
12257
+ class: GrokcliCommand,
12258
+ meta: {
12259
+ extension: "md",
12260
+ supportsProject: true,
12261
+ supportsGlobal: true,
12262
+ isSimulated: false,
12263
+ supportsSubdirectory: false
12264
+ }
12265
+ }],
11693
12266
  ["hermesagent", {
11694
12267
  class: HermesagentCommand,
11695
12268
  meta: {
@@ -11883,7 +12456,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11883
12456
  const factory = this.getFactory(this.toolTarget);
11884
12457
  await factory.class.validateRulesyncCommands?.({
11885
12458
  inputRoot: this.inputRoot,
11886
- rulesyncCommands
12459
+ rulesyncCommands,
12460
+ logger: this.logger
11887
12461
  });
11888
12462
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11889
12463
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -12021,7 +12595,10 @@ var CommandsProcessor = class extends FeatureProcessor {
12021
12595
  }));
12022
12596
  const currentContent = await readFileContentOrNull(configPath);
12023
12597
  if (currentContent === null) return changedCount;
12024
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12598
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12599
+ currentContent,
12600
+ global: this.global
12601
+ });
12025
12602
  if (nextContent === currentContent) return changedCount;
12026
12603
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
12027
12604
  else await writeFileContent(configPath, nextContent);
@@ -14483,64 +15060,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14483
15060
  }
14484
15061
  };
14485
15062
  //#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
15063
  //#region src/features/hooks/grokcli-hooks.ts
14545
15064
  const GROKCLI_CONVERTER_CONFIG = {
14546
15065
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14786,6 +15305,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14786
15305
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14787
15306
  };
14788
15307
  }
15308
+ /**
15309
+ * `config.yaml` under every spelling the global profile root can take.
15310
+ * @see getHermesagentSharedConfigWritePaths
15311
+ */
15312
+ static getExtraSharedWritePaths() {
15313
+ return getHermesagentSharedConfigWritePaths();
15314
+ }
14789
15315
  constructor(params) {
14790
15316
  super({
14791
15317
  ...params,
@@ -14823,7 +15349,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14823
15349
  }
14824
15350
  setFileContent(fileContent) {
14825
15351
  this.fileContent = applySharedConfigPatch({
14826
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15352
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14827
15353
  feature: "hooks",
14828
15354
  existingContent: fileContent,
14829
15355
  patch: parseSharedConfig({
@@ -15199,8 +15725,38 @@ function getKimiCodeHome() {
15199
15725
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
15200
15726
  return global && getKimiCodeHome() ? relativeDirPath : join(KIMI_CODE_DIR, relativeDirPath);
15201
15727
  }
15728
+ /**
15729
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15730
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15731
+ * Declared unconditionally so the derived shared-file keys — and the drift
15732
+ * guards checked against them — do not depend on the ambient environment.
15733
+ */
15734
+ function getKimiCodeSharedConfigWritePaths() {
15735
+ return [{
15736
+ relativeDirPath: KIMI_CODE_DIR,
15737
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15738
+ }, {
15739
+ relativeDirPath: ".",
15740
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15741
+ }];
15742
+ }
15743
+ /**
15744
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15745
+ * Both spellings carry the same declaration, but passing the key of the file
15746
+ * being written keeps the write path and the drift guards on the same entry.
15747
+ */
15748
+ function getKimiCodeConfigSharedFileKey({ global }) {
15749
+ return sharedConfigFileKey({
15750
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15751
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15752
+ });
15753
+ }
15202
15754
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
15203
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15755
+ return getToolRulesyncOutputRoot({
15756
+ nativeOutputRoot,
15757
+ global,
15758
+ toolHome: getKimiCodeHome
15759
+ });
15204
15760
  }
15205
15761
  //#endregion
15206
15762
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -15298,13 +15854,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
15298
15854
  isDeletable() {
15299
15855
  return false;
15300
15856
  }
15857
+ /**
15858
+ * `config.toml` under both spellings its directory can take.
15859
+ * @see getKimiCodeSharedConfigWritePaths
15860
+ */
15861
+ static getExtraSharedWritePaths() {
15862
+ return getKimiCodeSharedConfigWritePaths();
15863
+ }
15301
15864
  shouldMergeExistingFileContent() {
15302
15865
  return true;
15303
15866
  }
15304
15867
  setFileContent(fileContent) {
15305
15868
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
15306
15869
  this.fileContent = applySharedConfigPatch({
15307
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15870
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
15308
15871
  feature: "hooks",
15309
15872
  existingContent: fileContent,
15310
15873
  patch: parseSharedConfig({
@@ -17995,6 +18558,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
17995
18558
  }
17996
18559
  };
17997
18560
  //#endregion
18561
+ //#region src/features/shared/reasonix-config-table.ts
18562
+ /**
18563
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18564
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18565
+ *
18566
+ * TOML is only structurally validated on parse, so a hand-edited config can
18567
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18568
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18569
+ * before merging, so the narrowing lives here once rather than being re-spelled
18570
+ * (and re-diverging) per feature.
18571
+ */
18572
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18573
+ function toReasonixStringArray(value) {
18574
+ if (!Array.isArray(value)) return [];
18575
+ return value.filter((entry) => typeof entry === "string");
18576
+ }
18577
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18578
+ function toReasonixTable(value) {
18579
+ if (!isPlainObject$1(value)) return {};
18580
+ return { ...value };
18581
+ }
18582
+ //#endregion
18583
+ //#region src/features/ignore/reasonix-ignore.ts
18584
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18585
+ /**
18586
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18587
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18588
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18589
+ * gateway already implements applies verbatim; only the surrounding file
18590
+ * format differs. Sibling keys such as `mode` pass through untouched.
18591
+ */
18592
+ const asClaudeStyleSettings = (document) => {
18593
+ const table = permissionsTableOf(document);
18594
+ return {
18595
+ ...document,
18596
+ permissions: {
18597
+ ...table,
18598
+ allow: toReasonixStringArray(table.allow),
18599
+ ask: toReasonixStringArray(table.ask),
18600
+ deny: toReasonixStringArray(table.deny)
18601
+ }
18602
+ };
18603
+ };
18604
+ /**
18605
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18606
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18607
+ * one.
18608
+ */
18609
+ const withoutEmptyPermissions = (settings) => {
18610
+ const document = { ...settings };
18611
+ const permissions = document.permissions;
18612
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18613
+ return document;
18614
+ };
18615
+ /**
18616
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18617
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18618
+ * `~/.reasonix/config.toml` (global).
18619
+ *
18620
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18621
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18622
+ * while `forbid_read` is documented as absolute paths with no glob support.
18623
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18624
+ */
18625
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18626
+ constructor(params) {
18627
+ super(params);
18628
+ const document = parseSharedConfig({
18629
+ format: "toml",
18630
+ fileContent: this.fileContent
18631
+ });
18632
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18633
+ }
18634
+ static getSettablePaths({ global = false } = {}) {
18635
+ return {
18636
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18637
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18638
+ };
18639
+ }
18640
+ /**
18641
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18642
+ * permissions feature and user-authored tables, so rulesync must never
18643
+ * delete it.
18644
+ */
18645
+ isDeletable() {
18646
+ return false;
18647
+ }
18648
+ toRulesyncIgnore() {
18649
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18650
+ return new RulesyncIgnore({
18651
+ outputRoot: this.outputRoot,
18652
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18653
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18654
+ fileContent: rulesyncPatterns.join("\n")
18655
+ });
18656
+ }
18657
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18658
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18659
+ const paths = this.getSettablePaths({ global });
18660
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18661
+ const existingDocument = parseSharedConfig({
18662
+ format: "toml",
18663
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18664
+ filePath
18665
+ });
18666
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18667
+ settings: asClaudeStyleSettings(existingDocument),
18668
+ readDenies
18669
+ }));
18670
+ return new ReasonixIgnore({
18671
+ outputRoot,
18672
+ relativeDirPath: paths.relativeDirPath,
18673
+ relativeFilePath: paths.relativeFilePath,
18674
+ fileContent: stringifySharedConfig({
18675
+ format: "toml",
18676
+ document
18677
+ }),
18678
+ validate: true,
18679
+ global
18680
+ });
18681
+ }
18682
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18683
+ const paths = this.getSettablePaths({ global });
18684
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18685
+ return new ReasonixIgnore({
18686
+ outputRoot,
18687
+ relativeDirPath: paths.relativeDirPath,
18688
+ relativeFilePath: paths.relativeFilePath,
18689
+ fileContent,
18690
+ validate,
18691
+ global
18692
+ });
18693
+ }
18694
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18695
+ return new ReasonixIgnore({
18696
+ outputRoot,
18697
+ relativeDirPath,
18698
+ relativeFilePath,
18699
+ fileContent: "",
18700
+ validate: false,
18701
+ global
18702
+ });
18703
+ }
18704
+ };
18705
+ //#endregion
17998
18706
  //#region src/features/ignore/roo-ignore.ts
17999
18707
  /**
18000
18708
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -18152,6 +18860,14 @@ const ZED_GLOBAL_WIN32_DIR = join("AppData", "Roaming", "Zed");
18152
18860
  function getZedGlobalDir() {
18153
18861
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
18154
18862
  }
18863
+ /**
18864
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18865
+ * spelling per platform, but the shared-write derivation (and the gateway
18866
+ * ownership table it is checked against) must know both on every platform.
18867
+ */
18868
+ function getZedOtherPlatformGlobalDir() {
18869
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18870
+ }
18155
18871
  const ZED_SETTINGS_FILE_NAME = "settings.json";
18156
18872
  const ZED_RULE_FILE_NAME = ".rules";
18157
18873
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -18164,12 +18880,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18164
18880
  const jsonValue = JSON.parse(this.fileContent);
18165
18881
  this.patterns = jsonValue.private_files ?? [];
18166
18882
  }
18167
- static getSettablePaths() {
18883
+ static getSettablePaths({ global = false } = {}) {
18168
18884
  return {
18169
- relativeDirPath: ZED_DIR,
18885
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
18170
18886
  relativeFilePath: ZED_SETTINGS_FILE_NAME
18171
18887
  };
18172
18888
  }
18889
+ /** @see getZedOtherPlatformGlobalDir */
18890
+ static getExtraSharedWritePaths({ global = false } = {}) {
18891
+ if (!global) return [];
18892
+ return [{
18893
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18894
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18895
+ }];
18896
+ }
18173
18897
  /**
18174
18898
  * ZedIgnore uses settings.json which is a user-managed config file.
18175
18899
  * It should not be deleted by rulesync.
@@ -18180,48 +18904,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18180
18904
  toRulesyncIgnore() {
18181
18905
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
18182
18906
  return new RulesyncIgnore({
18183
- outputRoot: this.outputRoot,
18907
+ outputRoot: ".",
18184
18908
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18185
18909
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18186
18910
  fileContent
18187
18911
  });
18188
18912
  }
18189
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18913
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18190
18914
  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);
18915
+ const paths = this.getSettablePaths({ global });
18916
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18192
18917
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
18193
- const mergedPatterns = uniq([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18918
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
18194
18919
  return new ZedIgnore({
18195
18920
  outputRoot,
18196
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18197
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18921
+ relativeDirPath: paths.relativeDirPath,
18922
+ relativeFilePath: paths.relativeFilePath,
18198
18923
  fileContent: applySharedConfigPatch({
18199
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18924
+ fileKey: sharedConfigFileKey(paths),
18200
18925
  feature: "ignore",
18201
18926
  existingContent: existingFileContent,
18202
- patch: { private_files: mergedPatterns },
18927
+ patch: { private_files: managedPatterns },
18203
18928
  filePath
18204
18929
  }),
18205
- validate: true
18930
+ validate: true,
18931
+ global
18206
18932
  });
18207
18933
  }
18208
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
18209
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
18934
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18935
+ const paths = this.getSettablePaths({ global });
18936
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
18210
18937
  return new ZedIgnore({
18211
18938
  outputRoot,
18212
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18213
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18939
+ relativeDirPath: paths.relativeDirPath,
18940
+ relativeFilePath: paths.relativeFilePath,
18214
18941
  fileContent,
18215
- validate
18942
+ validate,
18943
+ global
18216
18944
  });
18217
18945
  }
18218
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18946
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18219
18947
  return new ZedIgnore({
18220
18948
  outputRoot,
18221
18949
  relativeDirPath,
18222
18950
  relativeFilePath,
18223
18951
  fileContent: "{}",
18224
- validate: false
18952
+ validate: false,
18953
+ global
18225
18954
  });
18226
18955
  }
18227
18956
  };
@@ -18243,6 +18972,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
18243
18972
  ["kiro-cli", { class: KiroIgnore }],
18244
18973
  ["kiro-ide", { class: KiroIgnore }],
18245
18974
  ["qwencode", { class: QwencodeIgnore }],
18975
+ ["reasonix", { class: ReasonixIgnore }],
18246
18976
  ["roo", { class: RooIgnore }],
18247
18977
  ["devin", { class: DevinIgnore }],
18248
18978
  ["vibe", { class: VibeIgnore }],
@@ -18253,7 +18983,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
18253
18983
  const ignoreProcessorGlobalToolTargets = [
18254
18984
  "kiro",
18255
18985
  "kiro-cli",
18256
- "kiro-ide"
18986
+ "kiro-ide",
18987
+ "reasonix",
18988
+ "zed"
18257
18989
  ];
18258
18990
  const defaultGetFactory$4 = (target) => {
18259
18991
  const factory = toolIgnoreFactories.get(target);
@@ -20849,7 +21581,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20849
21581
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20850
21582
  this.config = merged;
20851
21583
  super.setFileContent(applySharedConfigPatch({
20852
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21584
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20853
21585
  feature: "mcp",
20854
21586
  existingContent: fileContent,
20855
21587
  patch: { mcp_servers: merged.mcp_servers }
@@ -20867,6 +21599,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20867
21599
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20868
21600
  };
20869
21601
  }
21602
+ /**
21603
+ * `config.yaml` under every spelling the global profile root can take.
21604
+ * @see getHermesagentSharedConfigWritePaths
21605
+ */
21606
+ static getExtraSharedWritePaths() {
21607
+ return getHermesagentSharedConfigWritePaths();
21608
+ }
20870
21609
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20871
21610
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20872
21611
  const paths = this.getSettablePaths({ global });
@@ -20893,7 +21632,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20893
21632
  relativeDirPath: paths.relativeDirPath,
20894
21633
  relativeFilePath: paths.relativeFilePath,
20895
21634
  fileContent: applySharedConfigPatch({
20896
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21635
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20897
21636
  feature: "mcp",
20898
21637
  existingContent: fileContent,
20899
21638
  patch: { mcp_servers: merged.mcp_servers }
@@ -21354,9 +22093,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
21354
22093
  * Merge a list of project rule file globs into the `instructions` array of the
21355
22094
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
21356
22095
  * (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.
22096
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22097
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22098
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22099
+ * config load — which is why `KiloRule` registers instructions in project
22100
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22101
+ * stable output.
21360
22102
  *
21361
22103
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
21362
22104
  */
@@ -21569,7 +22311,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21569
22311
  const existingContent = existing.content;
21570
22312
  const existingSection = existing.mcp;
21571
22313
  const fileContent = applySharedConfigPatch({
21572
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22314
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21573
22315
  feature: "mcp",
21574
22316
  existingContent,
21575
22317
  patch: { mcp: {
@@ -21656,11 +22398,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21656
22398
  * derivation sees this feature as one of that file's writers — it is not a
21657
22399
  * settable path, since the servers themselves live in `mcp.json`.
21658
22400
  */
21659
- static getExtraSharedWritePaths({ global = false } = {}) {
21660
- return global ? [{
21661
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21662
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21663
- }] : [];
22401
+ static getExtraSharedWritePaths() {
22402
+ return getKimiCodeSharedConfigWritePaths();
21664
22403
  }
21665
22404
  /**
21666
22405
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -23296,16 +24035,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
23296
24035
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23297
24036
  };
23298
24037
  }
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
- */
24038
+ /** @see getZedOtherPlatformGlobalDir */
23305
24039
  static getExtraSharedWritePaths({ global = false } = {}) {
23306
24040
  if (!global) return [];
23307
24041
  return [{
23308
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24042
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
23309
24043
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23310
24044
  }];
23311
24045
  }
@@ -27735,6 +28469,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27735
28469
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27736
28470
  };
27737
28471
  }
28472
+ /**
28473
+ * `config.yaml` under every spelling the global profile root can take.
28474
+ * @see getHermesagentSharedConfigWritePaths
28475
+ */
28476
+ static getExtraSharedWritePaths() {
28477
+ return getHermesagentSharedConfigWritePaths();
28478
+ }
27738
28479
  constructor(params) {
27739
28480
  super({
27740
28481
  ...params,
@@ -27772,7 +28513,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27772
28513
  }
27773
28514
  setFileContent(fileContent) {
27774
28515
  this.fileContent = applySharedConfigPatch({
27775
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28516
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27776
28517
  feature: "permissions",
27777
28518
  existingContent: fileContent,
27778
28519
  patch: parseSharedConfig({
@@ -28185,7 +28926,10 @@ const KiloPermissionSchema = z.union([z.enum([
28185
28926
  "ask",
28186
28927
  "deny"
28187
28928
  ]))]);
28188
- const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
28929
+ const KiloPermissionsConfigSchema = z.looseObject({
28930
+ permission: z.optional(z.record(z.string(), KiloPermissionSchema)),
28931
+ sandbox: z.optional(z.unknown())
28932
+ });
28189
28933
  /**
28190
28934
  * Kilo permission keys that share a name with a canonical rulesync category and
28191
28935
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -28242,6 +28986,25 @@ function collectKiloDenyPatterns(value) {
28242
28986
  }
28243
28987
  return [];
28244
28988
  }
28989
+ function asKiloRecord(value) {
28990
+ return isPlainObject$1(value) ? { ...value } : {};
28991
+ }
28992
+ /**
28993
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
28994
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
28995
+ * project config merely tighten — so writing the wider keys into a project file
28996
+ * would produce config Kilo ignores.
28997
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
28998
+ */
28999
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29000
+ function narrowSandboxToProjectScope({ authored, logger }) {
29001
+ const emitted = {};
29002
+ const dropped = [];
29003
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29004
+ else dropped.push(key);
29005
+ 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.`);
29006
+ return emitted;
29007
+ }
28245
29008
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
28246
29009
  json;
28247
29010
  constructor(params) {
@@ -28321,8 +29084,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28321
29084
  const basePaths = KiloPermissions.getSettablePaths({ global });
28322
29085
  const filePath = join(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
28323
29086
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
28324
- const parsedPermission = parsed.permission;
28325
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29087
+ const existingPermission = asKiloRecord(parsed.permission);
28326
29088
  const rulesyncJson = rulesyncPermissions.getJson();
28327
29089
  const kiloOverride = rulesyncJson.kilo;
28328
29090
  const incomingPermission = {
@@ -28348,6 +29110,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28348
29110
  ...parsed,
28349
29111
  permission: mergedPermission
28350
29112
  };
29113
+ if (kiloOverride?.sandbox !== void 0) {
29114
+ const authored = asKiloRecord(kiloOverride.sandbox);
29115
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29116
+ authored,
29117
+ logger
29118
+ });
29119
+ const merged = {
29120
+ ...asKiloRecord(parsed.sandbox),
29121
+ ...emitted
29122
+ };
29123
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29124
+ }
28351
29125
  return new KiloPermissions({
28352
29126
  outputRoot,
28353
29127
  relativeDirPath: basePaths.relativeDirPath,
@@ -28362,9 +29136,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28362
29136
  const overrideOnly = {};
28363
29137
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
28364
29138
  else overrideOnly[key] = value;
28365
- const json = Object.keys(overrideOnly).length > 0 ? {
29139
+ const sandbox = this.json.sandbox;
29140
+ const override = {
29141
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29142
+ ...isPlainObject$1(sandbox) && { sandbox }
29143
+ };
29144
+ const json = Object.keys(override).length > 0 ? {
28366
29145
  permission: shared,
28367
- kilo: { permission: overrideOnly }
29146
+ kilo: override
28368
29147
  } : { permission: shared };
28369
29148
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
28370
29149
  }
@@ -28605,6 +29384,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28605
29384
  isDeletable() {
28606
29385
  return false;
28607
29386
  }
29387
+ /**
29388
+ * `config.toml` under both spellings its directory can take.
29389
+ * @see getKimiCodeSharedConfigWritePaths
29390
+ */
29391
+ static getExtraSharedWritePaths() {
29392
+ return getKimiCodeSharedConfigWritePaths();
29393
+ }
28608
29394
  shouldMergeExistingFileContent() {
28609
29395
  return true;
28610
29396
  }
@@ -28619,7 +29405,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28619
29405
  patch
28620
29406
  });
28621
29407
  this.fileContent = applySharedConfigPatch({
28622
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29408
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
28623
29409
  feature: "permissions",
28624
29410
  existingContent: fileContent,
28625
29411
  patch: {
@@ -29543,14 +30329,6 @@ function parseReasonixConfig(fileContent) {
29543
30329
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
29544
30330
  return { ...parsed };
29545
30331
  }
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
30332
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
29555
30333
  /**
29556
30334
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -29607,12 +30385,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29607
30385
  const config = rulesyncPermissions.getJson();
29608
30386
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
29609
30387
  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));
30388
+ const existingPermissions = toReasonixTable(parsed.permissions);
30389
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30390
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30391
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29614
30392
  if (logger && managedToolNames.has("Read")) {
29615
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30393
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
29616
30394
  const { toolName } = parseReasonixPermissionEntry(entry);
29617
30395
  return toolName === "Read";
29618
30396
  });
@@ -29659,11 +30437,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29659
30437
  });
29660
30438
  }
29661
30439
  toRulesyncPermissions() {
29662
- const permissions = toPermissionsTable(this.toml.permissions);
30440
+ const permissions = toReasonixTable(this.toml.permissions);
29663
30441
  const config = convertReasonixToRulesyncPermissions({
29664
- allow: toStringArray$1(permissions.allow),
29665
- ask: toStringArray$1(permissions.ask),
29666
- deny: toStringArray$1(permissions.deny)
30442
+ allow: toReasonixStringArray(permissions.allow),
30443
+ ask: toReasonixStringArray(permissions.ask),
30444
+ deny: toReasonixStringArray(permissions.deny)
29667
30445
  });
29668
30446
  const sandbox = asReasonixRecord(this.toml.sandbox);
29669
30447
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29755,19 +30533,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29755
30533
  "open_files",
29756
30534
  "expand_code_chunks",
29757
30535
  "expand_folder",
29758
- "grep"
30536
+ "grep",
30537
+ "getJiraIssue",
30538
+ "getConfluencePage"
29759
30539
  ],
29760
30540
  edit: [
29761
30541
  "find_and_replace_code",
29762
30542
  "create_file",
29763
30543
  "delete_file",
29764
- "move_file"
30544
+ "move_file",
30545
+ "createTechnicalPlan",
30546
+ "createJiraIssue",
30547
+ "updateJiraIssue",
30548
+ "createConfluencePage",
30549
+ "updateConfluencePage"
29765
30550
  ],
29766
30551
  write: [
29767
30552
  "create_file",
29768
30553
  "delete_file",
29769
30554
  "move_file",
29770
- "find_and_replace_code"
30555
+ "find_and_replace_code",
30556
+ "createTechnicalPlan",
30557
+ "createJiraIssue",
30558
+ "updateJiraIssue",
30559
+ "createConfluencePage",
30560
+ "updateConfluencePage"
29771
30561
  ]
29772
30562
  };
29773
30563
  const TOOL_KEY_TO_CATEGORY = {
@@ -29775,13 +30565,24 @@ const TOOL_KEY_TO_CATEGORY = {
29775
30565
  expand_code_chunks: "read",
29776
30566
  expand_folder: "read",
29777
30567
  grep: "read",
30568
+ getJiraIssue: "read",
30569
+ getConfluencePage: "read",
29778
30570
  find_and_replace_code: "edit",
29779
30571
  create_file: "edit",
29780
30572
  delete_file: "edit",
29781
- move_file: "edit"
30573
+ move_file: "edit",
30574
+ createTechnicalPlan: "edit",
30575
+ createJiraIssue: "edit",
30576
+ updateJiraIssue: "edit",
30577
+ createConfluencePage: "edit",
30578
+ updateConfluencePage: "edit"
29782
30579
  };
29783
30580
  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"];
30581
+ const OWNED_TOOL_PERMISSION_KEYS = [
30582
+ "bash",
30583
+ "allowedExternalPaths",
30584
+ "default"
30585
+ ];
29785
30586
  /**
29786
30587
  * Permissions adapter for Rovo Dev CLI.
29787
30588
  *
@@ -29795,9 +30596,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29795
30596
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29796
30597
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29797
30598
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30599
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30600
+ * the level Rovo Dev falls back to for any tool with no more specific
30601
+ * setting (Rovo Dev's own default is `ask`).
29798
30602
  * - `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`).
30603
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30604
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30605
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30606
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30607
+ * `updateConfluencePage`) — so these two categories reach Jira and
30608
+ * Confluence, not just the working tree.
29801
30609
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29802
30610
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29803
30611
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -29977,6 +30785,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
29977
30785
  delete toolPermissions.allowedExternalPaths;
29978
30786
  strippedKeys.push("allowedExternalPaths");
29979
30787
  }
30788
+ if (toolPermissions.default === "allow") {
30789
+ delete toolPermissions.default;
30790
+ strippedKeys.push("default");
30791
+ }
29980
30792
  const bash = toolPermissions.bash;
29981
30793
  if (isRecord(bash)) {
29982
30794
  const stripped = { ...bash };
@@ -30001,10 +30813,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
30001
30813
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30002
30814
  const toolPermissions = {};
30003
30815
  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.`);
30816
+ warnOnEditWriteConflict({
30817
+ config,
30818
+ logger
30819
+ });
30007
30820
  for (const [category, rules] of Object.entries(config.permission)) {
30821
+ if (category === CATCH_ALL_PATTERN$1) {
30822
+ const toolWideDefault = convertAllToolsRules({
30823
+ rules,
30824
+ logger
30825
+ });
30826
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30827
+ continue;
30828
+ }
30008
30829
  if (category === "bash") {
30009
30830
  const bash = convertBashRules(rules);
30010
30831
  if (bash) toolPermissions.bash = bash;
@@ -30031,6 +30852,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30031
30852
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
30032
30853
  return toolPermissions;
30033
30854
  }
30855
+ /**
30856
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30857
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30858
+ * is happening; the conversion keeps the stricter of the two — the same
30859
+ * fail-closed rule the import direction uses when those tools disagree, so the
30860
+ * resolution never grants more than the author asked for.
30861
+ */
30862
+ function warnOnEditWriteConflict({ config, logger }) {
30863
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30864
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30865
+ 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.`);
30866
+ }
30867
+ /**
30868
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30869
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30870
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30871
+ * under `*` have no counterpart (the default is a single level), so they are
30872
+ * reported and skipped like any other rule Rovo Dev cannot express.
30873
+ */
30874
+ function convertAllToolsRules({ rules, logger }) {
30875
+ let toolWideDefault;
30876
+ for (const [pattern, action] of Object.entries(rules)) {
30877
+ if (pattern === CATCH_ALL_PATTERN$1) {
30878
+ toolWideDefault = action;
30879
+ continue;
30880
+ }
30881
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30882
+ }
30883
+ return toolWideDefault;
30884
+ }
30034
30885
  function convertBashRules(rules) {
30035
30886
  const bash = {};
30036
30887
  const commands = [];
@@ -30052,6 +30903,7 @@ function convertBashRules(rules) {
30052
30903
  */
30053
30904
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30054
30905
  const permission = {};
30906
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
30055
30907
  const bash = toolPermissions.bash;
30056
30908
  if (isRecord(bash)) {
30057
30909
  const bashRules = {};
@@ -30062,12 +30914,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30062
30914
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
30063
30915
  }
30064
30916
  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;
30917
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30918
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30919
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30920
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30921
+ return isPermissionAction(value) ? value : void 0;
30922
+ });
30923
+ if (levels.every((level) => level === void 0)) continue;
30068
30924
  permission[category] ??= {};
30069
- const current = permission[category][CATCH_ALL_PATTERN$1];
30070
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30925
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
30071
30926
  }
30072
30927
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
30073
30928
  permission.read ??= {};
@@ -30899,16 +31754,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30899
31754
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30900
31755
  };
30901
31756
  }
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
- */
31757
+ /** @see getZedOtherPlatformGlobalDir */
30908
31758
  static getExtraSharedWritePaths({ global = false } = {}) {
30909
31759
  if (!global) return [];
30910
31760
  return [{
30911
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31761
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30912
31762
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30913
31763
  }];
30914
31764
  }
@@ -39480,7 +40330,7 @@ def register(ctx):
39480
40330
  _register_subagent(ctx, subagent)
39481
40331
  `;
39482
40332
  }
39483
- function getEnabledPluginConfigContent(currentContent) {
40333
+ function getEnabledPluginConfigContent({ currentContent, global }) {
39484
40334
  const config = parseSharedConfig({
39485
40335
  format: "yaml",
39486
40336
  fileContent: currentContent
@@ -39488,7 +40338,7 @@ function getEnabledPluginConfigContent(currentContent) {
39488
40338
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
39489
40339
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
39490
40340
  return applySharedConfigPatch({
39491
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40341
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
39492
40342
  feature: "subagents",
39493
40343
  existingContent: currentContent,
39494
40344
  patch: { plugins: {
@@ -39539,6 +40389,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39539
40389
  return !targets || targets.includes("*") || targets.includes("hermesagent");
39540
40390
  }
39541
40391
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40392
+ const pluginDirPath = getHermesagentRelativeDirPath({
40393
+ global,
40394
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40395
+ });
39542
40396
  return [
39543
40397
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
39544
40398
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -39547,20 +40401,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39547
40401
  global
39548
40402
  })),
39549
40403
  new HermesagentSubagent({
39550
- relativeDirPath: getHermesagentRelativeDirPath({
39551
- global,
39552
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39553
- }),
40404
+ relativeDirPath: pluginDirPath,
39554
40405
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
39555
40406
  fileContent: "",
39556
40407
  outputRoot,
39557
40408
  global
39558
40409
  }),
39559
40410
  new HermesagentSubagent({
39560
- relativeDirPath: getHermesagentRelativeDirPath({
39561
- global,
39562
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39563
- }),
40411
+ relativeDirPath: pluginDirPath,
39564
40412
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
39565
40413
  fileContent: "",
39566
40414
  outputRoot,
@@ -39600,14 +40448,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39600
40448
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
39601
40449
  * so the write must be declared for the shared-file order derivation.
39602
40450
  */
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
- }] : [];
40451
+ /**
40452
+ * `config.yaml` under every spelling the global profile root can take.
40453
+ * @see getHermesagentSharedConfigWritePaths
40454
+ */
40455
+ static getExtraSharedWritePaths() {
40456
+ return getHermesagentSharedConfigWritePaths();
39611
40457
  }
39612
40458
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
39613
40459
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -39640,7 +40486,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39640
40486
  }
39641
40487
  setFileContent(newFileContent) {
39642
40488
  if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) {
39643
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40489
+ super.setFileContent(getEnabledPluginConfigContent({
40490
+ currentContent: newFileContent,
40491
+ global: this.global
40492
+ }));
39644
40493
  return;
39645
40494
  }
39646
40495
  super.setFileContent(newFileContent);
@@ -39648,7 +40497,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39648
40497
  getFileContent() {
39649
40498
  if (this.getRelativeFilePath() === basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
39650
40499
  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());
40500
+ if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40501
+ currentContent: super.getFileContent(),
40502
+ global: this.global
40503
+ });
39652
40504
  return super.getFileContent();
39653
40505
  }
39654
40506
  };
@@ -44727,10 +45579,13 @@ var JunieRule = class JunieRule extends ToolRule {
44727
45579
  //#region src/features/rules/kilo-rule.ts
44728
45580
  var KiloRule = class KiloRule extends ToolRule {
44729
45581
  static getSettablePaths({ global, excludeToolDir } = {}) {
44730
- if (global) return { root: {
44731
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
44732
- relativeFilePath: KILO_RULE_FILE_NAME
44733
- } };
45582
+ if (global) return {
45583
+ root: {
45584
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45585
+ relativeFilePath: KILO_RULE_FILE_NAME
45586
+ },
45587
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45588
+ };
44734
45589
  return {
44735
45590
  root: {
44736
45591
  relativeDirPath: ".",
@@ -46315,6 +47170,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46315
47170
  extension: "md",
46316
47171
  supportsGlobal: false,
46317
47172
  ruleDiscoveryMode: "toon",
47173
+ collisionPolicy: "compose",
46318
47174
  additionalConventions: {
46319
47175
  commands: { commandClass: AgentsmdCommand },
46320
47176
  subagents: { subagentClass: AgentsmdSubagent },
@@ -46335,7 +47191,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46335
47191
  meta: {
46336
47192
  extension: "md",
46337
47193
  supportsGlobal: true,
46338
- ruleDiscoveryMode: "toon"
47194
+ ruleDiscoveryMode: "toon",
47195
+ collisionPolicy: "compose"
46339
47196
  }
46340
47197
  }],
46341
47198
  ["antigravity-cli", {
@@ -46412,7 +47269,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46412
47269
  extension: "md",
46413
47270
  supportsGlobal: true,
46414
47271
  ruleDiscoveryMode: "auto",
46415
- foldsNonRootIntoRoot: true
47272
+ collisionPolicy: "fold"
46416
47273
  }
46417
47274
  }],
46418
47275
  ["copilot", {
@@ -46445,7 +47302,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46445
47302
  extension: "md",
46446
47303
  supportsGlobal: true,
46447
47304
  ruleDiscoveryMode: "auto",
46448
- foldsNonRootIntoRoot: true
47305
+ collisionPolicy: "fold"
46449
47306
  }
46450
47307
  }],
46451
47308
  ["factorydroid", {
@@ -46453,7 +47310,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46453
47310
  meta: {
46454
47311
  extension: "md",
46455
47312
  supportsGlobal: true,
46456
- ruleDiscoveryMode: "toon"
47313
+ ruleDiscoveryMode: "toon",
47314
+ collisionPolicy: "compose"
46457
47315
  }
46458
47316
  }],
46459
47317
  ["goose", {
@@ -46462,7 +47320,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46462
47320
  extension: "md",
46463
47321
  supportsGlobal: true,
46464
47322
  ruleDiscoveryMode: "auto",
46465
- foldsNonRootIntoRoot: true
47323
+ collisionPolicy: "fold"
46466
47324
  }
46467
47325
  }],
46468
47326
  ["hermesagent", {
@@ -46471,7 +47329,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46471
47329
  extension: "md",
46472
47330
  supportsGlobal: false,
46473
47331
  ruleDiscoveryMode: "auto",
46474
- foldsNonRootIntoRoot: true
47332
+ collisionPolicy: "fold"
46475
47333
  }
46476
47334
  }],
46477
47335
  ["grokcli", {
@@ -46488,7 +47346,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46488
47346
  extension: "md",
46489
47347
  supportsGlobal: true,
46490
47348
  ruleDiscoveryMode: "auto",
46491
- foldsNonRootIntoRoot: true
47349
+ collisionPolicy: "fold"
46492
47350
  }
46493
47351
  }],
46494
47352
  ["kilo", {
@@ -46497,7 +47355,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46497
47355
  extension: "md",
46498
47356
  supportsGlobal: true,
46499
47357
  ruleDiscoveryMode: "auto",
46500
- mcpInstructionsRegistrar: KiloMcp
47358
+ mcpInstructionsRegistrar: KiloMcp,
47359
+ collisionPolicy: "compose"
46501
47360
  }
46502
47361
  }],
46503
47362
  ["kimi-code", {
@@ -46506,7 +47365,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46506
47365
  extension: "md",
46507
47366
  supportsGlobal: true,
46508
47367
  ruleDiscoveryMode: "auto",
46509
- foldsNonRootIntoRoot: true
47368
+ collisionPolicy: "fold"
46510
47369
  }
46511
47370
  }],
46512
47371
  ["kiro", {
@@ -46539,7 +47398,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46539
47398
  extension: "md",
46540
47399
  supportsGlobal: true,
46541
47400
  ruleDiscoveryMode: "toon",
46542
- mcpInstructionsRegistrar: OpencodeMcp
47401
+ mcpInstructionsRegistrar: OpencodeMcp,
47402
+ collisionPolicy: "compose"
46543
47403
  }
46544
47404
  }],
46545
47405
  ["pi", {
@@ -46548,7 +47408,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46548
47408
  extension: "md",
46549
47409
  supportsGlobal: true,
46550
47410
  ruleDiscoveryMode: "auto",
46551
- foldsNonRootIntoRoot: true
47411
+ collisionPolicy: "fold"
46552
47412
  }
46553
47413
  }],
46554
47414
  ["qwencode", {
@@ -46566,7 +47426,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46566
47426
  extension: "md",
46567
47427
  supportsGlobal: true,
46568
47428
  ruleDiscoveryMode: "auto",
46569
- foldsNonRootIntoRoot: true
47429
+ collisionPolicy: "fold"
46570
47430
  }
46571
47431
  }],
46572
47432
  ["replit", {
@@ -46623,7 +47483,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46623
47483
  extension: "md",
46624
47484
  supportsGlobal: false,
46625
47485
  ruleDiscoveryMode: "toon",
46626
- foldsNonRootIntoRoot: true
47486
+ collisionPolicy: "fold"
46627
47487
  }
46628
47488
  }],
46629
47489
  ["devin", {
@@ -46692,16 +47552,23 @@ var RulesProcessor = class extends FeatureProcessor {
46692
47552
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
46693
47553
  const factory = this.getFactory(this.toolTarget);
46694
47554
  const { meta } = factory;
46695
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47555
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
46696
47556
  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
- });
47557
+ return {
47558
+ toolRule: factory.class.fromRulesyncRule({
47559
+ outputRoot: this.outputRoot,
47560
+ rulesyncRule,
47561
+ validate: true,
47562
+ global: this.global
47563
+ }),
47564
+ rulesyncRule
47565
+ };
46703
47566
  }).filter((rule) => rule !== null);
46704
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47567
+ this.mergeRulesByOutputPath({
47568
+ convertedRules,
47569
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47570
+ });
47571
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
46705
47572
  this.applyLocalRootRules({
46706
47573
  toolRules,
46707
47574
  localRootRules,
@@ -46719,7 +47586,12 @@ var RulesProcessor = class extends FeatureProcessor {
46719
47586
  toolRules,
46720
47587
  factory
46721
47588
  });
46722
- return [...toolRules, ...extraFiles];
47589
+ const outputFiles = [...toolRules, ...extraFiles];
47590
+ this.warnForOutputPathCollisions({
47591
+ outputFiles,
47592
+ convertedRules
47593
+ });
47594
+ return outputFiles;
46723
47595
  }
46724
47596
  /**
46725
47597
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -46806,39 +47678,80 @@ var RulesProcessor = class extends FeatureProcessor {
46806
47678
  });
46807
47679
  }
46808
47680
  /**
46809
- * Fold every non-root rule body into the single root rule file.
47681
+ * Reconcile rules that resolve to the same output path.
46810
47682
  *
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).
47683
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47684
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47685
+ * neither scans a modular rules directory nor follows references. For example,
47686
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47687
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47688
+ * body into one instance because last-writer-wins would silently drop content.
47689
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
46817
47690
  *
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.
47691
+ * A generated root rule becomes the merge target when present. A `fold` group
47692
+ * without one uses its first rule. A group only composes when every rendered
47693
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47694
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47695
+ * Root-involved collisions that cannot be composed safely fail; other
47696
+ * collisions remain separate and are reported by the final output-path check.
47697
+ * Mutates `convertedRules` in place.
46821
47698
  */
46822
- foldNonRootRulesIntoRootRule(toolRules) {
46823
- if (toolRules.length <= 1) return;
47699
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47700
+ if (convertedRules.length <= 1) return;
46824
47701
  const groups = /* @__PURE__ */ new Map();
46825
- for (const rule of toolRules) {
46826
- const path = join(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47702
+ for (const conversion of convertedRules) {
47703
+ const path = join(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
46827
47704
  const group = groups.get(path);
46828
- if (group) group.push(rule);
46829
- else groups.set(path, [rule]);
47705
+ if (group) group.push(conversion);
47706
+ else groups.set(path, [conversion]);
46830
47707
  }
46831
47708
  const survivors = /* @__PURE__ */ new Set();
46832
- for (const group of groups.values()) {
46833
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47709
+ for (const [path, group] of groups) {
47710
+ if (group.length === 1) {
47711
+ const conversion = group[0];
47712
+ if (conversion) {
47713
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47714
+ survivors.add(conversion);
47715
+ }
47716
+ continue;
47717
+ }
47718
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47719
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47720
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47721
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47722
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47723
+ 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))}`);
47724
+ if (!shouldCompose) {
47725
+ for (const conversion of group) survivors.add(conversion);
47726
+ continue;
47727
+ }
47728
+ const target = rootConversion ?? group[0];
46834
47729
  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);
47730
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47731
+ target.toolRule.setFileContent(mergedContent);
46837
47732
  survivors.add(target);
46838
47733
  }
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);
47734
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47735
+ const conversion = convertedRules[i];
47736
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47737
+ }
47738
+ }
47739
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47740
+ const seen = /* @__PURE__ */ new Map();
47741
+ const describeSource = (file) => {
47742
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47743
+ return source ? formatRulePaths([source]) : join(file.getRelativeDirPath(), file.getRelativeFilePath());
47744
+ };
47745
+ for (const file of outputFiles) {
47746
+ const path = join(file.getRelativeDirPath(), file.getRelativeFilePath());
47747
+ const key = path.toLowerCase();
47748
+ const previous = seen.get(key);
47749
+ if (previous) {
47750
+ const previousPath = join(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47751
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47752
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47753
+ }
47754
+ seen.set(key, file);
46842
47755
  }
46843
47756
  }
46844
47757
  /**
@@ -46998,14 +47911,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
46998
47911
  }));
46999
47912
  const factory = this.getFactory(this.toolTarget);
47000
47913
  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
47914
  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
47915
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47004
47916
  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
47917
  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
47918
  if (this.global) {
47007
47919
  const globalPaths = factory.class.getSettablePaths({ global: true });
47008
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47920
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
47009
47921
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
47010
47922
  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
47923
  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 +48160,36 @@ async function assertPluginRootSafe(params) {
47248
48160
  }
47249
48161
  //#endregion
47250
48162
  //#region src/utils/tool-output-root.ts
48163
+ /** The environment variable each tool reads for its profile root. */
48164
+ const TOOL_HOME_ENV_VARS = {
48165
+ hermesagent: "HERMES_HOME",
48166
+ "kimi-code": "KIMI_CODE_HOME"
48167
+ };
48168
+ /**
48169
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48170
+ * output root in global scope.
48171
+ *
48172
+ * The override wins over `--output-roots`: it names where the tool itself reads
48173
+ * its profile, so writing anywhere else would produce files the tool ignores.
48174
+ *
48175
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48176
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48177
+ * instead of silently becoming the output root. The rejection is re-thrown
48178
+ * naming the variable, since the user never passed an `--output-roots` flag.
48179
+ */
47251
48180
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
47252
48181
  if (!global) return outputRoot;
47253
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48182
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
47254
48183
  outputRoot,
47255
48184
  global
47256
- });
47257
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
47258
- return outputRoot;
48185
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48186
+ if (resolved === outputRoot) return resolved;
48187
+ try {
48188
+ validateOutputRoot(resolved);
48189
+ } catch (error) {
48190
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48191
+ }
48192
+ return resolved;
47259
48193
  }
47260
48194
  //#endregion
47261
48195
  //#region src/lib/convert.ts
@@ -48656,7 +49590,11 @@ async function generateChecksCore(params) {
48656
49590
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
48657
49591
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
48658
49592
  const processor = new ChecksProcessor({
48659
- outputRoot,
49593
+ outputRoot: resolveToolOutputRoot({
49594
+ outputRoot,
49595
+ toolTarget,
49596
+ global: config.getGlobal()
49597
+ }),
48660
49598
  inputRoot: config.getInputRoot(),
48661
49599
  toolTarget,
48662
49600
  global: config.getGlobal(),
@@ -49031,4 +49969,4 @@ async function importChecksCore(params) {
49031
49969
  //#endregion
49032
49970
  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 };
49033
49971
 
49034
- //# sourceMappingURL=import-BmXT7mRS.js.map
49972
+ //# sourceMappingURL=import-CArKOPG_.js.map