rulesync 16.3.0 → 16.5.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.
@@ -15,7 +15,7 @@ var __copyProps = (to, from, except, desc) => {
15
15
  }
16
16
  return to;
17
17
  };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
19
  value: mod,
20
20
  enumerable: true
21
21
  }) : target, mod));
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
134
134
  };
135
135
  //#endregion
136
136
  //#region src/constants/rulesync-paths.ts
137
- const { join: join$281 } = node_path.posix;
137
+ const { join: join$282 } = node_path.posix;
138
138
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
139
139
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
140
140
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
141
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$281(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
- join$281(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
141
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "rules");
142
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$282(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
143
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "commands");
144
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "subagents");
145
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "checks");
146
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
147
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
148
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
149
+ join$282(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
150
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
151
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
152
152
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
153
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
153
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
154
154
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
155
155
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
156
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$281(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
156
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$282(RULESYNC_RELATIVE_DIR_PATH, "skills");
157
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$282(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
158
158
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
159
159
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
160
160
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -166,7 +166,7 @@ const RULESYNC_PERMISSIONS_LEGACY_FILE_NAME = "permissions.json";
166
166
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
167
167
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
168
168
  const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
169
- const MAX_FILE_SIZE = 10 * 1024 * 1024;
169
+ const MAX_FILE_SIZE = 10485760;
170
170
  //#endregion
171
171
  //#region src/types/tool-target-tuples.ts
172
172
  const rulesProcessorToolTargetTuple = [
@@ -207,7 +207,8 @@ const rulesProcessorToolTargetTuple = [
207
207
  "vibe",
208
208
  "warp",
209
209
  "devin",
210
- "zed"
210
+ "zed",
211
+ "zoocode"
211
212
  ];
212
213
  const ignoreProcessorToolTargetTuple = [
213
214
  "aiassistant",
@@ -229,7 +230,8 @@ const ignoreProcessorToolTargetTuple = [
229
230
  "devin",
230
231
  "vibe",
231
232
  "warp",
232
- "zed"
233
+ "zed",
234
+ "zoocode"
233
235
  ];
234
236
  const mcpProcessorToolTargetTuple = [
235
237
  "aiassistant",
@@ -266,7 +268,8 @@ const mcpProcessorToolTargetTuple = [
266
268
  "vibe",
267
269
  "warp",
268
270
  "devin",
269
- "zed"
271
+ "zed",
272
+ "zoocode"
270
273
  ];
271
274
  const commandsProcessorToolTargetTuple = [
272
275
  "agentsmd",
@@ -294,6 +297,7 @@ const commandsProcessorToolTargetTuple = [
294
297
  "qwencode",
295
298
  "reasonix",
296
299
  "roo",
300
+ "zoocode",
297
301
  "rovodev",
298
302
  "takt",
299
303
  "devin",
@@ -329,6 +333,7 @@ const subagentsProcessorToolTargetTuple = [
329
333
  "qwencode",
330
334
  "reasonix",
331
335
  "roo",
336
+ "zoocode",
332
337
  "rovodev",
333
338
  "takt",
334
339
  "vibe"
@@ -372,7 +377,8 @@ const skillsProcessorToolTargetTuple = [
372
377
  "vibe",
373
378
  "warp",
374
379
  "devin",
375
- "zed"
380
+ "zed",
381
+ "zoocode"
376
382
  ];
377
383
  const hooksProcessorToolTargetTuple = [
378
384
  "amp",
@@ -685,7 +691,8 @@ async function findFilesByGlobs(globs, options = {}) {
685
691
  onlyFiles: false,
686
692
  onlyDirectories: false
687
693
  };
688
- const results = (0, globby.globbySync)(Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/"), {
694
+ const normalizedGlobs = Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/");
695
+ const results = (0, globby.globbySync)(normalizedGlobs, {
689
696
  absolute: true,
690
697
  followSymbolicLinks,
691
698
  ...ignore ? { ignore: ignore.map((pattern) => pattern.replaceAll("\\", "/")) } : {},
@@ -796,7 +803,8 @@ function validateOutputRoot(outputRoot) {
796
803
  function toKebabCaseFilename(filename) {
797
804
  const lastDotIndex = filename.lastIndexOf(".");
798
805
  const extension = lastDotIndex > 0 ? filename.slice(lastDotIndex) : "";
799
- return (0, es_toolkit.kebabCase)(lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename) + extension;
806
+ const nameWithoutExt = lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename;
807
+ return (0, es_toolkit.kebabCase)(nameWithoutExt) + extension;
800
808
  }
801
809
  /**
802
810
  * Create a temporary directory atomically and return its path.
@@ -1458,7 +1466,8 @@ const getDefaults = () => ({
1458
1466
  });
1459
1467
  const loadConfigFromFile = async (filePath) => {
1460
1468
  if (!await fileExists(filePath)) return {};
1461
- const jsonData = (0, jsonc_parser.parse)(await readFileContent(filePath));
1469
+ const fileContent = await readFileContent(filePath);
1470
+ const jsonData = (0, jsonc_parser.parse)(fileContent);
1462
1471
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
1463
1472
  assertTargetsFeaturesExclusive({
1464
1473
  targets: configParams.targets,
@@ -1548,7 +1557,8 @@ var ConfigResolver = class {
1548
1557
  if (inputRoot !== void 0) validateOutputRoot(inputRoot);
1549
1558
  const validatedConfigPath = resolvePath(configPath, (0, node_path.resolve)(inputRoot ?? cwd));
1550
1559
  const baseConfig = await loadConfigFromFile(validatedConfigPath);
1551
- const localConfigPath = (0, node_path.join)((0, node_path.dirname)(validatedConfigPath), RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
1560
+ const configDir = (0, node_path.dirname)(validatedConfigPath);
1561
+ const localConfigPath = (0, node_path.join)(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
1552
1562
  const localConfig = await loadConfigFromFile(localConfigPath);
1553
1563
  const configByFile = mergeConfigs(baseConfig, localConfig);
1554
1564
  if (inputRoot === void 0 && configByFile.inputRoot !== void 0) validateOutputRoot(configByFile.inputRoot);
@@ -1713,6 +1723,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
1713
1723
  ".devin/config.json",
1714
1724
  ".factory/settings.json",
1715
1725
  ".grok/config.toml",
1726
+ ".rovodev/config.yml",
1727
+ ".rovodev/mcp.json",
1716
1728
  ".vibe/config.toml",
1717
1729
  "reasonix.toml",
1718
1730
  ".vscode/settings.json",
@@ -1782,7 +1794,8 @@ var AiFile = class {
1782
1794
  getFilePath() {
1783
1795
  const fullPath = node_path.default.join(this.outputRoot, this.relativeDirPath, this.relativeFilePath);
1784
1796
  const resolvedFull = (0, node_path.resolve)(fullPath);
1785
- const rel = (0, node_path.relative)((0, node_path.resolve)(this.outputRoot), resolvedFull);
1797
+ const resolvedBase = (0, node_path.resolve)(this.outputRoot);
1798
+ const rel = (0, node_path.relative)(resolvedBase, resolvedFull);
1786
1799
  if (rel.startsWith("..") || node_path.default.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", relativeFilePath="${this.relativeFilePath}"`);
1787
1800
  return fullPath;
1788
1801
  }
@@ -2350,6 +2363,7 @@ const OPENCODE_HOOK_EVENTS = [
2350
2363
  "postToolUse",
2351
2364
  "stop",
2352
2365
  "afterFileEdit",
2366
+ "beforeShellExecution",
2353
2367
  "afterShellExecution",
2354
2368
  "permissionRequest",
2355
2369
  "preCompact",
@@ -2381,6 +2395,7 @@ const PI_HOOK_EVENTS = [
2381
2395
  "preToolUse",
2382
2396
  "postToolUse",
2383
2397
  "preModelInvocation",
2398
+ "postModelInvocation",
2384
2399
  "beforeSubmitPrompt",
2385
2400
  "stop",
2386
2401
  "preCompact",
@@ -3054,7 +3069,6 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
3054
3069
  postToolUse: "tool.execute.after",
3055
3070
  stop: "session.idle",
3056
3071
  afterFileEdit: "file.edited",
3057
- afterShellExecution: "command.executed",
3058
3072
  permissionRequest: "permission.asked",
3059
3073
  preCompact: "experimental.session.compacting",
3060
3074
  postCompact: "session.compacted",
@@ -3087,6 +3101,7 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
3087
3101
  preToolUse: "tool_call",
3088
3102
  postToolUse: "tool_result",
3089
3103
  preModelInvocation: "context",
3104
+ postModelInvocation: "message_end",
3090
3105
  beforeSubmitPrompt: "input",
3091
3106
  stop: "agent_end",
3092
3107
  preCompact: "session_before_compact",
@@ -4120,10 +4135,22 @@ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
4120
4135
  * category; the shared `permission` block still drives the base permission and
4121
4136
  * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
4122
4137
  *
4138
+ * Vibe's top-level `enabled_tools` is an **exclusive** allowlist — when set,
4139
+ * only the listed tools (name or pattern) are active and every other builtin
4140
+ * and MCP tool is off. That narrowing cannot be derived from canonical `allow`
4141
+ * rules (an allow grants one tool without revoking the rest), so the list is
4142
+ * only ever authored explicitly here, verbatim in Vibe's tool-name vocabulary,
4143
+ * and round-trips through this override on import.
4144
+ *
4123
4145
  * @example
4124
4146
  * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
4147
+ * @example
4148
+ * { "enabled_tools": ["bash", "read_file", "grep"] }
4125
4149
  */
4126
- const VibePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({ sensitive_patterns: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))) });
4150
+ const VibePermissionsOverrideSchema = zod_mini.z.looseObject({
4151
+ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({ sensitive_patterns: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))),
4152
+ enabled_tools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4153
+ });
4127
4154
  /**
4128
4155
  * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
4129
4156
  * autonomy settings with no canonical permission category — `approvalMode`
@@ -4195,7 +4222,10 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
4195
4222
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
4196
4223
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4197
4224
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
4198
- agent: zod_mini.z.optional(zod_mini.z.looseObject({}))
4225
+ agent: zod_mini.z.optional(zod_mini.z.looseObject({})),
4226
+ rawAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4227
+ rawAsk: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4228
+ rawDeny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4199
4229
  });
4200
4230
  /**
4201
4231
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -4226,12 +4256,69 @@ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
4226
4256
  * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
4227
4257
  * Fields placed here are merged into `[agents.profiles]` of Warp's global
4228
4258
  * `settings.toml`, while the shared `permission` block continues to drive the
4229
- * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
4230
- * Warp permissions are global-only.
4259
+ * command regex arrays (legacy `agent_mode_command_execution_allowlist`/
4260
+ * `_denylist` plus the `default` execution profile's `command_allowlist`/
4261
+ * `command_denylist`). On migrated installs the `[agents.profiles]` keys are
4262
+ * inert; their execution-profile counterparts are authored via the nested
4263
+ * `execution_profile` block instead, which is merged into the `default` record
4264
+ * of `[agents.execution_profiles.<id>]`. Warp permissions are global-only.
4231
4265
  *
4232
4266
  * @example
4233
- * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
4267
+ * {
4268
+ * "agent_mode_coding_permissions": "always_allow_reading",
4269
+ * "agent_mode_execute_readonly_commands": true,
4270
+ * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
4271
+ * }
4272
+ */
4273
+ /**
4274
+ * Per-action autonomy value of Warp's execution profiles
4275
+ * (`FileActionPermission` in the Warp repository's
4276
+ * `app/src/ai/execution_profiles/config.rs`, serialized snake_case).
4234
4277
  */
4278
+ const WarpFileActionPermissionSchema = zod_mini.z.enum([
4279
+ "agent_decides",
4280
+ "always_allow",
4281
+ "always_ask"
4282
+ ]);
4283
+ /**
4284
+ * Permission keys of the `default` record in Warp's
4285
+ * `[agents.execution_profiles.<id>]` collection (the surface runtime
4286
+ * enforcement reads on migrated installs). Loose so forward-compat keys pass
4287
+ * through verbatim. The rulesync-owned `command_allowlist`/`command_denylist`
4288
+ * are driven by the shared `permission.bash` block and always win over values
4289
+ * placed here.
4290
+ *
4291
+ * @see https://github.com/warpdotdev/warp/blob/main/app/src/ai/execution_profiles/config.rs (`ExecutionProfileFile`)
4292
+ */
4293
+ const WarpExecutionProfileOverrideSchema = zod_mini.z.looseObject({
4294
+ read_files: zod_mini.z.optional(WarpFileActionPermissionSchema),
4295
+ apply_code_diffs: zod_mini.z.optional(WarpFileActionPermissionSchema),
4296
+ execute_commands: zod_mini.z.optional(WarpFileActionPermissionSchema),
4297
+ mcp_permissions: zod_mini.z.optional(WarpFileActionPermissionSchema),
4298
+ write_to_pty: zod_mini.z.optional(zod_mini.z.enum([
4299
+ "always_allow",
4300
+ "always_ask",
4301
+ "ask_on_first_write"
4302
+ ])),
4303
+ ask_user_question: zod_mini.z.optional(zod_mini.z.enum([
4304
+ "never",
4305
+ "ask_except_in_auto_approve",
4306
+ "always_ask"
4307
+ ])),
4308
+ run_agents: zod_mini.z.optional(zod_mini.z.enum([
4309
+ "never_allow",
4310
+ "always_allow",
4311
+ "always_ask"
4312
+ ])),
4313
+ computer_use: zod_mini.z.optional(zod_mini.z.enum([
4314
+ "never",
4315
+ "always_ask",
4316
+ "always_allow"
4317
+ ])),
4318
+ directory_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4319
+ mcp_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4320
+ mcp_denylist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
4321
+ });
4235
4322
  const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
4236
4323
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4237
4324
  agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.enum([
@@ -4240,7 +4327,8 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
4240
4327
  "allow_reading_specific_files"
4241
4328
  ])),
4242
4329
  agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4243
- agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean())
4330
+ agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean()),
4331
+ execution_profile: zod_mini.z.optional(WarpExecutionProfileOverrideSchema)
4244
4332
  });
4245
4333
  /**
4246
4334
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -4972,7 +5060,8 @@ var AiDir = class {
4972
5060
  getDirPath() {
4973
5061
  const fullPath = node_path.default.join(this.outputRoot, this.relativeDirPath, this.dirName);
4974
5062
  const resolvedFull = (0, node_path.resolve)(fullPath);
4975
- const rel = (0, node_path.relative)((0, node_path.resolve)(this.outputRoot), resolvedFull);
5063
+ const resolvedBase = (0, node_path.resolve)(this.outputRoot);
5064
+ const rel = (0, node_path.relative)(resolvedBase, resolvedFull);
4976
5065
  if (rel.startsWith("..") || node_path.default.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", dirName="${this.dirName}"`);
4977
5066
  return fullPath;
4978
5067
  }
@@ -5255,6 +5344,9 @@ const RulesyncSubagentFrontmatterSchema = zod_mini.z.looseObject({
5255
5344
  customInstructions: zod_mini.z.optional(zod_mini.z.string()),
5256
5345
  groups: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown()))
5257
5346
  })),
5347
+ zoocode: zod_mini.z.optional(zod_mini.z.looseObject({
5348
+ /** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
5349
+ allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
5258
5350
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({
5259
5351
  agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
5260
5352
  display_name: zod_mini.z.optional(zod_mini.z.string()),
@@ -5394,7 +5486,6 @@ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
5394
5486
  const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
5395
5487
  const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
5396
5488
  const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5397
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5398
5489
  const ROVODEV_MCP_FILE_NAME = "mcp.json";
5399
5490
  const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5400
5491
  const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
@@ -6613,6 +6704,7 @@ const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6613
6704
  const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6614
6705
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6615
6706
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6707
+ const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
6616
6708
  /**
6617
6709
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
6618
6710
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -6852,24 +6944,14 @@ const SHARED_CONFIG_OWNERSHIP = {
6852
6944
  },
6853
6945
  ".devin/config.json": {
6854
6946
  format: "json",
6855
- features: {
6856
- mcp: {
6857
- kind: "replace-owned-keys",
6858
- ownedKeys: ["mcpServers"]
6859
- },
6860
- permissions: {
6861
- kind: "replace-owned-keys",
6862
- ownedKeys: ["permissions"]
6863
- }
6864
- }
6947
+ features: { permissions: {
6948
+ kind: "replace-owned-keys",
6949
+ ownedKeys: ["permissions"]
6950
+ } }
6865
6951
  },
6866
6952
  ".config/devin/config.json": {
6867
6953
  format: "json",
6868
6954
  features: {
6869
- mcp: {
6870
- kind: "replace-owned-keys",
6871
- ownedKeys: ["mcpServers"]
6872
- },
6873
6955
  hooks: {
6874
6956
  kind: "replace-owned-keys",
6875
6957
  ownedKeys: ["hooks"]
@@ -6962,6 +7044,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6962
7044
  permissions: {
6963
7045
  kind: "replace-owned-keys",
6964
7046
  ownedKeys: ["permission"]
7047
+ },
7048
+ rules: {
7049
+ kind: "replace-owned-keys",
7050
+ ownedKeys: ["instructions"]
6965
7051
  }
6966
7052
  }
6967
7053
  },
@@ -7079,6 +7165,19 @@ const SHARED_CONFIG_OWNERSHIP = {
7079
7165
  ]
7080
7166
  }
7081
7167
  }
7168
+ },
7169
+ [ROVODEV_CONFIG_SHARED_FILE_KEY]: {
7170
+ format: "yaml",
7171
+ features: {
7172
+ mcp: {
7173
+ kind: "replace-owned-keys",
7174
+ ownedKeys: ["mcp"]
7175
+ },
7176
+ permissions: {
7177
+ kind: "replace-owned-keys",
7178
+ ownedKeys: ["toolPermissions"]
7179
+ }
7180
+ }
7082
7181
  }
7083
7182
  };
7084
7183
  /**
@@ -7500,7 +7599,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7500
7599
  class: CursorCheck,
7501
7600
  meta: {
7502
7601
  supportsGlobal: false,
7503
- filePattern: CURSOR_BUGBOT_FILE_NAME
7602
+ filePattern: CURSOR_BUGBOT_FILE_NAME,
7603
+ committedOutput: true
7504
7604
  }
7505
7605
  }],
7506
7606
  ["hermesagent", {
@@ -7514,7 +7614,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7514
7614
  class: RovodevCheck,
7515
7615
  meta: {
7516
7616
  supportsGlobal: false,
7517
- filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7617
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME,
7618
+ committedOutput: true
7518
7619
  }
7519
7620
  }],
7520
7621
  ["takt", {
@@ -9069,6 +9170,7 @@ const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
9069
9170
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
9070
9171
  const DEVIN_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
9071
9172
  const DEVIN_CONFIG_FILE_NAME = "config.json";
9173
+ const DEVIN_MCP_CONFIG_FILE_NAME = "mcp_config.json";
9072
9174
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9073
9175
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9074
9176
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
@@ -9675,7 +9777,8 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9675
9777
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9676
9778
  const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
9677
9779
  if (commandNames.size === 0) return;
9678
- const shadowed = (await findFilesByGlobs((0, node_path.join)((0, node_path.join)(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
9780
+ const skillsRoot = (0, node_path.join)(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH);
9781
+ const shadowed = (await findFilesByGlobs((0, node_path.join)(skillsRoot, "**", "SKILL.md"))).map((filePath) => (0, node_path.basename)((0, node_path.dirname)(filePath))).filter((skillName) => commandNames.has(skillName));
9679
9782
  if (shadowed.length > 0) logger.warn(`Grok CLI resolves skills before commands, so these skills shadow the same-named commands, which will never be reachable: ${[...new Set(shadowed)].toSorted().join(", ")}. Rename either side to make both invocable.`);
9680
9783
  }
9681
9784
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -9710,6 +9813,20 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9710
9813
  function toolSkillSearchRoots(paths) {
9711
9814
  return [paths.relativeDirPath, ...paths.alternativeSkillRoots ?? []];
9712
9815
  }
9816
+ /**
9817
+ * The Agent Skills interoperability roots: project `.agents/skills/`, global
9818
+ * `~/.agents/skills/` (same relative path, home output root), and Amp's global
9819
+ * equivalent `~/.config/agents/skills/`. Foreign-authored — potentially
9820
+ * non-conformant — skills are the most likely to live here, so every tool that
9821
+ * scans one of these roots imports it leniently (skip-and-warn per skill), per
9822
+ * the Agent Skills client-implementation guide, while keeping fail-fast
9823
+ * behavior for its own native root.
9824
+ * https://agentskills.io/client-implementation/adding-skills-support
9825
+ */
9826
+ const AGENT_SKILLS_INTEROP_ROOTS = /* @__PURE__ */ new Set([toPosixPath(AGENTSMD_SKILLS_DIR_PATH), toPosixPath(AMP_SKILLS_GLOBAL_DIR)]);
9827
+ function isAgentSkillsInteropRoot(relativeDirPath) {
9828
+ return AGENT_SKILLS_INTEROP_ROOTS.has(toPosixPath(relativeDirPath));
9829
+ }
9713
9830
  /** Ordered import roots: managed roots first, followed by read-only discovery roots. */
9714
9831
  function toolSkillImportRoots(paths) {
9715
9832
  return [...toolSkillSearchRoots(paths), ...paths.importOnlySkillRoots ?? []];
@@ -11326,6 +11443,7 @@ const QWENCODE_COMMANDS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "commands")
11326
11443
  const QWENCODE_AGENTS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "agents");
11327
11444
  const QWENCODE_SKILLS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "skills");
11328
11445
  const QWENCODE_RULE_FILE_NAME = "QWEN.md";
11446
+ const QWENCODE_LOCAL_RULE_FILE_NAME = "QWEN.local.md";
11329
11447
  const QWENCODE_IGNORE_FILE_NAME = ".qwenignore";
11330
11448
  const QWENCODE_SETTINGS_FILE_NAME = "settings.json";
11331
11449
  //#endregion
@@ -11652,7 +11770,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11652
11770
  };
11653
11771
  const body = rulesyncCommand.getBody();
11654
11772
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
11655
- return new RooCommand({
11773
+ return new this({
11656
11774
  outputRoot,
11657
11775
  frontmatter: rooFrontmatter,
11658
11776
  body,
@@ -11689,7 +11807,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11689
11807
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
11690
11808
  const result = RooCommandFrontmatterSchema.safeParse(frontmatter);
11691
11809
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
11692
- return new RooCommand({
11810
+ return new this({
11693
11811
  outputRoot,
11694
11812
  relativeDirPath: RooCommand.getSettablePaths().relativeDirPath,
11695
11813
  relativeFilePath,
@@ -11700,7 +11818,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11700
11818
  });
11701
11819
  }
11702
11820
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
11703
- return new RooCommand({
11821
+ return new this({
11704
11822
  outputRoot,
11705
11823
  relativeDirPath,
11706
11824
  relativeFilePath,
@@ -12072,6 +12190,7 @@ const WARP_SKILLS_DIR_PATH = (0, node_path.join)(WARP_DIR, "skills");
12072
12190
  const WARP_LINUX_DIR = (0, node_path.join)(".config", "warp-terminal");
12073
12191
  const WARP_WIN32_DIR = (0, node_path.join)("AppData", "Local", "warp", "Warp", "config");
12074
12192
  const WARP_RULE_FILE_NAME = "AGENTS.md";
12193
+ const WARP_GLOBAL_RULE_DIR = ".agents";
12075
12194
  const WARP_MCP_FILE_NAME = ".mcp.json";
12076
12195
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
12077
12196
  const WARP_IGNORE_FILE_NAME = ".warpindexingignore";
@@ -12157,6 +12276,39 @@ var WarpCommand = class WarpCommand extends ToolCommand {
12157
12276
  }
12158
12277
  };
12159
12278
  //#endregion
12279
+ //#region src/features/commands/zoocode-command.ts
12280
+ /**
12281
+ * Command generator for **Zoo Code** (the community continuation of Roo Code).
12282
+ * Zoo Code keeps Roo's `.roo/commands/` layout, so this target reuses
12283
+ * {@link RooCommand} verbatim and only narrows the targeting.
12284
+ *
12285
+ * @see https://docs.zoocode.dev
12286
+ */
12287
+ var ZoocodeCommand = class extends RooCommand {
12288
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
12289
+ return this.isTargetedByRulesyncCommandDefault({
12290
+ rulesyncCommand,
12291
+ toolTarget: "zoocode"
12292
+ });
12293
+ }
12294
+ toRulesyncCommand() {
12295
+ const command = super.toRulesyncCommand();
12296
+ const frontmatter = {
12297
+ ...command.getFrontmatter(),
12298
+ targets: ["zoocode"]
12299
+ };
12300
+ return new RulesyncCommand({
12301
+ outputRoot: command.getOutputRoot(),
12302
+ relativeDirPath: command.getRelativeDirPath(),
12303
+ relativeFilePath: command.getRelativeFilePath(),
12304
+ frontmatter,
12305
+ body: command.getBody(),
12306
+ fileContent: stringifyFrontmatter(command.getBody(), frontmatter),
12307
+ validate: true
12308
+ });
12309
+ }
12310
+ };
12311
+ //#endregion
12160
12312
  //#region src/features/commands/commands-processor.ts
12161
12313
  const CommandsProcessorToolTargetSchema = zod_mini.z.enum(commandsProcessorToolTargetTuple);
12162
12314
  /**
@@ -12416,6 +12568,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
12416
12568
  supportsSubdirectory: true
12417
12569
  }
12418
12570
  }],
12571
+ ["zoocode", {
12572
+ class: ZoocodeCommand,
12573
+ meta: {
12574
+ extension: "md",
12575
+ supportsProject: true,
12576
+ supportsGlobal: true,
12577
+ isSimulated: false,
12578
+ supportsSubdirectory: true
12579
+ }
12580
+ }],
12419
12581
  ["rovodev", {
12420
12582
  class: RovodevCommand,
12421
12583
  meta: {
@@ -13795,16 +13957,17 @@ var ClaudecodeHooks = class extends ToolHooks {
13795
13957
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13796
13958
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
13797
13959
  const config = rulesyncHooks.getJson();
13960
+ const claudeHooks = canonicalToToolHooks({
13961
+ config,
13962
+ toolOverrideHooks: config.claudecode?.hooks,
13963
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
13964
+ logger
13965
+ });
13798
13966
  const fileContent = applySharedConfigPatch({
13799
13967
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
13800
13968
  feature: "hooks",
13801
13969
  existingContent,
13802
- patch: { hooks: canonicalToToolHooks({
13803
- config,
13804
- toolOverrideHooks: config.claudecode?.hooks,
13805
- converterConfig: CLAUDE_CONVERTER_CONFIG,
13806
- logger
13807
- }) },
13970
+ patch: { hooks: claudeHooks },
13808
13971
  filePath
13809
13972
  });
13810
13973
  return new this({
@@ -14807,9 +14970,9 @@ const DEVIN_CONVERTER_CONFIG = {
14807
14970
  * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
14808
14971
  * level IS the event map (no wrapper key).
14809
14972
  * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
14810
- * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
14811
- * features, so reads and writes merge into the existing JSON and the file is
14812
- * never deleted in global mode.
14973
+ * file is shared with the permissions (`permissions`) feature (MCP moved to
14974
+ * the dedicated mcp_config.json in v3000.3), so reads and writes merge into
14975
+ * the existing JSON and the file is never deleted in global mode.
14813
14976
  *
14814
14977
  * @see https://docs.devin.ai/cli/extensibility/hooks/overview
14815
14978
  */
@@ -15558,6 +15721,30 @@ const NAMED_HOOK_MATCHER_SUBJECTS = {
15558
15721
  "tool.execute.after": "input.tool",
15559
15722
  "experimental.session.compacting": null
15560
15723
  };
15724
+ /**
15725
+ * OpenCode (and Kilo) have no shell-execution lifecycle event — the
15726
+ * `command.executed` event these canonical events were once mapped to is a
15727
+ * *slash-command* event, so a hook wired there never fired on bash commands
15728
+ * and fired on every slash command instead. Observing a shell invocation is
15729
+ * done with the named `tool.execute.before/after` hooks gated on the `bash`
15730
+ * tool (the `.env protection` pattern in the OpenCode plugins doc), so these
15731
+ * canonical events are emitted into those named hooks with an implicit
15732
+ * `input.tool === "bash"` gate. Matchers on them are dropped upstream with a
15733
+ * warning (`matcherEvents` covers only `preToolUse`/`postToolUse`) — the
15734
+ * named hooks expose no command text to match against.
15735
+ *
15736
+ * @see https://opencode.ai/docs/plugins/
15737
+ */
15738
+ const SHELL_EVENT_TOOL_GATES = {
15739
+ beforeShellExecution: {
15740
+ toolEvent: "tool.execute.before",
15741
+ tool: "bash"
15742
+ },
15743
+ afterShellExecution: {
15744
+ toolEvent: "tool.execute.after",
15745
+ tool: "bash"
15746
+ }
15747
+ };
15561
15748
  function escapeForTemplateLiteral(command) {
15562
15749
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
15563
15750
  }
@@ -15578,15 +15765,19 @@ function validateAndSanitizeMatcher(matcher) {
15578
15765
  */
15579
15766
  function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
15580
15767
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
15581
- const toolEvent = eventMap[canonicalEvent];
15768
+ const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
15769
+ const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
15582
15770
  if (!toolEvent) continue;
15771
+ const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
15583
15772
  const handlers = [];
15584
15773
  for (const def of definitions) {
15585
15774
  if ((def.type ?? "command") !== "command") continue;
15586
15775
  if (!def.command) continue;
15776
+ if (def.matcher && !matcherSupported) continue;
15587
15777
  handlers.push({
15588
15778
  command: def.command,
15589
- matcher: def.matcher ? def.matcher : void 0
15779
+ matcher: def.matcher ? def.matcher : void 0,
15780
+ ...shellGate ? { toolGate: shellGate.tool } : {}
15590
15781
  });
15591
15782
  }
15592
15783
  if (handlers.length > 0) {
@@ -15623,7 +15814,11 @@ function buildNamedEventBodyLines(namedEventHandlers) {
15623
15814
  bodyLines.push(` "${eventName}": async (input) => {`);
15624
15815
  for (const handler of handlers) {
15625
15816
  const escapedCommand = escapeForTemplateLiteral(handler.command);
15626
- if (handler.matcher && matcherSubject !== null) {
15817
+ if (handler.toolGate) {
15818
+ bodyLines.push(` if (input.tool === "${handler.toolGate}") {`);
15819
+ bodyLines.push(` await $\`${escapedCommand}\`;`);
15820
+ bodyLines.push(" }");
15821
+ } else if (handler.matcher && matcherSubject !== null) {
15627
15822
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
15628
15823
  bodyLines.push(" {");
15629
15824
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
@@ -16421,6 +16616,13 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
16421
16616
  */
16422
16617
  const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
16423
16618
  /**
16619
+ * Pi extension events that fire for every message role (user, assistant,
16620
+ * toolResult). The generated handler gates these on the assistant role so a
16621
+ * `postModelInvocation` hook runs once per finalized model response rather
16622
+ * than for every message in the conversation.
16623
+ */
16624
+ const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
16625
+ /**
16424
16626
  * Validate a hook matcher as a regular expression and return it as a JS
16425
16627
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
16426
16628
  */
@@ -16461,7 +16663,10 @@ function buildSubscriptionLines(handlerGroups) {
16461
16663
  const lines = [];
16462
16664
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
16463
16665
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
16464
- lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesToolName ? "event" : ""}) => {`);
16666
+ const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
16667
+ const usesEvent = usesToolName || gatesOnAssistant;
16668
+ lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
16669
+ if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
16465
16670
  for (const handler of handlers) {
16466
16671
  const embeddedCommand = JSON.stringify(handler.command);
16467
16672
  if (usesToolName && handler.matcher) {
@@ -16596,6 +16801,7 @@ function canonicalDefToQwencodeHook(def) {
16596
16801
  const type = def.type ?? "command";
16597
16802
  const isHttp = type === "http";
16598
16803
  const isCommand = type === "command";
16804
+ const isPrompt = type === "prompt";
16599
16805
  return {
16600
16806
  type,
16601
16807
  ...compact({
@@ -16605,6 +16811,8 @@ function canonicalDefToQwencodeHook(def) {
16605
16811
  name: def.name,
16606
16812
  description: def.description,
16607
16813
  statusMessage: def.statusMessage,
16814
+ prompt: isPrompt ? def.prompt : void 0,
16815
+ model: isPrompt ? def.model : void 0,
16608
16816
  async: isCommand ? def.async : void 0,
16609
16817
  env: isCommand ? def.env : void 0,
16610
16818
  shell: isCommand ? def.shell : void 0,
@@ -16623,7 +16831,7 @@ function canonicalDefToQwencodeHook(def) {
16623
16831
  * is needed here — commands are passed through verbatim and any such variable
16624
16832
  * reference stays intact for Qwen Code to expand.
16625
16833
  */
16626
- function canonicalToQwencodeHooks(config) {
16834
+ function canonicalToQwencodeHooks(config, logger) {
16627
16835
  const qwencodeSupported = new Set(QWENCODE_HOOK_EVENTS);
16628
16836
  const sharedHooks = {};
16629
16837
  for (const [event, defs] of Object.entries(config.hooks)) if (qwencodeSupported.has(event)) sharedHooks[event] = defs;
@@ -16643,6 +16851,7 @@ function canonicalToQwencodeHooks(config) {
16643
16851
  const byMatcher = /* @__PURE__ */ new Map();
16644
16852
  for (const def of definitions) {
16645
16853
  if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
16854
+ if (def.type === "prompt" && !def.prompt) logger?.warn(`Qwen Code prompt hook on '${eventName}' has no 'prompt' field; Qwen Code will load it and fail it at runtime.`);
16646
16855
  const key = def.matcher ?? "";
16647
16856
  const list = byMatcher.get(key);
16648
16857
  if (list) list.push(def);
@@ -16681,7 +16890,9 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
16681
16890
  shell: zod_mini.z.optional(zod_mini.z.string()),
16682
16891
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16683
16892
  allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16684
- once: zod_mini.z.optional(zod_mini.z.boolean())
16893
+ once: zod_mini.z.optional(zod_mini.z.boolean()),
16894
+ prompt: zod_mini.z.optional(zod_mini.z.string()),
16895
+ model: zod_mini.z.optional(zod_mini.z.string())
16685
16896
  });
16686
16897
  /**
16687
16898
  * A matcher group entry in a Qwen Code event array.
@@ -16696,37 +16907,43 @@ const QwencodeMatcherEntrySchema = zod_mini.z.looseObject({
16696
16907
  /**
16697
16908
  * Convert a single parsed Qwen Code matcher group into canonical hook definitions.
16698
16909
  */
16910
+ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
16911
+ const h = hook;
16912
+ const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16913
+ const isHttp = hookType === "http";
16914
+ const isCommand = hookType === "command";
16915
+ const isPrompt = hookType === "prompt";
16916
+ const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16917
+ return {
16918
+ type: hookType,
16919
+ ...compact({
16920
+ command: h.command,
16921
+ url: h.url,
16922
+ timeout: h.timeout,
16923
+ name: h.name,
16924
+ description: h.description,
16925
+ statusMessage: h.statusMessage,
16926
+ async: isCommand ? h.async : void 0,
16927
+ env: isCommand ? h.env : void 0,
16928
+ shell: isCommand ? shell : void 0,
16929
+ headers: isHttp ? h.headers : void 0,
16930
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16931
+ once: isHttp ? h.once : void 0,
16932
+ prompt: isPrompt ? h.prompt : void 0,
16933
+ model: isPrompt ? h.model : void 0,
16934
+ sequential: sequential ? true : void 0,
16935
+ matcher
16936
+ })
16937
+ };
16938
+ }
16699
16939
  function qwencodeMatcherEntryToCanonical(entry) {
16700
- const defs = [];
16701
- const hooks = entry.hooks ?? [];
16702
16940
  const sequential = entry.sequential === true;
16703
16941
  const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
16704
- for (const h of hooks) {
16705
- const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16706
- const isHttp = hookType === "http";
16707
- const isCommand = hookType === "command";
16708
- const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16709
- defs.push({
16710
- type: hookType,
16711
- ...compact({
16712
- command: h.command,
16713
- url: h.url,
16714
- timeout: h.timeout,
16715
- name: h.name,
16716
- description: h.description,
16717
- statusMessage: h.statusMessage,
16718
- async: isCommand ? h.async : void 0,
16719
- env: isCommand ? h.env : void 0,
16720
- shell: isCommand ? shell : void 0,
16721
- headers: isHttp ? h.headers : void 0,
16722
- allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16723
- once: isHttp ? h.once : void 0,
16724
- sequential: sequential ? true : void 0,
16725
- matcher
16726
- })
16727
- });
16728
- }
16729
- return defs;
16942
+ return (entry.hooks ?? []).map((hook) => qwencodeHookEntryToCanonical({
16943
+ hook,
16944
+ sequential,
16945
+ matcher
16946
+ }));
16730
16947
  }
16731
16948
  /**
16732
16949
  * Extract hooks from Qwen Code settings.json into canonical format.
@@ -16774,12 +16991,12 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
16774
16991
  validate
16775
16992
  });
16776
16993
  }
16777
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
16994
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16778
16995
  const paths = QwencodeHooks.getSettablePaths({ global });
16779
16996
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16780
16997
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16781
16998
  const config = rulesyncHooks.getJson();
16782
- const patch = { hooks: canonicalToQwencodeHooks(config) };
16999
+ const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
16783
17000
  const disableAllHooks = config.qwencode?.disableAllHooks;
16784
17001
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
16785
17002
  const fileContent = applySharedConfigPatch({
@@ -17331,7 +17548,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17331
17548
  },
17332
17549
  supportedEvents: KILO_HOOK_EVENTS,
17333
17550
  supportedHookTypes: ["command"],
17334
- supportsMatcher: true
17551
+ supportsMatcher: true,
17552
+ matcherEvents: ["preToolUse", "postToolUse"]
17335
17553
  }],
17336
17554
  ["opencode", {
17337
17555
  class: OpencodeHooks,
@@ -17342,7 +17560,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17342
17560
  },
17343
17561
  supportedEvents: OPENCODE_HOOK_EVENTS,
17344
17562
  supportedHookTypes: ["command"],
17345
- supportsMatcher: true
17563
+ supportsMatcher: true,
17564
+ matcherEvents: ["preToolUse", "postToolUse"]
17346
17565
  }],
17347
17566
  ["pi", {
17348
17567
  class: PiHooks,
@@ -19016,6 +19235,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
19016
19235
  ["qwencode", { class: QwencodeIgnore }],
19017
19236
  ["reasonix", { class: ReasonixIgnore }],
19018
19237
  ["roo", { class: RooIgnore }],
19238
+ ["zoocode", { class: RooIgnore }],
19019
19239
  ["devin", { class: DevinIgnore }],
19020
19240
  ["vibe", { class: VibeIgnore }],
19021
19241
  ["warp", { class: WarpIgnore }],
@@ -20385,7 +20605,6 @@ const resolveCopilotcliServerType = (server) => {
20385
20605
  case "streamable-http": return "http";
20386
20606
  case "stdio":
20387
20607
  case "local": return declared;
20388
- default: break;
20389
20608
  }
20390
20609
  if (server.command !== void 0) return "stdio";
20391
20610
  return isRemoteMcpServer(server) ? "http" : "stdio";
@@ -20805,18 +21024,24 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
20805
21024
  /**
20806
21025
  * MCP generator for Devin Local (native `.devin/` configuration).
20807
21026
  *
20808
- * Devin reads MCP servers from the `mcpServers` key of its native config file:
20809
- * - Project scope: `.devin/config.json`
20810
- * - Global scope: `~/.config/devin/config.json`
21027
+ * Since v3000.3 (the Local 3.6 release), Devin reads MCP servers from a
21028
+ * dedicated `mcpServers`-keyed config file rather than the shared
21029
+ * `config.json`:
21030
+ * - Project scope: `.devin/mcp_config.json`
21031
+ * - Global scope: `~/.config/devin/mcp_config.json`
20811
21032
  *
20812
- * The config file is shared with the permissions feature (`permissions` key)
20813
- * and, in global mode, the hooks feature (`hooks` key), so reads and writes
20814
- * merge into the existing JSON rather than overwriting it, and the file is
20815
- * never deleted. Each server is a stdio entry ({ command, args, env }) or a
20816
- * remote entry ({ serverUrl | url, headers }), and may carry an optional
20817
- * `disabledTools` array.
21033
+ * Legacy `mcpServers` entries left in `config.json` are auto-migrated into
21034
+ * the dedicated file on Devin startup, so writing the legacy key would fight
21035
+ * the migration. Import still falls back to the legacy `config.json`
21036
+ * `mcpServers` key when no dedicated file exists, so pre-v3000.3 repos
21037
+ * migrate cleanly. The gitignored `.devin/mcp_config.local.json` override is
21038
+ * the user's personal territory and is never read or written.
20818
21039
  *
20819
- * @see https://docs.devin.ai/cli/extensibility/configuration
21040
+ * Each server is a stdio entry ({ command, args, env }) or a remote entry
21041
+ * ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
21042
+ * array.
21043
+ *
21044
+ * @see https://docs.devin.ai/cli/extensibility/mcp/configuration
20820
21045
  */
20821
21046
  var DevinMcp = class DevinMcp extends ToolMcp {
20822
21047
  json;
@@ -20834,31 +21059,27 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20834
21059
  throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
20835
21060
  }
20836
21061
  }
20837
- /**
20838
- * config.json may carry the permissions/hooks features' keys, so it is never
20839
- * deleted; only the managed `mcpServers` key is rewritten.
20840
- */
20841
- isDeletable() {
20842
- return false;
20843
- }
20844
21062
  static getSettablePaths({ global = false } = {}) {
20845
21063
  if (global) return {
20846
21064
  relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
20847
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
21065
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20848
21066
  };
20849
21067
  return {
20850
21068
  relativeDirPath: DEVIN_DIR,
20851
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
21069
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20852
21070
  };
20853
21071
  }
20854
21072
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20855
21073
  const paths = this.getSettablePaths({ global });
20856
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
20857
- const json = this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath);
20858
- const newJson = {
20859
- ...json,
20860
- mcpServers: json.mcpServers ?? {}
20861
- };
21074
+ let fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
21075
+ if (fileContent === null) {
21076
+ const legacyContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME));
21077
+ if (legacyContent !== null) {
21078
+ const legacyJson = this.parseJsonOrThrow(legacyContent, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME);
21079
+ fileContent = JSON.stringify({ mcpServers: legacyJson.mcpServers ?? {} }, null, 2);
21080
+ }
21081
+ }
21082
+ const newJson = { mcpServers: this.parseJsonOrThrow(fileContent ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath).mcpServers ?? {} };
20862
21083
  return new DevinMcp({
20863
21084
  outputRoot,
20864
21085
  relativeDirPath: paths.relativeDirPath,
@@ -20868,21 +21089,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20868
21089
  global
20869
21090
  });
20870
21091
  }
20871
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
21092
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
20872
21093
  const paths = this.getSettablePaths({ global });
20873
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20874
- const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({ mcpServers: {} }, null, 2);
21094
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
21095
+ if (existingContent !== null) {
21096
+ const existingJson = this.parseJsonOrThrow(existingContent, paths.relativeDirPath, paths.relativeFilePath);
21097
+ const existingServers = existingJson.mcpServers && typeof existingJson.mcpServers === "object" ? Object.keys(existingJson.mcpServers) : [];
21098
+ const managedServers = new Set(Object.keys(rulesyncMcp.getMcpServers()));
21099
+ const dropped = existingServers.filter((name) => !managedServers.has(name));
21100
+ if (dropped.length > 0) logger?.warn(`Devin MCP servers not managed by rulesync will be removed from ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${dropped.join(", ")}. Run 'rulesync import' first to keep them, or move them to mcp_config.local.json.`);
21101
+ }
20875
21102
  return new DevinMcp({
20876
21103
  outputRoot,
20877
21104
  relativeDirPath: paths.relativeDirPath,
20878
21105
  relativeFilePath: paths.relativeFilePath,
20879
- fileContent: applySharedConfigPatch({
20880
- fileKey: sharedConfigFileKey(paths),
20881
- feature: "mcp",
20882
- existingContent,
20883
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
20884
- filePath
20885
- }),
21106
+ fileContent: JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2),
20886
21107
  validate,
20887
21108
  global
20888
21109
  });
@@ -22156,9 +22377,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22156
22377
  fileContent = await readFileContentOrNull(jsonPath);
22157
22378
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
22158
22379
  }
22380
+ if (instructions.length === 0 && fileContent === null) return null;
22159
22381
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
22160
22382
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22161
- const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
22383
+ const managedPrefix = `${toPosixPath((0, node_path.join)(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
22384
+ const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
22385
+ const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...preservedInstructions, ...instructions])).toSorted();
22162
22386
  return new KiloMcp({
22163
22387
  outputRoot,
22164
22388
  relativeDirPath: basePaths.relativeDirPath,
@@ -22167,7 +22391,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22167
22391
  fileKey: sharedConfigFileKey(basePaths),
22168
22392
  feature: "rules",
22169
22393
  existingContent: fileContent ?? "",
22170
- patch: { instructions: mergedInstructions },
22394
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22171
22395
  filePath: (0, node_path.join)(jsonDir, relativeFilePath)
22172
22396
  }),
22173
22397
  validate
@@ -22889,6 +23113,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22889
23113
  static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
22890
23114
  const basePaths = this.getSettablePaths({ global });
22891
23115
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
23116
+ const configDirPrefix = `${toPosixPath(basePaths.relativeDirPath).replace(/\/+$/, "")}/`;
23117
+ const normalizedInstructions = instructions.map((path) => global && path.startsWith(configDirPrefix) ? path.slice(configDirPrefix.length) : path);
22892
23118
  let fileContent = null;
22893
23119
  let relativeFilePath = OPENCODE_JSONC_FILE_NAME;
22894
23120
  const jsoncPath = (0, node_path.join)(jsonDir, OPENCODE_JSONC_FILE_NAME);
@@ -22898,9 +23124,15 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22898
23124
  fileContent = await readFileContentOrNull(jsonPath);
22899
23125
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
22900
23126
  }
23127
+ if (instructions.length === 0 && fileContent === null) return null;
22901
23128
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
22902
23129
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22903
- const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
23130
+ const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
23131
+ const preservedInstructions = existingInstructions.filter((entry) => {
23132
+ const normalized = toPosixPath(entry).replace(/^\.\//, "");
23133
+ return !managedPrefixes.some((prefix) => normalized.startsWith(prefix));
23134
+ });
23135
+ const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...preservedInstructions, ...normalizedInstructions])).toSorted();
22904
23136
  return new OpencodeMcp({
22905
23137
  outputRoot,
22906
23138
  relativeDirPath: basePaths.relativeDirPath,
@@ -22909,7 +23141,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22909
23141
  fileKey: sharedConfigFileKey(basePaths),
22910
23142
  feature: "rules",
22911
23143
  existingContent: fileContent ?? "",
22912
- patch: { instructions: mergedInstructions },
23144
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22913
23145
  filePath: (0, node_path.join)(jsonDir, relativeFilePath)
22914
23146
  }),
22915
23147
  validate
@@ -23349,9 +23581,13 @@ function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
23349
23581
  return parsed;
23350
23582
  }
23351
23583
  /**
23352
- * Rovodev MCP: global only at ~/.rovodev/mcp.json.
23353
- * Same shape as Cursor: { mcpServers: { ... } }. See Rovodev MCP docs.
23354
- * Project-level MCP is not supported; use --global when generating.
23584
+ * Rovodev MCP: `~/.rovodev/mcp.json` (global) and the repo-committed project
23585
+ * `.rovodev/mcp.json` documented by the Bitbucket Cloud Agentic Pipelines
23586
+ * guide ("Register your MCP server in `.rovodev/mcp.json`", referenced via
23587
+ * `mcp.mcpConfigPath`). Same shape as Cursor: { mcpServers: { ... } }.
23588
+ * A server the canonical config marks `disabled: true` is still written here
23589
+ * and switched off through `mcp.disabledMcpServers` in the sibling
23590
+ * `config.yml` — the file Rovo Dev actually consults for disabling.
23355
23591
  */
23356
23592
  /**
23357
23593
  * Rovo Dev documents the per-server transport key as `transport`, with the
@@ -23377,11 +23613,7 @@ function lookupTransport(map, key) {
23377
23613
  return Object.hasOwn(map, key) ? map[key] : void 0;
23378
23614
  }
23379
23615
  function toRovodevServer(name, server, logger) {
23380
- const { type, transport, disabled, ...rest } = server;
23381
- if (disabled === true) {
23382
- logger?.warn(`Rovo Dev MCP: skipping "${name}" because it is disabled and mcp.json has no disable flag.`);
23383
- return null;
23384
- }
23616
+ const { type, transport, disabled: _disabled, ...rest } = server;
23385
23617
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
23386
23618
  if (declared === void 0) return rest;
23387
23619
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -23403,6 +23635,44 @@ function fromRovodevServer(server) {
23403
23635
  type: mapped
23404
23636
  };
23405
23637
  }
23638
+ /**
23639
+ * Read the sibling `config.yml` (same scope root as `mcp.json`) and return the
23640
+ * parsed document, `null` when the file does not exist. A malformed file
23641
+ * **throws**: the disable toggle lives here, so pretending a broken file says
23642
+ * nothing would silently re-enable servers on import and leave disabled ones
23643
+ * running on generate — both flip a security toggle the wrong way.
23644
+ */
23645
+ async function readRovodevConfigYaml({ outputRoot }) {
23646
+ const content = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME));
23647
+ if (content === null) return null;
23648
+ return parseSharedConfig({
23649
+ format: "yaml",
23650
+ fileContent: content,
23651
+ filePath: (0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23652
+ });
23653
+ }
23654
+ function disabledNamesOf(config) {
23655
+ const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
23656
+ return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
23657
+ }
23658
+ /**
23659
+ * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
23660
+ * `~/.rovodev/config.yml` (global). Carries `disabledMcpServers` — the key
23661
+ * Rovo Dev actually consults to switch a server off — recomputed from the
23662
+ * existing block so user keys (`mcpConfigPath`, `allowedMcpServers`, ...) and
23663
+ * disabled names for servers rulesync does not manage survive.
23664
+ */
23665
+ var RovodevMcpConfigYaml = class extends ToolFile {
23666
+ isDeletable() {
23667
+ return false;
23668
+ }
23669
+ validate() {
23670
+ return {
23671
+ success: true,
23672
+ error: null
23673
+ };
23674
+ }
23675
+ };
23406
23676
  var RovodevMcp = class RovodevMcp extends ToolMcp {
23407
23677
  json;
23408
23678
  constructor(params) {
@@ -23423,13 +23693,24 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23423
23693
  };
23424
23694
  }
23425
23695
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
23426
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23427
23696
  const paths = this.getSettablePaths({ global });
23428
23697
  const json = parseRovodevMcpJson(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
23429
23698
  const newJson = {
23430
23699
  ...json,
23431
23700
  mcpServers: json.mcpServers ?? {}
23432
23701
  };
23702
+ const disabledNames = disabledNamesOf(await readRovodevConfigYaml({ outputRoot }));
23703
+ if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
23704
+ const servers = newJson.mcpServers;
23705
+ for (const name of disabledNames) {
23706
+ if (!Object.hasOwn(servers, name)) continue;
23707
+ const server = servers[name];
23708
+ if (isPlainObject$1(server)) servers[name] = {
23709
+ ...server,
23710
+ disabled: true
23711
+ };
23712
+ }
23713
+ }
23433
23714
  return new RovodevMcp({
23434
23715
  outputRoot,
23435
23716
  relativeDirPath: paths.relativeDirPath,
@@ -23440,11 +23721,21 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23440
23721
  });
23441
23722
  }
23442
23723
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23443
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23444
23724
  const paths = this.getSettablePaths({ global });
23445
23725
  const json = parseRovodevMcpJson(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2), paths.relativeDirPath, paths.relativeFilePath);
23726
+ let canWriteDisableToggle = true;
23727
+ try {
23728
+ await readRovodevConfigYaml({ outputRoot });
23729
+ } catch {
23730
+ canWriteDisableToggle = false;
23731
+ }
23446
23732
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
23447
- const converted = toRovodevServer(name, server, logger);
23733
+ const record = server;
23734
+ if (record.disabled === true && !canWriteDisableToggle) {
23735
+ logger?.warn(`Rovo Dev MCP: skipping disabled server "${name}" because config.yml cannot be parsed, so mcp.disabledMcpServers cannot be written to switch it off.`);
23736
+ return null;
23737
+ }
23738
+ const converted = toRovodevServer(name, record, logger);
23448
23739
  return converted === null ? null : [name, converted];
23449
23740
  }).filter((entry) => entry !== null));
23450
23741
  const rovodevConfig = {
@@ -23460,6 +23751,64 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23460
23751
  global
23461
23752
  });
23462
23753
  }
23754
+ /**
23755
+ * `mcp.disabledMcpServers` lives in the shared `config.yml` the permissions
23756
+ * feature also writes. Declared here so the write-order derivation sees this
23757
+ * feature as one of that file's writers — it is not a settable path, since
23758
+ * the servers themselves live in `mcp.json`.
23759
+ */
23760
+ static getExtraSharedWritePaths() {
23761
+ return [{
23762
+ relativeDirPath: ROVODEV_DIR,
23763
+ relativeFilePath: ROVODEV_CONFIG_FILE_NAME
23764
+ }];
23765
+ }
23766
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
23767
+ const servers = rulesyncMcp.forTarget({
23768
+ toolTarget: "rovodev",
23769
+ logger
23770
+ }).getMcpServers();
23771
+ const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
23772
+ const disabledNames = managedNames.filter((name) => {
23773
+ const server = servers[name];
23774
+ return isRecord(server) && server.disabled === true;
23775
+ });
23776
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
23777
+ let existingParsed;
23778
+ try {
23779
+ existingParsed = parseSharedConfig({
23780
+ format: "yaml",
23781
+ fileContent: existingContent,
23782
+ filePath: (0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23783
+ });
23784
+ } catch (error) {
23785
+ logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
23786
+ return [];
23787
+ }
23788
+ const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
23789
+ const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
23790
+ const managedNameSet = new Set(managedNames);
23791
+ const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
23792
+ if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
23793
+ const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
23794
+ if (mergedDisabled.length > 0) existingMcp.disabledMcpServers = mergedDisabled;
23795
+ else delete existingMcp.disabledMcpServers;
23796
+ if (mergedDisabled.length === 0 && existingContent.trim() === "") return [];
23797
+ const fileContent = applySharedConfigPatch({
23798
+ fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
23799
+ feature: "mcp",
23800
+ existingContent,
23801
+ patch: { mcp: Object.keys(existingMcp).length > 0 ? existingMcp : void 0 },
23802
+ filePath: (0, node_path.join)(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23803
+ });
23804
+ return [new RovodevMcpConfigYaml({
23805
+ outputRoot,
23806
+ relativeDirPath: ROVODEV_DIR,
23807
+ relativeFilePath: ROVODEV_CONFIG_FILE_NAME,
23808
+ fileContent,
23809
+ global
23810
+ })];
23811
+ }
23463
23812
  toRulesyncMcp() {
23464
23813
  const rawServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
23465
23814
  const mcpServers = Object.fromEntries(Object.entries(rawServers).filter(([, server]) => isPlainObject$1(server)).map(([name, server]) => [name, fromRovodevServer(server)]));
@@ -23608,7 +23957,6 @@ function transportOf(server) {
23608
23957
  case "http":
23609
23958
  case "streamable-http":
23610
23959
  case "ws": return "http";
23611
- default: break;
23612
23960
  }
23613
23961
  if (server.command !== void 0) return "stdio";
23614
23962
  if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
@@ -23636,6 +23984,20 @@ function deriveTransportAllowlist(servers) {
23636
23984
  return allowlist;
23637
23985
  }
23638
23986
  //#endregion
23987
+ //#region src/features/shared/vibe-config-scope.ts
23988
+ /**
23989
+ * Vibe selects exactly **one** persistence TOML and does not merge scopes: the
23990
+ * trusted project `.vibe/config.toml` when one is discovered, otherwise
23991
+ * `~/.vibe/config.toml` (single code path since v2.22.0's ConfigOrchestrator
23992
+ * migration). A `--global` run that writes the home file is therefore inert in
23993
+ * any project that has its own config — worth a heads-up, since the other Vibe
23994
+ * surfaces (rules, hooks, agents, skills) genuinely combine scopes.
23995
+ */
23996
+ async function warnIfGlobalVibeConfigIsShadowed(logger) {
23997
+ if (!await fileExists((0, node_path.join)(process.cwd(), ".vibe", "config.toml"))) return;
23998
+ logger?.warn("Vibe reads exactly one config.toml (project .vibe/config.toml when present, otherwise ~/.vibe/config.toml — a fallback, not a merge). This project has .vibe/config.toml, so the global file written by --global is ignored here.");
23999
+ }
24000
+ //#endregion
23639
24001
  //#region src/features/mcp/vibe-mcp.ts
23640
24002
  const VIBE_MCP_SERVER_FIELDS = [
23641
24003
  "transport",
@@ -23704,8 +24066,9 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23704
24066
  global
23705
24067
  });
23706
24068
  }
23707
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
24069
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger, global = false }) {
23708
24070
  const paths = this.getSettablePaths({ global });
24071
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
23709
24072
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
23710
24073
  const existingContent = await readFileContentOrNull(filePath) ?? "";
23711
24074
  const existingServers = normalizeMcpServersArray(parseSharedConfig({
@@ -23714,7 +24077,10 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23714
24077
  filePath
23715
24078
  }).mcp_servers);
23716
24079
  const existingByName = new Map(existingServers.map((server) => [server.name, server]));
23717
- const mcpServers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
24080
+ const rulesyncServers = rulesyncMcp.getMcpServers();
24081
+ const managedNames = new Set(Object.keys(rulesyncServers));
24082
+ const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
24083
+ const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
23718
24084
  return new VibeMcp({
23719
24085
  outputRoot,
23720
24086
  relativeDirPath: paths.relativeDirPath,
@@ -23723,7 +24089,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23723
24089
  fileKey: sharedConfigFileKey(paths),
23724
24090
  feature: "mcp",
23725
24091
  existingContent,
23726
- patch: { mcp_servers: mcpServers },
24092
+ patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
23727
24093
  filePath
23728
24094
  }),
23729
24095
  validate,
@@ -24425,10 +24791,19 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
24425
24791
  supportsDisabledTools: false
24426
24792
  }
24427
24793
  }],
24794
+ ["zoocode", {
24795
+ class: RooMcp,
24796
+ meta: {
24797
+ supportsProject: true,
24798
+ supportsGlobal: false,
24799
+ supportsEnabledTools: false,
24800
+ supportsDisabledTools: false
24801
+ }
24802
+ }],
24428
24803
  ["rovodev", {
24429
24804
  class: RovodevMcp,
24430
24805
  meta: {
24431
- supportsProject: false,
24806
+ supportsProject: true,
24432
24807
  supportsGlobal: true,
24433
24808
  supportsEnabledTools: false,
24434
24809
  supportsDisabledTools: false
@@ -25294,9 +25669,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
25294
25669
  case "ask":
25295
25670
  ask.push(entry);
25296
25671
  break;
25297
- case "deny":
25298
- deny.push(entry);
25299
- break;
25672
+ case "deny": deny.push(entry);
25300
25673
  }
25301
25674
  }
25302
25675
  }
@@ -25507,9 +25880,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
25507
25880
  case "ask":
25508
25881
  ask.push(entry);
25509
25882
  break;
25510
- case "deny":
25511
- deny.push(entry);
25512
- break;
25883
+ case "deny": deny.push(entry);
25513
25884
  }
25514
25885
  }
25515
25886
  }
@@ -26241,9 +26612,7 @@ function convertRulesyncToClaudePermissions({ config, logger }) {
26241
26612
  case "ask":
26242
26613
  ask.push(entry);
26243
26614
  break;
26244
- case "deny":
26245
- deny.push(entry);
26246
- break;
26615
+ case "deny": deny.push(entry);
26247
26616
  }
26248
26617
  }
26249
26618
  }
@@ -27493,9 +27862,7 @@ function convertRulesyncToCursorPermissions(config, logger) {
27493
27862
  case "ask":
27494
27863
  logger?.warn(`Cursor CLI permissions do not support the "ask" action. Skipping ${category} rule: ${pattern}`);
27495
27864
  break;
27496
- case "deny":
27497
- deny.push(entry);
27498
- break;
27865
+ case "deny": deny.push(entry);
27499
27866
  }
27500
27867
  }
27501
27868
  }
@@ -27600,9 +27967,10 @@ function buildDevinPermissionEntry(scope, pattern) {
27600
27967
  * - Project scope: `.devin/config.json`
27601
27968
  * - Global scope: `~/.config/devin/config.json`
27602
27969
  *
27603
- * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
27604
- * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
27605
- * the file is never deleted; only the managed `permissions` key is rewritten.
27970
+ * In global mode the config file is shared with the hooks (`hooks`) feature
27971
+ * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
27972
+ * merge into the existing JSON and the file is never deleted; only the managed
27973
+ * `permissions` key is rewritten.
27606
27974
  *
27607
27975
  * @see https://docs.devin.ai/cli/reference/permissions
27608
27976
  */
@@ -27731,9 +28099,7 @@ function convertRulesyncToDevinPermissions(config) {
27731
28099
  case "ask":
27732
28100
  ask.push(entry);
27733
28101
  break;
27734
- case "deny":
27735
- deny.push(entry);
27736
- break;
28102
+ case "deny": deny.push(entry);
27737
28103
  }
27738
28104
  }
27739
28105
  }
@@ -27921,10 +28287,7 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
27921
28287
  case "allow":
27922
28288
  allow.push(pattern);
27923
28289
  break;
27924
- case "deny":
27925
- deny.push(pattern);
27926
- break;
27927
- case "ask": break;
28290
+ case "deny": deny.push(pattern);
27928
28291
  }
27929
28292
  }
27930
28293
  return {
@@ -28043,10 +28406,11 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28043
28406
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28044
28407
  }
28045
28408
  const config = isRecord(parsed) ? { ...parsed } : {};
28046
- config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
28409
+ const userPermission = convertRulesyncToGoosePermissionConfig({
28047
28410
  config: rulesyncPermissions.getJson(),
28048
28411
  logger
28049
28412
  });
28413
+ config[GOOSE_USER_KEY] = userPermission;
28050
28414
  return new GoosePermissions({
28051
28415
  outputRoot,
28052
28416
  relativeDirPath: paths.relativeDirPath,
@@ -30264,9 +30628,7 @@ function convertRulesyncToQwenPermissions(config) {
30264
30628
  case "ask":
30265
30629
  ask.push(entry);
30266
30630
  break;
30267
- case "deny":
30268
- deny.push(entry);
30269
- break;
30631
+ case "deny": deny.push(entry);
30270
30632
  }
30271
30633
  }
30272
30634
  }
@@ -30339,6 +30701,20 @@ function toCanonicalToolName$2(reasonixName) {
30339
30701
  return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
30340
30702
  }
30341
30703
  /**
30704
+ * Whether an entry uses the first-class `Tool=<literal>` exact-command form
30705
+ * (SPEC §3.7, v1.18.0): metacharacters in the literal are ordinary characters
30706
+ * and only the identical complete command matches. These entries have no
30707
+ * canonical tool→pattern→action equivalent — glob-style `Tool(...)` matches
30708
+ * differently by design — so they are preserved verbatim and round-trip
30709
+ * through the `reasonix` override's rawAllow/rawAsk/rawDeny arrays.
30710
+ */
30711
+ function isReasonixExactCommandEntry(entry) {
30712
+ const eqIndex = entry.indexOf("=");
30713
+ if (eqIndex <= 0) return false;
30714
+ const parenIndex = entry.indexOf("(");
30715
+ return parenIndex === -1 || eqIndex < parenIndex;
30716
+ }
30717
+ /**
30342
30718
  * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
30343
30719
  * If no parentheses, returns the tool name with "*" as the pattern.
30344
30720
  */
@@ -30427,10 +30803,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30427
30803
  const config = rulesyncPermissions.getJson();
30428
30804
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
30429
30805
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
30806
+ const override = config.reasonix;
30807
+ const rawAllow = toReasonixStringArray(override?.rawAllow);
30808
+ const rawAsk = toReasonixStringArray(override?.rawAsk);
30809
+ const rawDeny = toReasonixStringArray(override?.rawDeny);
30810
+ const rawOwnedEntries = /* @__PURE__ */ new Set([
30811
+ ...rawAllow,
30812
+ ...rawAsk,
30813
+ ...rawDeny
30814
+ ]);
30430
30815
  const existingPermissions = toReasonixTable(parsed.permissions);
30431
- const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30432
- const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30433
- const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30816
+ const { allow: preservedAllow, ask: preservedAsk, deny: preservedDeny } = preserveExistingEntries({
30817
+ existingPermissions,
30818
+ managedToolNames,
30819
+ rawOwnedEntries,
30820
+ logger
30821
+ });
30434
30822
  if (logger && managedToolNames.has("Read")) {
30435
30823
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
30436
30824
  const { toolName } = parseReasonixPermissionEntry(entry);
@@ -30439,17 +30827,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30439
30827
  if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages 'Read' tool and will overwrite ${droppedReadDenyEntries.length} existing Read deny entries (possibly from ignore feature). Permissions take precedence.`);
30440
30828
  }
30441
30829
  const mergedPermissions = { ...existingPermissions };
30442
- const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
30443
- const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
30444
- const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
30445
- if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
30446
- else delete mergedPermissions.allow;
30447
- if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
30448
- else delete mergedPermissions.ask;
30449
- if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
30450
- else delete mergedPermissions.deny;
30830
+ setOrDeleteEntries(mergedPermissions, "allow", [
30831
+ ...preservedAllow,
30832
+ ...allow,
30833
+ ...rawAllow
30834
+ ]);
30835
+ setOrDeleteEntries(mergedPermissions, "ask", [
30836
+ ...preservedAsk,
30837
+ ...ask,
30838
+ ...rawAsk
30839
+ ]);
30840
+ setOrDeleteEntries(mergedPermissions, "deny", [
30841
+ ...preservedDeny,
30842
+ ...deny,
30843
+ ...rawDeny
30844
+ ]);
30451
30845
  const patch = { permissions: mergedPermissions };
30452
- const override = config.reasonix;
30453
30846
  if (override?.sandbox !== void 0) patch.sandbox = {
30454
30847
  ...asReasonixRecord(parsed.sandbox),
30455
30848
  ...asReasonixRecord(override.sandbox)
@@ -30480,16 +30873,29 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30480
30873
  }
30481
30874
  toRulesyncPermissions() {
30482
30875
  const permissions = toReasonixTable(this.toml.permissions);
30876
+ const splitExact = (entries) => {
30877
+ const exact = entries.filter((entry) => isReasonixExactCommandEntry(entry));
30878
+ return {
30879
+ translated: entries.filter((entry) => !isReasonixExactCommandEntry(entry)),
30880
+ exact
30881
+ };
30882
+ };
30883
+ const allowSplit = splitExact(toReasonixStringArray(permissions.allow));
30884
+ const askSplit = splitExact(toReasonixStringArray(permissions.ask));
30885
+ const denySplit = splitExact(toReasonixStringArray(permissions.deny));
30483
30886
  const config = convertReasonixToRulesyncPermissions({
30484
- allow: toReasonixStringArray(permissions.allow),
30485
- ask: toReasonixStringArray(permissions.ask),
30486
- deny: toReasonixStringArray(permissions.deny)
30887
+ allow: allowSplit.translated,
30888
+ ask: askSplit.translated,
30889
+ deny: denySplit.translated
30487
30890
  });
30488
30891
  const sandbox = asReasonixRecord(this.toml.sandbox);
30489
30892
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
30490
30893
  const reasonixOverride = {};
30491
30894
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
30492
30895
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
30896
+ if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
30897
+ if (askSplit.exact.length > 0) reasonixOverride.rawAsk = askSplit.exact;
30898
+ if (denySplit.exact.length > 0) reasonixOverride.rawDeny = denySplit.exact;
30493
30899
  const result = { ...config };
30494
30900
  if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
30495
30901
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -30519,6 +30925,41 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30519
30925
  }
30520
30926
  };
30521
30927
  /**
30928
+ * Preserve existing permission entries rulesync does not manage: entries for
30929
+ * tools NOT in the permissions config, plus exact `Tool=<literal>` entries not
30930
+ * owned by the override's raw arrays — Reasonix writes those itself as
30931
+ * remembered approvals, and dropping them would revoke grants the canonical
30932
+ * glob rules cannot re-express. Remove one by deleting it from `reasonix.toml`
30933
+ * (or by naming it in a raw array, which takes ownership). Preserved exact
30934
+ * entries are logged so the kept-alive grants stay visible.
30935
+ */
30936
+ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwnedEntries, logger }) {
30937
+ const isPreserved = (entry) => {
30938
+ if (isReasonixExactCommandEntry(entry)) return !rawOwnedEntries.has(entry);
30939
+ return !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName);
30940
+ };
30941
+ const allow = toReasonixStringArray(existingPermissions.allow).filter(isPreserved);
30942
+ const ask = toReasonixStringArray(existingPermissions.ask).filter(isPreserved);
30943
+ const deny = toReasonixStringArray(existingPermissions.deny).filter(isPreserved);
30944
+ const preservedExact = [
30945
+ ...allow,
30946
+ ...ask,
30947
+ ...deny
30948
+ ].filter((entry) => isReasonixExactCommandEntry(entry));
30949
+ if (preservedExact.length > 0) logger?.info(`Preserving ${preservedExact.length} exact Reasonix permission entries (remembered approvals): ${preservedExact.join(", ")}`);
30950
+ return {
30951
+ allow,
30952
+ ask,
30953
+ deny
30954
+ };
30955
+ }
30956
+ /** Sort, dedupe and set a permissions array key, deleting it when empty. */
30957
+ function setOrDeleteEntries(table, key, entries) {
30958
+ const merged = (0, es_toolkit.uniq)(entries.toSorted());
30959
+ if (merged.length > 0) table[key] = merged;
30960
+ else delete table[key];
30961
+ }
30962
+ /**
30522
30963
  * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
30523
30964
  */
30524
30965
  function convertRulesyncToReasonixPermissions(config) {
@@ -30536,9 +30977,7 @@ function convertRulesyncToReasonixPermissions(config) {
30536
30977
  case "ask":
30537
30978
  ask.push(entry);
30538
30979
  break;
30539
- case "deny":
30540
- deny.push(entry);
30541
- break;
30980
+ case "deny": deny.push(entry);
30542
30981
  }
30543
30982
  }
30544
30983
  }
@@ -30568,7 +31007,6 @@ function convertReasonixToRulesyncPermissions(params) {
30568
31007
  }
30569
31008
  //#endregion
30570
31009
  //#region src/features/permissions/rovodev-permissions.ts
30571
- const ROVODEV_GLOBAL_ONLY_MESSAGE = "Rovodev permissions are global-only; use --global to sync ~/.rovodev/config.yml";
30572
31010
  const CATCH_ALL_PATTERN$1 = "*";
30573
31011
  const CATEGORY_TO_TOOL_KEYS = {
30574
31012
  read: [
@@ -30628,9 +31066,12 @@ const OWNED_TOOL_PERMISSION_KEYS = [
30628
31066
  /**
30629
31067
  * Permissions adapter for Rovo Dev CLI.
30630
31068
  *
30631
- * Rovo Dev reads tool permissions from the `toolPermissions` block of the global
30632
- * `~/.rovodev/config.yml`. This surface is **global only** there is no
30633
- * project-scoped Rovo Dev permissions file (mirrors the Rovodev MCP adapter).
31069
+ * Rovo Dev reads tool permissions from the `toolPermissions` block of
31070
+ * `config.yml` the global `~/.rovodev/config.yml`, and since the Bitbucket
31071
+ * Cloud Agentic Pipelines docs, also the repo-committed project
31072
+ * `.rovodev/config.yml` (referenced from `bitbucket-pipelines.yml` via
31073
+ * `config.path`, or the `--config-file` CLI flag). Both scopes use the
31074
+ * same relative path, resolved against the project root or home directory.
30634
31075
  *
30635
31076
  * Rovo Dev's three levels (`allow`/`ask`/`deny`) are an exact 1:1 with rulesync's
30636
31077
  * canonical action enum, so action values pass through verbatim.
@@ -30677,7 +31118,6 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30677
31118
  };
30678
31119
  }
30679
31120
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30680
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30681
31121
  const paths = RovodevPermissions.getSettablePaths({ global });
30682
31122
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
30683
31123
  return new RovodevPermissions({
@@ -30686,11 +31126,10 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30686
31126
  relativeFilePath: paths.relativeFilePath,
30687
31127
  fileContent,
30688
31128
  validate,
30689
- global: true
31129
+ global
30690
31130
  });
30691
31131
  }
30692
31132
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
30693
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30694
31133
  const paths = RovodevPermissions.getSettablePaths({ global });
30695
31134
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30696
31135
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -30713,14 +31152,20 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30713
31152
  filePath,
30714
31153
  logger
30715
31154
  });
30716
- if (resolvedToolPermissions !== void 0) config.toolPermissions = resolvedToolPermissions;
31155
+ const fileContent = resolvedToolPermissions !== void 0 ? applySharedConfigPatch({
31156
+ fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
31157
+ feature: "permissions",
31158
+ existingContent,
31159
+ patch: { toolPermissions: resolvedToolPermissions },
31160
+ filePath
31161
+ }) : (0, js_yaml.dump)(config);
30717
31162
  return new RovodevPermissions({
30718
31163
  outputRoot,
30719
31164
  relativeDirPath: paths.relativeDirPath,
30720
31165
  relativeFilePath: paths.relativeFilePath,
30721
- fileContent: (0, js_yaml.dump)(config),
31166
+ fileContent,
30722
31167
  validate: true,
30723
- global: true
31168
+ global
30724
31169
  });
30725
31170
  }
30726
31171
  toRulesyncPermissions() {
@@ -31257,6 +31702,7 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
31257
31702
  write: "write_file",
31258
31703
  webfetch: "web_fetch",
31259
31704
  websearch: "web_search",
31705
+ grep: "grep",
31260
31706
  agent: "task"
31261
31707
  };
31262
31708
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
@@ -31266,6 +31712,7 @@ const VIBE_TO_CANONICAL_TOOL_NAMES = {
31266
31712
  write_file: "write",
31267
31713
  web_fetch: "webfetch",
31268
31714
  web_search: "websearch",
31715
+ grep: "grep",
31269
31716
  task: "agent"
31270
31717
  };
31271
31718
  var VibePermissions = class VibePermissions extends ToolPermissions {
@@ -31303,6 +31750,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31303
31750
  }
31304
31751
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
31305
31752
  const paths = this.getSettablePaths({ global });
31753
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
31306
31754
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
31307
31755
  const existingContent = await readFileContentOrNull(filePath) ?? "";
31308
31756
  const config = parseVibeConfig(existingContent);
@@ -31310,46 +31758,31 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31310
31758
  const tools = toVibeToolsRecord(config.tools);
31311
31759
  const enabledTools = new Set(toStringArray(config.enabled_tools));
31312
31760
  const disabledTools = new Set(toStringArray(config.disabled_tools));
31761
+ const removedEnabledTools = [];
31313
31762
  for (const category of Object.keys(permission)) {
31763
+ if (!hasVibeToolName(category)) continue;
31314
31764
  const vibeToolName = toVibeToolName(category);
31315
- enabledTools.delete(vibeToolName);
31765
+ if (enabledTools.delete(vibeToolName)) removedEnabledTools.push(vibeToolName);
31316
31766
  disabledTools.delete(vibeToolName);
31317
31767
  }
31318
31768
  for (const [category, rules] of Object.entries(permission)) {
31319
- const vibeToolName = toVibeToolName(category);
31320
- const existingTool = toVibeToolConfig(tools[vibeToolName]);
31321
- const nextTool = { ...existingTool };
31322
- const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31323
- const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31324
- for (const [pattern, action] of Object.entries(rules)) {
31325
- if (pattern === "*") {
31326
- applyWildcardPermission({
31327
- action,
31328
- toolConfig: nextTool
31329
- });
31330
- if (action === "deny") {
31331
- disabledTools.add(vibeToolName);
31332
- enabledTools.delete(vibeToolName);
31333
- } else if (action === "allow") {
31334
- enabledTools.add(vibeToolName);
31335
- disabledTools.delete(vibeToolName);
31336
- }
31337
- continue;
31338
- }
31339
- if (action === "ask") {
31340
- logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31341
- continue;
31342
- }
31343
- if (action === "allow") allow.add(pattern);
31344
- else deny.add(pattern);
31769
+ if (!hasVibeToolName(category)) {
31770
+ const ruleCount = Object.keys(rules).length;
31771
+ if (ruleCount > 0) logger?.warn(`Vibe has no builtin tool for the '${category}' category; skipping ${ruleCount} rule(s) instead of emitting an inert [tools.${category}] table.`);
31772
+ continue;
31345
31773
  }
31346
- delete nextTool.allow;
31347
- delete nextTool.deny;
31348
- if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31349
- if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31350
- tools[vibeToolName] = nextTool;
31774
+ applyCategoryRules({
31775
+ category,
31776
+ rules,
31777
+ tools,
31778
+ enabledTools,
31779
+ disabledTools,
31780
+ logger
31781
+ });
31351
31782
  }
31352
- applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
31783
+ const vibeOverride = rulesyncPermissions.getJson().vibe;
31784
+ applyVibeSensitivePatterns(tools, vibeOverride, logger);
31785
+ if (removedEnabledTools.length > 0 && vibeOverride?.enabled_tools === void 0 && logger) logger.warn(`Removed ${removedEnabledTools.join(", ")} from Vibe's exclusive enabled_tools list (rulesync owns the tools it configures and no longer expresses allows through that key). If the exclusive narrowing was intentional, declare the full list explicitly via vibe.enabled_tools in .rulesync/permissions.jsonc.`);
31353
31786
  return new VibePermissions({
31354
31787
  outputRoot,
31355
31788
  relativeDirPath: paths.relativeDirPath,
@@ -31360,7 +31793,10 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31360
31793
  existingContent,
31361
31794
  patch: {
31362
31795
  tools,
31363
- enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
31796
+ enabled_tools: resolveEnabledToolsPatch({
31797
+ vibeOverride,
31798
+ enabledTools
31799
+ }),
31364
31800
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
31365
31801
  },
31366
31802
  filePath
@@ -31372,7 +31808,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31372
31808
  toRulesyncPermissions() {
31373
31809
  const permission = {};
31374
31810
  const vibeOverridePermission = {};
31375
- for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
31811
+ const enabledToolsList = toStringArray(this.toml.enabled_tools);
31376
31812
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
31377
31813
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
31378
31814
  const category = toCanonicalToolName$1(vibeToolName);
@@ -31385,9 +31821,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31385
31821
  if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
31386
31822
  }
31387
31823
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
31388
- const json = Object.keys(vibeOverridePermission).length > 0 ? {
31824
+ const vibeOverride = {};
31825
+ if (Object.keys(vibeOverridePermission).length > 0) vibeOverride.permission = vibeOverridePermission;
31826
+ if (enabledToolsList.length > 0) vibeOverride.enabled_tools = enabledToolsList;
31827
+ const json = Object.keys(vibeOverride).length > 0 ? {
31389
31828
  permission,
31390
- vibe: { permission: vibeOverridePermission }
31829
+ vibe: vibeOverride
31391
31830
  } : { permission };
31392
31831
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
31393
31832
  }
@@ -31422,8 +31861,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31422
31861
  * list for any category the override names: a present list is set, an empty
31423
31862
  * one clears it. Categories not named keep whatever the existing file had.
31424
31863
  */
31425
- function applyVibeSensitivePatterns(tools, vibeOverride) {
31864
+ function applyVibeSensitivePatterns(tools, vibeOverride, logger) {
31426
31865
  for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
31866
+ if (!hasVibeToolName(category)) {
31867
+ logger?.warn(`Vibe has no builtin tool for the '${category}' category; skipping its vibe.permission.${category}.sensitive_patterns override.`);
31868
+ continue;
31869
+ }
31427
31870
  const vibeToolName = toVibeToolName(category);
31428
31871
  const nextTool = toVibeToolConfig(tools[vibeToolName]);
31429
31872
  const patterns = toStringArray(toolOverride.sensitive_patterns);
@@ -31432,6 +31875,57 @@ function applyVibeSensitivePatterns(tools, vibeOverride) {
31432
31875
  tools[vibeToolName] = nextTool;
31433
31876
  }
31434
31877
  }
31878
+ /**
31879
+ * Translate one canonical category's rules into the tool's `[tools.<name>]`
31880
+ * table and the top-level `disabled_tools` filter, mutating `tools` and the
31881
+ * filter sets in place.
31882
+ */
31883
+ function applyCategoryRules({ category, rules, tools, enabledTools, disabledTools, logger }) {
31884
+ const vibeToolName = toVibeToolName(category);
31885
+ const existingTool = toVibeToolConfig(tools[vibeToolName]);
31886
+ const nextTool = { ...existingTool };
31887
+ const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31888
+ const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31889
+ for (const [pattern, action] of Object.entries(rules)) {
31890
+ if (pattern === "*") {
31891
+ applyWildcardPermission({
31892
+ action,
31893
+ toolConfig: nextTool
31894
+ });
31895
+ if (action === "deny") {
31896
+ disabledTools.add(vibeToolName);
31897
+ enabledTools.delete(vibeToolName);
31898
+ } else if (action === "allow") disabledTools.delete(vibeToolName);
31899
+ continue;
31900
+ }
31901
+ if (action === "ask") {
31902
+ logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31903
+ continue;
31904
+ }
31905
+ if (action === "allow") allow.add(pattern);
31906
+ else deny.add(pattern);
31907
+ }
31908
+ delete nextTool.allow;
31909
+ delete nextTool.deny;
31910
+ if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31911
+ if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31912
+ tools[vibeToolName] = nextTool;
31913
+ }
31914
+ /**
31915
+ * `enabled_tools` is an exclusive allowlist and therefore only ever
31916
+ * rulesync-authored explicitly, through `vibe.enabled_tools`. When the
31917
+ * override declares it (even empty), rulesync owns the whole list; when it
31918
+ * does not, the on-disk list survives minus the entries the caller's cleanup
31919
+ * loop removed for rulesync-configured tools (which also migrates away the
31920
+ * exclusive entries earlier rulesync versions wrote for allow rules).
31921
+ */
31922
+ function resolveEnabledToolsPatch({ vibeOverride, enabledTools }) {
31923
+ if (vibeOverride?.enabled_tools !== void 0) {
31924
+ const declared = [...new Set(toStringArray(vibeOverride.enabled_tools))];
31925
+ return declared.length > 0 ? declared.toSorted() : void 0;
31926
+ }
31927
+ return enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0;
31928
+ }
31435
31929
  function parseVibeConfig(fileContent) {
31436
31930
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
31437
31931
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -31440,6 +31934,17 @@ function parseVibeConfig(fileContent) {
31440
31934
  function toVibeToolName(category) {
31441
31935
  return CANONICAL_TO_VIBE_TOOL_NAMES[category] ?? category;
31442
31936
  }
31937
+ /**
31938
+ * Whether a canonical category has a Vibe builtin tool to land on. Categories
31939
+ * without one (e.g. `glob`, `notebookedit`) must not emit a
31940
+ * `[tools.<category>]` table: Vibe has no such tool, so a `deny` authored
31941
+ * there would look applied while being silently inert — the dangerous
31942
+ * direction. Unlike `agent` → `task` there is no correct name to rename to,
31943
+ * so the category is skipped with a warning (the grokcli adapter's pattern).
31944
+ */
31945
+ function hasVibeToolName(category) {
31946
+ return Object.hasOwn(CANONICAL_TO_VIBE_TOOL_NAMES, category);
31947
+ }
31443
31948
  function toCanonicalToolName$1(vibeToolName) {
31444
31949
  return VIBE_TO_CANONICAL_TOOL_NAMES[vibeToolName] ?? vibeToolName;
31445
31950
  }
@@ -31457,7 +31962,7 @@ function toStringArray(value) {
31457
31962
  function applyWildcardPermission({ action, toolConfig }) {
31458
31963
  if (action === "allow") toolConfig.permission = "always";
31459
31964
  else if (action === "ask") toolConfig.permission = "ask";
31460
- else if (toolConfig.permission !== void 0) delete toolConfig.permission;
31965
+ else toolConfig.permission = "never";
31461
31966
  }
31462
31967
  function fromVibePermission(value) {
31463
31968
  if (value === "always" || value === "allow") return "allow";
@@ -31476,11 +31981,29 @@ function ensurePermission(permission, category) {
31476
31981
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
31477
31982
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
31478
31983
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
31984
+ const EXECUTION_PROFILES_KEY = "execution_profiles";
31985
+ const DEFAULT_PROFILE_KEY = "default";
31986
+ const PROFILE_ALLOWLIST_KEY = "command_allowlist";
31987
+ const PROFILE_DENYLIST_KEY = "command_denylist";
31479
31988
  const WARP_OVERRIDE_KEYS = [
31480
31989
  "agent_mode_coding_permissions",
31481
31990
  "agent_mode_coding_file_read_allowlist",
31482
31991
  "agent_mode_execute_readonly_commands"
31483
31992
  ];
31993
+ const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
31994
+ const WARP_EXECUTION_PROFILE_KEYS = [
31995
+ "read_files",
31996
+ "apply_code_diffs",
31997
+ "execute_commands",
31998
+ "mcp_permissions",
31999
+ "write_to_pty",
32000
+ "ask_user_question",
32001
+ "run_agents",
32002
+ "computer_use",
32003
+ "directory_allowlist",
32004
+ "mcp_allowlist",
32005
+ "mcp_denylist"
32006
+ ];
31484
32007
  /**
31485
32008
  * Warp's `settings.toml` lives in a different directory per platform (Stable
31486
32009
  * channel). The home directory is resolved by the processor through
@@ -31503,11 +32026,21 @@ function warpSettingsDir() {
31503
32026
  /**
31504
32027
  * Permissions adapter for Warp.
31505
32028
  *
31506
- * Warp gates **shell command** execution through two regex arrays under the
31507
- * `[agents.profiles]` table of the global user `settings.toml`:
31508
- * - `agent_mode_command_execution_allowlist` commands that auto-execute.
31509
- * - `agent_mode_command_execution_denylist` — commands that always require
31510
- * permission (the denylist wins over the allowlist).
32029
+ * Warp gates **shell command** execution through two regex arrays in the
32030
+ * global user `settings.toml`. Since file-backed execution profiles went
32031
+ * Stable (2026-07-28) the authoritative surface is the `default` record of the
32032
+ * `[agents.execution_profiles.<id>]` collection:
32033
+ * - `command_allowlist` commands that auto-execute.
32034
+ * - `command_denylist` — commands that always require permission (the denylist
32035
+ * wins over the allowlist).
32036
+ *
32037
+ * The legacy `[agents.profiles]` keys
32038
+ * (`agent_mode_command_execution_allowlist` / `denylist`) are consumed only
32039
+ * once by Warp's one-shot migration and are ignored afterwards. Both surfaces
32040
+ * are written: the legacy block keeps un-migrated installs and old clients
32041
+ * working, and the `default` execution profile (merged in place, only when the
32042
+ * collection already exists) keeps migrated installs enforcing the lists.
32043
+ * Import prefers the execution profile and falls back to the legacy keys.
31511
32044
  *
31512
32045
  * This surface is **global only** — there is no project-scoped Warp permissions
31513
32046
  * file. rulesync's canonical `permission.bash` patterns map directly (`allow` →
@@ -31525,7 +32058,14 @@ function warpSettingsDir() {
31525
32058
  * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
31526
32059
  * on **import** they are lifted from `settings.toml` into the override, and on
31527
32060
  * **export** they are merged back into `[agents.profiles]` (the override wins).
31528
- * MCP allow/deny is a separate surface not modeled here.
32061
+ *
32062
+ * On migrated installs those legacy keys are inert; their execution-profile
32063
+ * counterparts (`read_files` / `apply_code_diffs` / `execute_commands`
32064
+ * action permissions, `directory_allowlist`, `mcp_allowlist` /
32065
+ * `mcp_denylist`) are authored through the nested `warp.execution_profile`
32066
+ * override, merged into the `default` record of
32067
+ * `[agents.execution_profiles.<id>]` under the same collection-exists guard as
32068
+ * the command lists, and lifted back from the `default` profile on import.
31529
32069
  *
31530
32070
  * The `settings.toml` file holds all of Warp's settings, so the
31531
32071
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -31578,7 +32118,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31578
32118
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
31579
32119
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
31580
32120
  const override = config.warp;
31581
- if (isRecord(override)) Object.assign(profiles, override);
32121
+ const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32122
+ if (isRecord(override)) {
32123
+ const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
32124
+ Object.assign(profiles, legacyOverride);
32125
+ }
31582
32126
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
31583
32127
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
31584
32128
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -31586,6 +32130,13 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31586
32130
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31587
32131
  else delete profiles[DENYLIST_KEY];
31588
32132
  agents.profiles = profiles;
32133
+ mergeIntoDefaultExecutionProfile({
32134
+ agents,
32135
+ mergedAllow,
32136
+ mergedDeny,
32137
+ executionProfileOverride,
32138
+ logger
32139
+ });
31589
32140
  settings.agents = agents;
31590
32141
  return new WarpPermissions({
31591
32142
  outputRoot,
@@ -31605,12 +32156,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31605
32156
  }
31606
32157
  const agents = isRecord(settings.agents) ? settings.agents : {};
31607
32158
  const profiles = isRecord(agents.profiles) ? agents.profiles : {};
32159
+ const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32160
+ const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
31608
32161
  const config = convertWarpToRulesyncPermissions({
31609
- allow: isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
31610
- deny: isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
32162
+ allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
32163
+ deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
31611
32164
  });
31612
32165
  const warpOverride = {};
31613
32166
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
32167
+ if (defaultProfile) {
32168
+ const executionProfileOverride = {};
32169
+ for (const key of WARP_EXECUTION_PROFILE_KEYS) if (defaultProfile[key] !== void 0) executionProfileOverride[key] = defaultProfile[key];
32170
+ if (Object.keys(executionProfileOverride).length > 0) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
32171
+ }
31614
32172
  const result = { ...config };
31615
32173
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
31616
32174
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -31633,6 +32191,36 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31633
32191
  }
31634
32192
  };
31635
32193
  /**
32194
+ * On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
32195
+ * enforcement reads the active `[agents.execution_profiles.<id>]` record.
32196
+ * Merge the command lists (and the `warp.execution_profile` override's
32197
+ * autonomy keys) into the `default` profile in place so every other profile
32198
+ * key and profile ID survives. When the collection does not exist yet the
32199
+ * install is un-migrated — the legacy keys are still live there, and creating
32200
+ * the collection ourselves would mark Warp's one-shot migration complete early
32201
+ * and strand the user's other legacy settings (file-read allowlist, preferred
32202
+ * model), so it is deliberately left unset and the override is skipped with a
32203
+ * warning instead.
32204
+ */
32205
+ function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
32206
+ const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
32207
+ if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
32208
+ if (hasOverrideKeys && logger) logger.warn("The warp.execution_profile permissions override was skipped: settings.toml has no [agents.execution_profiles] collection yet (un-migrated install). Open Warp once to run its settings migration, then re-run rulesync generate.");
32209
+ return;
32210
+ }
32211
+ const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
32212
+ const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32213
+ if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
32214
+ if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
32215
+ else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
32216
+ if (mergedDeny.length > 0) defaultProfile[PROFILE_DENYLIST_KEY] = mergedDeny;
32217
+ else delete defaultProfile[PROFILE_DENYLIST_KEY];
32218
+ executionProfiles[DEFAULT_PROFILE_KEY] = defaultProfile;
32219
+ agents[EXECUTION_PROFILES_KEY] = executionProfiles;
32220
+ const otherProfileIds = Object.keys(executionProfiles).filter((id) => id !== DEFAULT_PROFILE_KEY);
32221
+ if ((mergedDeny.length > 0 || hasOverrideKeys) && otherProfileIds.length > 0 && logger) logger.warn(`Warp command deny rules and execution_profile override keys were written to the 'default' execution profile only; they are not enforced while another profile (${otherProfileIds.join(", ")}) is active.`);
32222
+ }
32223
+ /**
31636
32224
  * Convert rulesync permissions config to Warp command allow/deny regex lists.
31637
32225
  * Only the `bash` category maps; `ask` rules and non-`bash` categories are
31638
32226
  * dropped (the latter with a warning when they carry `deny` rules).
@@ -31649,10 +32237,7 @@ function convertRulesyncToWarpPermissions({ config, logger }) {
31649
32237
  case "allow":
31650
32238
  allow.push(pattern);
31651
32239
  break;
31652
- case "deny":
31653
- deny.push(pattern);
31654
- break;
31655
- case "ask": break;
32240
+ case "deny": deny.push(pattern);
31656
32241
  }
31657
32242
  }
31658
32243
  return {
@@ -32107,7 +32692,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
32107
32692
  ["rovodev", {
32108
32693
  class: RovodevPermissions,
32109
32694
  meta: {
32110
- supportsProject: false,
32695
+ supportsProject: true,
32111
32696
  supportsGlobal: true,
32112
32697
  supportsImport: true
32113
32698
  }
@@ -33304,6 +33889,38 @@ var AugmentcodeSkill = class AugmentcodeSkill extends ToolSkill {
33304
33889
  }
33305
33890
  };
33306
33891
  //#endregion
33892
+ //#region src/features/rules/nested-scan-exclusions.ts
33893
+ /**
33894
+ * Shared exclusion lists for the nested rule-file scans (`AGENTS.md`,
33895
+ * `REASONIX.md`), which walk the whole project tree on import rather than a
33896
+ * rulesync-owned directory.
33897
+ */
33898
+ /**
33899
+ * Dependency trees never scanned for nested rule files, at any depth. A rule
33900
+ * file there describes somebody else's project, and neither name is ever a
33901
+ * package name. Hidden directories are excluded separately, because a rule
33902
+ * file inside one is another tool's generated output (rulesync writes several
33903
+ * itself).
33904
+ */
33905
+ const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
33906
+ /**
33907
+ * Build, vendoring and scratch directories, excluded at the **project root
33908
+ * only**. A top-level `build/` is a build directory; `packages/build/` is a
33909
+ * package, and dropping it silently would lose a real subproject.
33910
+ */
33911
+ const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
33912
+ "vendor",
33913
+ "third_party",
33914
+ "dist",
33915
+ "build",
33916
+ "out",
33917
+ "target",
33918
+ "coverage",
33919
+ "tmp",
33920
+ "temp",
33921
+ "venv"
33922
+ ];
33923
+ //#endregion
33307
33924
  //#region src/features/skills/claudecode-skill.ts
33308
33925
  const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
33309
33926
  name: zod_mini.z.string(),
@@ -33389,6 +34006,41 @@ var ClaudecodeSkill = class extends ToolSkill {
33389
34006
  alternativeSkillRoots: [CLAUDECODE_SCHEDULED_TASKS_DIR_PATH]
33390
34007
  };
33391
34008
  }
34009
+ /**
34010
+ * Claude Code v2.1.178+ also loads skills from **nested** `.claude/skills/`
34011
+ * directories below the working directory (a skill in
34012
+ * `apps/web/.claude/skills/` becomes available when working on files
34013
+ * there). Discover those directories so `rulesync import` sees them —
34014
+ * import-only and lenient, like every configured root; the project-root
34015
+ * `.claude/skills/` stays the sole generation target. The scan reuses the
34016
+ * nested-rule-file exclusions (dependency trees at any depth, build/vendor
34017
+ * dirs at the root, hidden dirs other than the `.claude` being matched) and
34018
+ * never runs in global mode. On a name clash with a root skill, the root
34019
+ * one wins the import (Claude Code itself keeps both under a
34020
+ * directory-qualified name, which rulesync's flat skill namespace cannot
34021
+ * express).
34022
+ *
34023
+ * @see https://code.claude.com/docs/en/skills
34024
+ */
34025
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
34026
+ if (global) return [];
34027
+ const root = toPosixPath(outputRoot);
34028
+ return filterOutPathsInGitIgnoredDirectories({
34029
+ rootDir: outputRoot,
34030
+ filePaths: await findFilesByGlobs([`${root}/*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`], {
34031
+ type: "dir",
34032
+ followSymbolicLinks: false,
34033
+ ignore: [
34034
+ `${root}/**/.*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`,
34035
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
34036
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
34037
+ ]
34038
+ })
34039
+ }).toSorted().map((dirPath) => ({
34040
+ outputRoot,
34041
+ relativeDirPath: (0, node_path.relative)(outputRoot, dirPath)
34042
+ }));
34043
+ }
33392
34044
  getFrontmatter() {
33393
34045
  return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
33394
34046
  }
@@ -36787,7 +37439,8 @@ var TaktSkill = class TaktSkill extends ToolSkill {
36787
37439
  getDirPath() {
36788
37440
  const fullPath = (0, node_path.join)(this.outputRoot, this.relativeDirPath);
36789
37441
  const resolvedFull = (0, node_path.resolve)(fullPath);
36790
- const rel = (0, node_path.relative)((0, node_path.resolve)(this.outputRoot), resolvedFull);
37442
+ const resolvedBase = (0, node_path.resolve)(this.outputRoot);
37443
+ const rel = (0, node_path.relative)(resolvedBase, resolvedFull);
36791
37444
  if (rel.startsWith("..") || node_path.default.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}"`);
36792
37445
  return fullPath;
36793
37446
  }
@@ -37337,6 +37990,23 @@ var ZedSkill = class ZedSkill extends ToolSkill {
37337
37990
  }
37338
37991
  };
37339
37992
  //#endregion
37993
+ //#region src/features/skills/zoocode-skill.ts
37994
+ /**
37995
+ * Skill generator for **Zoo Code** (the community continuation of Roo Code).
37996
+ * Zoo Code keeps Roo's skills layout (`.roo/skills/` project,
37997
+ * `~/.roo/skills/` global) and the `roo:` frontmatter section (`modeSlugs`),
37998
+ * so this target reuses {@link RooSkill} verbatim and only narrows the
37999
+ * targeting.
38000
+ *
38001
+ * @see https://docs.zoocode.dev/features/skills
38002
+ */
38003
+ var ZoocodeSkill = class extends RooSkill {
38004
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
38005
+ const targets = rulesyncSkill.getFrontmatter().targets;
38006
+ return targets.includes("*") || targets.includes("zoocode");
38007
+ }
38008
+ };
38009
+ //#endregion
37340
38010
  //#region src/features/skills/skills-processor.ts
37341
38011
  const SkillsProcessorToolTargetSchema = zod_mini.z.enum(skillsProcessorToolTargetTuple);
37342
38012
  /**
@@ -37357,7 +38027,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37357
38027
  meta: {
37358
38028
  supportsProject: true,
37359
38029
  supportsSimulated: false,
37360
- supportsGlobal: true
38030
+ supportsGlobal: true,
38031
+ lenientImport: true
37361
38032
  }
37362
38033
  }],
37363
38034
  ["aiassistant", {
@@ -37608,6 +38279,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37608
38279
  supportsGlobal: true
37609
38280
  }
37610
38281
  }],
38282
+ ["zoocode", {
38283
+ class: ZoocodeSkill,
38284
+ meta: {
38285
+ supportsProject: true,
38286
+ supportsSimulated: false,
38287
+ supportsGlobal: true
38288
+ }
38289
+ }],
37611
38290
  ["rovodev", {
37612
38291
  class: RovodevSkill,
37613
38292
  meta: {
@@ -37770,7 +38449,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37770
38449
  for (const root of roots) {
37771
38450
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
37772
38451
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37773
- const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
38452
+ const isLenientRoot = configuredRootPaths.has(relativeDirPath) || factory.meta.lenientImport === true || isAgentSkillsInteropRoot(relativeDirPath);
37774
38453
  const skillsDirPath = (0, node_path.join)(rootOutputRoot, relativeDirPath);
37775
38454
  if (!await directoryExists(skillsDirPath)) continue;
37776
38455
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
@@ -37794,7 +38473,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37794
38473
  global: this.global
37795
38474
  });
37796
38475
  } catch (error) {
37797
- if (!isConfiguredRoot) throw error;
38476
+ if (!isLenientRoot) throw error;
37798
38477
  this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
37799
38478
  return null;
37800
38479
  }
@@ -37809,12 +38488,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37809
38488
  const fromFlatFile = factory.class.fromFlatFile;
37810
38489
  const directoryStems = new Set(ownedDirNames);
37811
38490
  const flatFilePaths = (await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has((0, node_path.basename)(filePath, ".md")));
37812
- const flatSkills = await Promise.all(flatFilePaths.map((filePath) => fromFlatFile({
37813
- outputRoot: rootOutputRoot,
37814
- relativeDirPath,
37815
- relativeFilePath: (0, node_path.basename)(filePath),
37816
- global: this.global
37817
- })));
38491
+ const flatSkills = (await Promise.all(flatFilePaths.map(async (filePath) => {
38492
+ try {
38493
+ return await fromFlatFile({
38494
+ outputRoot: rootOutputRoot,
38495
+ relativeDirPath,
38496
+ relativeFilePath: (0, node_path.basename)(filePath),
38497
+ global: this.global
38498
+ });
38499
+ } catch (error) {
38500
+ if (!isLenientRoot) throw error;
38501
+ this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, (0, node_path.basename)(filePath))}: ${formatError(error)}`);
38502
+ return null;
38503
+ }
38504
+ }))).filter((skill) => skill !== null);
37818
38505
  for (const skill of flatSkills) {
37819
38506
  const skillName = skill.getImportIdentity();
37820
38507
  if (seenSkillNames.has(skillName)) continue;
@@ -37887,14 +38574,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37887
38574
  */
37888
38575
  static getSimulatedConventionSection({ skillList }) {
37889
38576
  if (!skillList || skillList.length === 0) return "";
38577
+ const skillListWithAtPrefix = skillList.map((skill) => ({
38578
+ ...skill,
38579
+ path: `@${skill.path}`
38580
+ }));
37890
38581
  return `## Simulated Skills
37891
38582
 
37892
38583
  Simulated skills are specialized capabilities that can be invoked to handle specific types of tasks. When you determine that a skill would be helpful for the current task, read the corresponding SKILL.md file and execute its instructions.
37893
38584
 
37894
- ${(0, _toon_format_toon.encode)({ skillList: skillList.map((skill) => ({
37895
- ...skill,
37896
- path: `@${skill.path}`
37897
- })) })}`;
38585
+ ${(0, _toon_format_toon.encode)({ skillList: skillListWithAtPrefix })}`;
37898
38586
  }
37899
38587
  /**
37900
38588
  * Return the tool targets that this processor supports in global mode
@@ -40498,7 +41186,8 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
40498
41186
  return getHermesagentSharedConfigWritePaths();
40499
41187
  }
40500
41188
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
40501
- return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
41189
+ const slug = subagentSlug(rulesyncSubagent.getRelativePathFromCwd());
41190
+ return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${slug}.json`)];
40502
41191
  }
40503
41192
  toRulesyncSubagent() {
40504
41193
  const slug = (0, node_path.basename)(this.getRelativeFilePath(), ".json");
@@ -41733,7 +42422,7 @@ function stringifyRooModes(modes) {
41733
42422
  *
41734
42423
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
41735
42424
  */
41736
- var RooSubagent = class RooSubagent extends ToolSubagent {
42425
+ var RooSubagent = class extends ToolSubagent {
41737
42426
  modes;
41738
42427
  constructor({ modes, ...rest }) {
41739
42428
  if (rest.validate !== false) {
@@ -41789,7 +42478,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41789
42478
  const mode = this.toRooMode(rulesyncSubagent);
41790
42479
  bySlug.set(mode.slug, mode);
41791
42480
  }
41792
- return new RooSubagent({
42481
+ return new this({
41793
42482
  outputRoot,
41794
42483
  relativeDirPath: this.getSettablePaths().relativeDirPath,
41795
42484
  relativeFilePath: ROO_MODES_FILE_NAME,
@@ -41868,7 +42557,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41868
42557
  }
41869
42558
  const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
41870
42559
  if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
41871
- return new RooSubagent({
42560
+ return new this({
41872
42561
  outputRoot,
41873
42562
  relativeDirPath: paths.relativeDirPath,
41874
42563
  relativeFilePath: (0, node_path.basename)(relativeFilePath),
@@ -41877,7 +42566,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41877
42566
  });
41878
42567
  }
41879
42568
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
41880
- return new RooSubagent({
42569
+ return new this({
41881
42570
  outputRoot,
41882
42571
  relativeDirPath,
41883
42572
  relativeFilePath,
@@ -42123,6 +42812,60 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
42123
42812
  }
42124
42813
  };
42125
42814
  //#endregion
42815
+ //#region src/features/subagents/zoocode-subagent.ts
42816
+ /**
42817
+ * Subagent (custom-mode) generator for **Zoo Code**, the community
42818
+ * continuation of Roo Code. Zoo Code keeps the aggregated `.roomodes` file and
42819
+ * Roo's mode schema, and the shared mode fields keep riding the `roo:`
42820
+ * frontmatter section — one rulesync source must not produce two different
42821
+ * spellings of the same field.
42822
+ *
42823
+ * The post-fork divergence is carried by the `zoocode:` section:
42824
+ * `allowedMcpServers` (Zoo Code v3.60.0+) — a per-mode MCP server allowlist.
42825
+ * "When omitted, all servers are available. When set, only the listed servers
42826
+ * are injected." On import the key is lifted back into `zoocode:` so it
42827
+ * round-trips (and never leaks into the `roo:` section, where the archived
42828
+ * Roo Code would not understand it).
42829
+ *
42830
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code
42831
+ * @see https://docs.zoocode.dev/features/custom-modes
42832
+ */
42833
+ var ZoocodeSubagent = class extends RooSubagent {
42834
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
42835
+ return this.isTargetedByRulesyncSubagentDefault({
42836
+ rulesyncSubagent,
42837
+ toolTarget: "zoocode"
42838
+ });
42839
+ }
42840
+ static toRooMode(rulesyncSubagent) {
42841
+ const mode = super.toRooMode(rulesyncSubagent);
42842
+ const frontmatter = rulesyncSubagent.getFrontmatter();
42843
+ const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
42844
+ if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
42845
+ return mode;
42846
+ }
42847
+ toRulesyncSubagents() {
42848
+ return super.toRulesyncSubagents().map((subagent) => {
42849
+ const frontmatter = subagent.getFrontmatter();
42850
+ const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
42851
+ const rebuilt = {
42852
+ ...frontmatter,
42853
+ targets: ["zoocode"],
42854
+ roo: restRooSection
42855
+ };
42856
+ if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
42857
+ return new RulesyncSubagent({
42858
+ outputRoot: ".",
42859
+ frontmatter: rebuilt,
42860
+ body: subagent.getBody(),
42861
+ relativeDirPath: subagent.getRelativeDirPath(),
42862
+ relativeFilePath: subagent.getRelativeFilePath(),
42863
+ validate: true
42864
+ });
42865
+ });
42866
+ }
42867
+ };
42868
+ //#endregion
42126
42869
  //#region src/features/subagents/subagents-processor.ts
42127
42870
  const SubagentsProcessorToolTargetSchema = zod_mini.z.enum(subagentsProcessorToolTargetTuple);
42128
42871
  /**
@@ -42362,6 +43105,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
42362
43105
  filePattern: ".roomodes"
42363
43106
  }
42364
43107
  }],
43108
+ ["zoocode", {
43109
+ class: ZoocodeSubagent,
43110
+ meta: {
43111
+ supportsSimulated: false,
43112
+ supportsGlobal: false,
43113
+ filePattern: ".roomodes"
43114
+ }
43115
+ }],
42365
43116
  ["rovodev", {
42366
43117
  class: RovodevSubagent,
42367
43118
  meta: {
@@ -42622,11 +43373,13 @@ For example, if the user instructs \`Call planner subagent to plan the refactori
42622
43373
  //#region src/features/rules/tool-rule.ts
42623
43374
  var ToolRule = class extends ToolFile {
42624
43375
  root;
43376
+ localRoot;
42625
43377
  description;
42626
43378
  globs;
42627
- constructor({ root = false, description, globs, ...rest }) {
43379
+ constructor({ root = false, localRoot = false, description, globs, ...rest }) {
42628
43380
  super(rest);
42629
43381
  this.root = root;
43382
+ this.localRoot = localRoot;
42630
43383
  this.description = description;
42631
43384
  this.globs = globs;
42632
43385
  }
@@ -42709,6 +43462,38 @@ var ToolRule = class extends ToolFile {
42709
43462
  isRoot() {
42710
43463
  return this.root;
42711
43464
  }
43465
+ isLocalRoot() {
43466
+ return this.localRoot;
43467
+ }
43468
+ /**
43469
+ * Convert a tool's separate personal local-root file (see
43470
+ * {@link ToolRuleParams.localRoot}) back to a canonical `localRoot: true`
43471
+ * rulesync rule. The rulesync file keeps the tool-side basename
43472
+ * (`CLAUDE.local.md`, `AGENTS.local.md`, ...), which the derived `.gitignore`
43473
+ * already covers via `.rulesync/rules/*.local.md`, so personal content stays
43474
+ * untracked after import.
43475
+ *
43476
+ * `targets` is scoped to the importing tool rather than `"*"`. A wildcard
43477
+ * would spread the personal content to every tool on the next generate —
43478
+ * including being appended into the committed root file of tools without a
43479
+ * separate local file — and a multi-target import would produce several
43480
+ * wildcard localRoot rules, which the per-target validation rejects.
43481
+ */
43482
+ toLocalRootRulesyncRule({ targets }) {
43483
+ return new RulesyncRule({
43484
+ outputRoot: this.getOutputRoot(),
43485
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
43486
+ relativeFilePath: this.getRelativeFilePath(),
43487
+ frontmatter: {
43488
+ root: false,
43489
+ localRoot: true,
43490
+ targets,
43491
+ globs: []
43492
+ },
43493
+ body: this.getFileContent(),
43494
+ validate: true
43495
+ });
43496
+ }
42712
43497
  /**
42713
43498
  * Whether this rule must be left out of the root rule's reference/MCP
42714
43499
  * instruction listings even though it is a non-root survivor. Used by files
@@ -42741,31 +43526,6 @@ function buildToolPath(toolDir, subDir, excludeToolDir) {
42741
43526
  }
42742
43527
  //#endregion
42743
43528
  //#region src/features/rules/agentsmd-rule.ts
42744
- /**
42745
- * Dependency trees never scanned for nested `AGENTS.md` files, at any depth. An
42746
- * `AGENTS.md` there describes somebody else's project, and neither name is ever
42747
- * a package name. Hidden directories are excluded separately, because an
42748
- * `AGENTS.md` inside one is another tool's generated output (rulesync writes
42749
- * several itself).
42750
- */
42751
- const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
42752
- /**
42753
- * Build, vendoring and scratch directories, excluded at the **project root
42754
- * only**. A top-level `build/` is a build directory; `packages/build/` is a
42755
- * package, and dropping it silently would lose a real subproject.
42756
- */
42757
- const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
42758
- "vendor",
42759
- "third_party",
42760
- "dist",
42761
- "build",
42762
- "out",
42763
- "target",
42764
- "coverage",
42765
- "tmp",
42766
- "temp",
42767
- "venv"
42768
- ];
42769
43529
  var AgentsMdRule = class AgentsMdRule extends ToolRule {
42770
43530
  constructor({ fileContent, root, ...rest }) {
42771
43531
  super({
@@ -42822,7 +43582,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
42822
43582
  const normalizedDirPath = relativeDirPath === void 0 ? "." : toPosixPath(relativeDirPath);
42823
43583
  const isNested = relativeFilePath === "AGENTS.md" && normalizedDirPath !== "." && normalizedDirPath !== "" && !normalizedDirPath.startsWith(".");
42824
43584
  const isRoot = !isNested && relativeFilePath === "AGENTS.md" && (normalizedDirPath === "." || normalizedDirPath === "");
42825
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, isNested ? (0, node_path.join)(normalizedDirPath, relativeFilePath) : isRoot ? AGENTSMD_RULE_FILE_NAME : (0, node_path.join)(AGENTSMD_MEMORIES_DIR_PATH, relativeFilePath)));
43585
+ const relativePath = isNested ? (0, node_path.join)(normalizedDirPath, relativeFilePath) : isRoot ? AGENTSMD_RULE_FILE_NAME : (0, node_path.join)(AGENTSMD_MEMORIES_DIR_PATH, relativeFilePath);
43586
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
42826
43587
  return new AgentsMdRule({
42827
43588
  outputRoot,
42828
43589
  relativeDirPath: isNested ? (0, node_path.join)(normalizedDirPath) : isRoot ? this.getSettablePaths().root.relativeDirPath : this.getSettablePaths().nonRoot.relativeDirPath,
@@ -43129,7 +43890,8 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
43129
43890
  });
43130
43891
  }
43131
43892
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43132
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath)));
43893
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
43894
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
43133
43895
  return new AntigravityCliRule({
43134
43896
  outputRoot,
43135
43897
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43590,7 +44352,8 @@ var AugmentcodeLegacyRule = class AugmentcodeLegacyRule extends ToolRule {
43590
44352
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
43591
44353
  const settablePaths = this.getSettablePaths();
43592
44354
  const isRoot = relativeFilePath === settablePaths.root.relativeFilePath;
43593
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, isRoot ? settablePaths.root.relativeFilePath : (0, node_path.join)(settablePaths.nonRoot.relativeDirPath, relativeFilePath)));
44355
+ const relativePath = isRoot ? settablePaths.root.relativeFilePath : (0, node_path.join)(settablePaths.nonRoot.relativeDirPath, relativeFilePath);
44356
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
43594
44357
  return new AugmentcodeLegacyRule({
43595
44358
  outputRoot,
43596
44359
  relativeDirPath: isRoot ? settablePaths.root.relativeDirPath : settablePaths.nonRoot.relativeDirPath,
@@ -43814,7 +44577,8 @@ var ClaudecodeLegacyRule = class ClaudecodeLegacyRule extends ToolRule {
43814
44577
  });
43815
44578
  }
43816
44579
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43817
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath)));
44580
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
44581
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
43818
44582
  return new ClaudecodeLegacyRule({
43819
44583
  outputRoot,
43820
44584
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43935,7 +44699,8 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
43935
44699
  });
43936
44700
  }
43937
44701
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43938
- const filePath = (0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath));
44702
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
44703
+ const filePath = (0, node_path.join)(outputRoot, relativePath);
43939
44704
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
43940
44705
  const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
43941
44706
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44322,7 +45087,8 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
44322
45087
  }
44323
45088
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44324
45089
  const { root } = this.getSettablePaths({ global });
44325
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
45090
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
45091
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
44326
45092
  return new CodexcliRule({
44327
45093
  outputRoot,
44328
45094
  relativeDirPath: root.relativeDirPath,
@@ -44491,7 +45257,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44491
45257
  }) : relativeFilePath === paths.root.relativeFilePath;
44492
45258
  const resolvedRelativeDirPath = relativeDirPath ?? (isRoot ? paths.root.relativeDirPath : paths.nonRoot?.relativeDirPath ?? paths.root.relativeDirPath);
44493
45259
  if (isRoot) {
44494
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath)));
45260
+ const relativePath = (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath);
45261
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
44495
45262
  return new CopilotRule({
44496
45263
  outputRoot,
44497
45264
  relativeDirPath: paths.root.relativeDirPath,
@@ -44503,7 +45270,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44503
45270
  });
44504
45271
  }
44505
45272
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
44506
- const filePath = (0, node_path.join)(outputRoot, (0, node_path.join)(resolvedRelativeDirPath, relativeFilePath));
45273
+ const relativePath = (0, node_path.join)(resolvedRelativeDirPath, relativeFilePath);
45274
+ const filePath = (0, node_path.join)(outputRoot, relativePath);
44507
45275
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
44508
45276
  const result = CopilotRuleFrontmatterSchema.safeParse(frontmatter);
44509
45277
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44775,7 +45543,8 @@ var DeepagentsRule = class DeepagentsRule extends ToolRule {
44775
45543
  }
44776
45544
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44777
45545
  const settablePaths = this.getSettablePaths({ global });
44778
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(settablePaths.root.relativeDirPath, settablePaths.root.relativeFilePath)));
45546
+ const relativePath = (0, node_path.join)(settablePaths.root.relativeDirPath, settablePaths.root.relativeFilePath);
45547
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
44779
45548
  return new DeepagentsRule({
44780
45549
  outputRoot,
44781
45550
  relativeDirPath: settablePaths.root.relativeDirPath,
@@ -45202,7 +45971,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45202
45971
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
45203
45972
  const paths = this.getSettablePaths({ global });
45204
45973
  if (relativeFilePath === paths.root.relativeFilePath) {
45205
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath)));
45974
+ const relativePath = (0, node_path.join)(paths.root.relativeDirPath, paths.root.relativeFilePath);
45975
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
45206
45976
  return new FactorydroidRule({
45207
45977
  outputRoot,
45208
45978
  relativeDirPath: paths.root.relativeDirPath,
@@ -45213,7 +45983,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45213
45983
  });
45214
45984
  }
45215
45985
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45216
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath)));
45986
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
45987
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
45217
45988
  return new FactorydroidRule({
45218
45989
  outputRoot,
45219
45990
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45443,7 +46214,8 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
45443
46214
  }
45444
46215
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
45445
46216
  const { root } = this.getSettablePaths();
45446
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
46217
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
46218
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
45447
46219
  return new HermesagentRule({
45448
46220
  outputRoot,
45449
46221
  relativeDirPath: root.relativeDirPath,
@@ -45554,7 +46326,8 @@ var JunieRule = class JunieRule extends ToolRule {
45554
46326
  });
45555
46327
  }
45556
46328
  const relativeDirPath = this.getSettablePaths().root.relativeDirPath;
45557
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(relativeDirPath, relativeFilePath)));
46329
+ const relativePath = (0, node_path.join)(relativeDirPath, relativeFilePath);
46330
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
45558
46331
  return new JunieRule({
45559
46332
  outputRoot,
45560
46333
  relativeDirPath,
@@ -45654,7 +46427,8 @@ var KiloRule = class KiloRule extends ToolRule {
45654
46427
  });
45655
46428
  }
45656
46429
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45657
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath)));
46430
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
46431
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
45658
46432
  return new KiloRule({
45659
46433
  outputRoot,
45660
46434
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45991,10 +46765,13 @@ var KiroIdeRule = class extends KiroRule {
45991
46765
  //#region src/features/rules/opencode-rule.ts
45992
46766
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
45993
46767
  static getSettablePaths({ global, excludeToolDir } = {}) {
45994
- if (global) return { root: {
45995
- relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
45996
- relativeFilePath: OPENCODE_RULE_FILE_NAME
45997
- } };
46768
+ if (global) return {
46769
+ root: {
46770
+ relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
46771
+ relativeFilePath: OPENCODE_RULE_FILE_NAME
46772
+ },
46773
+ nonRoot: { relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, "memories", excludeToolDir) }
46774
+ };
45998
46775
  return {
45999
46776
  root: {
46000
46777
  relativeDirPath: ".",
@@ -46004,7 +46781,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46004
46781
  };
46005
46782
  }
46006
46783
  static getExtraSharedWritePaths({ global = false } = {}) {
46007
- return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
46784
+ return [OpencodeMcp.getSettablePaths({ global })];
46008
46785
  }
46009
46786
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46010
46787
  const paths = this.getSettablePaths({ global });
@@ -46021,7 +46798,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46021
46798
  });
46022
46799
  }
46023
46800
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
46024
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath)));
46801
+ const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
46802
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
46025
46803
  return new OpenCodeRule({
46026
46804
  outputRoot,
46027
46805
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46136,7 +46914,8 @@ var PiRule = class PiRule extends ToolRule {
46136
46914
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46137
46915
  const { root, appendSystemPrompt } = this.getSettablePaths({ global });
46138
46916
  if (relativeFilePath === "APPEND_SYSTEM.md") {
46139
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(appendSystemPrompt.relativeDirPath, appendSystemPrompt.relativeFilePath)));
46917
+ const relativePath = (0, node_path.join)(appendSystemPrompt.relativeDirPath, appendSystemPrompt.relativeFilePath);
46918
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
46140
46919
  return new PiRule({
46141
46920
  outputRoot,
46142
46921
  relativeDirPath: appendSystemPrompt.relativeDirPath,
@@ -46147,7 +46926,8 @@ var PiRule = class PiRule extends ToolRule {
46147
46926
  appendSystemPrompt: true
46148
46927
  });
46149
46928
  }
46150
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
46929
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
46930
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
46151
46931
  return new PiRule({
46152
46932
  outputRoot,
46153
46933
  relativeDirPath: root.relativeDirPath,
@@ -46443,6 +47223,13 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46443
47223
  toolTarget: "qwencode"
46444
47224
  });
46445
47225
  }
47226
+ /**
47227
+ * The personal local context file lives under `.qwen/`, not at the project
47228
+ * root where the settable root path points, so override the deletion glob.
47229
+ */
47230
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
47231
+ return (0, node_path.join)(outputRoot, QWENCODE_DIR, fileName);
47232
+ }
46446
47233
  };
46447
47234
  //#endregion
46448
47235
  //#region src/features/rules/reasonix-rule.ts
@@ -46460,9 +47247,51 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46460
47247
  relativeFilePath: REASONIX_RULE_FILE_NAME
46461
47248
  } };
46462
47249
  }
46463
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47250
+ /**
47251
+ * Context Engine v2 (v1.18.0) walks from the workspace root to the target
47252
+ * path loading per-directory instruction files, so nested `REASONIX.md`
47253
+ * files are a real scoping surface: "Deeper directories beat broader
47254
+ * directories." The scan mirrors the AGENTS.md standard's nested discovery
47255
+ * (same exclusions, import-only, project scope).
47256
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v1.18.0/docs/SESSION_MEMORY_RETRIEVAL.md
47257
+ */
47258
+ static getNestedFilePatterns({ outputRoot }) {
47259
+ const root = toPosixPath(outputRoot);
47260
+ return {
47261
+ include: [`${root}/**/${REASONIX_RULE_FILE_NAME}`],
47262
+ ignore: [
47263
+ `${root}/${REASONIX_RULE_FILE_NAME}`,
47264
+ `${root}/**/.*/**`,
47265
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
47266
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
47267
+ ]
47268
+ };
47269
+ }
47270
+ /**
47271
+ * The subproject directory this rule scopes, or `undefined` for the root
47272
+ * file (project or global).
47273
+ */
47274
+ getSubprojectPath() {
47275
+ if (this.isRoot()) return;
47276
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
47277
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
47278
+ return relativeDirPath;
47279
+ }
47280
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
46464
47281
  const { root } = this.getSettablePaths({ global });
46465
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
47282
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
47283
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, REASONIX_RULE_FILE_NAME));
47284
+ return new ReasonixRule({
47285
+ outputRoot,
47286
+ relativeDirPath: overrideDirPath,
47287
+ relativeFilePath: REASONIX_RULE_FILE_NAME,
47288
+ fileContent,
47289
+ validate,
47290
+ root: false
47291
+ });
47292
+ }
47293
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
47294
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
46466
47295
  return new ReasonixRule({
46467
47296
  outputRoot,
46468
47297
  relativeDirPath: root.relativeDirPath,
@@ -46474,7 +47303,17 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46474
47303
  }
46475
47304
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
46476
47305
  const { root } = this.getSettablePaths({ global });
46477
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47306
+ const frontmatter = rulesyncRule.getFrontmatter();
47307
+ const isRoot = frontmatter.root ?? false;
47308
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
47309
+ if (!global && !isRoot && subprojectPath) return new ReasonixRule({
47310
+ outputRoot,
47311
+ relativeDirPath: (0, node_path.join)(subprojectPath),
47312
+ relativeFilePath: REASONIX_RULE_FILE_NAME,
47313
+ fileContent: rulesyncRule.getBody(),
47314
+ validate,
47315
+ root: false
47316
+ });
46478
47317
  return new ReasonixRule({
46479
47318
  outputRoot,
46480
47319
  relativeDirPath: root.relativeDirPath,
@@ -46485,7 +47324,22 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46485
47324
  });
46486
47325
  }
46487
47326
  toRulesyncRule() {
46488
- return this.toRulesyncRuleDefault();
47327
+ const subprojectPath = this.getSubprojectPath();
47328
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
47329
+ const slug = subprojectPath.replaceAll("/", "-");
47330
+ return new RulesyncRule({
47331
+ outputRoot: this.getOutputRoot(),
47332
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
47333
+ relativeFilePath: `${slug}-reasonix.md`,
47334
+ frontmatter: {
47335
+ targets: ["reasonix"],
47336
+ root: false,
47337
+ globs: [`${subprojectPath}/**/*`],
47338
+ agentsmd: { subprojectPath }
47339
+ },
47340
+ body: this.getFileContent(),
47341
+ validate: true
47342
+ });
46489
47343
  }
46490
47344
  validate() {
46491
47345
  return {
@@ -46655,7 +47509,7 @@ var RooRule = class RooRule extends ToolRule {
46655
47509
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
46656
47510
  * at the project root, not under `.roo/` (mirrors rovodev).
46657
47511
  */
46658
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47512
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46659
47513
  return (0, node_path.join)(outputRoot, fileName);
46660
47514
  }
46661
47515
  };
@@ -46787,19 +47641,6 @@ var RovodevRule = class RovodevRule extends ToolRule {
46787
47641
  }));
46788
47642
  }
46789
47643
  toRulesyncRule() {
46790
- if (this.getRelativeFilePath() === "AGENTS.local.md") return new RulesyncRule({
46791
- outputRoot: this.getOutputRoot(),
46792
- relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
46793
- relativeFilePath: ROVODEV_LEGACY_RULE_FILE_NAME,
46794
- frontmatter: {
46795
- targets: ["rovodev"],
46796
- root: false,
46797
- localRoot: true,
46798
- globs: []
46799
- },
46800
- body: this.getFileContent(),
46801
- validate: true
46802
- });
46803
47644
  if (!this.isRoot()) return new RulesyncRule({
46804
47645
  outputRoot: this.getOutputRoot(),
46805
47646
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -46856,7 +47697,7 @@ var RovodevRule = class RovodevRule extends ToolRule {
46856
47697
  };
46857
47698
  }
46858
47699
  /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
46859
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47700
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46860
47701
  return (0, node_path.join)(outputRoot, fileName);
46861
47702
  }
46862
47703
  };
@@ -47046,15 +47887,20 @@ var WarpRule = class WarpRule extends ToolRule {
47046
47887
  root: root ?? false
47047
47888
  });
47048
47889
  }
47049
- static getSettablePaths(_options = {}) {
47890
+ static getSettablePaths({ global = false } = {}) {
47891
+ if (global) return { root: {
47892
+ relativeDirPath: WARP_GLOBAL_RULE_DIR,
47893
+ relativeFilePath: WARP_RULE_FILE_NAME
47894
+ } };
47050
47895
  return { root: {
47051
47896
  relativeDirPath: ".",
47052
47897
  relativeFilePath: WARP_RULE_FILE_NAME
47053
47898
  } };
47054
47899
  }
47055
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
47056
- const { root } = this.getSettablePaths();
47057
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
47900
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47901
+ const { root } = this.getSettablePaths({ global });
47902
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
47903
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
47058
47904
  return new WarpRule({
47059
47905
  outputRoot,
47060
47906
  relativeDirPath: root.relativeDirPath,
@@ -47064,8 +47910,8 @@ var WarpRule = class WarpRule extends ToolRule {
47064
47910
  root: true
47065
47911
  });
47066
47912
  }
47067
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
47068
- const { root } = this.getSettablePaths();
47913
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
47914
+ const { root } = this.getSettablePaths({ global });
47069
47915
  const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47070
47916
  return new WarpRule({
47071
47917
  outputRoot,
@@ -47085,8 +47931,8 @@ var WarpRule = class WarpRule extends ToolRule {
47085
47931
  error: null
47086
47932
  };
47087
47933
  }
47088
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
47089
- const { root } = this.getSettablePaths();
47934
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
47935
+ const { root } = this.getSettablePaths({ global });
47090
47936
  const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
47091
47937
  return new WarpRule({
47092
47938
  outputRoot,
@@ -47179,6 +48025,27 @@ var ZedRule = class ZedRule extends ToolRule {
47179
48025
  }
47180
48026
  };
47181
48027
  //#endregion
48028
+ //#region src/features/rules/zoocode-rule.ts
48029
+ /**
48030
+ * Rule generator for **Zoo Code**, the community continuation of Roo Code
48031
+ * (named by the Roo shutdown notice; releases continue Roo's numbering from
48032
+ * v3.54.0). Zoo Code still resolves `~/.roo` and the project `.roo/` layout —
48033
+ * the `.zoo` renaming is confined to provider/auth code — so this target
48034
+ * reuses {@link RooRule}'s behavior verbatim and only narrows the targeting to
48035
+ * the `zoocode` tool name.
48036
+ *
48037
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code
48038
+ * @see https://docs.zoocode.dev
48039
+ */
48040
+ var ZoocodeRule = class extends RooRule {
48041
+ static isTargetedByRulesyncRule(rulesyncRule) {
48042
+ return this.isTargetedByRulesyncRuleDefault({
48043
+ rulesyncRule,
48044
+ toolTarget: "zoocode"
48045
+ });
48046
+ }
48047
+ };
48048
+ //#endregion
47182
48049
  //#region src/features/rules/rules-processor.ts
47183
48050
  const RulesProcessorToolTargetSchema = zod_mini.z.enum(rulesProcessorToolTargetTuple);
47184
48051
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
@@ -47441,6 +48308,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47441
48308
  supportsGlobal: true,
47442
48309
  ruleDiscoveryMode: "toon",
47443
48310
  mcpInstructionsRegistrar: OpencodeMcp,
48311
+ mcpInstructionsRegistrarGlobal: true,
47444
48312
  collisionPolicy: "compose"
47445
48313
  }
47446
48314
  }],
@@ -47459,6 +48327,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47459
48327
  extension: "md",
47460
48328
  supportsGlobal: true,
47461
48329
  ruleDiscoveryMode: "auto",
48330
+ localRootMode: "separate-local-file",
48331
+ localRootFileName: QWENCODE_LOCAL_RULE_FILE_NAME,
47462
48332
  additionalConventions: { subagents: { subagentClass: QwencodeSubagent } }
47463
48333
  }
47464
48334
  }],
@@ -47503,6 +48373,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47503
48373
  localRootFileName: "AGENTS.local.md"
47504
48374
  }
47505
48375
  }],
48376
+ ["zoocode", {
48377
+ class: ZoocodeRule,
48378
+ meta: {
48379
+ extension: "md",
48380
+ supportsGlobal: true,
48381
+ ruleDiscoveryMode: "auto",
48382
+ localRootMode: "separate-local-file",
48383
+ localRootFileName: "AGENTS.local.md"
48384
+ }
48385
+ }],
47506
48386
  ["takt", {
47507
48387
  class: TaktRule,
47508
48388
  meta: {
@@ -47523,7 +48403,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47523
48403
  class: WarpRule,
47524
48404
  meta: {
47525
48405
  extension: "md",
47526
- supportsGlobal: false,
48406
+ supportsGlobal: true,
47527
48407
  ruleDiscoveryMode: "toon",
47528
48408
  collisionPolicy: "fold"
47529
48409
  }
@@ -47675,18 +48555,19 @@ var RulesProcessor = class extends FeatureProcessor {
47675
48555
  /**
47676
48556
  * Non-root rules of some tools are not auto-loaded; the tool's MCP feature
47677
48557
  * registers them in its shared config's `instructions` key. The root rule is
47678
- * auto-loaded and never registered. Project scope only.
48558
+ * auto-loaded and never registered. Global scope opt-in via
48559
+ * `mcpInstructionsRegistrarGlobal`.
47679
48560
  */
47680
48561
  async buildMcpInstructionFiles({ toolRules, meta }) {
47681
- if (!meta.mcpInstructionsRegistrar || this.global) return [];
48562
+ if (!meta.mcpInstructionsRegistrar || this.global && !meta.mcpInstructionsRegistrarGlobal) return [];
47682
48563
  const instructionPaths = toolRules.filter((rule) => !rule.isRoot() && !rule.isExcludedFromRootReferences()).map((rule) => toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
47683
- if (instructionPaths.length === 0) return [];
47684
- return [await meta.mcpInstructionsRegistrar.fromInstructions({
48564
+ const registered = await meta.mcpInstructionsRegistrar.fromInstructions({
47685
48565
  outputRoot: this.outputRoot,
47686
48566
  instructions: instructionPaths,
47687
48567
  validate: true,
47688
48568
  global: this.global
47689
- })];
48569
+ });
48570
+ return registered ? [registered] : [];
47690
48571
  }
47691
48572
  /**
47692
48573
  * For tools that don't create a separate conventions rule, prepend the
@@ -47817,45 +48698,58 @@ var RulesProcessor = class extends FeatureProcessor {
47817
48698
  const rootRule = toolRules.find((rule) => rule.isRoot());
47818
48699
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
47819
48700
  }
47820
- buildLocalRootFile({ factory, fileName, body }) {
48701
+ buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
47821
48702
  if (factory.class === ClaudecodeRule) {
47822
48703
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
47823
48704
  return new ClaudecodeRule({
47824
48705
  outputRoot: this.outputRoot,
47825
- relativeDirPath: paths.root.relativeDirPath,
48706
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47826
48707
  relativeFilePath: fileName,
47827
48708
  frontmatter: {},
47828
48709
  body,
47829
48710
  validate: true,
47830
- root: true
48711
+ root: true,
48712
+ localRoot
47831
48713
  });
47832
48714
  }
47833
48715
  if (factory.class === ClaudecodeLegacyRule) {
47834
48716
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
47835
48717
  return new ClaudecodeLegacyRule({
47836
48718
  outputRoot: this.outputRoot,
47837
- relativeDirPath: paths.root.relativeDirPath,
48719
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47838
48720
  relativeFilePath: fileName,
47839
48721
  fileContent: body,
47840
48722
  validate: true,
47841
- root: true
48723
+ root: true,
48724
+ localRoot
47842
48725
  });
47843
48726
  }
47844
48727
  if (factory.class === RovodevRule) return new RovodevRule({
47845
48728
  outputRoot: this.outputRoot,
47846
- relativeDirPath: ".",
48729
+ relativeDirPath: relativeDirPath ?? ".",
47847
48730
  relativeFilePath: fileName,
47848
48731
  fileContent: body,
47849
48732
  validate: true,
47850
- root: true
48733
+ root: true,
48734
+ localRoot
47851
48735
  });
47852
48736
  if (factory.class === RooRule) return new RooRule({
47853
48737
  outputRoot: this.outputRoot,
47854
- relativeDirPath: ".",
48738
+ relativeDirPath: relativeDirPath ?? ".",
47855
48739
  relativeFilePath: fileName,
47856
48740
  fileContent: body,
47857
48741
  validate: true,
47858
- root: true
48742
+ root: true,
48743
+ localRoot
48744
+ });
48745
+ if (factory.class === QwencodeRule) return new QwencodeRule({
48746
+ outputRoot: this.outputRoot,
48747
+ relativeDirPath: relativeDirPath ?? ".qwen",
48748
+ relativeFilePath: fileName,
48749
+ fileContent: body,
48750
+ validate: true,
48751
+ root: true,
48752
+ localRoot
47859
48753
  });
47860
48754
  return null;
47861
48755
  }
@@ -47893,6 +48787,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
47893
48787
  async convertToolFilesToRulesyncFiles(toolFiles) {
47894
48788
  const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
47895
48789
  const rulesyncRules = toolRules.map((toolRule) => {
48790
+ if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
47896
48791
  return toolRule.toRulesyncRule();
47897
48792
  });
47898
48793
  const claimedBy = /* @__PURE__ */ new Map();
@@ -48026,20 +48921,34 @@ As this project's AI coding tool, you must follow the additional conventions bel
48026
48921
  })();
48027
48922
  this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
48028
48923
  const localRootToolRules = await (async () => {
48029
- if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
48924
+ if (this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
48030
48925
  const fileName = factory.meta.localRootFileName;
48031
- if (factory.class.getLocalRootDeletionGlob) {
48032
- const filePaths = await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
48926
+ const filePaths = await (async () => {
48927
+ if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
48033
48928
  outputRoot: this.outputRoot,
48034
48929
  fileName
48035
48930
  }));
48036
- return buildDeletionRulesFromPaths(filePaths);
48037
- }
48038
- if (!settablePaths.root) return [];
48039
- const filePaths = await findFilesWithFallback((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName));
48040
- return buildDeletionRulesFromPaths(filePaths);
48931
+ if (!settablePaths.root) return [];
48932
+ return await findFilesWithFallback((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName));
48933
+ })();
48934
+ if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
48935
+ return (await Promise.all(filePaths.map(async (filePath) => {
48936
+ const relativeDirPath = resolveRelativeDirPath(filePath);
48937
+ checkPathTraversal({
48938
+ relativePath: relativeDirPath,
48939
+ intendedRootDir: this.outputRoot
48940
+ });
48941
+ const body = await readFileContent(filePath);
48942
+ return this.buildLocalRootFile({
48943
+ factory,
48944
+ fileName: (0, node_path.basename)(filePath),
48945
+ body,
48946
+ relativeDirPath,
48947
+ localRoot: true
48948
+ });
48949
+ }))).filter((rule) => rule !== null);
48041
48950
  })();
48042
- this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
48951
+ this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files`);
48043
48952
  const rootMirrorDeletionRules = await (async () => {
48044
48953
  const rootMirror = factory.class.getRootMirror?.();
48045
48954
  if (!forDeletion || this.global || !rootMirror) return [];
@@ -48165,13 +49074,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
48165
49074
  const lines = [];
48166
49075
  lines.push("Please also reference the following rules as needed. The list below is provided in TOON format, and `@` stands for the project root directory.");
48167
49076
  lines.push("");
48168
- const toonContent = (0, _toon_format_toon.encode)({ rules: toolRulesWithoutRoot.map((toolRule) => {
49077
+ const rules = toolRulesWithoutRoot.map((toolRule) => {
48169
49078
  const frontmatter = toolRule.toRulesyncRule().getFrontmatter();
48170
49079
  const rule = { path: `@${toolRule.getRelativePathFromCwd()}` };
48171
49080
  if (frontmatter.description) rule.description = frontmatter.description;
48172
49081
  if (frontmatter.globs && frontmatter.globs.length > 0) rule.applyTo = frontmatter.globs;
48173
49082
  return rule;
48174
- }) });
49083
+ });
49084
+ const toonContent = (0, _toon_format_toon.encode)({ rules });
48175
49085
  lines.push(toonContent);
48176
49086
  return lines.join("\n") + "\n\n";
48177
49087
  }
@@ -49333,8 +50243,8 @@ async function generateIgnoreCore(params) {
49333
50243
  allPaths.push(...result.paths);
49334
50244
  if (result.hasDiff) hasDiff = true;
49335
50245
  } catch (error) {
49336
- logger.warn(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
49337
- continue;
50246
+ logger.error(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
50247
+ throw error;
49338
50248
  }
49339
50249
  }
49340
50250
  return {
@@ -49606,8 +50516,8 @@ async function generatePermissionsCore(params) {
49606
50516
  allPaths.push(...result.paths);
49607
50517
  if (result.hasDiff) hasDiff = true;
49608
50518
  } catch (error) {
49609
- logger.warn(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
49610
- continue;
50519
+ logger.error(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
50520
+ throw error;
49611
50521
  }
49612
50522
  }
49613
50523
  return {
@@ -50171,6 +51081,18 @@ Object.defineProperty(exports, "PACKAGING_TOOL_TARGETS", {
50171
51081
  return PACKAGING_TOOL_TARGETS;
50172
51082
  }
50173
51083
  });
51084
+ Object.defineProperty(exports, "QWENCODE_DIR", {
51085
+ enumerable: true,
51086
+ get: function() {
51087
+ return QWENCODE_DIR;
51088
+ }
51089
+ });
51090
+ Object.defineProperty(exports, "QWENCODE_LOCAL_RULE_FILE_NAME", {
51091
+ enumerable: true,
51092
+ get: function() {
51093
+ return QWENCODE_LOCAL_RULE_FILE_NAME;
51094
+ }
51095
+ });
50174
51096
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
50175
51097
  enumerable: true,
50176
51098
  get: function() {