rulesync 16.4.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",
@@ -3055,7 +3069,6 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
3055
3069
  postToolUse: "tool.execute.after",
3056
3070
  stop: "session.idle",
3057
3071
  afterFileEdit: "file.edited",
3058
- afterShellExecution: "command.executed",
3059
3072
  permissionRequest: "permission.asked",
3060
3073
  preCompact: "experimental.session.compacting",
3061
3074
  postCompact: "session.compacted",
@@ -4122,10 +4135,22 @@ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
4122
4135
  * category; the shared `permission` block still drives the base permission and
4123
4136
  * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
4124
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
+ *
4125
4145
  * @example
4126
4146
  * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
4147
+ * @example
4148
+ * { "enabled_tools": ["bash", "read_file", "grep"] }
4127
4149
  */
4128
- 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
+ });
4129
4154
  /**
4130
4155
  * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
4131
4156
  * autonomy settings with no canonical permission category — `approvalMode`
@@ -4197,7 +4222,10 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
4197
4222
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
4198
4223
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4199
4224
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
4200
- 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()))
4201
4229
  });
4202
4230
  /**
4203
4231
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -4228,12 +4256,69 @@ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
4228
4256
  * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
4229
4257
  * Fields placed here are merged into `[agents.profiles]` of Warp's global
4230
4258
  * `settings.toml`, while the shared `permission` block continues to drive the
4231
- * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
4232
- * 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.
4233
4265
  *
4234
4266
  * @example
4235
- * { "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).
4236
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
+ });
4237
4322
  const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
4238
4323
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
4239
4324
  agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.enum([
@@ -4242,7 +4327,8 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
4242
4327
  "allow_reading_specific_files"
4243
4328
  ])),
4244
4329
  agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
4245
- 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)
4246
4332
  });
4247
4333
  /**
4248
4334
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -4974,7 +5060,8 @@ var AiDir = class {
4974
5060
  getDirPath() {
4975
5061
  const fullPath = node_path.default.join(this.outputRoot, this.relativeDirPath, this.dirName);
4976
5062
  const resolvedFull = (0, node_path.resolve)(fullPath);
4977
- 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);
4978
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}"`);
4979
5066
  return fullPath;
4980
5067
  }
@@ -5257,6 +5344,9 @@ const RulesyncSubagentFrontmatterSchema = zod_mini.z.looseObject({
5257
5344
  customInstructions: zod_mini.z.optional(zod_mini.z.string()),
5258
5345
  groups: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.unknown()))
5259
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())) })),
5260
5350
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({
5261
5351
  agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
5262
5352
  display_name: zod_mini.z.optional(zod_mini.z.string()),
@@ -5396,7 +5486,6 @@ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
5396
5486
  const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
5397
5487
  const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
5398
5488
  const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5399
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5400
5489
  const ROVODEV_MCP_FILE_NAME = "mcp.json";
5401
5490
  const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5402
5491
  const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
@@ -6615,6 +6704,7 @@ const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6615
6704
  const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6616
6705
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6617
6706
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6707
+ const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
6618
6708
  /**
6619
6709
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
6620
6710
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -6954,6 +7044,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6954
7044
  permissions: {
6955
7045
  kind: "replace-owned-keys",
6956
7046
  ownedKeys: ["permission"]
7047
+ },
7048
+ rules: {
7049
+ kind: "replace-owned-keys",
7050
+ ownedKeys: ["instructions"]
6957
7051
  }
6958
7052
  }
6959
7053
  },
@@ -7071,6 +7165,19 @@ const SHARED_CONFIG_OWNERSHIP = {
7071
7165
  ]
7072
7166
  }
7073
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
+ }
7074
7181
  }
7075
7182
  };
7076
7183
  /**
@@ -7492,7 +7599,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7492
7599
  class: CursorCheck,
7493
7600
  meta: {
7494
7601
  supportsGlobal: false,
7495
- filePattern: CURSOR_BUGBOT_FILE_NAME
7602
+ filePattern: CURSOR_BUGBOT_FILE_NAME,
7603
+ committedOutput: true
7496
7604
  }
7497
7605
  }],
7498
7606
  ["hermesagent", {
@@ -7506,7 +7614,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7506
7614
  class: RovodevCheck,
7507
7615
  meta: {
7508
7616
  supportsGlobal: false,
7509
- filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7617
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME,
7618
+ committedOutput: true
7510
7619
  }
7511
7620
  }],
7512
7621
  ["takt", {
@@ -9668,7 +9777,8 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9668
9777
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9669
9778
  const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => (0, node_path.basename)(command.getRelativeFilePath(), ".md")));
9670
9779
  if (commandNames.size === 0) return;
9671
- 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));
9672
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.`);
9673
9783
  }
9674
9784
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -9703,6 +9813,20 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9703
9813
  function toolSkillSearchRoots(paths) {
9704
9814
  return [paths.relativeDirPath, ...paths.alternativeSkillRoots ?? []];
9705
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
+ }
9706
9830
  /** Ordered import roots: managed roots first, followed by read-only discovery roots. */
9707
9831
  function toolSkillImportRoots(paths) {
9708
9832
  return [...toolSkillSearchRoots(paths), ...paths.importOnlySkillRoots ?? []];
@@ -11646,7 +11770,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11646
11770
  };
11647
11771
  const body = rulesyncCommand.getBody();
11648
11772
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
11649
- return new RooCommand({
11773
+ return new this({
11650
11774
  outputRoot,
11651
11775
  frontmatter: rooFrontmatter,
11652
11776
  body,
@@ -11683,7 +11807,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11683
11807
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
11684
11808
  const result = RooCommandFrontmatterSchema.safeParse(frontmatter);
11685
11809
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
11686
- return new RooCommand({
11810
+ return new this({
11687
11811
  outputRoot,
11688
11812
  relativeDirPath: RooCommand.getSettablePaths().relativeDirPath,
11689
11813
  relativeFilePath,
@@ -11694,7 +11818,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11694
11818
  });
11695
11819
  }
11696
11820
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
11697
- return new RooCommand({
11821
+ return new this({
11698
11822
  outputRoot,
11699
11823
  relativeDirPath,
11700
11824
  relativeFilePath,
@@ -12152,6 +12276,39 @@ var WarpCommand = class WarpCommand extends ToolCommand {
12152
12276
  }
12153
12277
  };
12154
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
12155
12312
  //#region src/features/commands/commands-processor.ts
12156
12313
  const CommandsProcessorToolTargetSchema = zod_mini.z.enum(commandsProcessorToolTargetTuple);
12157
12314
  /**
@@ -12411,6 +12568,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
12411
12568
  supportsSubdirectory: true
12412
12569
  }
12413
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
+ }],
12414
12581
  ["rovodev", {
12415
12582
  class: RovodevCommand,
12416
12583
  meta: {
@@ -13790,16 +13957,17 @@ var ClaudecodeHooks = class extends ToolHooks {
13790
13957
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13791
13958
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
13792
13959
  const config = rulesyncHooks.getJson();
13960
+ const claudeHooks = canonicalToToolHooks({
13961
+ config,
13962
+ toolOverrideHooks: config.claudecode?.hooks,
13963
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
13964
+ logger
13965
+ });
13793
13966
  const fileContent = applySharedConfigPatch({
13794
13967
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
13795
13968
  feature: "hooks",
13796
13969
  existingContent,
13797
- patch: { hooks: canonicalToToolHooks({
13798
- config,
13799
- toolOverrideHooks: config.claudecode?.hooks,
13800
- converterConfig: CLAUDE_CONVERTER_CONFIG,
13801
- logger
13802
- }) },
13970
+ patch: { hooks: claudeHooks },
13803
13971
  filePath
13804
13972
  });
13805
13973
  return new this({
@@ -15553,6 +15721,30 @@ const NAMED_HOOK_MATCHER_SUBJECTS = {
15553
15721
  "tool.execute.after": "input.tool",
15554
15722
  "experimental.session.compacting": null
15555
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
+ };
15556
15748
  function escapeForTemplateLiteral(command) {
15557
15749
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
15558
15750
  }
@@ -15573,15 +15765,19 @@ function validateAndSanitizeMatcher(matcher) {
15573
15765
  */
15574
15766
  function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
15575
15767
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
15576
- const toolEvent = eventMap[canonicalEvent];
15768
+ const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
15769
+ const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
15577
15770
  if (!toolEvent) continue;
15771
+ const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
15578
15772
  const handlers = [];
15579
15773
  for (const def of definitions) {
15580
15774
  if ((def.type ?? "command") !== "command") continue;
15581
15775
  if (!def.command) continue;
15776
+ if (def.matcher && !matcherSupported) continue;
15582
15777
  handlers.push({
15583
15778
  command: def.command,
15584
- matcher: def.matcher ? def.matcher : void 0
15779
+ matcher: def.matcher ? def.matcher : void 0,
15780
+ ...shellGate ? { toolGate: shellGate.tool } : {}
15585
15781
  });
15586
15782
  }
15587
15783
  if (handlers.length > 0) {
@@ -15618,7 +15814,11 @@ function buildNamedEventBodyLines(namedEventHandlers) {
15618
15814
  bodyLines.push(` "${eventName}": async (input) => {`);
15619
15815
  for (const handler of handlers) {
15620
15816
  const escapedCommand = escapeForTemplateLiteral(handler.command);
15621
- 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) {
15622
15822
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
15623
15823
  bodyLines.push(" {");
15624
15824
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
@@ -17348,7 +17548,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17348
17548
  },
17349
17549
  supportedEvents: KILO_HOOK_EVENTS,
17350
17550
  supportedHookTypes: ["command"],
17351
- supportsMatcher: true
17551
+ supportsMatcher: true,
17552
+ matcherEvents: ["preToolUse", "postToolUse"]
17352
17553
  }],
17353
17554
  ["opencode", {
17354
17555
  class: OpencodeHooks,
@@ -17359,7 +17560,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17359
17560
  },
17360
17561
  supportedEvents: OPENCODE_HOOK_EVENTS,
17361
17562
  supportedHookTypes: ["command"],
17362
- supportsMatcher: true
17563
+ supportsMatcher: true,
17564
+ matcherEvents: ["preToolUse", "postToolUse"]
17363
17565
  }],
17364
17566
  ["pi", {
17365
17567
  class: PiHooks,
@@ -19033,6 +19235,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
19033
19235
  ["qwencode", { class: QwencodeIgnore }],
19034
19236
  ["reasonix", { class: ReasonixIgnore }],
19035
19237
  ["roo", { class: RooIgnore }],
19238
+ ["zoocode", { class: RooIgnore }],
19036
19239
  ["devin", { class: DevinIgnore }],
19037
19240
  ["vibe", { class: VibeIgnore }],
19038
19241
  ["warp", { class: WarpIgnore }],
@@ -20402,7 +20605,6 @@ const resolveCopilotcliServerType = (server) => {
20402
20605
  case "streamable-http": return "http";
20403
20606
  case "stdio":
20404
20607
  case "local": return declared;
20405
- default: break;
20406
20608
  }
20407
20609
  if (server.command !== void 0) return "stdio";
20408
20610
  return isRemoteMcpServer(server) ? "http" : "stdio";
@@ -22175,9 +22377,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22175
22377
  fileContent = await readFileContentOrNull(jsonPath);
22176
22378
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
22177
22379
  }
22380
+ if (instructions.length === 0 && fileContent === null) return null;
22178
22381
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
22179
22382
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22180
- 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();
22181
22386
  return new KiloMcp({
22182
22387
  outputRoot,
22183
22388
  relativeDirPath: basePaths.relativeDirPath,
@@ -22186,7 +22391,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22186
22391
  fileKey: sharedConfigFileKey(basePaths),
22187
22392
  feature: "rules",
22188
22393
  existingContent: fileContent ?? "",
22189
- patch: { instructions: mergedInstructions },
22394
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22190
22395
  filePath: (0, node_path.join)(jsonDir, relativeFilePath)
22191
22396
  }),
22192
22397
  validate
@@ -22908,6 +23113,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22908
23113
  static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
22909
23114
  const basePaths = this.getSettablePaths({ global });
22910
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);
22911
23118
  let fileContent = null;
22912
23119
  let relativeFilePath = OPENCODE_JSONC_FILE_NAME;
22913
23120
  const jsoncPath = (0, node_path.join)(jsonDir, OPENCODE_JSONC_FILE_NAME);
@@ -22917,9 +23124,15 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22917
23124
  fileContent = await readFileContentOrNull(jsonPath);
22918
23125
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
22919
23126
  }
23127
+ if (instructions.length === 0 && fileContent === null) return null;
22920
23128
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
22921
23129
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22922
- 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();
22923
23136
  return new OpencodeMcp({
22924
23137
  outputRoot,
22925
23138
  relativeDirPath: basePaths.relativeDirPath,
@@ -22928,7 +23141,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22928
23141
  fileKey: sharedConfigFileKey(basePaths),
22929
23142
  feature: "rules",
22930
23143
  existingContent: fileContent ?? "",
22931
- patch: { instructions: mergedInstructions },
23144
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22932
23145
  filePath: (0, node_path.join)(jsonDir, relativeFilePath)
22933
23146
  }),
22934
23147
  validate
@@ -23368,9 +23581,13 @@ function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
23368
23581
  return parsed;
23369
23582
  }
23370
23583
  /**
23371
- * Rovodev MCP: global only at ~/.rovodev/mcp.json.
23372
- * Same shape as Cursor: { mcpServers: { ... } }. See Rovodev MCP docs.
23373
- * 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.
23374
23591
  */
23375
23592
  /**
23376
23593
  * Rovo Dev documents the per-server transport key as `transport`, with the
@@ -23396,11 +23613,7 @@ function lookupTransport(map, key) {
23396
23613
  return Object.hasOwn(map, key) ? map[key] : void 0;
23397
23614
  }
23398
23615
  function toRovodevServer(name, server, logger) {
23399
- const { type, transport, disabled, ...rest } = server;
23400
- if (disabled === true) {
23401
- logger?.warn(`Rovo Dev MCP: skipping "${name}" because it is disabled and mcp.json has no disable flag.`);
23402
- return null;
23403
- }
23616
+ const { type, transport, disabled: _disabled, ...rest } = server;
23404
23617
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
23405
23618
  if (declared === void 0) return rest;
23406
23619
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -23422,6 +23635,44 @@ function fromRovodevServer(server) {
23422
23635
  type: mapped
23423
23636
  };
23424
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
+ };
23425
23676
  var RovodevMcp = class RovodevMcp extends ToolMcp {
23426
23677
  json;
23427
23678
  constructor(params) {
@@ -23442,13 +23693,24 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23442
23693
  };
23443
23694
  }
23444
23695
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
23445
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23446
23696
  const paths = this.getSettablePaths({ global });
23447
23697
  const json = parseRovodevMcpJson(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
23448
23698
  const newJson = {
23449
23699
  ...json,
23450
23700
  mcpServers: json.mcpServers ?? {}
23451
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
+ }
23452
23714
  return new RovodevMcp({
23453
23715
  outputRoot,
23454
23716
  relativeDirPath: paths.relativeDirPath,
@@ -23459,11 +23721,21 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23459
23721
  });
23460
23722
  }
23461
23723
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23462
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23463
23724
  const paths = this.getSettablePaths({ global });
23464
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
+ }
23465
23732
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
23466
- 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);
23467
23739
  return converted === null ? null : [name, converted];
23468
23740
  }).filter((entry) => entry !== null));
23469
23741
  const rovodevConfig = {
@@ -23479,6 +23751,64 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23479
23751
  global
23480
23752
  });
23481
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
+ }
23482
23812
  toRulesyncMcp() {
23483
23813
  const rawServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
23484
23814
  const mcpServers = Object.fromEntries(Object.entries(rawServers).filter(([, server]) => isPlainObject$1(server)).map(([name, server]) => [name, fromRovodevServer(server)]));
@@ -23627,7 +23957,6 @@ function transportOf(server) {
23627
23957
  case "http":
23628
23958
  case "streamable-http":
23629
23959
  case "ws": return "http";
23630
- default: break;
23631
23960
  }
23632
23961
  if (server.command !== void 0) return "stdio";
23633
23962
  if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
@@ -23655,6 +23984,20 @@ function deriveTransportAllowlist(servers) {
23655
23984
  return allowlist;
23656
23985
  }
23657
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
23658
24001
  //#region src/features/mcp/vibe-mcp.ts
23659
24002
  const VIBE_MCP_SERVER_FIELDS = [
23660
24003
  "transport",
@@ -23723,8 +24066,9 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23723
24066
  global
23724
24067
  });
23725
24068
  }
23726
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
24069
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger, global = false }) {
23727
24070
  const paths = this.getSettablePaths({ global });
24071
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
23728
24072
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
23729
24073
  const existingContent = await readFileContentOrNull(filePath) ?? "";
23730
24074
  const existingServers = normalizeMcpServersArray(parseSharedConfig({
@@ -23733,7 +24077,10 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23733
24077
  filePath
23734
24078
  }).mcp_servers);
23735
24079
  const existingByName = new Map(existingServers.map((server) => [server.name, server]));
23736
- 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));
23737
24084
  return new VibeMcp({
23738
24085
  outputRoot,
23739
24086
  relativeDirPath: paths.relativeDirPath,
@@ -23742,7 +24089,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23742
24089
  fileKey: sharedConfigFileKey(paths),
23743
24090
  feature: "mcp",
23744
24091
  existingContent,
23745
- patch: { mcp_servers: mcpServers },
24092
+ patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
23746
24093
  filePath
23747
24094
  }),
23748
24095
  validate,
@@ -24444,10 +24791,19 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
24444
24791
  supportsDisabledTools: false
24445
24792
  }
24446
24793
  }],
24794
+ ["zoocode", {
24795
+ class: RooMcp,
24796
+ meta: {
24797
+ supportsProject: true,
24798
+ supportsGlobal: false,
24799
+ supportsEnabledTools: false,
24800
+ supportsDisabledTools: false
24801
+ }
24802
+ }],
24447
24803
  ["rovodev", {
24448
24804
  class: RovodevMcp,
24449
24805
  meta: {
24450
- supportsProject: false,
24806
+ supportsProject: true,
24451
24807
  supportsGlobal: true,
24452
24808
  supportsEnabledTools: false,
24453
24809
  supportsDisabledTools: false
@@ -25313,9 +25669,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
25313
25669
  case "ask":
25314
25670
  ask.push(entry);
25315
25671
  break;
25316
- case "deny":
25317
- deny.push(entry);
25318
- break;
25672
+ case "deny": deny.push(entry);
25319
25673
  }
25320
25674
  }
25321
25675
  }
@@ -25526,9 +25880,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
25526
25880
  case "ask":
25527
25881
  ask.push(entry);
25528
25882
  break;
25529
- case "deny":
25530
- deny.push(entry);
25531
- break;
25883
+ case "deny": deny.push(entry);
25532
25884
  }
25533
25885
  }
25534
25886
  }
@@ -26260,9 +26612,7 @@ function convertRulesyncToClaudePermissions({ config, logger }) {
26260
26612
  case "ask":
26261
26613
  ask.push(entry);
26262
26614
  break;
26263
- case "deny":
26264
- deny.push(entry);
26265
- break;
26615
+ case "deny": deny.push(entry);
26266
26616
  }
26267
26617
  }
26268
26618
  }
@@ -27512,9 +27862,7 @@ function convertRulesyncToCursorPermissions(config, logger) {
27512
27862
  case "ask":
27513
27863
  logger?.warn(`Cursor CLI permissions do not support the "ask" action. Skipping ${category} rule: ${pattern}`);
27514
27864
  break;
27515
- case "deny":
27516
- deny.push(entry);
27517
- break;
27865
+ case "deny": deny.push(entry);
27518
27866
  }
27519
27867
  }
27520
27868
  }
@@ -27751,9 +28099,7 @@ function convertRulesyncToDevinPermissions(config) {
27751
28099
  case "ask":
27752
28100
  ask.push(entry);
27753
28101
  break;
27754
- case "deny":
27755
- deny.push(entry);
27756
- break;
28102
+ case "deny": deny.push(entry);
27757
28103
  }
27758
28104
  }
27759
28105
  }
@@ -27941,10 +28287,7 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
27941
28287
  case "allow":
27942
28288
  allow.push(pattern);
27943
28289
  break;
27944
- case "deny":
27945
- deny.push(pattern);
27946
- break;
27947
- case "ask": break;
28290
+ case "deny": deny.push(pattern);
27948
28291
  }
27949
28292
  }
27950
28293
  return {
@@ -28063,10 +28406,11 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28063
28406
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28064
28407
  }
28065
28408
  const config = isRecord(parsed) ? { ...parsed } : {};
28066
- config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
28409
+ const userPermission = convertRulesyncToGoosePermissionConfig({
28067
28410
  config: rulesyncPermissions.getJson(),
28068
28411
  logger
28069
28412
  });
28413
+ config[GOOSE_USER_KEY] = userPermission;
28070
28414
  return new GoosePermissions({
28071
28415
  outputRoot,
28072
28416
  relativeDirPath: paths.relativeDirPath,
@@ -30284,9 +30628,7 @@ function convertRulesyncToQwenPermissions(config) {
30284
30628
  case "ask":
30285
30629
  ask.push(entry);
30286
30630
  break;
30287
- case "deny":
30288
- deny.push(entry);
30289
- break;
30631
+ case "deny": deny.push(entry);
30290
30632
  }
30291
30633
  }
30292
30634
  }
@@ -30359,6 +30701,20 @@ function toCanonicalToolName$2(reasonixName) {
30359
30701
  return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
30360
30702
  }
30361
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
+ /**
30362
30718
  * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
30363
30719
  * If no parentheses, returns the tool name with "*" as the pattern.
30364
30720
  */
@@ -30447,10 +30803,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30447
30803
  const config = rulesyncPermissions.getJson();
30448
30804
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
30449
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
+ ]);
30450
30815
  const existingPermissions = toReasonixTable(parsed.permissions);
30451
- const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30452
- const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30453
- 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
+ });
30454
30822
  if (logger && managedToolNames.has("Read")) {
30455
30823
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
30456
30824
  const { toolName } = parseReasonixPermissionEntry(entry);
@@ -30459,17 +30827,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30459
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.`);
30460
30828
  }
30461
30829
  const mergedPermissions = { ...existingPermissions };
30462
- const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
30463
- const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
30464
- const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
30465
- if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
30466
- else delete mergedPermissions.allow;
30467
- if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
30468
- else delete mergedPermissions.ask;
30469
- if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
30470
- 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
+ ]);
30471
30845
  const patch = { permissions: mergedPermissions };
30472
- const override = config.reasonix;
30473
30846
  if (override?.sandbox !== void 0) patch.sandbox = {
30474
30847
  ...asReasonixRecord(parsed.sandbox),
30475
30848
  ...asReasonixRecord(override.sandbox)
@@ -30500,16 +30873,29 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30500
30873
  }
30501
30874
  toRulesyncPermissions() {
30502
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));
30503
30886
  const config = convertReasonixToRulesyncPermissions({
30504
- allow: toReasonixStringArray(permissions.allow),
30505
- ask: toReasonixStringArray(permissions.ask),
30506
- deny: toReasonixStringArray(permissions.deny)
30887
+ allow: allowSplit.translated,
30888
+ ask: askSplit.translated,
30889
+ deny: denySplit.translated
30507
30890
  });
30508
30891
  const sandbox = asReasonixRecord(this.toml.sandbox);
30509
30892
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
30510
30893
  const reasonixOverride = {};
30511
30894
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
30512
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;
30513
30899
  const result = { ...config };
30514
30900
  if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
30515
30901
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -30539,6 +30925,41 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30539
30925
  }
30540
30926
  };
30541
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
+ /**
30542
30963
  * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
30543
30964
  */
30544
30965
  function convertRulesyncToReasonixPermissions(config) {
@@ -30556,9 +30977,7 @@ function convertRulesyncToReasonixPermissions(config) {
30556
30977
  case "ask":
30557
30978
  ask.push(entry);
30558
30979
  break;
30559
- case "deny":
30560
- deny.push(entry);
30561
- break;
30980
+ case "deny": deny.push(entry);
30562
30981
  }
30563
30982
  }
30564
30983
  }
@@ -30588,7 +31007,6 @@ function convertReasonixToRulesyncPermissions(params) {
30588
31007
  }
30589
31008
  //#endregion
30590
31009
  //#region src/features/permissions/rovodev-permissions.ts
30591
- const ROVODEV_GLOBAL_ONLY_MESSAGE = "Rovodev permissions are global-only; use --global to sync ~/.rovodev/config.yml";
30592
31010
  const CATCH_ALL_PATTERN$1 = "*";
30593
31011
  const CATEGORY_TO_TOOL_KEYS = {
30594
31012
  read: [
@@ -30648,9 +31066,12 @@ const OWNED_TOOL_PERMISSION_KEYS = [
30648
31066
  /**
30649
31067
  * Permissions adapter for Rovo Dev CLI.
30650
31068
  *
30651
- * Rovo Dev reads tool permissions from the `toolPermissions` block of the global
30652
- * `~/.rovodev/config.yml`. This surface is **global only** there is no
30653
- * 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.
30654
31075
  *
30655
31076
  * Rovo Dev's three levels (`allow`/`ask`/`deny`) are an exact 1:1 with rulesync's
30656
31077
  * canonical action enum, so action values pass through verbatim.
@@ -30697,7 +31118,6 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30697
31118
  };
30698
31119
  }
30699
31120
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30700
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30701
31121
  const paths = RovodevPermissions.getSettablePaths({ global });
30702
31122
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
30703
31123
  return new RovodevPermissions({
@@ -30706,11 +31126,10 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30706
31126
  relativeFilePath: paths.relativeFilePath,
30707
31127
  fileContent,
30708
31128
  validate,
30709
- global: true
31129
+ global
30710
31130
  });
30711
31131
  }
30712
31132
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
30713
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30714
31133
  const paths = RovodevPermissions.getSettablePaths({ global });
30715
31134
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30716
31135
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -30733,14 +31152,20 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30733
31152
  filePath,
30734
31153
  logger
30735
31154
  });
30736
- 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);
30737
31162
  return new RovodevPermissions({
30738
31163
  outputRoot,
30739
31164
  relativeDirPath: paths.relativeDirPath,
30740
31165
  relativeFilePath: paths.relativeFilePath,
30741
- fileContent: (0, js_yaml.dump)(config),
31166
+ fileContent,
30742
31167
  validate: true,
30743
- global: true
31168
+ global
30744
31169
  });
30745
31170
  }
30746
31171
  toRulesyncPermissions() {
@@ -31277,6 +31702,7 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
31277
31702
  write: "write_file",
31278
31703
  webfetch: "web_fetch",
31279
31704
  websearch: "web_search",
31705
+ grep: "grep",
31280
31706
  agent: "task"
31281
31707
  };
31282
31708
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
@@ -31286,6 +31712,7 @@ const VIBE_TO_CANONICAL_TOOL_NAMES = {
31286
31712
  write_file: "write",
31287
31713
  web_fetch: "webfetch",
31288
31714
  web_search: "websearch",
31715
+ grep: "grep",
31289
31716
  task: "agent"
31290
31717
  };
31291
31718
  var VibePermissions = class VibePermissions extends ToolPermissions {
@@ -31323,6 +31750,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31323
31750
  }
31324
31751
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
31325
31752
  const paths = this.getSettablePaths({ global });
31753
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
31326
31754
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
31327
31755
  const existingContent = await readFileContentOrNull(filePath) ?? "";
31328
31756
  const config = parseVibeConfig(existingContent);
@@ -31330,46 +31758,31 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31330
31758
  const tools = toVibeToolsRecord(config.tools);
31331
31759
  const enabledTools = new Set(toStringArray(config.enabled_tools));
31332
31760
  const disabledTools = new Set(toStringArray(config.disabled_tools));
31761
+ const removedEnabledTools = [];
31333
31762
  for (const category of Object.keys(permission)) {
31763
+ if (!hasVibeToolName(category)) continue;
31334
31764
  const vibeToolName = toVibeToolName(category);
31335
- enabledTools.delete(vibeToolName);
31765
+ if (enabledTools.delete(vibeToolName)) removedEnabledTools.push(vibeToolName);
31336
31766
  disabledTools.delete(vibeToolName);
31337
31767
  }
31338
31768
  for (const [category, rules] of Object.entries(permission)) {
31339
- const vibeToolName = toVibeToolName(category);
31340
- const existingTool = toVibeToolConfig(tools[vibeToolName]);
31341
- const nextTool = { ...existingTool };
31342
- const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31343
- const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31344
- for (const [pattern, action] of Object.entries(rules)) {
31345
- if (pattern === "*") {
31346
- applyWildcardPermission({
31347
- action,
31348
- toolConfig: nextTool
31349
- });
31350
- if (action === "deny") {
31351
- disabledTools.add(vibeToolName);
31352
- enabledTools.delete(vibeToolName);
31353
- } else if (action === "allow") {
31354
- enabledTools.add(vibeToolName);
31355
- disabledTools.delete(vibeToolName);
31356
- }
31357
- continue;
31358
- }
31359
- if (action === "ask") {
31360
- logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31361
- continue;
31362
- }
31363
- if (action === "allow") allow.add(pattern);
31364
- 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;
31365
31773
  }
31366
- delete nextTool.allow;
31367
- delete nextTool.deny;
31368
- if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31369
- if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31370
- tools[vibeToolName] = nextTool;
31774
+ applyCategoryRules({
31775
+ category,
31776
+ rules,
31777
+ tools,
31778
+ enabledTools,
31779
+ disabledTools,
31780
+ logger
31781
+ });
31371
31782
  }
31372
- 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.`);
31373
31786
  return new VibePermissions({
31374
31787
  outputRoot,
31375
31788
  relativeDirPath: paths.relativeDirPath,
@@ -31380,7 +31793,10 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31380
31793
  existingContent,
31381
31794
  patch: {
31382
31795
  tools,
31383
- enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
31796
+ enabled_tools: resolveEnabledToolsPatch({
31797
+ vibeOverride,
31798
+ enabledTools
31799
+ }),
31384
31800
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
31385
31801
  },
31386
31802
  filePath
@@ -31392,7 +31808,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31392
31808
  toRulesyncPermissions() {
31393
31809
  const permission = {};
31394
31810
  const vibeOverridePermission = {};
31395
- for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
31811
+ const enabledToolsList = toStringArray(this.toml.enabled_tools);
31396
31812
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
31397
31813
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
31398
31814
  const category = toCanonicalToolName$1(vibeToolName);
@@ -31405,9 +31821,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31405
31821
  if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
31406
31822
  }
31407
31823
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
31408
- 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 ? {
31409
31828
  permission,
31410
- vibe: { permission: vibeOverridePermission }
31829
+ vibe: vibeOverride
31411
31830
  } : { permission };
31412
31831
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
31413
31832
  }
@@ -31442,8 +31861,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31442
31861
  * list for any category the override names: a present list is set, an empty
31443
31862
  * one clears it. Categories not named keep whatever the existing file had.
31444
31863
  */
31445
- function applyVibeSensitivePatterns(tools, vibeOverride) {
31864
+ function applyVibeSensitivePatterns(tools, vibeOverride, logger) {
31446
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
+ }
31447
31870
  const vibeToolName = toVibeToolName(category);
31448
31871
  const nextTool = toVibeToolConfig(tools[vibeToolName]);
31449
31872
  const patterns = toStringArray(toolOverride.sensitive_patterns);
@@ -31452,6 +31875,57 @@ function applyVibeSensitivePatterns(tools, vibeOverride) {
31452
31875
  tools[vibeToolName] = nextTool;
31453
31876
  }
31454
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
+ }
31455
31929
  function parseVibeConfig(fileContent) {
31456
31930
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
31457
31931
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -31460,6 +31934,17 @@ function parseVibeConfig(fileContent) {
31460
31934
  function toVibeToolName(category) {
31461
31935
  return CANONICAL_TO_VIBE_TOOL_NAMES[category] ?? category;
31462
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
+ }
31463
31948
  function toCanonicalToolName$1(vibeToolName) {
31464
31949
  return VIBE_TO_CANONICAL_TOOL_NAMES[vibeToolName] ?? vibeToolName;
31465
31950
  }
@@ -31477,7 +31962,7 @@ function toStringArray(value) {
31477
31962
  function applyWildcardPermission({ action, toolConfig }) {
31478
31963
  if (action === "allow") toolConfig.permission = "always";
31479
31964
  else if (action === "ask") toolConfig.permission = "ask";
31480
- else if (toolConfig.permission !== void 0) delete toolConfig.permission;
31965
+ else toolConfig.permission = "never";
31481
31966
  }
31482
31967
  function fromVibePermission(value) {
31483
31968
  if (value === "always" || value === "allow") return "allow";
@@ -31505,6 +31990,20 @@ const WARP_OVERRIDE_KEYS = [
31505
31990
  "agent_mode_coding_file_read_allowlist",
31506
31991
  "agent_mode_execute_readonly_commands"
31507
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
+ ];
31508
32007
  /**
31509
32008
  * Warp's `settings.toml` lives in a different directory per platform (Stable
31510
32009
  * channel). The home directory is resolved by the processor through
@@ -31559,7 +32058,14 @@ function warpSettingsDir() {
31559
32058
  * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
31560
32059
  * on **import** they are lifted from `settings.toml` into the override, and on
31561
32060
  * **export** they are merged back into `[agents.profiles]` (the override wins).
31562
- * 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.
31563
32069
  *
31564
32070
  * The `settings.toml` file holds all of Warp's settings, so the
31565
32071
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -31612,7 +32118,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31612
32118
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
31613
32119
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
31614
32120
  const override = config.warp;
31615
- 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
+ }
31616
32126
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
31617
32127
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
31618
32128
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -31620,18 +32130,13 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31620
32130
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31621
32131
  else delete profiles[DENYLIST_KEY];
31622
32132
  agents.profiles = profiles;
31623
- if (isRecord(agents[EXECUTION_PROFILES_KEY])) {
31624
- const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
31625
- const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
31626
- if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
31627
- else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
31628
- if (mergedDeny.length > 0) defaultProfile[PROFILE_DENYLIST_KEY] = mergedDeny;
31629
- else delete defaultProfile[PROFILE_DENYLIST_KEY];
31630
- executionProfiles[DEFAULT_PROFILE_KEY] = defaultProfile;
31631
- agents[EXECUTION_PROFILES_KEY] = executionProfiles;
31632
- const otherProfileIds = Object.keys(executionProfiles).filter((id) => id !== DEFAULT_PROFILE_KEY);
31633
- if (mergedDeny.length > 0 && otherProfileIds.length > 0 && logger) logger.warn(`Warp command deny rules were written to the 'default' execution profile only; they are not enforced while another profile (${otherProfileIds.join(", ")}) is active.`);
31634
- }
32133
+ mergeIntoDefaultExecutionProfile({
32134
+ agents,
32135
+ mergedAllow,
32136
+ mergedDeny,
32137
+ executionProfileOverride,
32138
+ logger
32139
+ });
31635
32140
  settings.agents = agents;
31636
32141
  return new WarpPermissions({
31637
32142
  outputRoot,
@@ -31659,6 +32164,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31659
32164
  });
31660
32165
  const warpOverride = {};
31661
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
+ }
31662
32172
  const result = { ...config };
31663
32173
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
31664
32174
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -31681,6 +32191,36 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31681
32191
  }
31682
32192
  };
31683
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
+ /**
31684
32224
  * Convert rulesync permissions config to Warp command allow/deny regex lists.
31685
32225
  * Only the `bash` category maps; `ask` rules and non-`bash` categories are
31686
32226
  * dropped (the latter with a warning when they carry `deny` rules).
@@ -31697,10 +32237,7 @@ function convertRulesyncToWarpPermissions({ config, logger }) {
31697
32237
  case "allow":
31698
32238
  allow.push(pattern);
31699
32239
  break;
31700
- case "deny":
31701
- deny.push(pattern);
31702
- break;
31703
- case "ask": break;
32240
+ case "deny": deny.push(pattern);
31704
32241
  }
31705
32242
  }
31706
32243
  return {
@@ -32155,7 +32692,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
32155
32692
  ["rovodev", {
32156
32693
  class: RovodevPermissions,
32157
32694
  meta: {
32158
- supportsProject: false,
32695
+ supportsProject: true,
32159
32696
  supportsGlobal: true,
32160
32697
  supportsImport: true
32161
32698
  }
@@ -33352,6 +33889,38 @@ var AugmentcodeSkill = class AugmentcodeSkill extends ToolSkill {
33352
33889
  }
33353
33890
  };
33354
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
33355
33924
  //#region src/features/skills/claudecode-skill.ts
33356
33925
  const ClaudecodeSkillFrontmatterSchema = zod_mini.z.looseObject({
33357
33926
  name: zod_mini.z.string(),
@@ -33437,6 +34006,41 @@ var ClaudecodeSkill = class extends ToolSkill {
33437
34006
  alternativeSkillRoots: [CLAUDECODE_SCHEDULED_TASKS_DIR_PATH]
33438
34007
  };
33439
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
+ }
33440
34044
  getFrontmatter() {
33441
34045
  return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
33442
34046
  }
@@ -36835,7 +37439,8 @@ var TaktSkill = class TaktSkill extends ToolSkill {
36835
37439
  getDirPath() {
36836
37440
  const fullPath = (0, node_path.join)(this.outputRoot, this.relativeDirPath);
36837
37441
  const resolvedFull = (0, node_path.resolve)(fullPath);
36838
- 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);
36839
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}"`);
36840
37445
  return fullPath;
36841
37446
  }
@@ -37385,6 +37990,23 @@ var ZedSkill = class ZedSkill extends ToolSkill {
37385
37990
  }
37386
37991
  };
37387
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
37388
38010
  //#region src/features/skills/skills-processor.ts
37389
38011
  const SkillsProcessorToolTargetSchema = zod_mini.z.enum(skillsProcessorToolTargetTuple);
37390
38012
  /**
@@ -37657,6 +38279,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37657
38279
  supportsGlobal: true
37658
38280
  }
37659
38281
  }],
38282
+ ["zoocode", {
38283
+ class: ZoocodeSkill,
38284
+ meta: {
38285
+ supportsProject: true,
38286
+ supportsSimulated: false,
38287
+ supportsGlobal: true
38288
+ }
38289
+ }],
37660
38290
  ["rovodev", {
37661
38291
  class: RovodevSkill,
37662
38292
  meta: {
@@ -37819,7 +38449,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37819
38449
  for (const root of roots) {
37820
38450
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
37821
38451
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37822
- const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
38452
+ const isLenientRoot = configuredRootPaths.has(relativeDirPath) || factory.meta.lenientImport === true || isAgentSkillsInteropRoot(relativeDirPath);
37823
38453
  const skillsDirPath = (0, node_path.join)(rootOutputRoot, relativeDirPath);
37824
38454
  if (!await directoryExists(skillsDirPath)) continue;
37825
38455
  const dirPaths = await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*"), { type: "dir" });
@@ -37843,7 +38473,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37843
38473
  global: this.global
37844
38474
  });
37845
38475
  } catch (error) {
37846
- if (!isConfiguredRoot && !factory.meta.lenientImport) throw error;
38476
+ if (!isLenientRoot) throw error;
37847
38477
  this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
37848
38478
  return null;
37849
38479
  }
@@ -37858,12 +38488,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37858
38488
  const fromFlatFile = factory.class.fromFlatFile;
37859
38489
  const directoryStems = new Set(ownedDirNames);
37860
38490
  const flatFilePaths = (await findFilesByGlobs((0, node_path.join)(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has((0, node_path.basename)(filePath, ".md")));
37861
- const flatSkills = await Promise.all(flatFilePaths.map((filePath) => fromFlatFile({
37862
- outputRoot: rootOutputRoot,
37863
- relativeDirPath,
37864
- relativeFilePath: (0, node_path.basename)(filePath),
37865
- global: this.global
37866
- })));
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);
37867
38505
  for (const skill of flatSkills) {
37868
38506
  const skillName = skill.getImportIdentity();
37869
38507
  if (seenSkillNames.has(skillName)) continue;
@@ -37936,14 +38574,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37936
38574
  */
37937
38575
  static getSimulatedConventionSection({ skillList }) {
37938
38576
  if (!skillList || skillList.length === 0) return "";
38577
+ const skillListWithAtPrefix = skillList.map((skill) => ({
38578
+ ...skill,
38579
+ path: `@${skill.path}`
38580
+ }));
37939
38581
  return `## Simulated Skills
37940
38582
 
37941
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.
37942
38584
 
37943
- ${(0, _toon_format_toon.encode)({ skillList: skillList.map((skill) => ({
37944
- ...skill,
37945
- path: `@${skill.path}`
37946
- })) })}`;
38585
+ ${(0, _toon_format_toon.encode)({ skillList: skillListWithAtPrefix })}`;
37947
38586
  }
37948
38587
  /**
37949
38588
  * Return the tool targets that this processor supports in global mode
@@ -40547,7 +41186,8 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
40547
41186
  return getHermesagentSharedConfigWritePaths();
40548
41187
  }
40549
41188
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
40550
- 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`)];
40551
41191
  }
40552
41192
  toRulesyncSubagent() {
40553
41193
  const slug = (0, node_path.basename)(this.getRelativeFilePath(), ".json");
@@ -41782,7 +42422,7 @@ function stringifyRooModes(modes) {
41782
42422
  *
41783
42423
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
41784
42424
  */
41785
- var RooSubagent = class RooSubagent extends ToolSubagent {
42425
+ var RooSubagent = class extends ToolSubagent {
41786
42426
  modes;
41787
42427
  constructor({ modes, ...rest }) {
41788
42428
  if (rest.validate !== false) {
@@ -41838,7 +42478,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41838
42478
  const mode = this.toRooMode(rulesyncSubagent);
41839
42479
  bySlug.set(mode.slug, mode);
41840
42480
  }
41841
- return new RooSubagent({
42481
+ return new this({
41842
42482
  outputRoot,
41843
42483
  relativeDirPath: this.getSettablePaths().relativeDirPath,
41844
42484
  relativeFilePath: ROO_MODES_FILE_NAME,
@@ -41917,7 +42557,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41917
42557
  }
41918
42558
  const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
41919
42559
  if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
41920
- return new RooSubagent({
42560
+ return new this({
41921
42561
  outputRoot,
41922
42562
  relativeDirPath: paths.relativeDirPath,
41923
42563
  relativeFilePath: (0, node_path.basename)(relativeFilePath),
@@ -41926,7 +42566,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41926
42566
  });
41927
42567
  }
41928
42568
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
41929
- return new RooSubagent({
42569
+ return new this({
41930
42570
  outputRoot,
41931
42571
  relativeDirPath,
41932
42572
  relativeFilePath,
@@ -42172,6 +42812,60 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
42172
42812
  }
42173
42813
  };
42174
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
42175
42869
  //#region src/features/subagents/subagents-processor.ts
42176
42870
  const SubagentsProcessorToolTargetSchema = zod_mini.z.enum(subagentsProcessorToolTargetTuple);
42177
42871
  /**
@@ -42411,6 +43105,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
42411
43105
  filePattern: ".roomodes"
42412
43106
  }
42413
43107
  }],
43108
+ ["zoocode", {
43109
+ class: ZoocodeSubagent,
43110
+ meta: {
43111
+ supportsSimulated: false,
43112
+ supportsGlobal: false,
43113
+ filePattern: ".roomodes"
43114
+ }
43115
+ }],
42414
43116
  ["rovodev", {
42415
43117
  class: RovodevSubagent,
42416
43118
  meta: {
@@ -42671,11 +43373,13 @@ For example, if the user instructs \`Call planner subagent to plan the refactori
42671
43373
  //#region src/features/rules/tool-rule.ts
42672
43374
  var ToolRule = class extends ToolFile {
42673
43375
  root;
43376
+ localRoot;
42674
43377
  description;
42675
43378
  globs;
42676
- constructor({ root = false, description, globs, ...rest }) {
43379
+ constructor({ root = false, localRoot = false, description, globs, ...rest }) {
42677
43380
  super(rest);
42678
43381
  this.root = root;
43382
+ this.localRoot = localRoot;
42679
43383
  this.description = description;
42680
43384
  this.globs = globs;
42681
43385
  }
@@ -42758,6 +43462,38 @@ var ToolRule = class extends ToolFile {
42758
43462
  isRoot() {
42759
43463
  return this.root;
42760
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
+ }
42761
43497
  /**
42762
43498
  * Whether this rule must be left out of the root rule's reference/MCP
42763
43499
  * instruction listings even though it is a non-root survivor. Used by files
@@ -42790,31 +43526,6 @@ function buildToolPath(toolDir, subDir, excludeToolDir) {
42790
43526
  }
42791
43527
  //#endregion
42792
43528
  //#region src/features/rules/agentsmd-rule.ts
42793
- /**
42794
- * Dependency trees never scanned for nested `AGENTS.md` files, at any depth. An
42795
- * `AGENTS.md` there describes somebody else's project, and neither name is ever
42796
- * a package name. Hidden directories are excluded separately, because an
42797
- * `AGENTS.md` inside one is another tool's generated output (rulesync writes
42798
- * several itself).
42799
- */
42800
- const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
42801
- /**
42802
- * Build, vendoring and scratch directories, excluded at the **project root
42803
- * only**. A top-level `build/` is a build directory; `packages/build/` is a
42804
- * package, and dropping it silently would lose a real subproject.
42805
- */
42806
- const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
42807
- "vendor",
42808
- "third_party",
42809
- "dist",
42810
- "build",
42811
- "out",
42812
- "target",
42813
- "coverage",
42814
- "tmp",
42815
- "temp",
42816
- "venv"
42817
- ];
42818
43529
  var AgentsMdRule = class AgentsMdRule extends ToolRule {
42819
43530
  constructor({ fileContent, root, ...rest }) {
42820
43531
  super({
@@ -42871,7 +43582,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
42871
43582
  const normalizedDirPath = relativeDirPath === void 0 ? "." : toPosixPath(relativeDirPath);
42872
43583
  const isNested = relativeFilePath === "AGENTS.md" && normalizedDirPath !== "." && normalizedDirPath !== "" && !normalizedDirPath.startsWith(".");
42873
43584
  const isRoot = !isNested && relativeFilePath === "AGENTS.md" && (normalizedDirPath === "." || normalizedDirPath === "");
42874
- 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));
42875
43587
  return new AgentsMdRule({
42876
43588
  outputRoot,
42877
43589
  relativeDirPath: isNested ? (0, node_path.join)(normalizedDirPath) : isRoot ? this.getSettablePaths().root.relativeDirPath : this.getSettablePaths().nonRoot.relativeDirPath,
@@ -43178,7 +43890,8 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
43178
43890
  });
43179
43891
  }
43180
43892
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43181
- 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));
43182
43895
  return new AntigravityCliRule({
43183
43896
  outputRoot,
43184
43897
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43639,7 +44352,8 @@ var AugmentcodeLegacyRule = class AugmentcodeLegacyRule extends ToolRule {
43639
44352
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
43640
44353
  const settablePaths = this.getSettablePaths();
43641
44354
  const isRoot = relativeFilePath === settablePaths.root.relativeFilePath;
43642
- 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));
43643
44357
  return new AugmentcodeLegacyRule({
43644
44358
  outputRoot,
43645
44359
  relativeDirPath: isRoot ? settablePaths.root.relativeDirPath : settablePaths.nonRoot.relativeDirPath,
@@ -43863,7 +44577,8 @@ var ClaudecodeLegacyRule = class ClaudecodeLegacyRule extends ToolRule {
43863
44577
  });
43864
44578
  }
43865
44579
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43866
- 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));
43867
44582
  return new ClaudecodeLegacyRule({
43868
44583
  outputRoot,
43869
44584
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43984,7 +44699,8 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
43984
44699
  });
43985
44700
  }
43986
44701
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43987
- 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);
43988
44704
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
43989
44705
  const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
43990
44706
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44371,7 +45087,8 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
44371
45087
  }
44372
45088
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44373
45089
  const { root } = this.getSettablePaths({ global });
44374
- 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));
44375
45092
  return new CodexcliRule({
44376
45093
  outputRoot,
44377
45094
  relativeDirPath: root.relativeDirPath,
@@ -44540,7 +45257,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44540
45257
  }) : relativeFilePath === paths.root.relativeFilePath;
44541
45258
  const resolvedRelativeDirPath = relativeDirPath ?? (isRoot ? paths.root.relativeDirPath : paths.nonRoot?.relativeDirPath ?? paths.root.relativeDirPath);
44542
45259
  if (isRoot) {
44543
- 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));
44544
45262
  return new CopilotRule({
44545
45263
  outputRoot,
44546
45264
  relativeDirPath: paths.root.relativeDirPath,
@@ -44552,7 +45270,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44552
45270
  });
44553
45271
  }
44554
45272
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
44555
- 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);
44556
45275
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
44557
45276
  const result = CopilotRuleFrontmatterSchema.safeParse(frontmatter);
44558
45277
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44824,7 +45543,8 @@ var DeepagentsRule = class DeepagentsRule extends ToolRule {
44824
45543
  }
44825
45544
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44826
45545
  const settablePaths = this.getSettablePaths({ global });
44827
- 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));
44828
45548
  return new DeepagentsRule({
44829
45549
  outputRoot,
44830
45550
  relativeDirPath: settablePaths.root.relativeDirPath,
@@ -45251,7 +45971,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45251
45971
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
45252
45972
  const paths = this.getSettablePaths({ global });
45253
45973
  if (relativeFilePath === paths.root.relativeFilePath) {
45254
- 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));
45255
45976
  return new FactorydroidRule({
45256
45977
  outputRoot,
45257
45978
  relativeDirPath: paths.root.relativeDirPath,
@@ -45262,7 +45983,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45262
45983
  });
45263
45984
  }
45264
45985
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45265
- 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));
45266
45988
  return new FactorydroidRule({
45267
45989
  outputRoot,
45268
45990
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45492,7 +46214,8 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
45492
46214
  }
45493
46215
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
45494
46216
  const { root } = this.getSettablePaths();
45495
- 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));
45496
46219
  return new HermesagentRule({
45497
46220
  outputRoot,
45498
46221
  relativeDirPath: root.relativeDirPath,
@@ -45603,7 +46326,8 @@ var JunieRule = class JunieRule extends ToolRule {
45603
46326
  });
45604
46327
  }
45605
46328
  const relativeDirPath = this.getSettablePaths().root.relativeDirPath;
45606
- 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));
45607
46331
  return new JunieRule({
45608
46332
  outputRoot,
45609
46333
  relativeDirPath,
@@ -45703,7 +46427,8 @@ var KiloRule = class KiloRule extends ToolRule {
45703
46427
  });
45704
46428
  }
45705
46429
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45706
- 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));
45707
46432
  return new KiloRule({
45708
46433
  outputRoot,
45709
46434
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46040,10 +46765,13 @@ var KiroIdeRule = class extends KiroRule {
46040
46765
  //#region src/features/rules/opencode-rule.ts
46041
46766
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
46042
46767
  static getSettablePaths({ global, excludeToolDir } = {}) {
46043
- if (global) return { root: {
46044
- relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
46045
- relativeFilePath: OPENCODE_RULE_FILE_NAME
46046
- } };
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
+ };
46047
46775
  return {
46048
46776
  root: {
46049
46777
  relativeDirPath: ".",
@@ -46053,7 +46781,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46053
46781
  };
46054
46782
  }
46055
46783
  static getExtraSharedWritePaths({ global = false } = {}) {
46056
- return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
46784
+ return [OpencodeMcp.getSettablePaths({ global })];
46057
46785
  }
46058
46786
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46059
46787
  const paths = this.getSettablePaths({ global });
@@ -46070,7 +46798,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46070
46798
  });
46071
46799
  }
46072
46800
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
46073
- 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));
46074
46803
  return new OpenCodeRule({
46075
46804
  outputRoot,
46076
46805
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46185,7 +46914,8 @@ var PiRule = class PiRule extends ToolRule {
46185
46914
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46186
46915
  const { root, appendSystemPrompt } = this.getSettablePaths({ global });
46187
46916
  if (relativeFilePath === "APPEND_SYSTEM.md") {
46188
- 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));
46189
46919
  return new PiRule({
46190
46920
  outputRoot,
46191
46921
  relativeDirPath: appendSystemPrompt.relativeDirPath,
@@ -46196,7 +46926,8 @@ var PiRule = class PiRule extends ToolRule {
46196
46926
  appendSystemPrompt: true
46197
46927
  });
46198
46928
  }
46199
- 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));
46200
46931
  return new PiRule({
46201
46932
  outputRoot,
46202
46933
  relativeDirPath: root.relativeDirPath,
@@ -46496,7 +47227,7 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46496
47227
  * The personal local context file lives under `.qwen/`, not at the project
46497
47228
  * root where the settable root path points, so override the deletion glob.
46498
47229
  */
46499
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47230
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46500
47231
  return (0, node_path.join)(outputRoot, QWENCODE_DIR, fileName);
46501
47232
  }
46502
47233
  };
@@ -46516,9 +47247,51 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46516
47247
  relativeFilePath: REASONIX_RULE_FILE_NAME
46517
47248
  } };
46518
47249
  }
46519
- 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 }) {
46520
47281
  const { root } = this.getSettablePaths({ global });
46521
- 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));
46522
47295
  return new ReasonixRule({
46523
47296
  outputRoot,
46524
47297
  relativeDirPath: root.relativeDirPath,
@@ -46530,7 +47303,17 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46530
47303
  }
46531
47304
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
46532
47305
  const { root } = this.getSettablePaths({ global });
46533
- 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
+ });
46534
47317
  return new ReasonixRule({
46535
47318
  outputRoot,
46536
47319
  relativeDirPath: root.relativeDirPath,
@@ -46541,7 +47324,22 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46541
47324
  });
46542
47325
  }
46543
47326
  toRulesyncRule() {
46544
- 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
+ });
46545
47343
  }
46546
47344
  validate() {
46547
47345
  return {
@@ -46711,7 +47509,7 @@ var RooRule = class RooRule extends ToolRule {
46711
47509
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
46712
47510
  * at the project root, not under `.roo/` (mirrors rovodev).
46713
47511
  */
46714
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47512
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46715
47513
  return (0, node_path.join)(outputRoot, fileName);
46716
47514
  }
46717
47515
  };
@@ -46843,19 +47641,6 @@ var RovodevRule = class RovodevRule extends ToolRule {
46843
47641
  }));
46844
47642
  }
46845
47643
  toRulesyncRule() {
46846
- if (this.getRelativeFilePath() === "AGENTS.local.md") return new RulesyncRule({
46847
- outputRoot: this.getOutputRoot(),
46848
- relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
46849
- relativeFilePath: ROVODEV_LEGACY_RULE_FILE_NAME,
46850
- frontmatter: {
46851
- targets: ["rovodev"],
46852
- root: false,
46853
- localRoot: true,
46854
- globs: []
46855
- },
46856
- body: this.getFileContent(),
46857
- validate: true
46858
- });
46859
47644
  if (!this.isRoot()) return new RulesyncRule({
46860
47645
  outputRoot: this.getOutputRoot(),
46861
47646
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -46912,7 +47697,7 @@ var RovodevRule = class RovodevRule extends ToolRule {
46912
47697
  };
46913
47698
  }
46914
47699
  /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
46915
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47700
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46916
47701
  return (0, node_path.join)(outputRoot, fileName);
46917
47702
  }
46918
47703
  };
@@ -47114,7 +47899,8 @@ var WarpRule = class WarpRule extends ToolRule {
47114
47899
  }
47115
47900
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47116
47901
  const { root } = this.getSettablePaths({ global });
47117
- const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
47902
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
47903
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
47118
47904
  return new WarpRule({
47119
47905
  outputRoot,
47120
47906
  relativeDirPath: root.relativeDirPath,
@@ -47239,6 +48025,27 @@ var ZedRule = class ZedRule extends ToolRule {
47239
48025
  }
47240
48026
  };
47241
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
47242
48049
  //#region src/features/rules/rules-processor.ts
47243
48050
  const RulesProcessorToolTargetSchema = zod_mini.z.enum(rulesProcessorToolTargetTuple);
47244
48051
  const formatRulePaths = (rules) => rules.map((r) => (0, node_path.join)(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
@@ -47501,6 +48308,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47501
48308
  supportsGlobal: true,
47502
48309
  ruleDiscoveryMode: "toon",
47503
48310
  mcpInstructionsRegistrar: OpencodeMcp,
48311
+ mcpInstructionsRegistrarGlobal: true,
47504
48312
  collisionPolicy: "compose"
47505
48313
  }
47506
48314
  }],
@@ -47565,6 +48373,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47565
48373
  localRootFileName: "AGENTS.local.md"
47566
48374
  }
47567
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
+ }],
47568
48386
  ["takt", {
47569
48387
  class: TaktRule,
47570
48388
  meta: {
@@ -47737,18 +48555,19 @@ var RulesProcessor = class extends FeatureProcessor {
47737
48555
  /**
47738
48556
  * Non-root rules of some tools are not auto-loaded; the tool's MCP feature
47739
48557
  * registers them in its shared config's `instructions` key. The root rule is
47740
- * auto-loaded and never registered. Project scope only.
48558
+ * auto-loaded and never registered. Global scope opt-in via
48559
+ * `mcpInstructionsRegistrarGlobal`.
47741
48560
  */
47742
48561
  async buildMcpInstructionFiles({ toolRules, meta }) {
47743
- if (!meta.mcpInstructionsRegistrar || this.global) return [];
48562
+ if (!meta.mcpInstructionsRegistrar || this.global && !meta.mcpInstructionsRegistrarGlobal) return [];
47744
48563
  const instructionPaths = toolRules.filter((rule) => !rule.isRoot() && !rule.isExcludedFromRootReferences()).map((rule) => toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
47745
- if (instructionPaths.length === 0) return [];
47746
- return [await meta.mcpInstructionsRegistrar.fromInstructions({
48564
+ const registered = await meta.mcpInstructionsRegistrar.fromInstructions({
47747
48565
  outputRoot: this.outputRoot,
47748
48566
  instructions: instructionPaths,
47749
48567
  validate: true,
47750
48568
  global: this.global
47751
- })];
48569
+ });
48570
+ return registered ? [registered] : [];
47752
48571
  }
47753
48572
  /**
47754
48573
  * For tools that don't create a separate conventions rule, prepend the
@@ -47879,53 +48698,58 @@ var RulesProcessor = class extends FeatureProcessor {
47879
48698
  const rootRule = toolRules.find((rule) => rule.isRoot());
47880
48699
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
47881
48700
  }
47882
- buildLocalRootFile({ factory, fileName, body }) {
48701
+ buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
47883
48702
  if (factory.class === ClaudecodeRule) {
47884
48703
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
47885
48704
  return new ClaudecodeRule({
47886
48705
  outputRoot: this.outputRoot,
47887
- relativeDirPath: paths.root.relativeDirPath,
48706
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47888
48707
  relativeFilePath: fileName,
47889
48708
  frontmatter: {},
47890
48709
  body,
47891
48710
  validate: true,
47892
- root: true
48711
+ root: true,
48712
+ localRoot
47893
48713
  });
47894
48714
  }
47895
48715
  if (factory.class === ClaudecodeLegacyRule) {
47896
48716
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
47897
48717
  return new ClaudecodeLegacyRule({
47898
48718
  outputRoot: this.outputRoot,
47899
- relativeDirPath: paths.root.relativeDirPath,
48719
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47900
48720
  relativeFilePath: fileName,
47901
48721
  fileContent: body,
47902
48722
  validate: true,
47903
- root: true
48723
+ root: true,
48724
+ localRoot
47904
48725
  });
47905
48726
  }
47906
48727
  if (factory.class === RovodevRule) return new RovodevRule({
47907
48728
  outputRoot: this.outputRoot,
47908
- relativeDirPath: ".",
48729
+ relativeDirPath: relativeDirPath ?? ".",
47909
48730
  relativeFilePath: fileName,
47910
48731
  fileContent: body,
47911
48732
  validate: true,
47912
- root: true
48733
+ root: true,
48734
+ localRoot
47913
48735
  });
47914
48736
  if (factory.class === RooRule) return new RooRule({
47915
48737
  outputRoot: this.outputRoot,
47916
- relativeDirPath: ".",
48738
+ relativeDirPath: relativeDirPath ?? ".",
47917
48739
  relativeFilePath: fileName,
47918
48740
  fileContent: body,
47919
48741
  validate: true,
47920
- root: true
48742
+ root: true,
48743
+ localRoot
47921
48744
  });
47922
48745
  if (factory.class === QwencodeRule) return new QwencodeRule({
47923
48746
  outputRoot: this.outputRoot,
47924
- relativeDirPath: QWENCODE_DIR,
48747
+ relativeDirPath: relativeDirPath ?? ".qwen",
47925
48748
  relativeFilePath: fileName,
47926
48749
  fileContent: body,
47927
48750
  validate: true,
47928
- root: true
48751
+ root: true,
48752
+ localRoot
47929
48753
  });
47930
48754
  return null;
47931
48755
  }
@@ -47963,6 +48787,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
47963
48787
  async convertToolFilesToRulesyncFiles(toolFiles) {
47964
48788
  const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
47965
48789
  const rulesyncRules = toolRules.map((toolRule) => {
48790
+ if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
47966
48791
  return toolRule.toRulesyncRule();
47967
48792
  });
47968
48793
  const claimedBy = /* @__PURE__ */ new Map();
@@ -48096,20 +48921,34 @@ As this project's AI coding tool, you must follow the additional conventions bel
48096
48921
  })();
48097
48922
  this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
48098
48923
  const localRootToolRules = await (async () => {
48099
- 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 [];
48100
48925
  const fileName = factory.meta.localRootFileName;
48101
- if (factory.class.getLocalRootDeletionGlob) {
48102
- const filePaths = await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
48926
+ const filePaths = await (async () => {
48927
+ if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
48103
48928
  outputRoot: this.outputRoot,
48104
48929
  fileName
48105
48930
  }));
48106
- return buildDeletionRulesFromPaths(filePaths);
48107
- }
48108
- if (!settablePaths.root) return [];
48109
- 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));
48110
- 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);
48111
48950
  })();
48112
- 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`);
48113
48952
  const rootMirrorDeletionRules = await (async () => {
48114
48953
  const rootMirror = factory.class.getRootMirror?.();
48115
48954
  if (!forDeletion || this.global || !rootMirror) return [];
@@ -48235,13 +49074,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
48235
49074
  const lines = [];
48236
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.");
48237
49076
  lines.push("");
48238
- const toonContent = (0, _toon_format_toon.encode)({ rules: toolRulesWithoutRoot.map((toolRule) => {
49077
+ const rules = toolRulesWithoutRoot.map((toolRule) => {
48239
49078
  const frontmatter = toolRule.toRulesyncRule().getFrontmatter();
48240
49079
  const rule = { path: `@${toolRule.getRelativePathFromCwd()}` };
48241
49080
  if (frontmatter.description) rule.description = frontmatter.description;
48242
49081
  if (frontmatter.globs && frontmatter.globs.length > 0) rule.applyTo = frontmatter.globs;
48243
49082
  return rule;
48244
- }) });
49083
+ });
49084
+ const toonContent = (0, _toon_format_toon.encode)({ rules });
48245
49085
  lines.push(toonContent);
48246
49086
  return lines.join("\n") + "\n\n";
48247
49087
  }
@@ -49403,8 +50243,8 @@ async function generateIgnoreCore(params) {
49403
50243
  allPaths.push(...result.paths);
49404
50244
  if (result.hasDiff) hasDiff = true;
49405
50245
  } catch (error) {
49406
- logger.warn(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
49407
- continue;
50246
+ logger.error(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
50247
+ throw error;
49408
50248
  }
49409
50249
  }
49410
50250
  return {
@@ -49676,8 +50516,8 @@ async function generatePermissionsCore(params) {
49676
50516
  allPaths.push(...result.paths);
49677
50517
  if (result.hasDiff) hasDiff = true;
49678
50518
  } catch (error) {
49679
- logger.warn(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
49680
- continue;
50519
+ logger.error(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
50520
+ throw error;
49681
50521
  }
49682
50522
  }
49683
50523
  return {