rulesync 16.1.0 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,6 +86,13 @@ const ALL_FEATURES = [
86
86
  ...ACTIVE_FEATURES_AFTER_IGNORE
87
87
  ];
88
88
  const ALL_FEATURES_WITH_WILDCARD = [...ALL_FEATURES, "*"];
89
+ /**
90
+ * Features that remain accepted for compatibility but are superseded by a
91
+ * newer feature. Maps each deprecated feature to its replacement; consumers
92
+ * (e.g. `rulesync doctor`) derive their deprecation warnings from this map so
93
+ * the set cannot drift from the schema above.
94
+ */
95
+ const DEPRECATED_FEATURE_REPLACEMENTS = { ignore: "permissions" };
89
96
  const ACTIVE_FEATURES = [...ACTIVE_FEATURES_BEFORE_IGNORE, ...ACTIVE_FEATURES_AFTER_IGNORE];
90
97
  const DeprecatedIgnoreFeatureSchema = zod_mini.z.literal("ignore").check((0, zod_mini.meta)({
91
98
  deprecated: true,
@@ -127,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
127
134
  };
128
135
  //#endregion
129
136
  //#region src/constants/rulesync-paths.ts
130
- const { join: join$277 } = node_path.posix;
137
+ const { join: join$281 } = node_path.posix;
131
138
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
132
139
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
133
140
  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");
141
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
+ join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
145
152
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
146
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$277(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
153
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
147
154
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
148
155
  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");
156
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
151
158
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
152
159
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
153
160
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -217,6 +224,7 @@ const ignoreProcessorToolTargetTuple = [
217
224
  "kiro-cli",
218
225
  "kiro-ide",
219
226
  "qwencode",
227
+ "reasonix",
220
228
  "roo",
221
229
  "devin",
222
230
  "vibe",
@@ -274,6 +282,7 @@ const commandsProcessorToolTargetTuple = [
274
282
  "cursor",
275
283
  "factorydroid",
276
284
  "goose",
285
+ "grokcli",
277
286
  "hermesagent",
278
287
  "junie",
279
288
  "kilo",
@@ -429,6 +438,7 @@ const checksProcessorToolTargetTuple = [
429
438
  "amp",
430
439
  "cursor",
431
440
  "hermesagent",
441
+ "rovodev",
432
442
  "takt"
433
443
  ];
434
444
  //#endregion
@@ -832,6 +842,7 @@ const ErrorCodes = {
832
842
  GITIGNORE_FAILED: "GITIGNORE_FAILED",
833
843
  INIT_FAILED: "INIT_FAILED",
834
844
  MCP_FAILED: "MCP_FAILED",
845
+ DOCTOR_FAILED: "DOCTOR_FAILED",
835
846
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
836
847
  };
837
848
  /**
@@ -840,10 +851,12 @@ const ErrorCodes = {
840
851
  var CLIError = class extends Error {
841
852
  code;
842
853
  exitCode;
843
- constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1) {
854
+ details;
855
+ constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1, details) {
844
856
  super(message);
845
857
  this.code = code;
846
858
  this.exitCode = exitCode;
859
+ this.details = details;
847
860
  this.name = "CLIError";
848
861
  }
849
862
  };
@@ -967,6 +980,7 @@ var JsonLogger = class extends BaseLogger {
967
980
  message: errorMessage
968
981
  };
969
982
  if (this._verbose && message instanceof Error && message.stack) errorInfo.stack = message.stack;
983
+ if (message instanceof CLIError && message.details !== void 0) errorInfo.details = message.details;
970
984
  this.outputJson(false, errorInfo);
971
985
  }
972
986
  debug(_message, ..._args) {}
@@ -1027,6 +1041,10 @@ function hasControlCharacters(value) {
1027
1041
  }
1028
1042
  //#endregion
1029
1043
  //#region src/config/config.ts
1044
+ /**
1045
+ * Key accepted alongside feature names in the per-feature object form of
1046
+ * `targets`. Exported so `rulesync doctor` treats the same key as valid.
1047
+ */
1030
1048
  const GITIGNORE_DESTINATION_KEY = "gitignoreDestination";
1031
1049
  /**
1032
1050
  * Schema for a single source entry in the sources array.
@@ -1082,7 +1100,20 @@ const ConfigFileSchema = zod_mini.z.object({
1082
1100
  });
1083
1101
  zod_mini.z.required(ConfigParamsSchema);
1084
1102
  /**
1085
- * Conflicting target pairs that cannot be used together
1103
+ * Normalizes the configuration file location to an absolute path.
1104
+ *
1105
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1106
+ * only covers direct programmatic construction, where the conventional
1107
+ * location next to the input root is the best guess.
1108
+ */
1109
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1110
+ if (configFilePath === void 0) return (0, node_path.join)(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1111
+ return (0, node_path.isAbsolute)(configFilePath) ? configFilePath : (0, node_path.resolve)(configFilePath);
1112
+ }
1113
+ /**
1114
+ * Conflicting target pairs that cannot be used together.
1115
+ * Exported so `rulesync doctor` can report the same conflicts as diagnostics
1116
+ * without duplicating the list.
1086
1117
  */
1087
1118
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
1088
1119
  /**
@@ -1152,8 +1183,9 @@ var Config = class Config {
1152
1183
  dryRun;
1153
1184
  check;
1154
1185
  inputRoot;
1186
+ configFilePath;
1155
1187
  sources;
1156
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1188
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1157
1189
  assertTargetsFeaturesExclusive({
1158
1190
  targets,
1159
1191
  features
@@ -1186,6 +1218,10 @@ var Config = class Config {
1186
1218
  this.dryRun = dryRun ?? false;
1187
1219
  this.check = check ?? false;
1188
1220
  this.inputRoot = inputRoot === void 0 ? process.cwd() : (0, node_path.isAbsolute)(inputRoot) ? inputRoot : (0, node_path.resolve)(inputRoot);
1221
+ this.configFilePath = normalizeConfigFilePath({
1222
+ configFilePath,
1223
+ inputRoot: this.inputRoot
1224
+ });
1189
1225
  this.sources = sources ?? [];
1190
1226
  }
1191
1227
  /**
@@ -1379,6 +1415,14 @@ var Config = class Config {
1379
1415
  getInputRoot() {
1380
1416
  return this.inputRoot;
1381
1417
  }
1418
+ /**
1419
+ * Returns the absolute path of the configuration file this config was
1420
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1421
+ * without one — so callers must treat this as a location, not a guarantee.
1422
+ */
1423
+ getConfigFilePath() {
1424
+ return this.configFilePath;
1425
+ }
1382
1426
  getSources() {
1383
1427
  return this.sources;
1384
1428
  }
@@ -1601,6 +1645,7 @@ var ConfigResolver = class {
1601
1645
  fallback: getDefaults().check
1602
1646
  }),
1603
1647
  inputRoot: resolvedInputRoot !== void 0 ? (0, node_path.resolve)(resolvedInputRoot) : cwd,
1648
+ configFilePath: validatedConfigPath,
1604
1649
  sources: configByFile.sources ?? getDefaults().sources,
1605
1650
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1606
1651
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -4011,10 +4056,30 @@ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
4011
4056
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
4012
4057
  * override; each value may be a bare action string or a pattern map.
4013
4058
  *
4059
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4060
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4061
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4062
+ * `writable_paths`. It has no canonical permission category, so it is authored
4063
+ * here and emitted only for Kilo.
4064
+ *
4065
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4066
+ * `writable_paths` are honored from the global config only, and a project
4067
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4068
+ * project-level network denial even clears the global destination exceptions.
4069
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4070
+ * written, and the rest are dropped with a warning rather than emitted into a
4071
+ * file Kilo would ignore.
4072
+ *
4014
4073
  * @example
4015
4074
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4075
+ * @example
4076
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4077
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
4016
4078
  */
4017
- const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
4079
+ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({
4080
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)),
4081
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
4082
+ });
4018
4083
  /**
4019
4084
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
4020
4085
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -5323,6 +5388,26 @@ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5323
5388
  const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5324
5389
  const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5325
5390
  //#endregion
5391
+ //#region src/constants/rovodev-paths.ts
5392
+ const ROVODEV_DIR = ".rovodev";
5393
+ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
5394
+ const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
5395
+ const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
5396
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5397
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5398
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5399
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5400
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
5401
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5402
+ const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
5403
+ /**
5404
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5405
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5406
+ * dot in the file name.
5407
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5408
+ */
5409
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5410
+ //#endregion
5326
5411
  //#region src/constants/takt-paths.ts
5327
5412
  const TAKT_DIR = ".takt";
5328
5413
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5784,12 +5869,19 @@ function slugifyCheckName(value) {
5784
5869
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5785
5870
  }
5786
5871
  //#endregion
5787
- //#region src/features/checks/cursor-check.ts
5872
+ //#region src/features/checks/aggregated-check-file.ts
5788
5873
  /**
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.
5874
+ * Shared machinery for the tools whose checks surface is **one aggregated
5875
+ * instruction file** rather than a file per check Cursor Bugbot's
5876
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5877
+ *
5878
+ * Both read the file as free prose, so the check identities have to be carried
5879
+ * in something invisible to the reader: an HTML-comment marker per section.
5880
+ * That marker convention, the escaping that keeps a check body from splitting
5881
+ * itself, and the import-side split are the same for both files, so they live
5882
+ * here once.
5792
5883
  */
5884
+ /** Marks where one check starts inside the single instruction file. */
5793
5885
  const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5794
5886
  /**
5795
5887
  * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
@@ -5800,17 +5892,16 @@ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5800
5892
  */
5801
5893
  const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5802
5894
  const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5803
- const FALLBACK_CHECK_NAME = "bugbot";
5804
- function renderMarker(name) {
5895
+ function renderCheckMarker(name) {
5805
5896
  return `<!-- rulesync:check:${name} -->`;
5806
5897
  }
5807
- function escapeMarkers(content) {
5898
+ function escapeCheckMarkers(content) {
5808
5899
  return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5809
5900
  }
5810
- function unescapeMarkers(content) {
5901
+ function unescapeCheckMarkers(content) {
5811
5902
  return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5812
5903
  }
5813
- function findMarkers(fileContent) {
5904
+ function findCheckMarkers(fileContent) {
5814
5905
  CHECK_MARKER_PATTERN.lastIndex = 0;
5815
5906
  const markers = [];
5816
5907
  let match = CHECK_MARKER_PATTERN.exec(fileContent);
@@ -5825,23 +5916,47 @@ function findMarkers(fileContent) {
5825
5916
  return markers;
5826
5917
  }
5827
5918
  /**
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.
5919
+ * Whether the file holds instruction text ahead of the first marker the
5920
+ * question the "generating replaces this" warning asks. A file with no marker
5921
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5922
+ * since there is nothing to replace.
5923
+ */
5924
+ function hasHandWrittenPreamble(fileContent) {
5925
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5926
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5927
+ }
5928
+ /**
5929
+ * Whether the file is nothing but sections rulesync generated — the question
5930
+ * the deletion guard asks, and a stricter one than
5931
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5932
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5933
+ * empty one is somebody's placeholder rather than our leftover.
5934
+ */
5935
+ function isOnlyGeneratedSections(fileContent) {
5936
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5937
+ if (firstMarkerStart === void 0) return false;
5938
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5939
+ }
5940
+ /**
5941
+ * The instruction text one check contributes. Neither file has a field to put a
5942
+ * summary in, so `description` is used only when there is no body.
5831
5943
  */
5832
5944
  function toInstruction(rulesyncCheck) {
5833
5945
  const body = rulesyncCheck.getBody().trim();
5834
5946
  if (body.length > 0) return body;
5835
5947
  return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5836
5948
  }
5837
- function renderSection(rulesyncCheck) {
5949
+ function renderCheckSection(rulesyncCheck) {
5838
5950
  const name = (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
5839
5951
  const heading = `## ${name}`;
5840
5952
  const instruction = toInstruction(rulesyncCheck);
5841
- const lines = [renderMarker(name), heading];
5842
- if (instruction.length > 0) lines.push("", escapeMarkers(instruction));
5953
+ const lines = [renderCheckMarker(name), heading];
5954
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5843
5955
  return lines.join("\n");
5844
5956
  }
5957
+ function renderCheckFile(rulesyncChecks) {
5958
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5959
+ }
5845
5960
  /** Drop the heading generate writes, so a round trip does not stack headings. */
5846
5961
  function stripGeneratedHeading(section, name) {
5847
5962
  const [firstLine, ...rest] = section.split("\n");
@@ -5849,6 +5964,53 @@ function stripGeneratedHeading(section, name) {
5849
5964
  return section.trim();
5850
5965
  }
5851
5966
  /**
5967
+ * Split an aggregated instruction file back into one check per section.
5968
+ *
5969
+ * Content ahead of the first marker — and a hand-written file with no markers
5970
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5971
+ * is dropped.
5972
+ */
5973
+ function splitCheckFile({ fileContent, fallbackName }) {
5974
+ const sections = [];
5975
+ const markers = findCheckMarkers(fileContent);
5976
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5977
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5978
+ if (preamble.length > 0) sections.push({
5979
+ name: fallbackName,
5980
+ content: unescapeCheckMarkers(preamble)
5981
+ });
5982
+ for (const [index, marker] of markers.entries()) {
5983
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5984
+ const markerName = marker.name.trim();
5985
+ const name = slugifyCheckName(markerName) || fallbackName;
5986
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5987
+ sections.push({
5988
+ name,
5989
+ content: unescapeCheckMarkers(content)
5990
+ });
5991
+ }
5992
+ const used = /* @__PURE__ */ new Set();
5993
+ return sections.map(({ name, content }) => {
5994
+ let uniqueName = name;
5995
+ let suffix = 2;
5996
+ while (used.has(uniqueName)) {
5997
+ uniqueName = `${name}-${suffix}`;
5998
+ suffix += 1;
5999
+ }
6000
+ used.add(uniqueName);
6001
+ return new RulesyncCheck({
6002
+ outputRoot: ".",
6003
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
6004
+ relativeFilePath: `${uniqueName}.md`,
6005
+ frontmatter: { targets: ["*"] },
6006
+ body: content
6007
+ });
6008
+ });
6009
+ }
6010
+ //#endregion
6011
+ //#region src/features/checks/cursor-check.ts
6012
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
6013
+ /**
5852
6014
  * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5853
6015
  *
5854
6016
  * Bugbot takes one aggregated instruction file per directory rather than a file
@@ -5905,9 +6067,7 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5905
6067
  const paths = CursorCheck.getSettablePaths();
5906
6068
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
5907
6069
  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;
6070
+ return isOnlyGeneratedSections(fileContent);
5911
6071
  }
5912
6072
  static fromRulesyncCheck(_params) {
5913
6073
  throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
@@ -5917,10 +6077,8 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5917
6077
  const paths = CursorCheck.getSettablePaths({ global });
5918
6078
  const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
5919
6079
  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`;
6080
+ 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.`);
6081
+ const fileContent = renderCheckFile(rulesyncChecks);
5924
6082
  return [new CursorCheck({
5925
6083
  outputRoot,
5926
6084
  relativeDirPath: paths.relativeDirPath,
@@ -5963,41 +6121,9 @@ var CursorCheck = class CursorCheck extends ToolCheck {
5963
6121
  return first;
5964
6122
  }
5965
6123
  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
- });
6124
+ return splitCheckFile({
6125
+ fileContent: this.getFileContent(),
6126
+ fallbackName: FALLBACK_CHECK_NAME$1
6001
6127
  });
6002
6128
  }
6003
6129
  };
@@ -6019,8 +6145,22 @@ var CursorCheck = class CursorCheck extends ToolCheck {
6019
6145
  */
6020
6146
  /** Project-root instruction file auto-injected by Hermes Agent. */
6021
6147
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
6022
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6148
+ /**
6149
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6150
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6151
+ */
6023
6152
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6153
+ /**
6154
+ * Home-relative global profile root on Windows: upstream defaults to
6155
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6156
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6157
+ *
6158
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6159
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6160
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6161
+ * followed; those users should set `HERMES_HOME` explicitly.
6162
+ */
6163
+ const HERMESAGENT_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Local", "hermes");
6024
6164
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
6025
6165
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
6026
6166
  const HERMESAGENT_CONFIG_FILE_PATH = (0, node_path.join)(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -6243,6 +6383,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
6243
6383
  }
6244
6384
  };
6245
6385
  //#endregion
6386
+ //#region src/features/checks/rovodev-check.ts
6387
+ const FALLBACK_CHECK_NAME = "review-agent";
6388
+ /**
6389
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6390
+ * (`.rovodev/.review-agent.md`).
6391
+ *
6392
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6393
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6394
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6395
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6396
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6397
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6398
+ * adapters share).
6399
+ *
6400
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6401
+ * have no equivalent there: they are not written and do not come back on
6402
+ * import. Neither does `description` whenever the check also has a body.
6403
+ *
6404
+ * Project scope only — these are per-repository review instructions, and Rovo
6405
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6406
+ * same tool is the opposite: global only.)
6407
+ *
6408
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6409
+ */
6410
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6411
+ static getSettablePaths(_options = {}) {
6412
+ return {
6413
+ relativeDirPath: ROVODEV_DIR,
6414
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6415
+ };
6416
+ }
6417
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6418
+ return this.isTargetedByRulesyncCheckDefault({
6419
+ rulesyncCheck,
6420
+ toolTarget: "rovodev"
6421
+ });
6422
+ }
6423
+ /**
6424
+ * Ownership guard the processor consults before it deletes anything for this
6425
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6426
+ * to hand-write, so anything in it that rulesync did not write is not
6427
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6428
+ * take somebody's hand-written review instructions with it.
6429
+ */
6430
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6431
+ const paths = RovodevCheck.getSettablePaths();
6432
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6433
+ if (fileContent === null) return true;
6434
+ return isOnlyGeneratedSections(fileContent);
6435
+ }
6436
+ static fromRulesyncCheck(_params) {
6437
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6438
+ }
6439
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6440
+ if (rulesyncChecks.length === 0) return [];
6441
+ const paths = RovodevCheck.getSettablePaths({ global });
6442
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6443
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6444
+ 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.`);
6445
+ return [new RovodevCheck({
6446
+ outputRoot,
6447
+ relativeDirPath: paths.relativeDirPath,
6448
+ relativeFilePath,
6449
+ fileContent: renderCheckFile(rulesyncChecks),
6450
+ global
6451
+ })];
6452
+ }
6453
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6454
+ const paths = RovodevCheck.getSettablePaths({ global });
6455
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6456
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6457
+ return new RovodevCheck({
6458
+ outputRoot,
6459
+ relativeDirPath: paths.relativeDirPath,
6460
+ relativeFilePath,
6461
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6462
+ global
6463
+ });
6464
+ }
6465
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6466
+ return new RovodevCheck({
6467
+ outputRoot,
6468
+ relativeDirPath,
6469
+ relativeFilePath,
6470
+ fileContent: "",
6471
+ validate: false,
6472
+ global
6473
+ });
6474
+ }
6475
+ validate() {
6476
+ return {
6477
+ success: true,
6478
+ error: null
6479
+ };
6480
+ }
6481
+ toRulesyncCheck() {
6482
+ const first = this.toRulesyncChecks()[0];
6483
+ if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6484
+ return first;
6485
+ }
6486
+ toRulesyncChecks() {
6487
+ return splitCheckFile({
6488
+ fileContent: this.getFileContent(),
6489
+ fallbackName: FALLBACK_CHECK_NAME
6490
+ });
6491
+ }
6492
+ };
6493
+ //#endregion
6246
6494
  //#region src/constants/codexcli-paths.ts
6247
6495
  const CODEXCLI_DIR = ".codex";
6248
6496
  const CODEXCLI_PROMPTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "prompts");
@@ -6355,11 +6603,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6355
6603
  }
6356
6604
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6357
6605
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6606
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6607
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6358
6608
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6359
6609
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6360
6610
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6361
6611
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6362
6612
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6613
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6363
6614
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6364
6615
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6365
6616
  /**
@@ -6383,6 +6634,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6383
6634
  * lock-step with the writers derived from the processor registry, so an
6384
6635
  * undeclared writer fails CI instead of merging by accident.
6385
6636
  */
6637
+ /**
6638
+ * Hermes writes one `config.yaml`, but its global profile root has three
6639
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6640
+ * itself). They are the same file with the same owners, so the declaration is
6641
+ * written once and shared — a policy edit cannot land on one spelling only.
6642
+ */
6643
+ const HERMES_CONFIG_DECLARATION = {
6644
+ format: "yaml",
6645
+ features: {
6646
+ commands: {
6647
+ kind: "replace-owned-keys",
6648
+ ownedKeys: ["plugins"]
6649
+ },
6650
+ subagents: {
6651
+ kind: "replace-owned-keys",
6652
+ ownedKeys: ["plugins"]
6653
+ },
6654
+ mcp: {
6655
+ kind: "replace-owned-keys",
6656
+ ownedKeys: ["mcp_servers"]
6657
+ },
6658
+ hooks: {
6659
+ kind: "replace-owned-keys",
6660
+ ownedKeys: ["hooks"]
6661
+ },
6662
+ permissions: {
6663
+ kind: "deep-merge",
6664
+ replaceKeys: ["permissions"]
6665
+ }
6666
+ }
6667
+ };
6668
+ /**
6669
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6670
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6671
+ * name the profile directory itself, so the file has two spellings that share
6672
+ * one declaration — a policy edit cannot land on only one of them.
6673
+ */
6674
+ const KIMI_CODE_CONFIG_DECLARATION = {
6675
+ format: "toml",
6676
+ invalidRootPolicy: "error",
6677
+ features: {
6678
+ hooks: {
6679
+ kind: "replace-owned-keys",
6680
+ ownedKeys: ["hooks"]
6681
+ },
6682
+ mcp: {
6683
+ kind: "replace-owned-keys",
6684
+ ownedKeys: ["mcp"]
6685
+ },
6686
+ permissions: {
6687
+ kind: "replace-owned-keys",
6688
+ ownedKeys: [
6689
+ "permission",
6690
+ "default_permission_mode",
6691
+ "tools"
6692
+ ]
6693
+ }
6694
+ }
6695
+ };
6386
6696
  const SHARED_CONFIG_OWNERSHIP = {
6387
6697
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6388
6698
  format: "json",
@@ -6401,31 +6711,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6401
6711
  }
6402
6712
  }
6403
6713
  },
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
- },
6714
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6715
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6716
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6429
6717
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6430
6718
  format: "yaml",
6431
6719
  invalidRootPolicy: "error",
@@ -6471,6 +6759,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6471
6759
  ".config/zed/settings.json": {
6472
6760
  format: "json",
6473
6761
  features: {
6762
+ ignore: {
6763
+ kind: "replace-owned-keys",
6764
+ ownedKeys: ["private_files"]
6765
+ },
6474
6766
  mcp: {
6475
6767
  kind: "replace-owned-keys",
6476
6768
  ownedKeys: ["context_servers"]
@@ -6484,6 +6776,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6484
6776
  "AppData/Roaming/Zed/settings.json": {
6485
6777
  format: "json",
6486
6778
  features: {
6779
+ ignore: {
6780
+ kind: "replace-owned-keys",
6781
+ ownedKeys: ["private_files"]
6782
+ },
6487
6783
  mcp: {
6488
6784
  kind: "replace-owned-keys",
6489
6785
  ownedKeys: ["context_servers"]
@@ -6740,31 +7036,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6740
7036
  }
6741
7037
  }
6742
7038
  },
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
- },
7039
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
7040
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6765
7041
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6766
7042
  format: "toml",
6767
7043
  features: {
7044
+ ignore: {
7045
+ kind: "custom",
7046
+ policyFunction: "applyIgnoreReadDenies"
7047
+ },
6768
7048
  mcp: {
6769
7049
  kind: "replace-owned-keys",
6770
7050
  ownedKeys: ["plugins"]
@@ -6782,6 +7062,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6782
7062
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6783
7063
  format: "toml",
6784
7064
  features: {
7065
+ ignore: {
7066
+ kind: "custom",
7067
+ policyFunction: "applyIgnoreReadDenies"
7068
+ },
6785
7069
  mcp: {
6786
7070
  kind: "replace-owned-keys",
6787
7071
  ownedKeys: ["plugins"]
@@ -7226,6 +7510,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7226
7510
  filePattern: "*.json"
7227
7511
  }
7228
7512
  }],
7513
+ ["rovodev", {
7514
+ class: RovodevCheck,
7515
+ meta: {
7516
+ supportsGlobal: false,
7517
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7518
+ }
7519
+ }],
7229
7520
  ["takt", {
7230
7521
  class: TaktCheck,
7231
7522
  meta: {
@@ -7381,19 +7672,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7381
7672
  }
7382
7673
  };
7383
7674
  //#endregion
7675
+ //#region src/utils/tool-home.ts
7676
+ /**
7677
+ * Where the rulesync-side source files of a tool with a home override belong.
7678
+ *
7679
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7680
+ * imported back out of it are not part of the tool's profile — they stay under
7681
+ * the rulesync home. When no override is set, the native output root already is
7682
+ * that place.
7683
+ */
7684
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7685
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7686
+ }
7687
+ //#endregion
7384
7688
  //#region src/utils/hermesagent.ts
7385
7689
  function getHermesagentHome() {
7386
7690
  const configuredHome = process.env.HERMES_HOME?.trim();
7387
7691
  return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
7388
7692
  }
7693
+ /**
7694
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7695
+ *
7696
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7697
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7698
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7699
+ * would land where Hermes never reads.
7700
+ *
7701
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7702
+ */
7703
+ function getHermesagentGlobalDir() {
7704
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7705
+ }
7389
7706
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7390
7707
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7391
7708
  }
7709
+ /**
7710
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7711
+ * actually writes in the requested scope.
7712
+ *
7713
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7714
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7715
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7716
+ * platform default directory takes its place.
7717
+ */
7392
7718
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7393
- if (!global || !getHermesagentHome()) return relativeDirPath;
7719
+ if (!global) return relativeDirPath;
7394
7720
  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;
7721
+ try {
7722
+ checkPathTraversal({
7723
+ relativePath: relativeDirPath,
7724
+ intendedRootDir: "."
7725
+ });
7726
+ checkPathTraversal({
7727
+ relativePath,
7728
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7729
+ });
7730
+ } catch {
7731
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7732
+ }
7733
+ return getHermesagentHome() ? relativePath || "." : (0, node_path.join)(getHermesagentGlobalDir(), relativePath);
7397
7734
  }
7398
7735
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7399
7736
  return (0, node_path.join)(getHermesagentRelativeDirPath({
@@ -7401,8 +7738,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7401
7738
  relativeDirPath: (0, node_path.dirname)(relativeFilePath)
7402
7739
  }), (0, node_path.basename)(relativeFilePath));
7403
7740
  }
7741
+ /**
7742
+ * Every spelling `config.yaml` can take in global scope, so that the
7743
+ * shared-write derivation and the gateway ownership table it is checked against
7744
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7745
+ *
7746
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7747
+ * which would otherwise make the derived shared-file key depend on the ambient
7748
+ * environment — the drift guards would then go blind in precisely the
7749
+ * configuration this feature exists for.
7750
+ */
7751
+ function getHermesagentSharedConfigWritePaths() {
7752
+ return [
7753
+ {
7754
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7755
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7756
+ },
7757
+ {
7758
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7759
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7760
+ },
7761
+ {
7762
+ relativeDirPath: ".",
7763
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7764
+ }
7765
+ ];
7766
+ }
7767
+ /**
7768
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7769
+ * writes. All three spellings carry the same declaration, but passing the key of
7770
+ * the file being written keeps the write path and the drift guards reading the
7771
+ * same entry.
7772
+ */
7773
+ function getHermesagentConfigSharedFileKey({ global }) {
7774
+ return sharedConfigFileKey({
7775
+ relativeDirPath: getHermesagentRelativeDirPath({
7776
+ global,
7777
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7778
+ }),
7779
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7780
+ });
7781
+ }
7404
7782
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7405
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7783
+ return getToolRulesyncOutputRoot({
7784
+ nativeOutputRoot,
7785
+ global,
7786
+ toolHome: getHermesagentHome
7787
+ });
7406
7788
  }
7407
7789
  //#endregion
7408
7790
  //#region src/constants/agentsmd-paths.ts
@@ -9106,6 +9488,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
9106
9488
  }
9107
9489
  };
9108
9490
  //#endregion
9491
+ //#region src/constants/grokcli-paths.ts
9492
+ /**
9493
+ * Grok Build CLI (xAI) configuration-layout conventions.
9494
+ *
9495
+ * Single source of truth for where Grok Build expects its files. Grok Build
9496
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9497
+ * with project/global scopes resolved by the directory the CLI runs in
9498
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9499
+ *
9500
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9501
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9502
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9503
+ * @see https://docs.x.ai/build/overview
9504
+ */
9505
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9506
+ const GROKCLI_DIR = ".grok";
9507
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9508
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9509
+ /**
9510
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9511
+ * permission mode, and other settings all live here; permissions reuse the same
9512
+ * file name as MCP since Grok consolidates everything into one config.
9513
+ */
9514
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9515
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9516
+ const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
9517
+ /**
9518
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9519
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9520
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9521
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9522
+ * @see https://docs.x.ai/build/features/hooks
9523
+ */
9524
+ const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
9525
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9526
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9527
+ /**
9528
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9529
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9530
+ * (global), each a Markdown file with YAML frontmatter (verified via
9531
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9532
+ */
9533
+ const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
9534
+ /**
9535
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9536
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9537
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9538
+ * MCP/skills/subagents adapters).
9539
+ */
9540
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9541
+ /**
9542
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9543
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9544
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9545
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9546
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9547
+ * for Claude Code.
9548
+ *
9549
+ * Skills are collected before commands and win name collisions, so a
9550
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9551
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9552
+ */
9553
+ const GROKCLI_COMMANDS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "commands");
9554
+ /**
9555
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9556
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9557
+ * walks, and `~/.grok/rules/` in the home scope.
9558
+ * @see https://docs.x.ai/build/overview
9559
+ */
9560
+ const GROKCLI_RULES_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "rules");
9561
+ //#endregion
9562
+ //#region src/features/commands/grokcli-command.ts
9563
+ /**
9564
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9565
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9566
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9567
+ *
9568
+ * Two upstream constraints shape this adapter:
9569
+ *
9570
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9571
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9572
+ * onto their basename).
9573
+ * - Skills are collected before commands and win name collisions, so a
9574
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9575
+ *
9576
+ * `description` and `argument-hint` describe the command, while
9577
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9578
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9579
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9580
+ */
9581
+ const GrokcliCommandFrontmatterSchema = zod_mini.z.looseObject({
9582
+ description: zod_mini.z.optional(zod_mini.z.string()),
9583
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
9584
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9585
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
9586
+ });
9587
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9588
+ frontmatter;
9589
+ body;
9590
+ constructor({ frontmatter, body, ...rest }) {
9591
+ if (rest.validate) {
9592
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9593
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9594
+ }
9595
+ super({
9596
+ ...rest,
9597
+ fileContent: stringifyFrontmatter(body, frontmatter)
9598
+ });
9599
+ this.frontmatter = frontmatter;
9600
+ this.body = body;
9601
+ }
9602
+ static getSettablePaths(_options = {}) {
9603
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9604
+ }
9605
+ getBody() {
9606
+ return this.body;
9607
+ }
9608
+ getFrontmatter() {
9609
+ return this.frontmatter;
9610
+ }
9611
+ toRulesyncCommand() {
9612
+ const { description, ...restFields } = this.frontmatter;
9613
+ const rulesyncFrontmatter = {
9614
+ targets: ["*"],
9615
+ description,
9616
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9617
+ };
9618
+ return new RulesyncCommand({
9619
+ outputRoot: ".",
9620
+ frontmatter: rulesyncFrontmatter,
9621
+ body: this.body,
9622
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9623
+ relativeFilePath: this.relativeFilePath,
9624
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9625
+ validate: true
9626
+ });
9627
+ }
9628
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9629
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9630
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9631
+ const grokcliFrontmatter = {
9632
+ description: rulesyncFrontmatter.description,
9633
+ ...grokcliFields
9634
+ };
9635
+ const paths = this.getSettablePaths({ global });
9636
+ return new GrokcliCommand({
9637
+ outputRoot,
9638
+ frontmatter: grokcliFrontmatter,
9639
+ body: rulesyncCommand.getBody(),
9640
+ relativeDirPath: paths.relativeDirPath,
9641
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9642
+ validate
9643
+ });
9644
+ }
9645
+ validate() {
9646
+ if (!this.frontmatter) return {
9647
+ success: true,
9648
+ error: null
9649
+ };
9650
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9651
+ if (result.success) return {
9652
+ success: true,
9653
+ error: null
9654
+ };
9655
+ return {
9656
+ success: false,
9657
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9658
+ };
9659
+ }
9660
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9661
+ return this.isTargetedByRulesyncCommandDefault({
9662
+ rulesyncCommand,
9663
+ toolTarget: "grokcli"
9664
+ });
9665
+ }
9666
+ /**
9667
+ * Warn when a rulesync skill would shadow a rulesync command.
9668
+ *
9669
+ * Grok collects skills before commands and lets skills win name collisions,
9670
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9671
+ * Both files are still written correctly — nothing is overwritten and no
9672
+ * output is lost — so this warns rather than failing the run the way the
9673
+ * Hermes check does, where the two surfaces really do write the same path.
9674
+ */
9675
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9676
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
9677
+ if (commandNames.size === 0) return;
9678
+ 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));
9679
+ 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.`);
9680
+ }
9681
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9682
+ const paths = this.getSettablePaths({ global });
9683
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
9684
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9685
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9686
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9687
+ return new GrokcliCommand({
9688
+ outputRoot,
9689
+ relativeDirPath: paths.relativeDirPath,
9690
+ relativeFilePath,
9691
+ frontmatter: result.data,
9692
+ body: content.trim(),
9693
+ validate
9694
+ });
9695
+ }
9696
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9697
+ return new GrokcliCommand({
9698
+ outputRoot,
9699
+ relativeDirPath,
9700
+ relativeFilePath,
9701
+ frontmatter: { description: "" },
9702
+ body: "",
9703
+ validate: false
9704
+ });
9705
+ }
9706
+ };
9707
+ //#endregion
9109
9708
  //#region src/features/skills/tool-skill.ts
9110
9709
  /** Ordered skill directory roots: primary first. */
9111
9710
  function toolSkillSearchRoots(paths) {
@@ -9740,7 +10339,7 @@ def register(ctx):
9740
10339
  _register_command(ctx, command)
9741
10340
  `;
9742
10341
  }
9743
- function getEnabledPluginConfigContent$1(currentContent) {
10342
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9744
10343
  const config = parseSharedConfig({
9745
10344
  format: "yaml",
9746
10345
  fileContent: currentContent
@@ -9748,7 +10347,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9748
10347
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9749
10348
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9750
10349
  return applySharedConfigPatch({
9751
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10350
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9752
10351
  feature: "commands",
9753
10352
  existingContent: currentContent,
9754
10353
  patch: { plugins: {
@@ -9757,7 +10356,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9757
10356
  } }
9758
10357
  });
9759
10358
  }
9760
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10359
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9761
10360
  const config = parseSharedConfig({
9762
10361
  format: "yaml",
9763
10362
  fileContent: currentContent
@@ -9765,7 +10364,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9765
10364
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9766
10365
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9767
10366
  return applySharedConfigPatch({
9768
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10367
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9769
10368
  feature: "commands",
9770
10369
  existingContent: currentContent,
9771
10370
  patch: { plugins: {
@@ -9781,35 +10380,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9781
10380
  error: null
9782
10381
  };
9783
10382
  }
9784
- shouldMergeExistingFileContent() {
10383
+ /**
10384
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10385
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10386
+ */
10387
+ matchesPath(relativeFilePath) {
9785
10388
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9786
10389
  global: this.global,
9787
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10390
+ relativeFilePath
9788
10391
  }));
9789
10392
  }
10393
+ shouldMergeExistingFileContent() {
10394
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10395
+ }
9790
10396
  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));
10397
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10398
+ super.setFileContent(getEnabledPluginConfigContent$1({
10399
+ currentContent: newFileContent,
10400
+ global: this.global
10401
+ }));
9796
10402
  return;
9797
10403
  }
9798
10404
  super.setFileContent(newFileContent);
9799
10405
  }
9800
10406
  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());
10407
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10408
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10409
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10410
+ currentContent: super.getFileContent(),
10411
+ global: this.global
10412
+ });
9813
10413
  return super.getFileContent();
9814
10414
  }
9815
10415
  };
@@ -9826,14 +10426,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9826
10426
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9827
10427
  }) };
9828
10428
  }
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
- }];
10429
+ /**
10430
+ * `config.yaml` under every spelling the global profile root can take.
10431
+ * @see getHermesagentSharedConfigWritePaths
10432
+ */
10433
+ static getExtraSharedWritePaths() {
10434
+ return getHermesagentSharedConfigWritePaths();
9837
10435
  }
9838
10436
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9839
10437
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9858,33 +10456,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9858
10456
  }
9859
10457
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9860
10458
  if (toolCommands.length === 0 && !forDeletion) return [];
10459
+ const pluginDirPath = getHermesagentRelativeDirPath({
10460
+ global,
10461
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10462
+ });
9861
10463
  const pluginFiles = [
9862
10464
  new HermesagentCommandAuxiliaryFile({
9863
10465
  outputRoot,
9864
- relativeDirPath: getHermesagentRelativeDirPath({
9865
- global,
9866
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9867
- }),
10466
+ relativeDirPath: pluginDirPath,
9868
10467
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9869
10468
  fileContent: "",
9870
10469
  global
9871
10470
  }),
9872
10471
  new HermesagentCommandAuxiliaryFile({
9873
10472
  outputRoot,
9874
- relativeDirPath: getHermesagentRelativeDirPath({
9875
- global,
9876
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9877
- }),
10473
+ relativeDirPath: pluginDirPath,
9878
10474
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9879
10475
  fileContent: "Generated and owned by RuleSync.\n",
9880
10476
  global
9881
10477
  }),
9882
10478
  new HermesagentCommandAuxiliaryFile({
9883
10479
  outputRoot,
9884
- relativeDirPath: getHermesagentRelativeDirPath({
9885
- global,
9886
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9887
- }),
10480
+ relativeDirPath: pluginDirPath,
9888
10481
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9889
10482
  fileContent: "",
9890
10483
  global
@@ -11119,19 +11712,6 @@ var RooCommand = class RooCommand extends ToolCommand {
11119
11712
  }
11120
11713
  };
11121
11714
  //#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
11715
  //#region src/features/commands/rovodev-command.ts
11136
11716
  /**
11137
11717
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11715,6 +12295,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11715
12295
  supportsSubdirectory: false
11716
12296
  }
11717
12297
  }],
12298
+ ["grokcli", {
12299
+ class: GrokcliCommand,
12300
+ meta: {
12301
+ extension: "md",
12302
+ supportsProject: true,
12303
+ supportsGlobal: true,
12304
+ isSimulated: false,
12305
+ supportsSubdirectory: false
12306
+ }
12307
+ }],
11718
12308
  ["hermesagent", {
11719
12309
  class: HermesagentCommand,
11720
12310
  meta: {
@@ -11908,7 +12498,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11908
12498
  const factory = this.getFactory(this.toolTarget);
11909
12499
  await factory.class.validateRulesyncCommands?.({
11910
12500
  inputRoot: this.inputRoot,
11911
- rulesyncCommands
12501
+ rulesyncCommands,
12502
+ logger: this.logger
11912
12503
  });
11913
12504
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11914
12505
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -12046,7 +12637,10 @@ var CommandsProcessor = class extends FeatureProcessor {
12046
12637
  }));
12047
12638
  const currentContent = await readFileContentOrNull(configPath);
12048
12639
  if (currentContent === null) return changedCount;
12049
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12640
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12641
+ currentContent,
12642
+ global: this.global
12643
+ });
12050
12644
  if (nextContent === currentContent) return changedCount;
12051
12645
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
12052
12646
  else await writeFileContent(configPath, nextContent);
@@ -14508,64 +15102,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14508
15102
  }
14509
15103
  };
14510
15104
  //#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
15105
  //#region src/features/hooks/grokcli-hooks.ts
14570
15106
  const GROKCLI_CONVERTER_CONFIG = {
14571
15107
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14811,6 +15347,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14811
15347
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14812
15348
  };
14813
15349
  }
15350
+ /**
15351
+ * `config.yaml` under every spelling the global profile root can take.
15352
+ * @see getHermesagentSharedConfigWritePaths
15353
+ */
15354
+ static getExtraSharedWritePaths() {
15355
+ return getHermesagentSharedConfigWritePaths();
15356
+ }
14814
15357
  constructor(params) {
14815
15358
  super({
14816
15359
  ...params,
@@ -14848,7 +15391,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14848
15391
  }
14849
15392
  setFileContent(fileContent) {
14850
15393
  this.fileContent = applySharedConfigPatch({
14851
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15394
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14852
15395
  feature: "hooks",
14853
15396
  existingContent: fileContent,
14854
15397
  patch: parseSharedConfig({
@@ -15224,8 +15767,38 @@ function getKimiCodeHome() {
15224
15767
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
15225
15768
  return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
15226
15769
  }
15770
+ /**
15771
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15772
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15773
+ * Declared unconditionally so the derived shared-file keys — and the drift
15774
+ * guards checked against them — do not depend on the ambient environment.
15775
+ */
15776
+ function getKimiCodeSharedConfigWritePaths() {
15777
+ return [{
15778
+ relativeDirPath: KIMI_CODE_DIR,
15779
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15780
+ }, {
15781
+ relativeDirPath: ".",
15782
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15783
+ }];
15784
+ }
15785
+ /**
15786
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15787
+ * Both spellings carry the same declaration, but passing the key of the file
15788
+ * being written keeps the write path and the drift guards on the same entry.
15789
+ */
15790
+ function getKimiCodeConfigSharedFileKey({ global }) {
15791
+ return sharedConfigFileKey({
15792
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15793
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15794
+ });
15795
+ }
15227
15796
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
15228
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15797
+ return getToolRulesyncOutputRoot({
15798
+ nativeOutputRoot,
15799
+ global,
15800
+ toolHome: getKimiCodeHome
15801
+ });
15229
15802
  }
15230
15803
  //#endregion
15231
15804
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -15323,13 +15896,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
15323
15896
  isDeletable() {
15324
15897
  return false;
15325
15898
  }
15899
+ /**
15900
+ * `config.toml` under both spellings its directory can take.
15901
+ * @see getKimiCodeSharedConfigWritePaths
15902
+ */
15903
+ static getExtraSharedWritePaths() {
15904
+ return getKimiCodeSharedConfigWritePaths();
15905
+ }
15326
15906
  shouldMergeExistingFileContent() {
15327
15907
  return true;
15328
15908
  }
15329
15909
  setFileContent(fileContent) {
15330
15910
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
15331
15911
  this.fileContent = applySharedConfigPatch({
15332
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15912
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
15333
15913
  feature: "hooks",
15334
15914
  existingContent: fileContent,
15335
15915
  patch: parseSharedConfig({
@@ -18020,6 +18600,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
18020
18600
  }
18021
18601
  };
18022
18602
  //#endregion
18603
+ //#region src/features/shared/reasonix-config-table.ts
18604
+ /**
18605
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18606
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18607
+ *
18608
+ * TOML is only structurally validated on parse, so a hand-edited config can
18609
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18610
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18611
+ * before merging, so the narrowing lives here once rather than being re-spelled
18612
+ * (and re-diverging) per feature.
18613
+ */
18614
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18615
+ function toReasonixStringArray(value) {
18616
+ if (!Array.isArray(value)) return [];
18617
+ return value.filter((entry) => typeof entry === "string");
18618
+ }
18619
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18620
+ function toReasonixTable(value) {
18621
+ if (!isPlainObject$1(value)) return {};
18622
+ return { ...value };
18623
+ }
18624
+ //#endregion
18625
+ //#region src/features/ignore/reasonix-ignore.ts
18626
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18627
+ /**
18628
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18629
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18630
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18631
+ * gateway already implements applies verbatim; only the surrounding file
18632
+ * format differs. Sibling keys such as `mode` pass through untouched.
18633
+ */
18634
+ const asClaudeStyleSettings = (document) => {
18635
+ const table = permissionsTableOf(document);
18636
+ return {
18637
+ ...document,
18638
+ permissions: {
18639
+ ...table,
18640
+ allow: toReasonixStringArray(table.allow),
18641
+ ask: toReasonixStringArray(table.ask),
18642
+ deny: toReasonixStringArray(table.deny)
18643
+ }
18644
+ };
18645
+ };
18646
+ /**
18647
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18648
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18649
+ * one.
18650
+ */
18651
+ const withoutEmptyPermissions = (settings) => {
18652
+ const document = { ...settings };
18653
+ const permissions = document.permissions;
18654
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18655
+ return document;
18656
+ };
18657
+ /**
18658
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18659
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18660
+ * `~/.reasonix/config.toml` (global).
18661
+ *
18662
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18663
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18664
+ * while `forbid_read` is documented as absolute paths with no glob support.
18665
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18666
+ */
18667
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18668
+ constructor(params) {
18669
+ super(params);
18670
+ const document = parseSharedConfig({
18671
+ format: "toml",
18672
+ fileContent: this.fileContent
18673
+ });
18674
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18675
+ }
18676
+ static getSettablePaths({ global = false } = {}) {
18677
+ return {
18678
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18679
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18680
+ };
18681
+ }
18682
+ /**
18683
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18684
+ * permissions feature and user-authored tables, so rulesync must never
18685
+ * delete it.
18686
+ */
18687
+ isDeletable() {
18688
+ return false;
18689
+ }
18690
+ toRulesyncIgnore() {
18691
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18692
+ return new RulesyncIgnore({
18693
+ outputRoot: this.outputRoot,
18694
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18695
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18696
+ fileContent: rulesyncPatterns.join("\n")
18697
+ });
18698
+ }
18699
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18700
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18701
+ const paths = this.getSettablePaths({ global });
18702
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18703
+ const existingDocument = parseSharedConfig({
18704
+ format: "toml",
18705
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18706
+ filePath
18707
+ });
18708
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18709
+ settings: asClaudeStyleSettings(existingDocument),
18710
+ readDenies
18711
+ }));
18712
+ return new ReasonixIgnore({
18713
+ outputRoot,
18714
+ relativeDirPath: paths.relativeDirPath,
18715
+ relativeFilePath: paths.relativeFilePath,
18716
+ fileContent: stringifySharedConfig({
18717
+ format: "toml",
18718
+ document
18719
+ }),
18720
+ validate: true,
18721
+ global
18722
+ });
18723
+ }
18724
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18725
+ const paths = this.getSettablePaths({ global });
18726
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18727
+ return new ReasonixIgnore({
18728
+ outputRoot,
18729
+ relativeDirPath: paths.relativeDirPath,
18730
+ relativeFilePath: paths.relativeFilePath,
18731
+ fileContent,
18732
+ validate,
18733
+ global
18734
+ });
18735
+ }
18736
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18737
+ return new ReasonixIgnore({
18738
+ outputRoot,
18739
+ relativeDirPath,
18740
+ relativeFilePath,
18741
+ fileContent: "",
18742
+ validate: false,
18743
+ global
18744
+ });
18745
+ }
18746
+ };
18747
+ //#endregion
18023
18748
  //#region src/features/ignore/roo-ignore.ts
18024
18749
  /**
18025
18750
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -18177,6 +18902,14 @@ const ZED_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Roaming", "Zed");
18177
18902
  function getZedGlobalDir() {
18178
18903
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
18179
18904
  }
18905
+ /**
18906
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18907
+ * spelling per platform, but the shared-write derivation (and the gateway
18908
+ * ownership table it is checked against) must know both on every platform.
18909
+ */
18910
+ function getZedOtherPlatformGlobalDir() {
18911
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18912
+ }
18180
18913
  const ZED_SETTINGS_FILE_NAME = "settings.json";
18181
18914
  const ZED_RULE_FILE_NAME = ".rules";
18182
18915
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -18189,12 +18922,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18189
18922
  const jsonValue = JSON.parse(this.fileContent);
18190
18923
  this.patterns = jsonValue.private_files ?? [];
18191
18924
  }
18192
- static getSettablePaths() {
18925
+ static getSettablePaths({ global = false } = {}) {
18193
18926
  return {
18194
- relativeDirPath: ZED_DIR,
18927
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
18195
18928
  relativeFilePath: ZED_SETTINGS_FILE_NAME
18196
18929
  };
18197
18930
  }
18931
+ /** @see getZedOtherPlatformGlobalDir */
18932
+ static getExtraSharedWritePaths({ global = false } = {}) {
18933
+ if (!global) return [];
18934
+ return [{
18935
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18936
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18937
+ }];
18938
+ }
18198
18939
  /**
18199
18940
  * ZedIgnore uses settings.json which is a user-managed config file.
18200
18941
  * It should not be deleted by rulesync.
@@ -18205,48 +18946,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
18205
18946
  toRulesyncIgnore() {
18206
18947
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
18207
18948
  return new RulesyncIgnore({
18208
- outputRoot: this.outputRoot,
18949
+ outputRoot: ".",
18209
18950
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18210
18951
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18211
18952
  fileContent
18212
18953
  });
18213
18954
  }
18214
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18955
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18215
18956
  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);
18957
+ const paths = this.getSettablePaths({ global });
18958
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18217
18959
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
18218
- const mergedPatterns = (0, es_toolkit.uniq)([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18960
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
18219
18961
  return new ZedIgnore({
18220
18962
  outputRoot,
18221
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18222
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18963
+ relativeDirPath: paths.relativeDirPath,
18964
+ relativeFilePath: paths.relativeFilePath,
18223
18965
  fileContent: applySharedConfigPatch({
18224
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18966
+ fileKey: sharedConfigFileKey(paths),
18225
18967
  feature: "ignore",
18226
18968
  existingContent: existingFileContent,
18227
- patch: { private_files: mergedPatterns },
18969
+ patch: { private_files: managedPatterns },
18228
18970
  filePath
18229
18971
  }),
18230
- validate: true
18972
+ validate: true,
18973
+ global
18231
18974
  });
18232
18975
  }
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));
18976
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18977
+ const paths = this.getSettablePaths({ global });
18978
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
18235
18979
  return new ZedIgnore({
18236
18980
  outputRoot,
18237
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18238
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18981
+ relativeDirPath: paths.relativeDirPath,
18982
+ relativeFilePath: paths.relativeFilePath,
18239
18983
  fileContent,
18240
- validate
18984
+ validate,
18985
+ global
18241
18986
  });
18242
18987
  }
18243
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18988
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18244
18989
  return new ZedIgnore({
18245
18990
  outputRoot,
18246
18991
  relativeDirPath,
18247
18992
  relativeFilePath,
18248
18993
  fileContent: "{}",
18249
- validate: false
18994
+ validate: false,
18995
+ global
18250
18996
  });
18251
18997
  }
18252
18998
  };
@@ -18268,6 +19014,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
18268
19014
  ["kiro-cli", { class: KiroIgnore }],
18269
19015
  ["kiro-ide", { class: KiroIgnore }],
18270
19016
  ["qwencode", { class: QwencodeIgnore }],
19017
+ ["reasonix", { class: ReasonixIgnore }],
18271
19018
  ["roo", { class: RooIgnore }],
18272
19019
  ["devin", { class: DevinIgnore }],
18273
19020
  ["vibe", { class: VibeIgnore }],
@@ -18278,7 +19025,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
18278
19025
  const ignoreProcessorGlobalToolTargets = [
18279
19026
  "kiro",
18280
19027
  "kiro-cli",
18281
- "kiro-ide"
19028
+ "kiro-ide",
19029
+ "reasonix",
19030
+ "zed"
18282
19031
  ];
18283
19032
  const defaultGetFactory$4 = (target) => {
18284
19033
  const factory = toolIgnoreFactories.get(target);
@@ -20874,7 +21623,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20874
21623
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20875
21624
  this.config = merged;
20876
21625
  super.setFileContent(applySharedConfigPatch({
20877
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21626
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20878
21627
  feature: "mcp",
20879
21628
  existingContent: fileContent,
20880
21629
  patch: { mcp_servers: merged.mcp_servers }
@@ -20892,6 +21641,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20892
21641
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20893
21642
  };
20894
21643
  }
21644
+ /**
21645
+ * `config.yaml` under every spelling the global profile root can take.
21646
+ * @see getHermesagentSharedConfigWritePaths
21647
+ */
21648
+ static getExtraSharedWritePaths() {
21649
+ return getHermesagentSharedConfigWritePaths();
21650
+ }
20895
21651
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20896
21652
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20897
21653
  const paths = this.getSettablePaths({ global });
@@ -20918,7 +21674,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20918
21674
  relativeDirPath: paths.relativeDirPath,
20919
21675
  relativeFilePath: paths.relativeFilePath,
20920
21676
  fileContent: applySharedConfigPatch({
20921
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21677
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20922
21678
  feature: "mcp",
20923
21679
  existingContent: fileContent,
20924
21680
  patch: { mcp_servers: merged.mcp_servers }
@@ -21379,9 +22135,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
21379
22135
  * Merge a list of project rule file globs into the `instructions` array of the
21380
22136
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
21381
22137
  * (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.
22138
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22139
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22140
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22141
+ * config load — which is why `KiloRule` registers instructions in project
22142
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22143
+ * stable output.
21385
22144
  *
21386
22145
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
21387
22146
  */
@@ -21594,7 +22353,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21594
22353
  const existingContent = existing.content;
21595
22354
  const existingSection = existing.mcp;
21596
22355
  const fileContent = applySharedConfigPatch({
21597
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22356
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21598
22357
  feature: "mcp",
21599
22358
  existingContent,
21600
22359
  patch: { mcp: {
@@ -21681,11 +22440,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21681
22440
  * derivation sees this feature as one of that file's writers — it is not a
21682
22441
  * settable path, since the servers themselves live in `mcp.json`.
21683
22442
  */
21684
- static getExtraSharedWritePaths({ global = false } = {}) {
21685
- return global ? [{
21686
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21687
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21688
- }] : [];
22443
+ static getExtraSharedWritePaths() {
22444
+ return getKimiCodeSharedConfigWritePaths();
21689
22445
  }
21690
22446
  /**
21691
22447
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -23321,16 +24077,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
23321
24077
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23322
24078
  };
23323
24079
  }
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
- */
24080
+ /** @see getZedOtherPlatformGlobalDir */
23330
24081
  static getExtraSharedWritePaths({ global = false } = {}) {
23331
24082
  if (!global) return [];
23332
24083
  return [{
23333
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24084
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
23334
24085
  relativeFilePath: ZED_SETTINGS_FILE_NAME
23335
24086
  }];
23336
24087
  }
@@ -27760,6 +28511,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27760
28511
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27761
28512
  };
27762
28513
  }
28514
+ /**
28515
+ * `config.yaml` under every spelling the global profile root can take.
28516
+ * @see getHermesagentSharedConfigWritePaths
28517
+ */
28518
+ static getExtraSharedWritePaths() {
28519
+ return getHermesagentSharedConfigWritePaths();
28520
+ }
27763
28521
  constructor(params) {
27764
28522
  super({
27765
28523
  ...params,
@@ -27797,7 +28555,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27797
28555
  }
27798
28556
  setFileContent(fileContent) {
27799
28557
  this.fileContent = applySharedConfigPatch({
27800
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28558
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27801
28559
  feature: "permissions",
27802
28560
  existingContent: fileContent,
27803
28561
  patch: parseSharedConfig({
@@ -28210,7 +28968,10 @@ const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
28210
28968
  "ask",
28211
28969
  "deny"
28212
28970
  ]))]);
28213
- const KiloPermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)) });
28971
+ const KiloPermissionsConfigSchema = zod_mini.z.looseObject({
28972
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)),
28973
+ sandbox: zod_mini.z.optional(zod_mini.z.unknown())
28974
+ });
28214
28975
  /**
28215
28976
  * Kilo permission keys that share a name with a canonical rulesync category and
28216
28977
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -28267,6 +29028,25 @@ function collectKiloDenyPatterns(value) {
28267
29028
  }
28268
29029
  return [];
28269
29030
  }
29031
+ function asKiloRecord(value) {
29032
+ return isPlainObject$1(value) ? { ...value } : {};
29033
+ }
29034
+ /**
29035
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
29036
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
29037
+ * project config merely tighten — so writing the wider keys into a project file
29038
+ * would produce config Kilo ignores.
29039
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
29040
+ */
29041
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29042
+ function narrowSandboxToProjectScope({ authored, logger }) {
29043
+ const emitted = {};
29044
+ const dropped = [];
29045
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29046
+ else dropped.push(key);
29047
+ 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.`);
29048
+ return emitted;
29049
+ }
28270
29050
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
28271
29051
  json;
28272
29052
  constructor(params) {
@@ -28346,8 +29126,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28346
29126
  const basePaths = KiloPermissions.getSettablePaths({ global });
28347
29127
  const filePath = (0, node_path.join)(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
28348
29128
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
28349
- const parsedPermission = parsed.permission;
28350
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29129
+ const existingPermission = asKiloRecord(parsed.permission);
28351
29130
  const rulesyncJson = rulesyncPermissions.getJson();
28352
29131
  const kiloOverride = rulesyncJson.kilo;
28353
29132
  const incomingPermission = {
@@ -28373,6 +29152,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28373
29152
  ...parsed,
28374
29153
  permission: mergedPermission
28375
29154
  };
29155
+ if (kiloOverride?.sandbox !== void 0) {
29156
+ const authored = asKiloRecord(kiloOverride.sandbox);
29157
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29158
+ authored,
29159
+ logger
29160
+ });
29161
+ const merged = {
29162
+ ...asKiloRecord(parsed.sandbox),
29163
+ ...emitted
29164
+ };
29165
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29166
+ }
28376
29167
  return new KiloPermissions({
28377
29168
  outputRoot,
28378
29169
  relativeDirPath: basePaths.relativeDirPath,
@@ -28387,9 +29178,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
28387
29178
  const overrideOnly = {};
28388
29179
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
28389
29180
  else overrideOnly[key] = value;
28390
- const json = Object.keys(overrideOnly).length > 0 ? {
29181
+ const sandbox = this.json.sandbox;
29182
+ const override = {
29183
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29184
+ ...isPlainObject$1(sandbox) && { sandbox }
29185
+ };
29186
+ const json = Object.keys(override).length > 0 ? {
28391
29187
  permission: shared,
28392
- kilo: { permission: overrideOnly }
29188
+ kilo: override
28393
29189
  } : { permission: shared };
28394
29190
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
28395
29191
  }
@@ -28630,6 +29426,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28630
29426
  isDeletable() {
28631
29427
  return false;
28632
29428
  }
29429
+ /**
29430
+ * `config.toml` under both spellings its directory can take.
29431
+ * @see getKimiCodeSharedConfigWritePaths
29432
+ */
29433
+ static getExtraSharedWritePaths() {
29434
+ return getKimiCodeSharedConfigWritePaths();
29435
+ }
28633
29436
  shouldMergeExistingFileContent() {
28634
29437
  return true;
28635
29438
  }
@@ -28644,7 +29447,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
28644
29447
  patch
28645
29448
  });
28646
29449
  this.fileContent = applySharedConfigPatch({
28647
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29450
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
28648
29451
  feature: "permissions",
28649
29452
  existingContent: fileContent,
28650
29453
  patch: {
@@ -29568,14 +30371,6 @@ function parseReasonixConfig(fileContent) {
29568
30371
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
29569
30372
  return { ...parsed };
29570
30373
  }
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
30374
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
29580
30375
  /**
29581
30376
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -29632,12 +30427,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29632
30427
  const config = rulesyncPermissions.getJson();
29633
30428
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
29634
30429
  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));
30430
+ const existingPermissions = toReasonixTable(parsed.permissions);
30431
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30432
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30433
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
29639
30434
  if (logger && managedToolNames.has("Read")) {
29640
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30435
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
29641
30436
  const { toolName } = parseReasonixPermissionEntry(entry);
29642
30437
  return toolName === "Read";
29643
30438
  });
@@ -29684,11 +30479,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29684
30479
  });
29685
30480
  }
29686
30481
  toRulesyncPermissions() {
29687
- const permissions = toPermissionsTable(this.toml.permissions);
30482
+ const permissions = toReasonixTable(this.toml.permissions);
29688
30483
  const config = convertReasonixToRulesyncPermissions({
29689
- allow: toStringArray$1(permissions.allow),
29690
- ask: toStringArray$1(permissions.ask),
29691
- deny: toStringArray$1(permissions.deny)
30484
+ allow: toReasonixStringArray(permissions.allow),
30485
+ ask: toReasonixStringArray(permissions.ask),
30486
+ deny: toReasonixStringArray(permissions.deny)
29692
30487
  });
29693
30488
  const sandbox = asReasonixRecord(this.toml.sandbox);
29694
30489
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29780,19 +30575,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29780
30575
  "open_files",
29781
30576
  "expand_code_chunks",
29782
30577
  "expand_folder",
29783
- "grep"
30578
+ "grep",
30579
+ "getJiraIssue",
30580
+ "getConfluencePage"
29784
30581
  ],
29785
30582
  edit: [
29786
30583
  "find_and_replace_code",
29787
30584
  "create_file",
29788
30585
  "delete_file",
29789
- "move_file"
30586
+ "move_file",
30587
+ "createTechnicalPlan",
30588
+ "createJiraIssue",
30589
+ "updateJiraIssue",
30590
+ "createConfluencePage",
30591
+ "updateConfluencePage"
29790
30592
  ],
29791
30593
  write: [
29792
30594
  "create_file",
29793
30595
  "delete_file",
29794
30596
  "move_file",
29795
- "find_and_replace_code"
30597
+ "find_and_replace_code",
30598
+ "createTechnicalPlan",
30599
+ "createJiraIssue",
30600
+ "updateJiraIssue",
30601
+ "createConfluencePage",
30602
+ "updateConfluencePage"
29796
30603
  ]
29797
30604
  };
29798
30605
  const TOOL_KEY_TO_CATEGORY = {
@@ -29800,13 +30607,24 @@ const TOOL_KEY_TO_CATEGORY = {
29800
30607
  expand_code_chunks: "read",
29801
30608
  expand_folder: "read",
29802
30609
  grep: "read",
30610
+ getJiraIssue: "read",
30611
+ getConfluencePage: "read",
29803
30612
  find_and_replace_code: "edit",
29804
30613
  create_file: "edit",
29805
30614
  delete_file: "edit",
29806
- move_file: "edit"
30615
+ move_file: "edit",
30616
+ createTechnicalPlan: "edit",
30617
+ createJiraIssue: "edit",
30618
+ updateJiraIssue: "edit",
30619
+ createConfluencePage: "edit",
30620
+ updateConfluencePage: "edit"
29807
30621
  };
29808
30622
  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"];
30623
+ const OWNED_TOOL_PERMISSION_KEYS = [
30624
+ "bash",
30625
+ "allowedExternalPaths",
30626
+ "default"
30627
+ ];
29810
30628
  /**
29811
30629
  * Permissions adapter for Rovo Dev CLI.
29812
30630
  *
@@ -29820,9 +30638,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29820
30638
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29821
30639
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29822
30640
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30641
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30642
+ * the level Rovo Dev falls back to for any tool with no more specific
30643
+ * setting (Rovo Dev's own default is `ask`).
29823
30644
  * - `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`).
30645
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30646
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30647
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30648
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30649
+ * `updateConfluencePage`) — so these two categories reach Jira and
30650
+ * Confluence, not just the working tree.
29826
30651
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29827
30652
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29828
30653
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -30002,6 +30827,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
30002
30827
  delete toolPermissions.allowedExternalPaths;
30003
30828
  strippedKeys.push("allowedExternalPaths");
30004
30829
  }
30830
+ if (toolPermissions.default === "allow") {
30831
+ delete toolPermissions.default;
30832
+ strippedKeys.push("default");
30833
+ }
30005
30834
  const bash = toolPermissions.bash;
30006
30835
  if (isRecord(bash)) {
30007
30836
  const stripped = { ...bash };
@@ -30026,10 +30855,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
30026
30855
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30027
30856
  const toolPermissions = {};
30028
30857
  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.`);
30858
+ warnOnEditWriteConflict({
30859
+ config,
30860
+ logger
30861
+ });
30032
30862
  for (const [category, rules] of Object.entries(config.permission)) {
30863
+ if (category === CATCH_ALL_PATTERN$1) {
30864
+ const toolWideDefault = convertAllToolsRules({
30865
+ rules,
30866
+ logger
30867
+ });
30868
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30869
+ continue;
30870
+ }
30033
30871
  if (category === "bash") {
30034
30872
  const bash = convertBashRules(rules);
30035
30873
  if (bash) toolPermissions.bash = bash;
@@ -30056,6 +30894,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
30056
30894
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
30057
30895
  return toolPermissions;
30058
30896
  }
30897
+ /**
30898
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30899
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30900
+ * is happening; the conversion keeps the stricter of the two — the same
30901
+ * fail-closed rule the import direction uses when those tools disagree, so the
30902
+ * resolution never grants more than the author asked for.
30903
+ */
30904
+ function warnOnEditWriteConflict({ config, logger }) {
30905
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30906
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30907
+ 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.`);
30908
+ }
30909
+ /**
30910
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30911
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30912
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30913
+ * under `*` have no counterpart (the default is a single level), so they are
30914
+ * reported and skipped like any other rule Rovo Dev cannot express.
30915
+ */
30916
+ function convertAllToolsRules({ rules, logger }) {
30917
+ let toolWideDefault;
30918
+ for (const [pattern, action] of Object.entries(rules)) {
30919
+ if (pattern === CATCH_ALL_PATTERN$1) {
30920
+ toolWideDefault = action;
30921
+ continue;
30922
+ }
30923
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30924
+ }
30925
+ return toolWideDefault;
30926
+ }
30059
30927
  function convertBashRules(rules) {
30060
30928
  const bash = {};
30061
30929
  const commands = [];
@@ -30077,6 +30945,7 @@ function convertBashRules(rules) {
30077
30945
  */
30078
30946
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30079
30947
  const permission = {};
30948
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
30080
30949
  const bash = toolPermissions.bash;
30081
30950
  if (isRecord(bash)) {
30082
30951
  const bashRules = {};
@@ -30087,12 +30956,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
30087
30956
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
30088
30957
  }
30089
30958
  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;
30959
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30960
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30961
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30962
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30963
+ return isPermissionAction(value) ? value : void 0;
30964
+ });
30965
+ if (levels.every((level) => level === void 0)) continue;
30093
30966
  permission[category] ??= {};
30094
- const current = permission[category][CATCH_ALL_PATTERN$1];
30095
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30967
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
30096
30968
  }
30097
30969
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
30098
30970
  permission.read ??= {};
@@ -30924,16 +31796,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30924
31796
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30925
31797
  };
30926
31798
  }
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
- */
31799
+ /** @see getZedOtherPlatformGlobalDir */
30933
31800
  static getExtraSharedWritePaths({ global = false } = {}) {
30934
31801
  if (!global) return [];
30935
31802
  return [{
30936
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31803
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30937
31804
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30938
31805
  }];
30939
31806
  }
@@ -39505,7 +40372,7 @@ def register(ctx):
39505
40372
  _register_subagent(ctx, subagent)
39506
40373
  `;
39507
40374
  }
39508
- function getEnabledPluginConfigContent(currentContent) {
40375
+ function getEnabledPluginConfigContent({ currentContent, global }) {
39509
40376
  const config = parseSharedConfig({
39510
40377
  format: "yaml",
39511
40378
  fileContent: currentContent
@@ -39513,7 +40380,7 @@ function getEnabledPluginConfigContent(currentContent) {
39513
40380
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
39514
40381
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
39515
40382
  return applySharedConfigPatch({
39516
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40383
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
39517
40384
  feature: "subagents",
39518
40385
  existingContent: currentContent,
39519
40386
  patch: { plugins: {
@@ -39564,6 +40431,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39564
40431
  return !targets || targets.includes("*") || targets.includes("hermesagent");
39565
40432
  }
39566
40433
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40434
+ const pluginDirPath = getHermesagentRelativeDirPath({
40435
+ global,
40436
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40437
+ });
39567
40438
  return [
39568
40439
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
39569
40440
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -39572,20 +40443,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39572
40443
  global
39573
40444
  })),
39574
40445
  new HermesagentSubagent({
39575
- relativeDirPath: getHermesagentRelativeDirPath({
39576
- global,
39577
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39578
- }),
40446
+ relativeDirPath: pluginDirPath,
39579
40447
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
39580
40448
  fileContent: "",
39581
40449
  outputRoot,
39582
40450
  global
39583
40451
  }),
39584
40452
  new HermesagentSubagent({
39585
- relativeDirPath: getHermesagentRelativeDirPath({
39586
- global,
39587
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
39588
- }),
40453
+ relativeDirPath: pluginDirPath,
39589
40454
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
39590
40455
  fileContent: "",
39591
40456
  outputRoot,
@@ -39625,14 +40490,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39625
40490
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
39626
40491
  * so the write must be declared for the shared-file order derivation.
39627
40492
  */
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
- }] : [];
40493
+ /**
40494
+ * `config.yaml` under every spelling the global profile root can take.
40495
+ * @see getHermesagentSharedConfigWritePaths
40496
+ */
40497
+ static getExtraSharedWritePaths() {
40498
+ return getHermesagentSharedConfigWritePaths();
39636
40499
  }
39637
40500
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
39638
40501
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -39665,7 +40528,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39665
40528
  }
39666
40529
  setFileContent(newFileContent) {
39667
40530
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) {
39668
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40531
+ super.setFileContent(getEnabledPluginConfigContent({
40532
+ currentContent: newFileContent,
40533
+ global: this.global
40534
+ }));
39669
40535
  return;
39670
40536
  }
39671
40537
  super.setFileContent(newFileContent);
@@ -39673,7 +40539,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
39673
40539
  getFileContent() {
39674
40540
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
39675
40541
  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());
40542
+ if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40543
+ currentContent: super.getFileContent(),
40544
+ global: this.global
40545
+ });
39677
40546
  return super.getFileContent();
39678
40547
  }
39679
40548
  };
@@ -44752,10 +45621,13 @@ var JunieRule = class JunieRule extends ToolRule {
44752
45621
  //#region src/features/rules/kilo-rule.ts
44753
45622
  var KiloRule = class KiloRule extends ToolRule {
44754
45623
  static getSettablePaths({ global, excludeToolDir } = {}) {
44755
- if (global) return { root: {
44756
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
44757
- relativeFilePath: KILO_RULE_FILE_NAME
44758
- } };
45624
+ if (global) return {
45625
+ root: {
45626
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45627
+ relativeFilePath: KILO_RULE_FILE_NAME
45628
+ },
45629
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45630
+ };
44759
45631
  return {
44760
45632
  root: {
44761
45633
  relativeDirPath: ".",
@@ -46340,6 +47212,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46340
47212
  extension: "md",
46341
47213
  supportsGlobal: false,
46342
47214
  ruleDiscoveryMode: "toon",
47215
+ collisionPolicy: "compose",
46343
47216
  additionalConventions: {
46344
47217
  commands: { commandClass: AgentsmdCommand },
46345
47218
  subagents: { subagentClass: AgentsmdSubagent },
@@ -46360,7 +47233,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46360
47233
  meta: {
46361
47234
  extension: "md",
46362
47235
  supportsGlobal: true,
46363
- ruleDiscoveryMode: "toon"
47236
+ ruleDiscoveryMode: "toon",
47237
+ collisionPolicy: "compose"
46364
47238
  }
46365
47239
  }],
46366
47240
  ["antigravity-cli", {
@@ -46437,7 +47311,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46437
47311
  extension: "md",
46438
47312
  supportsGlobal: true,
46439
47313
  ruleDiscoveryMode: "auto",
46440
- foldsNonRootIntoRoot: true
47314
+ collisionPolicy: "fold"
46441
47315
  }
46442
47316
  }],
46443
47317
  ["copilot", {
@@ -46470,7 +47344,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46470
47344
  extension: "md",
46471
47345
  supportsGlobal: true,
46472
47346
  ruleDiscoveryMode: "auto",
46473
- foldsNonRootIntoRoot: true
47347
+ collisionPolicy: "fold"
46474
47348
  }
46475
47349
  }],
46476
47350
  ["factorydroid", {
@@ -46478,7 +47352,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46478
47352
  meta: {
46479
47353
  extension: "md",
46480
47354
  supportsGlobal: true,
46481
- ruleDiscoveryMode: "toon"
47355
+ ruleDiscoveryMode: "toon",
47356
+ collisionPolicy: "compose"
46482
47357
  }
46483
47358
  }],
46484
47359
  ["goose", {
@@ -46487,7 +47362,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46487
47362
  extension: "md",
46488
47363
  supportsGlobal: true,
46489
47364
  ruleDiscoveryMode: "auto",
46490
- foldsNonRootIntoRoot: true
47365
+ collisionPolicy: "fold"
46491
47366
  }
46492
47367
  }],
46493
47368
  ["hermesagent", {
@@ -46496,7 +47371,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46496
47371
  extension: "md",
46497
47372
  supportsGlobal: false,
46498
47373
  ruleDiscoveryMode: "auto",
46499
- foldsNonRootIntoRoot: true
47374
+ collisionPolicy: "fold"
46500
47375
  }
46501
47376
  }],
46502
47377
  ["grokcli", {
@@ -46513,7 +47388,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46513
47388
  extension: "md",
46514
47389
  supportsGlobal: true,
46515
47390
  ruleDiscoveryMode: "auto",
46516
- foldsNonRootIntoRoot: true
47391
+ collisionPolicy: "fold"
46517
47392
  }
46518
47393
  }],
46519
47394
  ["kilo", {
@@ -46522,7 +47397,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46522
47397
  extension: "md",
46523
47398
  supportsGlobal: true,
46524
47399
  ruleDiscoveryMode: "auto",
46525
- mcpInstructionsRegistrar: KiloMcp
47400
+ mcpInstructionsRegistrar: KiloMcp,
47401
+ collisionPolicy: "compose"
46526
47402
  }
46527
47403
  }],
46528
47404
  ["kimi-code", {
@@ -46531,7 +47407,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46531
47407
  extension: "md",
46532
47408
  supportsGlobal: true,
46533
47409
  ruleDiscoveryMode: "auto",
46534
- foldsNonRootIntoRoot: true
47410
+ collisionPolicy: "fold"
46535
47411
  }
46536
47412
  }],
46537
47413
  ["kiro", {
@@ -46564,7 +47440,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46564
47440
  extension: "md",
46565
47441
  supportsGlobal: true,
46566
47442
  ruleDiscoveryMode: "toon",
46567
- mcpInstructionsRegistrar: OpencodeMcp
47443
+ mcpInstructionsRegistrar: OpencodeMcp,
47444
+ collisionPolicy: "compose"
46568
47445
  }
46569
47446
  }],
46570
47447
  ["pi", {
@@ -46573,7 +47450,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46573
47450
  extension: "md",
46574
47451
  supportsGlobal: true,
46575
47452
  ruleDiscoveryMode: "auto",
46576
- foldsNonRootIntoRoot: true
47453
+ collisionPolicy: "fold"
46577
47454
  }
46578
47455
  }],
46579
47456
  ["qwencode", {
@@ -46591,7 +47468,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46591
47468
  extension: "md",
46592
47469
  supportsGlobal: true,
46593
47470
  ruleDiscoveryMode: "auto",
46594
- foldsNonRootIntoRoot: true
47471
+ collisionPolicy: "fold"
46595
47472
  }
46596
47473
  }],
46597
47474
  ["replit", {
@@ -46648,7 +47525,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
46648
47525
  extension: "md",
46649
47526
  supportsGlobal: false,
46650
47527
  ruleDiscoveryMode: "toon",
46651
- foldsNonRootIntoRoot: true
47528
+ collisionPolicy: "fold"
46652
47529
  }
46653
47530
  }],
46654
47531
  ["devin", {
@@ -46717,16 +47594,23 @@ var RulesProcessor = class extends FeatureProcessor {
46717
47594
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
46718
47595
  const factory = this.getFactory(this.toolTarget);
46719
47596
  const { meta } = factory;
46720
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47597
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
46721
47598
  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
- });
47599
+ return {
47600
+ toolRule: factory.class.fromRulesyncRule({
47601
+ outputRoot: this.outputRoot,
47602
+ rulesyncRule,
47603
+ validate: true,
47604
+ global: this.global
47605
+ }),
47606
+ rulesyncRule
47607
+ };
46728
47608
  }).filter((rule) => rule !== null);
46729
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47609
+ this.mergeRulesByOutputPath({
47610
+ convertedRules,
47611
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47612
+ });
47613
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
46730
47614
  this.applyLocalRootRules({
46731
47615
  toolRules,
46732
47616
  localRootRules,
@@ -46744,7 +47628,12 @@ var RulesProcessor = class extends FeatureProcessor {
46744
47628
  toolRules,
46745
47629
  factory
46746
47630
  });
46747
- return [...toolRules, ...extraFiles];
47631
+ const outputFiles = [...toolRules, ...extraFiles];
47632
+ this.warnForOutputPathCollisions({
47633
+ outputFiles,
47634
+ convertedRules
47635
+ });
47636
+ return outputFiles;
46748
47637
  }
46749
47638
  /**
46750
47639
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -46831,39 +47720,80 @@ var RulesProcessor = class extends FeatureProcessor {
46831
47720
  });
46832
47721
  }
46833
47722
  /**
46834
- * Fold every non-root rule body into the single root rule file.
47723
+ * Reconcile rules that resolve to the same output path.
46835
47724
  *
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).
47725
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47726
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47727
+ * neither scans a modular rules directory nor follows references. For example,
47728
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47729
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47730
+ * body into one instance because last-writer-wins would silently drop content.
47731
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
46842
47732
  *
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.
47733
+ * A generated root rule becomes the merge target when present. A `fold` group
47734
+ * without one uses its first rule. A group only composes when every rendered
47735
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47736
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47737
+ * Root-involved collisions that cannot be composed safely fail; other
47738
+ * collisions remain separate and are reported by the final output-path check.
47739
+ * Mutates `convertedRules` in place.
46846
47740
  */
46847
- foldNonRootRulesIntoRootRule(toolRules) {
46848
- if (toolRules.length <= 1) return;
47741
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47742
+ if (convertedRules.length <= 1) return;
46849
47743
  const groups = /* @__PURE__ */ new Map();
46850
- for (const rule of toolRules) {
46851
- const path = (0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47744
+ for (const conversion of convertedRules) {
47745
+ const path = (0, node_path.join)(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
46852
47746
  const group = groups.get(path);
46853
- if (group) group.push(rule);
46854
- else groups.set(path, [rule]);
47747
+ if (group) group.push(conversion);
47748
+ else groups.set(path, [conversion]);
46855
47749
  }
46856
47750
  const survivors = /* @__PURE__ */ new Set();
46857
- for (const group of groups.values()) {
46858
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47751
+ for (const [path, group] of groups) {
47752
+ if (group.length === 1) {
47753
+ const conversion = group[0];
47754
+ if (conversion) {
47755
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47756
+ survivors.add(conversion);
47757
+ }
47758
+ continue;
47759
+ }
47760
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47761
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47762
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47763
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47764
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47765
+ 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))}`);
47766
+ if (!shouldCompose) {
47767
+ for (const conversion of group) survivors.add(conversion);
47768
+ continue;
47769
+ }
47770
+ const target = rootConversion ?? group[0];
46859
47771
  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);
47772
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47773
+ target.toolRule.setFileContent(mergedContent);
46862
47774
  survivors.add(target);
46863
47775
  }
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);
47776
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47777
+ const conversion = convertedRules[i];
47778
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47779
+ }
47780
+ }
47781
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47782
+ const seen = /* @__PURE__ */ new Map();
47783
+ const describeSource = (file) => {
47784
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47785
+ return source ? formatRulePaths([source]) : (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47786
+ };
47787
+ for (const file of outputFiles) {
47788
+ const path = (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47789
+ const key = path.toLowerCase();
47790
+ const previous = seen.get(key);
47791
+ if (previous) {
47792
+ const previousPath = (0, node_path.join)(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47793
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47794
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47795
+ }
47796
+ seen.set(key, file);
46867
47797
  }
46868
47798
  }
46869
47799
  /**
@@ -47023,14 +47953,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
47023
47953
  }));
47024
47954
  const factory = this.getFactory(this.toolTarget);
47025
47955
  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
47956
  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
47957
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
47029
47958
  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
47959
  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
47960
  if (this.global) {
47032
47961
  const globalPaths = factory.class.getSettablePaths({ global: true });
47033
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47962
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
47034
47963
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
47035
47964
  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
47965
  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 +48202,36 @@ async function assertPluginRootSafe(params) {
47273
48202
  }
47274
48203
  //#endregion
47275
48204
  //#region src/utils/tool-output-root.ts
48205
+ /** The environment variable each tool reads for its profile root. */
48206
+ const TOOL_HOME_ENV_VARS = {
48207
+ hermesagent: "HERMES_HOME",
48208
+ "kimi-code": "KIMI_CODE_HOME"
48209
+ };
48210
+ /**
48211
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48212
+ * output root in global scope.
48213
+ *
48214
+ * The override wins over `--output-roots`: it names where the tool itself reads
48215
+ * its profile, so writing anywhere else would produce files the tool ignores.
48216
+ *
48217
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48218
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48219
+ * instead of silently becoming the output root. The rejection is re-thrown
48220
+ * naming the variable, since the user never passed an `--output-roots` flag.
48221
+ */
47276
48222
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
47277
48223
  if (!global) return outputRoot;
47278
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48224
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
47279
48225
  outputRoot,
47280
48226
  global
47281
- });
47282
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
47283
- return outputRoot;
48227
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48228
+ if (resolved === outputRoot) return resolved;
48229
+ try {
48230
+ validateOutputRoot(resolved);
48231
+ } catch (error) {
48232
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48233
+ }
48234
+ return resolved;
47284
48235
  }
47285
48236
  //#endregion
47286
48237
  //#region src/lib/convert.ts
@@ -48681,7 +49632,11 @@ async function generateChecksCore(params) {
48681
49632
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
48682
49633
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
48683
49634
  const processor = new ChecksProcessor({
48684
- outputRoot,
49635
+ outputRoot: resolveToolOutputRoot({
49636
+ outputRoot,
49637
+ toolTarget,
49638
+ global: config.getGlobal()
49639
+ }),
48685
49640
  inputRoot: config.getInputRoot(),
48686
49641
  toolTarget,
48687
49642
  global: config.getGlobal(),
@@ -49126,6 +50081,12 @@ Object.defineProperty(exports, "CODEXCLI_DIR", {
49126
50081
  return CODEXCLI_DIR;
49127
50082
  }
49128
50083
  });
50084
+ Object.defineProperty(exports, "CONFLICTING_TARGET_PAIRS", {
50085
+ enumerable: true,
50086
+ get: function() {
50087
+ return CONFLICTING_TARGET_PAIRS;
50088
+ }
50089
+ });
49129
50090
  Object.defineProperty(exports, "ChecksProcessor", {
49130
50091
  enumerable: true,
49131
50092
  get: function() {
@@ -49156,12 +50117,24 @@ Object.defineProperty(exports, "ConsoleLogger", {
49156
50117
  return ConsoleLogger;
49157
50118
  }
49158
50119
  });
50120
+ Object.defineProperty(exports, "DEPRECATED_FEATURE_REPLACEMENTS", {
50121
+ enumerable: true,
50122
+ get: function() {
50123
+ return DEPRECATED_FEATURE_REPLACEMENTS;
50124
+ }
50125
+ });
49159
50126
  Object.defineProperty(exports, "ErrorCodes", {
49160
50127
  enumerable: true,
49161
50128
  get: function() {
49162
50129
  return ErrorCodes;
49163
50130
  }
49164
50131
  });
50132
+ Object.defineProperty(exports, "GITIGNORE_DESTINATION_KEY", {
50133
+ enumerable: true,
50134
+ get: function() {
50135
+ return GITIGNORE_DESTINATION_KEY;
50136
+ }
50137
+ });
49165
50138
  Object.defineProperty(exports, "HooksProcessor", {
49166
50139
  enumerable: true,
49167
50140
  get: function() {