rulesync 16.0.0 → 16.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { ZodError } from "zod";
2
2
  import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
3
3
  import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
4
- import path, { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
4
+ import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
5
5
  import { parse, printParseErrorCode } from "jsonc-parser";
6
6
  import os from "node:os";
7
7
  import { intersection, kebabCase, uniq } from "es-toolkit";
@@ -192,6 +192,7 @@ const ignoreProcessorToolTargetTuple = [
192
192
  "kiro-cli",
193
193
  "kiro-ide",
194
194
  "qwencode",
195
+ "reasonix",
195
196
  "roo",
196
197
  "devin",
197
198
  "vibe",
@@ -249,6 +250,7 @@ const commandsProcessorToolTargetTuple = [
249
250
  "cursor",
250
251
  "factorydroid",
251
252
  "goose",
253
+ "grokcli",
252
254
  "hermesagent",
253
255
  "junie",
254
256
  "kilo",
@@ -268,6 +270,9 @@ const commandsProcessorToolTargetTuple = [
268
270
  const subagentsProcessorToolTargetTuple = [
269
271
  "kilo",
270
272
  "agentsmd",
273
+ "antigravity-cli",
274
+ "antigravity-ide",
275
+ "antigravity-plugin",
271
276
  "augmentcode",
272
277
  "claudecode",
273
278
  "claudecode-plugin",
@@ -399,7 +404,9 @@ const permissionsProcessorToolTargetTuple = [
399
404
  ];
400
405
  const checksProcessorToolTargetTuple = [
401
406
  "amp",
407
+ "cursor",
402
408
  "hermesagent",
409
+ "rovodev",
403
410
  "takt"
404
411
  ];
405
412
  //#endregion
@@ -1053,6 +1060,17 @@ const ConfigFileSchema = z.object({
1053
1060
  });
1054
1061
  z.required(ConfigParamsSchema);
1055
1062
  /**
1063
+ * Normalizes the configuration file location to an absolute path.
1064
+ *
1065
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1066
+ * only covers direct programmatic construction, where the conventional
1067
+ * location next to the input root is the best guess.
1068
+ */
1069
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1070
+ if (configFilePath === void 0) return join(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1071
+ return isAbsolute(configFilePath) ? configFilePath : resolve(configFilePath);
1072
+ }
1073
+ /**
1056
1074
  * Conflicting target pairs that cannot be used together
1057
1075
  */
1058
1076
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
@@ -1123,8 +1141,9 @@ var Config = class Config {
1123
1141
  dryRun;
1124
1142
  check;
1125
1143
  inputRoot;
1144
+ configFilePath;
1126
1145
  sources;
1127
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1146
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1128
1147
  assertTargetsFeaturesExclusive({
1129
1148
  targets,
1130
1149
  features
@@ -1157,6 +1176,10 @@ var Config = class Config {
1157
1176
  this.dryRun = dryRun ?? false;
1158
1177
  this.check = check ?? false;
1159
1178
  this.inputRoot = inputRoot === void 0 ? process.cwd() : isAbsolute(inputRoot) ? inputRoot : resolve(inputRoot);
1179
+ this.configFilePath = normalizeConfigFilePath({
1180
+ configFilePath,
1181
+ inputRoot: this.inputRoot
1182
+ });
1160
1183
  this.sources = sources ?? [];
1161
1184
  }
1162
1185
  /**
@@ -1350,6 +1373,14 @@ var Config = class Config {
1350
1373
  getInputRoot() {
1351
1374
  return this.inputRoot;
1352
1375
  }
1376
+ /**
1377
+ * Returns the absolute path of the configuration file this config was
1378
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1379
+ * without one — so callers must treat this as a location, not a guarantee.
1380
+ */
1381
+ getConfigFilePath() {
1382
+ return this.configFilePath;
1383
+ }
1353
1384
  getSources() {
1354
1385
  return this.sources;
1355
1386
  }
@@ -1572,6 +1603,7 @@ var ConfigResolver = class {
1572
1603
  fallback: getDefaults().check
1573
1604
  }),
1574
1605
  inputRoot: resolvedInputRoot !== void 0 ? resolve(resolvedInputRoot) : cwd,
1606
+ configFilePath: validatedConfigPath,
1575
1607
  sources: configByFile.sources ?? getDefaults().sources,
1576
1608
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1577
1609
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -2109,7 +2141,10 @@ const HookDefinitionSchema = z.looseObject({
2109
2141
  model: z.optional(safeString),
2110
2142
  args: z.optional(z.array(safeString)),
2111
2143
  metadata: z.optional(z.looseObject({})),
2112
- if: z.optional(safeString)
2144
+ if: z.optional(safeString),
2145
+ commandWindows: z.optional(safeString),
2146
+ asyncRewake: z.optional(z.boolean()),
2147
+ continueOnBlock: z.optional(z.boolean())
2113
2148
  });
2114
2149
  /**
2115
2150
  * All canonical hook event names.
@@ -2163,6 +2198,7 @@ const HOOK_EVENTS = [
2163
2198
  "configChange",
2164
2199
  "cwdChanged",
2165
2200
  "fileChanged",
2201
+ "directoryAdded",
2166
2202
  "elicitation",
2167
2203
  "elicitationResult"
2168
2204
  ];
@@ -2224,6 +2260,7 @@ const CLAUDE_HOOK_EVENTS = [
2224
2260
  "configChange",
2225
2261
  "cwdChanged",
2226
2262
  "fileChanged",
2263
+ "directoryAdded",
2227
2264
  "postCompact",
2228
2265
  "elicitation",
2229
2266
  "elicitationResult"
@@ -2256,7 +2293,15 @@ const DEVIN_HOOK_EVENTS = [
2256
2293
  "permissionRequest",
2257
2294
  "postCompact"
2258
2295
  ];
2259
- /** Hook events supported by OpenCode. */
2296
+ /**
2297
+ * Hook events supported by OpenCode.
2298
+ *
2299
+ * `preCompact` maps to `experimental.session.compacting`, which the plugin docs
2300
+ * document as a named `(input, output)` hook rather than an `event.type`
2301
+ * dispatch; the other entries are all generic events.
2302
+ *
2303
+ * @see https://opencode.ai/docs/plugins/
2304
+ */
2260
2305
  const OPENCODE_HOOK_EVENTS = [
2261
2306
  "sessionStart",
2262
2307
  "preToolUse",
@@ -2264,9 +2309,19 @@ const OPENCODE_HOOK_EVENTS = [
2264
2309
  "stop",
2265
2310
  "afterFileEdit",
2266
2311
  "afterShellExecution",
2267
- "permissionRequest"
2312
+ "permissionRequest",
2313
+ "preCompact",
2314
+ "postCompact",
2315
+ "afterError",
2316
+ "fileChanged"
2268
2317
  ];
2269
- /** Hook events supported by Kilo. (Currently identical to OpenCode) */
2318
+ /**
2319
+ * Hook events supported by Kilo. Identical to OpenCode: Kilo's plugin docs list
2320
+ * the same event surface, including `session.compacted`, `session.error`,
2321
+ * `file.watcher.updated` and the experimental compaction hook.
2322
+ *
2323
+ * @see https://kilo.ai/docs/automate/extending/plugins
2324
+ */
2270
2325
  const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
2271
2326
  /**
2272
2327
  * Hook events supported by Pi Coding Agent, bridged through a generated
@@ -2333,7 +2388,8 @@ const COPILOT_HOOK_EVENTS = [
2333
2388
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
2334
2389
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
2335
2390
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
2336
- * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
2391
+ * `notification`, `userPromptTransformed` ← `userPromptExpansion`,
2392
+ * `preMcpToolCall` ← `beforeMCPExecution`.
2337
2393
  *
2338
2394
  * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
2339
2395
  * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
@@ -2355,6 +2411,7 @@ const COPILOTCLI_HOOK_EVENTS = [
2355
2411
  "preCompact",
2356
2412
  "permissionRequest",
2357
2413
  "notification",
2414
+ "userPromptExpansion",
2358
2415
  "beforeMCPExecution"
2359
2416
  ];
2360
2417
  /**
@@ -2399,6 +2456,7 @@ const DEEPAGENTS_HOOK_EVENTS = [
2399
2456
  /** Hook events supported by Codex CLI. */
2400
2457
  const CODEXCLI_HOOK_EVENTS = [
2401
2458
  "sessionStart",
2459
+ "sessionEnd",
2402
2460
  "preToolUse",
2403
2461
  "postToolUse",
2404
2462
  "beforeSubmitPrompt",
@@ -2833,6 +2891,7 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
2833
2891
  configChange: "ConfigChange",
2834
2892
  cwdChanged: "CwdChanged",
2835
2893
  fileChanged: "FileChanged",
2894
+ directoryAdded: "DirectoryAdded",
2836
2895
  postCompact: "PostCompact",
2837
2896
  elicitation: "Elicitation",
2838
2897
  elicitationResult: "ElicitationResult"
@@ -2954,7 +3013,11 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
2954
3013
  stop: "session.idle",
2955
3014
  afterFileEdit: "file.edited",
2956
3015
  afterShellExecution: "command.executed",
2957
- permissionRequest: "permission.asked"
3016
+ permissionRequest: "permission.asked",
3017
+ preCompact: "experimental.session.compacting",
3018
+ postCompact: "session.compacted",
3019
+ afterError: "session.error",
3020
+ fileChanged: "file.watcher.updated"
2958
3021
  };
2959
3022
  /**
2960
3023
  * Map canonical camelCase event names to Kilo dot-notation.
@@ -3030,6 +3093,7 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
3030
3093
  preCompact: "preCompact",
3031
3094
  permissionRequest: "permissionRequest",
3032
3095
  notification: "notification",
3096
+ userPromptExpansion: "userPromptTransformed",
3033
3097
  beforeMCPExecution: "preMcpToolCall"
3034
3098
  };
3035
3099
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
@@ -3039,6 +3103,7 @@ const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
3039
3103
  */
3040
3104
  const CANONICAL_TO_CODEXCLI_EVENT_NAMES = {
3041
3105
  sessionStart: "SessionStart",
3106
+ sessionEnd: "SessionEnd",
3042
3107
  preToolUse: "PreToolUse",
3043
3108
  postToolUse: "PostToolUse",
3044
3109
  beforeSubmitPrompt: "UserPromptSubmit",
@@ -3461,6 +3526,25 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
3461
3526
  //#endregion
3462
3527
  //#region src/types/mcp.ts
3463
3528
  const EnvVarNameSchema = z.string().check(refine((value) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value), "envVars entries must be valid environment variable names"));
3529
+ /**
3530
+ * One `envVars` entry. A bare name reads the variable from Codex's own
3531
+ * environment; the object form names the environment to read it from, where
3532
+ * `source = "remote"` reads from the remote executor environment.
3533
+ * @see https://learn.chatgpt.com/docs/extend/mcp
3534
+ */
3535
+ const EnvVarEntrySchema = z.union([EnvVarNameSchema, z.strictObject({
3536
+ name: EnvVarNameSchema,
3537
+ source: z.optional(z.enum(["local", "remote"]))
3538
+ })]);
3539
+ /**
3540
+ * Whether a value is usable as `envVars`. Applied in both directions by the
3541
+ * codex adapter, so an entry read out of somebody's `config.toml` can never be
3542
+ * imported into a `.rulesync/mcp.jsonc` that the next generate would refuse to
3543
+ * parse.
3544
+ */
3545
+ function isEnvVarEntryArray(value) {
3546
+ return Array.isArray(value) && value.every((entry) => EnvVarEntrySchema.safeParse(entry).success);
3547
+ }
3464
3548
  const McpServerSchema = z.looseObject({
3465
3549
  type: z.optional(z.enum([
3466
3550
  "local",
@@ -3475,7 +3559,8 @@ const McpServerSchema = z.looseObject({
3475
3559
  url: z.optional(z.string()),
3476
3560
  httpUrl: z.optional(z.string()),
3477
3561
  env: z.optional(z.record(z.string(), z.string())),
3478
- envVars: z.optional(z.array(EnvVarNameSchema)),
3562
+ envVars: z.optional(z.array(EnvVarEntrySchema)),
3563
+ experimentalEnvironment: z.optional(z.string()),
3479
3564
  disabled: z.optional(z.boolean()),
3480
3565
  networkTimeout: z.optional(z.number()),
3481
3566
  timeout: z.optional(z.number()),
@@ -3641,6 +3726,8 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3641
3726
  "description",
3642
3727
  "exposed",
3643
3728
  "envVars",
3729
+ "experimentalEnvironment",
3730
+ "experimental_environment",
3644
3731
  "enabled"
3645
3732
  ])];
3646
3733
  }));
@@ -3927,10 +4014,30 @@ const ClinePermissionsOverrideSchema = z.looseObject({
3927
4014
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
3928
4015
  * override; each value may be a bare action string or a pattern map.
3929
4016
  *
4017
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4018
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4019
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4020
+ * `writable_paths`. It has no canonical permission category, so it is authored
4021
+ * here and emitted only for Kilo.
4022
+ *
4023
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4024
+ * `writable_paths` are honored from the global config only, and a project
4025
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4026
+ * project-level network denial even clears the global destination exceptions.
4027
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4028
+ * written, and the rest are dropped with a warning rather than emitted into a
4029
+ * file Kilo would ignore.
4030
+ *
3930
4031
  * @example
3931
4032
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4033
+ * @example
4034
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4035
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
3932
4036
  */
3933
- const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
4037
+ const KiloPermissionsOverrideSchema = z.looseObject({
4038
+ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)),
4039
+ sandbox: z.optional(z.looseObject({}))
4040
+ });
3934
4041
  /**
3935
4042
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
3936
4043
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -3943,12 +4050,23 @@ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
3943
4050
  * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
3944
4051
  * ignored here (rulesync owns them).
3945
4052
  *
4053
+ * `sandbox` is the sibling top-level settings subtree that governs the sandbox
4054
+ * Claude Code runs commands in (`sandbox.network.*`, `sandbox.filesystem.*`,
4055
+ * `sandbox.credentials`, `sandbox.allowAppleEvents`, ...). It has no canonical
4056
+ * permission category either — it constrains how a permitted command runs
4057
+ * rather than which commands are permitted — so it is a loose passthrough on
4058
+ * the same terms, merged into the top level of `.claude/settings.json`.
4059
+ *
3946
4060
  * @example
3947
4061
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
4062
+ * @example
4063
+ * { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
4064
+ * @see https://code.claude.com/docs/en/sandboxing
3948
4065
  */
3949
4066
  const ClaudecodePermissionsOverrideSchema = z.looseObject({
3950
4067
  permission: z.optional(ToolScopedPermissionSchema),
3951
- permissions: z.optional(z.looseObject({}))
4068
+ permissions: z.optional(z.looseObject({})),
4069
+ sandbox: z.optional(z.looseObject({}))
3952
4070
  });
3953
4071
  /**
3954
4072
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
@@ -4221,19 +4339,24 @@ const AmpPermissionsOverrideSchema = z.looseObject({
4221
4339
  });
4222
4340
  /**
4223
4341
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4224
- * `settings.json` carries two global autonomy/sandbox knobs outside the
4342
+ * `settings.json` carries four global autonomy/sandbox knobs outside the
4225
4343
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4226
4344
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4227
- * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
4228
- * agent-run commands to OS containment). Antigravity applies the allow/deny
4345
+ * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
4346
+ * agent-run commands to OS containment), `artifactReviewPolicy` (whether the
4347
+ * agent's artifact changes are gated on a review prompt — `asks-for-review`
4348
+ * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4349
+ * (a boolean, off by default, letting the agent read or write files outside the
4350
+ * active workspace roots). Antigravity applies the allow/deny
4229
4351
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4230
4352
  * authors these keys verbatim — no precedence modeling is needed on our side.
4231
4353
  * Fields placed here are merged onto the top level of
4232
4354
  * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
4233
4355
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4234
4356
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4235
- * Verified against https://antigravity.google/docs/cli/reference and
4236
- * https://antigravity.google/docs/cli/sandbox.
4357
+ * Verified against https://antigravity.google/docs/cli/reference,
4358
+ * https://antigravity.google/docs/cli/sandbox and
4359
+ * https://antigravity.google/docs/cli/settings.
4237
4360
  *
4238
4361
  * @example
4239
4362
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
@@ -4246,7 +4369,13 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
4246
4369
  "always-proceed",
4247
4370
  "strict"
4248
4371
  ])),
4249
- enableTerminalSandbox: z.optional(z.boolean())
4372
+ enableTerminalSandbox: z.optional(z.boolean()),
4373
+ artifactReviewPolicy: z.optional(z.enum([
4374
+ "asks-for-review",
4375
+ "agent-decides",
4376
+ "always-proceed"
4377
+ ])),
4378
+ allowNonWorkspaceAccess: z.optional(z.boolean())
4250
4379
  });
4251
4380
  /**
4252
4381
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -4650,11 +4779,14 @@ const RulesyncRuleFrontmatterSchema = z.object({
4650
4779
  description: z.optional(z.string()),
4651
4780
  globs: z.optional(z.array(z.string()))
4652
4781
  })),
4653
- copilot: z.optional(z.looseObject({ excludeAgent: z.optional(z.union([
4654
- z.literal("code-review"),
4655
- z.literal("cloud-agent"),
4656
- z.literal("coding-agent")
4657
- ])) })),
4782
+ copilot: z.optional(z.looseObject({
4783
+ excludeAgent: z.optional(z.union([
4784
+ z.literal("code-review"),
4785
+ z.literal("cloud-agent"),
4786
+ z.literal("coding-agent")
4787
+ ])),
4788
+ name: z.optional(z.string())
4789
+ })),
4658
4790
  antigravity: z.optional(z.looseObject({
4659
4791
  trigger: z.optional(z.string()),
4660
4792
  globs: z.optional(z.array(z.string()))
@@ -4862,6 +4994,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
4862
4994
  arguments: z.optional(z.union([z.string(), z.array(z.string())])),
4863
4995
  context: z.optional(z.string()),
4864
4996
  agent: z.optional(z.string()),
4997
+ background: z.optional(z.boolean()),
4865
4998
  hooks: z.optional(z.looseObject({})),
4866
4999
  shell: z.optional(z.string()),
4867
5000
  "disable-model-invocation": z.optional(z.boolean()),
@@ -4913,7 +5046,9 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
4913
5046
  copilotcli: z.optional(z.looseObject({
4914
5047
  license: z.optional(z.string()),
4915
5048
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
4916
- "argument-hint": z.optional(z.string())
5049
+ "argument-hint": z.optional(z.string()),
5050
+ "user-invocable": z.optional(z.boolean()),
5051
+ "disable-model-invocation": z.optional(z.boolean())
4917
5052
  })),
4918
5053
  pi: z.optional(z.looseObject({
4919
5054
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
@@ -5199,6 +5334,38 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5199
5334
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5200
5335
  }
5201
5336
  //#endregion
5337
+ //#region src/constants/cursor-paths.ts
5338
+ const CURSOR_DIR = ".cursor";
5339
+ const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
5340
+ const CURSOR_SKILLS_DIR_PATH = join(CURSOR_DIR, "skills");
5341
+ const CURSOR_AGENTS_DIR_PATH = join(CURSOR_DIR, "agents");
5342
+ const CURSOR_BUGBOT_FILE_NAME = "BUGBOT.md";
5343
+ const CURSOR_MCP_FILE_NAME = "mcp.json";
5344
+ const CURSOR_HOOKS_FILE_NAME = "hooks.json";
5345
+ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5346
+ const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5347
+ const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5348
+ //#endregion
5349
+ //#region src/constants/rovodev-paths.ts
5350
+ const ROVODEV_DIR = ".rovodev";
5351
+ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
5352
+ const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
5353
+ const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
5354
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5355
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5356
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5357
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5358
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
5359
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5360
+ const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
5361
+ /**
5362
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5363
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5364
+ * dot in the file name.
5365
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5366
+ */
5367
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5368
+ //#endregion
5202
5369
  //#region src/constants/takt-paths.ts
5203
5370
  const TAKT_DIR = ".takt";
5204
5371
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5649,6 +5816,276 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5649
5816
  }
5650
5817
  };
5651
5818
  //#endregion
5819
+ //#region src/features/checks/check-slug.ts
5820
+ /**
5821
+ * Turn a name that came out of a tool's own config file into one safe to use as
5822
+ * a `.rulesync/checks/<name>.md` file name. Shared by the adapters whose checks
5823
+ * collapse into a single file, since the name they read back is whatever the
5824
+ * user wrote there.
5825
+ */
5826
+ function slugifyCheckName(value) {
5827
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5828
+ }
5829
+ //#endregion
5830
+ //#region src/features/checks/aggregated-check-file.ts
5831
+ /**
5832
+ * Shared machinery for the tools whose checks surface is **one aggregated
5833
+ * instruction file** rather than a file per check — Cursor Bugbot's
5834
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5835
+ *
5836
+ * Both read the file as free prose, so the check identities have to be carried
5837
+ * in something invisible to the reader: an HTML-comment marker per section.
5838
+ * That marker convention, the escaping that keeps a check body from splitting
5839
+ * itself, and the import-side split are the same for both files, so they live
5840
+ * here once.
5841
+ */
5842
+ /** Marks where one check starts inside the single instruction file. */
5843
+ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5844
+ /**
5845
+ * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
5846
+ * code block, say. Emitting it verbatim would split that check in two on the
5847
+ * next import, so `literal-` is inserted before `check:` on the way out and
5848
+ * taken off on the way back. `(?:literal-)*` makes it a ladder, so a body that
5849
+ * already contains an escaped marker survives the round trip too.
5850
+ */
5851
+ const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5852
+ const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5853
+ function renderCheckMarker(name) {
5854
+ return `<!-- rulesync:check:${name} -->`;
5855
+ }
5856
+ function escapeCheckMarkers(content) {
5857
+ return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5858
+ }
5859
+ function unescapeCheckMarkers(content) {
5860
+ return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5861
+ }
5862
+ function findCheckMarkers(fileContent) {
5863
+ CHECK_MARKER_PATTERN.lastIndex = 0;
5864
+ const markers = [];
5865
+ let match = CHECK_MARKER_PATTERN.exec(fileContent);
5866
+ while (match !== null) {
5867
+ markers.push({
5868
+ name: match[1] ?? "",
5869
+ start: match.index,
5870
+ end: match.index + match[0].length
5871
+ });
5872
+ match = CHECK_MARKER_PATTERN.exec(fileContent);
5873
+ }
5874
+ return markers;
5875
+ }
5876
+ /**
5877
+ * Whether the file holds instruction text ahead of the first marker — the
5878
+ * question the "generating replaces this" warning asks. A file with no marker
5879
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5880
+ * since there is nothing to replace.
5881
+ */
5882
+ function hasHandWrittenPreamble(fileContent) {
5883
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5884
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5885
+ }
5886
+ /**
5887
+ * Whether the file is nothing but sections rulesync generated — the question
5888
+ * the deletion guard asks, and a stricter one than
5889
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5890
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5891
+ * empty one is somebody's placeholder rather than our leftover.
5892
+ */
5893
+ function isOnlyGeneratedSections(fileContent) {
5894
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5895
+ if (firstMarkerStart === void 0) return false;
5896
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5897
+ }
5898
+ /**
5899
+ * The instruction text one check contributes. Neither file has a field to put a
5900
+ * summary in, so `description` is used only when there is no body.
5901
+ */
5902
+ function toInstruction(rulesyncCheck) {
5903
+ const body = rulesyncCheck.getBody().trim();
5904
+ if (body.length > 0) return body;
5905
+ return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5906
+ }
5907
+ function renderCheckSection(rulesyncCheck) {
5908
+ const name = basename(rulesyncCheck.getRelativeFilePath(), ".md");
5909
+ const heading = `## ${name}`;
5910
+ const instruction = toInstruction(rulesyncCheck);
5911
+ const lines = [renderCheckMarker(name), heading];
5912
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5913
+ return lines.join("\n");
5914
+ }
5915
+ function renderCheckFile(rulesyncChecks) {
5916
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5917
+ }
5918
+ /** Drop the heading generate writes, so a round trip does not stack headings. */
5919
+ function stripGeneratedHeading(section, name) {
5920
+ const [firstLine, ...rest] = section.split("\n");
5921
+ if (firstLine?.trim() === `## ${name}`) return rest.join("\n").trim();
5922
+ return section.trim();
5923
+ }
5924
+ /**
5925
+ * Split an aggregated instruction file back into one check per section.
5926
+ *
5927
+ * Content ahead of the first marker — and a hand-written file with no markers
5928
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5929
+ * is dropped.
5930
+ */
5931
+ function splitCheckFile({ fileContent, fallbackName }) {
5932
+ const sections = [];
5933
+ const markers = findCheckMarkers(fileContent);
5934
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5935
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5936
+ if (preamble.length > 0) sections.push({
5937
+ name: fallbackName,
5938
+ content: unescapeCheckMarkers(preamble)
5939
+ });
5940
+ for (const [index, marker] of markers.entries()) {
5941
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5942
+ const markerName = marker.name.trim();
5943
+ const name = slugifyCheckName(markerName) || fallbackName;
5944
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5945
+ sections.push({
5946
+ name,
5947
+ content: unescapeCheckMarkers(content)
5948
+ });
5949
+ }
5950
+ const used = /* @__PURE__ */ new Set();
5951
+ return sections.map(({ name, content }) => {
5952
+ let uniqueName = name;
5953
+ let suffix = 2;
5954
+ while (used.has(uniqueName)) {
5955
+ uniqueName = `${name}-${suffix}`;
5956
+ suffix += 1;
5957
+ }
5958
+ used.add(uniqueName);
5959
+ return new RulesyncCheck({
5960
+ outputRoot: ".",
5961
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5962
+ relativeFilePath: `${uniqueName}.md`,
5963
+ frontmatter: { targets: ["*"] },
5964
+ body: content
5965
+ });
5966
+ });
5967
+ }
5968
+ //#endregion
5969
+ //#region src/features/checks/cursor-check.ts
5970
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
5971
+ /**
5972
+ * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5973
+ *
5974
+ * Bugbot takes one aggregated instruction file per directory rather than a file
5975
+ * per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
5976
+ * the repository-root `.cursor/BUGBOT.md` — hence {@link fromRulesyncChecks}
5977
+ * rather than the usual per-check conversion. Each check becomes one section:
5978
+ * an HTML-comment marker carrying the check name, an `## <name>` heading, and
5979
+ * the check body as the instruction text (the `description` is used when the
5980
+ * body is empty).
5981
+ *
5982
+ * Bugbot reads the file as free prose, so a check's `severity` and `tools` have
5983
+ * no equivalent there: they are not written and do not come back on import. So
5984
+ * is `description` whenever the check also has a body.
5985
+ *
5986
+ * Project scope only — Bugbot reads repository files, and there is no
5987
+ * user-level instruction file. Bugbot also merges nested `<dir>/.cursor/BUGBOT.md`
5988
+ * files found while traversing upward from changed files, but rulesync check
5989
+ * sources carry no directory-placement semantics, so only the root file is
5990
+ * generated.
5991
+ *
5992
+ * On import the markers split the file back into one check per section; content
5993
+ * before the first marker — and a hand-written file with no markers at all —
5994
+ * becomes a single `bugbot` check, so nothing in the file is dropped. A file
5995
+ * holding anything rulesync did not write is never deleted either (see
5996
+ * {@link canDeleteAuxiliaryFiles}), though generating checks for Cursor does
5997
+ * replace it — import first to keep what is there, which is warned about.
5998
+ *
5999
+ * @see https://cursor.com/docs/bugbot
6000
+ */
6001
+ var CursorCheck = class CursorCheck extends ToolCheck {
6002
+ static getSettablePaths(_options = {}) {
6003
+ return {
6004
+ relativeDirPath: CURSOR_DIR,
6005
+ relativeFilePath: CURSOR_BUGBOT_FILE_NAME
6006
+ };
6007
+ }
6008
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6009
+ return this.isTargetedByRulesyncCheckDefault({
6010
+ rulesyncCheck,
6011
+ toolTarget: "cursor"
6012
+ });
6013
+ }
6014
+ /**
6015
+ * Ownership guard the processor consults before it deletes anything for this
6016
+ * tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
6017
+ * to hand-write, so anything in it that rulesync did not write is not
6018
+ * rulesync's to remove — dropping the last check targeting Cursor must not
6019
+ * take somebody's hand-written review instructions with it. Deletion is
6020
+ * therefore allowed only for a file that is nothing but generated sections:
6021
+ * one that carries no marker at all, or that carries hand-written text ahead
6022
+ * of the first marker, stays.
6023
+ */
6024
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6025
+ const paths = CursorCheck.getSettablePaths();
6026
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
6027
+ if (fileContent === null) return true;
6028
+ return isOnlyGeneratedSections(fileContent);
6029
+ }
6030
+ static fromRulesyncCheck(_params) {
6031
+ throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
6032
+ }
6033
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6034
+ if (rulesyncChecks.length === 0) return [];
6035
+ const paths = CursorCheck.getSettablePaths({ global });
6036
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
6037
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6038
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
6039
+ const fileContent = renderCheckFile(rulesyncChecks);
6040
+ return [new CursorCheck({
6041
+ outputRoot,
6042
+ relativeDirPath: paths.relativeDirPath,
6043
+ relativeFilePath,
6044
+ fileContent,
6045
+ global
6046
+ })];
6047
+ }
6048
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6049
+ const paths = CursorCheck.getSettablePaths({ global });
6050
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
6051
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6052
+ return new CursorCheck({
6053
+ outputRoot,
6054
+ relativeDirPath: paths.relativeDirPath,
6055
+ relativeFilePath,
6056
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6057
+ global
6058
+ });
6059
+ }
6060
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6061
+ return new CursorCheck({
6062
+ outputRoot,
6063
+ relativeDirPath,
6064
+ relativeFilePath,
6065
+ fileContent: "",
6066
+ validate: false,
6067
+ global
6068
+ });
6069
+ }
6070
+ validate() {
6071
+ return {
6072
+ success: true,
6073
+ error: null
6074
+ };
6075
+ }
6076
+ toRulesyncCheck() {
6077
+ const first = this.toRulesyncChecks()[0];
6078
+ if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6079
+ return first;
6080
+ }
6081
+ toRulesyncChecks() {
6082
+ return splitCheckFile({
6083
+ fileContent: this.getFileContent(),
6084
+ fallbackName: FALLBACK_CHECK_NAME$1
6085
+ });
6086
+ }
6087
+ };
6088
+ //#endregion
5652
6089
  //#region src/constants/hermesagent-paths.ts
5653
6090
  /**
5654
6091
  * Hermes Agent configuration-layout conventions.
@@ -5666,8 +6103,22 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5666
6103
  */
5667
6104
  /** Project-root instruction file auto-injected by Hermes Agent. */
5668
6105
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
5669
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6106
+ /**
6107
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6108
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6109
+ */
5670
6110
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6111
+ /**
6112
+ * Home-relative global profile root on Windows: upstream defaults to
6113
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6114
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6115
+ *
6116
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6117
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6118
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6119
+ * followed; those users should set `HERMES_HOME` explicitly.
6120
+ */
6121
+ const HERMESAGENT_GLOBAL_WIN32_DIR = join("AppData", "Local", "hermes");
5671
6122
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
5672
6123
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
5673
6124
  const HERMESAGENT_CONFIG_FILE_PATH = join(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -5890,6 +6341,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
5890
6341
  }
5891
6342
  };
5892
6343
  //#endregion
6344
+ //#region src/features/checks/rovodev-check.ts
6345
+ const FALLBACK_CHECK_NAME = "review-agent";
6346
+ /**
6347
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6348
+ * (`.rovodev/.review-agent.md`).
6349
+ *
6350
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6351
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6352
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6353
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6354
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6355
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6356
+ * adapters share).
6357
+ *
6358
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6359
+ * have no equivalent there: they are not written and do not come back on
6360
+ * import. Neither does `description` whenever the check also has a body.
6361
+ *
6362
+ * Project scope only — these are per-repository review instructions, and Rovo
6363
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6364
+ * same tool is the opposite: global only.)
6365
+ *
6366
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6367
+ */
6368
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6369
+ static getSettablePaths(_options = {}) {
6370
+ return {
6371
+ relativeDirPath: ROVODEV_DIR,
6372
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6373
+ };
6374
+ }
6375
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6376
+ return this.isTargetedByRulesyncCheckDefault({
6377
+ rulesyncCheck,
6378
+ toolTarget: "rovodev"
6379
+ });
6380
+ }
6381
+ /**
6382
+ * Ownership guard the processor consults before it deletes anything for this
6383
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6384
+ * to hand-write, so anything in it that rulesync did not write is not
6385
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6386
+ * take somebody's hand-written review instructions with it.
6387
+ */
6388
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6389
+ const paths = RovodevCheck.getSettablePaths();
6390
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6391
+ if (fileContent === null) return true;
6392
+ return isOnlyGeneratedSections(fileContent);
6393
+ }
6394
+ static fromRulesyncCheck(_params) {
6395
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6396
+ }
6397
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6398
+ if (rulesyncChecks.length === 0) return [];
6399
+ const paths = RovodevCheck.getSettablePaths({ global });
6400
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6401
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6402
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Rovo Dev checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets rovodev --features checks\` first to keep them.`);
6403
+ return [new RovodevCheck({
6404
+ outputRoot,
6405
+ relativeDirPath: paths.relativeDirPath,
6406
+ relativeFilePath,
6407
+ fileContent: renderCheckFile(rulesyncChecks),
6408
+ global
6409
+ })];
6410
+ }
6411
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6412
+ const paths = RovodevCheck.getSettablePaths({ global });
6413
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6414
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
6415
+ return new RovodevCheck({
6416
+ outputRoot,
6417
+ relativeDirPath: paths.relativeDirPath,
6418
+ relativeFilePath,
6419
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6420
+ global
6421
+ });
6422
+ }
6423
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6424
+ return new RovodevCheck({
6425
+ outputRoot,
6426
+ relativeDirPath,
6427
+ relativeFilePath,
6428
+ fileContent: "",
6429
+ validate: false,
6430
+ global
6431
+ });
6432
+ }
6433
+ validate() {
6434
+ return {
6435
+ success: true,
6436
+ error: null
6437
+ };
6438
+ }
6439
+ toRulesyncCheck() {
6440
+ const first = this.toRulesyncChecks()[0];
6441
+ if (!first) throw new Error(`No check instructions found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6442
+ return first;
6443
+ }
6444
+ toRulesyncChecks() {
6445
+ return splitCheckFile({
6446
+ fileContent: this.getFileContent(),
6447
+ fallbackName: FALLBACK_CHECK_NAME
6448
+ });
6449
+ }
6450
+ };
6451
+ //#endregion
5893
6452
  //#region src/constants/codexcli-paths.ts
5894
6453
  const CODEXCLI_DIR = ".codex";
5895
6454
  const CODEXCLI_PROMPTS_DIR_PATH = join(CODEXCLI_DIR, "prompts");
@@ -6002,11 +6561,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6002
6561
  }
6003
6562
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6004
6563
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6564
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6565
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6005
6566
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6006
6567
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6007
6568
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6008
6569
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6009
6570
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6571
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6010
6572
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6011
6573
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6012
6574
  /**
@@ -6030,6 +6592,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6030
6592
  * lock-step with the writers derived from the processor registry, so an
6031
6593
  * undeclared writer fails CI instead of merging by accident.
6032
6594
  */
6595
+ /**
6596
+ * Hermes writes one `config.yaml`, but its global profile root has three
6597
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6598
+ * itself). They are the same file with the same owners, so the declaration is
6599
+ * written once and shared — a policy edit cannot land on one spelling only.
6600
+ */
6601
+ const HERMES_CONFIG_DECLARATION = {
6602
+ format: "yaml",
6603
+ features: {
6604
+ commands: {
6605
+ kind: "replace-owned-keys",
6606
+ ownedKeys: ["plugins"]
6607
+ },
6608
+ subagents: {
6609
+ kind: "replace-owned-keys",
6610
+ ownedKeys: ["plugins"]
6611
+ },
6612
+ mcp: {
6613
+ kind: "replace-owned-keys",
6614
+ ownedKeys: ["mcp_servers"]
6615
+ },
6616
+ hooks: {
6617
+ kind: "replace-owned-keys",
6618
+ ownedKeys: ["hooks"]
6619
+ },
6620
+ permissions: {
6621
+ kind: "deep-merge",
6622
+ replaceKeys: ["permissions"]
6623
+ }
6624
+ }
6625
+ };
6626
+ /**
6627
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6628
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6629
+ * name the profile directory itself, so the file has two spellings that share
6630
+ * one declaration — a policy edit cannot land on only one of them.
6631
+ */
6632
+ const KIMI_CODE_CONFIG_DECLARATION = {
6633
+ format: "toml",
6634
+ invalidRootPolicy: "error",
6635
+ features: {
6636
+ hooks: {
6637
+ kind: "replace-owned-keys",
6638
+ ownedKeys: ["hooks"]
6639
+ },
6640
+ mcp: {
6641
+ kind: "replace-owned-keys",
6642
+ ownedKeys: ["mcp"]
6643
+ },
6644
+ permissions: {
6645
+ kind: "replace-owned-keys",
6646
+ ownedKeys: [
6647
+ "permission",
6648
+ "default_permission_mode",
6649
+ "tools"
6650
+ ]
6651
+ }
6652
+ }
6653
+ };
6033
6654
  const SHARED_CONFIG_OWNERSHIP = {
6034
6655
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6035
6656
  format: "json",
@@ -6048,31 +6669,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6048
6669
  }
6049
6670
  }
6050
6671
  },
6051
- [HERMES_CONFIG_SHARED_FILE_KEY]: {
6052
- format: "yaml",
6053
- features: {
6054
- commands: {
6055
- kind: "replace-owned-keys",
6056
- ownedKeys: ["plugins"]
6057
- },
6058
- subagents: {
6059
- kind: "replace-owned-keys",
6060
- ownedKeys: ["plugins"]
6061
- },
6062
- mcp: {
6063
- kind: "replace-owned-keys",
6064
- ownedKeys: ["mcp_servers"]
6065
- },
6066
- hooks: {
6067
- kind: "replace-owned-keys",
6068
- ownedKeys: ["hooks"]
6069
- },
6070
- permissions: {
6071
- kind: "deep-merge",
6072
- replaceKeys: ["permissions"]
6073
- }
6074
- }
6075
- },
6672
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6673
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6674
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6076
6675
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6077
6676
  format: "yaml",
6078
6677
  invalidRootPolicy: "error",
@@ -6118,6 +6717,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6118
6717
  ".config/zed/settings.json": {
6119
6718
  format: "json",
6120
6719
  features: {
6720
+ ignore: {
6721
+ kind: "replace-owned-keys",
6722
+ ownedKeys: ["private_files"]
6723
+ },
6121
6724
  mcp: {
6122
6725
  kind: "replace-owned-keys",
6123
6726
  ownedKeys: ["context_servers"]
@@ -6131,6 +6734,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6131
6734
  "AppData/Roaming/Zed/settings.json": {
6132
6735
  format: "json",
6133
6736
  features: {
6737
+ ignore: {
6738
+ kind: "replace-owned-keys",
6739
+ ownedKeys: ["private_files"]
6740
+ },
6134
6741
  mcp: {
6135
6742
  kind: "replace-owned-keys",
6136
6743
  ownedKeys: ["context_servers"]
@@ -6147,7 +6754,20 @@ const SHARED_CONFIG_OWNERSHIP = {
6147
6754
  jsoncParseErrors: "error",
6148
6755
  features: { permissions: {
6149
6756
  kind: "replace-owned-keys",
6150
- ownedKeys: ["chat.tools.terminal.autoApprove"]
6757
+ ownedKeys: [
6758
+ "chat.tools.terminal.autoApprove",
6759
+ "chat.tools.edits.autoApprove",
6760
+ "chat.tools.urls.autoApprove"
6761
+ ]
6762
+ } }
6763
+ },
6764
+ ".vscode/mcp.json": {
6765
+ format: "jsonc",
6766
+ invalidRootPolicy: "error",
6767
+ jsoncParseErrors: "error",
6768
+ features: { mcp: {
6769
+ kind: "replace-owned-keys",
6770
+ ownedKeys: ["servers"]
6151
6771
  } }
6152
6772
  },
6153
6773
  ".qwen/settings.json": {
@@ -6374,31 +6994,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6374
6994
  }
6375
6995
  }
6376
6996
  },
6377
- [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: {
6378
- format: "toml",
6379
- invalidRootPolicy: "error",
6380
- features: {
6381
- hooks: {
6382
- kind: "replace-owned-keys",
6383
- ownedKeys: ["hooks"]
6384
- },
6385
- mcp: {
6386
- kind: "replace-owned-keys",
6387
- ownedKeys: ["mcp"]
6388
- },
6389
- permissions: {
6390
- kind: "replace-owned-keys",
6391
- ownedKeys: [
6392
- "permission",
6393
- "default_permission_mode",
6394
- "tools"
6395
- ]
6396
- }
6397
- }
6398
- },
6997
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6998
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6399
6999
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6400
7000
  format: "toml",
6401
7001
  features: {
7002
+ ignore: {
7003
+ kind: "custom",
7004
+ policyFunction: "applyIgnoreReadDenies"
7005
+ },
6402
7006
  mcp: {
6403
7007
  kind: "replace-owned-keys",
6404
7008
  ownedKeys: ["plugins"]
@@ -6416,6 +7020,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6416
7020
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6417
7021
  format: "toml",
6418
7022
  features: {
7023
+ ignore: {
7024
+ kind: "custom",
7025
+ policyFunction: "applyIgnoreReadDenies"
7026
+ },
6419
7027
  mcp: {
6420
7028
  kind: "replace-owned-keys",
6421
7029
  ownedKeys: ["plugins"]
@@ -6513,7 +7121,12 @@ const applyIgnoreReadDenies = (params) => {
6513
7121
  const applyPermissions = (params) => {
6514
7122
  const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
6515
7123
  const current = parsePermissionsBlock(settings);
6516
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
7124
+ const emitted = /* @__PURE__ */ new Set([
7125
+ ...allow,
7126
+ ...ask,
7127
+ ...deny
7128
+ ]);
7129
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)) && !emitted.has(entry));
6517
7130
  if (logger && managedToolNames.has(READ_TOOL_NAME)) {
6518
7131
  const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
6519
7132
  if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
@@ -6663,12 +7276,9 @@ function toRulesyncCheckFromGate({ gate, index, scope, editOnly }) {
6663
7276
  body: ""
6664
7277
  });
6665
7278
  }
6666
- function slugify(value) {
6667
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6668
- }
6669
7279
  function slugForGate({ gate, index, scope }) {
6670
- const slug = slugify(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
6671
- return `${scope ? `${slugify(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
7280
+ const slug = slugifyCheckName(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
7281
+ return `${scope ? `${slugifyCheckName(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
6672
7282
  }
6673
7283
  /**
6674
7284
  * Checks adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -6844,6 +7454,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6844
7454
  filePattern: "*.md"
6845
7455
  }
6846
7456
  }],
7457
+ ["cursor", {
7458
+ class: CursorCheck,
7459
+ meta: {
7460
+ supportsGlobal: false,
7461
+ filePattern: CURSOR_BUGBOT_FILE_NAME
7462
+ }
7463
+ }],
6847
7464
  ["hermesagent", {
6848
7465
  class: HermesagentCheck,
6849
7466
  meta: {
@@ -6851,6 +7468,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6851
7468
  filePattern: "*.json"
6852
7469
  }
6853
7470
  }],
7471
+ ["rovodev", {
7472
+ class: RovodevCheck,
7473
+ meta: {
7474
+ supportsGlobal: false,
7475
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7476
+ }
7477
+ }],
6854
7478
  ["takt", {
6855
7479
  class: TaktCheck,
6856
7480
  meta: {
@@ -7006,19 +7630,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7006
7630
  }
7007
7631
  };
7008
7632
  //#endregion
7633
+ //#region src/utils/tool-home.ts
7634
+ /**
7635
+ * Where the rulesync-side source files of a tool with a home override belong.
7636
+ *
7637
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7638
+ * imported back out of it are not part of the tool's profile — they stay under
7639
+ * the rulesync home. When no override is set, the native output root already is
7640
+ * that place.
7641
+ */
7642
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7643
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7644
+ }
7645
+ //#endregion
7009
7646
  //#region src/utils/hermesagent.ts
7010
7647
  function getHermesagentHome() {
7011
7648
  const configuredHome = process.env.HERMES_HOME?.trim();
7012
7649
  return configuredHome ? resolve(configuredHome) : void 0;
7013
7650
  }
7651
+ /**
7652
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7653
+ *
7654
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7655
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7656
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7657
+ * would land where Hermes never reads.
7658
+ *
7659
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7660
+ */
7661
+ function getHermesagentGlobalDir() {
7662
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7663
+ }
7014
7664
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7015
7665
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7016
7666
  }
7667
+ /**
7668
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7669
+ * actually writes in the requested scope.
7670
+ *
7671
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7672
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7673
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7674
+ * platform default directory takes its place.
7675
+ */
7017
7676
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7018
- if (!global || !getHermesagentHome()) return relativeDirPath;
7677
+ if (!global) return relativeDirPath;
7019
7678
  const relativePath = relative(HERMESAGENT_GLOBAL_DIR, relativeDirPath);
7020
- if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7021
- return relativePath;
7679
+ try {
7680
+ checkPathTraversal({
7681
+ relativePath: relativeDirPath,
7682
+ intendedRootDir: "."
7683
+ });
7684
+ checkPathTraversal({
7685
+ relativePath,
7686
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7687
+ });
7688
+ } catch {
7689
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7690
+ }
7691
+ return getHermesagentHome() ? relativePath || "." : join(getHermesagentGlobalDir(), relativePath);
7022
7692
  }
7023
7693
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7024
7694
  return join(getHermesagentRelativeDirPath({
@@ -7026,8 +7696,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7026
7696
  relativeDirPath: dirname(relativeFilePath)
7027
7697
  }), basename(relativeFilePath));
7028
7698
  }
7699
+ /**
7700
+ * Every spelling `config.yaml` can take in global scope, so that the
7701
+ * shared-write derivation and the gateway ownership table it is checked against
7702
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7703
+ *
7704
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7705
+ * which would otherwise make the derived shared-file key depend on the ambient
7706
+ * environment — the drift guards would then go blind in precisely the
7707
+ * configuration this feature exists for.
7708
+ */
7709
+ function getHermesagentSharedConfigWritePaths() {
7710
+ return [
7711
+ {
7712
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7713
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7714
+ },
7715
+ {
7716
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7717
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7718
+ },
7719
+ {
7720
+ relativeDirPath: ".",
7721
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7722
+ }
7723
+ ];
7724
+ }
7725
+ /**
7726
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7727
+ * writes. All three spellings carry the same declaration, but passing the key of
7728
+ * the file being written keeps the write path and the drift guards reading the
7729
+ * same entry.
7730
+ */
7731
+ function getHermesagentConfigSharedFileKey({ global }) {
7732
+ return sharedConfigFileKey({
7733
+ relativeDirPath: getHermesagentRelativeDirPath({
7734
+ global,
7735
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7736
+ }),
7737
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7738
+ });
7739
+ }
7029
7740
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7030
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7741
+ return getToolRulesyncOutputRoot({
7742
+ nativeOutputRoot,
7743
+ global,
7744
+ toolHome: getHermesagentHome
7745
+ });
7031
7746
  }
7032
7747
  //#endregion
7033
7748
  //#region src/constants/agentsmd-paths.ts
@@ -7259,6 +7974,7 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
7259
7974
  const ANTIGRAVITY_DIR = ".agents";
7260
7975
  const ANTIGRAVITY_SKILLS_DIR_PATH = join(ANTIGRAVITY_DIR, "skills");
7261
7976
  const ANTIGRAVITY_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_DIR, "workflows");
7977
+ const ANTIGRAVITY_AGENTS_DIR_PATH = join(ANTIGRAVITY_DIR, "agents");
7262
7978
  const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
7263
7979
  const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
7264
7980
  const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
@@ -7269,6 +7985,7 @@ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
7269
7985
  const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
7270
7986
  const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
7271
7987
  const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
7988
+ const ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH = join(ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH, "agents");
7272
7989
  //#endregion
7273
7990
  //#region src/constants/antigravity-cli-paths.ts
7274
7991
  const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
@@ -7827,6 +8544,7 @@ const CLAUDECODE_PLUGIN_HOOKS_DIR = "hooks";
7827
8544
  const CLAUDECODE_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7828
8545
  const ANTIGRAVITY_PLUGIN_RULES_DIR = "rules";
7829
8546
  const ANTIGRAVITY_PLUGIN_SKILLS_DIR = "skills";
8547
+ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
7830
8548
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
7831
8549
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7832
8550
  //#endregion
@@ -8051,6 +8769,7 @@ const COPILOT_SKILLS_DIR_PATH = join(GITHUB_DIR, "skills");
8051
8769
  const COPILOT_AGENTS_DIR_PATH = join(GITHUB_DIR, "agents");
8052
8770
  const COPILOT_HOOKS_DIR_PATH = join(GITHUB_DIR, "hooks");
8053
8771
  const COPILOT_HOOKS_FILE_NAME = "copilot-hooks.json";
8772
+ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
8054
8773
  const COPILOT_MCP_DIR = ".vscode";
8055
8774
  const COPILOT_MCP_FILE_NAME = "mcp.json";
8056
8775
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
@@ -8058,6 +8777,7 @@ const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
8058
8777
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
8059
8778
  const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
8060
8779
  const COPILOTCLI_HOOKS_DIR_PATH = join(COPILOT_DIR, "hooks");
8780
+ const COPILOT_GLOBAL_HOOKS_DIR_PATH = COPILOTCLI_HOOKS_DIR_PATH;
8061
8781
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
8062
8782
  const COPILOT_SKILLS_GLOBAL_DIR_PATH = join(COPILOT_DIR, "skills");
8063
8783
  //#endregion
@@ -8181,17 +8901,6 @@ var CopilotCommand = class CopilotCommand extends ToolCommand {
8181
8901
  }
8182
8902
  };
8183
8903
  //#endregion
8184
- //#region src/constants/cursor-paths.ts
8185
- const CURSOR_DIR = ".cursor";
8186
- const CURSOR_COMMANDS_DIR_PATH = join(CURSOR_DIR, "commands");
8187
- const CURSOR_SKILLS_DIR_PATH = join(CURSOR_DIR, "skills");
8188
- const CURSOR_AGENTS_DIR_PATH = join(CURSOR_DIR, "agents");
8189
- const CURSOR_MCP_FILE_NAME = "mcp.json";
8190
- const CURSOR_HOOKS_FILE_NAME = "hooks.json";
8191
- const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
8192
- const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
8193
- const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
8194
- //#endregion
8195
8904
  //#region src/features/commands/cursor-command.ts
8196
8905
  const CursorCommandFrontmatterSchema = z.looseObject({
8197
8906
  description: z.optional(z.string()),
@@ -8737,6 +9446,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
8737
9446
  }
8738
9447
  };
8739
9448
  //#endregion
9449
+ //#region src/constants/grokcli-paths.ts
9450
+ /**
9451
+ * Grok Build CLI (xAI) configuration-layout conventions.
9452
+ *
9453
+ * Single source of truth for where Grok Build expects its files. Grok Build
9454
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9455
+ * with project/global scopes resolved by the directory the CLI runs in
9456
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9457
+ *
9458
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9459
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9460
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9461
+ * @see https://docs.x.ai/build/overview
9462
+ */
9463
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9464
+ const GROKCLI_DIR = ".grok";
9465
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9466
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9467
+ /**
9468
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9469
+ * permission mode, and other settings all live here; permissions reuse the same
9470
+ * file name as MCP since Grok consolidates everything into one config.
9471
+ */
9472
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9473
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9474
+ const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
9475
+ /**
9476
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9477
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9478
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9479
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9480
+ * @see https://docs.x.ai/build/features/hooks
9481
+ */
9482
+ const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
9483
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9484
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9485
+ /**
9486
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9487
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9488
+ * (global), each a Markdown file with YAML frontmatter (verified via
9489
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9490
+ */
9491
+ const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
9492
+ /**
9493
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9494
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9495
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9496
+ * MCP/skills/subagents adapters).
9497
+ */
9498
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9499
+ /**
9500
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9501
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9502
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9503
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9504
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9505
+ * for Claude Code.
9506
+ *
9507
+ * Skills are collected before commands and win name collisions, so a
9508
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9509
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9510
+ */
9511
+ const GROKCLI_COMMANDS_DIR_PATH = join(GROKCLI_DIR, "commands");
9512
+ /**
9513
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9514
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9515
+ * walks, and `~/.grok/rules/` in the home scope.
9516
+ * @see https://docs.x.ai/build/overview
9517
+ */
9518
+ const GROKCLI_RULES_DIR_PATH = join(GROKCLI_DIR, "rules");
9519
+ //#endregion
9520
+ //#region src/features/commands/grokcli-command.ts
9521
+ /**
9522
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9523
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9524
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9525
+ *
9526
+ * Two upstream constraints shape this adapter:
9527
+ *
9528
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9529
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9530
+ * onto their basename).
9531
+ * - Skills are collected before commands and win name collisions, so a
9532
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9533
+ *
9534
+ * `description` and `argument-hint` describe the command, while
9535
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9536
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9537
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9538
+ */
9539
+ const GrokcliCommandFrontmatterSchema = z.looseObject({
9540
+ description: z.optional(z.string()),
9541
+ "argument-hint": z.optional(z.string()),
9542
+ "user-invocable": z.optional(z.boolean()),
9543
+ "disable-model-invocation": z.optional(z.boolean())
9544
+ });
9545
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9546
+ frontmatter;
9547
+ body;
9548
+ constructor({ frontmatter, body, ...rest }) {
9549
+ if (rest.validate) {
9550
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9551
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9552
+ }
9553
+ super({
9554
+ ...rest,
9555
+ fileContent: stringifyFrontmatter(body, frontmatter)
9556
+ });
9557
+ this.frontmatter = frontmatter;
9558
+ this.body = body;
9559
+ }
9560
+ static getSettablePaths(_options = {}) {
9561
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9562
+ }
9563
+ getBody() {
9564
+ return this.body;
9565
+ }
9566
+ getFrontmatter() {
9567
+ return this.frontmatter;
9568
+ }
9569
+ toRulesyncCommand() {
9570
+ const { description, ...restFields } = this.frontmatter;
9571
+ const rulesyncFrontmatter = {
9572
+ targets: ["*"],
9573
+ description,
9574
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9575
+ };
9576
+ return new RulesyncCommand({
9577
+ outputRoot: ".",
9578
+ frontmatter: rulesyncFrontmatter,
9579
+ body: this.body,
9580
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9581
+ relativeFilePath: this.relativeFilePath,
9582
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9583
+ validate: true
9584
+ });
9585
+ }
9586
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9587
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9588
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9589
+ const grokcliFrontmatter = {
9590
+ description: rulesyncFrontmatter.description,
9591
+ ...grokcliFields
9592
+ };
9593
+ const paths = this.getSettablePaths({ global });
9594
+ return new GrokcliCommand({
9595
+ outputRoot,
9596
+ frontmatter: grokcliFrontmatter,
9597
+ body: rulesyncCommand.getBody(),
9598
+ relativeDirPath: paths.relativeDirPath,
9599
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9600
+ validate
9601
+ });
9602
+ }
9603
+ validate() {
9604
+ if (!this.frontmatter) return {
9605
+ success: true,
9606
+ error: null
9607
+ };
9608
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9609
+ if (result.success) return {
9610
+ success: true,
9611
+ error: null
9612
+ };
9613
+ return {
9614
+ success: false,
9615
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9616
+ };
9617
+ }
9618
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9619
+ return this.isTargetedByRulesyncCommandDefault({
9620
+ rulesyncCommand,
9621
+ toolTarget: "grokcli"
9622
+ });
9623
+ }
9624
+ /**
9625
+ * Warn when a rulesync skill would shadow a rulesync command.
9626
+ *
9627
+ * Grok collects skills before commands and lets skills win name collisions,
9628
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9629
+ * Both files are still written correctly — nothing is overwritten and no
9630
+ * output is lost — so this warns rather than failing the run the way the
9631
+ * Hermes check does, where the two surfaces really do write the same path.
9632
+ */
9633
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9634
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
9635
+ if (commandNames.size === 0) return;
9636
+ const shadowed = (await findFilesByGlobs(join(join(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
9637
+ if (shadowed.length > 0) logger.warn(`Grok CLI resolves skills before commands, so these skills shadow the same-named commands, which will never be reachable: ${[...new Set(shadowed)].toSorted().join(", ")}. Rename either side to make both invocable.`);
9638
+ }
9639
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9640
+ const paths = this.getSettablePaths({ global });
9641
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
9642
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9643
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9644
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9645
+ return new GrokcliCommand({
9646
+ outputRoot,
9647
+ relativeDirPath: paths.relativeDirPath,
9648
+ relativeFilePath,
9649
+ frontmatter: result.data,
9650
+ body: content.trim(),
9651
+ validate
9652
+ });
9653
+ }
9654
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9655
+ return new GrokcliCommand({
9656
+ outputRoot,
9657
+ relativeDirPath,
9658
+ relativeFilePath,
9659
+ frontmatter: { description: "" },
9660
+ body: "",
9661
+ validate: false
9662
+ });
9663
+ }
9664
+ };
9665
+ //#endregion
8740
9666
  //#region src/features/skills/tool-skill.ts
8741
9667
  /** Ordered skill directory roots: primary first. */
8742
9668
  function toolSkillSearchRoots(paths) {
@@ -9371,7 +10297,7 @@ def register(ctx):
9371
10297
  _register_command(ctx, command)
9372
10298
  `;
9373
10299
  }
9374
- function getEnabledPluginConfigContent$1(currentContent) {
10300
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9375
10301
  const config = parseSharedConfig({
9376
10302
  format: "yaml",
9377
10303
  fileContent: currentContent
@@ -9379,7 +10305,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9379
10305
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9380
10306
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9381
10307
  return applySharedConfigPatch({
9382
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10308
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9383
10309
  feature: "commands",
9384
10310
  existingContent: currentContent,
9385
10311
  patch: { plugins: {
@@ -9388,7 +10314,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9388
10314
  } }
9389
10315
  });
9390
10316
  }
9391
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10317
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9392
10318
  const config = parseSharedConfig({
9393
10319
  format: "yaml",
9394
10320
  fileContent: currentContent
@@ -9396,7 +10322,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9396
10322
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9397
10323
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9398
10324
  return applySharedConfigPatch({
9399
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10325
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9400
10326
  feature: "commands",
9401
10327
  existingContent: currentContent,
9402
10328
  patch: { plugins: {
@@ -9412,35 +10338,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9412
10338
  error: null
9413
10339
  };
9414
10340
  }
9415
- shouldMergeExistingFileContent() {
10341
+ /**
10342
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10343
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10344
+ */
10345
+ matchesPath(relativeFilePath) {
9416
10346
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9417
10347
  global: this.global,
9418
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10348
+ relativeFilePath
9419
10349
  }));
9420
10350
  }
10351
+ shouldMergeExistingFileContent() {
10352
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10353
+ }
9421
10354
  setFileContent(newFileContent) {
9422
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9423
- global: this.global,
9424
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9425
- }))) {
9426
- super.setFileContent(getEnabledPluginConfigContent$1(newFileContent));
10355
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10356
+ super.setFileContent(getEnabledPluginConfigContent$1({
10357
+ currentContent: newFileContent,
10358
+ global: this.global
10359
+ }));
9427
10360
  return;
9428
10361
  }
9429
10362
  super.setFileContent(newFileContent);
9430
10363
  }
9431
10364
  getFileContent() {
9432
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9433
- global: this.global,
9434
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH
9435
- }))) return getPluginManifestContent$2();
9436
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9437
- global: this.global,
9438
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH
9439
- }))) return getPluginInitContent$2();
9440
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9441
- global: this.global,
9442
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9443
- }))) return getEnabledPluginConfigContent$1(super.getFileContent());
10365
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10366
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10367
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10368
+ currentContent: super.getFileContent(),
10369
+ global: this.global
10370
+ });
9444
10371
  return super.getFileContent();
9445
10372
  }
9446
10373
  };
@@ -9457,14 +10384,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9457
10384
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9458
10385
  }) };
9459
10386
  }
9460
- static getExtraSharedWritePaths({ global = false } = {}) {
9461
- return [{
9462
- relativeDirPath: getHermesagentRelativeDirPath({
9463
- global,
9464
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
9465
- }),
9466
- relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
9467
- }];
10387
+ /**
10388
+ * `config.yaml` under every spelling the global profile root can take.
10389
+ * @see getHermesagentSharedConfigWritePaths
10390
+ */
10391
+ static getExtraSharedWritePaths() {
10392
+ return getHermesagentSharedConfigWritePaths();
9468
10393
  }
9469
10394
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9470
10395
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9489,33 +10414,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9489
10414
  }
9490
10415
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9491
10416
  if (toolCommands.length === 0 && !forDeletion) return [];
10417
+ const pluginDirPath = getHermesagentRelativeDirPath({
10418
+ global,
10419
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10420
+ });
9492
10421
  const pluginFiles = [
9493
10422
  new HermesagentCommandAuxiliaryFile({
9494
10423
  outputRoot,
9495
- relativeDirPath: getHermesagentRelativeDirPath({
9496
- global,
9497
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9498
- }),
10424
+ relativeDirPath: pluginDirPath,
9499
10425
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9500
10426
  fileContent: "",
9501
10427
  global
9502
10428
  }),
9503
10429
  new HermesagentCommandAuxiliaryFile({
9504
10430
  outputRoot,
9505
- relativeDirPath: getHermesagentRelativeDirPath({
9506
- global,
9507
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9508
- }),
10431
+ relativeDirPath: pluginDirPath,
9509
10432
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9510
10433
  fileContent: "Generated and owned by RuleSync.\n",
9511
10434
  global
9512
10435
  }),
9513
10436
  new HermesagentCommandAuxiliaryFile({
9514
10437
  outputRoot,
9515
- relativeDirPath: getHermesagentRelativeDirPath({
9516
- global,
9517
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9518
- }),
10438
+ relativeDirPath: pluginDirPath,
9519
10439
  relativeFilePath: basename(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9520
10440
  fileContent: "",
9521
10441
  global
@@ -10750,19 +11670,6 @@ var RooCommand = class RooCommand extends ToolCommand {
10750
11670
  }
10751
11671
  };
10752
11672
  //#endregion
10753
- //#region src/constants/rovodev-paths.ts
10754
- const ROVODEV_DIR = ".rovodev";
10755
- const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
10756
- const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
10757
- const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
10758
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
10759
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
10760
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
10761
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
10762
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
10763
- const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
10764
- const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
10765
- //#endregion
10766
11673
  //#region src/features/commands/rovodev-command.ts
10767
11674
  /**
10768
11675
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11346,6 +12253,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11346
12253
  supportsSubdirectory: false
11347
12254
  }
11348
12255
  }],
12256
+ ["grokcli", {
12257
+ class: GrokcliCommand,
12258
+ meta: {
12259
+ extension: "md",
12260
+ supportsProject: true,
12261
+ supportsGlobal: true,
12262
+ isSimulated: false,
12263
+ supportsSubdirectory: false
12264
+ }
12265
+ }],
11349
12266
  ["hermesagent", {
11350
12267
  class: HermesagentCommand,
11351
12268
  meta: {
@@ -11539,7 +12456,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11539
12456
  const factory = this.getFactory(this.toolTarget);
11540
12457
  await factory.class.validateRulesyncCommands?.({
11541
12458
  inputRoot: this.inputRoot,
11542
- rulesyncCommands
12459
+ rulesyncCommands,
12460
+ logger: this.logger
11543
12461
  });
11544
12462
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11545
12463
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -11677,7 +12595,10 @@ var CommandsProcessor = class extends FeatureProcessor {
11677
12595
  }));
11678
12596
  const currentContent = await readFileContentOrNull(configPath);
11679
12597
  if (currentContent === null) return changedCount;
11680
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12598
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12599
+ currentContent,
12600
+ global: this.global
12601
+ });
11681
12602
  if (nextContent === currentContent) return changedCount;
11682
12603
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
11683
12604
  else await writeFileContent(configPath, nextContent);
@@ -11960,6 +12881,13 @@ function groupDefinitionsByMatcher(definitions) {
11960
12881
  }
11961
12882
  return byMatcher;
11962
12883
  }
12884
+ /** `$CLAUDE_PROJECT_DIR` -> `${CLAUDE_PROJECT_DIR}`, the form the tool substitutes. */
12885
+ function bracePlaceholder(projectDirVar) {
12886
+ return `\${${projectDirVar.replace(/^\$/, "")}}`;
12887
+ }
12888
+ function stripSurroundingQuotes(value) {
12889
+ return value.replace(/^(["'])(.*)\1$/, "$2").replace(/^["']/, "");
12890
+ }
11963
12891
  /**
11964
12892
  * Apply the optional project directory variable prefix to a command string.
11965
12893
  */
@@ -11969,8 +12897,10 @@ function applyCommandPrefix({ def, converterConfig }) {
11969
12897
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
11970
12898
  const isDotRelativeCommand = unquotedCommand?.startsWith(".") ?? false;
11971
12899
  const isAbsoluteCommand = typeof unquotedCommand === "string" && (posix.isAbsolute(unquotedCommand) || win32.isAbsolute(unquotedCommand) || unquotedCommand.startsWith("~/"));
12900
+ const isExecForm = (converterConfig.arrayPassthroughFields?.some(({ canonical }) => canonical === "args") ?? false) && Array.isArray(def.args);
11972
12901
  if (!(converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && !isAbsoluteCommand && (!converterConfig.prefixDotRelativeCommandsOnly || isDotRelativeCommand)) || typeof trimmedCommand !== "string") return def.command;
11973
12902
  const relativeCommand = trimmedCommand.replace(/^(["'])\.\//, "$1").replace(/^\.\//, "");
12903
+ if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
11974
12904
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
11975
12905
  }
11976
12906
  /**
@@ -11978,8 +12908,11 @@ function applyCommandPrefix({ def, converterConfig }) {
11978
12908
  * canonical field name to its (possibly renamed) tool field name. Only boolean
11979
12909
  * values are carried through.
11980
12910
  */
11981
- function emitBooleanPassthroughFields({ def, converterConfig }) {
11982
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "boolean").map(({ canonical, tool }) => [tool, def[canonical]]));
12911
+ function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
12912
+ return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12913
+ if (commandOnly === true && hookType !== "command") return false;
12914
+ return typeof def[canonical] === "boolean";
12915
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
11983
12916
  }
11984
12917
  /**
11985
12918
  * Import the configured boolean passthrough fields back into canonical fields,
@@ -11993,8 +12926,11 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
11993
12926
  * canonical field name to its (possibly renamed) tool field name. Only non-empty
11994
12927
  * string values are carried through.
11995
12928
  */
11996
- function emitStringPassthroughFields({ def, converterConfig }) {
11997
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "string" && def[canonical] !== "").map(({ canonical, tool }) => [tool, def[canonical]]));
12929
+ function emitStringPassthroughFields({ def, hookType, converterConfig }) {
12930
+ return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12931
+ if (commandOnly === true && hookType !== "command") return false;
12932
+ return typeof def[canonical] === "string" && def[canonical] !== "";
12933
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
11998
12934
  }
11999
12935
  /**
12000
12936
  * Import the configured string passthrough fields back into canonical fields,
@@ -12007,8 +12943,11 @@ function importStringPassthroughFields({ h, converterConfig }) {
12007
12943
  /**
12008
12944
  * Emit the configured string-array passthrough fields on the tool side.
12009
12945
  */
12010
- function emitArrayPassthroughFields({ def, converterConfig }) {
12011
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical }) => isStringArray(def[canonical])).map(({ canonical, tool }) => [tool, def[canonical]]));
12946
+ function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
12947
+ return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12948
+ if (commandOnly === true && hookType !== "command") return false;
12949
+ return isStringArray(def[canonical]);
12950
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12012
12951
  }
12013
12952
  /**
12014
12953
  * Import the configured string-array passthrough fields, reversing
@@ -12085,14 +13024,17 @@ function buildToolHooks({ defs, converterConfig }) {
12085
13024
  hooks.push({
12086
13025
  ...emitBooleanPassthroughFields({
12087
13026
  def,
13027
+ hookType,
12088
13028
  converterConfig
12089
13029
  }),
12090
13030
  ...emitStringPassthroughFields({
12091
13031
  def,
13032
+ hookType,
12092
13033
  converterConfig
12093
13034
  }),
12094
13035
  ...emitArrayPassthroughFields({
12095
13036
  def,
13037
+ hookType,
12096
13038
  converterConfig
12097
13039
  }),
12098
13040
  type: hookType,
@@ -12176,6 +13118,8 @@ function stripCommandPrefix({ command, converterConfig }) {
12176
13118
  if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
12177
13119
  const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
12178
13120
  if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
13121
+ const bracedPrefix = `${bracePlaceholder(converterConfig.projectDirVar)}/`;
13122
+ if (cmd.startsWith(bracedPrefix)) return `./${cmd.slice(bracedPrefix.length)}`;
12179
13123
  if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
12180
13124
  const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12181
13125
  return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
@@ -12723,7 +13667,10 @@ const CLAUDE_CONVERTER_CONFIG = {
12723
13667
  "taskCreated",
12724
13668
  "taskCompleted",
12725
13669
  "teammateIdle",
12726
- "cwdChanged"
13670
+ "cwdChanged",
13671
+ "beforeSubmitPrompt",
13672
+ "stop",
13673
+ "directoryAdded"
12727
13674
  ]),
12728
13675
  supportedHookTypes: /* @__PURE__ */ new Set([
12729
13676
  "command",
@@ -12733,9 +13680,45 @@ const CLAUDE_CONVERTER_CONFIG = {
12733
13680
  "agent"
12734
13681
  ]),
12735
13682
  emitsPromptModel: true,
12736
- stringPassthroughFields: [{
12737
- canonical: "if",
12738
- tool: "if"
13683
+ stringPassthroughFields: [
13684
+ {
13685
+ canonical: "if",
13686
+ tool: "if"
13687
+ },
13688
+ {
13689
+ canonical: "statusMessage",
13690
+ tool: "statusMessage"
13691
+ },
13692
+ {
13693
+ canonical: "shell",
13694
+ tool: "shell",
13695
+ commandOnly: true
13696
+ }
13697
+ ],
13698
+ booleanPassthroughFields: [
13699
+ {
13700
+ canonical: "once",
13701
+ tool: "once"
13702
+ },
13703
+ {
13704
+ canonical: "async",
13705
+ tool: "async",
13706
+ commandOnly: true
13707
+ },
13708
+ {
13709
+ canonical: "asyncRewake",
13710
+ tool: "asyncRewake",
13711
+ commandOnly: true
13712
+ },
13713
+ {
13714
+ canonical: "continueOnBlock",
13715
+ tool: "continueOnBlock"
13716
+ }
13717
+ ],
13718
+ arrayPassthroughFields: [{
13719
+ canonical: "args",
13720
+ tool: "args",
13721
+ commandOnly: true
12739
13722
  }]
12740
13723
  };
12741
13724
  var ClaudecodeHooks = class extends ToolHooks {
@@ -12843,7 +13826,14 @@ const CODEXCLI_CONVERTER_CONFIG = {
12843
13826
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
12844
13827
  projectDirVar: "",
12845
13828
  supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
12846
- passthroughFields: ["name", "description"]
13829
+ passthroughFields: ["name", "description"],
13830
+ stringPassthroughFields: [{
13831
+ canonical: "commandWindows",
13832
+ tool: "commandWindows"
13833
+ }, {
13834
+ canonical: "statusMessage",
13835
+ tool: "statusMessage"
13836
+ }]
12847
13837
  };
12848
13838
  /**
12849
13839
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
@@ -13091,7 +14081,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13091
14081
  fileContent: params.fileContent ?? "{}"
13092
14082
  });
13093
14083
  }
13094
- static getSettablePaths(_options = {}) {
14084
+ static getSettablePaths({ global = false } = {}) {
14085
+ if (global) return {
14086
+ relativeDirPath: COPILOT_GLOBAL_HOOKS_DIR_PATH,
14087
+ relativeFilePath: COPILOT_GLOBAL_HOOKS_FILE_NAME
14088
+ };
13095
14089
  return {
13096
14090
  relativeDirPath: COPILOT_HOOKS_DIR_PATH,
13097
14091
  relativeFilePath: COPILOT_HOOKS_FILE_NAME
@@ -13105,11 +14099,12 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13105
14099
  relativeDirPath: paths.relativeDirPath,
13106
14100
  relativeFilePath: paths.relativeFilePath,
13107
14101
  fileContent,
13108
- validate
14102
+ validate,
14103
+ global
13109
14104
  });
13110
14105
  }
13111
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true }) {
13112
- const paths = CopilotHooks.getSettablePaths();
14106
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
14107
+ const paths = CopilotHooks.getSettablePaths({ global });
13113
14108
  const copilotHooks = canonicalToCopilotHooks(rulesyncHooks.getJson());
13114
14109
  const fileContent = JSON.stringify({
13115
14110
  version: 1,
@@ -13120,7 +14115,8 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13120
14115
  relativeDirPath: paths.relativeDirPath,
13121
14116
  relativeFilePath: paths.relativeFilePath,
13122
14117
  fileContent,
13123
- validate
14118
+ validate,
14119
+ global
13124
14120
  });
13125
14121
  }
13126
14122
  toRulesyncHooks(options) {
@@ -13164,10 +14160,16 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13164
14160
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
13165
14161
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
13166
14162
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
13167
- * `notification`, `preMcpToolCall`). Each entry supports three hook types:
14163
+ * `notification`, `userPromptTransformed`, `preMcpToolCall`). Each entry
14164
+ * supports three hook types:
13168
14165
  *
13169
14166
  * - `command` — the `bash` / `powershell` command-field shape with optional
13170
- * `timeoutSec`, plus optional `cwd` / `env`.
14167
+ * `timeoutSec`, plus optional `cwd` / `env`. Upstream also accepts the
14168
+ * portable `command` field (copied to both shells when neither is present)
14169
+ * and `timeout` as an alias for `timeoutSec`; rulesync reads both on import.
14170
+ * On generate the canonical `shell` selector picks `bash` or `powershell`,
14171
+ * and without it the portable `command` field is written — so the generated
14172
+ * file does not depend on the machine rulesync ran on.
13171
14173
  * - `prompt` — a `prompt` string (Copilot CLI only honors prompt hooks on
13172
14174
  * `sessionStart`, so prompt hooks on other events are skipped).
13173
14175
  * - `http` — `url` / `headers` / `allowedEnvVars` with optional `timeoutSec`.
@@ -13193,11 +14195,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13193
14195
  * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
13194
14196
  * root and will revisit if the spec later mandates an alternate layout.
13195
14197
  *
13196
- * Hook entries on `preToolUse` / `postToolUse` may carry an optional `matcher`
13197
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`); it is
13198
- * emitted on those events and dropped (with a warning) on any other event,
13199
- * which never honors matchers. See changelog v1.0.36 (2026-04-24) and v1.0.63
13200
- * (2026-06-15). Reference: https://docs.github.com/en/copilot/reference/hooks-reference
14198
+ * Hook entries on the six matcher-aware events (see
14199
+ * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
14200
+ * is emitted on those events and dropped (with a warning) on any other event,
14201
+ * which never honors matchers.
14202
+ * Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13201
14203
  *
13202
14204
  * The output JSON schema and platform-specific `bash` / `powershell` command
13203
14205
  * field selection match `copilot-hooks.ts`, but the event surface diverges (the
@@ -13212,19 +14214,30 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13212
14214
  * `Edit|Write`) are now honored instead of silently dropped").
13213
14215
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
13214
14216
  */
13215
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
14217
+ const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
14218
+ "preToolUse",
14219
+ "postToolUse",
14220
+ "notification",
14221
+ "permissionRequest",
14222
+ "preCompact",
14223
+ "subagentStart"
14224
+ ]);
14225
+ /** Human-readable list of the matcher-aware events, for the drop warning. */
14226
+ const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
13216
14227
  const CopilotCliHookEntrySchema = z.looseObject({
13217
14228
  type: z._default(z.string(), "command"),
13218
14229
  matcher: z.optional(z.string()),
13219
14230
  bash: z.optional(z.string()),
13220
14231
  powershell: z.optional(z.string()),
14232
+ command: z.optional(z.string()),
13221
14233
  prompt: z.optional(z.string()),
13222
14234
  url: z.optional(z.string()),
13223
14235
  headers: z.optional(z.record(z.string(), z.string())),
13224
14236
  allowedEnvVars: z.optional(z.array(z.string())),
13225
14237
  cwd: z.optional(z.string()),
13226
14238
  env: z.optional(z.record(z.string(), z.string())),
13227
- timeoutSec: z.optional(z.number())
14239
+ timeoutSec: z.optional(z.number()),
14240
+ timeout: z.optional(z.number())
13228
14241
  });
13229
14242
  /** Filter the shared config hooks down to events the Copilot CLI supports. */
13230
14243
  function filterSupportedCopilotCliHooks(hooks) {
@@ -13235,21 +14248,21 @@ function filterSupportedCopilotCliHooks(hooks) {
13235
14248
  }
13236
14249
  /**
13237
14250
  * Resolve the `matcher` part for an exported entry. Copilot CLI honors `matcher`
13238
- * only on preToolUse/postToolUse entries; on any other event a matcher would be
13239
- * silently dropped by the CLI, so we drop it here with a warning rather than
13240
- * emitting a dead field.
14251
+ * only on the events listed in {@link COPILOTCLI_MATCHER_EVENTS}; on any other
14252
+ * event a matcher would be silently dropped by the CLI, so we drop it here with
14253
+ * a warning rather than emitting a dead field.
13241
14254
  */
13242
14255
  function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger }) {
13243
14256
  if (matcher === void 0 || matcher === null || matcher === "") return {};
13244
14257
  if (matcherSupported) return { matcher };
13245
- logger?.warn(`Copilot CLI hook matchers are only honored on preToolUse/postToolUse; dropping matcher "${matcher}" on '${eventName}'.`);
14258
+ logger?.warn(`Copilot CLI hook matchers are only honored on ${COPILOTCLI_MATCHER_EVENTS_LABEL}; dropping matcher "${matcher}" on '${eventName}'.`);
13246
14259
  return {};
13247
14260
  }
13248
14261
  /**
13249
14262
  * Build the exported entries for a single canonical event. Returns an empty
13250
14263
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
13251
14264
  */
13252
- function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, commandField, logger }) {
14265
+ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, logger }) {
13253
14266
  const matcherSupported = COPILOTCLI_MATCHER_EVENTS.has(eventName);
13254
14267
  const entries = [];
13255
14268
  for (const def of definitions) {
@@ -13285,22 +14298,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
13285
14298
  ...timeoutPart,
13286
14299
  ...rest
13287
14300
  });
13288
- else if (hookType === "command") entries.push({
13289
- type: "command",
13290
- ...matcherPart,
13291
- ...compact({
13292
- [commandField]: def.command,
13293
- env: def.env
13294
- }),
13295
- ...timeoutPart,
13296
- ...rest
13297
- });
14301
+ else if (hookType === "command") {
14302
+ const commandField = def.shell ?? "command";
14303
+ entries.push({
14304
+ type: "command",
14305
+ ...matcherPart,
14306
+ ...compact({
14307
+ [commandField]: def.command,
14308
+ env: def.env
14309
+ }),
14310
+ ...timeoutPart,
14311
+ ...rest
14312
+ });
14313
+ }
13298
14314
  }
13299
14315
  return entries;
13300
14316
  }
13301
14317
  function canonicalToCopilotCliHooks(config, logger) {
13302
14318
  const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
13303
- const commandField = process.platform === "win32" ? "powershell" : "bash";
13304
14319
  const effectiveHooks = {
13305
14320
  ...filterSupportedCopilotCliHooks(config.hooks),
13306
14321
  ...config.copilot?.hooks,
@@ -13313,7 +14328,6 @@ function canonicalToCopilotCliHooks(config, logger) {
13313
14328
  eventName,
13314
14329
  definitions,
13315
14330
  canonicalSchemaKeys,
13316
- commandField,
13317
14331
  logger
13318
14332
  });
13319
14333
  if (entries.length > 0) out[copilotEventName] = entries;
@@ -13330,6 +14344,13 @@ function importPassthrough(entry) {
13330
14344
  if (entry.env !== void 0) passthrough.env = entry.env;
13331
14345
  return passthrough;
13332
14346
  }
14347
+ /**
14348
+ * Resolve the canonical command and its `shell` selector from an imported entry.
14349
+ *
14350
+ * A shell-specific field carries its `shell` through so re-export writes the
14351
+ * same field back. An entry using only the portable `command` field leaves
14352
+ * `shell` unset, which re-export renders as the portable field again.
14353
+ */
13333
14354
  function resolveImportCommand(entry, logger) {
13334
14355
  const hasBash = typeof entry.bash === "string";
13335
14356
  const hasPowershell = typeof entry.powershell === "string";
@@ -13338,9 +14359,22 @@ function resolveImportCommand(entry, logger) {
13338
14359
  const chosen = isWindows ? "powershell" : "bash";
13339
14360
  const ignored = isWindows ? "bash" : "powershell";
13340
14361
  logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
13341
- return isWindows ? entry.powershell : entry.bash;
13342
- } else if (hasBash) return entry.bash;
13343
- else if (hasPowershell) return entry.powershell;
14362
+ return isWindows ? {
14363
+ command: entry.powershell,
14364
+ shell: "powershell"
14365
+ } : {
14366
+ command: entry.bash,
14367
+ shell: "bash"
14368
+ };
14369
+ } else if (hasBash) return {
14370
+ command: entry.bash,
14371
+ shell: "bash"
14372
+ };
14373
+ else if (hasPowershell) return {
14374
+ command: entry.powershell,
14375
+ shell: "powershell"
14376
+ };
14377
+ return typeof entry.command === "string" ? { command: entry.command } : {};
13344
14378
  }
13345
14379
  function copilotCliHooksToCanonical(rawHooks, logger) {
13346
14380
  if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
@@ -13353,7 +14387,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13353
14387
  const parseResult = CopilotCliHookEntrySchema.safeParse(rawEntry);
13354
14388
  if (!parseResult.success) continue;
13355
14389
  const entry = parseResult.data;
13356
- const timeout = entry.timeoutSec;
14390
+ const timeout = entry.timeoutSec ?? entry.timeout;
13357
14391
  const timeoutPart = timeout !== void 0 ? { timeout } : {};
13358
14392
  const matcherPart = entry.matcher !== void 0 && entry.matcher !== "" ? { matcher: entry.matcher } : {};
13359
14393
  const passthrough = importPassthrough(entry);
@@ -13372,10 +14406,11 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13372
14406
  ...passthrough
13373
14407
  });
13374
14408
  else {
13375
- const command = resolveImportCommand(entry, logger);
14409
+ const { command, shell } = resolveImportCommand(entry, logger);
13376
14410
  defs.push({
13377
14411
  type: "command",
13378
14412
  ...command !== void 0 && { command },
14413
+ ...shell !== void 0 && { shell },
13379
14414
  ...matcherPart,
13380
14415
  ...timeoutPart,
13381
14416
  ...passthrough
@@ -14025,64 +15060,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14025
15060
  }
14026
15061
  };
14027
15062
  //#endregion
14028
- //#region src/constants/grokcli-paths.ts
14029
- /**
14030
- * Grok Build CLI (xAI) configuration-layout conventions.
14031
- *
14032
- * Single source of truth for where Grok Build expects its files. Grok Build
14033
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14034
- * with project/global scopes resolved by the directory the CLI runs in
14035
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14036
- *
14037
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14038
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14039
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14040
- * @see https://docs.x.ai/build/overview
14041
- */
14042
- /** Root directory for Grok Build configuration, relative to the scope root. */
14043
- const GROKCLI_DIR = ".grok";
14044
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14045
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14046
- /**
14047
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14048
- * permission mode, and other settings all live here; permissions reuse the same
14049
- * file name as MCP since Grok consolidates everything into one config.
14050
- */
14051
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14052
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14053
- const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
14054
- /**
14055
- * Hooks directory under `.grok/`. Grok Build discovers hook config files from
14056
- * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
14057
- * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
14058
- * shape. rulesync writes all its hooks into a single `rulesync.json`.
14059
- * @see https://docs.x.ai/build/features/hooks
14060
- */
14061
- const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
14062
- /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
14063
- const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
14064
- /**
14065
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14066
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14067
- * (global), each a Markdown file with YAML frontmatter (verified via
14068
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14069
- */
14070
- const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
14071
- /**
14072
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14073
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14074
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14075
- * MCP/skills/subagents adapters).
14076
- */
14077
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14078
- /**
14079
- * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
14080
- * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
14081
- * walks, and `~/.grok/rules/` in the home scope.
14082
- * @see https://docs.x.ai/build/overview
14083
- */
14084
- const GROKCLI_RULES_DIR_PATH = join(GROKCLI_DIR, "rules");
14085
- //#endregion
14086
15063
  //#region src/features/hooks/grokcli-hooks.ts
14087
15064
  const GROKCLI_CONVERTER_CONFIG = {
14088
15065
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14328,6 +15305,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14328
15305
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14329
15306
  };
14330
15307
  }
15308
+ /**
15309
+ * `config.yaml` under every spelling the global profile root can take.
15310
+ * @see getHermesagentSharedConfigWritePaths
15311
+ */
15312
+ static getExtraSharedWritePaths() {
15313
+ return getHermesagentSharedConfigWritePaths();
15314
+ }
14331
15315
  constructor(params) {
14332
15316
  super({
14333
15317
  ...params,
@@ -14365,7 +15349,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14365
15349
  }
14366
15350
  setFileContent(fileContent) {
14367
15351
  this.fileContent = applySharedConfigPatch({
14368
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15352
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14369
15353
  feature: "hooks",
14370
15354
  existingContent: fileContent,
14371
15355
  patch: parseSharedConfig({
@@ -14515,7 +15499,23 @@ var JunieHooks = class JunieHooks extends ToolHooks {
14515
15499
  };
14516
15500
  //#endregion
14517
15501
  //#region src/features/hooks/opencode-style-generator.ts
14518
- const NAMED_HOOKS = /* @__PURE__ */ new Set(["tool.execute.before", "tool.execute.after"]);
15502
+ /**
15503
+ * Tool events emitted as named `(input, ...)` hooks rather than through the
15504
+ * generic `event.type` dispatch, mapped to the expression a hook's `matcher`
15505
+ * regex is tested against — or `null` when the hook has no matchable subject,
15506
+ * in which case a matcher is dropped rather than compiled against a field that
15507
+ * does not exist.
15508
+ *
15509
+ * `experimental.session.compacting` receives `(input, output)` and exposes no
15510
+ * per-invocation identifier worth matching on, so it takes `null`.
15511
+ *
15512
+ * @see https://opencode.ai/docs/plugins/
15513
+ */
15514
+ const NAMED_HOOK_MATCHER_SUBJECTS = {
15515
+ "tool.execute.before": "input.tool",
15516
+ "tool.execute.after": "input.tool",
15517
+ "experimental.session.compacting": null
15518
+ };
14519
15519
  function escapeForTemplateLiteral(command) {
14520
15520
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
14521
15521
  }
@@ -14548,7 +15548,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
14548
15548
  });
14549
15549
  }
14550
15550
  if (handlers.length > 0) {
14551
- const grouped = NAMED_HOOKS.has(toolEvent) ? namedEventHandlers : genericEventHandlers;
15551
+ const grouped = Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) ? namedEventHandlers : genericEventHandlers;
14552
15552
  const existing = grouped[toolEvent];
14553
15553
  if (existing) existing.push(...handlers);
14554
15554
  else grouped[toolEvent] = handlers;
@@ -14577,14 +15577,15 @@ function buildGenericEventBodyLines(genericEventHandlers) {
14577
15577
  function buildNamedEventBodyLines(namedEventHandlers) {
14578
15578
  const bodyLines = [];
14579
15579
  for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
15580
+ const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
14580
15581
  bodyLines.push(` "${eventName}": async (input) => {`);
14581
15582
  for (const handler of handlers) {
14582
15583
  const escapedCommand = escapeForTemplateLiteral(handler.command);
14583
- if (handler.matcher) {
15584
+ if (handler.matcher && matcherSubject !== null) {
14584
15585
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
14585
15586
  bodyLines.push(" {");
14586
15587
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
14587
- bodyLines.push(` if (__re.test(input.tool)) {`);
15588
+ bodyLines.push(` if (__re.test(${matcherSubject})) {`);
14588
15589
  bodyLines.push(` await $\`${escapedCommand}\`;`);
14589
15590
  bodyLines.push(" }");
14590
15591
  bodyLines.push(" }");
@@ -14724,8 +15725,38 @@ function getKimiCodeHome() {
14724
15725
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
14725
15726
  return global && getKimiCodeHome() ? relativeDirPath : join(KIMI_CODE_DIR, relativeDirPath);
14726
15727
  }
15728
+ /**
15729
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15730
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15731
+ * Declared unconditionally so the derived shared-file keys — and the drift
15732
+ * guards checked against them — do not depend on the ambient environment.
15733
+ */
15734
+ function getKimiCodeSharedConfigWritePaths() {
15735
+ return [{
15736
+ relativeDirPath: KIMI_CODE_DIR,
15737
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15738
+ }, {
15739
+ relativeDirPath: ".",
15740
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15741
+ }];
15742
+ }
15743
+ /**
15744
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15745
+ * Both spellings carry the same declaration, but passing the key of the file
15746
+ * being written keeps the write path and the drift guards on the same entry.
15747
+ */
15748
+ function getKimiCodeConfigSharedFileKey({ global }) {
15749
+ return sharedConfigFileKey({
15750
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15751
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15752
+ });
15753
+ }
14727
15754
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
14728
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15755
+ return getToolRulesyncOutputRoot({
15756
+ nativeOutputRoot,
15757
+ global,
15758
+ toolHome: getKimiCodeHome
15759
+ });
14729
15760
  }
14730
15761
  //#endregion
14731
15762
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -14823,13 +15854,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
14823
15854
  isDeletable() {
14824
15855
  return false;
14825
15856
  }
15857
+ /**
15858
+ * `config.toml` under both spellings its directory can take.
15859
+ * @see getKimiCodeSharedConfigWritePaths
15860
+ */
15861
+ static getExtraSharedWritePaths() {
15862
+ return getKimiCodeSharedConfigWritePaths();
15863
+ }
14826
15864
  shouldMergeExistingFileContent() {
14827
15865
  return true;
14828
15866
  }
14829
15867
  setFileContent(fileContent) {
14830
15868
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
14831
15869
  this.fileContent = applySharedConfigPatch({
14832
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15870
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
14833
15871
  feature: "hooks",
14834
15872
  existingContent: fileContent,
14835
15873
  patch: parseSharedConfig({
@@ -16220,7 +17258,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
16220
17258
  class: CopilotHooks,
16221
17259
  meta: {
16222
17260
  supportsProject: true,
16223
- supportsGlobal: false,
17261
+ supportsGlobal: true,
16224
17262
  supportsImport: true
16225
17263
  },
16226
17264
  supportedEvents: COPILOT_HOOK_EVENTS,
@@ -17520,6 +18558,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
17520
18558
  }
17521
18559
  };
17522
18560
  //#endregion
18561
+ //#region src/features/shared/reasonix-config-table.ts
18562
+ /**
18563
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18564
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18565
+ *
18566
+ * TOML is only structurally validated on parse, so a hand-edited config can
18567
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18568
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18569
+ * before merging, so the narrowing lives here once rather than being re-spelled
18570
+ * (and re-diverging) per feature.
18571
+ */
18572
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18573
+ function toReasonixStringArray(value) {
18574
+ if (!Array.isArray(value)) return [];
18575
+ return value.filter((entry) => typeof entry === "string");
18576
+ }
18577
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18578
+ function toReasonixTable(value) {
18579
+ if (!isPlainObject$1(value)) return {};
18580
+ return { ...value };
18581
+ }
18582
+ //#endregion
18583
+ //#region src/features/ignore/reasonix-ignore.ts
18584
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18585
+ /**
18586
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18587
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18588
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18589
+ * gateway already implements applies verbatim; only the surrounding file
18590
+ * format differs. Sibling keys such as `mode` pass through untouched.
18591
+ */
18592
+ const asClaudeStyleSettings = (document) => {
18593
+ const table = permissionsTableOf(document);
18594
+ return {
18595
+ ...document,
18596
+ permissions: {
18597
+ ...table,
18598
+ allow: toReasonixStringArray(table.allow),
18599
+ ask: toReasonixStringArray(table.ask),
18600
+ deny: toReasonixStringArray(table.deny)
18601
+ }
18602
+ };
18603
+ };
18604
+ /**
18605
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18606
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18607
+ * one.
18608
+ */
18609
+ const withoutEmptyPermissions = (settings) => {
18610
+ const document = { ...settings };
18611
+ const permissions = document.permissions;
18612
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18613
+ return document;
18614
+ };
18615
+ /**
18616
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18617
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18618
+ * `~/.reasonix/config.toml` (global).
18619
+ *
18620
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18621
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18622
+ * while `forbid_read` is documented as absolute paths with no glob support.
18623
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18624
+ */
18625
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18626
+ constructor(params) {
18627
+ super(params);
18628
+ const document = parseSharedConfig({
18629
+ format: "toml",
18630
+ fileContent: this.fileContent
18631
+ });
18632
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18633
+ }
18634
+ static getSettablePaths({ global = false } = {}) {
18635
+ return {
18636
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18637
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18638
+ };
18639
+ }
18640
+ /**
18641
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18642
+ * permissions feature and user-authored tables, so rulesync must never
18643
+ * delete it.
18644
+ */
18645
+ isDeletable() {
18646
+ return false;
18647
+ }
18648
+ toRulesyncIgnore() {
18649
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18650
+ return new RulesyncIgnore({
18651
+ outputRoot: this.outputRoot,
18652
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18653
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18654
+ fileContent: rulesyncPatterns.join("\n")
18655
+ });
18656
+ }
18657
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18658
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18659
+ const paths = this.getSettablePaths({ global });
18660
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18661
+ const existingDocument = parseSharedConfig({
18662
+ format: "toml",
18663
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18664
+ filePath
18665
+ });
18666
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18667
+ settings: asClaudeStyleSettings(existingDocument),
18668
+ readDenies
18669
+ }));
18670
+ return new ReasonixIgnore({
18671
+ outputRoot,
18672
+ relativeDirPath: paths.relativeDirPath,
18673
+ relativeFilePath: paths.relativeFilePath,
18674
+ fileContent: stringifySharedConfig({
18675
+ format: "toml",
18676
+ document
18677
+ }),
18678
+ validate: true,
18679
+ global
18680
+ });
18681
+ }
18682
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18683
+ const paths = this.getSettablePaths({ global });
18684
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18685
+ return new ReasonixIgnore({
18686
+ outputRoot,
18687
+ relativeDirPath: paths.relativeDirPath,
18688
+ relativeFilePath: paths.relativeFilePath,
18689
+ fileContent,
18690
+ validate,
18691
+ global
18692
+ });
18693
+ }
18694
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18695
+ return new ReasonixIgnore({
18696
+ outputRoot,
18697
+ relativeDirPath,
18698
+ relativeFilePath,
18699
+ fileContent: "",
18700
+ validate: false,
18701
+ global
18702
+ });
18703
+ }
18704
+ };
18705
+ //#endregion
17523
18706
  //#region src/features/ignore/roo-ignore.ts
17524
18707
  /**
17525
18708
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -17677,6 +18860,14 @@ const ZED_GLOBAL_WIN32_DIR = join("AppData", "Roaming", "Zed");
17677
18860
  function getZedGlobalDir() {
17678
18861
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
17679
18862
  }
18863
+ /**
18864
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18865
+ * spelling per platform, but the shared-write derivation (and the gateway
18866
+ * ownership table it is checked against) must know both on every platform.
18867
+ */
18868
+ function getZedOtherPlatformGlobalDir() {
18869
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18870
+ }
17680
18871
  const ZED_SETTINGS_FILE_NAME = "settings.json";
17681
18872
  const ZED_RULE_FILE_NAME = ".rules";
17682
18873
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -17689,12 +18880,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
17689
18880
  const jsonValue = JSON.parse(this.fileContent);
17690
18881
  this.patterns = jsonValue.private_files ?? [];
17691
18882
  }
17692
- static getSettablePaths() {
18883
+ static getSettablePaths({ global = false } = {}) {
17693
18884
  return {
17694
- relativeDirPath: ZED_DIR,
18885
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
17695
18886
  relativeFilePath: ZED_SETTINGS_FILE_NAME
17696
18887
  };
17697
18888
  }
18889
+ /** @see getZedOtherPlatformGlobalDir */
18890
+ static getExtraSharedWritePaths({ global = false } = {}) {
18891
+ if (!global) return [];
18892
+ return [{
18893
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18894
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18895
+ }];
18896
+ }
17698
18897
  /**
17699
18898
  * ZedIgnore uses settings.json which is a user-managed config file.
17700
18899
  * It should not be deleted by rulesync.
@@ -17705,48 +18904,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
17705
18904
  toRulesyncIgnore() {
17706
18905
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
17707
18906
  return new RulesyncIgnore({
17708
- outputRoot: this.outputRoot,
18907
+ outputRoot: ".",
17709
18908
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
17710
18909
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
17711
18910
  fileContent
17712
18911
  });
17713
18912
  }
17714
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18913
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
17715
18914
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
17716
- const filePath = join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
18915
+ const paths = this.getSettablePaths({ global });
18916
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17717
18917
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
17718
- const mergedPatterns = uniq([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18918
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
17719
18919
  return new ZedIgnore({
17720
18920
  outputRoot,
17721
- relativeDirPath: this.getSettablePaths().relativeDirPath,
17722
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18921
+ relativeDirPath: paths.relativeDirPath,
18922
+ relativeFilePath: paths.relativeFilePath,
17723
18923
  fileContent: applySharedConfigPatch({
17724
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18924
+ fileKey: sharedConfigFileKey(paths),
17725
18925
  feature: "ignore",
17726
18926
  existingContent: existingFileContent,
17727
- patch: { private_files: mergedPatterns },
18927
+ patch: { private_files: managedPatterns },
17728
18928
  filePath
17729
18929
  }),
17730
- validate: true
18930
+ validate: true,
18931
+ global
17731
18932
  });
17732
18933
  }
17733
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
17734
- const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
18934
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18935
+ const paths = this.getSettablePaths({ global });
18936
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
17735
18937
  return new ZedIgnore({
17736
18938
  outputRoot,
17737
- relativeDirPath: this.getSettablePaths().relativeDirPath,
17738
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18939
+ relativeDirPath: paths.relativeDirPath,
18940
+ relativeFilePath: paths.relativeFilePath,
17739
18941
  fileContent,
17740
- validate
18942
+ validate,
18943
+ global
17741
18944
  });
17742
18945
  }
17743
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18946
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
17744
18947
  return new ZedIgnore({
17745
18948
  outputRoot,
17746
18949
  relativeDirPath,
17747
18950
  relativeFilePath,
17748
18951
  fileContent: "{}",
17749
- validate: false
18952
+ validate: false,
18953
+ global
17750
18954
  });
17751
18955
  }
17752
18956
  };
@@ -17768,6 +18972,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
17768
18972
  ["kiro-cli", { class: KiroIgnore }],
17769
18973
  ["kiro-ide", { class: KiroIgnore }],
17770
18974
  ["qwencode", { class: QwencodeIgnore }],
18975
+ ["reasonix", { class: ReasonixIgnore }],
17771
18976
  ["roo", { class: RooIgnore }],
17772
18977
  ["devin", { class: DevinIgnore }],
17773
18978
  ["vibe", { class: VibeIgnore }],
@@ -17778,7 +18983,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
17778
18983
  const ignoreProcessorGlobalToolTargets = [
17779
18984
  "kiro",
17780
18985
  "kiro-cli",
17781
- "kiro-ide"
18986
+ "kiro-ide",
18987
+ "reasonix",
18988
+ "zed"
17782
18989
  ];
17783
18990
  const defaultGetFactory$4 = (target) => {
17784
18991
  const factory = toolIgnoreFactories.get(target);
@@ -18697,8 +19904,20 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
18697
19904
  disabledTools: "disabled_tools",
18698
19905
  envVars: "env_vars"
18699
19906
  };
19907
+ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimental_environment" };
19908
+ const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
18700
19909
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
18701
19910
  /**
19911
+ * `env_vars` entries are either a bare variable name or `{ name, source }`,
19912
+ * where `source = "remote"` reads the variable from the remote executor
19913
+ * environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
19914
+ * plain string arrays, so the widened check applies to `env_vars` only.
19915
+ * @see https://learn.chatgpt.com/docs/extend/mcp
19916
+ */
19917
+ function isValidRenamedArray(key, value) {
19918
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
19919
+ }
19920
+ /**
18702
19921
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
18703
19922
  * Code style camelCase) into the shape Codex CLI understands. Codex expects the
18704
19923
  * OAuth client id under snake_case `client_id`; without it `codex mcp login`
@@ -18758,8 +19977,12 @@ function convertFromCodexFormat(codexMcp) {
18758
19977
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
18759
19978
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
18760
19979
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
18761
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
19980
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18762
19981
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
19982
+ } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
19983
+ const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
19984
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
19985
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
18763
19986
  } else converted[key] = value;
18764
19987
  }
18765
19988
  result[name] = converted;
@@ -18781,8 +20004,12 @@ function convertToCodexFormat(mcpServers) {
18781
20004
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
18782
20005
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
18783
20006
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
18784
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
20007
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18785
20008
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
20009
+ } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
20010
+ const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
20011
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
20012
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
18786
20013
  } else converted[key] = value;
18787
20014
  }
18788
20015
  const previousName = originalNames.get(codexName);
@@ -18853,7 +20080,9 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18853
20080
  const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
18854
20081
  return [serverName, {
18855
20082
  ...serverConfig,
18856
- ...isRecord(rawServer) && isStringArray$1(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
20083
+ ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20084
+ ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20085
+ ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
18857
20086
  }];
18858
20087
  })));
18859
20088
  const filteredMcpServers = this.removeEmptyEntries(converted);
@@ -18924,9 +20153,6 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18924
20153
  };
18925
20154
  //#endregion
18926
20155
  //#region src/features/mcp/copilot-mcp.ts
18927
- function convertToCopilotFormat(mcpServers) {
18928
- return { servers: mcpServers };
18929
- }
18930
20156
  function convertFromCopilotFormat(copilotConfig) {
18931
20157
  return copilotConfig.servers ?? {};
18932
20158
  }
@@ -18955,13 +20181,21 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
18955
20181
  validate
18956
20182
  });
18957
20183
  }
18958
- static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
18959
- const copilotConfig = convertToCopilotFormat(rulesyncMcp.getMcpServers());
20184
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
20185
+ const paths = this.getSettablePaths();
20186
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20187
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
18960
20188
  return new CopilotMcp({
18961
20189
  outputRoot,
18962
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18963
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18964
- fileContent: JSON.stringify(copilotConfig, null, 2),
20190
+ relativeDirPath: paths.relativeDirPath,
20191
+ relativeFilePath: paths.relativeFilePath,
20192
+ fileContent: applySharedConfigPatch({
20193
+ fileKey: sharedConfigFileKey(paths),
20194
+ feature: "mcp",
20195
+ existingContent,
20196
+ patch: { servers: rulesyncMcp.getMcpServers() },
20197
+ filePath
20198
+ }),
18965
20199
  validate
18966
20200
  });
18967
20201
  }
@@ -20347,7 +21581,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20347
21581
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20348
21582
  this.config = merged;
20349
21583
  super.setFileContent(applySharedConfigPatch({
20350
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21584
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20351
21585
  feature: "mcp",
20352
21586
  existingContent: fileContent,
20353
21587
  patch: { mcp_servers: merged.mcp_servers }
@@ -20365,6 +21599,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20365
21599
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20366
21600
  };
20367
21601
  }
21602
+ /**
21603
+ * `config.yaml` under every spelling the global profile root can take.
21604
+ * @see getHermesagentSharedConfigWritePaths
21605
+ */
21606
+ static getExtraSharedWritePaths() {
21607
+ return getHermesagentSharedConfigWritePaths();
21608
+ }
20368
21609
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20369
21610
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20370
21611
  const paths = this.getSettablePaths({ global });
@@ -20391,7 +21632,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20391
21632
  relativeDirPath: paths.relativeDirPath,
20392
21633
  relativeFilePath: paths.relativeFilePath,
20393
21634
  fileContent: applySharedConfigPatch({
20394
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21635
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20395
21636
  feature: "mcp",
20396
21637
  existingContent: fileContent,
20397
21638
  patch: { mcp_servers: merged.mcp_servers }
@@ -20644,7 +21885,7 @@ const KILO_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
20644
21885
  * impossible to start — no command, no URL — so it is written as the toggle it
20645
21886
  * resembles rather than dropped, but the loss is said out loud.
20646
21887
  */
20647
- function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21888
+ function warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger) {
20648
21889
  const dropped = Object.keys(serverConfig).filter((key) => !KILO_TOGGLE_KEPT_KEYS.has(key));
20649
21890
  if (dropped.length === 0) return;
20650
21891
  logger?.warn(`Kilo MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
@@ -20657,7 +21898,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20657
21898
  if (declaresNoTransport(serverConfig)) {
20658
21899
  if (serverConfig.disabled === void 0) {
20659
21900
  if (existingEntry !== void 0 && !isKiloTransportServer(existingEntry)) {
20660
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21901
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20661
21902
  return existingEntry;
20662
21903
  }
20663
21904
  return warnAndSkipMcpServer({
@@ -20667,7 +21908,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20667
21908
  logger
20668
21909
  });
20669
21910
  }
20670
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21911
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20671
21912
  return { enabled: !serverConfig.disabled };
20672
21913
  }
20673
21914
  if (isRemoteMcpServer(serverConfig)) {
@@ -20852,9 +22093,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
20852
22093
  * Merge a list of project rule file globs into the `instructions` array of the
20853
22094
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
20854
22095
  * (notably `mcp`/`tools` written by the MCP feature). In Kilo v7, files under
20855
- * `.kilo/rules/` are NOT auto-loaded; they are only picked up when listed in
20856
- * the `instructions` key. The resulting `instructions` list is deduped and
20857
- * sorted for a stable output.
22096
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22097
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22098
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22099
+ * config load — which is why `KiloRule` registers instructions in project
22100
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22101
+ * stable output.
20858
22102
  *
20859
22103
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
20860
22104
  */
@@ -21067,7 +22311,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21067
22311
  const existingContent = existing.content;
21068
22312
  const existingSection = existing.mcp;
21069
22313
  const fileContent = applySharedConfigPatch({
21070
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22314
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21071
22315
  feature: "mcp",
21072
22316
  existingContent,
21073
22317
  patch: { mcp: {
@@ -21154,11 +22398,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21154
22398
  * derivation sees this feature as one of that file's writers — it is not a
21155
22399
  * settable path, since the servers themselves live in `mcp.json`.
21156
22400
  */
21157
- static getExtraSharedWritePaths({ global = false } = {}) {
21158
- return global ? [{
21159
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21160
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21161
- }] : [];
22401
+ static getExtraSharedWritePaths() {
22402
+ return getKimiCodeSharedConfigWritePaths();
21162
22403
  }
21163
22404
  /**
21164
22405
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -21293,13 +22534,49 @@ const OpencodeMcpRemoteServerSchema = z.looseObject({
21293
22534
  headers: z.optional(z.record(z.string(), z.string())),
21294
22535
  enabled: z._default(z.boolean(), true)
21295
22536
  });
21296
- const OpencodeMcpServerSchema = z.union([OpencodeMcpLocalServerSchema, OpencodeMcpRemoteServerSchema]);
22537
+ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
22538
+ const OPENCODE_MCP_TRANSPORT_KEYS = [
22539
+ ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
22540
+ ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
22541
+ ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
22542
+ ].filter((key) => key !== "enabled");
22543
+ /**
22544
+ * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
22545
+ * disabling a server another config layer defines. It is the third member of
22546
+ * OpenCode's own `mcp` union in the published schema, described in-source as
22547
+ * "the legacy `{ enabled: false }` form used to disable a server". Without this
22548
+ * arm the union rejects the entry and the whole MCP import aborts — taking
22549
+ * every valid server in the same file down with it.
22550
+ *
22551
+ * Loose, unlike the two transport arms, so a key OpenCode adds to a toggle
22552
+ * later does not bring that abort back — but refined to reject anything
22553
+ * carrying a key only a transport entry has. A plain loose arm would sit under
22554
+ * a malformed `local` or `remote` entry that happens to carry `enabled` and
22555
+ * swallow it, dropping its command, URL, or headers without a word instead of
22556
+ * failing the way it does today. Mirrors {@link KiloMcpToggleSchema}.
22557
+ *
22558
+ * @see https://opencode.ai/config.json
22559
+ */
22560
+ const OpencodeMcpToggleSchema = z.looseObject({ enabled: z.boolean() }).check(refine((entry) => OPENCODE_MCP_TRANSPORT_KEYS.every((key) => !(key in entry)), "not a valid OpenCode MCP server: expected a local server ({type: \"local\", command: [...]}), a remote server ({type: \"remote\", url: \"...\"}), or a bare toggle ({enabled: <bool>}) carrying no field of either"));
22561
+ const OpencodeMcpServerSchema = z.union([
22562
+ OpencodeMcpLocalServerSchema,
22563
+ OpencodeMcpRemoteServerSchema,
22564
+ OpencodeMcpToggleSchema
22565
+ ]);
21297
22566
  const OpencodeConfigSchema = z.looseObject({
21298
22567
  $schema: z.optional(z.string()),
21299
22568
  mcp: z.optional(z.record(z.string(), OpencodeMcpServerSchema)),
21300
22569
  tools: z.optional(z.record(z.string(), z.boolean()))
21301
22570
  });
21302
22571
  /**
22572
+ * Tell the two transport arms from a toggle entry. Both carry a `type` literal
22573
+ * and a toggle never does — the schema refuses one that tries — but the toggle
22574
+ * arm is loose, so its index signature hides that from `in` narrowing.
22575
+ */
22576
+ function isOpencodeTransportServer(server) {
22577
+ return server.type === "local" || server.type === "remote";
22578
+ }
22579
+ /**
21303
22580
  * Convert OpenCode native format back to standard MCP format
21304
22581
  * - type: "local" -> "stdio", "remote" -> "sse"
21305
22582
  * - command (array) -> command (first element) + args (rest)
@@ -21324,19 +22601,31 @@ function convertFromOpencodeFormat(opencodeMcp, tools) {
21324
22601
  ...convertOpencodeServers(opencodeMcp, tools)
21325
22602
  };
21326
22603
  }
22604
+ /** Split the shared top-level `tools` map into this server's own two lists. */
22605
+ function splitOpencodeServerTools(serverName, tools) {
22606
+ const enabledTools = [];
22607
+ const disabledTools = [];
22608
+ const prefix = `${serverName}_`;
22609
+ for (const [toolName, enabled] of Object.entries(tools ?? {})) {
22610
+ if (!toolName.startsWith(prefix)) continue;
22611
+ const toolSuffix = toolName.slice(prefix.length);
22612
+ (enabled ? enabledTools : disabledTools).push(toolSuffix);
22613
+ }
22614
+ return {
22615
+ enabledTools,
22616
+ disabledTools
22617
+ };
22618
+ }
21327
22619
  function convertOpencodeServers(opencodeMcp, tools) {
21328
22620
  return Object.fromEntries(Object.entries(opencodeMcp).map(([serverName, serverConfig]) => {
21329
22621
  const extraFields = Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !OPENCODE_KNOWN_SERVER_KEYS.has(key)));
21330
- const enabledTools = [];
21331
- const disabledTools = [];
21332
- const prefix = `${serverName}_`;
21333
- if (tools) {
21334
- for (const [toolName, enabled] of Object.entries(tools)) if (toolName.startsWith(prefix)) {
21335
- const toolSuffix = toolName.slice(prefix.length);
21336
- if (enabled) enabledTools.push(toolSuffix);
21337
- else disabledTools.push(toolSuffix);
21338
- }
21339
- }
22622
+ const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, tools);
22623
+ if (!isOpencodeTransportServer(serverConfig)) return [serverName, {
22624
+ ...extraFields,
22625
+ disabled: serverConfig.enabled === false,
22626
+ ...enabledTools.length > 0 && { enabledTools },
22627
+ ...disabledTools.length > 0 && { disabledTools }
22628
+ }];
21340
22629
  if (serverConfig.type === "remote") return [serverName, {
21341
22630
  ...extraFields,
21342
22631
  type: "sse",
@@ -21366,7 +22655,20 @@ function convertOpencodeServers(opencodeMcp, tools) {
21366
22655
  }];
21367
22656
  }));
21368
22657
  }
21369
- const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
22658
+ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
22659
+ "disabled",
22660
+ "enabledTools",
22661
+ "disabledTools"
22662
+ ]);
22663
+ /**
22664
+ * Warn about the fields a transport-less server loses by being written as a
22665
+ * bare toggle, so the drop is never silent.
22666
+ */
22667
+ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
22668
+ const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
22669
+ if (dropped.length === 0) return;
22670
+ logger?.warn(`OpenCode MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
22671
+ }
21370
22672
  /**
21371
22673
  * Convert standard MCP format to OpenCode native format
21372
22674
  * - type: "stdio" -> "local", "sse"/"http" -> "remote"
@@ -21376,11 +22678,27 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21376
22678
  * - enabledTools/disabledTools -> top-level tools map (with server name prefix)
21377
22679
  * - OpenCode-supported extras (timeout, oauth) -> passed through verbatim
21378
22680
  */
21379
- function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
22681
+ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
21380
22682
  const serverRecord = serverConfig;
21381
22683
  const passthrough = {};
21382
22684
  for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
21383
22685
  const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
22686
+ if (declaresNoTransport(serverConfig)) {
22687
+ if (serverConfig.disabled === void 0) {
22688
+ if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
22689
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
22690
+ return existingEntry;
22691
+ }
22692
+ return warnAndSkipMcpServer({
22693
+ toolName: "OpenCode",
22694
+ serverName,
22695
+ reason: "no transport and no enabled state, so there is nothing to toggle",
22696
+ logger
22697
+ });
22698
+ }
22699
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
22700
+ return { enabled };
22701
+ }
21384
22702
  if (isRemoteMcpServer(serverConfig)) {
21385
22703
  const url = resolveRemoteMcpUrl(serverConfig);
21386
22704
  if (url === void 0) return warnAndSkipMcpServer({
@@ -21401,7 +22719,7 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21401
22719
  if (commandArray.length === 0) return warnAndSkipMcpServer({
21402
22720
  toolName: "OpenCode",
21403
22721
  serverName,
21404
- reason: declaresNoTransport(serverConfig) ? "no transport at all" : "a local transport but no command",
22722
+ reason: "a local transport but no command",
21405
22723
  logger
21406
22724
  });
21407
22725
  return {
@@ -21413,11 +22731,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21413
22731
  ...serverConfig.cwd && { cwd: serverConfig.cwd }
21414
22732
  };
21415
22733
  }
21416
- function convertToOpencodeFormat(mcpServers, logger) {
22734
+ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
21417
22735
  const tools = {};
21418
22736
  return {
21419
22737
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
21420
- const converted = convertServerToOpencodeFormat(serverName, serverConfig, logger);
22738
+ const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
21421
22739
  if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
21422
22740
  if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
21423
22741
  return converted === null ? null : [serverName, converted];
@@ -21487,10 +22805,12 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
21487
22805
  fileContent = await readFileContentOrNull(jsonPath);
21488
22806
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
21489
22807
  }
21490
- const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
22808
+ const transformedServers = convertEnvVarRefsToToolFormat({
21491
22809
  mcpServers: rulesyncMcp.getMcpServers(),
21492
22810
  replacement: "{env:$1}"
21493
- }), logger);
22811
+ });
22812
+ const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
22813
+ const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
21494
22814
  return new OpencodeMcp({
21495
22815
  outputRoot,
21496
22816
  relativeDirPath: basePaths.relativeDirPath,
@@ -22715,16 +24035,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
22715
24035
  relativeFilePath: ZED_SETTINGS_FILE_NAME
22716
24036
  };
22717
24037
  }
22718
- /**
22719
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
22720
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
22721
- * derivation (and the gateway ownership table it is checked against) must
22722
- * know both spellings on every platform.
22723
- */
24038
+ /** @see getZedOtherPlatformGlobalDir */
22724
24039
  static getExtraSharedWritePaths({ global = false } = {}) {
22725
24040
  if (!global) return [];
22726
24041
  return [{
22727
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24042
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
22728
24043
  relativeFilePath: ZED_SETTINGS_FILE_NAME
22729
24044
  }];
22730
24045
  }
@@ -23800,10 +25115,11 @@ function buildPermissionEntry$1(toolName, pattern) {
23800
25115
  * file (global scope only). The file holds other CLI settings besides
23801
25116
  * permissions, so it is never deleted.
23802
25117
  *
23803
- * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
23804
- * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
23805
- * authored and round-tripped through the `antigravity-cli` permissions override
23806
- * (see `AntigravityCliPermissionsOverrideSchema`).
25118
+ * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
25119
+ * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
25120
+ * `artifactReviewPolicy` and `allowNonWorkspaceAccess` are authored and
25121
+ * round-tripped through the `antigravity-cli` permissions override (see
25122
+ * `AntigravityCliPermissionsOverrideSchema`).
23807
25123
  */
23808
25124
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
23809
25125
  constructor(params) {
@@ -23867,6 +25183,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23867
25183
  const override = config["antigravity-cli"];
23868
25184
  if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
23869
25185
  if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
25186
+ if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
25187
+ if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
23870
25188
  const fileContent = JSON.stringify(merged, null, 2);
23871
25189
  return new AntigravityCliPermissions({
23872
25190
  outputRoot,
@@ -23893,6 +25211,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23893
25211
  const override = {};
23894
25212
  if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
23895
25213
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
25214
+ if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
25215
+ if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
23896
25216
  const result = { ...config };
23897
25217
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
23898
25218
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -24696,12 +26016,57 @@ function parseClaudePermissionEntry(entry) {
24696
26016
  };
24697
26017
  }
24698
26018
  /**
26019
+ * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
26020
+ * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
26021
+ * but never matched by those checks, so Claude Code warns at startup for each
26022
+ * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
26023
+ * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
26024
+ * docs prescribe instead. A tool-name rule with no path is unaffected: it
26025
+ * matches the tool everywhere and produces no warning.
26026
+ * @see https://code.claude.com/docs/en/permissions
26027
+ */
26028
+ function isPlainRecord(value) {
26029
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26030
+ }
26031
+ /**
26032
+ * Merge `patch` into `base`, recursing into plain objects so a sibling key at
26033
+ * any depth survives. Arrays and scalars are replaced, since a list the author
26034
+ * states is the list they mean.
26035
+ */
26036
+ function deepMergeRecords(base, patch) {
26037
+ const merged = { ...base };
26038
+ for (const [key, value] of Object.entries(patch)) {
26039
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
26040
+ const existing = merged[key];
26041
+ merged[key] = isPlainRecord(existing) && isPlainRecord(value) ? deepMergeRecords(existing, value) : value;
26042
+ }
26043
+ return merged;
26044
+ }
26045
+ const CLAUDE_PATH_RULE_ALIASES = {
26046
+ Write: "Edit",
26047
+ NotebookEdit: "Edit",
26048
+ Glob: "Read"
26049
+ };
26050
+ /**
24699
26051
  * Build a Claude Code permission entry like "Bash(npm run *)".
24700
26052
  * If the pattern is "*", returns just the tool name.
24701
26053
  */
24702
26054
  function buildClaudePermissionEntry(toolName, pattern) {
24703
26055
  if (pattern === "*") return toolName;
24704
- return `${toolName}(${pattern})`;
26056
+ return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
26057
+ }
26058
+ /**
26059
+ * The Claude tool names the canonical config manages. Deliberately the tool
26060
+ * names the categories map to and *not* the aliases a path rule is rewritten
26061
+ * to: claiming `Edit` because a `write` rule exists would sweep away the
26062
+ * `Read`/`Edit` entries the ignore feature and the user wrote in the same file.
26063
+ * The rewritten entries are still rulesync's to place — `applyPermissions`
26064
+ * replaces an entry this run emits wherever it currently sits — and the
26065
+ * original name stays claimed so an entry an older rulesync wrote in the warned
26066
+ * form is cleaned up on the next generate.
26067
+ */
26068
+ function managedClaudeToolNames(config) {
26069
+ return new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
24705
26070
  }
24706
26071
  var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions {
24707
26072
  constructor(params) {
@@ -24741,7 +26106,10 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24741
26106
  throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
24742
26107
  }
24743
26108
  const config = rulesyncPermissions.getJson();
24744
- const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
26109
+ const { allow, ask, deny } = convertRulesyncToClaudePermissions({
26110
+ config,
26111
+ logger
26112
+ });
24745
26113
  const overridePermissions = config.claudecode?.permissions;
24746
26114
  if (overridePermissions && typeof overridePermissions === "object") {
24747
26115
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
@@ -24750,7 +26118,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24750
26118
  ...nonListFields
24751
26119
  };
24752
26120
  }
24753
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
26121
+ const overrideSandbox = config.claudecode?.sandbox;
26122
+ if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
26123
+ const managedToolNames = managedClaudeToolNames(config);
24754
26124
  const merged = applyPermissions({
24755
26125
  settings,
24756
26126
  managedToolNames,
@@ -24784,6 +26154,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24784
26154
  });
24785
26155
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
24786
26156
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
26157
+ const { sandbox } = settings;
26158
+ if (isPlainRecord(sandbox) && Object.keys(sandbox).length > 0) config.claudecode = {
26159
+ ...config.claudecode,
26160
+ sandbox
26161
+ };
24787
26162
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
24788
26163
  }
24789
26164
  validate() {
@@ -24805,14 +26180,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24805
26180
  /**
24806
26181
  * Convert rulesync permissions config to Claude Code allow/ask/deny arrays.
24807
26182
  */
24808
- function convertRulesyncToClaudePermissions(config) {
26183
+ function convertRulesyncToClaudePermissions({ config, logger }) {
24809
26184
  const allow = [];
24810
26185
  const ask = [];
24811
26186
  const deny = [];
26187
+ const actionByEntry = /* @__PURE__ */ new Map();
24812
26188
  for (const [category, rules] of Object.entries(config.permission)) {
24813
26189
  const claudeToolName = toClaudeToolName(category);
24814
26190
  for (const [pattern, action] of Object.entries(rules)) {
24815
26191
  const entry = buildClaudePermissionEntry(claudeToolName, pattern);
26192
+ const previous = actionByEntry.get(entry);
26193
+ if (previous !== void 0 && previous !== action) logger?.warn(`Claude Code permissions: rules from different categories both resolve to "${entry}" with conflicting actions (${previous} and ${action}). Both are written; Claude Code applies deny first, then ask, then allow.`);
26194
+ actionByEntry.set(entry, action);
24816
26195
  switch (action) {
24817
26196
  case "allow":
24818
26197
  allow.push(entry);
@@ -25625,19 +27004,24 @@ function mapBashActionToDecision(action) {
25625
27004
  //#endregion
25626
27005
  //#region src/features/permissions/copilot-permissions.ts
25627
27006
  /**
25628
- * The flat, dotted VS Code setting key this adapter manages. VS Code stores
25629
- * settings with dotted keys flat at the document top level, so this is a single
25630
- * literal key — not a nested `chat.tools.terminal` path.
27007
+ * The flat, dotted VS Code setting keys this adapter manages, one per canonical
27008
+ * permission category. VS Code stores settings with dotted keys flat at the
27009
+ * document top level, so each is a single literal key — not a nested
27010
+ * `chat.tools.terminal` path. All three share the same
27011
+ * pattern-to-boolean shape, so one conversion covers them.
27012
+ *
27013
+ * The canonical `read` and `write` categories stay unmapped: VS Code has no
27014
+ * read-approval surface, and folding `write` into the edits map alongside
27015
+ * `edit` would make the two indistinguishable on import.
27016
+ *
25631
27017
  * @see https://code.visualstudio.com/docs/agents/approvals
27018
+ * @see https://code.visualstudio.com/docs/copilot/chat/review-code-edits
25632
27019
  */
25633
- const AUTO_APPROVE_KEY = "chat.tools.terminal.autoApprove";
25634
- /**
25635
- * The canonical permission category this adapter maps. Only shell/terminal
25636
- * commands (`bash`) have a clean, high-fidelity representation in VS Code's
25637
- * `chat.tools.terminal.autoApprove` map; other categories (read/edit/webfetch/
25638
- * …) have no terminal-command equivalent and are intentionally not mapped.
25639
- */
25640
- const TERMINAL_CATEGORY = "bash";
27020
+ const AUTO_APPROVE_KEYS = {
27021
+ bash: "chat.tools.terminal.autoApprove",
27022
+ edit: "chat.tools.edits.autoApprove",
27023
+ webfetch: "chat.tools.urls.autoApprove"
27024
+ };
25641
27025
  function asAutoApproveMap(value) {
25642
27026
  if (!isPlainObject$1(value)) return {};
25643
27027
  const result = {};
@@ -25645,6 +27029,21 @@ function asAutoApproveMap(value) {
25645
27029
  return result;
25646
27030
  }
25647
27031
  /**
27032
+ * Render one canonical category's rules as a VS Code auto-approve map. Returns
27033
+ * `undefined` when the category contributes nothing, so the key is retracted
27034
+ * rather than written as an empty object.
27035
+ *
27036
+ * The resulting map replaces the file's existing value wholesale — rulesync
27037
+ * owns these keys, so a rule dropped from the canonical config disappears from
27038
+ * the settings file too.
27039
+ */
27040
+ function buildAutoApproveValue(rules) {
27041
+ const autoApprove = {};
27042
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
27043
+ else if (action === "deny") autoApprove[pattern] = false;
27044
+ return Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
27045
+ }
27046
+ /**
25648
27047
  * Permissions generator for GitHub Copilot Chat in VS Code.
25649
27048
  *
25650
27049
  * VS Code has no standalone, environment-agnostic Copilot policy file (like
@@ -25654,13 +27053,16 @@ function asAutoApproveMap(value) {
25654
27053
  * many unrelated keys, so reads and writes merge into the existing JSON
25655
27054
  * (touching only the one managed key) and the file is never deleted.
25656
27055
  *
25657
- * Scope is deliberately limited to `chat.tools.terminal.autoApprove` the one
25658
- * clean, non-lossy mapping. The canonical `bash` category's per-pattern rules
25659
- * map as: `allow` → `true` (auto-approve), `deny` → `false` (never approve),
25660
- * and `ask` → the entry is OMITTED (VS Code then falls through to its default
25661
- * in-chat approval prompt, i.e. "ask"). Only project scope is modeled: VS Code's
25662
- * user-scope settings.json lives at a platform-dependent path outside rulesync's
25663
- * home-relative global model.
27056
+ * Three canonical categories have a clean, non-lossy representation and are
27057
+ * mapped (see {@link AUTO_APPROVE_KEYS}): `bash`, `edit` and `webfetch`. In
27058
+ * every one, per-pattern rules map as: `allow` → `true` (auto-approve), `deny`
27059
+ * `false` (VS Code then always prompts note this is "never auto-approve",
27060
+ * not a hard block), and `ask` the entry is OMITTED (VS Code falls through to
27061
+ * the same default prompt). A key whose canonical category is absent entirely
27062
+ * is left untouched, so authoring only `bash` rules never disturbs a
27063
+ * hand-written edits or urls map.
27064
+ * Only project scope is modeled: VS Code's user-scope settings.json lives at a
27065
+ * platform-dependent path outside rulesync's home-relative global model.
25664
27066
  */
25665
27067
  var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25666
27068
  constructor(params) {
@@ -25697,11 +27099,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25697
27099
  const paths = CopilotPermissions.getSettablePaths();
25698
27100
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25699
27101
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25700
- const rules = rulesyncPermissions.getJson().permission[TERMINAL_CATEGORY] ?? {};
25701
- const autoApprove = {};
25702
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
25703
- else if (action === "deny") autoApprove[pattern] = false;
25704
- const patchValue = Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
27102
+ const config = rulesyncPermissions.getJson();
27103
+ const patch = {};
27104
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
27105
+ const rules = config.permission[category];
27106
+ if (rules === void 0) continue;
27107
+ patch[settingKey] = buildAutoApproveValue(rules);
27108
+ }
25705
27109
  return new CopilotPermissions({
25706
27110
  outputRoot,
25707
27111
  relativeDirPath: paths.relativeDirPath,
@@ -25710,7 +27114,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25710
27114
  fileKey: sharedConfigFileKey(paths),
25711
27115
  feature: "permissions",
25712
27116
  existingContent,
25713
- patch: { [AUTO_APPROVE_KEY]: patchValue },
27117
+ patch,
25714
27118
  filePath
25715
27119
  }),
25716
27120
  validate: true
@@ -25729,10 +27133,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25729
27133
  } catch (error) {
25730
27134
  throw new Error(`Failed to parse Copilot VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
25731
27135
  }
25732
- const autoApprove = asAutoApproveMap(settings[AUTO_APPROVE_KEY]);
25733
- const rules = {};
25734
- for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
25735
- const permission = Object.keys(rules).length > 0 ? { [TERMINAL_CATEGORY]: rules } : {};
27136
+ const permission = {};
27137
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
27138
+ const autoApprove = asAutoApproveMap(settings[settingKey]);
27139
+ const rules = {};
27140
+ for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
27141
+ if (Object.keys(rules).length > 0) permission[category] = rules;
27142
+ }
25736
27143
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
25737
27144
  }
25738
27145
  validate() {
@@ -27062,6 +28469,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27062
28469
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27063
28470
  };
27064
28471
  }
28472
+ /**
28473
+ * `config.yaml` under every spelling the global profile root can take.
28474
+ * @see getHermesagentSharedConfigWritePaths
28475
+ */
28476
+ static getExtraSharedWritePaths() {
28477
+ return getHermesagentSharedConfigWritePaths();
28478
+ }
27065
28479
  constructor(params) {
27066
28480
  super({
27067
28481
  ...params,
@@ -27099,7 +28513,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27099
28513
  }
27100
28514
  setFileContent(fileContent) {
27101
28515
  this.fileContent = applySharedConfigPatch({
27102
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28516
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27103
28517
  feature: "permissions",
27104
28518
  existingContent: fileContent,
27105
28519
  patch: parseSharedConfig({
@@ -27512,7 +28926,10 @@ const KiloPermissionSchema = z.union([z.enum([
27512
28926
  "ask",
27513
28927
  "deny"
27514
28928
  ]))]);
27515
- const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
28929
+ const KiloPermissionsConfigSchema = z.looseObject({
28930
+ permission: z.optional(z.record(z.string(), KiloPermissionSchema)),
28931
+ sandbox: z.optional(z.unknown())
28932
+ });
27516
28933
  /**
27517
28934
  * Kilo permission keys that share a name with a canonical rulesync category and
27518
28935
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -27569,6 +28986,25 @@ function collectKiloDenyPatterns(value) {
27569
28986
  }
27570
28987
  return [];
27571
28988
  }
28989
+ function asKiloRecord(value) {
28990
+ return isPlainObject$1(value) ? { ...value } : {};
28991
+ }
28992
+ /**
28993
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
28994
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
28995
+ * project config merely tighten — so writing the wider keys into a project file
28996
+ * would produce config Kilo ignores.
28997
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
28998
+ */
28999
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29000
+ function narrowSandboxToProjectScope({ authored, logger }) {
29001
+ const emitted = {};
29002
+ const dropped = [];
29003
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29004
+ else dropped.push(key);
29005
+ if (dropped.length > 0) logger?.warn(`Kilo honors these 'sandbox' keys from the global config only, so they were dropped from the project config: ${dropped.toSorted().join(", ")}. A project config may only tighten the sandbox ('enabled', 'network'); generate with --global to author the rest.`);
29006
+ return emitted;
29007
+ }
27572
29008
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
27573
29009
  json;
27574
29010
  constructor(params) {
@@ -27648,8 +29084,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27648
29084
  const basePaths = KiloPermissions.getSettablePaths({ global });
27649
29085
  const filePath = join(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
27650
29086
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
27651
- const parsedPermission = parsed.permission;
27652
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29087
+ const existingPermission = asKiloRecord(parsed.permission);
27653
29088
  const rulesyncJson = rulesyncPermissions.getJson();
27654
29089
  const kiloOverride = rulesyncJson.kilo;
27655
29090
  const incomingPermission = {
@@ -27675,6 +29110,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27675
29110
  ...parsed,
27676
29111
  permission: mergedPermission
27677
29112
  };
29113
+ if (kiloOverride?.sandbox !== void 0) {
29114
+ const authored = asKiloRecord(kiloOverride.sandbox);
29115
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29116
+ authored,
29117
+ logger
29118
+ });
29119
+ const merged = {
29120
+ ...asKiloRecord(parsed.sandbox),
29121
+ ...emitted
29122
+ };
29123
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29124
+ }
27678
29125
  return new KiloPermissions({
27679
29126
  outputRoot,
27680
29127
  relativeDirPath: basePaths.relativeDirPath,
@@ -27689,9 +29136,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27689
29136
  const overrideOnly = {};
27690
29137
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
27691
29138
  else overrideOnly[key] = value;
27692
- const json = Object.keys(overrideOnly).length > 0 ? {
29139
+ const sandbox = this.json.sandbox;
29140
+ const override = {
29141
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29142
+ ...isPlainObject$1(sandbox) && { sandbox }
29143
+ };
29144
+ const json = Object.keys(override).length > 0 ? {
27693
29145
  permission: shared,
27694
- kilo: { permission: overrideOnly }
29146
+ kilo: override
27695
29147
  } : { permission: shared };
27696
29148
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
27697
29149
  }
@@ -27932,6 +29384,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
27932
29384
  isDeletable() {
27933
29385
  return false;
27934
29386
  }
29387
+ /**
29388
+ * `config.toml` under both spellings its directory can take.
29389
+ * @see getKimiCodeSharedConfigWritePaths
29390
+ */
29391
+ static getExtraSharedWritePaths() {
29392
+ return getKimiCodeSharedConfigWritePaths();
29393
+ }
27935
29394
  shouldMergeExistingFileContent() {
27936
29395
  return true;
27937
29396
  }
@@ -27946,7 +29405,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
27946
29405
  patch
27947
29406
  });
27948
29407
  this.fileContent = applySharedConfigPatch({
27949
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29408
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
27950
29409
  feature: "permissions",
27951
29410
  existingContent: fileContent,
27952
29411
  patch: {
@@ -28870,14 +30329,6 @@ function parseReasonixConfig(fileContent) {
28870
30329
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
28871
30330
  return { ...parsed };
28872
30331
  }
28873
- function toStringArray$1(value) {
28874
- if (!Array.isArray(value)) return [];
28875
- return value.filter((entry) => typeof entry === "string");
28876
- }
28877
- function toPermissionsTable(value) {
28878
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
28879
- return { ...value };
28880
- }
28881
30332
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
28882
30333
  /**
28883
30334
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -28934,12 +30385,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
28934
30385
  const config = rulesyncPermissions.getJson();
28935
30386
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
28936
30387
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
28937
- const existingPermissions = toPermissionsTable(parsed.permissions);
28938
- const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28939
- const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28940
- const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30388
+ const existingPermissions = toReasonixTable(parsed.permissions);
30389
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30390
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30391
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28941
30392
  if (logger && managedToolNames.has("Read")) {
28942
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30393
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
28943
30394
  const { toolName } = parseReasonixPermissionEntry(entry);
28944
30395
  return toolName === "Read";
28945
30396
  });
@@ -28986,11 +30437,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
28986
30437
  });
28987
30438
  }
28988
30439
  toRulesyncPermissions() {
28989
- const permissions = toPermissionsTable(this.toml.permissions);
30440
+ const permissions = toReasonixTable(this.toml.permissions);
28990
30441
  const config = convertReasonixToRulesyncPermissions({
28991
- allow: toStringArray$1(permissions.allow),
28992
- ask: toStringArray$1(permissions.ask),
28993
- deny: toStringArray$1(permissions.deny)
30442
+ allow: toReasonixStringArray(permissions.allow),
30443
+ ask: toReasonixStringArray(permissions.ask),
30444
+ deny: toReasonixStringArray(permissions.deny)
28994
30445
  });
28995
30446
  const sandbox = asReasonixRecord(this.toml.sandbox);
28996
30447
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29082,19 +30533,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29082
30533
  "open_files",
29083
30534
  "expand_code_chunks",
29084
30535
  "expand_folder",
29085
- "grep"
30536
+ "grep",
30537
+ "getJiraIssue",
30538
+ "getConfluencePage"
29086
30539
  ],
29087
30540
  edit: [
29088
30541
  "find_and_replace_code",
29089
30542
  "create_file",
29090
30543
  "delete_file",
29091
- "move_file"
30544
+ "move_file",
30545
+ "createTechnicalPlan",
30546
+ "createJiraIssue",
30547
+ "updateJiraIssue",
30548
+ "createConfluencePage",
30549
+ "updateConfluencePage"
29092
30550
  ],
29093
30551
  write: [
29094
30552
  "create_file",
29095
30553
  "delete_file",
29096
30554
  "move_file",
29097
- "find_and_replace_code"
30555
+ "find_and_replace_code",
30556
+ "createTechnicalPlan",
30557
+ "createJiraIssue",
30558
+ "updateJiraIssue",
30559
+ "createConfluencePage",
30560
+ "updateConfluencePage"
29098
30561
  ]
29099
30562
  };
29100
30563
  const TOOL_KEY_TO_CATEGORY = {
@@ -29102,13 +30565,24 @@ const TOOL_KEY_TO_CATEGORY = {
29102
30565
  expand_code_chunks: "read",
29103
30566
  expand_folder: "read",
29104
30567
  grep: "read",
30568
+ getJiraIssue: "read",
30569
+ getConfluencePage: "read",
29105
30570
  find_and_replace_code: "edit",
29106
30571
  create_file: "edit",
29107
30572
  delete_file: "edit",
29108
- move_file: "edit"
30573
+ move_file: "edit",
30574
+ createTechnicalPlan: "edit",
30575
+ createJiraIssue: "edit",
30576
+ updateJiraIssue: "edit",
30577
+ createConfluencePage: "edit",
30578
+ updateConfluencePage: "edit"
29109
30579
  };
29110
30580
  const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
29111
- const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
30581
+ const OWNED_TOOL_PERMISSION_KEYS = [
30582
+ "bash",
30583
+ "allowedExternalPaths",
30584
+ "default"
30585
+ ];
29112
30586
  /**
29113
30587
  * Permissions adapter for Rovo Dev CLI.
29114
30588
  *
@@ -29122,9 +30596,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29122
30596
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29123
30597
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29124
30598
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30599
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30600
+ * the level Rovo Dev falls back to for any tool with no more specific
30601
+ * setting (Rovo Dev's own default is `ask`).
29125
30602
  * - `read` -> the inspection tools (`open_files`, `expand_code_chunks`,
29126
- * `expand_folder`, `grep`); `edit`/`write` -> the mutation tools
29127
- * (`find_and_replace_code`, `create_file`, `delete_file`, `move_file`).
30603
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30604
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30605
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30606
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30607
+ * `updateConfluencePage`) — so these two categories reach Jira and
30608
+ * Confluence, not just the working tree.
29128
30609
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29129
30610
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29130
30611
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -29304,6 +30785,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
29304
30785
  delete toolPermissions.allowedExternalPaths;
29305
30786
  strippedKeys.push("allowedExternalPaths");
29306
30787
  }
30788
+ if (toolPermissions.default === "allow") {
30789
+ delete toolPermissions.default;
30790
+ strippedKeys.push("default");
30791
+ }
29307
30792
  const bash = toolPermissions.bash;
29308
30793
  if (isRecord(bash)) {
29309
30794
  const stripped = { ...bash };
@@ -29328,10 +30813,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
29328
30813
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
29329
30814
  const toolPermissions = {};
29330
30815
  const allowedExternalPaths = [];
29331
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
29332
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
29333
- if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30816
+ warnOnEditWriteConflict({
30817
+ config,
30818
+ logger
30819
+ });
29334
30820
  for (const [category, rules] of Object.entries(config.permission)) {
30821
+ if (category === CATCH_ALL_PATTERN$1) {
30822
+ const toolWideDefault = convertAllToolsRules({
30823
+ rules,
30824
+ logger
30825
+ });
30826
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30827
+ continue;
30828
+ }
29335
30829
  if (category === "bash") {
29336
30830
  const bash = convertBashRules(rules);
29337
30831
  if (bash) toolPermissions.bash = bash;
@@ -29358,6 +30852,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
29358
30852
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
29359
30853
  return toolPermissions;
29360
30854
  }
30855
+ /**
30856
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30857
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30858
+ * is happening; the conversion keeps the stricter of the two — the same
30859
+ * fail-closed rule the import direction uses when those tools disagree, so the
30860
+ * resolution never grants more than the author asked for.
30861
+ */
30862
+ function warnOnEditWriteConflict({ config, logger }) {
30863
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30864
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30865
+ if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30866
+ }
30867
+ /**
30868
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30869
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30870
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30871
+ * under `*` have no counterpart (the default is a single level), so they are
30872
+ * reported and skipped like any other rule Rovo Dev cannot express.
30873
+ */
30874
+ function convertAllToolsRules({ rules, logger }) {
30875
+ let toolWideDefault;
30876
+ for (const [pattern, action] of Object.entries(rules)) {
30877
+ if (pattern === CATCH_ALL_PATTERN$1) {
30878
+ toolWideDefault = action;
30879
+ continue;
30880
+ }
30881
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30882
+ }
30883
+ return toolWideDefault;
30884
+ }
29361
30885
  function convertBashRules(rules) {
29362
30886
  const bash = {};
29363
30887
  const commands = [];
@@ -29379,6 +30903,7 @@ function convertBashRules(rules) {
29379
30903
  */
29380
30904
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
29381
30905
  const permission = {};
30906
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
29382
30907
  const bash = toolPermissions.bash;
29383
30908
  if (isRecord(bash)) {
29384
30909
  const bashRules = {};
@@ -29389,12 +30914,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
29389
30914
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
29390
30915
  }
29391
30916
  const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
29392
- for (const [toolKey, category] of Object.entries(TOOL_KEY_TO_CATEGORY)) {
29393
- const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
29394
- if (!isPermissionAction(value)) continue;
30917
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30918
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30919
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30920
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30921
+ return isPermissionAction(value) ? value : void 0;
30922
+ });
30923
+ if (levels.every((level) => level === void 0)) continue;
29395
30924
  permission[category] ??= {};
29396
- const current = permission[category][CATCH_ALL_PATTERN$1];
29397
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30925
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
29398
30926
  }
29399
30927
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
29400
30928
  permission.read ??= {};
@@ -30226,16 +31754,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30226
31754
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30227
31755
  };
30228
31756
  }
30229
- /**
30230
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
30231
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
30232
- * derivation (and the gateway ownership table it is checked against) must
30233
- * know both spellings on every platform.
30234
- */
31757
+ /** @see getZedOtherPlatformGlobalDir */
30235
31758
  static getExtraSharedWritePaths({ global = false } = {}) {
30236
31759
  if (!global) return [];
30237
31760
  return [{
30238
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31761
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30239
31762
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30240
31763
  }];
30241
31764
  }
@@ -31752,6 +33275,7 @@ const ClaudecodeSkillFrontmatterSchema = z.looseObject({
31752
33275
  arguments: z.optional(z.union([z.string(), z.array(z.string())])),
31753
33276
  context: z.optional(z.string()),
31754
33277
  agent: z.optional(z.string()),
33278
+ background: z.optional(z.boolean()),
31755
33279
  hooks: z.optional(z.looseObject({})),
31756
33280
  shell: z.optional(z.string()),
31757
33281
  "disable-model-invocation": z.optional(z.boolean()),
@@ -31778,6 +33302,7 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
31778
33302
  shell: section.shell
31779
33303
  };
31780
33304
  const definedFields = {
33305
+ background: section.background,
31781
33306
  arguments: section.arguments,
31782
33307
  hooks: section.hooks,
31783
33308
  "disable-model-invocation": resolvedDisableModelInvocation,
@@ -31855,6 +33380,7 @@ var ClaudecodeSkill = class extends ToolSkill {
31855
33380
  ...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
31856
33381
  ...frontmatter.context && { context: frontmatter.context },
31857
33382
  ...frontmatter.agent && { agent: frontmatter.agent },
33383
+ ...frontmatter.background !== void 0 && { background: frontmatter.background },
31858
33384
  ...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
31859
33385
  ...frontmatter.shell && { shell: frontmatter.shell },
31860
33386
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
@@ -32464,7 +33990,9 @@ const CopilotcliSkillFrontmatterSchema = z.looseObject({
32464
33990
  description: z.string(),
32465
33991
  license: z.optional(z.string()),
32466
33992
  "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
32467
- "argument-hint": z.optional(z.string())
33993
+ "argument-hint": z.optional(z.string()),
33994
+ "user-invocable": z.optional(z.boolean()),
33995
+ "disable-model-invocation": z.optional(z.boolean())
32468
33996
  });
32469
33997
  /**
32470
33998
  * Represents a GitHub Copilot CLI skill directory.
@@ -32523,7 +34051,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32523
34051
  const copilotcliSection = {
32524
34052
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
32525
34053
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
32526
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
34054
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
34055
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
34056
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
32527
34057
  };
32528
34058
  const rulesyncFrontmatter = {
32529
34059
  name: frontmatter.name,
@@ -32550,7 +34080,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32550
34080
  description: rulesyncFrontmatter.description,
32551
34081
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
32552
34082
  ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
32553
- ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
34083
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
34084
+ ...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
34085
+ ...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
32554
34086
  };
32555
34087
  return new CopilotcliSkill({
32556
34088
  outputRoot,
@@ -34220,6 +35752,37 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
34220
35752
  alternativeSkillRoots: [global ? OPENCODE_GLOBAL_SKILL_DIR_PATH : OPENCODE_SKILL_DIR_PATH]
34221
35753
  };
34222
35754
  }
35755
+ /**
35756
+ * Extra skill roots the project configured in `opencode.json` /
35757
+ * `opencode.jsonc` via `skills.paths` ("Additional paths to skill folders").
35758
+ * Without these, skills a project keeps outside `.opencode/skills/` are
35759
+ * invisible to `rulesync import` even though OpenCode loads them.
35760
+ *
35761
+ * Import-only: rulesync keeps writing to its own managed root, so a
35762
+ * configured path is read but never generated into. `skills.urls` is a
35763
+ * remote-fetch surface and is out of scope for a file-based generator.
35764
+ *
35765
+ * Absolute paths and paths escaping the output root are dropped — an import
35766
+ * root is joined onto `outputRoot`, and reaching outside it is not something
35767
+ * a project config should be able to ask for.
35768
+ *
35769
+ * @see https://opencode.ai/config.json
35770
+ */
35771
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
35772
+ const skills = asOpencodeEntries((await readOpencodeConfig({
35773
+ outputRoot,
35774
+ global
35775
+ })).skills);
35776
+ if (skills === null || !Array.isArray(skills.paths)) return [];
35777
+ const configDir = getOpencodeConfigDir({
35778
+ outputRoot,
35779
+ global
35780
+ });
35781
+ return skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !isAbsolute(candidate) && !normalize(candidate).startsWith("..")).map((relativeDirPath) => ({
35782
+ outputRoot: configDir,
35783
+ relativeDirPath
35784
+ }));
35785
+ }
34223
35786
  getFrontmatter() {
34224
35787
  return OpenCodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
34225
35788
  }
@@ -36153,12 +37716,19 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36153
37716
  */
36154
37717
  async loadToolDirs() {
36155
37718
  const factory = this.getFactory(this.toolTarget);
36156
- const roots = toolSkillImportRoots(factory.class.getSettablePaths({ global: this.global }));
37719
+ const paths = factory.class.getSettablePaths({ global: this.global });
37720
+ const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
37721
+ outputRoot: this.outputRoot,
37722
+ global: this.global
37723
+ }) : [];
37724
+ const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
37725
+ const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
36157
37726
  const seenSkillNames = /* @__PURE__ */ new Set();
36158
37727
  const toolSkills = [];
36159
37728
  for (const root of roots) {
36160
37729
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
36161
37730
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37731
+ const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
36162
37732
  const skillsDirPath = join(rootOutputRoot, relativeDirPath);
36163
37733
  if (!await directoryExists(skillsDirPath)) continue;
36164
37734
  const dirPaths = await findFilesByGlobs(join(skillsDirPath, "*"), { type: "dir" });
@@ -36173,12 +37743,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36173
37743
  })) continue;
36174
37744
  ownedDirNames.push(dirName);
36175
37745
  }
36176
- const directorySkills = await Promise.all(ownedDirNames.map((dirName) => factory.class.fromDir({
36177
- outputRoot: rootOutputRoot,
36178
- relativeDirPath,
36179
- dirName,
36180
- global: this.global
36181
- })));
37746
+ const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
37747
+ try {
37748
+ return await factory.class.fromDir({
37749
+ outputRoot: rootOutputRoot,
37750
+ relativeDirPath,
37751
+ dirName,
37752
+ global: this.global
37753
+ });
37754
+ } catch (error) {
37755
+ if (!isConfiguredRoot) throw error;
37756
+ this.logger.warn(`Skipping ${join(relativeDirPath, dirName)}: ${formatError(error)}`);
37757
+ return null;
37758
+ }
37759
+ }))).filter((skill) => skill !== null);
36182
37760
  for (const skill of directorySkills) {
36183
37761
  const skillName = skill.getImportIdentity();
36184
37762
  if (seenSkillNames.has(skillName)) continue;
@@ -36703,6 +38281,242 @@ var RovodevSubagent = class RovodevSubagent extends ToolSubagent {
36703
38281
  }
36704
38282
  };
36705
38283
  //#endregion
38284
+ //#region src/features/subagents/antigravity-shared-subagent.ts
38285
+ /**
38286
+ * Frontmatter of an Antigravity custom agent (Markdown format, CLI v1.1.6+).
38287
+ *
38288
+ * `name` and `description` are required upstream; the rest are optional and
38289
+ * documented with defaults (`tools: []`, `mainAgent: true`, `subagent: true`,
38290
+ * `model: inherit`, `commandExecutionPolicy: sandbox`, `mcpServers: []`,
38291
+ * `skills`/`plugins`: `[]`). `hidden` and `inheritMcp` appear in the v1.1.6
38292
+ * release notes but not in the documented frontmatter table, so they are
38293
+ * accepted as verbatim passthrough without any behavior modeled around them.
38294
+ * `looseObject` keeps unknown future fields round-tripping.
38295
+ *
38296
+ * @see https://antigravity.google/docs/subagents
38297
+ */
38298
+ const AntigravitySubagentFrontmatterSchema = z.looseObject({
38299
+ name: z.string(),
38300
+ description: z.string().check(z.minLength(1)),
38301
+ tools: z.optional(z.array(z.string())),
38302
+ mainAgent: z.optional(z.boolean()),
38303
+ subagent: z.optional(z.boolean()),
38304
+ model: z.optional(z.string()),
38305
+ commandExecutionPolicy: z.optional(z.string()),
38306
+ mcpServers: z.optional(z.array(z.unknown())),
38307
+ skills: z.optional(z.array(z.string())),
38308
+ plugins: z.optional(z.array(z.string())),
38309
+ hidden: z.optional(z.boolean()),
38310
+ inheritMcp: z.optional(z.boolean())
38311
+ });
38312
+ /**
38313
+ * Shared custom-agent (subagent) implementation for Google Antigravity 2.0,
38314
+ * used by the IDE, the CLI and plugin bundles.
38315
+ *
38316
+ * Antigravity discovers agents at `.agents/agents/<name>.md` (project) and
38317
+ * `~/.gemini/config/agents/<name>.md` (global, shared by the IDE and the CLI
38318
+ * exactly like `~/.gemini/config/hooks.json`). The directory form
38319
+ * (`<name>/agent.md`) is an equivalent alternative upstream; rulesync emits and
38320
+ * imports the flat file form. The body after the frontmatter is the agent's
38321
+ * system prompt.
38322
+ *
38323
+ * Concrete subclasses only supply the rulesync target name they answer to via
38324
+ * {@link AntigravitySharedSubagent.getToolTarget} and, where the shared file is
38325
+ * not involved, the sections they read via
38326
+ * {@link AntigravitySharedSubagent.getReadSectionKeys}.
38327
+ *
38328
+ * @see https://antigravity.google/docs/subagents
38329
+ */
38330
+ var AntigravitySharedSubagent = class extends ToolSubagent {
38331
+ frontmatter;
38332
+ body;
38333
+ constructor({ frontmatter, body, fileContent, ...rest }) {
38334
+ if (rest.validate !== false) {
38335
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
38336
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
38337
+ }
38338
+ super({
38339
+ ...rest,
38340
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
38341
+ });
38342
+ this.frontmatter = frontmatter;
38343
+ this.body = body;
38344
+ }
38345
+ /** The rulesync target name this subagent answers to. */
38346
+ static getToolTarget() {
38347
+ throw new Error("Please implement this method in the subclass.");
38348
+ }
38349
+ /**
38350
+ * Tool-specific sections this target reads, in increasing precedence order.
38351
+ *
38352
+ * `antigravity-ide` and `antigravity-cli` write the very same file, so a
38353
+ * target that read only its own section would silently drop the other's keys
38354
+ * — and which one survived would depend on `--targets` order. Every target
38355
+ * therefore merges the shared `antigravity-ide` → `antigravity-cli` sections
38356
+ * (the CLI block wins, matching the fixed order the MCP feature already uses
38357
+ * for the same shared-output reason), and the plugin target layers its own
38358
+ * section on top of that. Only `getToolTarget()` decides which section an
38359
+ * import writes back into.
38360
+ */
38361
+ static getReadSectionKeys() {
38362
+ return ["antigravity-ide", "antigravity-cli"];
38363
+ }
38364
+ static getSettablePaths({ global = false } = {}) {
38365
+ return { relativeDirPath: global ? ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH : ANTIGRAVITY_AGENTS_DIR_PATH };
38366
+ }
38367
+ getFrontmatter() {
38368
+ return this.frontmatter;
38369
+ }
38370
+ getBody() {
38371
+ return this.body;
38372
+ }
38373
+ toRulesyncSubagent() {
38374
+ const { name, description, ...restFields } = this.frontmatter;
38375
+ return new RulesyncSubagent({
38376
+ outputRoot: ".",
38377
+ frontmatter: {
38378
+ targets: ["*"],
38379
+ name,
38380
+ description,
38381
+ ...Object.keys(restFields).length > 0 && { [this.constructor.getToolTarget()]: restFields }
38382
+ },
38383
+ body: this.body,
38384
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
38385
+ relativeFilePath: this.getRelativeFilePath(),
38386
+ validate: true
38387
+ });
38388
+ }
38389
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
38390
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
38391
+ const mergedSection = Object.assign({}, ...this.getReadSectionKeys().map((key) => rulesyncFrontmatter[key] ?? {}));
38392
+ const toolSection = this.filterToolSpecificSection(mergedSection, ["name", "description"]);
38393
+ const rawFrontmatter = {
38394
+ name: rulesyncFrontmatter.name,
38395
+ description: rulesyncFrontmatter.description || `${rulesyncFrontmatter.name} subagent`,
38396
+ ...toolSection
38397
+ };
38398
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(rawFrontmatter);
38399
+ if (!result.success) throw new Error(`Invalid ${this.getToolTarget()} subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
38400
+ const frontmatter = result.data;
38401
+ const body = rulesyncSubagent.getBody();
38402
+ const paths = this.getSettablePaths({ global });
38403
+ return new this({
38404
+ outputRoot,
38405
+ frontmatter,
38406
+ body,
38407
+ relativeDirPath: paths.relativeDirPath,
38408
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
38409
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
38410
+ validate,
38411
+ global
38412
+ });
38413
+ }
38414
+ validate() {
38415
+ if (!this.frontmatter) return {
38416
+ success: true,
38417
+ error: null
38418
+ };
38419
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(this.frontmatter);
38420
+ if (result.success) return {
38421
+ success: true,
38422
+ error: null
38423
+ };
38424
+ return {
38425
+ success: false,
38426
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
38427
+ };
38428
+ }
38429
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
38430
+ return this.isTargetedByRulesyncSubagentDefault({
38431
+ rulesyncSubagent,
38432
+ toolTarget: this.getToolTarget()
38433
+ });
38434
+ }
38435
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
38436
+ const paths = this.getSettablePaths({ global });
38437
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
38438
+ const fileContent = await readFileContent(filePath);
38439
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
38440
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
38441
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
38442
+ return new this({
38443
+ outputRoot,
38444
+ relativeDirPath: paths.relativeDirPath,
38445
+ relativeFilePath,
38446
+ frontmatter: result.data,
38447
+ body: content.trim(),
38448
+ fileContent,
38449
+ validate,
38450
+ global
38451
+ });
38452
+ }
38453
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
38454
+ return new this({
38455
+ outputRoot,
38456
+ relativeDirPath,
38457
+ relativeFilePath,
38458
+ frontmatter: {
38459
+ name: "",
38460
+ description: ""
38461
+ },
38462
+ body: "",
38463
+ fileContent: "",
38464
+ validate: false,
38465
+ global
38466
+ });
38467
+ }
38468
+ };
38469
+ //#endregion
38470
+ //#region src/features/subagents/antigravity-cli-subagent.ts
38471
+ /**
38472
+ * Google Antigravity CLI custom agent (subagent), shipped in CLI v1.1.6.
38473
+ *
38474
+ * Shares all behavior with {@link AntigravitySharedSubagent}; the CLI reads the
38475
+ * same `.agents/agents/` and `~/.gemini/config/agents/` roots as the IDE and
38476
+ * exposes them through the `agy agents` subcommand and the `--agent` flag. It
38477
+ * answers to the `antigravity-cli` target.
38478
+ */
38479
+ var AntigravityCliSubagent = class extends AntigravitySharedSubagent {
38480
+ static getToolTarget() {
38481
+ return "antigravity-cli";
38482
+ }
38483
+ };
38484
+ //#endregion
38485
+ //#region src/features/subagents/antigravity-ide-subagent.ts
38486
+ /**
38487
+ * Google Antigravity IDE custom agent (subagent).
38488
+ *
38489
+ * Shares all behavior with {@link AntigravitySharedSubagent} — the subagents
38490
+ * documentation is product-wide and lists the same `.agents/agents/` and
38491
+ * `~/.gemini/config/agents/` roots for the IDE and the CLI. It answers to the
38492
+ * `antigravity-ide` target.
38493
+ */
38494
+ var AntigravityIdeSubagent = class extends AntigravitySharedSubagent {
38495
+ static getToolTarget() {
38496
+ return "antigravity-ide";
38497
+ }
38498
+ };
38499
+ //#endregion
38500
+ //#region src/features/subagents/antigravity-plugin-subagent.ts
38501
+ /**
38502
+ * Custom agent inside an Antigravity plugin bundle
38503
+ * (`<plugin_name>/agents/<name>.md`). Plugin bundles are project-scope output
38504
+ * only; they are staged into `~/.gemini/antigravity-cli/plugins/` by the user.
38505
+ *
38506
+ * @see https://antigravity.google/docs/cli/plugins
38507
+ */
38508
+ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
38509
+ static getToolTarget() {
38510
+ return "antigravity-plugin";
38511
+ }
38512
+ static getReadSectionKeys() {
38513
+ return [...super.getReadSectionKeys(), "antigravity-plugin"];
38514
+ }
38515
+ static getSettablePaths() {
38516
+ return { relativeDirPath: ANTIGRAVITY_PLUGIN_AGENTS_DIR };
38517
+ }
38518
+ };
38519
+ //#endregion
36706
38520
  //#region src/features/subagents/augmentcode-subagent.ts
36707
38521
  const AugmentcodeSubagentFrontmatterSchema = z.looseObject({
36708
38522
  name: z.string(),
@@ -38516,7 +40330,7 @@ def register(ctx):
38516
40330
  _register_subagent(ctx, subagent)
38517
40331
  `;
38518
40332
  }
38519
- function getEnabledPluginConfigContent(currentContent) {
40333
+ function getEnabledPluginConfigContent({ currentContent, global }) {
38520
40334
  const config = parseSharedConfig({
38521
40335
  format: "yaml",
38522
40336
  fileContent: currentContent
@@ -38524,7 +40338,7 @@ function getEnabledPluginConfigContent(currentContent) {
38524
40338
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
38525
40339
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
38526
40340
  return applySharedConfigPatch({
38527
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40341
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
38528
40342
  feature: "subagents",
38529
40343
  existingContent: currentContent,
38530
40344
  patch: { plugins: {
@@ -38575,6 +40389,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38575
40389
  return !targets || targets.includes("*") || targets.includes("hermesagent");
38576
40390
  }
38577
40391
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40392
+ const pluginDirPath = getHermesagentRelativeDirPath({
40393
+ global,
40394
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40395
+ });
38578
40396
  return [
38579
40397
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
38580
40398
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -38583,20 +40401,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38583
40401
  global
38584
40402
  })),
38585
40403
  new HermesagentSubagent({
38586
- relativeDirPath: getHermesagentRelativeDirPath({
38587
- global,
38588
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
38589
- }),
40404
+ relativeDirPath: pluginDirPath,
38590
40405
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
38591
40406
  fileContent: "",
38592
40407
  outputRoot,
38593
40408
  global
38594
40409
  }),
38595
40410
  new HermesagentSubagent({
38596
- relativeDirPath: getHermesagentRelativeDirPath({
38597
- global,
38598
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
38599
- }),
40411
+ relativeDirPath: pluginDirPath,
38600
40412
  relativeFilePath: basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
38601
40413
  fileContent: "",
38602
40414
  outputRoot,
@@ -38636,14 +40448,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38636
40448
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
38637
40449
  * so the write must be declared for the shared-file order derivation.
38638
40450
  */
38639
- static getExtraSharedWritePaths({ global = false } = {}) {
38640
- return global ? [{
38641
- relativeDirPath: getHermesagentRelativeDirPath({
38642
- global,
38643
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
38644
- }),
38645
- relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
38646
- }] : [];
40451
+ /**
40452
+ * `config.yaml` under every spelling the global profile root can take.
40453
+ * @see getHermesagentSharedConfigWritePaths
40454
+ */
40455
+ static getExtraSharedWritePaths() {
40456
+ return getHermesagentSharedConfigWritePaths();
38647
40457
  }
38648
40458
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
38649
40459
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -38676,7 +40486,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38676
40486
  }
38677
40487
  setFileContent(newFileContent) {
38678
40488
  if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) {
38679
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40489
+ super.setFileContent(getEnabledPluginConfigContent({
40490
+ currentContent: newFileContent,
40491
+ global: this.global
40492
+ }));
38680
40493
  return;
38681
40494
  }
38682
40495
  super.setFileContent(newFileContent);
@@ -38684,7 +40497,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38684
40497
  getFileContent() {
38685
40498
  if (this.getRelativeFilePath() === basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
38686
40499
  if (this.getRelativeFilePath() === basename(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH)) return getPluginInitContent();
38687
- if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent(super.getFileContent());
40500
+ if (this.getRelativeFilePath() === basename(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40501
+ currentContent: super.getFileContent(),
40502
+ global: this.global
40503
+ });
38688
40504
  return super.getFileContent();
38689
40505
  }
38690
40506
  };
@@ -40280,6 +42096,30 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
40280
42096
  filePattern: "*.md"
40281
42097
  }
40282
42098
  }],
42099
+ ["antigravity-cli", {
42100
+ class: AntigravityCliSubagent,
42101
+ meta: {
42102
+ supportsSimulated: false,
42103
+ supportsGlobal: true,
42104
+ filePattern: "*.md"
42105
+ }
42106
+ }],
42107
+ ["antigravity-ide", {
42108
+ class: AntigravityIdeSubagent,
42109
+ meta: {
42110
+ supportsSimulated: false,
42111
+ supportsGlobal: true,
42112
+ filePattern: "*.md"
42113
+ }
42114
+ }],
42115
+ ["antigravity-plugin", {
42116
+ class: AntigravityPluginSubagent,
42117
+ meta: {
42118
+ supportsSimulated: false,
42119
+ supportsGlobal: false,
42120
+ filePattern: "*.md"
42121
+ }
42122
+ }],
40283
42123
  ["augmentcode", {
40284
42124
  class: AugmentcodeSubagent,
40285
42125
  meta: {
@@ -42493,6 +44333,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
42493
44333
  const CopilotRuleFrontmatterSchema = z.object({
42494
44334
  description: z.optional(z.string()),
42495
44335
  applyTo: z.optional(z.string()),
44336
+ name: z.optional(z.string()),
42496
44337
  excludeAgent: z.optional(z.union([
42497
44338
  z.literal("code-review"),
42498
44339
  z.literal("cloud-agent"),
@@ -42550,7 +44391,10 @@ var CopilotRule = class CopilotRule extends ToolRule {
42550
44391
  root: this.isRoot(),
42551
44392
  description: this.frontmatter.description,
42552
44393
  globs,
42553
- ...this.frontmatter.excludeAgent && { copilot: { excludeAgent: this.frontmatter.excludeAgent } }
44394
+ ...(this.frontmatter.excludeAgent || this.frontmatter.name) && { copilot: {
44395
+ ...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
44396
+ ...this.frontmatter.name && { name: this.frontmatter.name }
44397
+ } }
42554
44398
  };
42555
44399
  const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
42556
44400
  return new RulesyncRule({
@@ -42569,7 +44413,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
42569
44413
  const copilotFrontmatter = {
42570
44414
  description: rulesyncFrontmatter.description,
42571
44415
  applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0,
42572
- excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent
44416
+ excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
44417
+ name: rulesyncFrontmatter.copilot?.name
42573
44418
  };
42574
44419
  const body = rulesyncRule.getBody();
42575
44420
  if (root) return new CopilotRule({
@@ -43734,10 +45579,13 @@ var JunieRule = class JunieRule extends ToolRule {
43734
45579
  //#region src/features/rules/kilo-rule.ts
43735
45580
  var KiloRule = class KiloRule extends ToolRule {
43736
45581
  static getSettablePaths({ global, excludeToolDir } = {}) {
43737
- if (global) return { root: {
43738
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
43739
- relativeFilePath: KILO_RULE_FILE_NAME
43740
- } };
45582
+ if (global) return {
45583
+ root: {
45584
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45585
+ relativeFilePath: KILO_RULE_FILE_NAME
45586
+ },
45587
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45588
+ };
43741
45589
  return {
43742
45590
  root: {
43743
45591
  relativeDirPath: ".",
@@ -45322,6 +47170,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45322
47170
  extension: "md",
45323
47171
  supportsGlobal: false,
45324
47172
  ruleDiscoveryMode: "toon",
47173
+ collisionPolicy: "compose",
45325
47174
  additionalConventions: {
45326
47175
  commands: { commandClass: AgentsmdCommand },
45327
47176
  subagents: { subagentClass: AgentsmdSubagent },
@@ -45342,7 +47191,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45342
47191
  meta: {
45343
47192
  extension: "md",
45344
47193
  supportsGlobal: true,
45345
- ruleDiscoveryMode: "toon"
47194
+ ruleDiscoveryMode: "toon",
47195
+ collisionPolicy: "compose"
45346
47196
  }
45347
47197
  }],
45348
47198
  ["antigravity-cli", {
@@ -45419,7 +47269,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45419
47269
  extension: "md",
45420
47270
  supportsGlobal: true,
45421
47271
  ruleDiscoveryMode: "auto",
45422
- foldsNonRootIntoRoot: true
47272
+ collisionPolicy: "fold"
45423
47273
  }
45424
47274
  }],
45425
47275
  ["copilot", {
@@ -45452,7 +47302,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45452
47302
  extension: "md",
45453
47303
  supportsGlobal: true,
45454
47304
  ruleDiscoveryMode: "auto",
45455
- foldsNonRootIntoRoot: true
47305
+ collisionPolicy: "fold"
45456
47306
  }
45457
47307
  }],
45458
47308
  ["factorydroid", {
@@ -45460,7 +47310,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45460
47310
  meta: {
45461
47311
  extension: "md",
45462
47312
  supportsGlobal: true,
45463
- ruleDiscoveryMode: "toon"
47313
+ ruleDiscoveryMode: "toon",
47314
+ collisionPolicy: "compose"
45464
47315
  }
45465
47316
  }],
45466
47317
  ["goose", {
@@ -45469,7 +47320,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45469
47320
  extension: "md",
45470
47321
  supportsGlobal: true,
45471
47322
  ruleDiscoveryMode: "auto",
45472
- foldsNonRootIntoRoot: true
47323
+ collisionPolicy: "fold"
45473
47324
  }
45474
47325
  }],
45475
47326
  ["hermesagent", {
@@ -45478,7 +47329,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45478
47329
  extension: "md",
45479
47330
  supportsGlobal: false,
45480
47331
  ruleDiscoveryMode: "auto",
45481
- foldsNonRootIntoRoot: true
47332
+ collisionPolicy: "fold"
45482
47333
  }
45483
47334
  }],
45484
47335
  ["grokcli", {
@@ -45495,7 +47346,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45495
47346
  extension: "md",
45496
47347
  supportsGlobal: true,
45497
47348
  ruleDiscoveryMode: "auto",
45498
- foldsNonRootIntoRoot: true
47349
+ collisionPolicy: "fold"
45499
47350
  }
45500
47351
  }],
45501
47352
  ["kilo", {
@@ -45504,7 +47355,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45504
47355
  extension: "md",
45505
47356
  supportsGlobal: true,
45506
47357
  ruleDiscoveryMode: "auto",
45507
- mcpInstructionsRegistrar: KiloMcp
47358
+ mcpInstructionsRegistrar: KiloMcp,
47359
+ collisionPolicy: "compose"
45508
47360
  }
45509
47361
  }],
45510
47362
  ["kimi-code", {
@@ -45513,7 +47365,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45513
47365
  extension: "md",
45514
47366
  supportsGlobal: true,
45515
47367
  ruleDiscoveryMode: "auto",
45516
- foldsNonRootIntoRoot: true
47368
+ collisionPolicy: "fold"
45517
47369
  }
45518
47370
  }],
45519
47371
  ["kiro", {
@@ -45546,7 +47398,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45546
47398
  extension: "md",
45547
47399
  supportsGlobal: true,
45548
47400
  ruleDiscoveryMode: "toon",
45549
- mcpInstructionsRegistrar: OpencodeMcp
47401
+ mcpInstructionsRegistrar: OpencodeMcp,
47402
+ collisionPolicy: "compose"
45550
47403
  }
45551
47404
  }],
45552
47405
  ["pi", {
@@ -45555,7 +47408,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45555
47408
  extension: "md",
45556
47409
  supportsGlobal: true,
45557
47410
  ruleDiscoveryMode: "auto",
45558
- foldsNonRootIntoRoot: true
47411
+ collisionPolicy: "fold"
45559
47412
  }
45560
47413
  }],
45561
47414
  ["qwencode", {
@@ -45573,7 +47426,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45573
47426
  extension: "md",
45574
47427
  supportsGlobal: true,
45575
47428
  ruleDiscoveryMode: "auto",
45576
- foldsNonRootIntoRoot: true
47429
+ collisionPolicy: "fold"
45577
47430
  }
45578
47431
  }],
45579
47432
  ["replit", {
@@ -45630,7 +47483,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45630
47483
  extension: "md",
45631
47484
  supportsGlobal: false,
45632
47485
  ruleDiscoveryMode: "toon",
45633
- foldsNonRootIntoRoot: true
47486
+ collisionPolicy: "fold"
45634
47487
  }
45635
47488
  }],
45636
47489
  ["devin", {
@@ -45699,16 +47552,23 @@ var RulesProcessor = class extends FeatureProcessor {
45699
47552
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
45700
47553
  const factory = this.getFactory(this.toolTarget);
45701
47554
  const { meta } = factory;
45702
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47555
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
45703
47556
  if (!factory.class.isTargetedByRulesyncRule(rulesyncRule)) return null;
45704
- return factory.class.fromRulesyncRule({
45705
- outputRoot: this.outputRoot,
45706
- rulesyncRule,
45707
- validate: true,
45708
- global: this.global
45709
- });
47557
+ return {
47558
+ toolRule: factory.class.fromRulesyncRule({
47559
+ outputRoot: this.outputRoot,
47560
+ rulesyncRule,
47561
+ validate: true,
47562
+ global: this.global
47563
+ }),
47564
+ rulesyncRule
47565
+ };
45710
47566
  }).filter((rule) => rule !== null);
45711
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47567
+ this.mergeRulesByOutputPath({
47568
+ convertedRules,
47569
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47570
+ });
47571
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
45712
47572
  this.applyLocalRootRules({
45713
47573
  toolRules,
45714
47574
  localRootRules,
@@ -45726,7 +47586,12 @@ var RulesProcessor = class extends FeatureProcessor {
45726
47586
  toolRules,
45727
47587
  factory
45728
47588
  });
45729
- return [...toolRules, ...extraFiles];
47589
+ const outputFiles = [...toolRules, ...extraFiles];
47590
+ this.warnForOutputPathCollisions({
47591
+ outputFiles,
47592
+ convertedRules
47593
+ });
47594
+ return outputFiles;
45730
47595
  }
45731
47596
  /**
45732
47597
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -45813,39 +47678,80 @@ var RulesProcessor = class extends FeatureProcessor {
45813
47678
  });
45814
47679
  }
45815
47680
  /**
45816
- * Fold every non-root rule body into the single root rule file.
47681
+ * Reconcile rules that resolve to the same output path.
45817
47682
  *
45818
- * Used for tools whose rules engine reads only one root `AGENTS.md` and neither
45819
- * scans a `memories/` directory nor follows references (deepagents' dcode reads
45820
- * `.deepagents/AGENTS.md`; Warp reads root/subdir `AGENTS.md` but never
45821
- * `.warp/memories/`). Those rule classes emit both root and non-root rules to
45822
- * the same root path, so all bodies must be merged into one instance to avoid
45823
- * colliding on that path (last-writer-wins would silently drop content).
47683
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47684
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47685
+ * neither scans a modular rules directory nor follows references. For example,
47686
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47687
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47688
+ * body into one instance because last-writer-wins would silently drop content.
47689
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
45824
47690
  *
45825
- * The root rule (if any) becomes the merge target and leads the merged content;
45826
- * otherwise the first rule is used so a rule set without a root overview still
45827
- * produces a single, complete file. Mutates `toolRules` in place.
47691
+ * A generated root rule becomes the merge target when present. A `fold` group
47692
+ * without one uses its first rule. A group only composes when every rendered
47693
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47694
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47695
+ * Root-involved collisions that cannot be composed safely fail; other
47696
+ * collisions remain separate and are reported by the final output-path check.
47697
+ * Mutates `convertedRules` in place.
45828
47698
  */
45829
- foldNonRootRulesIntoRootRule(toolRules) {
45830
- if (toolRules.length <= 1) return;
47699
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47700
+ if (convertedRules.length <= 1) return;
45831
47701
  const groups = /* @__PURE__ */ new Map();
45832
- for (const rule of toolRules) {
45833
- const path = join(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47702
+ for (const conversion of convertedRules) {
47703
+ const path = join(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
45834
47704
  const group = groups.get(path);
45835
- if (group) group.push(rule);
45836
- else groups.set(path, [rule]);
47705
+ if (group) group.push(conversion);
47706
+ else groups.set(path, [conversion]);
45837
47707
  }
45838
47708
  const survivors = /* @__PURE__ */ new Set();
45839
- for (const group of groups.values()) {
45840
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47709
+ for (const [path, group] of groups) {
47710
+ if (group.length === 1) {
47711
+ const conversion = group[0];
47712
+ if (conversion) {
47713
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47714
+ survivors.add(conversion);
47715
+ }
47716
+ continue;
47717
+ }
47718
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47719
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47720
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47721
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47722
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47723
+ if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
47724
+ if (!shouldCompose) {
47725
+ for (const conversion of group) survivors.add(conversion);
47726
+ continue;
47727
+ }
47728
+ const target = rootConversion ?? group[0];
45841
47729
  if (!target) continue;
45842
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map((rule) => rule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
45843
- target.setFileContent(mergedContent);
47730
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47731
+ target.toolRule.setFileContent(mergedContent);
45844
47732
  survivors.add(target);
45845
47733
  }
45846
- for (let i = toolRules.length - 1; i >= 0; i--) {
45847
- const rule = toolRules[i];
45848
- if (rule && !survivors.has(rule)) toolRules.splice(i, 1);
47734
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47735
+ const conversion = convertedRules[i];
47736
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47737
+ }
47738
+ }
47739
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47740
+ const seen = /* @__PURE__ */ new Map();
47741
+ const describeSource = (file) => {
47742
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47743
+ return source ? formatRulePaths([source]) : join(file.getRelativeDirPath(), file.getRelativeFilePath());
47744
+ };
47745
+ for (const file of outputFiles) {
47746
+ const path = join(file.getRelativeDirPath(), file.getRelativeFilePath());
47747
+ const key = path.toLowerCase();
47748
+ const previous = seen.get(key);
47749
+ if (previous) {
47750
+ const previousPath = join(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47751
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47752
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47753
+ }
47754
+ seen.set(key, file);
45849
47755
  }
45850
47756
  }
45851
47757
  /**
@@ -46005,14 +47911,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
46005
47911
  }));
46006
47912
  const factory = this.getFactory(this.toolTarget);
46007
47913
  const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
46008
- if (targetedRootRules.length > 1) throw new Error(`Multiple root rulesync rules found for target '${this.toolTarget}': ${formatRulePaths(targetedRootRules)}`);
46009
47914
  if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
46010
47915
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
46011
47916
  if (targetedLocalRootRules.length > 1) throw new Error(`Multiple localRoot rules found for target '${this.toolTarget}': ${formatRulePaths(targetedLocalRootRules)}. Only one rule can have localRoot: true`);
46012
47917
  if (targetedLocalRootRules.length > 0 && targetedRootRules.length === 0) throw new Error(`localRoot: true requires a root: true rule to exist for target '${this.toolTarget}' (found in ${formatRulePaths(targetedLocalRootRules)})`);
46013
47918
  if (this.global) {
46014
47919
  const globalPaths = factory.class.getSettablePaths({ global: true });
46015
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47920
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
46016
47921
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
46017
47922
  if (nonRootRules.length > 0 && !supportsGlobalNonRoot) this.logger.warn(`${nonRootRules.length} non-root rulesync rules found, but it's in global mode, so ignoring them: ${formatRulePaths(nonRootRules)}`);
46018
47923
  if (targetedLocalRootRules.length > 0) this.logger.warn(`${targetedLocalRootRules.length} localRoot rules found, but localRoot is not supported in global mode, ignoring them: ${formatRulePaths(targetedLocalRootRules)}`);
@@ -46255,14 +48160,36 @@ async function assertPluginRootSafe(params) {
46255
48160
  }
46256
48161
  //#endregion
46257
48162
  //#region src/utils/tool-output-root.ts
48163
+ /** The environment variable each tool reads for its profile root. */
48164
+ const TOOL_HOME_ENV_VARS = {
48165
+ hermesagent: "HERMES_HOME",
48166
+ "kimi-code": "KIMI_CODE_HOME"
48167
+ };
48168
+ /**
48169
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48170
+ * output root in global scope.
48171
+ *
48172
+ * The override wins over `--output-roots`: it names where the tool itself reads
48173
+ * its profile, so writing anywhere else would produce files the tool ignores.
48174
+ *
48175
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48176
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48177
+ * instead of silently becoming the output root. The rejection is re-thrown
48178
+ * naming the variable, since the user never passed an `--output-roots` flag.
48179
+ */
46258
48180
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
46259
48181
  if (!global) return outputRoot;
46260
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48182
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
46261
48183
  outputRoot,
46262
48184
  global
46263
- });
46264
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
46265
- return outputRoot;
48185
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48186
+ if (resolved === outputRoot) return resolved;
48187
+ try {
48188
+ validateOutputRoot(resolved);
48189
+ } catch (error) {
48190
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48191
+ }
48192
+ return resolved;
46266
48193
  }
46267
48194
  //#endregion
46268
48195
  //#region src/lib/convert.ts
@@ -47663,7 +49590,11 @@ async function generateChecksCore(params) {
47663
49590
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
47664
49591
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
47665
49592
  const processor = new ChecksProcessor({
47666
- outputRoot,
49593
+ outputRoot: resolveToolOutputRoot({
49594
+ outputRoot,
49595
+ toolTarget,
49596
+ global: config.getGlobal()
49597
+ }),
47667
49598
  inputRoot: config.getInputRoot(),
47668
49599
  toolTarget,
47669
49600
  global: config.getGlobal(),
@@ -48038,4 +49969,4 @@ async function importChecksCore(params) {
48038
49969
  //#endregion
48039
49970
  export { ErrorCodes as $, RULESYNC_SKILLS_RELATIVE_DIR_PATH as $t, RulesyncMcp as A, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as At, stringifyFrontmatter as B, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Bt, RulesyncSubagent as C, writeFileContent as Ct, RulesyncRule as D, ToolTargetSchema as Dt, RulesyncSkillFrontmatterSchema as E, PACKAGING_TOOL_TARGETS as Et, parseJsonc as F, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Ft, ConfigFileSchema as G, RULESYNC_MCP_SCHEMA_URL as Gt, SHARED_USER_MANAGED_CONFIG_PATHS as H, RULESYNC_MCP_FILE_NAME as Ht, RulesyncCommand as I, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as It, ConsoleLogger as J, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Jt, SourceEntrySchema as K, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Kt, RulesyncCommandFrontmatterSchema as L, RULESYNC_HOOKS_FILE_NAME as Lt, RulesyncHooks as M, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Mt, getRulesyncSourceCandidates as N, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Nt, RulesyncRuleFrontmatterSchema as O, MAX_FILE_SIZE as Ot, resolveRulesyncSourceWritePath as P, RULESYNC_CONFIG_SCHEMA_URL as Pt, CLIError as Q, RULESYNC_RULES_RELATIVE_DIR_PATH as Qt, RulesyncCheck as R, RULESYNC_HOOKS_LEGACY_FILE_NAME as Rt, getLocalSkillDirNames as S, toPosixPath as St, RulesyncSkill as T, ALL_TOOL_TARGETS_WITH_WILDCARD as Tt, SKILL_FILE_NAME as U, RULESYNC_MCP_LEGACY_FILE_NAME as Ut, loadYaml as V, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Vt, ConfigResolver as W, RULESYNC_MCP_RELATIVE_FILE_PATH as Wt, fallbackLogger as X, RULESYNC_PERMISSIONS_SCHEMA_URL as Xt, JsonLogger as Y, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Yt, warnOnConflictingFlags as Z, RULESYNC_RELATIVE_DIR_PATH as Zt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as _, removeFile as _t, convertFromTool as a, directoryExists as at, CODEXCLI_BASH_RULES_FILE_NAME as b, resolvePath as bt, SubagentsProcessor as c, findFilesByGlobs as ct, IgnoreProcessor as d, isSymlink as dt, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as en, assertDirectoryIfExists as et, HooksProcessor as f, listDirectoryFiles as ft, CLAUDECODE_MEMORIES_DIR_NAME as g, removeDirectoryStrict as gt, CLAUDECODE_LOCAL_RULE_FILE_NAME as h, removeDirectory as ht, getProcessorRegistryEntry as i, formatError as in, createTempDirectory as it, RulesyncIgnore as j, RULESYNC_CHECKS_RELATIVE_DIR_PATH as jt, RulesyncPermissions as k, RULESYNC_AIIGNORE_FILE_NAME as kt, SkillsProcessor as l, getFileSize as lt, CLAUDECODE_DIR as m, readFileContentOrNull as mt, checkRulesyncDirExists as n, ALL_FEATURES as nn, assertWritablePathInsideRoot as nt, isPackagingToolTarget as o, ensureDir as ot, CommandsProcessor as p, readFileContent as pt, findControlCharacter as q, RULESYNC_PERMISSIONS_FILE_NAME as qt, generate as r, ALL_FEATURES_WITH_WILDCARD as rn, checkPathTraversal as rt, RulesProcessor as s, fileExists as st, importFromTool as t, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as tn, assertTreeContainsNoSymlinks as tt, McpProcessor as u, getHomeDirectory as ut, CLAUDECODE_SKILLS_DIR_PATH as v, removeFileStrict as vt, RulesyncSubagentFrontmatterSchema as w, ALL_TOOL_TARGETS as wt, CODEXCLI_DIR as x, runWithDirectoryRollback as xt, ChecksProcessor as y, removeTempDirectory as yt, RulesyncCheckFrontmatterSchema as z, RULESYNC_HOOKS_RELATIVE_FILE_PATH as zt };
48040
49971
 
48041
- //# sourceMappingURL=import-BT5KR_K-.js.map
49972
+ //# sourceMappingURL=import-CArKOPG_.js.map