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.
@@ -127,27 +127,27 @@ const isFeatureValueEnabled = (value) => {
127
127
  };
128
128
  //#endregion
129
129
  //#region src/constants/rulesync-paths.ts
130
- const { join: join$275 } = node_path.posix;
130
+ const { join: join$281 } = node_path.posix;
131
131
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
132
132
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
133
133
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
134
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$275(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
- join$275(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
134
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "rules");
135
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
136
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "commands");
137
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "subagents");
138
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "checks");
139
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
140
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
141
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
142
+ join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
143
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
144
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
145
145
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
146
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
146
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
147
147
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
148
148
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
149
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$275(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$275(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
149
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "skills");
150
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
151
151
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
152
152
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
153
153
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -217,6 +217,7 @@ const ignoreProcessorToolTargetTuple = [
217
217
  "kiro-cli",
218
218
  "kiro-ide",
219
219
  "qwencode",
220
+ "reasonix",
220
221
  "roo",
221
222
  "devin",
222
223
  "vibe",
@@ -274,6 +275,7 @@ const commandsProcessorToolTargetTuple = [
274
275
  "cursor",
275
276
  "factorydroid",
276
277
  "goose",
278
+ "grokcli",
277
279
  "hermesagent",
278
280
  "junie",
279
281
  "kilo",
@@ -293,6 +295,9 @@ const commandsProcessorToolTargetTuple = [
293
295
  const subagentsProcessorToolTargetTuple = [
294
296
  "kilo",
295
297
  "agentsmd",
298
+ "antigravity-cli",
299
+ "antigravity-ide",
300
+ "antigravity-plugin",
296
301
  "augmentcode",
297
302
  "claudecode",
298
303
  "claudecode-plugin",
@@ -424,7 +429,9 @@ const permissionsProcessorToolTargetTuple = [
424
429
  ];
425
430
  const checksProcessorToolTargetTuple = [
426
431
  "amp",
432
+ "cursor",
427
433
  "hermesagent",
434
+ "rovodev",
428
435
  "takt"
429
436
  ];
430
437
  //#endregion
@@ -1078,6 +1085,17 @@ const ConfigFileSchema = zod_mini.z.object({
1078
1085
  });
1079
1086
  zod_mini.z.required(ConfigParamsSchema);
1080
1087
  /**
1088
+ * Normalizes the configuration file location to an absolute path.
1089
+ *
1090
+ * `ConfigResolver` always supplies the path it actually loaded; the fallback
1091
+ * only covers direct programmatic construction, where the conventional
1092
+ * location next to the input root is the best guess.
1093
+ */
1094
+ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1095
+ if (configFilePath === void 0) return (0, node_path.join)(inputRoot, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
1096
+ return (0, node_path.isAbsolute)(configFilePath) ? configFilePath : (0, node_path.resolve)(configFilePath);
1097
+ }
1098
+ /**
1081
1099
  * Conflicting target pairs that cannot be used together
1082
1100
  */
1083
1101
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
@@ -1148,8 +1166,9 @@ var Config = class Config {
1148
1166
  dryRun;
1149
1167
  check;
1150
1168
  inputRoot;
1169
+ configFilePath;
1151
1170
  sources;
1152
- constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, sources, configFileTargets }) {
1171
+ constructor({ outputRoots, targets, features, verbose, delete: isDelete, global, silent, simulateCommands, simulateSubagents, simulateSkills, flattenedCommandNaming, gitignoreTargetsOnly, gitignoreDestination, dryRun, check, inputRoot, configFilePath, sources, configFileTargets }) {
1153
1172
  assertTargetsFeaturesExclusive({
1154
1173
  targets,
1155
1174
  features
@@ -1182,6 +1201,10 @@ var Config = class Config {
1182
1201
  this.dryRun = dryRun ?? false;
1183
1202
  this.check = check ?? false;
1184
1203
  this.inputRoot = inputRoot === void 0 ? process.cwd() : (0, node_path.isAbsolute)(inputRoot) ? inputRoot : (0, node_path.resolve)(inputRoot);
1204
+ this.configFilePath = normalizeConfigFilePath({
1205
+ configFilePath,
1206
+ inputRoot: this.inputRoot
1207
+ });
1185
1208
  this.sources = sources ?? [];
1186
1209
  }
1187
1210
  /**
@@ -1375,6 +1398,14 @@ var Config = class Config {
1375
1398
  getInputRoot() {
1376
1399
  return this.inputRoot;
1377
1400
  }
1401
+ /**
1402
+ * Returns the absolute path of the configuration file this config was
1403
+ * resolved from. The file itself may not exist — `rulesync` runs fine
1404
+ * without one — so callers must treat this as a location, not a guarantee.
1405
+ */
1406
+ getConfigFilePath() {
1407
+ return this.configFilePath;
1408
+ }
1378
1409
  getSources() {
1379
1410
  return this.sources;
1380
1411
  }
@@ -1597,6 +1628,7 @@ var ConfigResolver = class {
1597
1628
  fallback: getDefaults().check
1598
1629
  }),
1599
1630
  inputRoot: resolvedInputRoot !== void 0 ? (0, node_path.resolve)(resolvedInputRoot) : cwd,
1631
+ configFilePath: validatedConfigPath,
1600
1632
  sources: configByFile.sources ?? getDefaults().sources,
1601
1633
  flattenedCommandNaming: configByFile.flattenedCommandNaming ?? getDefaults().flattenedCommandNaming,
1602
1634
  configFileTargets: extractConfigFileTargets(configByFile.targets)
@@ -2134,7 +2166,10 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
2134
2166
  model: zod_mini.z.optional(safeString),
2135
2167
  args: zod_mini.z.optional(zod_mini.z.array(safeString)),
2136
2168
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
2137
- if: zod_mini.z.optional(safeString)
2169
+ if: zod_mini.z.optional(safeString),
2170
+ commandWindows: zod_mini.z.optional(safeString),
2171
+ asyncRewake: zod_mini.z.optional(zod_mini.z.boolean()),
2172
+ continueOnBlock: zod_mini.z.optional(zod_mini.z.boolean())
2138
2173
  });
2139
2174
  /**
2140
2175
  * All canonical hook event names.
@@ -2188,6 +2223,7 @@ const HOOK_EVENTS = [
2188
2223
  "configChange",
2189
2224
  "cwdChanged",
2190
2225
  "fileChanged",
2226
+ "directoryAdded",
2191
2227
  "elicitation",
2192
2228
  "elicitationResult"
2193
2229
  ];
@@ -2249,6 +2285,7 @@ const CLAUDE_HOOK_EVENTS = [
2249
2285
  "configChange",
2250
2286
  "cwdChanged",
2251
2287
  "fileChanged",
2288
+ "directoryAdded",
2252
2289
  "postCompact",
2253
2290
  "elicitation",
2254
2291
  "elicitationResult"
@@ -2281,7 +2318,15 @@ const DEVIN_HOOK_EVENTS = [
2281
2318
  "permissionRequest",
2282
2319
  "postCompact"
2283
2320
  ];
2284
- /** Hook events supported by OpenCode. */
2321
+ /**
2322
+ * Hook events supported by OpenCode.
2323
+ *
2324
+ * `preCompact` maps to `experimental.session.compacting`, which the plugin docs
2325
+ * document as a named `(input, output)` hook rather than an `event.type`
2326
+ * dispatch; the other entries are all generic events.
2327
+ *
2328
+ * @see https://opencode.ai/docs/plugins/
2329
+ */
2285
2330
  const OPENCODE_HOOK_EVENTS = [
2286
2331
  "sessionStart",
2287
2332
  "preToolUse",
@@ -2289,9 +2334,19 @@ const OPENCODE_HOOK_EVENTS = [
2289
2334
  "stop",
2290
2335
  "afterFileEdit",
2291
2336
  "afterShellExecution",
2292
- "permissionRequest"
2337
+ "permissionRequest",
2338
+ "preCompact",
2339
+ "postCompact",
2340
+ "afterError",
2341
+ "fileChanged"
2293
2342
  ];
2294
- /** Hook events supported by Kilo. (Currently identical to OpenCode) */
2343
+ /**
2344
+ * Hook events supported by Kilo. Identical to OpenCode: Kilo's plugin docs list
2345
+ * the same event surface, including `session.compacted`, `session.error`,
2346
+ * `file.watcher.updated` and the experimental compaction hook.
2347
+ *
2348
+ * @see https://kilo.ai/docs/automate/extending/plugins
2349
+ */
2295
2350
  const KILO_HOOK_EVENTS = OPENCODE_HOOK_EVENTS;
2296
2351
  /**
2297
2352
  * Hook events supported by Pi Coding Agent, bridged through a generated
@@ -2358,7 +2413,8 @@ const COPILOT_HOOK_EVENTS = [
2358
2413
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
2359
2414
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
2360
2415
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
2361
- * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
2416
+ * `notification`, `userPromptTransformed` ← `userPromptExpansion`,
2417
+ * `preMcpToolCall` ← `beforeMCPExecution`.
2362
2418
  *
2363
2419
  * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
2364
2420
  * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
@@ -2380,6 +2436,7 @@ const COPILOTCLI_HOOK_EVENTS = [
2380
2436
  "preCompact",
2381
2437
  "permissionRequest",
2382
2438
  "notification",
2439
+ "userPromptExpansion",
2383
2440
  "beforeMCPExecution"
2384
2441
  ];
2385
2442
  /**
@@ -2424,6 +2481,7 @@ const DEEPAGENTS_HOOK_EVENTS = [
2424
2481
  /** Hook events supported by Codex CLI. */
2425
2482
  const CODEXCLI_HOOK_EVENTS = [
2426
2483
  "sessionStart",
2484
+ "sessionEnd",
2427
2485
  "preToolUse",
2428
2486
  "postToolUse",
2429
2487
  "beforeSubmitPrompt",
@@ -2858,6 +2916,7 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
2858
2916
  configChange: "ConfigChange",
2859
2917
  cwdChanged: "CwdChanged",
2860
2918
  fileChanged: "FileChanged",
2919
+ directoryAdded: "DirectoryAdded",
2861
2920
  postCompact: "PostCompact",
2862
2921
  elicitation: "Elicitation",
2863
2922
  elicitationResult: "ElicitationResult"
@@ -2979,7 +3038,11 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
2979
3038
  stop: "session.idle",
2980
3039
  afterFileEdit: "file.edited",
2981
3040
  afterShellExecution: "command.executed",
2982
- permissionRequest: "permission.asked"
3041
+ permissionRequest: "permission.asked",
3042
+ preCompact: "experimental.session.compacting",
3043
+ postCompact: "session.compacted",
3044
+ afterError: "session.error",
3045
+ fileChanged: "file.watcher.updated"
2983
3046
  };
2984
3047
  /**
2985
3048
  * Map canonical camelCase event names to Kilo dot-notation.
@@ -3055,6 +3118,7 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
3055
3118
  preCompact: "preCompact",
3056
3119
  permissionRequest: "permissionRequest",
3057
3120
  notification: "notification",
3121
+ userPromptExpansion: "userPromptTransformed",
3058
3122
  beforeMCPExecution: "preMcpToolCall"
3059
3123
  };
3060
3124
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
@@ -3064,6 +3128,7 @@ const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CA
3064
3128
  */
3065
3129
  const CANONICAL_TO_CODEXCLI_EVENT_NAMES = {
3066
3130
  sessionStart: "SessionStart",
3131
+ sessionEnd: "SessionEnd",
3067
3132
  preToolUse: "PreToolUse",
3068
3133
  postToolUse: "PostToolUse",
3069
3134
  beforeSubmitPrompt: "UserPromptSubmit",
@@ -3486,6 +3551,25 @@ var RulesyncIgnore = class RulesyncIgnore extends RulesyncFile {
3486
3551
  //#endregion
3487
3552
  //#region src/types/mcp.ts
3488
3553
  const EnvVarNameSchema = zod_mini.z.string().check((0, zod_mini.refine)((value) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(value), "envVars entries must be valid environment variable names"));
3554
+ /**
3555
+ * One `envVars` entry. A bare name reads the variable from Codex's own
3556
+ * environment; the object form names the environment to read it from, where
3557
+ * `source = "remote"` reads from the remote executor environment.
3558
+ * @see https://learn.chatgpt.com/docs/extend/mcp
3559
+ */
3560
+ const EnvVarEntrySchema = zod_mini.z.union([EnvVarNameSchema, zod_mini.z.strictObject({
3561
+ name: EnvVarNameSchema,
3562
+ source: zod_mini.z.optional(zod_mini.z.enum(["local", "remote"]))
3563
+ })]);
3564
+ /**
3565
+ * Whether a value is usable as `envVars`. Applied in both directions by the
3566
+ * codex adapter, so an entry read out of somebody's `config.toml` can never be
3567
+ * imported into a `.rulesync/mcp.jsonc` that the next generate would refuse to
3568
+ * parse.
3569
+ */
3570
+ function isEnvVarEntryArray(value) {
3571
+ return Array.isArray(value) && value.every((entry) => EnvVarEntrySchema.safeParse(entry).success);
3572
+ }
3489
3573
  const McpServerSchema = zod_mini.z.looseObject({
3490
3574
  type: zod_mini.z.optional(zod_mini.z.enum([
3491
3575
  "local",
@@ -3500,7 +3584,8 @@ const McpServerSchema = zod_mini.z.looseObject({
3500
3584
  url: zod_mini.z.optional(zod_mini.z.string()),
3501
3585
  httpUrl: zod_mini.z.optional(zod_mini.z.string()),
3502
3586
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
3503
- envVars: zod_mini.z.optional(zod_mini.z.array(EnvVarNameSchema)),
3587
+ envVars: zod_mini.z.optional(zod_mini.z.array(EnvVarEntrySchema)),
3588
+ experimentalEnvironment: zod_mini.z.optional(zod_mini.z.string()),
3504
3589
  disabled: zod_mini.z.optional(zod_mini.z.boolean()),
3505
3590
  networkTimeout: zod_mini.z.optional(zod_mini.z.number()),
3506
3591
  timeout: zod_mini.z.optional(zod_mini.z.number()),
@@ -3666,6 +3751,8 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
3666
3751
  "description",
3667
3752
  "exposed",
3668
3753
  "envVars",
3754
+ "experimentalEnvironment",
3755
+ "experimental_environment",
3669
3756
  "enabled"
3670
3757
  ])];
3671
3758
  }));
@@ -3952,10 +4039,30 @@ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
3952
4039
  * portable and keeps them out of other tools' configs. Mirrors the OpenCode
3953
4040
  * override; each value may be a bare action string or a pattern map.
3954
4041
  *
4042
+ * `sandbox` is the sibling top-level block that governs the sandbox Kilo runs
4043
+ * commands in: `enabled` (boolean), `network` (`"deny"` and friends),
4044
+ * `allowed_hosts` (a list of `host` / `host:port` destination exceptions) and
4045
+ * `writable_paths`. It has no canonical permission category, so it is authored
4046
+ * here and emitted only for Kilo.
4047
+ *
4048
+ * Upstream restricts what a *project* config may say: `allowed_hosts` and
4049
+ * `writable_paths` are honored from the global config only, and a project
4050
+ * config may merely tighten (`enabled: true`, `network: "deny"`) — a
4051
+ * project-level network denial even clears the global destination exceptions.
4052
+ * rulesync mirrors that: at project scope only `enabled` and `network` are
4053
+ * written, and the rest are dropped with a warning rather than emitted into a
4054
+ * file Kilo would ignore.
4055
+ *
3955
4056
  * @example
3956
4057
  * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
4058
+ * @example
4059
+ * { "sandbox": { "enabled": true, "network": "deny" } }
4060
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
3957
4061
  */
3958
- const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
4062
+ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({
4063
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)),
4064
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
4065
+ });
3959
4066
  /**
3960
4067
  * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
3961
4068
  * (in `.claude/settings.json`) carries non-list fields that have no canonical
@@ -3968,12 +4075,23 @@ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
3968
4075
  * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
3969
4076
  * ignored here (rulesync owns them).
3970
4077
  *
4078
+ * `sandbox` is the sibling top-level settings subtree that governs the sandbox
4079
+ * Claude Code runs commands in (`sandbox.network.*`, `sandbox.filesystem.*`,
4080
+ * `sandbox.credentials`, `sandbox.allowAppleEvents`, ...). It has no canonical
4081
+ * permission category either — it constrains how a permitted command runs
4082
+ * rather than which commands are permitted — so it is a loose passthrough on
4083
+ * the same terms, merged into the top level of `.claude/settings.json`.
4084
+ *
3971
4085
  * @example
3972
4086
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
4087
+ * @example
4088
+ * { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
4089
+ * @see https://code.claude.com/docs/en/sandboxing
3973
4090
  */
3974
4091
  const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
3975
4092
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
3976
- permissions: zod_mini.z.optional(zod_mini.z.looseObject({}))
4093
+ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})),
4094
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
3977
4095
  });
3978
4096
  /**
3979
4097
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
@@ -4246,19 +4364,24 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
4246
4364
  });
4247
4365
  /**
4248
4366
  * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
4249
- * `settings.json` carries two global autonomy/sandbox knobs outside the
4367
+ * `settings.json` carries four global autonomy/sandbox knobs outside the
4250
4368
  * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
4251
4369
  * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
4252
- * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
4253
- * agent-run commands to OS containment). Antigravity applies the allow/deny
4370
+ * `always-proceed` / `strict`), `enableTerminalSandbox` (a boolean confining
4371
+ * agent-run commands to OS containment), `artifactReviewPolicy` (whether the
4372
+ * agent's artifact changes are gated on a review prompt — `asks-for-review`
4373
+ * (default) / `agent-decides` / `always-proceed`) and `allowNonWorkspaceAccess`
4374
+ * (a boolean, off by default, letting the agent read or write files outside the
4375
+ * active workspace roots). Antigravity applies the allow/deny
4254
4376
  * lists as per-rule exceptions to the preset at runtime, so rulesync only
4255
4377
  * authors these keys verbatim — no precedence modeling is needed on our side.
4256
4378
  * Fields placed here are merged onto the top level of
4257
4379
  * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
4258
4380
  * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
4259
4381
  * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
4260
- * Verified against https://antigravity.google/docs/cli/reference and
4261
- * https://antigravity.google/docs/cli/sandbox.
4382
+ * Verified against https://antigravity.google/docs/cli/reference,
4383
+ * https://antigravity.google/docs/cli/sandbox and
4384
+ * https://antigravity.google/docs/cli/settings.
4262
4385
  *
4263
4386
  * @example
4264
4387
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
@@ -4271,7 +4394,13 @@ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
4271
4394
  "always-proceed",
4272
4395
  "strict"
4273
4396
  ])),
4274
- enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
4397
+ enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean()),
4398
+ artifactReviewPolicy: zod_mini.z.optional(zod_mini.z.enum([
4399
+ "asks-for-review",
4400
+ "agent-decides",
4401
+ "always-proceed"
4402
+ ])),
4403
+ allowNonWorkspaceAccess: zod_mini.z.optional(zod_mini.z.boolean())
4275
4404
  });
4276
4405
  /**
4277
4406
  * Tool-scoped override block for AugmentCode. AugmentCode's `toolPermissions[]`
@@ -4675,11 +4804,14 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
4675
4804
  description: zod_mini.z.optional(zod_mini.z.string()),
4676
4805
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4677
4806
  })),
4678
- copilot: zod_mini.z.optional(zod_mini.z.looseObject({ excludeAgent: zod_mini.z.optional(zod_mini.z.union([
4679
- zod_mini.z.literal("code-review"),
4680
- zod_mini.z.literal("cloud-agent"),
4681
- zod_mini.z.literal("coding-agent")
4682
- ])) })),
4807
+ copilot: zod_mini.z.optional(zod_mini.z.looseObject({
4808
+ excludeAgent: zod_mini.z.optional(zod_mini.z.union([
4809
+ zod_mini.z.literal("code-review"),
4810
+ zod_mini.z.literal("cloud-agent"),
4811
+ zod_mini.z.literal("coding-agent")
4812
+ ])),
4813
+ name: zod_mini.z.optional(zod_mini.z.string())
4814
+ })),
4683
4815
  antigravity: zod_mini.z.optional(zod_mini.z.looseObject({
4684
4816
  trigger: zod_mini.z.optional(zod_mini.z.string()),
4685
4817
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
@@ -4887,6 +5019,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
4887
5019
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
4888
5020
  context: zod_mini.z.optional(zod_mini.z.string()),
4889
5021
  agent: zod_mini.z.optional(zod_mini.z.string()),
5022
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
4890
5023
  hooks: zod_mini.z.optional(zod_mini.z.looseObject({})),
4891
5024
  shell: zod_mini.z.optional(zod_mini.z.string()),
4892
5025
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
@@ -4938,7 +5071,9 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
4938
5071
  copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
4939
5072
  license: zod_mini.z.optional(zod_mini.z.string()),
4940
5073
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
4941
- "argument-hint": zod_mini.z.optional(zod_mini.z.string())
5074
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
5075
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
5076
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
4942
5077
  })),
4943
5078
  pi: zod_mini.z.optional(zod_mini.z.looseObject({
4944
5079
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
@@ -5224,6 +5359,38 @@ function resolveUserInvocable({ rootFrontmatter, section }) {
5224
5359
  return section?.["user-invocable"] ?? rootFrontmatter["user-invocable"];
5225
5360
  }
5226
5361
  //#endregion
5362
+ //#region src/constants/cursor-paths.ts
5363
+ const CURSOR_DIR = ".cursor";
5364
+ const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
5365
+ const CURSOR_SKILLS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "skills");
5366
+ const CURSOR_AGENTS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "agents");
5367
+ const CURSOR_BUGBOT_FILE_NAME = "BUGBOT.md";
5368
+ const CURSOR_MCP_FILE_NAME = "mcp.json";
5369
+ const CURSOR_HOOKS_FILE_NAME = "hooks.json";
5370
+ const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
5371
+ const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
5372
+ const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
5373
+ //#endregion
5374
+ //#region src/constants/rovodev-paths.ts
5375
+ const ROVODEV_DIR = ".rovodev";
5376
+ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
5377
+ const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
5378
+ const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
5379
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5380
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5381
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
5382
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5383
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
5384
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
5385
+ const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
5386
+ /**
5387
+ * Custom instructions for Rovo Dev's code reviews: a plain-Markdown file (no
5388
+ * frontmatter) in the repository root's `.rovodev/` folder. Note the leading
5389
+ * dot in the file name.
5390
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
5391
+ */
5392
+ const ROVODEV_REVIEW_AGENT_FILE_NAME = ".review-agent.md";
5393
+ //#endregion
5227
5394
  //#region src/constants/takt-paths.ts
5228
5395
  const TAKT_DIR = ".takt";
5229
5396
  const TAKT_FACETS_SUBDIR = "facets";
@@ -5674,6 +5841,276 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5674
5841
  }
5675
5842
  };
5676
5843
  //#endregion
5844
+ //#region src/features/checks/check-slug.ts
5845
+ /**
5846
+ * Turn a name that came out of a tool's own config file into one safe to use as
5847
+ * a `.rulesync/checks/<name>.md` file name. Shared by the adapters whose checks
5848
+ * collapse into a single file, since the name they read back is whatever the
5849
+ * user wrote there.
5850
+ */
5851
+ function slugifyCheckName(value) {
5852
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
5853
+ }
5854
+ //#endregion
5855
+ //#region src/features/checks/aggregated-check-file.ts
5856
+ /**
5857
+ * Shared machinery for the tools whose checks surface is **one aggregated
5858
+ * instruction file** rather than a file per check — Cursor Bugbot's
5859
+ * `.cursor/BUGBOT.md` and Rovo Dev's `.rovodev/.review-agent.md`.
5860
+ *
5861
+ * Both read the file as free prose, so the check identities have to be carried
5862
+ * in something invisible to the reader: an HTML-comment marker per section.
5863
+ * That marker convention, the escaping that keeps a check body from splitting
5864
+ * itself, and the import-side split are the same for both files, so they live
5865
+ * here once.
5866
+ */
5867
+ /** Marks where one check starts inside the single instruction file. */
5868
+ const CHECK_MARKER_PATTERN = /^<!--\s*rulesync:check:(.+?)\s*-->[ \t]*$/gm;
5869
+ /**
5870
+ * A marker line a check body wrote itself — a rulesync doc fragment quoted in a
5871
+ * code block, say. Emitting it verbatim would split that check in two on the
5872
+ * next import, so `literal-` is inserted before `check:` on the way out and
5873
+ * taken off on the way back. `(?:literal-)*` makes it a ladder, so a body that
5874
+ * already contains an escaped marker survives the round trip too.
5875
+ */
5876
+ const ESCAPABLE_MARKER_PATTERN = /^(<!--\s*rulesync:)((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5877
+ const ESCAPED_MARKER_PATTERN = /^(<!--\s*rulesync:)literal-((?:literal-)*check:.+?\s*-->[ \t]*)$/gm;
5878
+ function renderCheckMarker(name) {
5879
+ return `<!-- rulesync:check:${name} -->`;
5880
+ }
5881
+ function escapeCheckMarkers(content) {
5882
+ return content.replace(ESCAPABLE_MARKER_PATTERN, "$1literal-$2");
5883
+ }
5884
+ function unescapeCheckMarkers(content) {
5885
+ return content.replace(ESCAPED_MARKER_PATTERN, "$1$2");
5886
+ }
5887
+ function findCheckMarkers(fileContent) {
5888
+ CHECK_MARKER_PATTERN.lastIndex = 0;
5889
+ const markers = [];
5890
+ let match = CHECK_MARKER_PATTERN.exec(fileContent);
5891
+ while (match !== null) {
5892
+ markers.push({
5893
+ name: match[1] ?? "",
5894
+ start: match.index,
5895
+ end: match.index + match[0].length
5896
+ });
5897
+ match = CHECK_MARKER_PATTERN.exec(fileContent);
5898
+ }
5899
+ return markers;
5900
+ }
5901
+ /**
5902
+ * Whether the file holds instruction text ahead of the first marker — the
5903
+ * question the "generating replaces this" warning asks. A file with no marker
5904
+ * at all is entirely hand-written, so it qualifies; an empty one does not,
5905
+ * since there is nothing to replace.
5906
+ */
5907
+ function hasHandWrittenPreamble(fileContent) {
5908
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start ?? fileContent.length;
5909
+ return fileContent.slice(0, firstMarkerStart).trim().length > 0;
5910
+ }
5911
+ /**
5912
+ * Whether the file is nothing but sections rulesync generated — the question
5913
+ * the deletion guard asks, and a stricter one than
5914
+ * {@link hasHandWrittenPreamble}. A file carrying no marker at all is not
5915
+ * rulesync's to remove even when it is empty: rulesync never wrote it, so an
5916
+ * empty one is somebody's placeholder rather than our leftover.
5917
+ */
5918
+ function isOnlyGeneratedSections(fileContent) {
5919
+ const firstMarkerStart = findCheckMarkers(fileContent)[0]?.start;
5920
+ if (firstMarkerStart === void 0) return false;
5921
+ return fileContent.slice(0, firstMarkerStart).trim().length === 0;
5922
+ }
5923
+ /**
5924
+ * The instruction text one check contributes. Neither file has a field to put a
5925
+ * summary in, so `description` is used only when there is no body.
5926
+ */
5927
+ function toInstruction(rulesyncCheck) {
5928
+ const body = rulesyncCheck.getBody().trim();
5929
+ if (body.length > 0) return body;
5930
+ return rulesyncCheck.getFrontmatter().description?.trim() ?? "";
5931
+ }
5932
+ function renderCheckSection(rulesyncCheck) {
5933
+ const name = (0, node_path.basename)(rulesyncCheck.getRelativeFilePath(), ".md");
5934
+ const heading = `## ${name}`;
5935
+ const instruction = toInstruction(rulesyncCheck);
5936
+ const lines = [renderCheckMarker(name), heading];
5937
+ if (instruction.length > 0) lines.push("", escapeCheckMarkers(instruction));
5938
+ return lines.join("\n");
5939
+ }
5940
+ function renderCheckFile(rulesyncChecks) {
5941
+ return `${rulesyncChecks.map(renderCheckSection).join("\n\n")}\n`;
5942
+ }
5943
+ /** Drop the heading generate writes, so a round trip does not stack headings. */
5944
+ function stripGeneratedHeading(section, name) {
5945
+ const [firstLine, ...rest] = section.split("\n");
5946
+ if (firstLine?.trim() === `## ${name}`) return rest.join("\n").trim();
5947
+ return section.trim();
5948
+ }
5949
+ /**
5950
+ * Split an aggregated instruction file back into one check per section.
5951
+ *
5952
+ * Content ahead of the first marker — and a hand-written file with no markers
5953
+ * at all — becomes a single check named `fallbackName`, so nothing in the file
5954
+ * is dropped.
5955
+ */
5956
+ function splitCheckFile({ fileContent, fallbackName }) {
5957
+ const sections = [];
5958
+ const markers = findCheckMarkers(fileContent);
5959
+ const preambleEnd = markers[0]?.start ?? fileContent.length;
5960
+ const preamble = fileContent.slice(0, preambleEnd).trim();
5961
+ if (preamble.length > 0) sections.push({
5962
+ name: fallbackName,
5963
+ content: unescapeCheckMarkers(preamble)
5964
+ });
5965
+ for (const [index, marker] of markers.entries()) {
5966
+ const sectionEnd = markers[index + 1]?.start ?? fileContent.length;
5967
+ const markerName = marker.name.trim();
5968
+ const name = slugifyCheckName(markerName) || fallbackName;
5969
+ const content = stripGeneratedHeading(fileContent.slice(marker.end, sectionEnd).trim(), markerName);
5970
+ sections.push({
5971
+ name,
5972
+ content: unescapeCheckMarkers(content)
5973
+ });
5974
+ }
5975
+ const used = /* @__PURE__ */ new Set();
5976
+ return sections.map(({ name, content }) => {
5977
+ let uniqueName = name;
5978
+ let suffix = 2;
5979
+ while (used.has(uniqueName)) {
5980
+ uniqueName = `${name}-${suffix}`;
5981
+ suffix += 1;
5982
+ }
5983
+ used.add(uniqueName);
5984
+ return new RulesyncCheck({
5985
+ outputRoot: ".",
5986
+ relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
5987
+ relativeFilePath: `${uniqueName}.md`,
5988
+ frontmatter: { targets: ["*"] },
5989
+ body: content
5990
+ });
5991
+ });
5992
+ }
5993
+ //#endregion
5994
+ //#region src/features/checks/cursor-check.ts
5995
+ const FALLBACK_CHECK_NAME$1 = "bugbot";
5996
+ /**
5997
+ * Checks adapter for Cursor Bugbot (`.cursor/BUGBOT.md`).
5998
+ *
5999
+ * Bugbot takes one aggregated instruction file per directory rather than a file
6000
+ * per check, so every `.rulesync/checks/*.md` targeting Cursor collapses into
6001
+ * the repository-root `.cursor/BUGBOT.md` — hence {@link fromRulesyncChecks}
6002
+ * rather than the usual per-check conversion. Each check becomes one section:
6003
+ * an HTML-comment marker carrying the check name, an `## <name>` heading, and
6004
+ * the check body as the instruction text (the `description` is used when the
6005
+ * body is empty).
6006
+ *
6007
+ * Bugbot reads the file as free prose, so a check's `severity` and `tools` have
6008
+ * no equivalent there: they are not written and do not come back on import. So
6009
+ * is `description` whenever the check also has a body.
6010
+ *
6011
+ * Project scope only — Bugbot reads repository files, and there is no
6012
+ * user-level instruction file. Bugbot also merges nested `<dir>/.cursor/BUGBOT.md`
6013
+ * files found while traversing upward from changed files, but rulesync check
6014
+ * sources carry no directory-placement semantics, so only the root file is
6015
+ * generated.
6016
+ *
6017
+ * On import the markers split the file back into one check per section; content
6018
+ * before the first marker — and a hand-written file with no markers at all —
6019
+ * becomes a single `bugbot` check, so nothing in the file is dropped. A file
6020
+ * holding anything rulesync did not write is never deleted either (see
6021
+ * {@link canDeleteAuxiliaryFiles}), though generating checks for Cursor does
6022
+ * replace it — import first to keep what is there, which is warned about.
6023
+ *
6024
+ * @see https://cursor.com/docs/bugbot
6025
+ */
6026
+ var CursorCheck = class CursorCheck extends ToolCheck {
6027
+ static getSettablePaths(_options = {}) {
6028
+ return {
6029
+ relativeDirPath: CURSOR_DIR,
6030
+ relativeFilePath: CURSOR_BUGBOT_FILE_NAME
6031
+ };
6032
+ }
6033
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6034
+ return this.isTargetedByRulesyncCheckDefault({
6035
+ rulesyncCheck,
6036
+ toolTarget: "cursor"
6037
+ });
6038
+ }
6039
+ /**
6040
+ * Ownership guard the processor consults before it deletes anything for this
6041
+ * tool. `.cursor/BUGBOT.md` is a file Cursor's own documentation tells users
6042
+ * to hand-write, so anything in it that rulesync did not write is not
6043
+ * rulesync's to remove — dropping the last check targeting Cursor must not
6044
+ * take somebody's hand-written review instructions with it. Deletion is
6045
+ * therefore allowed only for a file that is nothing but generated sections:
6046
+ * one that carries no marker at all, or that carries hand-written text ahead
6047
+ * of the first marker, stays.
6048
+ */
6049
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6050
+ const paths = CursorCheck.getSettablePaths();
6051
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? "BUGBOT.md"));
6052
+ if (fileContent === null) return true;
6053
+ return isOnlyGeneratedSections(fileContent);
6054
+ }
6055
+ static fromRulesyncCheck(_params) {
6056
+ throw new Error("Cursor checks are built from all checks at once; use fromRulesyncChecks.");
6057
+ }
6058
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6059
+ if (rulesyncChecks.length === 0) return [];
6060
+ const paths = CursorCheck.getSettablePaths({ global });
6061
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
6062
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6063
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Cursor checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets cursor --features checks\` first to keep them.`);
6064
+ const fileContent = renderCheckFile(rulesyncChecks);
6065
+ return [new CursorCheck({
6066
+ outputRoot,
6067
+ relativeDirPath: paths.relativeDirPath,
6068
+ relativeFilePath,
6069
+ fileContent,
6070
+ global
6071
+ })];
6072
+ }
6073
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6074
+ const paths = CursorCheck.getSettablePaths({ global });
6075
+ const relativeFilePath = paths.relativeFilePath ?? "BUGBOT.md";
6076
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6077
+ return new CursorCheck({
6078
+ outputRoot,
6079
+ relativeDirPath: paths.relativeDirPath,
6080
+ relativeFilePath,
6081
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6082
+ global
6083
+ });
6084
+ }
6085
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6086
+ return new CursorCheck({
6087
+ outputRoot,
6088
+ relativeDirPath,
6089
+ relativeFilePath,
6090
+ fileContent: "",
6091
+ validate: false,
6092
+ global
6093
+ });
6094
+ }
6095
+ validate() {
6096
+ return {
6097
+ success: true,
6098
+ error: null
6099
+ };
6100
+ }
6101
+ toRulesyncCheck() {
6102
+ const first = this.toRulesyncChecks()[0];
6103
+ if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6104
+ return first;
6105
+ }
6106
+ toRulesyncChecks() {
6107
+ return splitCheckFile({
6108
+ fileContent: this.getFileContent(),
6109
+ fallbackName: FALLBACK_CHECK_NAME$1
6110
+ });
6111
+ }
6112
+ };
6113
+ //#endregion
5677
6114
  //#region src/constants/hermesagent-paths.ts
5678
6115
  /**
5679
6116
  * Hermes Agent configuration-layout conventions.
@@ -5691,8 +6128,22 @@ var AmpCheck = class AmpCheck extends ToolCheck {
5691
6128
  */
5692
6129
  /** Project-root instruction file auto-injected by Hermes Agent. */
5693
6130
  const HERMESAGENT_RULE_FILE_NAME = ".hermes.md";
5694
- /** Root directory for Hermes Agent global configuration (the HERMES_HOME dir). */
6131
+ /**
6132
+ * Root directory for Hermes Agent global configuration (the HERMES_HOME dir).
6133
+ * Also the project-local plugin tree, which is `.hermes/` on every platform.
6134
+ */
5695
6135
  const HERMESAGENT_GLOBAL_DIR = ".hermes";
6136
+ /**
6137
+ * Home-relative global profile root on Windows: upstream defaults to
6138
+ * `%LOCALAPPDATA%\hermes` there, not `~/.hermes`.
6139
+ * Resolve it through `getHermesagentGlobalDir()` rather than reading it directly.
6140
+ *
6141
+ * Home-relative rather than read from `LOCALAPPDATA`, matching how every other
6142
+ * Windows global path in rulesync is spelled (`ZED_GLOBAL_WIN32_DIR`,
6143
+ * `WARP_WIN32_DIR`). A profile with `LOCALAPPDATA` redirected elsewhere is not
6144
+ * followed; those users should set `HERMES_HOME` explicitly.
6145
+ */
6146
+ const HERMESAGENT_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Local", "hermes");
5696
6147
  /** MCP servers and other settings live in `config.yaml` under `~/.hermes/`. */
5697
6148
  const HERMESAGENT_CONFIG_FILE_NAME = "config.yaml";
5698
6149
  const HERMESAGENT_CONFIG_FILE_PATH = (0, node_path.join)(HERMESAGENT_GLOBAL_DIR, HERMESAGENT_CONFIG_FILE_NAME);
@@ -5915,6 +6366,114 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
5915
6366
  }
5916
6367
  };
5917
6368
  //#endregion
6369
+ //#region src/features/checks/rovodev-check.ts
6370
+ const FALLBACK_CHECK_NAME = "review-agent";
6371
+ /**
6372
+ * Checks adapter for Rovo Dev CLI's code-review custom instructions
6373
+ * (`.rovodev/.review-agent.md`).
6374
+ *
6375
+ * Rovo Dev takes one plain-Markdown instruction file at the repository root's
6376
+ * `.rovodev/` folder — no frontmatter, and note the leading dot in the file
6377
+ * name. Like Cursor Bugbot it is a single aggregated file rather than a file
6378
+ * per check, so every `.rulesync/checks/*.md` targeting Rovo Dev collapses into
6379
+ * it via {@link fromRulesyncChecks}, with each check written as a marked
6380
+ * section (see `aggregated-check-file.ts` for the marker convention the two
6381
+ * adapters share).
6382
+ *
6383
+ * Rovo Dev reads the file as free prose, so a check's `severity` and `tools`
6384
+ * have no equivalent there: they are not written and do not come back on
6385
+ * import. Neither does `description` whenever the check also has a body.
6386
+ *
6387
+ * Project scope only — these are per-repository review instructions, and Rovo
6388
+ * Dev documents no user-level equivalent. (The `permissions` adapter for the
6389
+ * same tool is the opposite: global only.)
6390
+ *
6391
+ * @see https://support.atlassian.com/rovo/docs/set-custom-instructions-for-code-reviews/
6392
+ */
6393
+ var RovodevCheck = class RovodevCheck extends ToolCheck {
6394
+ static getSettablePaths(_options = {}) {
6395
+ return {
6396
+ relativeDirPath: ROVODEV_DIR,
6397
+ relativeFilePath: ROVODEV_REVIEW_AGENT_FILE_NAME
6398
+ };
6399
+ }
6400
+ static isTargetedByRulesyncCheck(rulesyncCheck) {
6401
+ return this.isTargetedByRulesyncCheckDefault({
6402
+ rulesyncCheck,
6403
+ toolTarget: "rovodev"
6404
+ });
6405
+ }
6406
+ /**
6407
+ * Ownership guard the processor consults before it deletes anything for this
6408
+ * tool. `.review-agent.md` is a file Rovo Dev's own documentation tells users
6409
+ * to hand-write, so anything in it that rulesync did not write is not
6410
+ * rulesync's to remove — dropping the last check targeting Rovo Dev must not
6411
+ * take somebody's hand-written review instructions with it.
6412
+ */
6413
+ static async canDeleteAuxiliaryFiles({ outputRoot }) {
6414
+ const paths = RovodevCheck.getSettablePaths();
6415
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath ?? ".review-agent.md"));
6416
+ if (fileContent === null) return true;
6417
+ return isOnlyGeneratedSections(fileContent);
6418
+ }
6419
+ static fromRulesyncCheck(_params) {
6420
+ throw new Error("Rovo Dev checks are built from all checks at once; use fromRulesyncChecks.");
6421
+ }
6422
+ static async fromRulesyncChecks({ outputRoot = process.cwd(), rulesyncChecks, global = false, logger }) {
6423
+ if (rulesyncChecks.length === 0) return [];
6424
+ const paths = RovodevCheck.getSettablePaths({ global });
6425
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6426
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6427
+ if (hasHandWrittenPreamble(await readFileContentOrNull(filePath) ?? "")) logger?.warn(`Rovo Dev checks: ${filePath} holds instructions rulesync did not write, and generating replaces the whole file. Run \`rulesync import --targets rovodev --features checks\` first to keep them.`);
6428
+ return [new RovodevCheck({
6429
+ outputRoot,
6430
+ relativeDirPath: paths.relativeDirPath,
6431
+ relativeFilePath,
6432
+ fileContent: renderCheckFile(rulesyncChecks),
6433
+ global
6434
+ })];
6435
+ }
6436
+ static async fromFile({ outputRoot = process.cwd(), global = false }) {
6437
+ const paths = RovodevCheck.getSettablePaths({ global });
6438
+ const relativeFilePath = paths.relativeFilePath ?? ".review-agent.md";
6439
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
6440
+ return new RovodevCheck({
6441
+ outputRoot,
6442
+ relativeDirPath: paths.relativeDirPath,
6443
+ relativeFilePath,
6444
+ fileContent: await readFileContentOrNull(filePath) ?? "",
6445
+ global
6446
+ });
6447
+ }
6448
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
6449
+ return new RovodevCheck({
6450
+ outputRoot,
6451
+ relativeDirPath,
6452
+ relativeFilePath,
6453
+ fileContent: "",
6454
+ validate: false,
6455
+ global
6456
+ });
6457
+ }
6458
+ validate() {
6459
+ return {
6460
+ success: true,
6461
+ error: null
6462
+ };
6463
+ }
6464
+ toRulesyncCheck() {
6465
+ const first = this.toRulesyncChecks()[0];
6466
+ if (!first) throw new Error(`No check instructions found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}.`);
6467
+ return first;
6468
+ }
6469
+ toRulesyncChecks() {
6470
+ return splitCheckFile({
6471
+ fileContent: this.getFileContent(),
6472
+ fallbackName: FALLBACK_CHECK_NAME
6473
+ });
6474
+ }
6475
+ };
6476
+ //#endregion
5918
6477
  //#region src/constants/codexcli-paths.ts
5919
6478
  const CODEXCLI_DIR = ".codex";
5920
6479
  const CODEXCLI_PROMPTS_DIR_PATH = (0, node_path.join)(CODEXCLI_DIR, "prompts");
@@ -6027,11 +6586,14 @@ function mergeSharedConfigDeep({ base, patch }) {
6027
6586
  }
6028
6587
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
6029
6588
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
6589
+ const HERMES_WIN32_CONFIG_SHARED_FILE_KEY = "AppData/Local/hermes/config.yaml";
6590
+ const HERMES_HOME_CONFIG_SHARED_FILE_KEY = "config.yaml";
6030
6591
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
6031
6592
  const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
6032
6593
  const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
6033
6594
  const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
6034
6595
  const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6596
+ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6035
6597
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6036
6598
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6037
6599
  /**
@@ -6055,6 +6617,65 @@ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
6055
6617
  * lock-step with the writers derived from the processor registry, so an
6056
6618
  * undeclared writer fails CI instead of merging by accident.
6057
6619
  */
6620
+ /**
6621
+ * Hermes writes one `config.yaml`, but its global profile root has three
6622
+ * spellings (`~/.hermes`, the win32 `%LOCALAPPDATA%\hermes`, and `HERMES_HOME`
6623
+ * itself). They are the same file with the same owners, so the declaration is
6624
+ * written once and shared — a policy edit cannot land on one spelling only.
6625
+ */
6626
+ const HERMES_CONFIG_DECLARATION = {
6627
+ format: "yaml",
6628
+ features: {
6629
+ commands: {
6630
+ kind: "replace-owned-keys",
6631
+ ownedKeys: ["plugins"]
6632
+ },
6633
+ subagents: {
6634
+ kind: "replace-owned-keys",
6635
+ ownedKeys: ["plugins"]
6636
+ },
6637
+ mcp: {
6638
+ kind: "replace-owned-keys",
6639
+ ownedKeys: ["mcp_servers"]
6640
+ },
6641
+ hooks: {
6642
+ kind: "replace-owned-keys",
6643
+ ownedKeys: ["hooks"]
6644
+ },
6645
+ permissions: {
6646
+ kind: "deep-merge",
6647
+ replaceKeys: ["permissions"]
6648
+ }
6649
+ }
6650
+ };
6651
+ /**
6652
+ * Kimi Code's user config: hooks owns the flat `hooks` array; permissions owns
6653
+ * the ordered rule list and optional coarse default mode. `KIMI_CODE_HOME` can
6654
+ * name the profile directory itself, so the file has two spellings that share
6655
+ * one declaration — a policy edit cannot land on only one of them.
6656
+ */
6657
+ const KIMI_CODE_CONFIG_DECLARATION = {
6658
+ format: "toml",
6659
+ invalidRootPolicy: "error",
6660
+ features: {
6661
+ hooks: {
6662
+ kind: "replace-owned-keys",
6663
+ ownedKeys: ["hooks"]
6664
+ },
6665
+ mcp: {
6666
+ kind: "replace-owned-keys",
6667
+ ownedKeys: ["mcp"]
6668
+ },
6669
+ permissions: {
6670
+ kind: "replace-owned-keys",
6671
+ ownedKeys: [
6672
+ "permission",
6673
+ "default_permission_mode",
6674
+ "tools"
6675
+ ]
6676
+ }
6677
+ }
6678
+ };
6058
6679
  const SHARED_CONFIG_OWNERSHIP = {
6059
6680
  [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
6060
6681
  format: "json",
@@ -6073,31 +6694,9 @@ const SHARED_CONFIG_OWNERSHIP = {
6073
6694
  }
6074
6695
  }
6075
6696
  },
6076
- [HERMES_CONFIG_SHARED_FILE_KEY]: {
6077
- format: "yaml",
6078
- features: {
6079
- commands: {
6080
- kind: "replace-owned-keys",
6081
- ownedKeys: ["plugins"]
6082
- },
6083
- subagents: {
6084
- kind: "replace-owned-keys",
6085
- ownedKeys: ["plugins"]
6086
- },
6087
- mcp: {
6088
- kind: "replace-owned-keys",
6089
- ownedKeys: ["mcp_servers"]
6090
- },
6091
- hooks: {
6092
- kind: "replace-owned-keys",
6093
- ownedKeys: ["hooks"]
6094
- },
6095
- permissions: {
6096
- kind: "deep-merge",
6097
- replaceKeys: ["permissions"]
6098
- }
6099
- }
6100
- },
6697
+ [HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6698
+ [HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6699
+ [HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
6101
6700
  [TAKT_CONFIG_SHARED_FILE_KEY]: {
6102
6701
  format: "yaml",
6103
6702
  invalidRootPolicy: "error",
@@ -6143,6 +6742,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6143
6742
  ".config/zed/settings.json": {
6144
6743
  format: "json",
6145
6744
  features: {
6745
+ ignore: {
6746
+ kind: "replace-owned-keys",
6747
+ ownedKeys: ["private_files"]
6748
+ },
6146
6749
  mcp: {
6147
6750
  kind: "replace-owned-keys",
6148
6751
  ownedKeys: ["context_servers"]
@@ -6156,6 +6759,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6156
6759
  "AppData/Roaming/Zed/settings.json": {
6157
6760
  format: "json",
6158
6761
  features: {
6762
+ ignore: {
6763
+ kind: "replace-owned-keys",
6764
+ ownedKeys: ["private_files"]
6765
+ },
6159
6766
  mcp: {
6160
6767
  kind: "replace-owned-keys",
6161
6768
  ownedKeys: ["context_servers"]
@@ -6172,7 +6779,20 @@ const SHARED_CONFIG_OWNERSHIP = {
6172
6779
  jsoncParseErrors: "error",
6173
6780
  features: { permissions: {
6174
6781
  kind: "replace-owned-keys",
6175
- ownedKeys: ["chat.tools.terminal.autoApprove"]
6782
+ ownedKeys: [
6783
+ "chat.tools.terminal.autoApprove",
6784
+ "chat.tools.edits.autoApprove",
6785
+ "chat.tools.urls.autoApprove"
6786
+ ]
6787
+ } }
6788
+ },
6789
+ ".vscode/mcp.json": {
6790
+ format: "jsonc",
6791
+ invalidRootPolicy: "error",
6792
+ jsoncParseErrors: "error",
6793
+ features: { mcp: {
6794
+ kind: "replace-owned-keys",
6795
+ ownedKeys: ["servers"]
6176
6796
  } }
6177
6797
  },
6178
6798
  ".qwen/settings.json": {
@@ -6399,31 +7019,15 @@ const SHARED_CONFIG_OWNERSHIP = {
6399
7019
  }
6400
7020
  }
6401
7021
  },
6402
- [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: {
6403
- format: "toml",
6404
- invalidRootPolicy: "error",
6405
- features: {
6406
- hooks: {
6407
- kind: "replace-owned-keys",
6408
- ownedKeys: ["hooks"]
6409
- },
6410
- mcp: {
6411
- kind: "replace-owned-keys",
6412
- ownedKeys: ["mcp"]
6413
- },
6414
- permissions: {
6415
- kind: "replace-owned-keys",
6416
- ownedKeys: [
6417
- "permission",
6418
- "default_permission_mode",
6419
- "tools"
6420
- ]
6421
- }
6422
- }
6423
- },
7022
+ [KIMI_CODE_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
7023
+ [KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY]: KIMI_CODE_CONFIG_DECLARATION,
6424
7024
  [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
6425
7025
  format: "toml",
6426
7026
  features: {
7027
+ ignore: {
7028
+ kind: "custom",
7029
+ policyFunction: "applyIgnoreReadDenies"
7030
+ },
6427
7031
  mcp: {
6428
7032
  kind: "replace-owned-keys",
6429
7033
  ownedKeys: ["plugins"]
@@ -6441,6 +7045,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6441
7045
  [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
6442
7046
  format: "toml",
6443
7047
  features: {
7048
+ ignore: {
7049
+ kind: "custom",
7050
+ policyFunction: "applyIgnoreReadDenies"
7051
+ },
6444
7052
  mcp: {
6445
7053
  kind: "replace-owned-keys",
6446
7054
  ownedKeys: ["plugins"]
@@ -6538,7 +7146,12 @@ const applyIgnoreReadDenies = (params) => {
6538
7146
  const applyPermissions = (params) => {
6539
7147
  const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
6540
7148
  const current = parsePermissionsBlock(settings);
6541
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
7149
+ const emitted = /* @__PURE__ */ new Set([
7150
+ ...allow,
7151
+ ...ask,
7152
+ ...deny
7153
+ ]);
7154
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)) && !emitted.has(entry));
6542
7155
  if (logger && managedToolNames.has(READ_TOOL_NAME)) {
6543
7156
  const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
6544
7157
  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.`);
@@ -6688,12 +7301,9 @@ function toRulesyncCheckFromGate({ gate, index, scope, editOnly }) {
6688
7301
  body: ""
6689
7302
  });
6690
7303
  }
6691
- function slugify(value) {
6692
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48).replace(/-+$/, "");
6693
- }
6694
7304
  function slugForGate({ gate, index, scope }) {
6695
- const slug = slugify(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
6696
- return `${scope ? `${slugify(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
7305
+ const slug = slugifyCheckName(typeof gate === "string" ? gate : typeof gate.name === "string" ? gate.name : typeof gate.command === "string" ? gate.command : "");
7306
+ return `${scope ? `${slugifyCheckName(scope.name)}-` : ""}${slug.length > 0 ? slug : "quality-gate"}-${index + 1}`;
6697
7307
  }
6698
7308
  /**
6699
7309
  * Checks adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -6869,6 +7479,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6869
7479
  filePattern: "*.md"
6870
7480
  }
6871
7481
  }],
7482
+ ["cursor", {
7483
+ class: CursorCheck,
7484
+ meta: {
7485
+ supportsGlobal: false,
7486
+ filePattern: CURSOR_BUGBOT_FILE_NAME
7487
+ }
7488
+ }],
6872
7489
  ["hermesagent", {
6873
7490
  class: HermesagentCheck,
6874
7491
  meta: {
@@ -6876,6 +7493,13 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
6876
7493
  filePattern: "*.json"
6877
7494
  }
6878
7495
  }],
7496
+ ["rovodev", {
7497
+ class: RovodevCheck,
7498
+ meta: {
7499
+ supportsGlobal: false,
7500
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7501
+ }
7502
+ }],
6879
7503
  ["takt", {
6880
7504
  class: TaktCheck,
6881
7505
  meta: {
@@ -7031,19 +7655,65 @@ var ChecksProcessor = class extends FeatureProcessor {
7031
7655
  }
7032
7656
  };
7033
7657
  //#endregion
7658
+ //#region src/utils/tool-home.ts
7659
+ /**
7660
+ * Where the rulesync-side source files of a tool with a home override belong.
7661
+ *
7662
+ * A home override redirects the tool's OWN output, but the `.rulesync/` sources
7663
+ * imported back out of it are not part of the tool's profile — they stay under
7664
+ * the rulesync home. When no override is set, the native output root already is
7665
+ * that place.
7666
+ */
7667
+ function getToolRulesyncOutputRoot({ nativeOutputRoot, global, toolHome }) {
7668
+ return global && toolHome() ? getHomeDirectory() : nativeOutputRoot;
7669
+ }
7670
+ //#endregion
7034
7671
  //#region src/utils/hermesagent.ts
7035
7672
  function getHermesagentHome() {
7036
7673
  const configuredHome = process.env.HERMES_HOME?.trim();
7037
7674
  return configuredHome ? (0, node_path.resolve)(configuredHome) : void 0;
7038
7675
  }
7676
+ /**
7677
+ * The home-relative Hermes profile directory used when `HERMES_HOME` is unset.
7678
+ *
7679
+ * Upstream `_get_platform_default_hermes_home()` returns `%LOCALAPPDATA%\hermes`
7680
+ * on win32 and `~/.hermes` everywhere else, so the global output directory is
7681
+ * platform-dependent — a global generate on Windows that wrote `~/.hermes`
7682
+ * would land where Hermes never reads.
7683
+ *
7684
+ * @see https://github.com/NousResearch/hermes-agent `hermes_constants.py`
7685
+ */
7686
+ function getHermesagentGlobalDir() {
7687
+ return process.platform === "win32" ? HERMESAGENT_GLOBAL_WIN32_DIR : HERMESAGENT_GLOBAL_DIR;
7688
+ }
7039
7689
  function resolveHermesagentOutputRoot({ outputRoot, global }) {
7040
7690
  return global ? getHermesagentHome() ?? outputRoot : outputRoot;
7041
7691
  }
7692
+ /**
7693
+ * Map a canonical `.hermes/...` path constant onto the directory rulesync
7694
+ * actually writes in the requested scope.
7695
+ *
7696
+ * Project scope keeps the constant as-is (the project tree is `.hermes/`
7697
+ * everywhere). Global scope strips the `.hermes` prefix and re-anchors it:
7698
+ * `HERMES_HOME` *is* the profile root, so nothing is prepended; otherwise the
7699
+ * platform default directory takes its place.
7700
+ */
7042
7701
  function getHermesagentRelativeDirPath({ global, relativeDirPath }) {
7043
- if (!global || !getHermesagentHome()) return relativeDirPath;
7702
+ if (!global) return relativeDirPath;
7044
7703
  const relativePath = (0, node_path.relative)(HERMESAGENT_GLOBAL_DIR, relativeDirPath);
7045
- if (relativePath === ".." || relativePath.startsWith(`..${node_path.sep}`) || (0, node_path.isAbsolute)(relativePath)) throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7046
- return relativePath;
7704
+ try {
7705
+ checkPathTraversal({
7706
+ relativePath: relativeDirPath,
7707
+ intendedRootDir: "."
7708
+ });
7709
+ checkPathTraversal({
7710
+ relativePath,
7711
+ intendedRootDir: HERMESAGENT_GLOBAL_DIR
7712
+ });
7713
+ } catch {
7714
+ throw new Error(`Hermes Agent global path must be within ${HERMESAGENT_GLOBAL_DIR}: ${relativeDirPath}`);
7715
+ }
7716
+ return getHermesagentHome() ? relativePath || "." : (0, node_path.join)(getHermesagentGlobalDir(), relativePath);
7047
7717
  }
7048
7718
  function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7049
7719
  return (0, node_path.join)(getHermesagentRelativeDirPath({
@@ -7051,8 +7721,53 @@ function getHermesagentRelativeFilePath({ global, relativeFilePath }) {
7051
7721
  relativeDirPath: (0, node_path.dirname)(relativeFilePath)
7052
7722
  }), (0, node_path.basename)(relativeFilePath));
7053
7723
  }
7724
+ /**
7725
+ * Every spelling `config.yaml` can take in global scope, so that the
7726
+ * shared-write derivation and the gateway ownership table it is checked against
7727
+ * see the same set of keys on every platform and with or without `HERMES_HOME`.
7728
+ *
7729
+ * `getHermesagentRelativeDirPath` resolves exactly one of these per process,
7730
+ * which would otherwise make the derived shared-file key depend on the ambient
7731
+ * environment — the drift guards would then go blind in precisely the
7732
+ * configuration this feature exists for.
7733
+ */
7734
+ function getHermesagentSharedConfigWritePaths() {
7735
+ return [
7736
+ {
7737
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
7738
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7739
+ },
7740
+ {
7741
+ relativeDirPath: HERMESAGENT_GLOBAL_WIN32_DIR,
7742
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7743
+ },
7744
+ {
7745
+ relativeDirPath: ".",
7746
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7747
+ }
7748
+ ];
7749
+ }
7750
+ /**
7751
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.yaml` this scope actually
7752
+ * writes. All three spellings carry the same declaration, but passing the key of
7753
+ * the file being written keeps the write path and the drift guards reading the
7754
+ * same entry.
7755
+ */
7756
+ function getHermesagentConfigSharedFileKey({ global }) {
7757
+ return sharedConfigFileKey({
7758
+ relativeDirPath: getHermesagentRelativeDirPath({
7759
+ global,
7760
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR
7761
+ }),
7762
+ relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
7763
+ });
7764
+ }
7054
7765
  function getHermesagentRulesyncOutputRoot({ nativeOutputRoot, global }) {
7055
- return global && getHermesagentHome() ? getHomeDirectory() : nativeOutputRoot;
7766
+ return getToolRulesyncOutputRoot({
7767
+ nativeOutputRoot,
7768
+ global,
7769
+ toolHome: getHermesagentHome
7770
+ });
7056
7771
  }
7057
7772
  //#endregion
7058
7773
  //#region src/constants/agentsmd-paths.ts
@@ -7284,6 +7999,7 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
7284
7999
  const ANTIGRAVITY_DIR = ".agents";
7285
8000
  const ANTIGRAVITY_SKILLS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "skills");
7286
8001
  const ANTIGRAVITY_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "workflows");
8002
+ const ANTIGRAVITY_AGENTS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "agents");
7287
8003
  const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
7288
8004
  const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
7289
8005
  const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
@@ -7294,6 +8010,7 @@ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
7294
8010
  const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
7295
8011
  const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
7296
8012
  const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
8013
+ const ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH, "agents");
7297
8014
  //#endregion
7298
8015
  //#region src/constants/antigravity-cli-paths.ts
7299
8016
  const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
@@ -7852,6 +8569,7 @@ const CLAUDECODE_PLUGIN_HOOKS_DIR = "hooks";
7852
8569
  const CLAUDECODE_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7853
8570
  const ANTIGRAVITY_PLUGIN_RULES_DIR = "rules";
7854
8571
  const ANTIGRAVITY_PLUGIN_SKILLS_DIR = "skills";
8572
+ const ANTIGRAVITY_PLUGIN_AGENTS_DIR = "agents";
7855
8573
  const ANTIGRAVITY_PLUGIN_MCP_FILE_NAME = "mcp_config.json";
7856
8574
  const ANTIGRAVITY_PLUGIN_HOOKS_FILE_NAME = "hooks.json";
7857
8575
  //#endregion
@@ -8076,6 +8794,7 @@ const COPILOT_SKILLS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "skills");
8076
8794
  const COPILOT_AGENTS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "agents");
8077
8795
  const COPILOT_HOOKS_DIR_PATH = (0, node_path.join)(GITHUB_DIR, "hooks");
8078
8796
  const COPILOT_HOOKS_FILE_NAME = "copilot-hooks.json";
8797
+ const COPILOT_GLOBAL_HOOKS_FILE_NAME = "copilot-ide-hooks.json";
8079
8798
  const COPILOT_MCP_DIR = ".vscode";
8080
8799
  const COPILOT_MCP_FILE_NAME = "mcp.json";
8081
8800
  const COPILOT_VSCODE_SETTINGS_FILE_NAME = "settings.json";
@@ -8083,6 +8802,7 @@ const COPILOTCLI_MCP_FILE_NAME = "mcp-config.json";
8083
8802
  const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
8084
8803
  const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
8085
8804
  const COPILOTCLI_HOOKS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "hooks");
8805
+ const COPILOT_GLOBAL_HOOKS_DIR_PATH = COPILOTCLI_HOOKS_DIR_PATH;
8086
8806
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
8087
8807
  const COPILOT_SKILLS_GLOBAL_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "skills");
8088
8808
  //#endregion
@@ -8206,17 +8926,6 @@ var CopilotCommand = class CopilotCommand extends ToolCommand {
8206
8926
  }
8207
8927
  };
8208
8928
  //#endregion
8209
- //#region src/constants/cursor-paths.ts
8210
- const CURSOR_DIR = ".cursor";
8211
- const CURSOR_COMMANDS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "commands");
8212
- const CURSOR_SKILLS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "skills");
8213
- const CURSOR_AGENTS_DIR_PATH = (0, node_path.join)(CURSOR_DIR, "agents");
8214
- const CURSOR_MCP_FILE_NAME = "mcp.json";
8215
- const CURSOR_HOOKS_FILE_NAME = "hooks.json";
8216
- const CURSOR_IGNORE_FILE_NAME = ".cursorignore";
8217
- const CURSOR_PERMISSIONS_FILE_NAME = "cli.json";
8218
- const CURSOR_PERMISSIONS_GLOBAL_FILE_NAME = "cli-config.json";
8219
- //#endregion
8220
8929
  //#region src/features/commands/cursor-command.ts
8221
8930
  const CursorCommandFrontmatterSchema = zod_mini.z.looseObject({
8222
8931
  description: zod_mini.z.optional(zod_mini.z.string()),
@@ -8762,6 +9471,223 @@ var GooseCommand = class GooseCommand extends ToolCommand {
8762
9471
  }
8763
9472
  };
8764
9473
  //#endregion
9474
+ //#region src/constants/grokcli-paths.ts
9475
+ /**
9476
+ * Grok Build CLI (xAI) configuration-layout conventions.
9477
+ *
9478
+ * Single source of truth for where Grok Build expects its files. Grok Build
9479
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9480
+ * with project/global scopes resolved by the directory the CLI runs in
9481
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9482
+ *
9483
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9484
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9485
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9486
+ * @see https://docs.x.ai/build/overview
9487
+ */
9488
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9489
+ const GROKCLI_DIR = ".grok";
9490
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9491
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9492
+ /**
9493
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9494
+ * permission mode, and other settings all live here; permissions reuse the same
9495
+ * file name as MCP since Grok consolidates everything into one config.
9496
+ */
9497
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9498
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9499
+ const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
9500
+ /**
9501
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9502
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9503
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9504
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9505
+ * @see https://docs.x.ai/build/features/hooks
9506
+ */
9507
+ const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
9508
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9509
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9510
+ /**
9511
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9512
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9513
+ * (global), each a Markdown file with YAML frontmatter (verified via
9514
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9515
+ */
9516
+ const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
9517
+ /**
9518
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9519
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9520
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9521
+ * MCP/skills/subagents adapters).
9522
+ */
9523
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9524
+ /**
9525
+ * Custom slash commands directory. Grok's `find_command_paths` scans
9526
+ * `commands/*.md` under every discovered config dir — `.grok/commands/`
9527
+ * (project, walked from cwd up to the git root) and `~/.grok/commands/`
9528
+ * (global). The scan is **flat and non-recursive**, so subdirectory
9529
+ * namespacing (`git/commit.md` → `/git:commit`) is not supported the way it is
9530
+ * for Claude Code.
9531
+ *
9532
+ * Skills are collected before commands and win name collisions, so a
9533
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9534
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9535
+ */
9536
+ const GROKCLI_COMMANDS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "commands");
9537
+ /**
9538
+ * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
9539
+ * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
9540
+ * walks, and `~/.grok/rules/` in the home scope.
9541
+ * @see https://docs.x.ai/build/overview
9542
+ */
9543
+ const GROKCLI_RULES_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "rules");
9544
+ //#endregion
9545
+ //#region src/features/commands/grokcli-command.ts
9546
+ /**
9547
+ * Grok CLI custom slash commands are Markdown files under `.grok/commands/`
9548
+ * (project) / `~/.grok/commands/` (global), discovered by the same
9549
+ * Claude-Code-compatible frontmatter parser Grok uses for skills.
9550
+ *
9551
+ * Two upstream constraints shape this adapter:
9552
+ *
9553
+ * - The scan is **flat and non-recursive**, so nested namespacing is not
9554
+ * modelled (`supportsSubdirectory: false` flattens nested rulesync commands
9555
+ * onto their basename).
9556
+ * - Skills are collected before commands and win name collisions, so a
9557
+ * `.grok/skills/<name>/` shadows `.grok/commands/<name>.md`.
9558
+ *
9559
+ * `description` and `argument-hint` describe the command, while
9560
+ * `user-invocable` (default true) and `disable-model-invocation` (default
9561
+ * false) control who may invoke it — the same pair `GrokcliSkill` emits.
9562
+ * @see https://docs.x.ai/build/features/skills-plugins-marketplaces
9563
+ */
9564
+ const GrokcliCommandFrontmatterSchema = zod_mini.z.looseObject({
9565
+ description: zod_mini.z.optional(zod_mini.z.string()),
9566
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
9567
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9568
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
9569
+ });
9570
+ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9571
+ frontmatter;
9572
+ body;
9573
+ constructor({ frontmatter, body, ...rest }) {
9574
+ if (rest.validate) {
9575
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9576
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
9577
+ }
9578
+ super({
9579
+ ...rest,
9580
+ fileContent: stringifyFrontmatter(body, frontmatter)
9581
+ });
9582
+ this.frontmatter = frontmatter;
9583
+ this.body = body;
9584
+ }
9585
+ static getSettablePaths(_options = {}) {
9586
+ return { relativeDirPath: GROKCLI_COMMANDS_DIR_PATH };
9587
+ }
9588
+ getBody() {
9589
+ return this.body;
9590
+ }
9591
+ getFrontmatter() {
9592
+ return this.frontmatter;
9593
+ }
9594
+ toRulesyncCommand() {
9595
+ const { description, ...restFields } = this.frontmatter;
9596
+ const rulesyncFrontmatter = {
9597
+ targets: ["*"],
9598
+ description,
9599
+ ...Object.keys(restFields).length > 0 && { grokcli: restFields }
9600
+ };
9601
+ return new RulesyncCommand({
9602
+ outputRoot: ".",
9603
+ frontmatter: rulesyncFrontmatter,
9604
+ body: this.body,
9605
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
9606
+ relativeFilePath: this.relativeFilePath,
9607
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
9608
+ validate: true
9609
+ });
9610
+ }
9611
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
9612
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
9613
+ const grokcliFields = rulesyncFrontmatter.grokcli ?? {};
9614
+ const grokcliFrontmatter = {
9615
+ description: rulesyncFrontmatter.description,
9616
+ ...grokcliFields
9617
+ };
9618
+ const paths = this.getSettablePaths({ global });
9619
+ return new GrokcliCommand({
9620
+ outputRoot,
9621
+ frontmatter: grokcliFrontmatter,
9622
+ body: rulesyncCommand.getBody(),
9623
+ relativeDirPath: paths.relativeDirPath,
9624
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
9625
+ validate
9626
+ });
9627
+ }
9628
+ validate() {
9629
+ if (!this.frontmatter) return {
9630
+ success: true,
9631
+ error: null
9632
+ };
9633
+ const result = GrokcliCommandFrontmatterSchema.safeParse(this.frontmatter);
9634
+ if (result.success) return {
9635
+ success: true,
9636
+ error: null
9637
+ };
9638
+ return {
9639
+ success: false,
9640
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
9641
+ };
9642
+ }
9643
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
9644
+ return this.isTargetedByRulesyncCommandDefault({
9645
+ rulesyncCommand,
9646
+ toolTarget: "grokcli"
9647
+ });
9648
+ }
9649
+ /**
9650
+ * Warn when a rulesync skill would shadow a rulesync command.
9651
+ *
9652
+ * Grok collects skills before commands and lets skills win name collisions,
9653
+ * so `.grok/skills/<name>/` makes `.grok/commands/<name>.md` unreachable.
9654
+ * Both files are still written correctly — nothing is overwritten and no
9655
+ * output is lost — so this warns rather than failing the run the way the
9656
+ * Hermes check does, where the two surfaces really do write the same path.
9657
+ */
9658
+ static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9659
+ const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
9660
+ if (commandNames.size === 0) return;
9661
+ const shadowed = (await findFilesByGlobs((0, node_path.join)((0, node_path.join)(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
9662
+ if (shadowed.length > 0) logger.warn(`Grok CLI resolves skills before commands, so these skills shadow the same-named commands, which will never be reachable: ${[...new Set(shadowed)].toSorted().join(", ")}. Rename either side to make both invocable.`);
9663
+ }
9664
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
9665
+ const paths = this.getSettablePaths({ global });
9666
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
9667
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
9668
+ const result = GrokcliCommandFrontmatterSchema.safeParse(frontmatter);
9669
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
9670
+ return new GrokcliCommand({
9671
+ outputRoot,
9672
+ relativeDirPath: paths.relativeDirPath,
9673
+ relativeFilePath,
9674
+ frontmatter: result.data,
9675
+ body: content.trim(),
9676
+ validate
9677
+ });
9678
+ }
9679
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9680
+ return new GrokcliCommand({
9681
+ outputRoot,
9682
+ relativeDirPath,
9683
+ relativeFilePath,
9684
+ frontmatter: { description: "" },
9685
+ body: "",
9686
+ validate: false
9687
+ });
9688
+ }
9689
+ };
9690
+ //#endregion
8765
9691
  //#region src/features/skills/tool-skill.ts
8766
9692
  /** Ordered skill directory roots: primary first. */
8767
9693
  function toolSkillSearchRoots(paths) {
@@ -9396,7 +10322,7 @@ def register(ctx):
9396
10322
  _register_command(ctx, command)
9397
10323
  `;
9398
10324
  }
9399
- function getEnabledPluginConfigContent$1(currentContent) {
10325
+ function getEnabledPluginConfigContent$1({ currentContent, global }) {
9400
10326
  const config = parseSharedConfig({
9401
10327
  format: "yaml",
9402
10328
  fileContent: currentContent
@@ -9404,7 +10330,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9404
10330
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9405
10331
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9406
10332
  return applySharedConfigPatch({
9407
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10333
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9408
10334
  feature: "commands",
9409
10335
  existingContent: currentContent,
9410
10336
  patch: { plugins: {
@@ -9413,7 +10339,7 @@ function getEnabledPluginConfigContent$1(currentContent) {
9413
10339
  } }
9414
10340
  });
9415
10341
  }
9416
- function getDisabledHermesCommandsPluginConfigContent(currentContent) {
10342
+ function getDisabledHermesCommandsPluginConfigContent({ currentContent, global }) {
9417
10343
  const config = parseSharedConfig({
9418
10344
  format: "yaml",
9419
10345
  fileContent: currentContent
@@ -9421,7 +10347,7 @@ function getDisabledHermesCommandsPluginConfigContent(currentContent) {
9421
10347
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
9422
10348
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
9423
10349
  return applySharedConfigPatch({
9424
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
10350
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
9425
10351
  feature: "commands",
9426
10352
  existingContent: currentContent,
9427
10353
  patch: { plugins: {
@@ -9437,35 +10363,36 @@ var HermesagentCommandAuxiliaryFile = class extends ToolFile {
9437
10363
  error: null
9438
10364
  };
9439
10365
  }
9440
- shouldMergeExistingFileContent() {
10366
+ /**
10367
+ * Whether this auxiliary file is the one at `relativeFilePath`, comparing
10368
+ * against the scope-resolved location of that canonical `.hermes/...` path.
10369
+ */
10370
+ matchesPath(relativeFilePath) {
9441
10371
  return this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9442
10372
  global: this.global,
9443
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
10373
+ relativeFilePath
9444
10374
  }));
9445
10375
  }
10376
+ shouldMergeExistingFileContent() {
10377
+ return this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH);
10378
+ }
9446
10379
  setFileContent(newFileContent) {
9447
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9448
- global: this.global,
9449
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9450
- }))) {
9451
- super.setFileContent(getEnabledPluginConfigContent$1(newFileContent));
10380
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) {
10381
+ super.setFileContent(getEnabledPluginConfigContent$1({
10382
+ currentContent: newFileContent,
10383
+ global: this.global
10384
+ }));
9452
10385
  return;
9453
10386
  }
9454
10387
  super.setFileContent(newFileContent);
9455
10388
  }
9456
10389
  getFileContent() {
9457
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9458
- global: this.global,
9459
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH
9460
- }))) return getPluginManifestContent$2();
9461
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9462
- global: this.global,
9463
- relativeFilePath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH
9464
- }))) return getPluginInitContent$2();
9465
- if (this.getRelativePathFromCwd() === toPosixPath(getHermesagentRelativeFilePath({
9466
- global: this.global,
9467
- relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
9468
- }))) return getEnabledPluginConfigContent$1(super.getFileContent());
10390
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent$2();
10391
+ if (this.matchesPath(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH)) return getPluginInitContent$2();
10392
+ if (this.matchesPath(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent$1({
10393
+ currentContent: super.getFileContent(),
10394
+ global: this.global
10395
+ });
9469
10396
  return super.getFileContent();
9470
10397
  }
9471
10398
  };
@@ -9482,14 +10409,12 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9482
10409
  relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_DIR_PATH
9483
10410
  }) };
9484
10411
  }
9485
- static getExtraSharedWritePaths({ global = false } = {}) {
9486
- return [{
9487
- relativeDirPath: getHermesagentRelativeDirPath({
9488
- global,
9489
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
9490
- }),
9491
- relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
9492
- }];
10412
+ /**
10413
+ * `config.yaml` under every spelling the global profile root can take.
10414
+ * @see getHermesagentSharedConfigWritePaths
10415
+ */
10416
+ static getExtraSharedWritePaths() {
10417
+ return getHermesagentSharedConfigWritePaths();
9493
10418
  }
9494
10419
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands }) {
9495
10420
  const commandSlugs = /* @__PURE__ */ new Set();
@@ -9514,33 +10439,28 @@ var HermesagentCommand = class HermesagentCommand extends ToolCommand {
9514
10439
  }
9515
10440
  static async getAuxiliaryFiles({ toolCommands, outputRoot, global = false, forDeletion = false }) {
9516
10441
  if (toolCommands.length === 0 && !forDeletion) return [];
10442
+ const pluginDirPath = getHermesagentRelativeDirPath({
10443
+ global,
10444
+ relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
10445
+ });
9517
10446
  const pluginFiles = [
9518
10447
  new HermesagentCommandAuxiliaryFile({
9519
10448
  outputRoot,
9520
- relativeDirPath: getHermesagentRelativeDirPath({
9521
- global,
9522
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9523
- }),
10449
+ relativeDirPath: pluginDirPath,
9524
10450
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_MANIFEST_PATH),
9525
10451
  fileContent: "",
9526
10452
  global
9527
10453
  }),
9528
10454
  new HermesagentCommandAuxiliaryFile({
9529
10455
  outputRoot,
9530
- relativeDirPath: getHermesagentRelativeDirPath({
9531
- global,
9532
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9533
- }),
10456
+ relativeDirPath: pluginDirPath,
9534
10457
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_OWNERSHIP_PATH),
9535
10458
  fileContent: "Generated and owned by RuleSync.\n",
9536
10459
  global
9537
10460
  }),
9538
10461
  new HermesagentCommandAuxiliaryFile({
9539
10462
  outputRoot,
9540
- relativeDirPath: getHermesagentRelativeDirPath({
9541
- global,
9542
- relativeDirPath: HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_DIR_PATH
9543
- }),
10463
+ relativeDirPath: pluginDirPath,
9544
10464
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_COMMANDS_PLUGIN_INIT_PATH),
9545
10465
  fileContent: "",
9546
10466
  global
@@ -10775,19 +11695,6 @@ var RooCommand = class RooCommand extends ToolCommand {
10775
11695
  }
10776
11696
  };
10777
11697
  //#endregion
10778
- //#region src/constants/rovodev-paths.ts
10779
- const ROVODEV_DIR = ".rovodev";
10780
- const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
10781
- const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
10782
- const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
10783
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
10784
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
10785
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
10786
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
10787
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
10788
- const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
10789
- const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
10790
- //#endregion
10791
11698
  //#region src/features/commands/rovodev-command.ts
10792
11699
  /**
10793
11700
  * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
@@ -11371,6 +12278,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
11371
12278
  supportsSubdirectory: false
11372
12279
  }
11373
12280
  }],
12281
+ ["grokcli", {
12282
+ class: GrokcliCommand,
12283
+ meta: {
12284
+ extension: "md",
12285
+ supportsProject: true,
12286
+ supportsGlobal: true,
12287
+ isSimulated: false,
12288
+ supportsSubdirectory: false
12289
+ }
12290
+ }],
11374
12291
  ["hermesagent", {
11375
12292
  class: HermesagentCommand,
11376
12293
  meta: {
@@ -11564,7 +12481,8 @@ var CommandsProcessor = class extends FeatureProcessor {
11564
12481
  const factory = this.getFactory(this.toolTarget);
11565
12482
  await factory.class.validateRulesyncCommands?.({
11566
12483
  inputRoot: this.inputRoot,
11567
- rulesyncCommands
12484
+ rulesyncCommands,
12485
+ logger: this.logger
11568
12486
  });
11569
12487
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
11570
12488
  const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
@@ -11702,7 +12620,10 @@ var CommandsProcessor = class extends FeatureProcessor {
11702
12620
  }));
11703
12621
  const currentContent = await readFileContentOrNull(configPath);
11704
12622
  if (currentContent === null) return changedCount;
11705
- const nextContent = getDisabledHermesCommandsPluginConfigContent(currentContent);
12623
+ const nextContent = getDisabledHermesCommandsPluginConfigContent({
12624
+ currentContent,
12625
+ global: this.global
12626
+ });
11706
12627
  if (nextContent === currentContent) return changedCount;
11707
12628
  if (this.dryRun) this.logger.info(`[DRY RUN] Would write: ${configPath}`);
11708
12629
  else await writeFileContent(configPath, nextContent);
@@ -11985,6 +12906,13 @@ function groupDefinitionsByMatcher(definitions) {
11985
12906
  }
11986
12907
  return byMatcher;
11987
12908
  }
12909
+ /** `$CLAUDE_PROJECT_DIR` -> `${CLAUDE_PROJECT_DIR}`, the form the tool substitutes. */
12910
+ function bracePlaceholder(projectDirVar) {
12911
+ return `\${${projectDirVar.replace(/^\$/, "")}}`;
12912
+ }
12913
+ function stripSurroundingQuotes(value) {
12914
+ return value.replace(/^(["'])(.*)\1$/, "$2").replace(/^["']/, "");
12915
+ }
11988
12916
  /**
11989
12917
  * Apply the optional project directory variable prefix to a command string.
11990
12918
  */
@@ -11994,8 +12922,10 @@ function applyCommandPrefix({ def, converterConfig }) {
11994
12922
  const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
11995
12923
  const isDotRelativeCommand = unquotedCommand?.startsWith(".") ?? false;
11996
12924
  const isAbsoluteCommand = typeof unquotedCommand === "string" && (node_path.posix.isAbsolute(unquotedCommand) || node_path.win32.isAbsolute(unquotedCommand) || unquotedCommand.startsWith("~/"));
12925
+ const isExecForm = (converterConfig.arrayPassthroughFields?.some(({ canonical }) => canonical === "args") ?? false) && Array.isArray(def.args);
11997
12926
  if (!(converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && !isAbsoluteCommand && (!converterConfig.prefixDotRelativeCommandsOnly || isDotRelativeCommand)) || typeof trimmedCommand !== "string") return def.command;
11998
12927
  const relativeCommand = trimmedCommand.replace(/^(["'])\.\//, "$1").replace(/^\.\//, "");
12928
+ if (isExecForm) return `${bracePlaceholder(converterConfig.projectDirVar)}/${stripSurroundingQuotes(relativeCommand)}`;
11999
12929
  return `"${converterConfig.projectDirVar}"/${relativeCommand}`;
12000
12930
  }
12001
12931
  /**
@@ -12003,8 +12933,11 @@ function applyCommandPrefix({ def, converterConfig }) {
12003
12933
  * canonical field name to its (possibly renamed) tool field name. Only boolean
12004
12934
  * values are carried through.
12005
12935
  */
12006
- function emitBooleanPassthroughFields({ def, converterConfig }) {
12007
- return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "boolean").map(({ canonical, tool }) => [tool, def[canonical]]));
12936
+ function emitBooleanPassthroughFields({ def, hookType, converterConfig }) {
12937
+ return Object.fromEntries((converterConfig.booleanPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12938
+ if (commandOnly === true && hookType !== "command") return false;
12939
+ return typeof def[canonical] === "boolean";
12940
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12008
12941
  }
12009
12942
  /**
12010
12943
  * Import the configured boolean passthrough fields back into canonical fields,
@@ -12018,8 +12951,11 @@ function importBooleanPassthroughFields({ h, converterConfig }) {
12018
12951
  * canonical field name to its (possibly renamed) tool field name. Only non-empty
12019
12952
  * string values are carried through.
12020
12953
  */
12021
- function emitStringPassthroughFields({ def, converterConfig }) {
12022
- return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical }) => typeof def[canonical] === "string" && def[canonical] !== "").map(({ canonical, tool }) => [tool, def[canonical]]));
12954
+ function emitStringPassthroughFields({ def, hookType, converterConfig }) {
12955
+ return Object.fromEntries((converterConfig.stringPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12956
+ if (commandOnly === true && hookType !== "command") return false;
12957
+ return typeof def[canonical] === "string" && def[canonical] !== "";
12958
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12023
12959
  }
12024
12960
  /**
12025
12961
  * Import the configured string passthrough fields back into canonical fields,
@@ -12032,8 +12968,11 @@ function importStringPassthroughFields({ h, converterConfig }) {
12032
12968
  /**
12033
12969
  * Emit the configured string-array passthrough fields on the tool side.
12034
12970
  */
12035
- function emitArrayPassthroughFields({ def, converterConfig }) {
12036
- return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical }) => isStringArray(def[canonical])).map(({ canonical, tool }) => [tool, def[canonical]]));
12971
+ function emitArrayPassthroughFields({ def, hookType, converterConfig }) {
12972
+ return Object.fromEntries((converterConfig.arrayPassthroughFields ?? []).filter(({ canonical, commandOnly }) => {
12973
+ if (commandOnly === true && hookType !== "command") return false;
12974
+ return isStringArray(def[canonical]);
12975
+ }).map(({ canonical, tool }) => [tool, def[canonical]]));
12037
12976
  }
12038
12977
  /**
12039
12978
  * Import the configured string-array passthrough fields, reversing
@@ -12110,14 +13049,17 @@ function buildToolHooks({ defs, converterConfig }) {
12110
13049
  hooks.push({
12111
13050
  ...emitBooleanPassthroughFields({
12112
13051
  def,
13052
+ hookType,
12113
13053
  converterConfig
12114
13054
  }),
12115
13055
  ...emitStringPassthroughFields({
12116
13056
  def,
13057
+ hookType,
12117
13058
  converterConfig
12118
13059
  }),
12119
13060
  ...emitArrayPassthroughFields({
12120
13061
  def,
13062
+ hookType,
12121
13063
  converterConfig
12122
13064
  }),
12123
13065
  type: hookType,
@@ -12201,6 +13143,8 @@ function stripCommandPrefix({ command, converterConfig }) {
12201
13143
  if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
12202
13144
  const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
12203
13145
  if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
13146
+ const bracedPrefix = `${bracePlaceholder(converterConfig.projectDirVar)}/`;
13147
+ if (cmd.startsWith(bracedPrefix)) return `./${cmd.slice(bracedPrefix.length)}`;
12204
13148
  if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
12205
13149
  const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12206
13150
  return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
@@ -12748,7 +13692,10 @@ const CLAUDE_CONVERTER_CONFIG = {
12748
13692
  "taskCreated",
12749
13693
  "taskCompleted",
12750
13694
  "teammateIdle",
12751
- "cwdChanged"
13695
+ "cwdChanged",
13696
+ "beforeSubmitPrompt",
13697
+ "stop",
13698
+ "directoryAdded"
12752
13699
  ]),
12753
13700
  supportedHookTypes: /* @__PURE__ */ new Set([
12754
13701
  "command",
@@ -12758,9 +13705,45 @@ const CLAUDE_CONVERTER_CONFIG = {
12758
13705
  "agent"
12759
13706
  ]),
12760
13707
  emitsPromptModel: true,
12761
- stringPassthroughFields: [{
12762
- canonical: "if",
12763
- tool: "if"
13708
+ stringPassthroughFields: [
13709
+ {
13710
+ canonical: "if",
13711
+ tool: "if"
13712
+ },
13713
+ {
13714
+ canonical: "statusMessage",
13715
+ tool: "statusMessage"
13716
+ },
13717
+ {
13718
+ canonical: "shell",
13719
+ tool: "shell",
13720
+ commandOnly: true
13721
+ }
13722
+ ],
13723
+ booleanPassthroughFields: [
13724
+ {
13725
+ canonical: "once",
13726
+ tool: "once"
13727
+ },
13728
+ {
13729
+ canonical: "async",
13730
+ tool: "async",
13731
+ commandOnly: true
13732
+ },
13733
+ {
13734
+ canonical: "asyncRewake",
13735
+ tool: "asyncRewake",
13736
+ commandOnly: true
13737
+ },
13738
+ {
13739
+ canonical: "continueOnBlock",
13740
+ tool: "continueOnBlock"
13741
+ }
13742
+ ],
13743
+ arrayPassthroughFields: [{
13744
+ canonical: "args",
13745
+ tool: "args",
13746
+ commandOnly: true
12764
13747
  }]
12765
13748
  };
12766
13749
  var ClaudecodeHooks = class extends ToolHooks {
@@ -12868,7 +13851,14 @@ const CODEXCLI_CONVERTER_CONFIG = {
12868
13851
  toolToCanonicalEventNames: CODEXCLI_TO_CANONICAL_EVENT_NAMES,
12869
13852
  projectDirVar: "",
12870
13853
  supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
12871
- passthroughFields: ["name", "description"]
13854
+ passthroughFields: ["name", "description"],
13855
+ stringPassthroughFields: [{
13856
+ canonical: "commandWindows",
13857
+ tool: "commandWindows"
13858
+ }, {
13859
+ canonical: "statusMessage",
13860
+ tool: "statusMessage"
13861
+ }]
12872
13862
  };
12873
13863
  /**
12874
13864
  * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
@@ -13116,7 +14106,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13116
14106
  fileContent: params.fileContent ?? "{}"
13117
14107
  });
13118
14108
  }
13119
- static getSettablePaths(_options = {}) {
14109
+ static getSettablePaths({ global = false } = {}) {
14110
+ if (global) return {
14111
+ relativeDirPath: COPILOT_GLOBAL_HOOKS_DIR_PATH,
14112
+ relativeFilePath: COPILOT_GLOBAL_HOOKS_FILE_NAME
14113
+ };
13120
14114
  return {
13121
14115
  relativeDirPath: COPILOT_HOOKS_DIR_PATH,
13122
14116
  relativeFilePath: COPILOT_HOOKS_FILE_NAME
@@ -13130,11 +14124,12 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13130
14124
  relativeDirPath: paths.relativeDirPath,
13131
14125
  relativeFilePath: paths.relativeFilePath,
13132
14126
  fileContent,
13133
- validate
14127
+ validate,
14128
+ global
13134
14129
  });
13135
14130
  }
13136
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true }) {
13137
- const paths = CopilotHooks.getSettablePaths();
14131
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
14132
+ const paths = CopilotHooks.getSettablePaths({ global });
13138
14133
  const copilotHooks = canonicalToCopilotHooks(rulesyncHooks.getJson());
13139
14134
  const fileContent = JSON.stringify({
13140
14135
  version: 1,
@@ -13145,7 +14140,8 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13145
14140
  relativeDirPath: paths.relativeDirPath,
13146
14141
  relativeFilePath: paths.relativeFilePath,
13147
14142
  fileContent,
13148
- validate
14143
+ validate,
14144
+ global
13149
14145
  });
13150
14146
  }
13151
14147
  toRulesyncHooks(options) {
@@ -13189,10 +14185,16 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13189
14185
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
13190
14186
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
13191
14187
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
13192
- * `notification`, `preMcpToolCall`). Each entry supports three hook types:
14188
+ * `notification`, `userPromptTransformed`, `preMcpToolCall`). Each entry
14189
+ * supports three hook types:
13193
14190
  *
13194
14191
  * - `command` — the `bash` / `powershell` command-field shape with optional
13195
- * `timeoutSec`, plus optional `cwd` / `env`.
14192
+ * `timeoutSec`, plus optional `cwd` / `env`. Upstream also accepts the
14193
+ * portable `command` field (copied to both shells when neither is present)
14194
+ * and `timeout` as an alias for `timeoutSec`; rulesync reads both on import.
14195
+ * On generate the canonical `shell` selector picks `bash` or `powershell`,
14196
+ * and without it the portable `command` field is written — so the generated
14197
+ * file does not depend on the machine rulesync ran on.
13196
14198
  * - `prompt` — a `prompt` string (Copilot CLI only honors prompt hooks on
13197
14199
  * `sessionStart`, so prompt hooks on other events are skipped).
13198
14200
  * - `http` — `url` / `headers` / `allowedEnvVars` with optional `timeoutSec`.
@@ -13218,11 +14220,11 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13218
14220
  * all rulesync-managed Copilot CLI files under the single `~/.copilot/`
13219
14221
  * root and will revisit if the spec later mandates an alternate layout.
13220
14222
  *
13221
- * Hook entries on `preToolUse` / `postToolUse` may carry an optional `matcher`
13222
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`); it is
13223
- * emitted on those events and dropped (with a warning) on any other event,
13224
- * which never honors matchers. See changelog v1.0.36 (2026-04-24) and v1.0.63
13225
- * (2026-06-15). Reference: https://docs.github.com/en/copilot/reference/hooks-reference
14223
+ * Hook entries on the six matcher-aware events (see
14224
+ * {@link COPILOTCLI_MATCHER_EVENTS}) may carry an optional `matcher` regex; it
14225
+ * is emitted on those events and dropped (with a warning) on any other event,
14226
+ * which never honors matchers.
14227
+ * Reference: https://docs.github.com/en/copilot/reference/hooks-reference
13226
14228
  *
13227
14229
  * The output JSON schema and platform-specific `bash` / `powershell` command
13228
14230
  * field selection match `copilot-hooks.ts`, but the event surface diverges (the
@@ -13237,19 +14239,30 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
13237
14239
  * `Edit|Write`) are now honored instead of silently dropped").
13238
14240
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
13239
14241
  */
13240
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
14242
+ const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
14243
+ "preToolUse",
14244
+ "postToolUse",
14245
+ "notification",
14246
+ "permissionRequest",
14247
+ "preCompact",
14248
+ "subagentStart"
14249
+ ]);
14250
+ /** Human-readable list of the matcher-aware events, for the drop warning. */
14251
+ const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
13241
14252
  const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
13242
14253
  type: zod_mini.z._default(zod_mini.z.string(), "command"),
13243
14254
  matcher: zod_mini.z.optional(zod_mini.z.string()),
13244
14255
  bash: zod_mini.z.optional(zod_mini.z.string()),
13245
14256
  powershell: zod_mini.z.optional(zod_mini.z.string()),
14257
+ command: zod_mini.z.optional(zod_mini.z.string()),
13246
14258
  prompt: zod_mini.z.optional(zod_mini.z.string()),
13247
14259
  url: zod_mini.z.optional(zod_mini.z.string()),
13248
14260
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
13249
14261
  allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
13250
14262
  cwd: zod_mini.z.optional(zod_mini.z.string()),
13251
14263
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
13252
- timeoutSec: zod_mini.z.optional(zod_mini.z.number())
14264
+ timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
14265
+ timeout: zod_mini.z.optional(zod_mini.z.number())
13253
14266
  });
13254
14267
  /** Filter the shared config hooks down to events the Copilot CLI supports. */
13255
14268
  function filterSupportedCopilotCliHooks(hooks) {
@@ -13260,21 +14273,21 @@ function filterSupportedCopilotCliHooks(hooks) {
13260
14273
  }
13261
14274
  /**
13262
14275
  * Resolve the `matcher` part for an exported entry. Copilot CLI honors `matcher`
13263
- * only on preToolUse/postToolUse entries; on any other event a matcher would be
13264
- * silently dropped by the CLI, so we drop it here with a warning rather than
13265
- * emitting a dead field.
14276
+ * only on the events listed in {@link COPILOTCLI_MATCHER_EVENTS}; on any other
14277
+ * event a matcher would be silently dropped by the CLI, so we drop it here with
14278
+ * a warning rather than emitting a dead field.
13266
14279
  */
13267
14280
  function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger }) {
13268
14281
  if (matcher === void 0 || matcher === null || matcher === "") return {};
13269
14282
  if (matcherSupported) return { matcher };
13270
- logger?.warn(`Copilot CLI hook matchers are only honored on preToolUse/postToolUse; dropping matcher "${matcher}" on '${eventName}'.`);
14283
+ logger?.warn(`Copilot CLI hook matchers are only honored on ${COPILOTCLI_MATCHER_EVENTS_LABEL}; dropping matcher "${matcher}" on '${eventName}'.`);
13271
14284
  return {};
13272
14285
  }
13273
14286
  /**
13274
14287
  * Build the exported entries for a single canonical event. Returns an empty
13275
14288
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
13276
14289
  */
13277
- function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, commandField, logger }) {
14290
+ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchemaKeys, logger }) {
13278
14291
  const matcherSupported = COPILOTCLI_MATCHER_EVENTS.has(eventName);
13279
14292
  const entries = [];
13280
14293
  for (const def of definitions) {
@@ -13310,22 +14323,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
13310
14323
  ...timeoutPart,
13311
14324
  ...rest
13312
14325
  });
13313
- else if (hookType === "command") entries.push({
13314
- type: "command",
13315
- ...matcherPart,
13316
- ...compact({
13317
- [commandField]: def.command,
13318
- env: def.env
13319
- }),
13320
- ...timeoutPart,
13321
- ...rest
13322
- });
14326
+ else if (hookType === "command") {
14327
+ const commandField = def.shell ?? "command";
14328
+ entries.push({
14329
+ type: "command",
14330
+ ...matcherPart,
14331
+ ...compact({
14332
+ [commandField]: def.command,
14333
+ env: def.env
14334
+ }),
14335
+ ...timeoutPart,
14336
+ ...rest
14337
+ });
14338
+ }
13323
14339
  }
13324
14340
  return entries;
13325
14341
  }
13326
14342
  function canonicalToCopilotCliHooks(config, logger) {
13327
14343
  const canonicalSchemaKeys = Object.keys(HookDefinitionSchema.shape);
13328
- const commandField = process.platform === "win32" ? "powershell" : "bash";
13329
14344
  const effectiveHooks = {
13330
14345
  ...filterSupportedCopilotCliHooks(config.hooks),
13331
14346
  ...config.copilot?.hooks,
@@ -13338,7 +14353,6 @@ function canonicalToCopilotCliHooks(config, logger) {
13338
14353
  eventName,
13339
14354
  definitions,
13340
14355
  canonicalSchemaKeys,
13341
- commandField,
13342
14356
  logger
13343
14357
  });
13344
14358
  if (entries.length > 0) out[copilotEventName] = entries;
@@ -13355,6 +14369,13 @@ function importPassthrough(entry) {
13355
14369
  if (entry.env !== void 0) passthrough.env = entry.env;
13356
14370
  return passthrough;
13357
14371
  }
14372
+ /**
14373
+ * Resolve the canonical command and its `shell` selector from an imported entry.
14374
+ *
14375
+ * A shell-specific field carries its `shell` through so re-export writes the
14376
+ * same field back. An entry using only the portable `command` field leaves
14377
+ * `shell` unset, which re-export renders as the portable field again.
14378
+ */
13358
14379
  function resolveImportCommand(entry, logger) {
13359
14380
  const hasBash = typeof entry.bash === "string";
13360
14381
  const hasPowershell = typeof entry.powershell === "string";
@@ -13363,9 +14384,22 @@ function resolveImportCommand(entry, logger) {
13363
14384
  const chosen = isWindows ? "powershell" : "bash";
13364
14385
  const ignored = isWindows ? "bash" : "powershell";
13365
14386
  logger?.warn(`Copilot CLI hook has both bash and powershell commands; using ${chosen} and ignoring ${ignored} on this platform.`);
13366
- return isWindows ? entry.powershell : entry.bash;
13367
- } else if (hasBash) return entry.bash;
13368
- else if (hasPowershell) return entry.powershell;
14387
+ return isWindows ? {
14388
+ command: entry.powershell,
14389
+ shell: "powershell"
14390
+ } : {
14391
+ command: entry.bash,
14392
+ shell: "bash"
14393
+ };
14394
+ } else if (hasBash) return {
14395
+ command: entry.bash,
14396
+ shell: "bash"
14397
+ };
14398
+ else if (hasPowershell) return {
14399
+ command: entry.powershell,
14400
+ shell: "powershell"
14401
+ };
14402
+ return typeof entry.command === "string" ? { command: entry.command } : {};
13369
14403
  }
13370
14404
  function copilotCliHooksToCanonical(rawHooks, logger) {
13371
14405
  if (rawHooks === null || rawHooks === void 0 || typeof rawHooks !== "object") return {};
@@ -13378,7 +14412,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13378
14412
  const parseResult = CopilotCliHookEntrySchema.safeParse(rawEntry);
13379
14413
  if (!parseResult.success) continue;
13380
14414
  const entry = parseResult.data;
13381
- const timeout = entry.timeoutSec;
14415
+ const timeout = entry.timeoutSec ?? entry.timeout;
13382
14416
  const timeoutPart = timeout !== void 0 ? { timeout } : {};
13383
14417
  const matcherPart = entry.matcher !== void 0 && entry.matcher !== "" ? { matcher: entry.matcher } : {};
13384
14418
  const passthrough = importPassthrough(entry);
@@ -13397,10 +14431,11 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
13397
14431
  ...passthrough
13398
14432
  });
13399
14433
  else {
13400
- const command = resolveImportCommand(entry, logger);
14434
+ const { command, shell } = resolveImportCommand(entry, logger);
13401
14435
  defs.push({
13402
14436
  type: "command",
13403
14437
  ...command !== void 0 && { command },
14438
+ ...shell !== void 0 && { shell },
13404
14439
  ...matcherPart,
13405
14440
  ...timeoutPart,
13406
14441
  ...passthrough
@@ -14050,64 +15085,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
14050
15085
  }
14051
15086
  };
14052
15087
  //#endregion
14053
- //#region src/constants/grokcli-paths.ts
14054
- /**
14055
- * Grok Build CLI (xAI) configuration-layout conventions.
14056
- *
14057
- * Single source of truth for where Grok Build expects its files. Grok Build
14058
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14059
- * with project/global scopes resolved by the directory the CLI runs in
14060
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14061
- *
14062
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14063
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14064
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14065
- * @see https://docs.x.ai/build/overview
14066
- */
14067
- /** Root directory for Grok Build configuration, relative to the scope root. */
14068
- const GROKCLI_DIR = ".grok";
14069
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14070
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14071
- /**
14072
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14073
- * permission mode, and other settings all live here; permissions reuse the same
14074
- * file name as MCP since Grok consolidates everything into one config.
14075
- */
14076
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14077
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14078
- const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
14079
- /**
14080
- * Hooks directory under `.grok/`. Grok Build discovers hook config files from
14081
- * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
14082
- * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
14083
- * shape. rulesync writes all its hooks into a single `rulesync.json`.
14084
- * @see https://docs.x.ai/build/features/hooks
14085
- */
14086
- const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
14087
- /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
14088
- const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
14089
- /**
14090
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14091
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14092
- * (global), each a Markdown file with YAML frontmatter (verified via
14093
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14094
- */
14095
- const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
14096
- /**
14097
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14098
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14099
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14100
- * MCP/skills/subagents adapters).
14101
- */
14102
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14103
- /**
14104
- * Non-root rules directory. Grok scans `*.md` here — flat, sorted by name —
14105
- * alongside the AGENTS.md family: `.grok/rules/` in each project directory it
14106
- * walks, and `~/.grok/rules/` in the home scope.
14107
- * @see https://docs.x.ai/build/overview
14108
- */
14109
- const GROKCLI_RULES_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "rules");
14110
- //#endregion
14111
15088
  //#region src/features/hooks/grokcli-hooks.ts
14112
15089
  const GROKCLI_CONVERTER_CONFIG = {
14113
15090
  supportedEvents: GROKCLI_HOOK_EVENTS,
@@ -14353,6 +15330,13 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14353
15330
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
14354
15331
  };
14355
15332
  }
15333
+ /**
15334
+ * `config.yaml` under every spelling the global profile root can take.
15335
+ * @see getHermesagentSharedConfigWritePaths
15336
+ */
15337
+ static getExtraSharedWritePaths() {
15338
+ return getHermesagentSharedConfigWritePaths();
15339
+ }
14356
15340
  constructor(params) {
14357
15341
  super({
14358
15342
  ...params,
@@ -14390,7 +15374,7 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
14390
15374
  }
14391
15375
  setFileContent(fileContent) {
14392
15376
  this.fileContent = applySharedConfigPatch({
14393
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
15377
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
14394
15378
  feature: "hooks",
14395
15379
  existingContent: fileContent,
14396
15380
  patch: parseSharedConfig({
@@ -14540,7 +15524,23 @@ var JunieHooks = class JunieHooks extends ToolHooks {
14540
15524
  };
14541
15525
  //#endregion
14542
15526
  //#region src/features/hooks/opencode-style-generator.ts
14543
- const NAMED_HOOKS = /* @__PURE__ */ new Set(["tool.execute.before", "tool.execute.after"]);
15527
+ /**
15528
+ * Tool events emitted as named `(input, ...)` hooks rather than through the
15529
+ * generic `event.type` dispatch, mapped to the expression a hook's `matcher`
15530
+ * regex is tested against — or `null` when the hook has no matchable subject,
15531
+ * in which case a matcher is dropped rather than compiled against a field that
15532
+ * does not exist.
15533
+ *
15534
+ * `experimental.session.compacting` receives `(input, output)` and exposes no
15535
+ * per-invocation identifier worth matching on, so it takes `null`.
15536
+ *
15537
+ * @see https://opencode.ai/docs/plugins/
15538
+ */
15539
+ const NAMED_HOOK_MATCHER_SUBJECTS = {
15540
+ "tool.execute.before": "input.tool",
15541
+ "tool.execute.after": "input.tool",
15542
+ "experimental.session.compacting": null
15543
+ };
14544
15544
  function escapeForTemplateLiteral(command) {
14545
15545
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
14546
15546
  }
@@ -14573,7 +15573,7 @@ function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHand
14573
15573
  });
14574
15574
  }
14575
15575
  if (handlers.length > 0) {
14576
- const grouped = NAMED_HOOKS.has(toolEvent) ? namedEventHandlers : genericEventHandlers;
15576
+ const grouped = Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) ? namedEventHandlers : genericEventHandlers;
14577
15577
  const existing = grouped[toolEvent];
14578
15578
  if (existing) existing.push(...handlers);
14579
15579
  else grouped[toolEvent] = handlers;
@@ -14602,14 +15602,15 @@ function buildGenericEventBodyLines(genericEventHandlers) {
14602
15602
  function buildNamedEventBodyLines(namedEventHandlers) {
14603
15603
  const bodyLines = [];
14604
15604
  for (const [eventName, handlers] of Object.entries(namedEventHandlers)) {
15605
+ const matcherSubject = NAMED_HOOK_MATCHER_SUBJECTS[eventName] ?? null;
14605
15606
  bodyLines.push(` "${eventName}": async (input) => {`);
14606
15607
  for (const handler of handlers) {
14607
15608
  const escapedCommand = escapeForTemplateLiteral(handler.command);
14608
- if (handler.matcher) {
15609
+ if (handler.matcher && matcherSubject !== null) {
14609
15610
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
14610
15611
  bodyLines.push(" {");
14611
15612
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
14612
- bodyLines.push(` if (__re.test(input.tool)) {`);
15613
+ bodyLines.push(` if (__re.test(${matcherSubject})) {`);
14613
15614
  bodyLines.push(` await $\`${escapedCommand}\`;`);
14614
15615
  bodyLines.push(" }");
14615
15616
  bodyLines.push(" }");
@@ -14749,8 +15750,38 @@ function getKimiCodeHome() {
14749
15750
  function getKimiCodeRelativeDirPath({ global, relativeDirPath = "." }) {
14750
15751
  return global && getKimiCodeHome() ? relativeDirPath : (0, node_path.join)(KIMI_CODE_DIR, relativeDirPath);
14751
15752
  }
15753
+ /**
15754
+ * Both spellings the shared user `config.toml` can take: under `.kimi-code/`,
15755
+ * or at the root of `KIMI_CODE_HOME` when that override names the profile dir.
15756
+ * Declared unconditionally so the derived shared-file keys — and the drift
15757
+ * guards checked against them — do not depend on the ambient environment.
15758
+ */
15759
+ function getKimiCodeSharedConfigWritePaths() {
15760
+ return [{
15761
+ relativeDirPath: KIMI_CODE_DIR,
15762
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15763
+ }, {
15764
+ relativeDirPath: ".",
15765
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15766
+ }];
15767
+ }
15768
+ /**
15769
+ * The `SHARED_CONFIG_OWNERSHIP` key of the `config.toml` actually being written.
15770
+ * Both spellings carry the same declaration, but passing the key of the file
15771
+ * being written keeps the write path and the drift guards on the same entry.
15772
+ */
15773
+ function getKimiCodeConfigSharedFileKey({ global }) {
15774
+ return sharedConfigFileKey({
15775
+ relativeDirPath: getKimiCodeRelativeDirPath({ global }),
15776
+ relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
15777
+ });
15778
+ }
14752
15779
  function getKimiCodeRulesyncOutputRoot({ nativeOutputRoot, global }) {
14753
- return global && getKimiCodeHome() ? getHomeDirectory() : nativeOutputRoot;
15780
+ return getToolRulesyncOutputRoot({
15781
+ nativeOutputRoot,
15782
+ global,
15783
+ toolHome: getKimiCodeHome
15784
+ });
14754
15785
  }
14755
15786
  //#endregion
14756
15787
  //#region src/features/hooks/kimi-code-hooks.ts
@@ -14848,13 +15879,20 @@ var KimiCodeHooks = class KimiCodeHooks extends ToolHooks {
14848
15879
  isDeletable() {
14849
15880
  return false;
14850
15881
  }
15882
+ /**
15883
+ * `config.toml` under both spellings its directory can take.
15884
+ * @see getKimiCodeSharedConfigWritePaths
15885
+ */
15886
+ static getExtraSharedWritePaths() {
15887
+ return getKimiCodeSharedConfigWritePaths();
15888
+ }
14851
15889
  shouldMergeExistingFileContent() {
14852
15890
  return true;
14853
15891
  }
14854
15892
  setFileContent(fileContent) {
14855
15893
  const paths = KimiCodeHooks.getSettablePaths({ global: this.global });
14856
15894
  this.fileContent = applySharedConfigPatch({
14857
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
15895
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
14858
15896
  feature: "hooks",
14859
15897
  existingContent: fileContent,
14860
15898
  patch: parseSharedConfig({
@@ -16245,7 +17283,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
16245
17283
  class: CopilotHooks,
16246
17284
  meta: {
16247
17285
  supportsProject: true,
16248
- supportsGlobal: false,
17286
+ supportsGlobal: true,
16249
17287
  supportsImport: true
16250
17288
  },
16251
17289
  supportedEvents: COPILOT_HOOK_EVENTS,
@@ -17545,6 +18583,151 @@ var QwencodeIgnore = class QwencodeIgnore extends ToolIgnore {
17545
18583
  }
17546
18584
  };
17547
18585
  //#endregion
18586
+ //#region src/features/shared/reasonix-config-table.ts
18587
+ /**
18588
+ * Shape-narrowing helpers for the Reasonix TOML config (`reasonix.toml` /
18589
+ * `~/.reasonix/config.toml`), shared by the features that read-modify-write it.
18590
+ *
18591
+ * TOML is only structurally validated on parse, so a hand-edited config can
18592
+ * hold any type under `permissions` or inside `allow`/`ask`/`deny`. Both the
18593
+ * `permissions` and `ignore` adapters have to narrow the same two shapes
18594
+ * before merging, so the narrowing lives here once rather than being re-spelled
18595
+ * (and re-diverging) per feature.
18596
+ */
18597
+ /** Keep only the string entries of a TOML array; anything else becomes `[]`. */
18598
+ function toReasonixStringArray(value) {
18599
+ if (!Array.isArray(value)) return [];
18600
+ return value.filter((entry) => typeof entry === "string");
18601
+ }
18602
+ /** Copy a TOML table; a non-table (scalar, array, missing) becomes `{}`. */
18603
+ function toReasonixTable(value) {
18604
+ if (!isPlainObject$1(value)) return {};
18605
+ return { ...value };
18606
+ }
18607
+ //#endregion
18608
+ //#region src/features/ignore/reasonix-ignore.ts
18609
+ const permissionsTableOf = (document) => toReasonixTable(document.permissions);
18610
+ /**
18611
+ * Reshape the parsed TOML document into the `permissions.allow/ask/deny` shape
18612
+ * {@link applyIgnoreReadDenies} operates on. Reasonix's `[permissions]` table
18613
+ * is Claude-Code-shaped (SPEC.md §3.7), so the entry-level ownership rule the
18614
+ * gateway already implements applies verbatim; only the surrounding file
18615
+ * format differs. Sibling keys such as `mode` pass through untouched.
18616
+ */
18617
+ const asClaudeStyleSettings = (document) => {
18618
+ const table = permissionsTableOf(document);
18619
+ return {
18620
+ ...document,
18621
+ permissions: {
18622
+ ...table,
18623
+ allow: toReasonixStringArray(table.allow),
18624
+ ask: toReasonixStringArray(table.ask),
18625
+ deny: toReasonixStringArray(table.deny)
18626
+ }
18627
+ };
18628
+ };
18629
+ /**
18630
+ * Drop a `[permissions]` table that ended up with nothing in it, so an empty
18631
+ * `.rulesyncignore` does not add a bare table header to a file that never had
18632
+ * one.
18633
+ */
18634
+ const withoutEmptyPermissions = (settings) => {
18635
+ const document = { ...settings };
18636
+ const permissions = document.permissions;
18637
+ if (isPlainObject$1(permissions) && Object.keys(permissions).length === 0) delete document.permissions;
18638
+ return document;
18639
+ };
18640
+ /**
18641
+ * Writes `.rulesyncignore` patterns as `Read(<pattern>)` entries in the
18642
+ * `[permissions] deny` table of `reasonix.toml` (project) /
18643
+ * `~/.reasonix/config.toml` (global).
18644
+ *
18645
+ * `deny` is the right target rather than `[sandbox] forbid_read`: deny rules
18646
+ * take glob specifiers (`Edit(docs/**)`) and are "a hard block in every mode",
18647
+ * while `forbid_read` is documented as absolute paths with no glob support.
18648
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
18649
+ */
18650
+ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
18651
+ constructor(params) {
18652
+ super(params);
18653
+ const document = parseSharedConfig({
18654
+ format: "toml",
18655
+ fileContent: this.fileContent
18656
+ });
18657
+ this.patterns = toReasonixStringArray(permissionsTableOf(document).deny);
18658
+ }
18659
+ static getSettablePaths({ global = false } = {}) {
18660
+ return {
18661
+ relativeDirPath: global ? REASONIX_GLOBAL_DIR : ".",
18662
+ relativeFilePath: global ? REASONIX_GLOBAL_PERMISSIONS_FILE_NAME : REASONIX_PROJECT_PERMISSIONS_FILE_NAME
18663
+ };
18664
+ }
18665
+ /**
18666
+ * The config file also carries `[[plugins]]`, `[permissions]` rules from the
18667
+ * permissions feature and user-authored tables, so rulesync must never
18668
+ * delete it.
18669
+ */
18670
+ isDeletable() {
18671
+ return false;
18672
+ }
18673
+ toRulesyncIgnore() {
18674
+ const rulesyncPatterns = this.patterns.filter((pattern) => isReadDenyEntry(pattern)).map((pattern) => pattern.slice(5, -1)).filter((pattern) => pattern.length > 0);
18675
+ return new RulesyncIgnore({
18676
+ outputRoot: this.outputRoot,
18677
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
18678
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
18679
+ fileContent: rulesyncPatterns.join("\n")
18680
+ });
18681
+ }
18682
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
18683
+ const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
18684
+ const paths = this.getSettablePaths({ global });
18685
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18686
+ const existingDocument = parseSharedConfig({
18687
+ format: "toml",
18688
+ fileContent: await readFileContentOrNull(filePath) ?? "",
18689
+ filePath
18690
+ });
18691
+ const document = withoutEmptyPermissions(applyIgnoreReadDenies({
18692
+ settings: asClaudeStyleSettings(existingDocument),
18693
+ readDenies
18694
+ }));
18695
+ return new ReasonixIgnore({
18696
+ outputRoot,
18697
+ relativeDirPath: paths.relativeDirPath,
18698
+ relativeFilePath: paths.relativeFilePath,
18699
+ fileContent: stringifySharedConfig({
18700
+ format: "toml",
18701
+ document
18702
+ }),
18703
+ validate: true,
18704
+ global
18705
+ });
18706
+ }
18707
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18708
+ const paths = this.getSettablePaths({ global });
18709
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
18710
+ return new ReasonixIgnore({
18711
+ outputRoot,
18712
+ relativeDirPath: paths.relativeDirPath,
18713
+ relativeFilePath: paths.relativeFilePath,
18714
+ fileContent,
18715
+ validate,
18716
+ global
18717
+ });
18718
+ }
18719
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
18720
+ return new ReasonixIgnore({
18721
+ outputRoot,
18722
+ relativeDirPath,
18723
+ relativeFilePath,
18724
+ fileContent: "",
18725
+ validate: false,
18726
+ global
18727
+ });
18728
+ }
18729
+ };
18730
+ //#endregion
17548
18731
  //#region src/features/ignore/roo-ignore.ts
17549
18732
  /**
17550
18733
  * RooIgnore represents ignore patterns for the Roo Code AI coding assistant.
@@ -17702,6 +18885,14 @@ const ZED_GLOBAL_WIN32_DIR = (0, node_path.join)("AppData", "Roaming", "Zed");
17702
18885
  function getZedGlobalDir() {
17703
18886
  return process.platform === "win32" ? ZED_GLOBAL_WIN32_DIR : ZED_GLOBAL_DIR;
17704
18887
  }
18888
+ /**
18889
+ * The global config dir of the OTHER platform. `getZedGlobalDir()` resolves one
18890
+ * spelling per platform, but the shared-write derivation (and the gateway
18891
+ * ownership table it is checked against) must know both on every platform.
18892
+ */
18893
+ function getZedOtherPlatformGlobalDir() {
18894
+ return process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR;
18895
+ }
17705
18896
  const ZED_SETTINGS_FILE_NAME = "settings.json";
17706
18897
  const ZED_RULE_FILE_NAME = ".rules";
17707
18898
  const ZED_GLOBAL_RULE_FILE_NAME = "AGENTS.md";
@@ -17714,12 +18905,20 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
17714
18905
  const jsonValue = JSON.parse(this.fileContent);
17715
18906
  this.patterns = jsonValue.private_files ?? [];
17716
18907
  }
17717
- static getSettablePaths() {
18908
+ static getSettablePaths({ global = false } = {}) {
17718
18909
  return {
17719
- relativeDirPath: ZED_DIR,
18910
+ relativeDirPath: global ? getZedGlobalDir() : ZED_DIR,
17720
18911
  relativeFilePath: ZED_SETTINGS_FILE_NAME
17721
18912
  };
17722
18913
  }
18914
+ /** @see getZedOtherPlatformGlobalDir */
18915
+ static getExtraSharedWritePaths({ global = false } = {}) {
18916
+ if (!global) return [];
18917
+ return [{
18918
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
18919
+ relativeFilePath: ZED_SETTINGS_FILE_NAME
18920
+ }];
18921
+ }
17723
18922
  /**
17724
18923
  * ZedIgnore uses settings.json which is a user-managed config file.
17725
18924
  * It should not be deleted by rulesync.
@@ -17730,48 +18929,53 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
17730
18929
  toRulesyncIgnore() {
17731
18930
  const fileContent = this.patterns.filter((pattern) => pattern.length > 0).join("\n");
17732
18931
  return new RulesyncIgnore({
17733
- outputRoot: this.outputRoot,
18932
+ outputRoot: ".",
17734
18933
  relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
17735
18934
  relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
17736
18935
  fileContent
17737
18936
  });
17738
18937
  }
17739
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
18938
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
17740
18939
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
17741
- const filePath = (0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
18940
+ const paths = this.getSettablePaths({ global });
18941
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
17742
18942
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
17743
- const mergedPatterns = (0, es_toolkit.uniq)([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
18943
+ const managedPatterns = patterns.length > 0 ? [...new Set(patterns)].toSorted() : void 0;
17744
18944
  return new ZedIgnore({
17745
18945
  outputRoot,
17746
- relativeDirPath: this.getSettablePaths().relativeDirPath,
17747
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18946
+ relativeDirPath: paths.relativeDirPath,
18947
+ relativeFilePath: paths.relativeFilePath,
17748
18948
  fileContent: applySharedConfigPatch({
17749
- fileKey: sharedConfigFileKey(this.getSettablePaths()),
18949
+ fileKey: sharedConfigFileKey(paths),
17750
18950
  feature: "ignore",
17751
18951
  existingContent: existingFileContent,
17752
- patch: { private_files: mergedPatterns },
18952
+ patch: { private_files: managedPatterns },
17753
18953
  filePath
17754
18954
  }),
17755
- validate: true
18955
+ validate: true,
18956
+ global
17756
18957
  });
17757
18958
  }
17758
- static async fromFile({ outputRoot = process.cwd(), validate = true }) {
17759
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
18959
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18960
+ const paths = this.getSettablePaths({ global });
18961
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
17760
18962
  return new ZedIgnore({
17761
18963
  outputRoot,
17762
- relativeDirPath: this.getSettablePaths().relativeDirPath,
17763
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18964
+ relativeDirPath: paths.relativeDirPath,
18965
+ relativeFilePath: paths.relativeFilePath,
17764
18966
  fileContent,
17765
- validate
18967
+ validate,
18968
+ global
17766
18969
  });
17767
18970
  }
17768
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18971
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
17769
18972
  return new ZedIgnore({
17770
18973
  outputRoot,
17771
18974
  relativeDirPath,
17772
18975
  relativeFilePath,
17773
18976
  fileContent: "{}",
17774
- validate: false
18977
+ validate: false,
18978
+ global
17775
18979
  });
17776
18980
  }
17777
18981
  };
@@ -17793,6 +18997,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
17793
18997
  ["kiro-cli", { class: KiroIgnore }],
17794
18998
  ["kiro-ide", { class: KiroIgnore }],
17795
18999
  ["qwencode", { class: QwencodeIgnore }],
19000
+ ["reasonix", { class: ReasonixIgnore }],
17796
19001
  ["roo", { class: RooIgnore }],
17797
19002
  ["devin", { class: DevinIgnore }],
17798
19003
  ["vibe", { class: VibeIgnore }],
@@ -17803,7 +19008,9 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
17803
19008
  const ignoreProcessorGlobalToolTargets = [
17804
19009
  "kiro",
17805
19010
  "kiro-cli",
17806
- "kiro-ide"
19011
+ "kiro-ide",
19012
+ "reasonix",
19013
+ "zed"
17807
19014
  ];
17808
19015
  const defaultGetFactory$4 = (target) => {
17809
19016
  const factory = toolIgnoreFactories.get(target);
@@ -18722,8 +19929,20 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
18722
19929
  disabledTools: "disabled_tools",
18723
19930
  envVars: "env_vars"
18724
19931
  };
19932
+ const RULESYNC_TO_CODEX_SCALAR_FIELD_MAP = { experimentalEnvironment: "experimental_environment" };
19933
+ const CODEX_TO_RULESYNC_SCALAR_FIELD_MAP = Object.fromEntries(Object.entries(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP).map(([canonical, codex]) => [codex, canonical]));
18725
19934
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
18726
19935
  /**
19936
+ * `env_vars` entries are either a bare variable name or `{ name, source }`,
19937
+ * where `source = "remote"` reads the variable from the remote executor
19938
+ * environment. The other renamed keys (`enabled_tools`, `disabled_tools`) stay
19939
+ * plain string arrays, so the widened check applies to `env_vars` only.
19940
+ * @see https://learn.chatgpt.com/docs/extend/mcp
19941
+ */
19942
+ function isValidRenamedArray(key, value) {
19943
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
19944
+ }
19945
+ /**
18727
19946
  * Translate a server's `oauth` table from the canonical rulesync shape (Claude
18728
19947
  * Code style camelCase) into the shape Codex CLI understands. Codex expects the
18729
19948
  * OAuth client id under snake_case `client_id`; without it `codex mcp login`
@@ -18783,8 +20002,12 @@ function convertFromCodexFormat(codexMcp) {
18783
20002
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
18784
20003
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
18785
20004
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
18786
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
20005
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18787
20006
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
20007
+ } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
20008
+ const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
20009
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
20010
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
18788
20011
  } else converted[key] = value;
18789
20012
  }
18790
20013
  result[name] = converted;
@@ -18806,8 +20029,12 @@ function convertToCodexFormat(mcpServers) {
18806
20029
  } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
18807
20030
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
18808
20031
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
18809
- if (mappedKey) if (isStringArray$1(value)) converted[mappedKey] = value;
20032
+ if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
18810
20033
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
20034
+ } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
20035
+ const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
20036
+ if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
20037
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
18811
20038
  } else converted[key] = value;
18812
20039
  }
18813
20040
  const previousName = originalNames.get(codexName);
@@ -18878,7 +20105,9 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18878
20105
  const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
18879
20106
  return [serverName, {
18880
20107
  ...serverConfig,
18881
- ...isRecord(rawServer) && isStringArray$1(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
20108
+ ...isRecord(rawServer) && isEnvVarEntryArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {},
20109
+ ...isRecord(rawServer) && typeof rawServer.experimental_environment === "string" ? { experimentalEnvironment: rawServer.experimental_environment } : {},
20110
+ ...isRecord(rawServer) && typeof rawServer.experimentalEnvironment === "string" ? { experimentalEnvironment: rawServer.experimentalEnvironment } : {}
18882
20111
  }];
18883
20112
  })));
18884
20113
  const filteredMcpServers = this.removeEmptyEntries(converted);
@@ -18949,9 +20178,6 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
18949
20178
  };
18950
20179
  //#endregion
18951
20180
  //#region src/features/mcp/copilot-mcp.ts
18952
- function convertToCopilotFormat(mcpServers) {
18953
- return { servers: mcpServers };
18954
- }
18955
20181
  function convertFromCopilotFormat(copilotConfig) {
18956
20182
  return copilotConfig.servers ?? {};
18957
20183
  }
@@ -18980,13 +20206,21 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
18980
20206
  validate
18981
20207
  });
18982
20208
  }
18983
- static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
18984
- const copilotConfig = convertToCopilotFormat(rulesyncMcp.getMcpServers());
20209
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true }) {
20210
+ const paths = this.getSettablePaths();
20211
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20212
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
18985
20213
  return new CopilotMcp({
18986
20214
  outputRoot,
18987
- relativeDirPath: this.getSettablePaths().relativeDirPath,
18988
- relativeFilePath: this.getSettablePaths().relativeFilePath,
18989
- fileContent: JSON.stringify(copilotConfig, null, 2),
20215
+ relativeDirPath: paths.relativeDirPath,
20216
+ relativeFilePath: paths.relativeFilePath,
20217
+ fileContent: applySharedConfigPatch({
20218
+ fileKey: sharedConfigFileKey(paths),
20219
+ feature: "mcp",
20220
+ existingContent,
20221
+ patch: { servers: rulesyncMcp.getMcpServers() },
20222
+ filePath
20223
+ }),
18990
20224
  validate
18991
20225
  });
18992
20226
  }
@@ -20372,7 +21606,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20372
21606
  }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
20373
21607
  this.config = merged;
20374
21608
  super.setFileContent(applySharedConfigPatch({
20375
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21609
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
20376
21610
  feature: "mcp",
20377
21611
  existingContent: fileContent,
20378
21612
  patch: { mcp_servers: merged.mcp_servers }
@@ -20390,6 +21624,13 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20390
21624
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
20391
21625
  };
20392
21626
  }
21627
+ /**
21628
+ * `config.yaml` under every spelling the global profile root can take.
21629
+ * @see getHermesagentSharedConfigWritePaths
21630
+ */
21631
+ static getExtraSharedWritePaths() {
21632
+ return getHermesagentSharedConfigWritePaths();
21633
+ }
20393
21634
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20394
21635
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
20395
21636
  const paths = this.getSettablePaths({ global });
@@ -20416,7 +21657,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
20416
21657
  relativeDirPath: paths.relativeDirPath,
20417
21658
  relativeFilePath: paths.relativeFilePath,
20418
21659
  fileContent: applySharedConfigPatch({
20419
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
21660
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
20420
21661
  feature: "mcp",
20421
21662
  existingContent: fileContent,
20422
21663
  patch: { mcp_servers: merged.mcp_servers }
@@ -20669,7 +21910,7 @@ const KILO_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
20669
21910
  * impossible to start — no command, no URL — so it is written as the toggle it
20670
21911
  * resembles rather than dropped, but the loss is said out loud.
20671
21912
  */
20672
- function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
21913
+ function warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger) {
20673
21914
  const dropped = Object.keys(serverConfig).filter((key) => !KILO_TOGGLE_KEPT_KEYS.has(key));
20674
21915
  if (dropped.length === 0) return;
20675
21916
  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.`);
@@ -20682,7 +21923,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20682
21923
  if (declaresNoTransport(serverConfig)) {
20683
21924
  if (serverConfig.disabled === void 0) {
20684
21925
  if (existingEntry !== void 0 && !isKiloTransportServer(existingEntry)) {
20685
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21926
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20686
21927
  return existingEntry;
20687
21928
  }
20688
21929
  return warnAndSkipMcpServer({
@@ -20692,7 +21933,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
20692
21933
  logger
20693
21934
  });
20694
21935
  }
20695
- warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
21936
+ warnAboutToggleDroppedKeys$1(serverName, serverConfig, logger);
20696
21937
  return { enabled: !serverConfig.disabled };
20697
21938
  }
20698
21939
  if (isRemoteMcpServer(serverConfig)) {
@@ -20877,9 +22118,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
20877
22118
  * Merge a list of project rule file globs into the `instructions` array of the
20878
22119
  * shared `kilo.jsonc` (or `kilo.json`) config, preserving every existing key
20879
22120
  * (notably `mcp`/`tools` written by the MCP feature). In Kilo v7, files under
20880
- * `.kilo/rules/` are NOT auto-loaded; they are only picked up when listed in
20881
- * the `instructions` key. The resulting `instructions` list is deduped and
20882
- * sorted for a stable output.
22121
+ * a *project* `.kilo/rules/` are NOT auto-loaded; they are only picked up
22122
+ * when listed in the `instructions` key. (The home-scope `~/.kilo/rules/` is
22123
+ * different the rules migrator's `globalRulesDirs()` walks it on every
22124
+ * config load — which is why `KiloRule` registers instructions in project
22125
+ * scope only.) The resulting `instructions` list is deduped and sorted for a
22126
+ * stable output.
20883
22127
  *
20884
22128
  * @see https://kilo.ai/docs/automate/mcp/using-in-kilo-code
20885
22129
  */
@@ -21092,7 +22336,7 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
21092
22336
  const existingContent = existing.content;
21093
22337
  const existingSection = existing.mcp;
21094
22338
  const fileContent = applySharedConfigPatch({
21095
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
22339
+ fileKey: getKimiCodeConfigSharedFileKey({ global: true }),
21096
22340
  feature: "mcp",
21097
22341
  existingContent,
21098
22342
  patch: { mcp: {
@@ -21179,11 +22423,8 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
21179
22423
  * derivation sees this feature as one of that file's writers — it is not a
21180
22424
  * settable path, since the servers themselves live in `mcp.json`.
21181
22425
  */
21182
- static getExtraSharedWritePaths({ global = false } = {}) {
21183
- return global ? [{
21184
- relativeDirPath: getKimiCodeRelativeDirPath({ global: true }),
21185
- relativeFilePath: KIMI_CODE_CONFIG_FILE_NAME
21186
- }] : [];
22426
+ static getExtraSharedWritePaths() {
22427
+ return getKimiCodeSharedConfigWritePaths();
21187
22428
  }
21188
22429
  /**
21189
22430
  * The `[mcp]` defaults live in the shared user `config.toml`, not in
@@ -21318,13 +22559,49 @@ const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
21318
22559
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
21319
22560
  enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
21320
22561
  });
21321
- const OpencodeMcpServerSchema = zod_mini.z.union([OpencodeMcpLocalServerSchema, OpencodeMcpRemoteServerSchema]);
22562
+ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
22563
+ const OPENCODE_MCP_TRANSPORT_KEYS = [
22564
+ ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
22565
+ ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
22566
+ ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
22567
+ ].filter((key) => key !== "enabled");
22568
+ /**
22569
+ * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
22570
+ * disabling a server another config layer defines. It is the third member of
22571
+ * OpenCode's own `mcp` union in the published schema, described in-source as
22572
+ * "the legacy `{ enabled: false }` form used to disable a server". Without this
22573
+ * arm the union rejects the entry and the whole MCP import aborts — taking
22574
+ * every valid server in the same file down with it.
22575
+ *
22576
+ * Loose, unlike the two transport arms, so a key OpenCode adds to a toggle
22577
+ * later does not bring that abort back — but refined to reject anything
22578
+ * carrying a key only a transport entry has. A plain loose arm would sit under
22579
+ * a malformed `local` or `remote` entry that happens to carry `enabled` and
22580
+ * swallow it, dropping its command, URL, or headers without a word instead of
22581
+ * failing the way it does today. Mirrors {@link KiloMcpToggleSchema}.
22582
+ *
22583
+ * @see https://opencode.ai/config.json
22584
+ */
22585
+ const OpencodeMcpToggleSchema = zod_mini.z.looseObject({ enabled: zod_mini.z.boolean() }).check((0, zod_mini.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"));
22586
+ const OpencodeMcpServerSchema = zod_mini.z.union([
22587
+ OpencodeMcpLocalServerSchema,
22588
+ OpencodeMcpRemoteServerSchema,
22589
+ OpencodeMcpToggleSchema
22590
+ ]);
21322
22591
  const OpencodeConfigSchema = zod_mini.z.looseObject({
21323
22592
  $schema: zod_mini.z.optional(zod_mini.z.string()),
21324
22593
  mcp: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeMcpServerSchema)),
21325
22594
  tools: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.boolean()))
21326
22595
  });
21327
22596
  /**
22597
+ * Tell the two transport arms from a toggle entry. Both carry a `type` literal
22598
+ * and a toggle never does — the schema refuses one that tries — but the toggle
22599
+ * arm is loose, so its index signature hides that from `in` narrowing.
22600
+ */
22601
+ function isOpencodeTransportServer(server) {
22602
+ return server.type === "local" || server.type === "remote";
22603
+ }
22604
+ /**
21328
22605
  * Convert OpenCode native format back to standard MCP format
21329
22606
  * - type: "local" -> "stdio", "remote" -> "sse"
21330
22607
  * - command (array) -> command (first element) + args (rest)
@@ -21349,19 +22626,31 @@ function convertFromOpencodeFormat(opencodeMcp, tools) {
21349
22626
  ...convertOpencodeServers(opencodeMcp, tools)
21350
22627
  };
21351
22628
  }
22629
+ /** Split the shared top-level `tools` map into this server's own two lists. */
22630
+ function splitOpencodeServerTools(serverName, tools) {
22631
+ const enabledTools = [];
22632
+ const disabledTools = [];
22633
+ const prefix = `${serverName}_`;
22634
+ for (const [toolName, enabled] of Object.entries(tools ?? {})) {
22635
+ if (!toolName.startsWith(prefix)) continue;
22636
+ const toolSuffix = toolName.slice(prefix.length);
22637
+ (enabled ? enabledTools : disabledTools).push(toolSuffix);
22638
+ }
22639
+ return {
22640
+ enabledTools,
22641
+ disabledTools
22642
+ };
22643
+ }
21352
22644
  function convertOpencodeServers(opencodeMcp, tools) {
21353
22645
  return Object.fromEntries(Object.entries(opencodeMcp).map(([serverName, serverConfig]) => {
21354
22646
  const extraFields = Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !OPENCODE_KNOWN_SERVER_KEYS.has(key)));
21355
- const enabledTools = [];
21356
- const disabledTools = [];
21357
- const prefix = `${serverName}_`;
21358
- if (tools) {
21359
- for (const [toolName, enabled] of Object.entries(tools)) if (toolName.startsWith(prefix)) {
21360
- const toolSuffix = toolName.slice(prefix.length);
21361
- if (enabled) enabledTools.push(toolSuffix);
21362
- else disabledTools.push(toolSuffix);
21363
- }
21364
- }
22647
+ const { enabledTools, disabledTools } = splitOpencodeServerTools(serverName, tools);
22648
+ if (!isOpencodeTransportServer(serverConfig)) return [serverName, {
22649
+ ...extraFields,
22650
+ disabled: serverConfig.enabled === false,
22651
+ ...enabledTools.length > 0 && { enabledTools },
22652
+ ...disabledTools.length > 0 && { disabledTools }
22653
+ }];
21365
22654
  if (serverConfig.type === "remote") return [serverName, {
21366
22655
  ...extraFields,
21367
22656
  type: "sse",
@@ -21391,7 +22680,20 @@ function convertOpencodeServers(opencodeMcp, tools) {
21391
22680
  }];
21392
22681
  }));
21393
22682
  }
21394
- const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
22683
+ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
22684
+ "disabled",
22685
+ "enabledTools",
22686
+ "disabledTools"
22687
+ ]);
22688
+ /**
22689
+ * Warn about the fields a transport-less server loses by being written as a
22690
+ * bare toggle, so the drop is never silent.
22691
+ */
22692
+ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
22693
+ const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
22694
+ if (dropped.length === 0) return;
22695
+ 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.`);
22696
+ }
21395
22697
  /**
21396
22698
  * Convert standard MCP format to OpenCode native format
21397
22699
  * - type: "stdio" -> "local", "sse"/"http" -> "remote"
@@ -21401,11 +22703,27 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = ["timeout", "oauth"];
21401
22703
  * - enabledTools/disabledTools -> top-level tools map (with server name prefix)
21402
22704
  * - OpenCode-supported extras (timeout, oauth) -> passed through verbatim
21403
22705
  */
21404
- function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
22706
+ function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, logger) {
21405
22707
  const serverRecord = serverConfig;
21406
22708
  const passthrough = {};
21407
22709
  for (const key of OPENCODE_PASSTHROUGH_SERVER_FIELDS) if (serverRecord[key] !== void 0) passthrough[key] = serverRecord[key];
21408
22710
  const enabled = serverConfig.disabled !== void 0 ? !serverConfig.disabled : true;
22711
+ if (declaresNoTransport(serverConfig)) {
22712
+ if (serverConfig.disabled === void 0) {
22713
+ if (existingEntry !== void 0 && !isOpencodeTransportServer(existingEntry)) {
22714
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
22715
+ return existingEntry;
22716
+ }
22717
+ return warnAndSkipMcpServer({
22718
+ toolName: "OpenCode",
22719
+ serverName,
22720
+ reason: "no transport and no enabled state, so there is nothing to toggle",
22721
+ logger
22722
+ });
22723
+ }
22724
+ warnAboutToggleDroppedKeys(serverName, serverConfig, logger);
22725
+ return { enabled };
22726
+ }
21409
22727
  if (isRemoteMcpServer(serverConfig)) {
21410
22728
  const url = resolveRemoteMcpUrl(serverConfig);
21411
22729
  if (url === void 0) return warnAndSkipMcpServer({
@@ -21426,7 +22744,7 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21426
22744
  if (commandArray.length === 0) return warnAndSkipMcpServer({
21427
22745
  toolName: "OpenCode",
21428
22746
  serverName,
21429
- reason: declaresNoTransport(serverConfig) ? "no transport at all" : "a local transport but no command",
22747
+ reason: "a local transport but no command",
21430
22748
  logger
21431
22749
  });
21432
22750
  return {
@@ -21438,11 +22756,11 @@ function convertServerToOpencodeFormat(serverName, serverConfig, logger) {
21438
22756
  ...serverConfig.cwd && { cwd: serverConfig.cwd }
21439
22757
  };
21440
22758
  }
21441
- function convertToOpencodeFormat(mcpServers, logger) {
22759
+ function convertToOpencodeFormat(mcpServers, existingMcp, logger) {
21442
22760
  const tools = {};
21443
22761
  return {
21444
22762
  mcp: Object.fromEntries(Object.entries(mcpServers).map(([serverName, serverConfig]) => {
21445
- const converted = convertServerToOpencodeFormat(serverName, serverConfig, logger);
22763
+ const converted = convertServerToOpencodeFormat(serverName, serverConfig, existingMcp[serverName], logger);
21446
22764
  if (serverConfig.enabledTools) for (const tool of serverConfig.enabledTools) tools[`${serverName}_${tool}`] = true;
21447
22765
  if (serverConfig.disabledTools) for (const tool of serverConfig.disabledTools) tools[`${serverName}_${tool}`] = false;
21448
22766
  return converted === null ? null : [serverName, converted];
@@ -21512,10 +22830,12 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
21512
22830
  fileContent = await readFileContentOrNull(jsonPath);
21513
22831
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
21514
22832
  }
21515
- const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
22833
+ const transformedServers = convertEnvVarRefsToToolFormat({
21516
22834
  mcpServers: rulesyncMcp.getMcpServers(),
21517
22835
  replacement: "{env:$1}"
21518
- }), logger);
22836
+ });
22837
+ const existingMcp = OpencodeConfigSchema.safeParse((0, jsonc_parser.parse)(fileContent || "{}"));
22838
+ const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
21519
22839
  return new OpencodeMcp({
21520
22840
  outputRoot,
21521
22841
  relativeDirPath: basePaths.relativeDirPath,
@@ -22740,16 +24060,11 @@ var ZedMcp = class ZedMcp extends ToolMcp {
22740
24060
  relativeFilePath: ZED_SETTINGS_FILE_NAME
22741
24061
  };
22742
24062
  }
22743
- /**
22744
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
22745
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
22746
- * derivation (and the gateway ownership table it is checked against) must
22747
- * know both spellings on every platform.
22748
- */
24063
+ /** @see getZedOtherPlatformGlobalDir */
22749
24064
  static getExtraSharedWritePaths({ global = false } = {}) {
22750
24065
  if (!global) return [];
22751
24066
  return [{
22752
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
24067
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
22753
24068
  relativeFilePath: ZED_SETTINGS_FILE_NAME
22754
24069
  }];
22755
24070
  }
@@ -23825,10 +25140,11 @@ function buildPermissionEntry$1(toolName, pattern) {
23825
25140
  * file (global scope only). The file holds other CLI settings besides
23826
25141
  * permissions, so it is never deleted.
23827
25142
  *
23828
- * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
23829
- * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
23830
- * authored and round-tripped through the `antigravity-cli` permissions override
23831
- * (see `AntigravityCliPermissionsOverrideSchema`).
25143
+ * Four CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
25144
+ * `toolPermission` (the global autonomy preset), `enableTerminalSandbox`,
25145
+ * `artifactReviewPolicy` and `allowNonWorkspaceAccess` are authored and
25146
+ * round-tripped through the `antigravity-cli` permissions override (see
25147
+ * `AntigravityCliPermissionsOverrideSchema`).
23832
25148
  */
23833
25149
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
23834
25150
  constructor(params) {
@@ -23892,6 +25208,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23892
25208
  const override = config["antigravity-cli"];
23893
25209
  if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
23894
25210
  if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
25211
+ if (override?.artifactReviewPolicy !== void 0) merged.artifactReviewPolicy = override.artifactReviewPolicy;
25212
+ if (override?.allowNonWorkspaceAccess !== void 0) merged.allowNonWorkspaceAccess = override.allowNonWorkspaceAccess;
23895
25213
  const fileContent = JSON.stringify(merged, null, 2);
23896
25214
  return new AntigravityCliPermissions({
23897
25215
  outputRoot,
@@ -23918,6 +25236,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
23918
25236
  const override = {};
23919
25237
  if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
23920
25238
  if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
25239
+ if (typeof settings.artifactReviewPolicy === "string") override.artifactReviewPolicy = settings.artifactReviewPolicy;
25240
+ if (typeof settings.allowNonWorkspaceAccess === "boolean") override.allowNonWorkspaceAccess = settings.allowNonWorkspaceAccess;
23921
25241
  const result = { ...config };
23922
25242
  if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
23923
25243
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -24721,12 +26041,57 @@ function parseClaudePermissionEntry(entry) {
24721
26041
  };
24722
26042
  }
24723
26043
  /**
26044
+ * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
26045
+ * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
26046
+ * but never matched by those checks, so Claude Code warns at startup for each
26047
+ * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
26048
+ * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
26049
+ * docs prescribe instead. A tool-name rule with no path is unaffected: it
26050
+ * matches the tool everywhere and produces no warning.
26051
+ * @see https://code.claude.com/docs/en/permissions
26052
+ */
26053
+ function isPlainRecord(value) {
26054
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26055
+ }
26056
+ /**
26057
+ * Merge `patch` into `base`, recursing into plain objects so a sibling key at
26058
+ * any depth survives. Arrays and scalars are replaced, since a list the author
26059
+ * states is the list they mean.
26060
+ */
26061
+ function deepMergeRecords(base, patch) {
26062
+ const merged = { ...base };
26063
+ for (const [key, value] of Object.entries(patch)) {
26064
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
26065
+ const existing = merged[key];
26066
+ merged[key] = isPlainRecord(existing) && isPlainRecord(value) ? deepMergeRecords(existing, value) : value;
26067
+ }
26068
+ return merged;
26069
+ }
26070
+ const CLAUDE_PATH_RULE_ALIASES = {
26071
+ Write: "Edit",
26072
+ NotebookEdit: "Edit",
26073
+ Glob: "Read"
26074
+ };
26075
+ /**
24724
26076
  * Build a Claude Code permission entry like "Bash(npm run *)".
24725
26077
  * If the pattern is "*", returns just the tool name.
24726
26078
  */
24727
26079
  function buildClaudePermissionEntry(toolName, pattern) {
24728
26080
  if (pattern === "*") return toolName;
24729
- return `${toolName}(${pattern})`;
26081
+ return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
26082
+ }
26083
+ /**
26084
+ * The Claude tool names the canonical config manages. Deliberately the tool
26085
+ * names the categories map to and *not* the aliases a path rule is rewritten
26086
+ * to: claiming `Edit` because a `write` rule exists would sweep away the
26087
+ * `Read`/`Edit` entries the ignore feature and the user wrote in the same file.
26088
+ * The rewritten entries are still rulesync's to place — `applyPermissions`
26089
+ * replaces an entry this run emits wherever it currently sits — and the
26090
+ * original name stays claimed so an entry an older rulesync wrote in the warned
26091
+ * form is cleaned up on the next generate.
26092
+ */
26093
+ function managedClaudeToolNames(config) {
26094
+ return new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
24730
26095
  }
24731
26096
  var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions {
24732
26097
  constructor(params) {
@@ -24766,7 +26131,10 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24766
26131
  throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
24767
26132
  }
24768
26133
  const config = rulesyncPermissions.getJson();
24769
- const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
26134
+ const { allow, ask, deny } = convertRulesyncToClaudePermissions({
26135
+ config,
26136
+ logger
26137
+ });
24770
26138
  const overridePermissions = config.claudecode?.permissions;
24771
26139
  if (overridePermissions && typeof overridePermissions === "object") {
24772
26140
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
@@ -24775,7 +26143,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24775
26143
  ...nonListFields
24776
26144
  };
24777
26145
  }
24778
- const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
26146
+ const overrideSandbox = config.claudecode?.sandbox;
26147
+ if (isPlainRecord(overrideSandbox)) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, overrideSandbox);
26148
+ const managedToolNames = managedClaudeToolNames(config);
24779
26149
  const merged = applyPermissions({
24780
26150
  settings,
24781
26151
  managedToolNames,
@@ -24809,6 +26179,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24809
26179
  });
24810
26180
  const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
24811
26181
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
26182
+ const { sandbox } = settings;
26183
+ if (isPlainRecord(sandbox) && Object.keys(sandbox).length > 0) config.claudecode = {
26184
+ ...config.claudecode,
26185
+ sandbox
26186
+ };
24812
26187
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
24813
26188
  }
24814
26189
  validate() {
@@ -24830,14 +26205,18 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
24830
26205
  /**
24831
26206
  * Convert rulesync permissions config to Claude Code allow/ask/deny arrays.
24832
26207
  */
24833
- function convertRulesyncToClaudePermissions(config) {
26208
+ function convertRulesyncToClaudePermissions({ config, logger }) {
24834
26209
  const allow = [];
24835
26210
  const ask = [];
24836
26211
  const deny = [];
26212
+ const actionByEntry = /* @__PURE__ */ new Map();
24837
26213
  for (const [category, rules] of Object.entries(config.permission)) {
24838
26214
  const claudeToolName = toClaudeToolName(category);
24839
26215
  for (const [pattern, action] of Object.entries(rules)) {
24840
26216
  const entry = buildClaudePermissionEntry(claudeToolName, pattern);
26217
+ const previous = actionByEntry.get(entry);
26218
+ 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.`);
26219
+ actionByEntry.set(entry, action);
24841
26220
  switch (action) {
24842
26221
  case "allow":
24843
26222
  allow.push(entry);
@@ -25650,19 +27029,24 @@ function mapBashActionToDecision(action) {
25650
27029
  //#endregion
25651
27030
  //#region src/features/permissions/copilot-permissions.ts
25652
27031
  /**
25653
- * The flat, dotted VS Code setting key this adapter manages. VS Code stores
25654
- * settings with dotted keys flat at the document top level, so this is a single
25655
- * literal key — not a nested `chat.tools.terminal` path.
27032
+ * The flat, dotted VS Code setting keys this adapter manages, one per canonical
27033
+ * permission category. VS Code stores settings with dotted keys flat at the
27034
+ * document top level, so each is a single literal key — not a nested
27035
+ * `chat.tools.terminal` path. All three share the same
27036
+ * pattern-to-boolean shape, so one conversion covers them.
27037
+ *
27038
+ * The canonical `read` and `write` categories stay unmapped: VS Code has no
27039
+ * read-approval surface, and folding `write` into the edits map alongside
27040
+ * `edit` would make the two indistinguishable on import.
27041
+ *
25656
27042
  * @see https://code.visualstudio.com/docs/agents/approvals
27043
+ * @see https://code.visualstudio.com/docs/copilot/chat/review-code-edits
25657
27044
  */
25658
- const AUTO_APPROVE_KEY = "chat.tools.terminal.autoApprove";
25659
- /**
25660
- * The canonical permission category this adapter maps. Only shell/terminal
25661
- * commands (`bash`) have a clean, high-fidelity representation in VS Code's
25662
- * `chat.tools.terminal.autoApprove` map; other categories (read/edit/webfetch/
25663
- * …) have no terminal-command equivalent and are intentionally not mapped.
25664
- */
25665
- const TERMINAL_CATEGORY = "bash";
27045
+ const AUTO_APPROVE_KEYS = {
27046
+ bash: "chat.tools.terminal.autoApprove",
27047
+ edit: "chat.tools.edits.autoApprove",
27048
+ webfetch: "chat.tools.urls.autoApprove"
27049
+ };
25666
27050
  function asAutoApproveMap(value) {
25667
27051
  if (!isPlainObject$1(value)) return {};
25668
27052
  const result = {};
@@ -25670,6 +27054,21 @@ function asAutoApproveMap(value) {
25670
27054
  return result;
25671
27055
  }
25672
27056
  /**
27057
+ * Render one canonical category's rules as a VS Code auto-approve map. Returns
27058
+ * `undefined` when the category contributes nothing, so the key is retracted
27059
+ * rather than written as an empty object.
27060
+ *
27061
+ * The resulting map replaces the file's existing value wholesale — rulesync
27062
+ * owns these keys, so a rule dropped from the canonical config disappears from
27063
+ * the settings file too.
27064
+ */
27065
+ function buildAutoApproveValue(rules) {
27066
+ const autoApprove = {};
27067
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
27068
+ else if (action === "deny") autoApprove[pattern] = false;
27069
+ return Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
27070
+ }
27071
+ /**
25673
27072
  * Permissions generator for GitHub Copilot Chat in VS Code.
25674
27073
  *
25675
27074
  * VS Code has no standalone, environment-agnostic Copilot policy file (like
@@ -25679,13 +27078,16 @@ function asAutoApproveMap(value) {
25679
27078
  * many unrelated keys, so reads and writes merge into the existing JSON
25680
27079
  * (touching only the one managed key) and the file is never deleted.
25681
27080
  *
25682
- * Scope is deliberately limited to `chat.tools.terminal.autoApprove` the one
25683
- * clean, non-lossy mapping. The canonical `bash` category's per-pattern rules
25684
- * map as: `allow` → `true` (auto-approve), `deny` → `false` (never approve),
25685
- * and `ask` → the entry is OMITTED (VS Code then falls through to its default
25686
- * in-chat approval prompt, i.e. "ask"). Only project scope is modeled: VS Code's
25687
- * user-scope settings.json lives at a platform-dependent path outside rulesync's
25688
- * home-relative global model.
27081
+ * Three canonical categories have a clean, non-lossy representation and are
27082
+ * mapped (see {@link AUTO_APPROVE_KEYS}): `bash`, `edit` and `webfetch`. In
27083
+ * every one, per-pattern rules map as: `allow` → `true` (auto-approve), `deny`
27084
+ * `false` (VS Code then always prompts note this is "never auto-approve",
27085
+ * not a hard block), and `ask` the entry is OMITTED (VS Code falls through to
27086
+ * the same default prompt). A key whose canonical category is absent entirely
27087
+ * is left untouched, so authoring only `bash` rules never disturbs a
27088
+ * hand-written edits or urls map.
27089
+ * Only project scope is modeled: VS Code's user-scope settings.json lives at a
27090
+ * platform-dependent path outside rulesync's home-relative global model.
25689
27091
  */
25690
27092
  var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25691
27093
  constructor(params) {
@@ -25722,11 +27124,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25722
27124
  const paths = CopilotPermissions.getSettablePaths();
25723
27125
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
25724
27126
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
25725
- const rules = rulesyncPermissions.getJson().permission[TERMINAL_CATEGORY] ?? {};
25726
- const autoApprove = {};
25727
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow") autoApprove[pattern] = true;
25728
- else if (action === "deny") autoApprove[pattern] = false;
25729
- const patchValue = Object.keys(autoApprove).length > 0 ? autoApprove : void 0;
27127
+ const config = rulesyncPermissions.getJson();
27128
+ const patch = {};
27129
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
27130
+ const rules = config.permission[category];
27131
+ if (rules === void 0) continue;
27132
+ patch[settingKey] = buildAutoApproveValue(rules);
27133
+ }
25730
27134
  return new CopilotPermissions({
25731
27135
  outputRoot,
25732
27136
  relativeDirPath: paths.relativeDirPath,
@@ -25735,7 +27139,7 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25735
27139
  fileKey: sharedConfigFileKey(paths),
25736
27140
  feature: "permissions",
25737
27141
  existingContent,
25738
- patch: { [AUTO_APPROVE_KEY]: patchValue },
27142
+ patch,
25739
27143
  filePath
25740
27144
  }),
25741
27145
  validate: true
@@ -25754,10 +27158,13 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
25754
27158
  } catch (error) {
25755
27159
  throw new Error(`Failed to parse Copilot VS Code settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
25756
27160
  }
25757
- const autoApprove = asAutoApproveMap(settings[AUTO_APPROVE_KEY]);
25758
- const rules = {};
25759
- for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
25760
- const permission = Object.keys(rules).length > 0 ? { [TERMINAL_CATEGORY]: rules } : {};
27161
+ const permission = {};
27162
+ for (const [category, settingKey] of Object.entries(AUTO_APPROVE_KEYS)) {
27163
+ const autoApprove = asAutoApproveMap(settings[settingKey]);
27164
+ const rules = {};
27165
+ for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
27166
+ if (Object.keys(rules).length > 0) permission[category] = rules;
27167
+ }
25761
27168
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
25762
27169
  }
25763
27170
  validate() {
@@ -27087,6 +28494,13 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27087
28494
  relativeFilePath: HERMESAGENT_CONFIG_FILE_NAME
27088
28495
  };
27089
28496
  }
28497
+ /**
28498
+ * `config.yaml` under every spelling the global profile root can take.
28499
+ * @see getHermesagentSharedConfigWritePaths
28500
+ */
28501
+ static getExtraSharedWritePaths() {
28502
+ return getHermesagentSharedConfigWritePaths();
28503
+ }
27090
28504
  constructor(params) {
27091
28505
  super({
27092
28506
  ...params,
@@ -27124,7 +28538,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
27124
28538
  }
27125
28539
  setFileContent(fileContent) {
27126
28540
  this.fileContent = applySharedConfigPatch({
27127
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
28541
+ fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
27128
28542
  feature: "permissions",
27129
28543
  existingContent: fileContent,
27130
28544
  patch: parseSharedConfig({
@@ -27537,7 +28951,10 @@ const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
27537
28951
  "ask",
27538
28952
  "deny"
27539
28953
  ]))]);
27540
- const KiloPermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)) });
28954
+ const KiloPermissionsConfigSchema = zod_mini.z.looseObject({
28955
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)),
28956
+ sandbox: zod_mini.z.optional(zod_mini.z.unknown())
28957
+ });
27541
28958
  /**
27542
28959
  * Kilo permission keys that share a name with a canonical rulesync category and
27543
28960
  * therefore stay in the shared `permission` block. Everything else (Kilo-only
@@ -27594,6 +29011,25 @@ function collectKiloDenyPatterns(value) {
27594
29011
  }
27595
29012
  return [];
27596
29013
  }
29014
+ function asKiloRecord(value) {
29015
+ return isPlainObject$1(value) ? { ...value } : {};
29016
+ }
29017
+ /**
29018
+ * The `sandbox` keys a *project* `kilo.jsonc` may state. Kilo honors
29019
+ * `allowed_hosts` and `writable_paths` from the global config only, and lets a
29020
+ * project config merely tighten — so writing the wider keys into a project file
29021
+ * would produce config Kilo ignores.
29022
+ * @see https://kilo.ai/docs/getting-started/settings/sandboxing
29023
+ */
29024
+ const KILO_PROJECT_SCOPE_SANDBOX_KEYS = /* @__PURE__ */ new Set(["enabled", "network"]);
29025
+ function narrowSandboxToProjectScope({ authored, logger }) {
29026
+ const emitted = {};
29027
+ const dropped = [];
29028
+ for (const [key, value] of Object.entries(authored)) if (KILO_PROJECT_SCOPE_SANDBOX_KEYS.has(key)) emitted[key] = value;
29029
+ else dropped.push(key);
29030
+ if (dropped.length > 0) logger?.warn(`Kilo honors these 'sandbox' keys from the global config only, so they were dropped from the project config: ${dropped.toSorted().join(", ")}. A project config may only tighten the sandbox ('enabled', 'network'); generate with --global to author the rest.`);
29031
+ return emitted;
29032
+ }
27597
29033
  var KiloPermissions = class KiloPermissions extends ToolPermissions {
27598
29034
  json;
27599
29035
  constructor(params) {
@@ -27673,8 +29109,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27673
29109
  const basePaths = KiloPermissions.getSettablePaths({ global });
27674
29110
  const filePath = (0, node_path.join)(outputRoot, basePaths.relativeDirPath, basePaths.relativeFilePath);
27675
29111
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
27676
- const parsedPermission = parsed.permission;
27677
- const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
29112
+ const existingPermission = asKiloRecord(parsed.permission);
27678
29113
  const rulesyncJson = rulesyncPermissions.getJson();
27679
29114
  const kiloOverride = rulesyncJson.kilo;
27680
29115
  const incomingPermission = {
@@ -27700,6 +29135,18 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27700
29135
  ...parsed,
27701
29136
  permission: mergedPermission
27702
29137
  };
29138
+ if (kiloOverride?.sandbox !== void 0) {
29139
+ const authored = asKiloRecord(kiloOverride.sandbox);
29140
+ const emitted = global ? authored : narrowSandboxToProjectScope({
29141
+ authored,
29142
+ logger
29143
+ });
29144
+ const merged = {
29145
+ ...asKiloRecord(parsed.sandbox),
29146
+ ...emitted
29147
+ };
29148
+ if (Object.keys(merged).length > 0) nextJson.sandbox = merged;
29149
+ }
27703
29150
  return new KiloPermissions({
27704
29151
  outputRoot,
27705
29152
  relativeDirPath: basePaths.relativeDirPath,
@@ -27714,9 +29161,14 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
27714
29161
  const overrideOnly = {};
27715
29162
  for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
27716
29163
  else overrideOnly[key] = value;
27717
- const json = Object.keys(overrideOnly).length > 0 ? {
29164
+ const sandbox = this.json.sandbox;
29165
+ const override = {
29166
+ ...Object.keys(overrideOnly).length > 0 && { permission: overrideOnly },
29167
+ ...isPlainObject$1(sandbox) && { sandbox }
29168
+ };
29169
+ const json = Object.keys(override).length > 0 ? {
27718
29170
  permission: shared,
27719
- kilo: { permission: overrideOnly }
29171
+ kilo: override
27720
29172
  } : { permission: shared };
27721
29173
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
27722
29174
  }
@@ -27957,6 +29409,13 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
27957
29409
  isDeletable() {
27958
29410
  return false;
27959
29411
  }
29412
+ /**
29413
+ * `config.toml` under both spellings its directory can take.
29414
+ * @see getKimiCodeSharedConfigWritePaths
29415
+ */
29416
+ static getExtraSharedWritePaths() {
29417
+ return getKimiCodeSharedConfigWritePaths();
29418
+ }
27960
29419
  shouldMergeExistingFileContent() {
27961
29420
  return true;
27962
29421
  }
@@ -27971,7 +29430,7 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
27971
29430
  patch
27972
29431
  });
27973
29432
  this.fileContent = applySharedConfigPatch({
27974
- fileKey: KIMI_CODE_CONFIG_SHARED_FILE_KEY,
29433
+ fileKey: getKimiCodeConfigSharedFileKey({ global: this.global }),
27975
29434
  feature: "permissions",
27976
29435
  existingContent: fileContent,
27977
29436
  patch: {
@@ -28895,14 +30354,6 @@ function parseReasonixConfig(fileContent) {
28895
30354
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
28896
30355
  return { ...parsed };
28897
30356
  }
28898
- function toStringArray$1(value) {
28899
- if (!Array.isArray(value)) return [];
28900
- return value.filter((entry) => typeof entry === "string");
28901
- }
28902
- function toPermissionsTable(value) {
28903
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
28904
- return { ...value };
28905
- }
28906
30357
  const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_read_only_commands"];
28907
30358
  /**
28908
30359
  * `[agent]` keys an older `reasonix.toml` may carry that left the documented
@@ -28959,12 +30410,12 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
28959
30410
  const config = rulesyncPermissions.getJson();
28960
30411
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
28961
30412
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
28962
- const existingPermissions = toPermissionsTable(parsed.permissions);
28963
- const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28964
- const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28965
- const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30413
+ const existingPermissions = toReasonixTable(parsed.permissions);
30414
+ const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30415
+ const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30416
+ const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
28966
30417
  if (logger && managedToolNames.has("Read")) {
28967
- const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
30418
+ const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
28968
30419
  const { toolName } = parseReasonixPermissionEntry(entry);
28969
30420
  return toolName === "Read";
28970
30421
  });
@@ -29011,11 +30462,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
29011
30462
  });
29012
30463
  }
29013
30464
  toRulesyncPermissions() {
29014
- const permissions = toPermissionsTable(this.toml.permissions);
30465
+ const permissions = toReasonixTable(this.toml.permissions);
29015
30466
  const config = convertReasonixToRulesyncPermissions({
29016
- allow: toStringArray$1(permissions.allow),
29017
- ask: toStringArray$1(permissions.ask),
29018
- deny: toStringArray$1(permissions.deny)
30467
+ allow: toReasonixStringArray(permissions.allow),
30468
+ ask: toReasonixStringArray(permissions.ask),
30469
+ deny: toReasonixStringArray(permissions.deny)
29019
30470
  });
29020
30471
  const sandbox = asReasonixRecord(this.toml.sandbox);
29021
30472
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
@@ -29107,19 +30558,31 @@ const CATEGORY_TO_TOOL_KEYS = {
29107
30558
  "open_files",
29108
30559
  "expand_code_chunks",
29109
30560
  "expand_folder",
29110
- "grep"
30561
+ "grep",
30562
+ "getJiraIssue",
30563
+ "getConfluencePage"
29111
30564
  ],
29112
30565
  edit: [
29113
30566
  "find_and_replace_code",
29114
30567
  "create_file",
29115
30568
  "delete_file",
29116
- "move_file"
30569
+ "move_file",
30570
+ "createTechnicalPlan",
30571
+ "createJiraIssue",
30572
+ "updateJiraIssue",
30573
+ "createConfluencePage",
30574
+ "updateConfluencePage"
29117
30575
  ],
29118
30576
  write: [
29119
30577
  "create_file",
29120
30578
  "delete_file",
29121
30579
  "move_file",
29122
- "find_and_replace_code"
30580
+ "find_and_replace_code",
30581
+ "createTechnicalPlan",
30582
+ "createJiraIssue",
30583
+ "updateJiraIssue",
30584
+ "createConfluencePage",
30585
+ "updateConfluencePage"
29123
30586
  ]
29124
30587
  };
29125
30588
  const TOOL_KEY_TO_CATEGORY = {
@@ -29127,13 +30590,24 @@ const TOOL_KEY_TO_CATEGORY = {
29127
30590
  expand_code_chunks: "read",
29128
30591
  expand_folder: "read",
29129
30592
  grep: "read",
30593
+ getJiraIssue: "read",
30594
+ getConfluencePage: "read",
29130
30595
  find_and_replace_code: "edit",
29131
30596
  create_file: "edit",
29132
30597
  delete_file: "edit",
29133
- move_file: "edit"
30598
+ move_file: "edit",
30599
+ createTechnicalPlan: "edit",
30600
+ createJiraIssue: "edit",
30601
+ updateJiraIssue: "edit",
30602
+ createConfluencePage: "edit",
30603
+ updateConfluencePage: "edit"
29134
30604
  };
29135
30605
  const MANAGED_TOOL_KEYS = [.../* @__PURE__ */ new Set([...Object.values(CATEGORY_TO_TOOL_KEYS).flat(), ...Object.keys(TOOL_KEY_TO_CATEGORY)])];
29136
- const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
30606
+ const OWNED_TOOL_PERMISSION_KEYS = [
30607
+ "bash",
30608
+ "allowedExternalPaths",
30609
+ "default"
30610
+ ];
29137
30611
  /**
29138
30612
  * Permissions adapter for Rovo Dev CLI.
29139
30613
  *
@@ -29147,9 +30621,16 @@ const OWNED_TOOL_PERMISSION_KEYS = ["bash", "allowedExternalPaths"];
29147
30621
  * Mapping decisions (rulesync canonical -> Rovo Dev):
29148
30622
  * - `bash`: the catch-all `*` pattern -> `bash.default`; every other pattern ->
29149
30623
  * a `bash.commands[]` entry `{ command: <pattern as regex>, permission }`.
30624
+ * - the all-tools category `*`: its catch-all -> `toolPermissions.default`,
30625
+ * the level Rovo Dev falls back to for any tool with no more specific
30626
+ * setting (Rovo Dev's own default is `ask`).
29150
30627
  * - `read` -> the inspection tools (`open_files`, `expand_code_chunks`,
29151
- * `expand_folder`, `grep`); `edit`/`write` -> the mutation tools
29152
- * (`find_and_replace_code`, `create_file`, `delete_file`, `move_file`).
30628
+ * `expand_folder`, `grep`, `getJiraIssue`, `getConfluencePage`);
30629
+ * `edit`/`write` -> the mutation tools (`find_and_replace_code`,
30630
+ * `create_file`, `delete_file`, `move_file`, `createTechnicalPlan`,
30631
+ * `createJiraIssue`, `updateJiraIssue`, `createConfluencePage`,
30632
+ * `updateConfluencePage`) — so these two categories reach Jira and
30633
+ * Confluence, not just the working tree.
29153
30634
  * These Rovo Dev keys hold a single level (no per-pattern rules), so only the
29154
30635
  * catch-all `*` of each category sets the level. Non-catch-all `allow` rules
29155
30636
  * in those categories are surfaced as `allowedExternalPaths` so explicit path
@@ -29329,6 +30810,10 @@ function stripPermissiveOwnedValues(toolPermissions) {
29329
30810
  delete toolPermissions.allowedExternalPaths;
29330
30811
  strippedKeys.push("allowedExternalPaths");
29331
30812
  }
30813
+ if (toolPermissions.default === "allow") {
30814
+ delete toolPermissions.default;
30815
+ strippedKeys.push("default");
30816
+ }
29332
30817
  const bash = toolPermissions.bash;
29333
30818
  if (isRecord(bash)) {
29334
30819
  const stripped = { ...bash };
@@ -29353,10 +30838,19 @@ function stripPermissiveOwnedValues(toolPermissions) {
29353
30838
  function convertRulesyncToRovodevToolPermissions({ config, logger }) {
29354
30839
  const toolPermissions = {};
29355
30840
  const allowedExternalPaths = [];
29356
- const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
29357
- const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
29358
- if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30841
+ warnOnEditWriteConflict({
30842
+ config,
30843
+ logger
30844
+ });
29359
30845
  for (const [category, rules] of Object.entries(config.permission)) {
30846
+ if (category === CATCH_ALL_PATTERN$1) {
30847
+ const toolWideDefault = convertAllToolsRules({
30848
+ rules,
30849
+ logger
30850
+ });
30851
+ if (toolWideDefault) toolPermissions.default = toolWideDefault;
30852
+ continue;
30853
+ }
29360
30854
  if (category === "bash") {
29361
30855
  const bash = convertBashRules(rules);
29362
30856
  if (bash) toolPermissions.bash = bash;
@@ -29383,6 +30877,36 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
29383
30877
  if (allowedExternalPaths.length > 0) toolPermissions.allowedExternalPaths = [...new Set(allowedExternalPaths)].toSorted();
29384
30878
  return toolPermissions;
29385
30879
  }
30880
+ /**
30881
+ * `edit` and `write` collapse onto the same Rovo Dev file-mutation tools, so a
30882
+ * conflicting catch-all between them cannot be represented. Warn that the loss
30883
+ * is happening; the conversion keeps the stricter of the two — the same
30884
+ * fail-closed rule the import direction uses when those tools disagree, so the
30885
+ * resolution never grants more than the author asked for.
30886
+ */
30887
+ function warnOnEditWriteConflict({ config, logger }) {
30888
+ const editCatchAll = config.permission.edit?.[CATCH_ALL_PATTERN$1];
30889
+ const writeCatchAll = config.permission.write?.[CATCH_ALL_PATTERN$1];
30890
+ if (editCatchAll && writeCatchAll && editCatchAll !== writeCatchAll) logger?.warn(`Rovo Dev maps both "edit" and "write" onto the same file-mutation tools, but they have conflicting catch-all permissions ("edit": "${editCatchAll}", "write": "${writeCatchAll}"). The stricter of the two ("${strictestAction(editCatchAll, writeCatchAll)}") is used.`);
30891
+ }
30892
+ /**
30893
+ * The canonical all-tools category. Its catch-all sets the tool-wide
30894
+ * `toolPermissions.default`, the same way `bash`'s catch-all sets
30895
+ * `bash.default` — both are the level Rovo Dev falls back to. Pattern rules
30896
+ * under `*` have no counterpart (the default is a single level), so they are
30897
+ * reported and skipped like any other rule Rovo Dev cannot express.
30898
+ */
30899
+ function convertAllToolsRules({ rules, logger }) {
30900
+ let toolWideDefault;
30901
+ for (const [pattern, action] of Object.entries(rules)) {
30902
+ if (pattern === CATCH_ALL_PATTERN$1) {
30903
+ toolWideDefault = action;
30904
+ continue;
30905
+ }
30906
+ logger?.warn(`Rovo Dev's tool-wide default is a single level, so it cannot express the pattern "${pattern}" in the "*" category. Skipping it.`);
30907
+ }
30908
+ return toolWideDefault;
30909
+ }
29386
30910
  function convertBashRules(rules) {
29387
30911
  const bash = {};
29388
30912
  const commands = [];
@@ -29404,6 +30928,7 @@ function convertBashRules(rules) {
29404
30928
  */
29405
30929
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
29406
30930
  const permission = {};
30931
+ if (isPermissionAction(toolPermissions.default)) permission[CATCH_ALL_PATTERN$1] = { [CATCH_ALL_PATTERN$1]: toolPermissions.default };
29407
30932
  const bash = toolPermissions.bash;
29408
30933
  if (isRecord(bash)) {
29409
30934
  const bashRules = {};
@@ -29414,12 +30939,15 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
29414
30939
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
29415
30940
  }
29416
30941
  const nestedTools = isRecord(toolPermissions.tools) ? toolPermissions.tools : {};
29417
- for (const [toolKey, category] of Object.entries(TOOL_KEY_TO_CATEGORY)) {
29418
- const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
29419
- if (!isPermissionAction(value)) continue;
30942
+ const implicitLevel = isPermissionAction(toolPermissions.default) ? toolPermissions.default : "ask";
30943
+ for (const category of new Set(Object.values(TOOL_KEY_TO_CATEGORY))) {
30944
+ const levels = Object.entries(TOOL_KEY_TO_CATEGORY).filter(([, mapped]) => mapped === category).map(([toolKey]) => {
30945
+ const value = Object.hasOwn(nestedTools, toolKey) ? nestedTools[toolKey] : toolPermissions[toolKey];
30946
+ return isPermissionAction(value) ? value : void 0;
30947
+ });
30948
+ if (levels.every((level) => level === void 0)) continue;
29420
30949
  permission[category] ??= {};
29421
- const current = permission[category][CATCH_ALL_PATTERN$1];
29422
- permission[category][CATCH_ALL_PATTERN$1] = strictestAction(current, value);
30950
+ permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
29423
30951
  }
29424
30952
  if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
29425
30953
  permission.read ??= {};
@@ -30251,16 +31779,11 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
30251
31779
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30252
31780
  };
30253
31781
  }
30254
- /**
30255
- * The global settings file of the OTHER platform: `getSettablePaths` resolves
30256
- * `~/.config/zed` vs `%APPDATA%\Zed` per platform, but the shared-write
30257
- * derivation (and the gateway ownership table it is checked against) must
30258
- * know both spellings on every platform.
30259
- */
31782
+ /** @see getZedOtherPlatformGlobalDir */
30260
31783
  static getExtraSharedWritePaths({ global = false } = {}) {
30261
31784
  if (!global) return [];
30262
31785
  return [{
30263
- relativeDirPath: process.platform === "win32" ? ZED_GLOBAL_DIR : ZED_GLOBAL_WIN32_DIR,
31786
+ relativeDirPath: getZedOtherPlatformGlobalDir(),
30264
31787
  relativeFilePath: ZED_SETTINGS_FILE_NAME
30265
31788
  }];
30266
31789
  }
@@ -31777,6 +33300,7 @@ const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
31777
33300
  arguments: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
31778
33301
  context: zod_mini.z.optional(zod_mini.z.string()),
31779
33302
  agent: zod_mini.z.optional(zod_mini.z.string()),
33303
+ background: zod_mini.z.optional(zod_mini.z.boolean()),
31780
33304
  hooks: zod_mini.z.optional(zod_mini.z.looseObject({})),
31781
33305
  shell: zod_mini.z.optional(zod_mini.z.string()),
31782
33306
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
@@ -31803,6 +33327,7 @@ function buildClaudecodeSkillFrontmatter({ rulesyncFrontmatter, resolvedDisableM
31803
33327
  shell: section.shell
31804
33328
  };
31805
33329
  const definedFields = {
33330
+ background: section.background,
31806
33331
  arguments: section.arguments,
31807
33332
  hooks: section.hooks,
31808
33333
  "disable-model-invocation": resolvedDisableModelInvocation,
@@ -31880,6 +33405,7 @@ var ClaudecodeSkill = class extends ToolSkill {
31880
33405
  ...frontmatter.arguments !== void 0 && { arguments: frontmatter.arguments },
31881
33406
  ...frontmatter.context && { context: frontmatter.context },
31882
33407
  ...frontmatter.agent && { agent: frontmatter.agent },
33408
+ ...frontmatter.background !== void 0 && { background: frontmatter.background },
31883
33409
  ...frontmatter.hooks !== void 0 && { hooks: frontmatter.hooks },
31884
33410
  ...frontmatter.shell && { shell: frontmatter.shell },
31885
33411
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
@@ -32489,7 +34015,9 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
32489
34015
  description: zod_mini.z.string(),
32490
34016
  license: zod_mini.z.optional(zod_mini.z.string()),
32491
34017
  "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
32492
- "argument-hint": zod_mini.z.optional(zod_mini.z.string())
34018
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
34019
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
34020
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean())
32493
34021
  });
32494
34022
  /**
32495
34023
  * Represents a GitHub Copilot CLI skill directory.
@@ -32548,7 +34076,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32548
34076
  const copilotcliSection = {
32549
34077
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
32550
34078
  ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
32551
- ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
34079
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] },
34080
+ ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
34081
+ ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] }
32552
34082
  };
32553
34083
  const rulesyncFrontmatter = {
32554
34084
  name: frontmatter.name,
@@ -32575,7 +34105,9 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
32575
34105
  description: rulesyncFrontmatter.description,
32576
34106
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
32577
34107
  ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
32578
- ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
34108
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] },
34109
+ ...rulesyncFrontmatter.copilotcli?.["user-invocable"] !== void 0 && { "user-invocable": rulesyncFrontmatter.copilotcli["user-invocable"] },
34110
+ ...rulesyncFrontmatter.copilotcli?.["disable-model-invocation"] !== void 0 && { "disable-model-invocation": rulesyncFrontmatter.copilotcli["disable-model-invocation"] }
32579
34111
  };
32580
34112
  return new CopilotcliSkill({
32581
34113
  outputRoot,
@@ -34245,6 +35777,37 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
34245
35777
  alternativeSkillRoots: [global ? OPENCODE_GLOBAL_SKILL_DIR_PATH : OPENCODE_SKILL_DIR_PATH]
34246
35778
  };
34247
35779
  }
35780
+ /**
35781
+ * Extra skill roots the project configured in `opencode.json` /
35782
+ * `opencode.jsonc` via `skills.paths` ("Additional paths to skill folders").
35783
+ * Without these, skills a project keeps outside `.opencode/skills/` are
35784
+ * invisible to `rulesync import` even though OpenCode loads them.
35785
+ *
35786
+ * Import-only: rulesync keeps writing to its own managed root, so a
35787
+ * configured path is read but never generated into. `skills.urls` is a
35788
+ * remote-fetch surface and is out of scope for a file-based generator.
35789
+ *
35790
+ * Absolute paths and paths escaping the output root are dropped — an import
35791
+ * root is joined onto `outputRoot`, and reaching outside it is not something
35792
+ * a project config should be able to ask for.
35793
+ *
35794
+ * @see https://opencode.ai/config.json
35795
+ */
35796
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
35797
+ const skills = asOpencodeEntries((await readOpencodeConfig({
35798
+ outputRoot,
35799
+ global
35800
+ })).skills);
35801
+ if (skills === null || !Array.isArray(skills.paths)) return [];
35802
+ const configDir = getOpencodeConfigDir({
35803
+ outputRoot,
35804
+ global
35805
+ });
35806
+ return skills.paths.filter((candidate) => typeof candidate === "string" && candidate !== "" && !(0, node_path.isAbsolute)(candidate) && !(0, node_path.normalize)(candidate).startsWith("..")).map((relativeDirPath) => ({
35807
+ outputRoot: configDir,
35808
+ relativeDirPath
35809
+ }));
35810
+ }
34248
35811
  getFrontmatter() {
34249
35812
  return OpenCodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
34250
35813
  }
@@ -36178,12 +37741,19 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36178
37741
  */
36179
37742
  async loadToolDirs() {
36180
37743
  const factory = this.getFactory(this.toolTarget);
36181
- const roots = toolSkillImportRoots(factory.class.getSettablePaths({ global: this.global }));
37744
+ const paths = factory.class.getSettablePaths({ global: this.global });
37745
+ const configuredRoots = factory.class.getConfiguredImportRoots ? await factory.class.getConfiguredImportRoots({
37746
+ outputRoot: this.outputRoot,
37747
+ global: this.global
37748
+ }) : [];
37749
+ const configuredRootPaths = new Set(configuredRoots.map((root) => root.relativeDirPath));
37750
+ const roots = [...toolSkillImportRoots(paths), ...configuredRoots];
36182
37751
  const seenSkillNames = /* @__PURE__ */ new Set();
36183
37752
  const toolSkills = [];
36184
37753
  for (const root of roots) {
36185
37754
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
36186
37755
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37756
+ const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
36187
37757
  const skillsDirPath = (0, node_path.join)(rootOutputRoot, relativeDirPath);
36188
37758
  if (!await directoryExists(skillsDirPath)) continue;
36189
37759
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
@@ -36198,12 +37768,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
36198
37768
  })) continue;
36199
37769
  ownedDirNames.push(dirName);
36200
37770
  }
36201
- const directorySkills = await Promise.all(ownedDirNames.map((dirName) => factory.class.fromDir({
36202
- outputRoot: rootOutputRoot,
36203
- relativeDirPath,
36204
- dirName,
36205
- global: this.global
36206
- })));
37771
+ const directorySkills = (await Promise.all(ownedDirNames.map(async (dirName) => {
37772
+ try {
37773
+ return await factory.class.fromDir({
37774
+ outputRoot: rootOutputRoot,
37775
+ relativeDirPath,
37776
+ dirName,
37777
+ global: this.global
37778
+ });
37779
+ } catch (error) {
37780
+ if (!isConfiguredRoot) throw error;
37781
+ this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
37782
+ return null;
37783
+ }
37784
+ }))).filter((skill) => skill !== null);
36207
37785
  for (const skill of directorySkills) {
36208
37786
  const skillName = skill.getImportIdentity();
36209
37787
  if (seenSkillNames.has(skillName)) continue;
@@ -36728,6 +38306,242 @@ var RovodevSubagent = class RovodevSubagent extends ToolSubagent {
36728
38306
  }
36729
38307
  };
36730
38308
  //#endregion
38309
+ //#region src/features/subagents/antigravity-shared-subagent.ts
38310
+ /**
38311
+ * Frontmatter of an Antigravity custom agent (Markdown format, CLI v1.1.6+).
38312
+ *
38313
+ * `name` and `description` are required upstream; the rest are optional and
38314
+ * documented with defaults (`tools: []`, `mainAgent: true`, `subagent: true`,
38315
+ * `model: inherit`, `commandExecutionPolicy: sandbox`, `mcpServers: []`,
38316
+ * `skills`/`plugins`: `[]`). `hidden` and `inheritMcp` appear in the v1.1.6
38317
+ * release notes but not in the documented frontmatter table, so they are
38318
+ * accepted as verbatim passthrough without any behavior modeled around them.
38319
+ * `looseObject` keeps unknown future fields round-tripping.
38320
+ *
38321
+ * @see https://antigravity.google/docs/subagents
38322
+ */
38323
+ const AntigravitySubagentFrontmatterSchema = zod_mini.z.looseObject({
38324
+ name: zod_mini.z.string(),
38325
+ description: zod_mini.z.string().check(zod_mini.z.minLength(1)),
38326
+ tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
38327
+ mainAgent: zod_mini.z.optional(zod_mini.z.boolean()),
38328
+ subagent: zod_mini.z.optional(zod_mini.z.boolean()),
38329
+ model: zod_mini.z.optional(zod_mini.z.string()),
38330
+ commandExecutionPolicy: zod_mini.z.optional(zod_mini.z.string()),
38331
+ mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown())),
38332
+ skills: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
38333
+ plugins: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
38334
+ hidden: zod_mini.z.optional(zod_mini.z.boolean()),
38335
+ inheritMcp: zod_mini.z.optional(zod_mini.z.boolean())
38336
+ });
38337
+ /**
38338
+ * Shared custom-agent (subagent) implementation for Google Antigravity 2.0,
38339
+ * used by the IDE, the CLI and plugin bundles.
38340
+ *
38341
+ * Antigravity discovers agents at `.agents/agents/<name>.md` (project) and
38342
+ * `~/.gemini/config/agents/<name>.md` (global, shared by the IDE and the CLI
38343
+ * exactly like `~/.gemini/config/hooks.json`). The directory form
38344
+ * (`<name>/agent.md`) is an equivalent alternative upstream; rulesync emits and
38345
+ * imports the flat file form. The body after the frontmatter is the agent's
38346
+ * system prompt.
38347
+ *
38348
+ * Concrete subclasses only supply the rulesync target name they answer to via
38349
+ * {@link AntigravitySharedSubagent.getToolTarget} and, where the shared file is
38350
+ * not involved, the sections they read via
38351
+ * {@link AntigravitySharedSubagent.getReadSectionKeys}.
38352
+ *
38353
+ * @see https://antigravity.google/docs/subagents
38354
+ */
38355
+ var AntigravitySharedSubagent = class extends ToolSubagent {
38356
+ frontmatter;
38357
+ body;
38358
+ constructor({ frontmatter, body, fileContent, ...rest }) {
38359
+ if (rest.validate !== false) {
38360
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
38361
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
38362
+ }
38363
+ super({
38364
+ ...rest,
38365
+ fileContent: fileContent ?? stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
38366
+ });
38367
+ this.frontmatter = frontmatter;
38368
+ this.body = body;
38369
+ }
38370
+ /** The rulesync target name this subagent answers to. */
38371
+ static getToolTarget() {
38372
+ throw new Error("Please implement this method in the subclass.");
38373
+ }
38374
+ /**
38375
+ * Tool-specific sections this target reads, in increasing precedence order.
38376
+ *
38377
+ * `antigravity-ide` and `antigravity-cli` write the very same file, so a
38378
+ * target that read only its own section would silently drop the other's keys
38379
+ * — and which one survived would depend on `--targets` order. Every target
38380
+ * therefore merges the shared `antigravity-ide` → `antigravity-cli` sections
38381
+ * (the CLI block wins, matching the fixed order the MCP feature already uses
38382
+ * for the same shared-output reason), and the plugin target layers its own
38383
+ * section on top of that. Only `getToolTarget()` decides which section an
38384
+ * import writes back into.
38385
+ */
38386
+ static getReadSectionKeys() {
38387
+ return ["antigravity-ide", "antigravity-cli"];
38388
+ }
38389
+ static getSettablePaths({ global = false } = {}) {
38390
+ return { relativeDirPath: global ? ANTIGRAVITY_GLOBAL_AGENTS_DIR_PATH : ANTIGRAVITY_AGENTS_DIR_PATH };
38391
+ }
38392
+ getFrontmatter() {
38393
+ return this.frontmatter;
38394
+ }
38395
+ getBody() {
38396
+ return this.body;
38397
+ }
38398
+ toRulesyncSubagent() {
38399
+ const { name, description, ...restFields } = this.frontmatter;
38400
+ return new RulesyncSubagent({
38401
+ outputRoot: ".",
38402
+ frontmatter: {
38403
+ targets: ["*"],
38404
+ name,
38405
+ description,
38406
+ ...Object.keys(restFields).length > 0 && { [this.constructor.getToolTarget()]: restFields }
38407
+ },
38408
+ body: this.body,
38409
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
38410
+ relativeFilePath: this.getRelativeFilePath(),
38411
+ validate: true
38412
+ });
38413
+ }
38414
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
38415
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
38416
+ const mergedSection = Object.assign({}, ...this.getReadSectionKeys().map((key) => rulesyncFrontmatter[key] ?? {}));
38417
+ const toolSection = this.filterToolSpecificSection(mergedSection, ["name", "description"]);
38418
+ const rawFrontmatter = {
38419
+ name: rulesyncFrontmatter.name,
38420
+ description: rulesyncFrontmatter.description || `${rulesyncFrontmatter.name} subagent`,
38421
+ ...toolSection
38422
+ };
38423
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(rawFrontmatter);
38424
+ if (!result.success) throw new Error(`Invalid ${this.getToolTarget()} subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
38425
+ const frontmatter = result.data;
38426
+ const body = rulesyncSubagent.getBody();
38427
+ const paths = this.getSettablePaths({ global });
38428
+ return new this({
38429
+ outputRoot,
38430
+ frontmatter,
38431
+ body,
38432
+ relativeDirPath: paths.relativeDirPath,
38433
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
38434
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
38435
+ validate,
38436
+ global
38437
+ });
38438
+ }
38439
+ validate() {
38440
+ if (!this.frontmatter) return {
38441
+ success: true,
38442
+ error: null
38443
+ };
38444
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(this.frontmatter);
38445
+ if (result.success) return {
38446
+ success: true,
38447
+ error: null
38448
+ };
38449
+ return {
38450
+ success: false,
38451
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
38452
+ };
38453
+ }
38454
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
38455
+ return this.isTargetedByRulesyncSubagentDefault({
38456
+ rulesyncSubagent,
38457
+ toolTarget: this.getToolTarget()
38458
+ });
38459
+ }
38460
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
38461
+ const paths = this.getSettablePaths({ global });
38462
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
38463
+ const fileContent = await readFileContent(filePath);
38464
+ const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
38465
+ const result = AntigravitySubagentFrontmatterSchema.safeParse(frontmatter);
38466
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
38467
+ return new this({
38468
+ outputRoot,
38469
+ relativeDirPath: paths.relativeDirPath,
38470
+ relativeFilePath,
38471
+ frontmatter: result.data,
38472
+ body: content.trim(),
38473
+ fileContent,
38474
+ validate,
38475
+ global
38476
+ });
38477
+ }
38478
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
38479
+ return new this({
38480
+ outputRoot,
38481
+ relativeDirPath,
38482
+ relativeFilePath,
38483
+ frontmatter: {
38484
+ name: "",
38485
+ description: ""
38486
+ },
38487
+ body: "",
38488
+ fileContent: "",
38489
+ validate: false,
38490
+ global
38491
+ });
38492
+ }
38493
+ };
38494
+ //#endregion
38495
+ //#region src/features/subagents/antigravity-cli-subagent.ts
38496
+ /**
38497
+ * Google Antigravity CLI custom agent (subagent), shipped in CLI v1.1.6.
38498
+ *
38499
+ * Shares all behavior with {@link AntigravitySharedSubagent}; the CLI reads the
38500
+ * same `.agents/agents/` and `~/.gemini/config/agents/` roots as the IDE and
38501
+ * exposes them through the `agy agents` subcommand and the `--agent` flag. It
38502
+ * answers to the `antigravity-cli` target.
38503
+ */
38504
+ var AntigravityCliSubagent = class extends AntigravitySharedSubagent {
38505
+ static getToolTarget() {
38506
+ return "antigravity-cli";
38507
+ }
38508
+ };
38509
+ //#endregion
38510
+ //#region src/features/subagents/antigravity-ide-subagent.ts
38511
+ /**
38512
+ * Google Antigravity IDE custom agent (subagent).
38513
+ *
38514
+ * Shares all behavior with {@link AntigravitySharedSubagent} — the subagents
38515
+ * documentation is product-wide and lists the same `.agents/agents/` and
38516
+ * `~/.gemini/config/agents/` roots for the IDE and the CLI. It answers to the
38517
+ * `antigravity-ide` target.
38518
+ */
38519
+ var AntigravityIdeSubagent = class extends AntigravitySharedSubagent {
38520
+ static getToolTarget() {
38521
+ return "antigravity-ide";
38522
+ }
38523
+ };
38524
+ //#endregion
38525
+ //#region src/features/subagents/antigravity-plugin-subagent.ts
38526
+ /**
38527
+ * Custom agent inside an Antigravity plugin bundle
38528
+ * (`<plugin_name>/agents/<name>.md`). Plugin bundles are project-scope output
38529
+ * only; they are staged into `~/.gemini/antigravity-cli/plugins/` by the user.
38530
+ *
38531
+ * @see https://antigravity.google/docs/cli/plugins
38532
+ */
38533
+ var AntigravityPluginSubagent = class extends AntigravitySharedSubagent {
38534
+ static getToolTarget() {
38535
+ return "antigravity-plugin";
38536
+ }
38537
+ static getReadSectionKeys() {
38538
+ return [...super.getReadSectionKeys(), "antigravity-plugin"];
38539
+ }
38540
+ static getSettablePaths() {
38541
+ return { relativeDirPath: ANTIGRAVITY_PLUGIN_AGENTS_DIR };
38542
+ }
38543
+ };
38544
+ //#endregion
36731
38545
  //#region src/features/subagents/augmentcode-subagent.ts
36732
38546
  const AugmentcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
36733
38547
  name: zod_mini.z.string(),
@@ -38541,7 +40355,7 @@ def register(ctx):
38541
40355
  _register_subagent(ctx, subagent)
38542
40356
  `;
38543
40357
  }
38544
- function getEnabledPluginConfigContent(currentContent) {
40358
+ function getEnabledPluginConfigContent({ currentContent, global }) {
38545
40359
  const config = parseSharedConfig({
38546
40360
  format: "yaml",
38547
40361
  fileContent: currentContent
@@ -38549,7 +40363,7 @@ function getEnabledPluginConfigContent(currentContent) {
38549
40363
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
38550
40364
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
38551
40365
  return applySharedConfigPatch({
38552
- fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
40366
+ fileKey: getHermesagentConfigSharedFileKey({ global }),
38553
40367
  feature: "subagents",
38554
40368
  existingContent: currentContent,
38555
40369
  patch: { plugins: {
@@ -38600,6 +40414,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38600
40414
  return !targets || targets.includes("*") || targets.includes("hermesagent");
38601
40415
  }
38602
40416
  static fromRulesyncSubagents({ rulesyncSubagents, outputRoot, global = false }) {
40417
+ const pluginDirPath = getHermesagentRelativeDirPath({
40418
+ global,
40419
+ relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
40420
+ });
38603
40421
  return [
38604
40422
  ...rulesyncSubagents.map((rulesyncSubagent) => HermesagentSubagent.fromRulesyncSubagent({
38605
40423
  relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
@@ -38608,20 +40426,14 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38608
40426
  global
38609
40427
  })),
38610
40428
  new HermesagentSubagent({
38611
- relativeDirPath: getHermesagentRelativeDirPath({
38612
- global,
38613
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
38614
- }),
40429
+ relativeDirPath: pluginDirPath,
38615
40430
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH),
38616
40431
  fileContent: "",
38617
40432
  outputRoot,
38618
40433
  global
38619
40434
  }),
38620
40435
  new HermesagentSubagent({
38621
- relativeDirPath: getHermesagentRelativeDirPath({
38622
- global,
38623
- relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_DIR_PATH
38624
- }),
40436
+ relativeDirPath: pluginDirPath,
38625
40437
  relativeFilePath: (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH),
38626
40438
  fileContent: "",
38627
40439
  outputRoot,
@@ -38661,14 +40473,12 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38661
40473
  * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
38662
40474
  * so the write must be declared for the shared-file order derivation.
38663
40475
  */
38664
- static getExtraSharedWritePaths({ global = false } = {}) {
38665
- return global ? [{
38666
- relativeDirPath: getHermesagentRelativeDirPath({
38667
- global,
38668
- relativeDirPath: HERMESAGENT_GLOBAL_DIR
38669
- }),
38670
- relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
38671
- }] : [];
40476
+ /**
40477
+ * `config.yaml` under every spelling the global profile root can take.
40478
+ * @see getHermesagentSharedConfigWritePaths
40479
+ */
40480
+ static getExtraSharedWritePaths() {
40481
+ return getHermesagentSharedConfigWritePaths();
38672
40482
  }
38673
40483
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
38674
40484
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
@@ -38701,7 +40511,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38701
40511
  }
38702
40512
  setFileContent(newFileContent) {
38703
40513
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) {
38704
- super.setFileContent(getEnabledPluginConfigContent(newFileContent));
40514
+ super.setFileContent(getEnabledPluginConfigContent({
40515
+ currentContent: newFileContent,
40516
+ global: this.global
40517
+ }));
38705
40518
  return;
38706
40519
  }
38707
40520
  super.setFileContent(newFileContent);
@@ -38709,7 +40522,10 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
38709
40522
  getFileContent() {
38710
40523
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_MANIFEST_PATH)) return getPluginManifestContent();
38711
40524
  if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_RULESYNC_SUBAGENTS_PLUGIN_INIT_PATH)) return getPluginInitContent();
38712
- if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent(super.getFileContent());
40525
+ if (this.getRelativeFilePath() === (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)) return getEnabledPluginConfigContent({
40526
+ currentContent: super.getFileContent(),
40527
+ global: this.global
40528
+ });
38713
40529
  return super.getFileContent();
38714
40530
  }
38715
40531
  };
@@ -40305,6 +42121,30 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
40305
42121
  filePattern: "*.md"
40306
42122
  }
40307
42123
  }],
42124
+ ["antigravity-cli", {
42125
+ class: AntigravityCliSubagent,
42126
+ meta: {
42127
+ supportsSimulated: false,
42128
+ supportsGlobal: true,
42129
+ filePattern: "*.md"
42130
+ }
42131
+ }],
42132
+ ["antigravity-ide", {
42133
+ class: AntigravityIdeSubagent,
42134
+ meta: {
42135
+ supportsSimulated: false,
42136
+ supportsGlobal: true,
42137
+ filePattern: "*.md"
42138
+ }
42139
+ }],
42140
+ ["antigravity-plugin", {
42141
+ class: AntigravityPluginSubagent,
42142
+ meta: {
42143
+ supportsSimulated: false,
42144
+ supportsGlobal: false,
42145
+ filePattern: "*.md"
42146
+ }
42147
+ }],
40308
42148
  ["augmentcode", {
40309
42149
  class: AugmentcodeSubagent,
40310
42150
  meta: {
@@ -42518,6 +44358,7 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
42518
44358
  const CopilotRuleFrontmatterSchema = zod_mini.z.object({
42519
44359
  description: zod_mini.z.optional(zod_mini.z.string()),
42520
44360
  applyTo: zod_mini.z.optional(zod_mini.z.string()),
44361
+ name: zod_mini.z.optional(zod_mini.z.string()),
42521
44362
  excludeAgent: zod_mini.z.optional(zod_mini.z.union([
42522
44363
  zod_mini.z.literal("code-review"),
42523
44364
  zod_mini.z.literal("cloud-agent"),
@@ -42575,7 +44416,10 @@ var CopilotRule = class CopilotRule extends ToolRule {
42575
44416
  root: this.isRoot(),
42576
44417
  description: this.frontmatter.description,
42577
44418
  globs,
42578
- ...this.frontmatter.excludeAgent && { copilot: { excludeAgent: this.frontmatter.excludeAgent } }
44419
+ ...(this.frontmatter.excludeAgent || this.frontmatter.name) && { copilot: {
44420
+ ...this.frontmatter.excludeAgent && { excludeAgent: this.frontmatter.excludeAgent },
44421
+ ...this.frontmatter.name && { name: this.frontmatter.name }
44422
+ } }
42579
44423
  };
42580
44424
  const relativeFilePath = this.getRelativeFilePath().replace(/\.instructions\.md$/, ".md");
42581
44425
  return new RulesyncRule({
@@ -42594,7 +44438,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
42594
44438
  const copilotFrontmatter = {
42595
44439
  description: rulesyncFrontmatter.description,
42596
44440
  applyTo: rulesyncFrontmatter.globs?.length ? rulesyncFrontmatter.globs.join(",") : void 0,
42597
- excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent
44441
+ excludeAgent: rulesyncFrontmatter.copilot?.excludeAgent,
44442
+ name: rulesyncFrontmatter.copilot?.name
42598
44443
  };
42599
44444
  const body = rulesyncRule.getBody();
42600
44445
  if (root) return new CopilotRule({
@@ -43759,10 +45604,13 @@ var JunieRule = class JunieRule extends ToolRule {
43759
45604
  //#region src/features/rules/kilo-rule.ts
43760
45605
  var KiloRule = class KiloRule extends ToolRule {
43761
45606
  static getSettablePaths({ global, excludeToolDir } = {}) {
43762
- if (global) return { root: {
43763
- relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
43764
- relativeFilePath: KILO_RULE_FILE_NAME
43765
- } };
45607
+ if (global) return {
45608
+ root: {
45609
+ relativeDirPath: buildToolPath(KILO_GLOBAL_DIR, ".", excludeToolDir),
45610
+ relativeFilePath: KILO_RULE_FILE_NAME
45611
+ },
45612
+ nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
45613
+ };
43766
45614
  return {
43767
45615
  root: {
43768
45616
  relativeDirPath: ".",
@@ -45347,6 +47195,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45347
47195
  extension: "md",
45348
47196
  supportsGlobal: false,
45349
47197
  ruleDiscoveryMode: "toon",
47198
+ collisionPolicy: "compose",
45350
47199
  additionalConventions: {
45351
47200
  commands: { commandClass: AgentsmdCommand },
45352
47201
  subagents: { subagentClass: AgentsmdSubagent },
@@ -45367,7 +47216,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45367
47216
  meta: {
45368
47217
  extension: "md",
45369
47218
  supportsGlobal: true,
45370
- ruleDiscoveryMode: "toon"
47219
+ ruleDiscoveryMode: "toon",
47220
+ collisionPolicy: "compose"
45371
47221
  }
45372
47222
  }],
45373
47223
  ["antigravity-cli", {
@@ -45444,7 +47294,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45444
47294
  extension: "md",
45445
47295
  supportsGlobal: true,
45446
47296
  ruleDiscoveryMode: "auto",
45447
- foldsNonRootIntoRoot: true
47297
+ collisionPolicy: "fold"
45448
47298
  }
45449
47299
  }],
45450
47300
  ["copilot", {
@@ -45477,7 +47327,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45477
47327
  extension: "md",
45478
47328
  supportsGlobal: true,
45479
47329
  ruleDiscoveryMode: "auto",
45480
- foldsNonRootIntoRoot: true
47330
+ collisionPolicy: "fold"
45481
47331
  }
45482
47332
  }],
45483
47333
  ["factorydroid", {
@@ -45485,7 +47335,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45485
47335
  meta: {
45486
47336
  extension: "md",
45487
47337
  supportsGlobal: true,
45488
- ruleDiscoveryMode: "toon"
47338
+ ruleDiscoveryMode: "toon",
47339
+ collisionPolicy: "compose"
45489
47340
  }
45490
47341
  }],
45491
47342
  ["goose", {
@@ -45494,7 +47345,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45494
47345
  extension: "md",
45495
47346
  supportsGlobal: true,
45496
47347
  ruleDiscoveryMode: "auto",
45497
- foldsNonRootIntoRoot: true
47348
+ collisionPolicy: "fold"
45498
47349
  }
45499
47350
  }],
45500
47351
  ["hermesagent", {
@@ -45503,7 +47354,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45503
47354
  extension: "md",
45504
47355
  supportsGlobal: false,
45505
47356
  ruleDiscoveryMode: "auto",
45506
- foldsNonRootIntoRoot: true
47357
+ collisionPolicy: "fold"
45507
47358
  }
45508
47359
  }],
45509
47360
  ["grokcli", {
@@ -45520,7 +47371,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45520
47371
  extension: "md",
45521
47372
  supportsGlobal: true,
45522
47373
  ruleDiscoveryMode: "auto",
45523
- foldsNonRootIntoRoot: true
47374
+ collisionPolicy: "fold"
45524
47375
  }
45525
47376
  }],
45526
47377
  ["kilo", {
@@ -45529,7 +47380,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45529
47380
  extension: "md",
45530
47381
  supportsGlobal: true,
45531
47382
  ruleDiscoveryMode: "auto",
45532
- mcpInstructionsRegistrar: KiloMcp
47383
+ mcpInstructionsRegistrar: KiloMcp,
47384
+ collisionPolicy: "compose"
45533
47385
  }
45534
47386
  }],
45535
47387
  ["kimi-code", {
@@ -45538,7 +47390,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45538
47390
  extension: "md",
45539
47391
  supportsGlobal: true,
45540
47392
  ruleDiscoveryMode: "auto",
45541
- foldsNonRootIntoRoot: true
47393
+ collisionPolicy: "fold"
45542
47394
  }
45543
47395
  }],
45544
47396
  ["kiro", {
@@ -45571,7 +47423,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45571
47423
  extension: "md",
45572
47424
  supportsGlobal: true,
45573
47425
  ruleDiscoveryMode: "toon",
45574
- mcpInstructionsRegistrar: OpencodeMcp
47426
+ mcpInstructionsRegistrar: OpencodeMcp,
47427
+ collisionPolicy: "compose"
45575
47428
  }
45576
47429
  }],
45577
47430
  ["pi", {
@@ -45580,7 +47433,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45580
47433
  extension: "md",
45581
47434
  supportsGlobal: true,
45582
47435
  ruleDiscoveryMode: "auto",
45583
- foldsNonRootIntoRoot: true
47436
+ collisionPolicy: "fold"
45584
47437
  }
45585
47438
  }],
45586
47439
  ["qwencode", {
@@ -45598,7 +47451,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45598
47451
  extension: "md",
45599
47452
  supportsGlobal: true,
45600
47453
  ruleDiscoveryMode: "auto",
45601
- foldsNonRootIntoRoot: true
47454
+ collisionPolicy: "fold"
45602
47455
  }
45603
47456
  }],
45604
47457
  ["replit", {
@@ -45655,7 +47508,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
45655
47508
  extension: "md",
45656
47509
  supportsGlobal: false,
45657
47510
  ruleDiscoveryMode: "toon",
45658
- foldsNonRootIntoRoot: true
47511
+ collisionPolicy: "fold"
45659
47512
  }
45660
47513
  }],
45661
47514
  ["devin", {
@@ -45724,16 +47577,23 @@ var RulesProcessor = class extends FeatureProcessor {
45724
47577
  const nonLocalRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().localRoot);
45725
47578
  const factory = this.getFactory(this.toolTarget);
45726
47579
  const { meta } = factory;
45727
- const toolRules = nonLocalRootRules.map((rulesyncRule) => {
47580
+ const convertedRules = nonLocalRootRules.map((rulesyncRule) => {
45728
47581
  if (!factory.class.isTargetedByRulesyncRule(rulesyncRule)) return null;
45729
- return factory.class.fromRulesyncRule({
45730
- outputRoot: this.outputRoot,
45731
- rulesyncRule,
45732
- validate: true,
45733
- global: this.global
45734
- });
47582
+ return {
47583
+ toolRule: factory.class.fromRulesyncRule({
47584
+ outputRoot: this.outputRoot,
47585
+ rulesyncRule,
47586
+ validate: true,
47587
+ global: this.global
47588
+ }),
47589
+ rulesyncRule
47590
+ };
45735
47591
  }).filter((rule) => rule !== null);
45736
- if (meta.foldsNonRootIntoRoot) this.foldNonRootRulesIntoRootRule(toolRules);
47592
+ this.mergeRulesByOutputPath({
47593
+ convertedRules,
47594
+ collisionPolicy: meta.collisionPolicy ?? "preserve"
47595
+ });
47596
+ const toolRules = convertedRules.map(({ toolRule }) => toolRule);
45737
47597
  this.applyLocalRootRules({
45738
47598
  toolRules,
45739
47599
  localRootRules,
@@ -45751,7 +47611,12 @@ var RulesProcessor = class extends FeatureProcessor {
45751
47611
  toolRules,
45752
47612
  factory
45753
47613
  });
45754
- return [...toolRules, ...extraFiles];
47614
+ const outputFiles = [...toolRules, ...extraFiles];
47615
+ this.warnForOutputPathCollisions({
47616
+ outputFiles,
47617
+ convertedRules
47618
+ });
47619
+ return outputFiles;
45755
47620
  }
45756
47621
  /**
45757
47622
  * Handle localRoot rules (only in non-global mode and when enabled). Mutates
@@ -45838,39 +47703,80 @@ var RulesProcessor = class extends FeatureProcessor {
45838
47703
  });
45839
47704
  }
45840
47705
  /**
45841
- * Fold every non-root rule body into the single root rule file.
47706
+ * Reconcile rules that resolve to the same output path.
45842
47707
  *
45843
- * Used for tools whose rules engine reads only one root `AGENTS.md` and neither
45844
- * scans a `memories/` directory nor follows references (deepagents' dcode reads
45845
- * `.deepagents/AGENTS.md`; Warp reads root/subdir `AGENTS.md` but never
45846
- * `.warp/memories/`). Those rule classes emit both root and non-root rules to
45847
- * the same root path, so all bodies must be merged into one instance to avoid
45848
- * colliding on that path (last-writer-wins would silently drop content).
47708
+ * Multiple root fragments are composed for tools that emit a fixed root file.
47709
+ * The `fold` policy is for tools whose rules engine reads only one root file and
47710
+ * neither scans a modular rules directory nor follows references. For example,
47711
+ * dcode reads `.deepagents/AGENTS.md`, while Warp reads root or subdirectory
47712
+ * `AGENTS.md` files but never `.warp/memories/`. Those adapters must fold every
47713
+ * body into one instance because last-writer-wins would silently drop content.
47714
+ * Plain-Markdown adapters can opt into `compose` for colliding modular outputs.
45849
47715
  *
45850
- * The root rule (if any) becomes the merge target and leads the merged content;
45851
- * otherwise the first rule is used so a rule set without a root overview still
45852
- * produces a single, complete file. Mutates `toolRules` in place.
47716
+ * A generated root rule becomes the merge target when present. A `fold` group
47717
+ * without one uses its first rule. A group only composes when every rendered
47718
+ * fragment is plain Markdown — a fragment carrying its own frontmatter block
47719
+ * (e.g. Amp's `globs:` gate) would end up mid-body where the tool ignores it.
47720
+ * Root-involved collisions that cannot be composed safely fail; other
47721
+ * collisions remain separate and are reported by the final output-path check.
47722
+ * Mutates `convertedRules` in place.
45853
47723
  */
45854
- foldNonRootRulesIntoRootRule(toolRules) {
45855
- if (toolRules.length <= 1) return;
47724
+ mergeRulesByOutputPath({ convertedRules, collisionPolicy }) {
47725
+ if (convertedRules.length <= 1) return;
45856
47726
  const groups = /* @__PURE__ */ new Map();
45857
- for (const rule of toolRules) {
45858
- const path = (0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath());
47727
+ for (const conversion of convertedRules) {
47728
+ const path = (0, node_path.join)(conversion.toolRule.getRelativeDirPath(), conversion.toolRule.getRelativeFilePath());
45859
47729
  const group = groups.get(path);
45860
- if (group) group.push(rule);
45861
- else groups.set(path, [rule]);
47730
+ if (group) group.push(conversion);
47731
+ else groups.set(path, [conversion]);
45862
47732
  }
45863
47733
  const survivors = /* @__PURE__ */ new Set();
45864
- for (const group of groups.values()) {
45865
- const target = group.find((rule) => rule.isRoot()) ?? group[0];
47734
+ for (const [path, group] of groups) {
47735
+ if (group.length === 1) {
47736
+ const conversion = group[0];
47737
+ if (conversion) {
47738
+ if (collisionPolicy === "fold") conversion.toolRule.setFileContent(conversion.toolRule.getFileContent().trim());
47739
+ survivors.add(conversion);
47740
+ }
47741
+ continue;
47742
+ }
47743
+ const rootConversion = group.find(({ toolRule }) => toolRule.isRoot());
47744
+ const allGeneratedRulesAreRoots = group.every(({ toolRule }) => toolRule.isRoot());
47745
+ const hasSourceRoot = group.some(({ rulesyncRule }) => rulesyncRule.getFrontmatter().root === true);
47746
+ const allFragmentsArePlain = group.every(({ toolRule }) => !/^---\r?\n/.test(toolRule.getFileContent()));
47747
+ const shouldCompose = (collisionPolicy === "fold" || collisionPolicy === "compose" || allGeneratedRulesAreRoots) && allFragmentsArePlain;
47748
+ if (!shouldCompose && hasSourceRoot) throw new Error(`Multiple generated rules resolve to output path '${path}' for target '${this.toolTarget}', but this target cannot safely compose a collision involving a root rule. Source rules: ${formatRulePaths(group.map(({ rulesyncRule }) => rulesyncRule))}`);
47749
+ if (!shouldCompose) {
47750
+ for (const conversion of group) survivors.add(conversion);
47751
+ continue;
47752
+ }
47753
+ const target = rootConversion ?? group[0];
45866
47754
  if (!target) continue;
45867
- const mergedContent = [target, ...group.filter((rule) => rule !== target)].map((rule) => rule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
45868
- target.setFileContent(mergedContent);
47755
+ const mergedContent = [target, ...group.filter((rule) => rule !== target)].map(({ toolRule }) => toolRule.getFileContent().trim()).filter((content) => content.length > 0).join("\n\n");
47756
+ target.toolRule.setFileContent(mergedContent);
45869
47757
  survivors.add(target);
45870
47758
  }
45871
- for (let i = toolRules.length - 1; i >= 0; i--) {
45872
- const rule = toolRules[i];
45873
- if (rule && !survivors.has(rule)) toolRules.splice(i, 1);
47759
+ for (let i = convertedRules.length - 1; i >= 0; i--) {
47760
+ const conversion = convertedRules[i];
47761
+ if (conversion && !survivors.has(conversion)) convertedRules.splice(i, 1);
47762
+ }
47763
+ }
47764
+ warnForOutputPathCollisions({ outputFiles, convertedRules }) {
47765
+ const seen = /* @__PURE__ */ new Map();
47766
+ const describeSource = (file) => {
47767
+ const source = convertedRules.find(({ toolRule }) => toolRule === file)?.rulesyncRule;
47768
+ return source ? formatRulePaths([source]) : (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47769
+ };
47770
+ for (const file of outputFiles) {
47771
+ const path = (0, node_path.join)(file.getRelativeDirPath(), file.getRelativeFilePath());
47772
+ const key = path.toLowerCase();
47773
+ const previous = seen.get(key);
47774
+ if (previous) {
47775
+ const previousPath = (0, node_path.join)(previous.getRelativeDirPath(), previous.getRelativeFilePath());
47776
+ const pathDescription = previousPath === path ? `'${path}'` : `'${previousPath}' and '${path}' (compared case-insensitively, as on macOS and Windows)`;
47777
+ this.logger.warn(`Both ${describeSource(previous)} and ${describeSource(file)} generate to ${pathDescription}; the last one wins wherever they collide.`);
47778
+ }
47779
+ seen.set(key, file);
45874
47780
  }
45875
47781
  }
45876
47782
  /**
@@ -46030,14 +47936,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
46030
47936
  }));
46031
47937
  const factory = this.getFactory(this.toolTarget);
46032
47938
  const targetedRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().root).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
46033
- if (targetedRootRules.length > 1) throw new Error(`Multiple root rulesync rules found for target '${this.toolTarget}': ${formatRulePaths(targetedRootRules)}`);
46034
47939
  if (targetedRootRules.length === 0 && rulesyncRules.length > 0) this.logger.warn(`No root rulesync rule file found for target '${this.toolTarget}'. Consider adding 'root: true' to one of your rule files in ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`);
46035
47940
  const targetedLocalRootRules = rulesyncRules.filter((rule) => rule.getFrontmatter().localRoot).filter((rule) => factory.class.isTargetedByRulesyncRule(rule));
46036
47941
  if (targetedLocalRootRules.length > 1) throw new Error(`Multiple localRoot rules found for target '${this.toolTarget}': ${formatRulePaths(targetedLocalRootRules)}. Only one rule can have localRoot: true`);
46037
47942
  if (targetedLocalRootRules.length > 0 && targetedRootRules.length === 0) throw new Error(`localRoot: true requires a root: true rule to exist for target '${this.toolTarget}' (found in ${formatRulePaths(targetedLocalRootRules)})`);
46038
47943
  if (this.global) {
46039
47944
  const globalPaths = factory.class.getSettablePaths({ global: true });
46040
- const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.foldsNonRootIntoRoot === true;
47945
+ const supportsGlobalNonRoot = "nonRoot" in globalPaths && globalPaths.nonRoot !== null || factory.meta.supportsGlobal && factory.meta.collisionPolicy === "fold";
46041
47946
  const nonRootRules = rulesyncRules.filter((rule) => !rule.getFrontmatter().root && !rule.getFrontmatter().localRoot && factory.class.isTargetedByRulesyncRule(rule));
46042
47947
  if (nonRootRules.length > 0 && !supportsGlobalNonRoot) this.logger.warn(`${nonRootRules.length} non-root rulesync rules found, but it's in global mode, so ignoring them: ${formatRulePaths(nonRootRules)}`);
46043
47948
  if (targetedLocalRootRules.length > 0) this.logger.warn(`${targetedLocalRootRules.length} localRoot rules found, but localRoot is not supported in global mode, ignoring them: ${formatRulePaths(targetedLocalRootRules)}`);
@@ -46280,14 +48185,36 @@ async function assertPluginRootSafe(params) {
46280
48185
  }
46281
48186
  //#endregion
46282
48187
  //#region src/utils/tool-output-root.ts
48188
+ /** The environment variable each tool reads for its profile root. */
48189
+ const TOOL_HOME_ENV_VARS = {
48190
+ hermesagent: "HERMES_HOME",
48191
+ "kimi-code": "KIMI_CODE_HOME"
48192
+ };
48193
+ /**
48194
+ * Substitute a tool's home override (`HERMES_HOME`, `KIMI_CODE_HOME`) for the
48195
+ * output root in global scope.
48196
+ *
48197
+ * The override wins over `--output-roots`: it names where the tool itself reads
48198
+ * its profile, so writing anywhere else would produce files the tool ignores.
48199
+ *
48200
+ * A substituted value goes through the same `validateOutputRoot` the CLI and
48201
+ * config paths use, so an override of `/` or an unnormalized path is rejected
48202
+ * instead of silently becoming the output root. The rejection is re-thrown
48203
+ * naming the variable, since the user never passed an `--output-roots` flag.
48204
+ */
46283
48205
  function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
46284
48206
  if (!global) return outputRoot;
46285
- if (toolTarget === "hermesagent") return resolveHermesagentOutputRoot({
48207
+ const resolved = toolTarget === "hermesagent" ? resolveHermesagentOutputRoot({
46286
48208
  outputRoot,
46287
48209
  global
46288
- });
46289
- if (toolTarget === "kimi-code") return getKimiCodeHome() ?? outputRoot;
46290
- return outputRoot;
48210
+ }) : toolTarget === "kimi-code" ? getKimiCodeHome() ?? outputRoot : outputRoot;
48211
+ if (resolved === outputRoot) return resolved;
48212
+ try {
48213
+ validateOutputRoot(resolved);
48214
+ } catch (error) {
48215
+ throw new Error(`${TOOL_HOME_ENV_VARS[toolTarget] ?? "The tool home override"} is not a usable output root: ${formatError(error)}`, { cause: error });
48216
+ }
48217
+ return resolved;
46291
48218
  }
46292
48219
  //#endregion
46293
48220
  //#region src/lib/convert.ts
@@ -47688,7 +49615,11 @@ async function generateChecksCore(params) {
47688
49615
  for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
47689
49616
  if (!config.getFeatures(toolTarget).includes("checks")) continue;
47690
49617
  const processor = new ChecksProcessor({
47691
- outputRoot,
49618
+ outputRoot: resolveToolOutputRoot({
49619
+ outputRoot,
49620
+ toolTarget,
49621
+ global: config.getGlobal()
49622
+ }),
47692
49623
  inputRoot: config.getInputRoot(),
47693
49624
  toolTarget,
47694
49625
  global: config.getGlobal(),