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.
@@ -127,27 +127,27 @@ const isFeatureValueEnabled = (value) => {
127
127
  };
128
128
  //#endregion
129
129
  //#region src/constants/rulesync-paths.ts
130
- const { join: join$277 } = node_path.posix;
130
+ const { join: join$281 } = node_path.posix;
131
131
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
132
132
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
133
133
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
134
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$277(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
- join$277(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
134
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
+ join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
145
145
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
146
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
146
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
147
147
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
148
148
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
149
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$277(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
149
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
151
151
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
152
152
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
153
153
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -217,6 +217,7 @@ const ignoreProcessorToolTargetTuple = [
217
217
  "kiro-cli",
218
218
  "kiro-ide",
219
219
  "qwencode",
220
+ "reasonix",
220
221
  "roo",
221
222
  "devin",
222
223
  "vibe",
@@ -274,6 +275,7 @@ const commandsProcessorToolTargetTuple = [
274
275
  "cursor",
275
276
  "factorydroid",
276
277
  "goose",
278
+ "grokcli",
277
279
  "hermesagent",
278
280
  "junie",
279
281
  "kilo",
@@ -429,6 +431,7 @@ const checksProcessorToolTargetTuple = [
429
431
  "amp",
430
432
  "cursor",
431
433
  "hermesagent",
434
+ "rovodev",
432
435
  "takt"
433
436
  ];
434
437
  //#endregion
@@ -1082,6 +1085,17 @@ const ConfigFileSchema = zod_mini.z.object({
1082
1085
  });
1083
1086
  zod_mini.z.required(ConfigParamsSchema);
1084
1087
  /**
1088
+ * Normalizes the configuration file location to an absolute path.
1089
+ *
1090
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1091
+ * only covers direct programmatic construction, where the conventional
1092
+ * location next to the input root is the best guess.
1093
+ */
1094
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1095
+ if (configFilePath === void 0) return (0, node_path.join)(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1096
+ return (0, node_path.isAbsolute)(configFilePath) ? configFilePath : (0, node_path.resolve)(configFilePath);
1097
+ }
1098
+ /**
1085
1099
  * Conflicting target pairs that cannot be used together
1086
1100
  */
1087
1101
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
@@ -1152,8 +1166,9 @@ var Config = class Config {
1152
1166
  dryRun;
1153
1167
  check;
1154
1168
  inputRoot;
1169
+ configFilePath;
1155
1170
  sources;
1156
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1171
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1157
1172
  assertTargetsFeaturesExclusive({
1158
1173
  targets,
1159
1174
  features
@@ -1186,6 +1201,10 @@ var Config = class Config {
1186
1201
  this.dryRun = dryRun ?? false;
1187
1202
  this.check = check ?? false;
1188
1203
  this.inputRoot = inputRoot === void 0 ? process.cwd() : (0, node_path.isAbsolute)(inputRoot) ? inputRoot : (0, node_path.resolve)(inputRoot);
1204
+ this.configFilePath = normalizeConfigFilePath({
1205
+ configFilePath,
1206
+ inputRoot: this.inputRoot
1207
+ });
1189
1208
  this.sources = sources ?? [];
1190
1209
  }
1191
1210
  /**
@@ -1379,6 +1398,14 @@ var Config = class Config {
1379
1398
  getInputRoot() {
1380
1399
  return this.inputRoot;
1381
1400
  }
1401
+ /**
1402
+ * Returns the absolute path of the configuration file this config was
1403
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1404
+ * without one — so callers must treat this as a location, not a guarantee.
1405
+ */
1406
+ getConfigFilePath() {
1407
+ return this.configFilePath;
1408
+ }
1382
1409
  getSources() {
1383
1410
  return this.sources;
1384
1411
  }
@@ -1601,6 +1628,7 @@ var ConfigResolver = class {
1601
1628
  fallback: getDefaults().check
1602
1629
  }),
1603
1630
  inputRoot: resolvedInputRoot !== void 0 ? (0, node_path.resolve)(resolvedInputRoot) : cwd,
1631
+ configFilePath: validatedConfigPath,
1604
1632
  sources: configByFile.sources ?? getDefaults().sources,
1605
1633
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1606
1634
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -4011,10 +4039,30 @@ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
4011
4039
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
4012
4040
  * override; each value may be a bare action string or a pattern map.
4013
4041
  *
4042
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4043
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4044
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4045
+ * `writable_paths`. It has no canonical permission category, so it is authored
4046
+ * here and emitted only for Kilo.
4047
+ *
4048
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4049
+ * `writable_paths` are honored from the global config only, and a project
4050
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4051
+ * project-level network denial even clears the global destination exceptions.
4052
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4053
+ * written, and the rest are dropped with a warning rather than emitted into a
4054
+ * file Kilo would ignore.
4055
+ *
4014
4056
  * @example
4015
4057
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4058
+ * @example
4059
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4060
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
4016
4061
  */
4017
- const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
4062
+ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({
4063
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)),
4064
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
4065
+ });
4018
4066
  /**
4019
4067
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
4020
4068
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -5323,6 +5371,26 @@ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5323
5371
  const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5324
5372
  const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5325
5373
  //#endregion
5374
+ //#region src/constants/rovodev-paths.ts
5375
+ const ROVODEV_DIR = ".rovodev";
5376
+ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
5377
+ const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
5378
+ const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
5379
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5380
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5381
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5382
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5383
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
5384
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5385
+ const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
5386
+ /**
5387
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5388
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5389
+ * dot in the file name.
5390
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5391
+ */
5392
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5393
+ //#endregion
5326
5394
  //#region src/constants/takt-paths.ts
5327
5395
  const TAKT_DIR = ".takt";
5328
5396
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5784,12 +5852,19 @@ function slugifyCheckName(value) {
5784
5852
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5785
5853
  }
5786
5854
  //#endregion
5787
- //#region src/features/checks/cursor-check.ts
5855
+ //#region src/features/checks/aggregated-check-file.ts
5788
5856
  /**
5789
- * Marks where one check starts inside the single instruction file. Bugbot reads
5790
- * the file as prose, and an HTML comment is invisible in rendered Markdown, so
5791
- * the marker carries the check identity without changing what Bugbot is told.
5857
+ * Shared machinery for the tools whose checks surface is **one aggregated
5858
+ * instruction file** rather than a file per check Cursor Bugbot's
5859
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5860
+ *
5861
+ * Both read the file as free prose, so the check identities have to be carried
5862
+ * in something invisible to the reader: an HTML-comment marker per section.
5863
+ * That marker convention, the escaping that keeps a check body from splitting
5864
+ * itself, and the import-side split are the same for both files, so they live
5865
+ * here once.
5792
5866
  */
5867
+ /** Marks where one check starts inside the single instruction file. */
5793
5868
  const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5794
5869
  /**
5795
5870
  * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
@@ -5800,17 +5875,16 @@ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5800
5875
  */
5801
5876
  const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5802
5877
  const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5803
- const FALLBACK_CHECK_NAME = "bugbot";
5804
- function renderMarker(name) {
5878
+ function renderCheckMarker(name) {
5805
5879
  return `<!-- rulesync:check:${name} -->`;
5806
5880
  }
5807
- function escapeMarkers(content) {
5881
+ function escapeCheckMarkers(content) {
5808
5882
  return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5809
5883
  }
5810
- function unescapeMarkers(content) {
5884
+ function unescapeCheckMarkers(content) {
5811
5885
  return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5812
5886
  }
5813
- function findMarkers(fileContent) {
5887
+ function findCheckMarkers(fileContent) {
5814
5888
  CHECK_MARKER_PATTERN.lastIndex = 0;
5815
5889
  const markers = [];
5816
5890
  let match = CHECK_MARKER_PATTERN.exec(fileContent);
@@ -5825,23 +5899,47 @@ function findMarkers(fileContent) {
5825
5899
  return markers;
5826
5900
  }
5827
5901
  /**
5828
- * The instruction text one check contributes. Bugbot has no field to put a
5829
- * summary in, so `description` is used only when there is no body — the same
5830
- * fallback the file-stem heading above it gets.
5902
+ * Whether the file holds instruction text ahead of the first marker the
5903
+ * question the "generating replaces this" warning asks. A file with no marker
5904
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5905
+ * since there is nothing to replace.
5906
+ */
5907
+ function hasHandWrittenPreamble(fileContent) {
5908
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5909
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5910
+ }
5911
+ /**
5912
+ * Whether the file is nothing but sections rulesync generated — the question
5913
+ * the deletion guard asks, and a stricter one than
5914
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5915
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5916
+ * empty one is somebody's placeholder rather than our leftover.
5917
+ */
5918
+ function isOnlyGeneratedSections(fileContent) {
5919
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5920
+ if (firstMarkerStart === void 0) return false;
5921
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5922
+ }
5923
+ /**
5924
+ * The instruction text one check contributes. Neither file has a field to put a
5925
+ * summary in, so `description` is used only when there is no body.
5831
5926
  */
5832
5927
  function toInstruction(rulesyncCheck) {
5833
5928
  const body = rulesyncCheck.getBody().trim();
5834
5929
  if (body.length > 0) return body;
5835
5930
  return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5836
5931
  }
5837
- function renderSection(rulesyncCheck) {
5932
+ function renderCheckSection(rulesyncCheck) {
5838
5933
  const name = (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
5839
5934
  const heading = `## ${name}`;
5840
5935
  const instruction = toInstruction(rulesyncCheck);
5841
- const lines = [renderMarker(name), heading];
5842
- if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5936
+ const lines = [renderCheckMarker(name), heading];
5937
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5843
5938
  return lines.join("\n");
5844
5939
  }
5940
+ function renderCheckFile(rulesyncChecks) {
5941
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5942
+ }
5845
5943
  /** Drop the heading generate writes, so a round trip does not stack headings. */
5846
5944
  function stripGeneratedHeading(section, name) {
5847
5945
  const [firstLine, ...rest] = section.split("\n");
@@ -5849,6 +5947,53 @@ function stripGeneratedHeading(section, name) {
5849
5947
  return section.trim();
5850
5948
  }
5851
5949
  /**
5950
+ * Split an aggregated instruction file back into one check per section.
5951
+ *
5952
+ * Content ahead of the first marker — and a hand-written file with no markers
5953
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5954
+ * is dropped.
5955
+ */
5956
+ function splitCheckFile({ fileContent, fallbackName }) {
5957
+ const sections = [];
5958
+ const markers = findCheckMarkers(fileContent);
5959
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5960
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5961
+ if (preamble.length > 0) sections.push({
5962
+ name: fallbackName,
5963
+ content: unescapeCheckMarkers(preamble)
5964
+ });
5965
+ for (const [index, marker] of markers.entries()) {
5966
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5967
+ const markerName = marker.name.trim();
5968
+ const name = slugifyCheckName(markerName) || fallbackName;
5969
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5970
+ sections.push({
5971
+ name,
5972
+ content: unescapeCheckMarkers(content)
5973
+ });
5974
+ }
5975
+ const used = /* @__PURE__ */ new Set();
5976
+ return sections.map(({ name, content }) => {
5977
+ let uniqueName = name;
5978
+ let suffix = 2;
5979
+ while (used.has(uniqueName)) {
5980
+ uniqueName = `${name}-${suffix}`;
5981
+ suffix += 1;
5982
+ }
5983
+ used.add(uniqueName);
5984
+ return new RulesyncCheck({
5985
+ outputRoot: ".",
5986
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5987
+ relativeFilePath: `${uniqueName}.md`,
5988
+ frontmatter: { targets: ["*"] },
5989
+ body: content
5990
+ });
5991
+ });
5992
+ }
5993
+ //#endregion
5994
+ //#region src/features/checks/cursor-check.ts
5995
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
5996
+ /**
5852
5997
  * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5853
5998
  *
5854
5999
  * Bugbot takes one aggregated instruction file per directory rather than a file
@@ -5905,9 +6050,7 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5905
6050
  const paths = CursorCheck.getSettablePaths();
5906
6051
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5907
6052
  if (fileContent === null) return true;
5908
- const firstMarkerStart = findMarkers(fileContent)[0]?.start;
5909
- if (firstMarkerStart === void 0) return false;
5910
- return fileContent.slice(0, firstMarkerStart).trim().length === 0;
6053
+ return isOnlyGeneratedSections(fileContent);
5911
6054
  }
5912
6055
  static fromRulesyncCheck(_params) {
5913
6056
  throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
@@ -5917,10 +6060,8 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5917
6060
  const paths = CursorCheck.getSettablePaths({ global });
5918
6061
  const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5919
6062
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
5920
- const existingContent = await readFileContentOrNull(filePath) ?? "";
5921
- const firstMarkerStart = findMarkers(existingContent)[0]?.start ?? existingContent.length;
5922
- 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.`);
5923
- const fileContent = `${rulesyncChecks.map(renderSection).join("\n\n")}\n`;
6063
+ 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.`);
6064
+ const fileContent = renderCheckFile(rulesyncChecks);
5924
6065
  return [new CursorCheck({
5925
6066
  outputRoot,
5926
6067
  relativeDirPath: paths.relativeDirPath,
@@ -5963,41 +6104,9 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5963
6104
  return first;
5964
6105
  }
5965
6106
  toRulesyncChecks() {
5966
- const fileContent = this.getFileContent();
5967
- const sections = [];
5968
- const markers = findMarkers(fileContent);
5969
- const preambleEnd = markers[0]?.start ?? fileContent.length;
5970
- const preamble = fileContent.slice(0, preambleEnd).trim();
5971
- if (preamble.length > 0) sections.push({
5972
- name: FALLBACK_CHECK_NAME,
5973
- content: unescapeMarkers(preamble)
5974
- });
5975
- for (const [index, marker] of markers.entries()) {
5976
- const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5977
- const markerName = marker.name.trim();
5978
- const name = slugifyCheckName(markerName) || FALLBACK_CHECK_NAME;
5979
- const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5980
- sections.push({
5981
- name,
5982
- content: unescapeMarkers(content)
5983
- });
5984
- }
5985
- const used = /* @__PURE__ */ new Set();
5986
- return sections.map(({ name, content }) => {
5987
- let uniqueName = name;
5988
- let suffix = 2;
5989
- while (used.has(uniqueName)) {
5990
- uniqueName = `${name}-${suffix}`;
5991
- suffix += 1;
5992
- }
5993
- used.add(uniqueName);
5994
- return new RulesyncCheck({
5995
- outputRoot: ".",
5996
- relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5997
- relativeFilePath: `${uniqueName}.md`,
5998
- frontmatter: { targets: ["*"] },
5999
- body: content
6000
- });
6107
+ return splitCheckFile({
6108
+ fileContent: this.getFileContent(),
6109
+ fallbackName: FALLBACK_CHECK_NAME$1
6001
6110
  });
6002
6111
  }
6003
6112
  };
@@ -6019,8 +6128,22 @@ var CursorCheck = class CursorCheck extends ToolCheck {
6019
6128
  */
6020
6129
  /** Project-root instruction file auto-injected by Hermes Agent. */
6021
6130
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
6022
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6131
+ /**
6132
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6133
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6134
+ */
6023
6135
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6136
+ /**
6137
+ * Home-relative global profile root on Windows: upstream defaults to
6138
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6139
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6140
+ *
6141
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6142
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6143
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6144
+ * followed; those users should set `HERMES_HOME` explicitly.
6145
+ */
6146
+ const HERMESAGENT_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Local", "hermes");
6024
6147
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
6025
6148
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
6026
6149
  const HERMESAGENT_CONFIG_FILE_PATH = (0, node_path.join)(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -6243,6 +6366,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
6243
6366
  }
6244
6367
  };
6245
6368
  //#endregion
6369
+ //#region src/features/checks/rovodev-check.ts
6370
+ const FALLBACK_CHECK_NAME = "review-agent";
6371
+ /**
6372
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6373
+ * (`.rovodev/.review-agent.md`).
6374
+ *
6375
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6376
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6377
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6378
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6379
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6380
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6381
+ * adapters share).
6382
+ *
6383
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6384
+ * have no equivalent there: they are not written and do not come back on
6385
+ * import. Neither does `description` whenever the check also has a body.
6386
+ *
6387
+ * Project scope only — these are per-repository review instructions, and Rovo
6388
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6389
+ * same tool is the opposite: global only.)
6390
+ *
6391
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6392
+ */
6393
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6394
+ static getSettablePaths(_options = {}) {
6395
+ return {
6396
+ relativeDirPath: ROVODEV_DIR,
6397
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6398
+ };
6399
+ }
6400
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6401
+ return this.isTargetedByRulesyncCheckDefault({
6402
+ rulesyncCheck,
6403
+ toolTarget: "rovodev"
6404
+ });
6405
+ }
6406
+ /**
6407
+ * Ownership guard the processor consults before it deletes anything for this
6408
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6409
+ * to hand-write, so anything in it that rulesync did not write is not
6410
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6411
+ * take somebody's hand-written review instructions with it.
6412
+ */
6413
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6414
+ const paths = RovodevCheck.getSettablePaths();
6415
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6416
+ if (fileContent === null) return true;
6417
+ return isOnlyGeneratedSections(fileContent);
6418
+ }
6419
+ static fromRulesyncCheck(_params) {
6420
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6421
+ }
6422
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6423
+ if (rulesyncChecks.length === 0) return [];
6424
+ const paths = RovodevCheck.getSettablePaths({ global });
6425
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6426
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6427
+ 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.`);
6428
+ return [new RovodevCheck({
6429
+ outputRoot,
6430
+ relativeDirPath: paths.relativeDirPath,
6431
+ relativeFilePath,
6432
+ fileContent: renderCheckFile(rulesyncChecks),
6433
+ global
6434
+ })];
6435
+ }
6436
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6437
+ const paths = RovodevCheck.getSettablePaths({ global });
6438
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6439
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6440
+ return new RovodevCheck({
6441
+ outputRoot,
6442
+ relativeDirPath: paths.relativeDirPath,
6443
+ relativeFilePath,
6444
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6445
+ global
6446
+ });
6447
+ }
6448
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6449
+ return new RovodevCheck({
6450
+ outputRoot,
6451
+ relativeDirPath,
6452
+ relativeFilePath,
6453
+ fileContent: "",
6454
+ validate: false,
6455
+ global
6456
+ });
6457
+ }
6458
+ validate() {
6459
+ return {
6460
+ success: true,
6461
+ error: null
6462
+ };
6463
+ }
6464
+ toRulesyncCheck() {
6465
+ const first = this.toRulesyncChecks()[0];
6466
+ if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6467
+ return first;
6468
+ }
6469
+ toRulesyncChecks() {
6470
+ return splitCheckFile({
6471
+ fileContent: this.getFileContent(),
6472
+ fallbackName: FALLBACK_CHECK_NAME
6473
+ });
6474
+ }
6475
+ };
6476
+ //#endregion
6246
6477
  //#region src/constants/codexcli-paths.ts
6247
6478
  const CODEXCLI_DIR = ".codex";
6248
6479
  const CODEXCLI_PROMPTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "prompts");
@@ -6355,11 +6586,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6355
6586
  }
6356
6587
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6357
6588
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6589
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6590
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6358
6591
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6359
6592
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6360
6593
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6361
6594
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6362
6595
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6596
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6363
6597
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6364
6598
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6365
6599
  /**
@@ -6383,6 +6617,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6383
6617
  * lock-step with the writers derived from the processor registry, so an
6384
6618
  * undeclared writer fails CI instead of merging by accident.
6385
6619
  */
6620
+ /**
6621
+ * Hermes writes one `config.yaml`, but its global profile root has three
6622
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6623
+ * itself). They are the same file with the same owners, so the declaration is
6624
+ * written once and shared — a policy edit cannot land on one spelling only.
6625
+ */
6626
+ const HERMES_CONFIG_DECLARATION = {
6627
+ format: "yaml",
6628
+ features: {
6629
+ commands: {
6630
+ kind: "replace-owned-keys",
6631
+ ownedKeys: ["plugins"]
6632
+ },
6633
+ subagents: {
6634
+ kind: "replace-owned-keys",
6635
+ ownedKeys: ["plugins"]
6636
+ },
6637
+ mcp: {
6638
+ kind: "replace-owned-keys",
6639
+ ownedKeys: ["mcp_servers"]
6640
+ },
6641
+ hooks: {
6642
+ kind: "replace-owned-keys",
6643
+ ownedKeys: ["hooks"]
6644
+ },
6645
+ permissions: {
6646
+ kind: "deep-merge",
6647
+ replaceKeys: ["permissions"]
6648
+ }
6649
+ }
6650
+ };
6651
+ /**
6652
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6653
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6654
+ * name the profile directory itself, so the file has two spellings that share
6655
+ * one declaration — a policy edit cannot land on only one of them.
6656
+ */
6657
+ const KIMI_CODE_CONFIG_DECLARATION = {
6658
+ format: "toml",
6659
+ invalidRootPolicy: "error",
6660
+ features: {
6661
+ hooks: {
6662
+ kind: "replace-owned-keys",
6663
+ ownedKeys: ["hooks"]
6664
+ },
6665
+ mcp: {
6666
+ kind: "replace-owned-keys",
6667
+ ownedKeys: ["mcp"]
6668
+ },
6669
+ permissions: {
6670
+ kind: "replace-owned-keys",
6671
+ ownedKeys: [
6672
+ "permission",
6673
+ "default_permission_mode",
6674
+ "tools"
6675
+ ]
6676
+ }
6677
+ }
6678
+ };
6386
6679
  const SHARED_CONFIG_OWNERSHIP = {
6387
6680
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6388
6681
  format: "json",
@@ -6401,31 +6694,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6401
6694
  }
6402
6695
  }
6403
6696
  },
6404
- [HERMES_CONFIG_SHARED_FILE_KEY]: {
6405
- format: "yaml",
6406
- features: {
6407
- commands: {
6408
- kind: "replace-owned-keys",
6409
- ownedKeys: ["plugins"]
6410
- },
6411
- subagents: {
6412
- kind: "replace-owned-keys",
6413
- ownedKeys: ["plugins"]
6414
- },
6415
- mcp: {
6416
- kind: "replace-owned-keys",
6417
- ownedKeys: ["mcp_servers"]
6418
- },
6419
- hooks: {
6420
- kind: "replace-owned-keys",
6421
- ownedKeys: ["hooks"]
6422
- },
6423
- permissions: {
6424
- kind: "deep-merge",
6425
- replaceKeys: ["permissions"]
6426
- }
6427
- }
6428
- },
6697
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6698
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6699
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6429
6700
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6430
6701
  format: "yaml",
6431
6702
  invalidRootPolicy: "error",
@@ -6471,6 +6742,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6471
6742
  ".config/zed/settings.json": {
6472
6743
  format: "json",
6473
6744
  features: {
6745
+ ignore: {
6746
+ kind: "replace-owned-keys",
6747
+ ownedKeys: ["private_files"]
6748
+ },
6474
6749
  mcp: {
6475
6750
  kind: "replace-owned-keys",
6476
6751
  ownedKeys: ["context_servers"]
@@ -6484,6 +6759,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6484
6759
  "AppData/Roaming/Zed/settings.json": {
6485
6760
  format: "json",
6486
6761
  features: {
6762
+ ignore: {
6763
+ kind: "replace-owned-keys",
6764
+ ownedKeys: ["private_files"]
6765
+ },
6487
6766
  mcp: {
6488
6767
  kind: "replace-owned-keys",
6489
6768
  ownedKeys: ["context_servers"]
@@ -6740,31 +7019,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6740
7019
  }
6741
7020
  }
6742
7021
  },
6743
- [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: {
6744
- format: "toml",
6745
- invalidRootPolicy: "error",
6746
- features: {
6747
- hooks: {
6748
- kind: "replace-owned-keys",
6749
- ownedKeys: ["hooks"]
6750
- },
6751
- mcp: {
6752
- kind: "replace-owned-keys",
6753
- ownedKeys: ["mcp"]
6754
- },
6755
- permissions: {
6756
- kind: "replace-owned-keys",
6757
- ownedKeys: [
6758
- "permission",
6759
- "default_permission_mode",
6760
- "tools"
6761
- ]
6762
- }
6763
- }
6764
- },
7022
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
7023
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6765
7024
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6766
7025
  format: "toml",
6767
7026
  features: {
7027
+ ignore: {
7028
+ kind: "custom",
7029
+ policyFunction: "applyIgnoreReadDenies"
7030
+ },
6768
7031
  mcp: {
6769
7032
  kind: "replace-owned-keys",
6770
7033
  ownedKeys: ["plugins"]
@@ -6782,6 +7045,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6782
7045
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6783
7046
  format: "toml",
6784
7047
  features: {
7048
+ ignore: {
7049
+ kind: "custom",
7050
+ policyFunction: "applyIgnoreReadDenies"
7051
+ },
6785
7052
  mcp: {
6786
7053
  kind: "replace-owned-keys",
6787
7054
  ownedKeys: ["plugins"]
@@ -7226,6 +7493,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7226
7493
  filePattern: "*.json"
7227
7494
  }
7228
7495
  }],
7496
+ ["rovodev", {
7497
+ class: RovodevCheck,
7498
+ meta: {
7499
+ supportsGlobal: false,
7500
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7501
+ }
7502
+ }],
7229
7503
  ["takt", {
7230
7504
  class: TaktCheck,
7231
7505
  meta: {
@@ -7381,19 +7655,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7381
7655
  }
7382
7656
  };
7383
7657
  //#endregion
7658
+ //#region src/utils/tool-home.ts
7659
+ /**
7660
+ * Where the rulesync-side source files of a tool with a home override belong.
7661
+ *
7662
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7663
+ * imported back out of it are not part of the tool's profile — they stay under
7664
+ * the rulesync home. When no override is set, the native output root already is
7665
+ * that place.
7666
+ */
7667
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7668
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7669
+ }
7670
+ //#endregion
7384
7671
  //#region src/utils/hermesagent.ts
7385
7672
  function getHermesagentHome() {
7386
7673
  const configuredHome = process.env.HERMES_HOME?.trim();
7387
7674
  return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
7388
7675
  }
7676
+ /**
7677
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7678
+ *
7679
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7680
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7681
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7682
+ * would land where Hermes never reads.
7683
+ *
7684
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7685
+ */
7686
+ function getHermesagentGlobalDir() {
7687
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7688
+ }
7389
7689
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7390
7690
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7391
7691
  }
7692
+ /**
7693
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7694
+ * actually writes in the requested scope.
7695
+ *
7696
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7697
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7698
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7699
+ * platform default directory takes its place.
7700
+ */
7392
7701
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7393
- if (!global || !getHermesagentHome()) return relativeDirPath;
7702
+ if (!global) return relativeDirPath;
7394
7703
  const relativePath = (0, node_path.relative)(HERMESAGENT_GLOBAL_DIR, relativeDirPath);
7395
- if (relativePath === ".." || relativePath.startsWith(`..${node_path.sep}`) || (0, node_path.isAbsolute)(relativePath)) throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7396
- return relativePath;
7704
+ try {
7705
+ checkPathTraversal({
7706
+ relativePath: relativeDirPath,
7707
+ intendedRootDir: "."
7708
+ });
7709
+ checkPathTraversal({
7710
+ relativePath,
7711
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7712
+ });
7713
+ } catch {
7714
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7715
+ }
7716
+ return getHermesagentHome() ? relativePath || "." : (0, node_path.join)(getHermesagentGlobalDir(), relativePath);
7397
7717
  }
7398
7718
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7399
7719
  return (0, node_path.join)(getHermesagentRelativeDirPath({
@@ -7401,8 +7721,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7401
7721
  relativeDirPath: (0, node_path.dirname)(relativeFilePath)
7402
7722
  }), (0, node_path.basename)(relativeFilePath));
7403
7723
  }
7724
+ /**
7725
+ * Every spelling `config.yaml` can take in global scope, so that the
7726
+ * shared-write derivation and the gateway ownership table it is checked against
7727
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7728
+ *
7729
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7730
+ * which would otherwise make the derived shared-file key depend on the ambient
7731
+ * environment — the drift guards would then go blind in precisely the
7732
+ * configuration this feature exists for.
7733
+ */
7734
+ function getHermesagentSharedConfigWritePaths() {
7735
+ return [
7736
+ {
7737
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7738
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7739
+ },
7740
+ {
7741
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7742
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7743
+ },
7744
+ {
7745
+ relativeDirPath: ".",
7746
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7747
+ }
7748
+ ];
7749
+ }
7750
+ /**
7751
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7752
+ * writes. All three spellings carry the same declaration, but passing the key of
7753
+ * the file being written keeps the write path and the drift guards reading the
7754
+ * same entry.
7755
+ */
7756
+ function getHermesagentConfigSharedFileKey({ global }) {
7757
+ return sharedConfigFileKey({
7758
+ relativeDirPath: getHermesagentRelativeDirPath({
7759
+ global,
7760
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7761
+ }),
7762
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7763
+ });
7764
+ }
7404
7765
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7405
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7766
+ return getToolRulesyncOutputRoot({
7767
+ nativeOutputRoot,
7768
+ global,
7769
+ toolHome: getHermesagentHome
7770
+ });
7406
7771
  }
7407
7772
  //#endregion
7408
7773
  //#region src/constants/agentsmd-paths.ts
@@ -9106,6 +9471,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
9106
9471
  }
9107
9472
  };
9108
9473
  //#endregion
9474
+ //#region src/constants/grokcli-paths.ts
9475
+ /**
9476
+ * Grok Build CLI (xAI) configuration-layout conventions.
9477
+ *
9478
+ * Single source of truth for where Grok Build expects its files. Grok Build
9479
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9480
+ * with project/global scopes resolved by the directory the CLI runs in
9481
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9482
+ *
9483
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9484
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9485
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9486
+ * @see https://docs.x.ai/build/overview
9487
+ */
9488
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9489
+ const GROKCLI_DIR = ".grok";
9490
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9491
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9492
+ /**
9493
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9494
+ * permission mode, and other settings all live here; permissions reuse the same
9495
+ * file name as MCP since Grok consolidates everything into one config.
9496
+ */
9497
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9498
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9499
+ const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
9500
+ /**
9501
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9502
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9503
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9504
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9505
+ * @see https://docs.x.ai/build/features/hooks
9506
+ */
9507
+ const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
9508
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9509
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9510
+ /**
9511
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9512
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9513
+ * (global), each a Markdown file with YAML frontmatter (verified via
9514
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9515
+ */
9516
+ const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
9517
+ /**
9518
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9519
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9520
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9521
+ * MCP/skills/subagents adapters).
9522
+ */
9523
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9524
+ /**
9525
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9526
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9527
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9528
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9529
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9530
+ * for Claude Code.
9531
+ *
9532
+ * Skills are collected before commands and win name collisions, so a
9533
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9534
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9535
+ */
9536
+ const GROKCLI_COMMANDS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "commands");
9537
+ /**
9538
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9539
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9540
+ * walks, and `~/.grok/rules/` in the home scope.
9541
+ * @see https://docs.x.ai/build/overview
9542
+ */
9543
+ const GROKCLI_RULES_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "rules");
9544
+ //#endregion
9545
+ //#region src/features/commands/grokcli-command.ts
9546
+ /**
9547
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9548
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9549
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9550
+ *
9551
+ * Two upstream constraints shape this adapter:
9552
+ *
9553
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9554
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9555
+ * onto their basename).
9556
+ * - Skills are collected before commands and win name collisions, so a
9557
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9558
+ *
9559
+ * `description` and `argument-hint` describe the command, while
9560
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9561
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9562
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9563
+ */
9564
+ const GrokcliCommandFrontmatterSchema = zod_mini.z.looseObject({
9565
+ description: zod_mini.z.optional(zod_mini.z.string()),
9566
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
9567
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9568
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
9569
+ });
9570
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9571
+ frontmatter;
9572
+ body;
9573
+ constructor({ frontmatter, body, ...rest }) {
9574
+ if (rest.validate) {
9575
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9576
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9577
+ }
9578
+ super({
9579
+ ...rest,
9580
+ fileContent: stringifyFrontmatter(body, frontmatter)
9581
+ });
9582
+ this.frontmatter = frontmatter;
9583
+ this.body = body;
9584
+ }
9585
+ static getSettablePaths(_options = {}) {
9586
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9587
+ }
9588
+ getBody() {
9589
+ return this.body;
9590
+ }
9591
+ getFrontmatter() {
9592
+ return this.frontmatter;
9593
+ }
9594
+ toRulesyncCommand() {
9595
+ const { description, ...restFields } = this.frontmatter;
9596
+ const rulesyncFrontmatter = {
9597
+ targets: ["*"],
9598
+ description,
9599
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9600
+ };
9601
+ return new RulesyncCommand({
9602
+ outputRoot: ".",
9603
+ frontmatter: rulesyncFrontmatter,
9604
+ body: this.body,
9605
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9606
+ relativeFilePath: this.relativeFilePath,
9607
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9608
+ validate: true
9609
+ });
9610
+ }
9611
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9612
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9613
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9614
+ const grokcliFrontmatter = {
9615
+ description: rulesyncFrontmatter.description,
9616
+ ...grokcliFields
9617
+ };
9618
+ const paths = this.getSettablePaths({ global });
9619
+ return new GrokcliCommand({
9620
+ outputRoot,
9621
+ frontmatter: grokcliFrontmatter,
9622
+ body: rulesyncCommand.getBody(),
9623
+ relativeDirPath: paths.relativeDirPath,
9624
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9625
+ validate
9626
+ });
9627
+ }
9628
+ validate() {
9629
+ if (!this.frontmatter) return {
9630
+ success: true,
9631
+ error: null
9632
+ };
9633
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9634
+ if (result.success) return {
9635
+ success: true,
9636
+ error: null
9637
+ };
9638
+ return {
9639
+ success: false,
9640
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9641
+ };
9642
+ }
9643
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9644
+ return this.isTargetedByRulesyncCommandDefault({
9645
+ rulesyncCommand,
9646
+ toolTarget: "grokcli"
9647
+ });
9648
+ }
9649
+ /**
9650
+ * Warn when a rulesync skill would shadow a rulesync command.
9651
+ *
9652
+ * Grok collects skills before commands and lets skills win name collisions,
9653
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9654
+ * Both files are still written correctly — nothing is overwritten and no
9655
+ * output is lost — so this warns rather than failing the run the way the
9656
+ * Hermes check does, where the two surfaces really do write the same path.
9657
+ */
9658
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9659
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
9660
+ if (commandNames.size === 0) return;
9661
+ const shadowed = (await findFilesByGlobs((0, node_path.join)((0, node_path.join)(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
9662
+ 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.`);
9663
+ }
9664
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9665
+ const paths = this.getSettablePaths({ global });
9666
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
9667
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9668
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9669
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9670
+ return new GrokcliCommand({
9671
+ outputRoot,
9672
+ relativeDirPath: paths.relativeDirPath,
9673
+ relativeFilePath,
9674
+ frontmatter: result.data,
9675
+ body: content.trim(),
9676
+ validate
9677
+ });
9678
+ }
9679
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9680
+ return new GrokcliCommand({
9681
+ outputRoot,
9682
+ relativeDirPath,
9683
+ relativeFilePath,
9684
+ frontmatter: { description: "" },
9685
+ body: "",
9686
+ validate: false
9687
+ });
9688
+ }
9689
+ };
9690
+ //#endregion
9109
9691
  //#region src/features/skills/tool-skill.ts
9110
9692
  /** Ordered skill directory roots: primary first. */
9111
9693
  function toolSkillSearchRoots(paths) {
@@ -9740,7 +10322,7 @@ def register(ctx):
9740
10322
  _register_command(ctx, command)
9741
10323
  `;
9742
10324
  }
9743
- function getEnabledPluginConfigContent$1(currentContent) {
10325
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9744
10326
  const config = parseSharedConfig({
9745
10327
  format: "yaml",
9746
10328
  fileContent: currentContent
@@ -9748,7 +10330,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9748
10330
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9749
10331
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9750
10332
  return applySharedConfigPatch({
9751
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10333
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9752
10334
  feature: "commands",
9753
10335
  existingContent: currentContent,
9754
10336
  patch: { plugins: {
@@ -9757,7 +10339,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9757
10339
  } }
9758
10340
  });
9759
10341
  }
9760
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10342
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9761
10343
  const config = parseSharedConfig({
9762
10344
  format: "yaml",
9763
10345
  fileContent: currentContent
@@ -9765,7 +10347,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9765
10347
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9766
10348
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9767
10349
  return applySharedConfigPatch({
9768
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10350
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9769
10351
  feature: "commands",
9770
10352
  existingContent: currentContent,
9771
10353
  patch: { plugins: {
@@ -9781,35 +10363,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9781
10363
  error: null
9782
10364
  };
9783
10365
  }
9784
- shouldMergeExistingFileContent() {
10366
+ /**
10367
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10368
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10369
+ */
10370
+ matchesPath(relativeFilePath) {
9785
10371
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9786
10372
  global: this.global,
9787
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10373
+ relativeFilePath
9788
10374
  }));
9789
10375
  }
10376
+ shouldMergeExistingFileContent() {
10377
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10378
+ }
9790
10379
  setFileContent(newFileContent) {
9791
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9792
- global: this.global,
9793
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9794
- }))) {
9795
- super.setFileContent(getEnabledPluginConfigContent$1(newFileContent));
10380
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10381
+ super.setFileContent(getEnabledPluginConfigContent$1({
10382
+ currentContent: newFileContent,
10383
+ global: this.global
10384
+ }));
9796
10385
  return;
9797
10386
  }
9798
10387
  super.setFileContent(newFileContent);
9799
10388
  }
9800
10389
  getFileContent() {
9801
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9802
- global: this.global,
9803
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH
9804
- }))) return getPluginManifestContent$2();
9805
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9806
- global: this.global,
9807
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH
9808
- }))) return getPluginInitContent$2();
9809
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9810
- global: this.global,
9811
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9812
- }))) return getEnabledPluginConfigContent$1(super.getFileContent());
10390
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10391
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10392
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10393
+ currentContent: super.getFileContent(),
10394
+ global: this.global
10395
+ });
9813
10396
  return super.getFileContent();
9814
10397
  }
9815
10398
  };
@@ -9826,14 +10409,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9826
10409
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9827
10410
  }) };
9828
10411
  }
9829
- static getExtraSharedWritePaths({ global = false } = {}) {
9830
- return [{
9831
- relativeDirPath: getHermesagentRelativeDirPath({
9832
- global,
9833
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
9834
- }),
9835
- relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
9836
- }];
10412
+ /**
10413
+ * `config.yaml` under every spelling the global profile root can take.
10414
+ * @see getHermesagentSharedConfigWritePaths
10415
+ */
10416
+ static getExtraSharedWritePaths() {
10417
+ return getHermesagentSharedConfigWritePaths();
9837
10418
  }
9838
10419
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9839
10420
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9858,33 +10439,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9858
10439
  }
9859
10440
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9860
10441
  if (toolCommands.length === 0 && !forDeletion) return [];
10442
+ const pluginDirPath = getHermesagentRelativeDirPath({
10443
+ global,
10444
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10445
+ });
9861
10446
  const pluginFiles = [
9862
10447
  new HermesagentCommandAuxiliaryFile({
9863
10448
  outputRoot,
9864
- relativeDirPath: getHermesagentRelativeDirPath({
9865
- global,
9866
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9867
- }),
10449
+ relativeDirPath: pluginDirPath,
9868
10450
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9869
10451
  fileContent: "",
9870
10452
  global
9871
10453
  }),
9872
10454
  new HermesagentCommandAuxiliaryFile({
9873
10455
  outputRoot,
9874
- relativeDirPath: getHermesagentRelativeDirPath({
9875
- global,
9876
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9877
- }),
10456
+ relativeDirPath: pluginDirPath,
9878
10457
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9879
10458
  fileContent: "Generated and owned by RuleSync.\n",
9880
10459
  global
9881
10460
  }),
9882
10461
  new HermesagentCommandAuxiliaryFile({
9883
10462
  outputRoot,
9884
- relativeDirPath: getHermesagentRelativeDirPath({
9885
- global,
9886
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9887
- }),
10463
+ relativeDirPath: pluginDirPath,
9888
10464
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9889
10465
  fileContent: "",
9890
10466
  global
@@ -11119,19 +11695,6 @@ var RooCommand = class RooCommand extends ToolCommand {
11119
11695
  }
11120
11696
  };
11121
11697
  //#endregion
11122
- //#region src/constants/rovodev-paths.ts
11123
- const ROVODEV_DIR = ".rovodev";
11124
- const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
11125
- const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
11126
- const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
11127
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
11128
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
11129
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
11130
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
11131
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
11132
- const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
11133
- const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
11134
- //#endregion
11135
11698
  //#region src/features/commands/rovodev-command.ts
11136
11699
  /**
11137
11700
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11715,6 +12278,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11715
12278
  supportsSubdirectory: false
11716
12279
  }
11717
12280
  }],
12281
+ ["grokcli", {
12282
+ class: GrokcliCommand,
12283
+ meta: {
12284
+ extension: "md",
12285
+ supportsProject: true,
12286
+ supportsGlobal: true,
12287
+ isSimulated: false,
12288
+ supportsSubdirectory: false
12289
+ }
12290
+ }],
11718
12291
  ["hermesagent", {
11719
12292
  class: HermesagentCommand,
11720
12293
  meta: {
@@ -11908,7 +12481,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11908
12481
  const factory = this.getFactory(this.toolTarget);
11909
12482
  await factory.class.validateRulesyncCommands?.({
11910
12483
  inputRoot: this.inputRoot,
11911
- rulesyncCommands
12484
+ rulesyncCommands,
12485
+ logger: this.logger
11912
12486
  });
11913
12487
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11914
12488
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -12046,7 +12620,10 @@ var CommandsProcessor = class extends FeatureProcessor {
12046
12620
  }));
12047
12621
  const currentContent = await readFileContentOrNull(configPath);
12048
12622
  if (currentContent === null) return changedCount;
12049
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12623
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12624
+ currentContent,
12625
+ global: this.global
12626
+ });
12050
12627
  if (nextContent === currentContent) return changedCount;
12051
12628
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
12052
12629
  else await writeFileContent(configPath, nextContent);
@@ -14508,64 +15085,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14508
15085
  }
14509
15086
  };
14510
15087
  //#endregion
14511
- //#region src/constants/grokcli-paths.ts
14512
- /**
14513
- * Grok Build CLI (xAI) configuration-layout conventions.
14514
- *
14515
- * Single source of truth for where Grok Build expects its files. Grok Build
14516
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14517
- * with project/global scopes resolved by the directory the CLI runs in
14518
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14519
- *
14520
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14521
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14522
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14523
- * @see https://docs.x.ai/build/overview
14524
- */
14525
- /** Root directory for Grok Build configuration, relative to the scope root. */
14526
- const GROKCLI_DIR = ".grok";
14527
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14528
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14529
- /**
14530
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14531
- * permission mode, and other settings all live here; permissions reuse the same
14532
- * file name as MCP since Grok consolidates everything into one config.
14533
- */
14534
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14535
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14536
- const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
14537
- /**
14538
- * Hooks directory under `.grok/`. Grok Build discovers hook config files from
14539
- * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
14540
- * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
14541
- * shape. rulesync writes all its hooks into a single `rulesync.json`.
14542
- * @see https://docs.x.ai/build/features/hooks
14543
- */
14544
- const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
14545
- /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
14546
- const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
14547
- /**
14548
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14549
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14550
- * (global), each a Markdown file with YAML frontmatter (verified via
14551
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14552
- */
14553
- const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
14554
- /**
14555
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14556
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14557
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14558
- * MCP/skills/subagents adapters).
14559
- */
14560
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14561
- /**
14562
- * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
14563
- * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
14564
- * walks, and `~/.grok/rules/` in the home scope.
14565
- * @see https://docs.x.ai/build/overview
14566
- */
14567
- const GROKCLI_RULES_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "rules");
14568
- //#endregion
14569
15088
  //#region src/features/hooks/grokcli-hooks.ts
14570
15089
  const GROKCLI_CONVERTER_CONFIG = {
14571
15090
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14811,6 +15330,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14811
15330
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14812
15331
  };
14813
15332
  }
15333
+ /**
15334
+ * `config.yaml` under every spelling the global profile root can take.
15335
+ * @see getHermesagentSharedConfigWritePaths
15336
+ */
15337
+ static getExtraSharedWritePaths() {
15338
+ return getHermesagentSharedConfigWritePaths();
15339
+ }
14814
15340
  constructor(params) {
14815
15341
  super({
14816
15342
  ...params,
@@ -14848,7 +15374,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14848
15374
  }
14849
15375
  setFileContent(fileContent) {
14850
15376
  this.fileContent = applySharedConfigPatch({
14851
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15377
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14852
15378
  feature: "hooks",
14853
15379
  existingContent: fileContent,
14854
15380
  patch: parseSharedConfig({
@@ -15224,8 +15750,38 @@ function getKimiCodeHome() {
15224
15750
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
15225
15751
  return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
15226
15752
  }
15753
+ /**
15754
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15755
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15756
+ * Declared unconditionally so the derived shared-file keys — and the drift
15757
+ * guards checked against them — do not depend on the ambient environment.
15758
+ */
15759
+ function getKimiCodeSharedConfigWritePaths() {
15760
+ return [{
15761
+ relativeDirPath: KIMI_CODE_DIR,
15762
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15763
+ }, {
15764
+ relativeDirPath: ".",
15765
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15766
+ }];
15767
+ }
15768
+ /**
15769
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15770
+ * Both spellings carry the same declaration, but passing the key of the file
15771
+ * being written keeps the write path and the drift guards on the same entry.
15772
+ */
15773
+ function getKimiCodeConfigSharedFileKey({ global }) {
15774
+ return sharedConfigFileKey({
15775
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15776
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15777
+ });
15778
+ }
15227
15779
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
15228
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15780
+ return getToolRulesyncOutputRoot({
15781
+ nativeOutputRoot,
15782
+ global,
15783
+ toolHome: getKimiCodeHome
15784
+ });
15229
15785
  }
15230
15786
  //#endregion
15231
15787
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -15323,13 +15879,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
15323
15879
  isDeletable() {
15324
15880
  return false;
15325
15881
  }
15882
+ /**
15883
+ * `config.toml` under both spellings its directory can take.
15884
+ * @see getKimiCodeSharedConfigWritePaths
15885
+ */
15886
+ static getExtraSharedWritePaths() {
15887
+ return getKimiCodeSharedConfigWritePaths();
15888
+ }
15326
15889
  shouldMergeExistingFileContent() {
15327
15890
  return true;
15328
15891
  }
15329
15892
  setFileContent(fileContent) {
15330
15893
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
15331
15894
  this.fileContent = applySharedConfigPatch({
15332
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15895
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
15333
15896
  feature: "hooks",
15334
15897
  existingContent: fileContent,
15335
15898
  patch: parseSharedConfig({
@@ -18020,6 +18583,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
18020
18583
  }
18021
18584
  };
18022
18585
  //#endregion
18586
+ //#region src/features/shared/reasonix-config-table.ts
18587
+ /**
18588
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18589
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18590
+ *
18591
+ * TOML is only structurally validated on parse, so a hand-edited config can
18592
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18593
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18594
+ * before merging, so the narrowing lives here once rather than being re-spelled
18595
+ * (and re-diverging) per feature.
18596
+ */
18597
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18598
+ function toReasonixStringArray(value) {
18599
+ if (!Array.isArray(value)) return [];
18600
+ return value.filter((entry) => typeof entry === "string");
18601
+ }
18602
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18603
+ function toReasonixTable(value) {
18604
+ if (!isPlainObject$1(value)) return {};
18605
+ return { ...value };
18606
+ }
18607
+ //#endregion
18608
+ //#region src/features/ignore/reasonix-ignore.ts
18609
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18610
+ /**
18611
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18612
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18613
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18614
+ * gateway already implements applies verbatim; only the surrounding file
18615
+ * format differs. Sibling keys such as `mode` pass through untouched.
18616
+ */
18617
+ const asClaudeStyleSettings = (document) => {
18618
+ const table = permissionsTableOf(document);
18619
+ return {
18620
+ ...document,
18621
+ permissions: {
18622
+ ...table,
18623
+ allow: toReasonixStringArray(table.allow),
18624
+ ask: toReasonixStringArray(table.ask),
18625
+ deny: toReasonixStringArray(table.deny)
18626
+ }
18627
+ };
18628
+ };
18629
+ /**
18630
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18631
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18632
+ * one.
18633
+ */
18634
+ const withoutEmptyPermissions = (settings) => {
18635
+ const document = { ...settings };
18636
+ const permissions = document.permissions;
18637
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18638
+ return document;
18639
+ };
18640
+ /**
18641
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18642
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18643
+ * `~/.reasonix/config.toml` (global).
18644
+ *
18645
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18646
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18647
+ * while `forbid_read` is documented as absolute paths with no glob support.
18648
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18649
+ */
18650
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18651
+ constructor(params) {
18652
+ super(params);
18653
+ const document = parseSharedConfig({
18654
+ format: "toml",
18655
+ fileContent: this.fileContent
18656
+ });
18657
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18658
+ }
18659
+ static getSettablePaths({ global = false } = {}) {
18660
+ return {
18661
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18662
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18663
+ };
18664
+ }
18665
+ /**
18666
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18667
+ * permissions feature and user-authored tables, so rulesync must never
18668
+ * delete it.
18669
+ */
18670
+ isDeletable() {
18671
+ return false;
18672
+ }
18673
+ toRulesyncIgnore() {
18674
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18675
+ return new RulesyncIgnore({
18676
+ outputRoot: this.outputRoot,
18677
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18678
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18679
+ fileContent: rulesyncPatterns.join("\n")
18680
+ });
18681
+ }
18682
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18683
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18684
+ const paths = this.getSettablePaths({ global });
18685
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18686
+ const existingDocument = parseSharedConfig({
18687
+ format: "toml",
18688
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18689
+ filePath
18690
+ });
18691
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18692
+ settings: asClaudeStyleSettings(existingDocument),
18693
+ readDenies
18694
+ }));
18695
+ return new ReasonixIgnore({
18696
+ outputRoot,
18697
+ relativeDirPath: paths.relativeDirPath,
18698
+ relativeFilePath: paths.relativeFilePath,
18699
+ fileContent: stringifySharedConfig({
18700
+ format: "toml",
18701
+ document
18702
+ }),
18703
+ validate: true,
18704
+ global
18705
+ });
18706
+ }
18707
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18708
+ const paths = this.getSettablePaths({ global });
18709
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18710
+ return new ReasonixIgnore({
18711
+ outputRoot,
18712
+ relativeDirPath: paths.relativeDirPath,
18713
+ relativeFilePath: paths.relativeFilePath,
18714
+ fileContent,
18715
+ validate,
18716
+ global
18717
+ });
18718
+ }
18719
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18720
+ return new ReasonixIgnore({
18721
+ outputRoot,
18722
+ relativeDirPath,
18723
+ relativeFilePath,
18724
+ fileContent: "",
18725
+ validate: false,
18726
+ global
18727
+ });
18728
+ }
18729
+ };
18730
+ //#endregion
18023
18731
  //#region src/features/ignore/roo-ignore.ts
18024
18732
  /**
18025
18733
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -18177,6 +18885,14 @@ const ZED_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Roaming", "Zed");
18177
18885
  function getZedGlobalDir() {
18178
18886
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
18179
18887
  }
18888
+ /**
18889
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18890
+ * spelling per platform, but the shared-write derivation (and the gateway
18891
+ * ownership table it is checked against) must know both on every platform.
18892
+ */
18893
+ function getZedOtherPlatformGlobalDir() {
18894
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18895
+ }
18180
18896
  const ZED_SETTINGS_FILE_NAME = "settings.json";
18181
18897
  const ZED_RULE_FILE_NAME = ".rules";
18182
18898
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -18189,12 +18905,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18189
18905
  const jsonValue = JSON.parse(this.fileContent);
18190
18906
  this.patterns = jsonValue.private_files ?? [];
18191
18907
  }
18192
- static getSettablePaths() {
18908
+ static getSettablePaths({ global = false } = {}) {
18193
18909
  return {
18194
- relativeDirPath: ZED_DIR,
18910
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
18195
18911
  relativeFilePath: ZED_SETTINGS_FILE_NAME
18196
18912
  };
18197
18913
  }
18914
+ /** @see getZedOtherPlatformGlobalDir */
18915
+ static getExtraSharedWritePaths({ global = false } = {}) {
18916
+ if (!global) return [];
18917
+ return [{
18918
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18919
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18920
+ }];
18921
+ }
18198
18922
  /**
18199
18923
  * ZedIgnore uses settings.json which is a user-managed config file.
18200
18924
  * It should not be deleted by rulesync.
@@ -18205,48 +18929,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18205
18929
  toRulesyncIgnore() {
18206
18930
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
18207
18931
  return new RulesyncIgnore({
18208
- outputRoot: this.outputRoot,
18932
+ outputRoot: ".",
18209
18933
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18210
18934
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18211
18935
  fileContent
18212
18936
  });
18213
18937
  }
18214
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18938
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18215
18939
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
18216
- const filePath = (0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
18940
+ const paths = this.getSettablePaths({ global });
18941
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18217
18942
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
18218
- const mergedPatterns = (0, es_toolkit.uniq)([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18943
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
18219
18944
  return new ZedIgnore({
18220
18945
  outputRoot,
18221
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18222
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18946
+ relativeDirPath: paths.relativeDirPath,
18947
+ relativeFilePath: paths.relativeFilePath,
18223
18948
  fileContent: applySharedConfigPatch({
18224
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18949
+ fileKey: sharedConfigFileKey(paths),
18225
18950
  feature: "ignore",
18226
18951
  existingContent: existingFileContent,
18227
- patch: { private_files: mergedPatterns },
18952
+ patch: { private_files: managedPatterns },
18228
18953
  filePath
18229
18954
  }),
18230
- validate: true
18955
+ validate: true,
18956
+ global
18231
18957
  });
18232
18958
  }
18233
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
18234
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
18959
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18960
+ const paths = this.getSettablePaths({ global });
18961
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
18235
18962
  return new ZedIgnore({
18236
18963
  outputRoot,
18237
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18238
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18964
+ relativeDirPath: paths.relativeDirPath,
18965
+ relativeFilePath: paths.relativeFilePath,
18239
18966
  fileContent,
18240
- validate
18967
+ validate,
18968
+ global
18241
18969
  });
18242
18970
  }
18243
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18971
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18244
18972
  return new ZedIgnore({
18245
18973
  outputRoot,
18246
18974
  relativeDirPath,
18247
18975
  relativeFilePath,
18248
18976
  fileContent: "{}",
18249
- validate: false
18977
+ validate: false,
18978
+ global
18250
18979
  });
18251
18980
  }
18252
18981
  };
@@ -18268,6 +18997,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
18268
18997
  ["kiro-cli", { class: KiroIgnore }],
18269
18998
  ["kiro-ide", { class: KiroIgnore }],
18270
18999
  ["qwencode", { class: QwencodeIgnore }],
19000
+ ["reasonix", { class: ReasonixIgnore }],
18271
19001
  ["roo", { class: RooIgnore }],
18272
19002
  ["devin", { class: DevinIgnore }],
18273
19003
  ["vibe", { class: VibeIgnore }],
@@ -18278,7 +19008,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
18278
19008
  const ignoreProcessorGlobalToolTargets = [
18279
19009
  "kiro",
18280
19010
  "kiro-cli",
18281
- "kiro-ide"
19011
+ "kiro-ide",
19012
+ "reasonix",
19013
+ "zed"
18282
19014
  ];
18283
19015
  const defaultGetFactory$4 = (target) => {
18284
19016
  const factory = toolIgnoreFactories.get(target);
@@ -20874,7 +21606,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20874
21606
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20875
21607
  this.config = merged;
20876
21608
  super.setFileContent(applySharedConfigPatch({
20877
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21609
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20878
21610
  feature: "mcp",
20879
21611
  existingContent: fileContent,
20880
21612
  patch: { mcp_servers: merged.mcp_servers }
@@ -20892,6 +21624,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20892
21624
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20893
21625
  };
20894
21626
  }
21627
+ /**
21628
+ * `config.yaml` under every spelling the global profile root can take.
21629
+ * @see getHermesagentSharedConfigWritePaths
21630
+ */
21631
+ static getExtraSharedWritePaths() {
21632
+ return getHermesagentSharedConfigWritePaths();
21633
+ }
20895
21634
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20896
21635
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20897
21636
  const paths = this.getSettablePaths({ global });
@@ -20918,7 +21657,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20918
21657
  relativeDirPath: paths.relativeDirPath,
20919
21658
  relativeFilePath: paths.relativeFilePath,
20920
21659
  fileContent: applySharedConfigPatch({
20921
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21660
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20922
21661
  feature: "mcp",
20923
21662
  existingContent: fileContent,
20924
21663
  patch: { mcp_servers: merged.mcp_servers }
@@ -21379,9 +22118,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
21379
22118
  * Merge a list of project rule file globs into the `instructions` array of the
21380
22119
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
21381
22120
  * (notably `mcp`/`tools` written by the MCP feature). In Kilo v7, files under
21382
- * `.kilo/rules/` are NOT auto-loaded; they are only picked up when listed in
21383
- * the `instructions` key. The resulting `instructions` list is deduped and
21384
- * sorted for a stable output.
22121
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22122
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22123
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22124
+ * config load — which is why `KiloRule` registers instructions in project
22125
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22126
+ * stable output.
21385
22127
  *
21386
22128
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
21387
22129
  */
@@ -21594,7 +22336,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21594
22336
  const existingContent = existing.content;
21595
22337
  const existingSection = existing.mcp;
21596
22338
  const fileContent = applySharedConfigPatch({
21597
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22339
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21598
22340
  feature: "mcp",
21599
22341
  existingContent,
21600
22342
  patch: { mcp: {
@@ -21681,11 +22423,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21681
22423
  * derivation sees this feature as one of that file's writers — it is not a
21682
22424
  * settable path, since the servers themselves live in `mcp.json`.
21683
22425
  */
21684
- static getExtraSharedWritePaths({ global = false } = {}) {
21685
- return global ? [{
21686
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21687
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21688
- }] : [];
22426
+ static getExtraSharedWritePaths() {
22427
+ return getKimiCodeSharedConfigWritePaths();
21689
22428
  }
21690
22429
  /**
21691
22430
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -23321,16 +24060,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
23321
24060
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23322
24061
  };
23323
24062
  }
23324
- /**
23325
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
23326
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
23327
- * derivation (and the gateway ownership table it is checked against) must
23328
- * know both spellings on every platform.
23329
- */
24063
+ /** @see getZedOtherPlatformGlobalDir */
23330
24064
  static getExtraSharedWritePaths({ global = false } = {}) {
23331
24065
  if (!global) return [];
23332
24066
  return [{
23333
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24067
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
23334
24068
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23335
24069
  }];
23336
24070
  }
@@ -27760,6 +28494,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27760
28494
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27761
28495
  };
27762
28496
  }
28497
+ /**
28498
+ * `config.yaml` under every spelling the global profile root can take.
28499
+ * @see getHermesagentSharedConfigWritePaths
28500
+ */
28501
+ static getExtraSharedWritePaths() {
28502
+ return getHermesagentSharedConfigWritePaths();
28503
+ }
27763
28504
  constructor(params) {
27764
28505
  super({
27765
28506
  ...params,
@@ -27797,7 +28538,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27797
28538
  }
27798
28539
  setFileContent(fileContent) {
27799
28540
  this.fileContent = applySharedConfigPatch({
27800
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28541
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27801
28542
  feature: "permissions",
27802
28543
  existingContent: fileContent,
27803
28544
  patch: parseSharedConfig({
@@ -28210,7 +28951,10 @@ const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
28210
28951
  "ask",
28211
28952
  "deny"
28212
28953
  ]))]);
28213
- const KiloPermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)) });
28954
+ const KiloPermissionsConfigSchema = zod_mini.z.looseObject({
28955
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)),
28956
+ sandbox: zod_mini.z.optional(zod_mini.z.unknown())
28957
+ });
28214
28958
  /**
28215
28959
  * Kilo permission keys that share a name with a canonical rulesync category and
28216
28960
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -28267,6 +29011,25 @@ function collectKiloDenyPatterns(value) {
28267
29011
  }
28268
29012
  return [];
28269
29013
  }
29014
+ function asKiloRecord(value) {
29015
+ return isPlainObject$1(value) ? { ...value } : {};
29016
+ }
29017
+ /**
29018
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
29019
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
29020
+ * project config merely tighten — so writing the wider keys into a project file
29021
+ * would produce config Kilo ignores.
29022
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
29023
+ */
29024
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29025
+ function narrowSandboxToProjectScope({ authored, logger }) {
29026
+ const emitted = {};
29027
+ const dropped = [];
29028
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29029
+ else dropped.push(key);
29030
+ 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.`);
29031
+ return emitted;
29032
+ }
28270
29033
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
28271
29034
  json;
28272
29035
  constructor(params) {
@@ -28346,8 +29109,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28346
29109
  const basePaths = KiloPermissions.getSettablePaths({ global });
28347
29110
  const filePath = (0, node_path.join)(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
28348
29111
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
28349
- const parsedPermission = parsed.permission;
28350
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29112
+ const existingPermission = asKiloRecord(parsed.permission);
28351
29113
  const rulesyncJson = rulesyncPermissions.getJson();
28352
29114
  const kiloOverride = rulesyncJson.kilo;
28353
29115
  const incomingPermission = {
@@ -28373,6 +29135,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28373
29135
  ...parsed,
28374
29136
  permission: mergedPermission
28375
29137
  };
29138
+ if (kiloOverride?.sandbox !== void 0) {
29139
+ const authored = asKiloRecord(kiloOverride.sandbox);
29140
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29141
+ authored,
29142
+ logger
29143
+ });
29144
+ const merged = {
29145
+ ...asKiloRecord(parsed.sandbox),
29146
+ ...emitted
29147
+ };
29148
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29149
+ }
28376
29150
  return new KiloPermissions({
28377
29151
  outputRoot,
28378
29152
  relativeDirPath: basePaths.relativeDirPath,
@@ -28387,9 +29161,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28387
29161
  const overrideOnly = {};
28388
29162
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
28389
29163
  else overrideOnly[key] = value;
28390
- const json = Object.keys(overrideOnly).length > 0 ? {
29164
+ const sandbox = this.json.sandbox;
29165
+ const override = {
29166
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29167
+ ...isPlainObject$1(sandbox) && { sandbox }
29168
+ };
29169
+ const json = Object.keys(override).length > 0 ? {
28391
29170
  permission: shared,
28392
- kilo: { permission: overrideOnly }
29171
+ kilo: override
28393
29172
  } : { permission: shared };
28394
29173
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
28395
29174
  }
@@ -28630,6 +29409,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28630
29409
  isDeletable() {
28631
29410
  return false;
28632
29411
  }
29412
+ /**
29413
+ * `config.toml` under both spellings its directory can take.
29414
+ * @see getKimiCodeSharedConfigWritePaths
29415
+ */
29416
+ static getExtraSharedWritePaths() {
29417
+ return getKimiCodeSharedConfigWritePaths();
29418
+ }
28633
29419
  shouldMergeExistingFileContent() {
28634
29420
  return true;
28635
29421
  }
@@ -28644,7 +29430,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28644
29430
  patch
28645
29431
  });
28646
29432
  this.fileContent = applySharedConfigPatch({
28647
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29433
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
28648
29434
  feature: "permissions",
28649
29435
  existingContent: fileContent,
28650
29436
  patch: {
@@ -29568,14 +30354,6 @@ function parseReasonixConfig(fileContent) {
29568
30354
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
29569
30355
  return { ...parsed };
29570
30356
  }
29571
- function toStringArray$1(value) {
29572
- if (!Array.isArray(value)) return [];
29573
- return value.filter((entry) => typeof entry === "string");
29574
- }
29575
- function toPermissionsTable(value) {
29576
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
29577
- return { ...value };
29578
- }
29579
30357
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
29580
30358
  /**
29581
30359
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -29632,12 +30410,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29632
30410
  const config = rulesyncPermissions.getJson();
29633
30411
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
29634
30412
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
29635
- const existingPermissions = toPermissionsTable(parsed.permissions);
29636
- const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29637
- const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29638
- const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30413
+ const existingPermissions = toReasonixTable(parsed.permissions);
30414
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30415
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30416
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29639
30417
  if (logger && managedToolNames.has("Read")) {
29640
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30418
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
29641
30419
  const { toolName } = parseReasonixPermissionEntry(entry);
29642
30420
  return toolName === "Read";
29643
30421
  });
@@ -29684,11 +30462,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29684
30462
  });
29685
30463
  }
29686
30464
  toRulesyncPermissions() {
29687
- const permissions = toPermissionsTable(this.toml.permissions);
30465
+ const permissions = toReasonixTable(this.toml.permissions);
29688
30466
  const config = convertReasonixToRulesyncPermissions({
29689
- allow: toStringArray$1(permissions.allow),
29690
- ask: toStringArray$1(permissions.ask),
29691
- deny: toStringArray$1(permissions.deny)
30467
+ allow: toReasonixStringArray(permissions.allow),
30468
+ ask: toReasonixStringArray(permissions.ask),
30469
+ deny: toReasonixStringArray(permissions.deny)
29692
30470
  });
29693
30471
  const sandbox = asReasonixRecord(this.toml.sandbox);
29694
30472
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29780,19 +30558,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29780
30558
  "open_files",
29781
30559
  "expand_code_chunks",
29782
30560
  "expand_folder",
29783
- "grep"
30561
+ "grep",
30562
+ "getJiraIssue",
30563
+ "getConfluencePage"
29784
30564
  ],
29785
30565
  edit: [
29786
30566
  "find_and_replace_code",
29787
30567
  "create_file",
29788
30568
  "delete_file",
29789
- "move_file"
30569
+ "move_file",
30570
+ "createTechnicalPlan",
30571
+ "createJiraIssue",
30572
+ "updateJiraIssue",
30573
+ "createConfluencePage",
30574
+ "updateConfluencePage"
29790
30575
  ],
29791
30576
  write: [
29792
30577
  "create_file",
29793
30578
  "delete_file",
29794
30579
  "move_file",
29795
- "find_and_replace_code"
30580
+ "find_and_replace_code",
30581
+ "createTechnicalPlan",
30582
+ "createJiraIssue",
30583
+ "updateJiraIssue",
30584
+ "createConfluencePage",
30585
+ "updateConfluencePage"
29796
30586
  ]
29797
30587
  };
29798
30588
  const TOOL_KEY_TO_CATEGORY = {
@@ -29800,13 +30590,24 @@ const TOOL_KEY_TO_CATEGORY = {
29800
30590
  expand_code_chunks: "read",
29801
30591
  expand_folder: "read",
29802
30592
  grep: "read",
30593
+ getJiraIssue: "read",
30594
+ getConfluencePage: "read",
29803
30595
  find_and_replace_code: "edit",
29804
30596
  create_file: "edit",
29805
30597
  delete_file: "edit",
29806
- move_file: "edit"
30598
+ move_file: "edit",
30599
+ createTechnicalPlan: "edit",
30600
+ createJiraIssue: "edit",
30601
+ updateJiraIssue: "edit",
30602
+ createConfluencePage: "edit",
30603
+ updateConfluencePage: "edit"
29807
30604
  };
29808
30605
  const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
29809
- const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
30606
+ const OWNED_TOOL_PERMISSION_KEYS = [
30607
+ "bash",
30608
+ "allowedExternalPaths",
30609
+ "default"
30610
+ ];
29810
30611
  /**
29811
30612
  * Permissions adapter for Rovo Dev CLI.
29812
30613
  *
@@ -29820,9 +30621,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29820
30621
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29821
30622
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29822
30623
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30624
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30625
+ * the level Rovo Dev falls back to for any tool with no more specific
30626
+ * setting (Rovo Dev's own default is `ask`).
29823
30627
  * - `read` -> the inspection tools (`open_files`, `expand_code_chunks`,
29824
- * `expand_folder`, `grep`); `edit`/`write` -> the mutation tools
29825
- * (`find_and_replace_code`, `create_file`, `delete_file`, `move_file`).
30628
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30629
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30630
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30631
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30632
+ * `updateConfluencePage`) — so these two categories reach Jira and
30633
+ * Confluence, not just the working tree.
29826
30634
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29827
30635
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29828
30636
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -30002,6 +30810,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
30002
30810
  delete toolPermissions.allowedExternalPaths;
30003
30811
  strippedKeys.push("allowedExternalPaths");
30004
30812
  }
30813
+ if (toolPermissions.default === "allow") {
30814
+ delete toolPermissions.default;
30815
+ strippedKeys.push("default");
30816
+ }
30005
30817
  const bash = toolPermissions.bash;
30006
30818
  if (isRecord(bash)) {
30007
30819
  const stripped = { ...bash };
@@ -30026,10 +30838,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
30026
30838
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30027
30839
  const toolPermissions = {};
30028
30840
  const allowedExternalPaths = [];
30029
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30030
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30031
- 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.`);
30841
+ warnOnEditWriteConflict({
30842
+ config,
30843
+ logger
30844
+ });
30032
30845
  for (const [category, rules] of Object.entries(config.permission)) {
30846
+ if (category === CATCH_ALL_PATTERN$1) {
30847
+ const toolWideDefault = convertAllToolsRules({
30848
+ rules,
30849
+ logger
30850
+ });
30851
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30852
+ continue;
30853
+ }
30033
30854
  if (category === "bash") {
30034
30855
  const bash = convertBashRules(rules);
30035
30856
  if (bash) toolPermissions.bash = bash;
@@ -30056,6 +30877,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30056
30877
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
30057
30878
  return toolPermissions;
30058
30879
  }
30880
+ /**
30881
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30882
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30883
+ * is happening; the conversion keeps the stricter of the two — the same
30884
+ * fail-closed rule the import direction uses when those tools disagree, so the
30885
+ * resolution never grants more than the author asked for.
30886
+ */
30887
+ function warnOnEditWriteConflict({ config, logger }) {
30888
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30889
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30890
+ 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.`);
30891
+ }
30892
+ /**
30893
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30894
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30895
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30896
+ * under `*` have no counterpart (the default is a single level), so they are
30897
+ * reported and skipped like any other rule Rovo Dev cannot express.
30898
+ */
30899
+ function convertAllToolsRules({ rules, logger }) {
30900
+ let toolWideDefault;
30901
+ for (const [pattern, action] of Object.entries(rules)) {
30902
+ if (pattern === CATCH_ALL_PATTERN$1) {
30903
+ toolWideDefault = action;
30904
+ continue;
30905
+ }
30906
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30907
+ }
30908
+ return toolWideDefault;
30909
+ }
30059
30910
  function convertBashRules(rules) {
30060
30911
  const bash = {};
30061
30912
  const commands = [];
@@ -30077,6 +30928,7 @@ function convertBashRules(rules) {
30077
30928
  */
30078
30929
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30079
30930
  const permission = {};
30931
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
30080
30932
  const bash = toolPermissions.bash;
30081
30933
  if (isRecord(bash)) {
30082
30934
  const bashRules = {};
@@ -30087,12 +30939,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30087
30939
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
30088
30940
  }
30089
30941
  const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
30090
- for (const [toolKey, category] of Object.entries(TOOL_KEY_TO_CATEGORY)) {
30091
- const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30092
- if (!isPermissionAction(value)) continue;
30942
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30943
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30944
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30945
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30946
+ return isPermissionAction(value) ? value : void 0;
30947
+ });
30948
+ if (levels.every((level) => level === void 0)) continue;
30093
30949
  permission[category] ??= {};
30094
- const current = permission[category][CATCH_ALL_PATTERN$1];
30095
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30950
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
30096
30951
  }
30097
30952
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
30098
30953
  permission.read ??= {};
@@ -30924,16 +31779,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30924
31779
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30925
31780
  };
30926
31781
  }
30927
- /**
30928
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
30929
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
30930
- * derivation (and the gateway ownership table it is checked against) must
30931
- * know both spellings on every platform.
30932
- */
31782
+ /** @see getZedOtherPlatformGlobalDir */
30933
31783
  static getExtraSharedWritePaths({ global = false } = {}) {
30934
31784
  if (!global) return [];
30935
31785
  return [{
30936
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31786
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30937
31787
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30938
31788
  }];
30939
31789
  }
@@ -39505,7 +40355,7 @@ def register(ctx):
39505
40355
  _register_subagent(ctx, subagent)
39506
40356
  `;
39507
40357
  }
39508
- function getEnabledPluginConfigContent(currentContent) {
40358
+ function getEnabledPluginConfigContent({ currentContent, global }) {
39509
40359
  const config = parseSharedConfig({
39510
40360
  format: "yaml",
39511
40361
  fileContent: currentContent
@@ -39513,7 +40363,7 @@ function getEnabledPluginConfigContent(currentContent) {
39513
40363
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
39514
40364
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
39515
40365
  return applySharedConfigPatch({
39516
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40366
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
39517
40367
  feature: "subagents",
39518
40368
  existingContent: currentContent,
39519
40369
  patch: { plugins: {
@@ -39564,6 +40414,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39564
40414
  return !targets || targets.includes("*") || targets.includes("hermesagent");
39565
40415
  }
39566
40416
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40417
+ const pluginDirPath = getHermesagentRelativeDirPath({
40418
+ global,
40419
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40420
+ });
39567
40421
  return [
39568
40422
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
39569
40423
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -39572,20 +40426,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39572
40426
  global
39573
40427
  })),
39574
40428
  new HermesagentSubagent({
39575
- relativeDirPath: getHermesagentRelativeDirPath({
39576
- global,
39577
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39578
- }),
40429
+ relativeDirPath: pluginDirPath,
39579
40430
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
39580
40431
  fileContent: "",
39581
40432
  outputRoot,
39582
40433
  global
39583
40434
  }),
39584
40435
  new HermesagentSubagent({
39585
- relativeDirPath: getHermesagentRelativeDirPath({
39586
- global,
39587
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39588
- }),
40436
+ relativeDirPath: pluginDirPath,
39589
40437
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
39590
40438
  fileContent: "",
39591
40439
  outputRoot,
@@ -39625,14 +40473,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39625
40473
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
39626
40474
  * so the write must be declared for the shared-file order derivation.
39627
40475
  */
39628
- static getExtraSharedWritePaths({ global = false } = {}) {
39629
- return global ? [{
39630
- relativeDirPath: getHermesagentRelativeDirPath({
39631
- global,
39632
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
39633
- }),
39634
- relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
39635
- }] : [];
40476
+ /**
40477
+ * `config.yaml` under every spelling the global profile root can take.
40478
+ * @see getHermesagentSharedConfigWritePaths
40479
+ */
40480
+ static getExtraSharedWritePaths() {
40481
+ return getHermesagentSharedConfigWritePaths();
39636
40482
  }
39637
40483
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
39638
40484
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -39665,7 +40511,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39665
40511
  }
39666
40512
  setFileContent(newFileContent) {
39667
40513
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) {
39668
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40514
+ super.setFileContent(getEnabledPluginConfigContent({
40515
+ currentContent: newFileContent,
40516
+ global: this.global
40517
+ }));
39669
40518
  return;
39670
40519
  }
39671
40520
  super.setFileContent(newFileContent);
@@ -39673,7 +40522,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39673
40522
  getFileContent() {
39674
40523
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
39675
40524
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH)) return getPluginInitContent();
39676
- if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent(super.getFileContent());
40525
+ if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40526
+ currentContent: super.getFileContent(),
40527
+ global: this.global
40528
+ });
39677
40529
  return super.getFileContent();
39678
40530
  }
39679
40531
  };
@@ -44752,10 +45604,13 @@ var JunieRule = class JunieRule extends ToolRule {
44752
45604
  //#region src/features/rules/kilo-rule.ts
44753
45605
  var KiloRule = class KiloRule extends ToolRule {
44754
45606
  static getSettablePaths({ global, excludeToolDir } = {}) {
44755
- if (global) return { root: {
44756
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
44757
- relativeFilePath: KILO_RULE_FILE_NAME
44758
- } };
45607
+ if (global) return {
45608
+ root: {
45609
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45610
+ relativeFilePath: KILO_RULE_FILE_NAME
45611
+ },
45612
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45613
+ };
44759
45614
  return {
44760
45615
  root: {
44761
45616
  relativeDirPath: ".",
@@ -46340,6 +47195,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46340
47195
  extension: "md",
46341
47196
  supportsGlobal: false,
46342
47197
  ruleDiscoveryMode: "toon",
47198
+ collisionPolicy: "compose",
46343
47199
  additionalConventions: {
46344
47200
  commands: { commandClass: AgentsmdCommand },
46345
47201
  subagents: { subagentClass: AgentsmdSubagent },
@@ -46360,7 +47216,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46360
47216
  meta: {
46361
47217
  extension: "md",
46362
47218
  supportsGlobal: true,
46363
- ruleDiscoveryMode: "toon"
47219
+ ruleDiscoveryMode: "toon",
47220
+ collisionPolicy: "compose"
46364
47221
  }
46365
47222
  }],
46366
47223
  ["antigravity-cli", {
@@ -46437,7 +47294,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46437
47294
  extension: "md",
46438
47295
  supportsGlobal: true,
46439
47296
  ruleDiscoveryMode: "auto",
46440
- foldsNonRootIntoRoot: true
47297
+ collisionPolicy: "fold"
46441
47298
  }
46442
47299
  }],
46443
47300
  ["copilot", {
@@ -46470,7 +47327,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46470
47327
  extension: "md",
46471
47328
  supportsGlobal: true,
46472
47329
  ruleDiscoveryMode: "auto",
46473
- foldsNonRootIntoRoot: true
47330
+ collisionPolicy: "fold"
46474
47331
  }
46475
47332
  }],
46476
47333
  ["factorydroid", {
@@ -46478,7 +47335,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46478
47335
  meta: {
46479
47336
  extension: "md",
46480
47337
  supportsGlobal: true,
46481
- ruleDiscoveryMode: "toon"
47338
+ ruleDiscoveryMode: "toon",
47339
+ collisionPolicy: "compose"
46482
47340
  }
46483
47341
  }],
46484
47342
  ["goose", {
@@ -46487,7 +47345,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46487
47345
  extension: "md",
46488
47346
  supportsGlobal: true,
46489
47347
  ruleDiscoveryMode: "auto",
46490
- foldsNonRootIntoRoot: true
47348
+ collisionPolicy: "fold"
46491
47349
  }
46492
47350
  }],
46493
47351
  ["hermesagent", {
@@ -46496,7 +47354,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46496
47354
  extension: "md",
46497
47355
  supportsGlobal: false,
46498
47356
  ruleDiscoveryMode: "auto",
46499
- foldsNonRootIntoRoot: true
47357
+ collisionPolicy: "fold"
46500
47358
  }
46501
47359
  }],
46502
47360
  ["grokcli", {
@@ -46513,7 +47371,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46513
47371
  extension: "md",
46514
47372
  supportsGlobal: true,
46515
47373
  ruleDiscoveryMode: "auto",
46516
- foldsNonRootIntoRoot: true
47374
+ collisionPolicy: "fold"
46517
47375
  }
46518
47376
  }],
46519
47377
  ["kilo", {
@@ -46522,7 +47380,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46522
47380
  extension: "md",
46523
47381
  supportsGlobal: true,
46524
47382
  ruleDiscoveryMode: "auto",
46525
- mcpInstructionsRegistrar: KiloMcp
47383
+ mcpInstructionsRegistrar: KiloMcp,
47384
+ collisionPolicy: "compose"
46526
47385
  }
46527
47386
  }],
46528
47387
  ["kimi-code", {
@@ -46531,7 +47390,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46531
47390
  extension: "md",
46532
47391
  supportsGlobal: true,
46533
47392
  ruleDiscoveryMode: "auto",
46534
- foldsNonRootIntoRoot: true
47393
+ collisionPolicy: "fold"
46535
47394
  }
46536
47395
  }],
46537
47396
  ["kiro", {
@@ -46564,7 +47423,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46564
47423
  extension: "md",
46565
47424
  supportsGlobal: true,
46566
47425
  ruleDiscoveryMode: "toon",
46567
- mcpInstructionsRegistrar: OpencodeMcp
47426
+ mcpInstructionsRegistrar: OpencodeMcp,
47427
+ collisionPolicy: "compose"
46568
47428
  }
46569
47429
  }],
46570
47430
  ["pi", {
@@ -46573,7 +47433,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46573
47433
  extension: "md",
46574
47434
  supportsGlobal: true,
46575
47435
  ruleDiscoveryMode: "auto",
46576
- foldsNonRootIntoRoot: true
47436
+ collisionPolicy: "fold"
46577
47437
  }
46578
47438
  }],
46579
47439
  ["qwencode", {
@@ -46591,7 +47451,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46591
47451
  extension: "md",
46592
47452
  supportsGlobal: true,
46593
47453
  ruleDiscoveryMode: "auto",
46594
- foldsNonRootIntoRoot: true
47454
+ collisionPolicy: "fold"
46595
47455
  }
46596
47456
  }],
46597
47457
  ["replit", {
@@ -46648,7 +47508,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46648
47508
  extension: "md",
46649
47509
  supportsGlobal: false,
46650
47510
  ruleDiscoveryMode: "toon",
46651
- foldsNonRootIntoRoot: true
47511
+ collisionPolicy: "fold"
46652
47512
  }
46653
47513
  }],
46654
47514
  ["devin", {
@@ -46717,16 +47577,23 @@ var RulesProcessor = class extends FeatureProcessor {
46717
47577
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
46718
47578
  const factory = this.getFactory(this.toolTarget);
46719
47579
  const { meta } = factory;
46720
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47580
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
46721
47581
  if (!factory.class.isTargetedByRulesyncRule(rulesyncRule)) return null;
46722
- return factory.class.fromRulesyncRule({
46723
- outputRoot: this.outputRoot,
46724
- rulesyncRule,
46725
- validate: true,
46726
- global: this.global
46727
- });
47582
+ return {
47583
+ toolRule: factory.class.fromRulesyncRule({
47584
+ outputRoot: this.outputRoot,
47585
+ rulesyncRule,
47586
+ validate: true,
47587
+ global: this.global
47588
+ }),
47589
+ rulesyncRule
47590
+ };
46728
47591
  }).filter((rule) => rule !== null);
46729
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47592
+ this.mergeRulesByOutputPath({
47593
+ convertedRules,
47594
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47595
+ });
47596
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
46730
47597
  this.applyLocalRootRules({
46731
47598
  toolRules,
46732
47599
  localRootRules,
@@ -46744,7 +47611,12 @@ var RulesProcessor = class extends FeatureProcessor {
46744
47611
  toolRules,
46745
47612
  factory
46746
47613
  });
46747
- return [...toolRules, ...extraFiles];
47614
+ const outputFiles = [...toolRules, ...extraFiles];
47615
+ this.warnForOutputPathCollisions({
47616
+ outputFiles,
47617
+ convertedRules
47618
+ });
47619
+ return outputFiles;
46748
47620
  }
46749
47621
  /**
46750
47622
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -46831,39 +47703,80 @@ var RulesProcessor = class extends FeatureProcessor {
46831
47703
  });
46832
47704
  }
46833
47705
  /**
46834
- * Fold every non-root rule body into the single root rule file.
47706
+ * Reconcile rules that resolve to the same output path.
46835
47707
  *
46836
- * Used for tools whose rules engine reads only one root `AGENTS.md` and neither
46837
- * scans a `memories/` directory nor follows references (deepagents' dcode reads
46838
- * `.deepagents/AGENTS.md`; Warp reads root/subdir `AGENTS.md` but never
46839
- * `.warp/memories/`). Those rule classes emit both root and non-root rules to
46840
- * the same root path, so all bodies must be merged into one instance to avoid
46841
- * colliding on that path (last-writer-wins would silently drop content).
47708
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47709
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47710
+ * neither scans a modular rules directory nor follows references. For example,
47711
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47712
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47713
+ * body into one instance because last-writer-wins would silently drop content.
47714
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
46842
47715
  *
46843
- * The root rule (if any) becomes the merge target and leads the merged content;
46844
- * otherwise the first rule is used so a rule set without a root overview still
46845
- * produces a single, complete file. Mutates `toolRules` in place.
47716
+ * A generated root rule becomes the merge target when present. A `fold` group
47717
+ * without one uses its first rule. A group only composes when every rendered
47718
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47719
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47720
+ * Root-involved collisions that cannot be composed safely fail; other
47721
+ * collisions remain separate and are reported by the final output-path check.
47722
+ * Mutates `convertedRules` in place.
46846
47723
  */
46847
- foldNonRootRulesIntoRootRule(toolRules) {
46848
- if (toolRules.length <= 1) return;
47724
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47725
+ if (convertedRules.length <= 1) return;
46849
47726
  const groups = /* @__PURE__ */ new Map();
46850
- for (const rule of toolRules) {
46851
- const path = (0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47727
+ for (const conversion of convertedRules) {
47728
+ const path = (0, node_path.join)(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
46852
47729
  const group = groups.get(path);
46853
- if (group) group.push(rule);
46854
- else groups.set(path, [rule]);
47730
+ if (group) group.push(conversion);
47731
+ else groups.set(path, [conversion]);
46855
47732
  }
46856
47733
  const survivors = /* @__PURE__ */ new Set();
46857
- for (const group of groups.values()) {
46858
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47734
+ for (const [path, group] of groups) {
47735
+ if (group.length === 1) {
47736
+ const conversion = group[0];
47737
+ if (conversion) {
47738
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47739
+ survivors.add(conversion);
47740
+ }
47741
+ continue;
47742
+ }
47743
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47744
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47745
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47746
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47747
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47748
+ 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))}`);
47749
+ if (!shouldCompose) {
47750
+ for (const conversion of group) survivors.add(conversion);
47751
+ continue;
47752
+ }
47753
+ const target = rootConversion ?? group[0];
46859
47754
  if (!target) continue;
46860
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map((rule) => rule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
46861
- target.setFileContent(mergedContent);
47755
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47756
+ target.toolRule.setFileContent(mergedContent);
46862
47757
  survivors.add(target);
46863
47758
  }
46864
- for (let i = toolRules.length - 1; i >= 0; i--) {
46865
- const rule = toolRules[i];
46866
- if (rule && !survivors.has(rule)) toolRules.splice(i, 1);
47759
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47760
+ const conversion = convertedRules[i];
47761
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47762
+ }
47763
+ }
47764
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47765
+ const seen = /* @__PURE__ */ new Map();
47766
+ const describeSource = (file) => {
47767
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47768
+ return source ? formatRulePaths([source]) : (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47769
+ };
47770
+ for (const file of outputFiles) {
47771
+ const path = (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47772
+ const key = path.toLowerCase();
47773
+ const previous = seen.get(key);
47774
+ if (previous) {
47775
+ const previousPath = (0, node_path.join)(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47776
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47777
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47778
+ }
47779
+ seen.set(key, file);
46867
47780
  }
46868
47781
  }
46869
47782
  /**
@@ -47023,14 +47936,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
47023
47936
  }));
47024
47937
  const factory = this.getFactory(this.toolTarget);
47025
47938
  const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47026
- if (targetedRootRules.length > 1) throw new Error(`Multiple root rulesync rules found for target '${this.toolTarget}': ${formatRulePaths(targetedRootRules)}`);
47027
47939
  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}.`);
47028
47940
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47029
47941
  if (targetedLocalRootRules.length > 1) throw new Error(`Multiple localRoot rules found for target '${this.toolTarget}': ${formatRulePaths(targetedLocalRootRules)}. Only one rule can have localRoot: true`);
47030
47942
  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)})`);
47031
47943
  if (this.global) {
47032
47944
  const globalPaths = factory.class.getSettablePaths({ global: true });
47033
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47945
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
47034
47946
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
47035
47947
  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)}`);
47036
47948
  if (targetedLocalRootRules.length > 0) this.logger.warn(`${targetedLocalRootRules.length} localRoot rules found, but localRoot is not supported in global mode, ignoring them: ${formatRulePaths(targetedLocalRootRules)}`);
@@ -47273,14 +48185,36 @@ async function assertPluginRootSafe(params) {
47273
48185
  }
47274
48186
  //#endregion
47275
48187
  //#region src/utils/tool-output-root.ts
48188
+ /** The environment variable each tool reads for its profile root. */
48189
+ const TOOL_HOME_ENV_VARS = {
48190
+ hermesagent: "HERMES_HOME",
48191
+ "kimi-code": "KIMI_CODE_HOME"
48192
+ };
48193
+ /**
48194
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48195
+ * output root in global scope.
48196
+ *
48197
+ * The override wins over `--output-roots`: it names where the tool itself reads
48198
+ * its profile, so writing anywhere else would produce files the tool ignores.
48199
+ *
48200
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48201
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48202
+ * instead of silently becoming the output root. The rejection is re-thrown
48203
+ * naming the variable, since the user never passed an `--output-roots` flag.
48204
+ */
47276
48205
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
47277
48206
  if (!global) return outputRoot;
47278
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48207
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
47279
48208
  outputRoot,
47280
48209
  global
47281
- });
47282
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
47283
- return outputRoot;
48210
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48211
+ if (resolved === outputRoot) return resolved;
48212
+ try {
48213
+ validateOutputRoot(resolved);
48214
+ } catch (error) {
48215
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48216
+ }
48217
+ return resolved;
47284
48218
  }
47285
48219
  //#endregion
47286
48220
  //#region src/lib/convert.ts
@@ -48681,7 +49615,11 @@ async function generateChecksCore(params) {
48681
49615
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
48682
49616
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
48683
49617
  const processor = new ChecksProcessor({
48684
- outputRoot,
49618
+ outputRoot: resolveToolOutputRoot({
49619
+ outputRoot,
49620
+ toolTarget,
49621
+ global: config.getGlobal()
49622
+ }),
48685
49623
  inputRoot: config.getInputRoot(),
48686
49624
  toolTarget,
48687
49625
  global: config.getGlobal(),