rulesync 16.3.0 → 16.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -141,7 +141,7 @@ const RULESYNC_PERMISSIONS_LEGACY_FILE_NAME = "permissions.json";
141
141
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
142
142
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
143
143
  const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
144
- const MAX_FILE_SIZE = 10 * 1024 * 1024;
144
+ const MAX_FILE_SIZE = 10485760;
145
145
  //#endregion
146
146
  //#region src/types/tool-target-tuples.ts
147
147
  const rulesProcessorToolTargetTuple = [
@@ -182,7 +182,8 @@ const rulesProcessorToolTargetTuple = [
182
182
  "vibe",
183
183
  "warp",
184
184
  "devin",
185
- "zed"
185
+ "zed",
186
+ "zoocode"
186
187
  ];
187
188
  const ignoreProcessorToolTargetTuple = [
188
189
  "aiassistant",
@@ -204,7 +205,8 @@ const ignoreProcessorToolTargetTuple = [
204
205
  "devin",
205
206
  "vibe",
206
207
  "warp",
207
- "zed"
208
+ "zed",
209
+ "zoocode"
208
210
  ];
209
211
  const mcpProcessorToolTargetTuple = [
210
212
  "aiassistant",
@@ -241,7 +243,8 @@ const mcpProcessorToolTargetTuple = [
241
243
  "vibe",
242
244
  "warp",
243
245
  "devin",
244
- "zed"
246
+ "zed",
247
+ "zoocode"
245
248
  ];
246
249
  const commandsProcessorToolTargetTuple = [
247
250
  "agentsmd",
@@ -269,6 +272,7 @@ const commandsProcessorToolTargetTuple = [
269
272
  "qwencode",
270
273
  "reasonix",
271
274
  "roo",
275
+ "zoocode",
272
276
  "rovodev",
273
277
  "takt",
274
278
  "devin",
@@ -304,6 +308,7 @@ const subagentsProcessorToolTargetTuple = [
304
308
  "qwencode",
305
309
  "reasonix",
306
310
  "roo",
311
+ "zoocode",
307
312
  "rovodev",
308
313
  "takt",
309
314
  "vibe"
@@ -347,7 +352,8 @@ const skillsProcessorToolTargetTuple = [
347
352
  "vibe",
348
353
  "warp",
349
354
  "devin",
350
- "zed"
355
+ "zed",
356
+ "zoocode"
351
357
  ];
352
358
  const hooksProcessorToolTargetTuple = [
353
359
  "amp",
@@ -660,7 +666,8 @@ async function findFilesByGlobs(globs, options = {}) {
660
666
  onlyFiles: false,
661
667
  onlyDirectories: false
662
668
  };
663
- const results = globbySync(Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/"), {
669
+ const normalizedGlobs = Array.isArray(globs) ? globs.map((g) => g.replaceAll("\\", "/")) : globs.replaceAll("\\", "/");
670
+ const results = globbySync(normalizedGlobs, {
664
671
  absolute: true,
665
672
  followSymbolicLinks,
666
673
  ...ignore ? { ignore: ignore.map((pattern) => pattern.replaceAll("\\", "/")) } : {},
@@ -771,7 +778,8 @@ function validateOutputRoot(outputRoot) {
771
778
  function toKebabCaseFilename(filename) {
772
779
  const lastDotIndex = filename.lastIndexOf(".");
773
780
  const extension = lastDotIndex > 0 ? filename.slice(lastDotIndex) : "";
774
- return kebabCase(lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename) + extension;
781
+ const nameWithoutExt = lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename;
782
+ return kebabCase(nameWithoutExt) + extension;
775
783
  }
776
784
  /**
777
785
  * Create a temporary directory atomically and return its path.
@@ -1433,7 +1441,8 @@ const getDefaults = () => ({
1433
1441
  });
1434
1442
  const loadConfigFromFile = async (filePath) => {
1435
1443
  if (!await fileExists(filePath)) return {};
1436
- const jsonData = parse(await readFileContent(filePath));
1444
+ const fileContent = await readFileContent(filePath);
1445
+ const jsonData = parse(fileContent);
1437
1446
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
1438
1447
  assertTargetsFeaturesExclusive({
1439
1448
  targets: configParams.targets,
@@ -1523,7 +1532,8 @@ var ConfigResolver = class {
1523
1532
  if (inputRoot !== void 0) validateOutputRoot(inputRoot);
1524
1533
  const validatedConfigPath = resolvePath(configPath, resolve(inputRoot ?? cwd));
1525
1534
  const baseConfig = await loadConfigFromFile(validatedConfigPath);
1526
- const localConfigPath = join(dirname(validatedConfigPath), RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
1535
+ const configDir = dirname(validatedConfigPath);
1536
+ const localConfigPath = join(configDir, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
1527
1537
  const localConfig = await loadConfigFromFile(localConfigPath);
1528
1538
  const configByFile = mergeConfigs(baseConfig, localConfig);
1529
1539
  if (inputRoot === void 0 && configByFile.inputRoot !== void 0) validateOutputRoot(configByFile.inputRoot);
@@ -1688,6 +1698,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
1688
1698
  ".devin/config.json",
1689
1699
  ".factory/settings.json",
1690
1700
  ".grok/config.toml",
1701
+ ".rovodev/config.yml",
1702
+ ".rovodev/mcp.json",
1691
1703
  ".vibe/config.toml",
1692
1704
  "reasonix.toml",
1693
1705
  ".vscode/settings.json",
@@ -1757,7 +1769,8 @@ var AiFile = class {
1757
1769
  getFilePath() {
1758
1770
  const fullPath = path.join(this.outputRoot, this.relativeDirPath, this.relativeFilePath);
1759
1771
  const resolvedFull = resolve(fullPath);
1760
- const rel = relative(resolve(this.outputRoot), resolvedFull);
1772
+ const resolvedBase = resolve(this.outputRoot);
1773
+ const rel = relative(resolvedBase, resolvedFull);
1761
1774
  if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", relativeFilePath="${this.relativeFilePath}"`);
1762
1775
  return fullPath;
1763
1776
  }
@@ -2325,6 +2338,7 @@ const OPENCODE_HOOK_EVENTS = [
2325
2338
  "postToolUse",
2326
2339
  "stop",
2327
2340
  "afterFileEdit",
2341
+ "beforeShellExecution",
2328
2342
  "afterShellExecution",
2329
2343
  "permissionRequest",
2330
2344
  "preCompact",
@@ -2356,6 +2370,7 @@ const PI_HOOK_EVENTS = [
2356
2370
  "preToolUse",
2357
2371
  "postToolUse",
2358
2372
  "preModelInvocation",
2373
+ "postModelInvocation",
2359
2374
  "beforeSubmitPrompt",
2360
2375
  "stop",
2361
2376
  "preCompact",
@@ -3029,7 +3044,6 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
3029
3044
  postToolUse: "tool.execute.after",
3030
3045
  stop: "session.idle",
3031
3046
  afterFileEdit: "file.edited",
3032
- afterShellExecution: "command.executed",
3033
3047
  permissionRequest: "permission.asked",
3034
3048
  preCompact: "experimental.session.compacting",
3035
3049
  postCompact: "session.compacted",
@@ -3062,6 +3076,7 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
3062
3076
  preToolUse: "tool_call",
3063
3077
  postToolUse: "tool_result",
3064
3078
  preModelInvocation: "context",
3079
+ postModelInvocation: "message_end",
3065
3080
  beforeSubmitPrompt: "input",
3066
3081
  stop: "agent_end",
3067
3082
  preCompact: "session_before_compact",
@@ -4095,10 +4110,22 @@ const ClaudecodePermissionsOverrideSchema = z.looseObject({
4095
4110
  * category; the shared `permission` block still drives the base permission and
4096
4111
  * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
4097
4112
  *
4113
+ * Vibe's top-level `enabled_tools` is an **exclusive** allowlist — when set,
4114
+ * only the listed tools (name or pattern) are active and every other builtin
4115
+ * and MCP tool is off. That narrowing cannot be derived from canonical `allow`
4116
+ * rules (an allow grants one tool without revoking the rest), so the list is
4117
+ * only ever authored explicitly here, verbatim in Vibe's tool-name vocabulary,
4118
+ * and round-trips through this override on import.
4119
+ *
4098
4120
  * @example
4099
4121
  * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
4122
+ * @example
4123
+ * { "enabled_tools": ["bash", "read_file", "grep"] }
4100
4124
  */
4101
- const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))) });
4125
+ const VibePermissionsOverrideSchema = z.looseObject({
4126
+ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))),
4127
+ enabled_tools: z.optional(z.array(z.string()))
4128
+ });
4102
4129
  /**
4103
4130
  * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
4104
4131
  * autonomy settings with no canonical permission category — `approvalMode`
@@ -4170,7 +4197,10 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
4170
4197
  const ReasonixPermissionsOverrideSchema = z.looseObject({
4171
4198
  permission: z.optional(ToolScopedPermissionSchema),
4172
4199
  sandbox: z.optional(z.looseObject({})),
4173
- agent: z.optional(z.looseObject({}))
4200
+ agent: z.optional(z.looseObject({})),
4201
+ rawAllow: z.optional(z.array(z.string())),
4202
+ rawAsk: z.optional(z.array(z.string())),
4203
+ rawDeny: z.optional(z.array(z.string()))
4174
4204
  });
4175
4205
  /**
4176
4206
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -4201,12 +4231,69 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({
4201
4231
  * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
4202
4232
  * Fields placed here are merged into `[agents.profiles]` of Warp's global
4203
4233
  * `settings.toml`, while the shared `permission` block continues to drive the
4204
- * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
4205
- * Warp permissions are global-only.
4234
+ * command regex arrays (legacy `agent_mode_command_execution_allowlist`/
4235
+ * `_denylist` plus the `default` execution profile's `command_allowlist`/
4236
+ * `command_denylist`). On migrated installs the `[agents.profiles]` keys are
4237
+ * inert; their execution-profile counterparts are authored via the nested
4238
+ * `execution_profile` block instead, which is merged into the `default` record
4239
+ * of `[agents.execution_profiles.<id>]`. Warp permissions are global-only.
4206
4240
  *
4207
4241
  * @example
4208
- * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
4242
+ * {
4243
+ * "agent_mode_coding_permissions": "always_allow_reading",
4244
+ * "agent_mode_execute_readonly_commands": true,
4245
+ * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
4246
+ * }
4247
+ */
4248
+ /**
4249
+ * Per-action autonomy value of Warp's execution profiles
4250
+ * (`FileActionPermission` in the Warp repository's
4251
+ * `app/src/ai/execution_profiles/config.rs`, serialized snake_case).
4209
4252
  */
4253
+ const WarpFileActionPermissionSchema = z.enum([
4254
+ "agent_decides",
4255
+ "always_allow",
4256
+ "always_ask"
4257
+ ]);
4258
+ /**
4259
+ * Permission keys of the `default` record in Warp's
4260
+ * `[agents.execution_profiles.<id>]` collection (the surface runtime
4261
+ * enforcement reads on migrated installs). Loose so forward-compat keys pass
4262
+ * through verbatim. The rulesync-owned `command_allowlist`/`command_denylist`
4263
+ * are driven by the shared `permission.bash` block and always win over values
4264
+ * placed here.
4265
+ *
4266
+ * @see https://github.com/warpdotdev/warp/blob/main/app/src/ai/execution_profiles/config.rs (`ExecutionProfileFile`)
4267
+ */
4268
+ const WarpExecutionProfileOverrideSchema = z.looseObject({
4269
+ read_files: z.optional(WarpFileActionPermissionSchema),
4270
+ apply_code_diffs: z.optional(WarpFileActionPermissionSchema),
4271
+ execute_commands: z.optional(WarpFileActionPermissionSchema),
4272
+ mcp_permissions: z.optional(WarpFileActionPermissionSchema),
4273
+ write_to_pty: z.optional(z.enum([
4274
+ "always_allow",
4275
+ "always_ask",
4276
+ "ask_on_first_write"
4277
+ ])),
4278
+ ask_user_question: z.optional(z.enum([
4279
+ "never",
4280
+ "ask_except_in_auto_approve",
4281
+ "always_ask"
4282
+ ])),
4283
+ run_agents: z.optional(z.enum([
4284
+ "never_allow",
4285
+ "always_allow",
4286
+ "always_ask"
4287
+ ])),
4288
+ computer_use: z.optional(z.enum([
4289
+ "never",
4290
+ "always_ask",
4291
+ "always_allow"
4292
+ ])),
4293
+ directory_allowlist: z.optional(z.array(z.string())),
4294
+ mcp_allowlist: z.optional(z.array(z.string())),
4295
+ mcp_denylist: z.optional(z.array(z.string()))
4296
+ });
4210
4297
  const WarpPermissionsOverrideSchema = z.looseObject({
4211
4298
  permission: z.optional(ToolScopedPermissionSchema),
4212
4299
  agent_mode_coding_permissions: z.optional(z.enum([
@@ -4215,7 +4302,8 @@ const WarpPermissionsOverrideSchema = z.looseObject({
4215
4302
  "allow_reading_specific_files"
4216
4303
  ])),
4217
4304
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
4218
- agent_mode_execute_readonly_commands: z.optional(z.boolean())
4305
+ agent_mode_execute_readonly_commands: z.optional(z.boolean()),
4306
+ execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
4219
4307
  });
4220
4308
  /**
4221
4309
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -4947,7 +5035,8 @@ var AiDir = class {
4947
5035
  getDirPath() {
4948
5036
  const fullPath = path.join(this.outputRoot, this.relativeDirPath, this.dirName);
4949
5037
  const resolvedFull = resolve(fullPath);
4950
- const rel = relative(resolve(this.outputRoot), resolvedFull);
5038
+ const resolvedBase = resolve(this.outputRoot);
5039
+ const rel = relative(resolvedBase, resolvedFull);
4951
5040
  if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}", dirName="${this.dirName}"`);
4952
5041
  return fullPath;
4953
5042
  }
@@ -5230,6 +5319,9 @@ const RulesyncSubagentFrontmatterSchema = z.looseObject({
5230
5319
  customInstructions: z.optional(z.string()),
5231
5320
  groups: z.optional(z.array(z.unknown()))
5232
5321
  })),
5322
+ zoocode: z.optional(z.looseObject({
5323
+ /** Per-mode MCP server allowlist (Zoo Code v3.60.0+); omitted = all. */
5324
+ allowedMcpServers: z.optional(z.array(z.string())) })),
5233
5325
  vibe: z.optional(z.looseObject({
5234
5326
  agent_type: z.optional(z.enum(["agent", "subagent"])),
5235
5327
  display_name: z.optional(z.string()),
@@ -5369,7 +5461,6 @@ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
5369
5461
  const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
5370
5462
  const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
5371
5463
  const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5372
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5373
5464
  const ROVODEV_MCP_FILE_NAME = "mcp.json";
5374
5465
  const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5375
5466
  const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
@@ -6588,6 +6679,7 @@ const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6588
6679
  const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6589
6680
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6590
6681
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6682
+ const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
6591
6683
  /**
6592
6684
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
6593
6685
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -6827,24 +6919,14 @@ const SHARED_CONFIG_OWNERSHIP = {
6827
6919
  },
6828
6920
  ".devin/config.json": {
6829
6921
  format: "json",
6830
- features: {
6831
- mcp: {
6832
- kind: "replace-owned-keys",
6833
- ownedKeys: ["mcpServers"]
6834
- },
6835
- permissions: {
6836
- kind: "replace-owned-keys",
6837
- ownedKeys: ["permissions"]
6838
- }
6839
- }
6922
+ features: { permissions: {
6923
+ kind: "replace-owned-keys",
6924
+ ownedKeys: ["permissions"]
6925
+ } }
6840
6926
  },
6841
6927
  ".config/devin/config.json": {
6842
6928
  format: "json",
6843
6929
  features: {
6844
- mcp: {
6845
- kind: "replace-owned-keys",
6846
- ownedKeys: ["mcpServers"]
6847
- },
6848
6930
  hooks: {
6849
6931
  kind: "replace-owned-keys",
6850
6932
  ownedKeys: ["hooks"]
@@ -6937,6 +7019,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6937
7019
  permissions: {
6938
7020
  kind: "replace-owned-keys",
6939
7021
  ownedKeys: ["permission"]
7022
+ },
7023
+ rules: {
7024
+ kind: "replace-owned-keys",
7025
+ ownedKeys: ["instructions"]
6940
7026
  }
6941
7027
  }
6942
7028
  },
@@ -7054,6 +7140,19 @@ const SHARED_CONFIG_OWNERSHIP = {
7054
7140
  ]
7055
7141
  }
7056
7142
  }
7143
+ },
7144
+ [ROVODEV_CONFIG_SHARED_FILE_KEY]: {
7145
+ format: "yaml",
7146
+ features: {
7147
+ mcp: {
7148
+ kind: "replace-owned-keys",
7149
+ ownedKeys: ["mcp"]
7150
+ },
7151
+ permissions: {
7152
+ kind: "replace-owned-keys",
7153
+ ownedKeys: ["toolPermissions"]
7154
+ }
7155
+ }
7057
7156
  }
7058
7157
  };
7059
7158
  /**
@@ -7475,7 +7574,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7475
7574
  class: CursorCheck,
7476
7575
  meta: {
7477
7576
  supportsGlobal: false,
7478
- filePattern: CURSOR_BUGBOT_FILE_NAME
7577
+ filePattern: CURSOR_BUGBOT_FILE_NAME,
7578
+ committedOutput: true
7479
7579
  }
7480
7580
  }],
7481
7581
  ["hermesagent", {
@@ -7489,7 +7589,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7489
7589
  class: RovodevCheck,
7490
7590
  meta: {
7491
7591
  supportsGlobal: false,
7492
- filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7592
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME,
7593
+ committedOutput: true
7493
7594
  }
7494
7595
  }],
7495
7596
  ["takt", {
@@ -9044,6 +9145,7 @@ const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
9044
9145
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
9045
9146
  const DEVIN_GLOBAL_SKILLS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
9046
9147
  const DEVIN_CONFIG_FILE_NAME = "config.json";
9148
+ const DEVIN_MCP_CONFIG_FILE_NAME = "mcp_config.json";
9047
9149
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9048
9150
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9049
9151
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
@@ -9650,7 +9752,8 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9650
9752
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9651
9753
  const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
9652
9754
  if (commandNames.size === 0) return;
9653
- const shadowed = (await findFilesByGlobs(join(join(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH), "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
9755
+ const skillsRoot = join(inputRoot, RULESYNC_SKILLS_RELATIVE_DIR_PATH);
9756
+ const shadowed = (await findFilesByGlobs(join(skillsRoot, "**", "SKILL.md"))).map((filePath) => basename(dirname(filePath))).filter((skillName) => commandNames.has(skillName));
9654
9757
  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.`);
9655
9758
  }
9656
9759
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -9685,6 +9788,20 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9685
9788
  function toolSkillSearchRoots(paths) {
9686
9789
  return [paths.relativeDirPath, ...paths.alternativeSkillRoots ?? []];
9687
9790
  }
9791
+ /**
9792
+ * The Agent Skills interoperability roots: project `.agents/skills/`, global
9793
+ * `~/.agents/skills/` (same relative path, home output root), and Amp's global
9794
+ * equivalent `~/.config/agents/skills/`. Foreign-authored — potentially
9795
+ * non-conformant — skills are the most likely to live here, so every tool that
9796
+ * scans one of these roots imports it leniently (skip-and-warn per skill), per
9797
+ * the Agent Skills client-implementation guide, while keeping fail-fast
9798
+ * behavior for its own native root.
9799
+ * https://agentskills.io/client-implementation/adding-skills-support
9800
+ */
9801
+ const AGENT_SKILLS_INTEROP_ROOTS = /* @__PURE__ */ new Set([toPosixPath(AGENTSMD_SKILLS_DIR_PATH), toPosixPath(AMP_SKILLS_GLOBAL_DIR)]);
9802
+ function isAgentSkillsInteropRoot(relativeDirPath) {
9803
+ return AGENT_SKILLS_INTEROP_ROOTS.has(toPosixPath(relativeDirPath));
9804
+ }
9688
9805
  /** Ordered import roots: managed roots first, followed by read-only discovery roots. */
9689
9806
  function toolSkillImportRoots(paths) {
9690
9807
  return [...toolSkillSearchRoots(paths), ...paths.importOnlySkillRoots ?? []];
@@ -11301,6 +11418,7 @@ const QWENCODE_COMMANDS_DIR_PATH = join(QWENCODE_DIR, "commands");
11301
11418
  const QWENCODE_AGENTS_DIR_PATH = join(QWENCODE_DIR, "agents");
11302
11419
  const QWENCODE_SKILLS_DIR_PATH = join(QWENCODE_DIR, "skills");
11303
11420
  const QWENCODE_RULE_FILE_NAME = "QWEN.md";
11421
+ const QWENCODE_LOCAL_RULE_FILE_NAME = "QWEN.local.md";
11304
11422
  const QWENCODE_IGNORE_FILE_NAME = ".qwenignore";
11305
11423
  const QWENCODE_SETTINGS_FILE_NAME = "settings.json";
11306
11424
  //#endregion
@@ -11627,7 +11745,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11627
11745
  };
11628
11746
  const body = rulesyncCommand.getBody();
11629
11747
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
11630
- return new RooCommand({
11748
+ return new this({
11631
11749
  outputRoot,
11632
11750
  frontmatter: rooFrontmatter,
11633
11751
  body,
@@ -11664,7 +11782,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11664
11782
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
11665
11783
  const result = RooCommandFrontmatterSchema.safeParse(frontmatter);
11666
11784
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
11667
- return new RooCommand({
11785
+ return new this({
11668
11786
  outputRoot,
11669
11787
  relativeDirPath: RooCommand.getSettablePaths().relativeDirPath,
11670
11788
  relativeFilePath,
@@ -11675,7 +11793,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11675
11793
  });
11676
11794
  }
11677
11795
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
11678
- return new RooCommand({
11796
+ return new this({
11679
11797
  outputRoot,
11680
11798
  relativeDirPath,
11681
11799
  relativeFilePath,
@@ -12047,6 +12165,7 @@ const WARP_SKILLS_DIR_PATH = join(WARP_DIR, "skills");
12047
12165
  const WARP_LINUX_DIR = join(".config", "warp-terminal");
12048
12166
  const WARP_WIN32_DIR = join("AppData", "Local", "warp", "Warp", "config");
12049
12167
  const WARP_RULE_FILE_NAME = "AGENTS.md";
12168
+ const WARP_GLOBAL_RULE_DIR = ".agents";
12050
12169
  const WARP_MCP_FILE_NAME = ".mcp.json";
12051
12170
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
12052
12171
  const WARP_IGNORE_FILE_NAME = ".warpindexingignore";
@@ -12132,6 +12251,39 @@ var WarpCommand = class WarpCommand extends ToolCommand {
12132
12251
  }
12133
12252
  };
12134
12253
  //#endregion
12254
+ //#region src/features/commands/zoocode-command.ts
12255
+ /**
12256
+ * Command generator for **Zoo Code** (the community continuation of Roo Code).
12257
+ * Zoo Code keeps Roo's `.roo/commands/` layout, so this target reuses
12258
+ * {@link RooCommand} verbatim and only narrows the targeting.
12259
+ *
12260
+ * @see https://docs.zoocode.dev
12261
+ */
12262
+ var ZoocodeCommand = class extends RooCommand {
12263
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
12264
+ return this.isTargetedByRulesyncCommandDefault({
12265
+ rulesyncCommand,
12266
+ toolTarget: "zoocode"
12267
+ });
12268
+ }
12269
+ toRulesyncCommand() {
12270
+ const command = super.toRulesyncCommand();
12271
+ const frontmatter = {
12272
+ ...command.getFrontmatter(),
12273
+ targets: ["zoocode"]
12274
+ };
12275
+ return new RulesyncCommand({
12276
+ outputRoot: command.getOutputRoot(),
12277
+ relativeDirPath: command.getRelativeDirPath(),
12278
+ relativeFilePath: command.getRelativeFilePath(),
12279
+ frontmatter,
12280
+ body: command.getBody(),
12281
+ fileContent: stringifyFrontmatter(command.getBody(), frontmatter),
12282
+ validate: true
12283
+ });
12284
+ }
12285
+ };
12286
+ //#endregion
12135
12287
  //#region src/features/commands/commands-processor.ts
12136
12288
  const CommandsProcessorToolTargetSchema = z.enum(commandsProcessorToolTargetTuple);
12137
12289
  /**
@@ -12391,6 +12543,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
12391
12543
  supportsSubdirectory: true
12392
12544
  }
12393
12545
  }],
12546
+ ["zoocode", {
12547
+ class: ZoocodeCommand,
12548
+ meta: {
12549
+ extension: "md",
12550
+ supportsProject: true,
12551
+ supportsGlobal: true,
12552
+ isSimulated: false,
12553
+ supportsSubdirectory: true
12554
+ }
12555
+ }],
12394
12556
  ["rovodev", {
12395
12557
  class: RovodevCommand,
12396
12558
  meta: {
@@ -13770,16 +13932,17 @@ var ClaudecodeHooks = class extends ToolHooks {
13770
13932
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13771
13933
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
13772
13934
  const config = rulesyncHooks.getJson();
13935
+ const claudeHooks = canonicalToToolHooks({
13936
+ config,
13937
+ toolOverrideHooks: config.claudecode?.hooks,
13938
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
13939
+ logger
13940
+ });
13773
13941
  const fileContent = applySharedConfigPatch({
13774
13942
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
13775
13943
  feature: "hooks",
13776
13944
  existingContent,
13777
- patch: { hooks: canonicalToToolHooks({
13778
- config,
13779
- toolOverrideHooks: config.claudecode?.hooks,
13780
- converterConfig: CLAUDE_CONVERTER_CONFIG,
13781
- logger
13782
- }) },
13945
+ patch: { hooks: claudeHooks },
13783
13946
  filePath
13784
13947
  });
13785
13948
  return new this({
@@ -14782,9 +14945,9 @@ const DEVIN_CONVERTER_CONFIG = {
14782
14945
  * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
14783
14946
  * level IS the event map (no wrapper key).
14784
14947
  * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
14785
- * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
14786
- * features, so reads and writes merge into the existing JSON and the file is
14787
- * never deleted in global mode.
14948
+ * file is shared with the permissions (`permissions`) feature (MCP moved to
14949
+ * the dedicated mcp_config.json in v3000.3), so reads and writes merge into
14950
+ * the existing JSON and the file is never deleted in global mode.
14788
14951
  *
14789
14952
  * @see https://docs.devin.ai/cli/extensibility/hooks/overview
14790
14953
  */
@@ -15533,6 +15696,30 @@ const NAMED_HOOK_MATCHER_SUBJECTS = {
15533
15696
  "tool.execute.after": "input.tool",
15534
15697
  "experimental.session.compacting": null
15535
15698
  };
15699
+ /**
15700
+ * OpenCode (and Kilo) have no shell-execution lifecycle event — the
15701
+ * `command.executed` event these canonical events were once mapped to is a
15702
+ * *slash-command* event, so a hook wired there never fired on bash commands
15703
+ * and fired on every slash command instead. Observing a shell invocation is
15704
+ * done with the named `tool.execute.before/after` hooks gated on the `bash`
15705
+ * tool (the `.env protection` pattern in the OpenCode plugins doc), so these
15706
+ * canonical events are emitted into those named hooks with an implicit
15707
+ * `input.tool === "bash"` gate. Matchers on them are dropped upstream with a
15708
+ * warning (`matcherEvents` covers only `preToolUse`/`postToolUse`) — the
15709
+ * named hooks expose no command text to match against.
15710
+ *
15711
+ * @see https://opencode.ai/docs/plugins/
15712
+ */
15713
+ const SHELL_EVENT_TOOL_GATES = {
15714
+ beforeShellExecution: {
15715
+ toolEvent: "tool.execute.before",
15716
+ tool: "bash"
15717
+ },
15718
+ afterShellExecution: {
15719
+ toolEvent: "tool.execute.after",
15720
+ tool: "bash"
15721
+ }
15722
+ };
15536
15723
  function escapeForTemplateLiteral(command) {
15537
15724
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
15538
15725
  }
@@ -15553,15 +15740,19 @@ function validateAndSanitizeMatcher(matcher) {
15553
15740
  */
15554
15741
  function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
15555
15742
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
15556
- const toolEvent = eventMap[canonicalEvent];
15743
+ const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
15744
+ const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
15557
15745
  if (!toolEvent) continue;
15746
+ const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
15558
15747
  const handlers = [];
15559
15748
  for (const def of definitions) {
15560
15749
  if ((def.type ?? "command") !== "command") continue;
15561
15750
  if (!def.command) continue;
15751
+ if (def.matcher && !matcherSupported) continue;
15562
15752
  handlers.push({
15563
15753
  command: def.command,
15564
- matcher: def.matcher ? def.matcher : void 0
15754
+ matcher: def.matcher ? def.matcher : void 0,
15755
+ ...shellGate ? { toolGate: shellGate.tool } : {}
15565
15756
  });
15566
15757
  }
15567
15758
  if (handlers.length > 0) {
@@ -15598,7 +15789,11 @@ function buildNamedEventBodyLines(namedEventHandlers) {
15598
15789
  bodyLines.push(` "${eventName}": async (input) => {`);
15599
15790
  for (const handler of handlers) {
15600
15791
  const escapedCommand = escapeForTemplateLiteral(handler.command);
15601
- if (handler.matcher && matcherSubject !== null) {
15792
+ if (handler.toolGate) {
15793
+ bodyLines.push(` if (input.tool === "${handler.toolGate}") {`);
15794
+ bodyLines.push(` await $\`${escapedCommand}\`;`);
15795
+ bodyLines.push(" }");
15796
+ } else if (handler.matcher && matcherSubject !== null) {
15602
15797
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
15603
15798
  bodyLines.push(" {");
15604
15799
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
@@ -16396,6 +16591,13 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
16396
16591
  */
16397
16592
  const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
16398
16593
  /**
16594
+ * Pi extension events that fire for every message role (user, assistant,
16595
+ * toolResult). The generated handler gates these on the assistant role so a
16596
+ * `postModelInvocation` hook runs once per finalized model response rather
16597
+ * than for every message in the conversation.
16598
+ */
16599
+ const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
16600
+ /**
16399
16601
  * Validate a hook matcher as a regular expression and return it as a JS
16400
16602
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
16401
16603
  */
@@ -16436,7 +16638,10 @@ function buildSubscriptionLines(handlerGroups) {
16436
16638
  const lines = [];
16437
16639
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
16438
16640
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
16439
- lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesToolName ? "event" : ""}) => {`);
16641
+ const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
16642
+ const usesEvent = usesToolName || gatesOnAssistant;
16643
+ lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
16644
+ if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
16440
16645
  for (const handler of handlers) {
16441
16646
  const embeddedCommand = JSON.stringify(handler.command);
16442
16647
  if (usesToolName && handler.matcher) {
@@ -16571,6 +16776,7 @@ function canonicalDefToQwencodeHook(def) {
16571
16776
  const type = def.type ?? "command";
16572
16777
  const isHttp = type === "http";
16573
16778
  const isCommand = type === "command";
16779
+ const isPrompt = type === "prompt";
16574
16780
  return {
16575
16781
  type,
16576
16782
  ...compact({
@@ -16580,6 +16786,8 @@ function canonicalDefToQwencodeHook(def) {
16580
16786
  name: def.name,
16581
16787
  description: def.description,
16582
16788
  statusMessage: def.statusMessage,
16789
+ prompt: isPrompt ? def.prompt : void 0,
16790
+ model: isPrompt ? def.model : void 0,
16583
16791
  async: isCommand ? def.async : void 0,
16584
16792
  env: isCommand ? def.env : void 0,
16585
16793
  shell: isCommand ? def.shell : void 0,
@@ -16598,7 +16806,7 @@ function canonicalDefToQwencodeHook(def) {
16598
16806
  * is needed here — commands are passed through verbatim and any such variable
16599
16807
  * reference stays intact for Qwen Code to expand.
16600
16808
  */
16601
- function canonicalToQwencodeHooks(config) {
16809
+ function canonicalToQwencodeHooks(config, logger) {
16602
16810
  const qwencodeSupported = new Set(QWENCODE_HOOK_EVENTS);
16603
16811
  const sharedHooks = {};
16604
16812
  for (const [event, defs] of Object.entries(config.hooks)) if (qwencodeSupported.has(event)) sharedHooks[event] = defs;
@@ -16618,6 +16826,7 @@ function canonicalToQwencodeHooks(config) {
16618
16826
  const byMatcher = /* @__PURE__ */ new Map();
16619
16827
  for (const def of definitions) {
16620
16828
  if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
16829
+ if (def.type === "prompt" && !def.prompt) logger?.warn(`Qwen Code prompt hook on '${eventName}' has no 'prompt' field; Qwen Code will load it and fail it at runtime.`);
16621
16830
  const key = def.matcher ?? "";
16622
16831
  const list = byMatcher.get(key);
16623
16832
  if (list) list.push(def);
@@ -16656,7 +16865,9 @@ const QwencodeHookEntrySchema = z.looseObject({
16656
16865
  shell: z.optional(z.string()),
16657
16866
  headers: z.optional(z.record(z.string(), z.string())),
16658
16867
  allowedEnvVars: z.optional(z.array(z.string())),
16659
- once: z.optional(z.boolean())
16868
+ once: z.optional(z.boolean()),
16869
+ prompt: z.optional(z.string()),
16870
+ model: z.optional(z.string())
16660
16871
  });
16661
16872
  /**
16662
16873
  * A matcher group entry in a Qwen Code event array.
@@ -16671,37 +16882,43 @@ const QwencodeMatcherEntrySchema = z.looseObject({
16671
16882
  /**
16672
16883
  * Convert a single parsed Qwen Code matcher group into canonical hook definitions.
16673
16884
  */
16885
+ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
16886
+ const h = hook;
16887
+ const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16888
+ const isHttp = hookType === "http";
16889
+ const isCommand = hookType === "command";
16890
+ const isPrompt = hookType === "prompt";
16891
+ const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16892
+ return {
16893
+ type: hookType,
16894
+ ...compact({
16895
+ command: h.command,
16896
+ url: h.url,
16897
+ timeout: h.timeout,
16898
+ name: h.name,
16899
+ description: h.description,
16900
+ statusMessage: h.statusMessage,
16901
+ async: isCommand ? h.async : void 0,
16902
+ env: isCommand ? h.env : void 0,
16903
+ shell: isCommand ? shell : void 0,
16904
+ headers: isHttp ? h.headers : void 0,
16905
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16906
+ once: isHttp ? h.once : void 0,
16907
+ prompt: isPrompt ? h.prompt : void 0,
16908
+ model: isPrompt ? h.model : void 0,
16909
+ sequential: sequential ? true : void 0,
16910
+ matcher
16911
+ })
16912
+ };
16913
+ }
16674
16914
  function qwencodeMatcherEntryToCanonical(entry) {
16675
- const defs = [];
16676
- const hooks = entry.hooks ?? [];
16677
16915
  const sequential = entry.sequential === true;
16678
16916
  const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
16679
- for (const h of hooks) {
16680
- const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16681
- const isHttp = hookType === "http";
16682
- const isCommand = hookType === "command";
16683
- const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16684
- defs.push({
16685
- type: hookType,
16686
- ...compact({
16687
- command: h.command,
16688
- url: h.url,
16689
- timeout: h.timeout,
16690
- name: h.name,
16691
- description: h.description,
16692
- statusMessage: h.statusMessage,
16693
- async: isCommand ? h.async : void 0,
16694
- env: isCommand ? h.env : void 0,
16695
- shell: isCommand ? shell : void 0,
16696
- headers: isHttp ? h.headers : void 0,
16697
- allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16698
- once: isHttp ? h.once : void 0,
16699
- sequential: sequential ? true : void 0,
16700
- matcher
16701
- })
16702
- });
16703
- }
16704
- return defs;
16917
+ return (entry.hooks ?? []).map((hook) => qwencodeHookEntryToCanonical({
16918
+ hook,
16919
+ sequential,
16920
+ matcher
16921
+ }));
16705
16922
  }
16706
16923
  /**
16707
16924
  * Extract hooks from Qwen Code settings.json into canonical format.
@@ -16749,12 +16966,12 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
16749
16966
  validate
16750
16967
  });
16751
16968
  }
16752
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
16969
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16753
16970
  const paths = QwencodeHooks.getSettablePaths({ global });
16754
16971
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16755
16972
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16756
16973
  const config = rulesyncHooks.getJson();
16757
- const patch = { hooks: canonicalToQwencodeHooks(config) };
16974
+ const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
16758
16975
  const disableAllHooks = config.qwencode?.disableAllHooks;
16759
16976
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
16760
16977
  const fileContent = applySharedConfigPatch({
@@ -17306,7 +17523,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17306
17523
  },
17307
17524
  supportedEvents: KILO_HOOK_EVENTS,
17308
17525
  supportedHookTypes: ["command"],
17309
- supportsMatcher: true
17526
+ supportsMatcher: true,
17527
+ matcherEvents: ["preToolUse", "postToolUse"]
17310
17528
  }],
17311
17529
  ["opencode", {
17312
17530
  class: OpencodeHooks,
@@ -17317,7 +17535,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17317
17535
  },
17318
17536
  supportedEvents: OPENCODE_HOOK_EVENTS,
17319
17537
  supportedHookTypes: ["command"],
17320
- supportsMatcher: true
17538
+ supportsMatcher: true,
17539
+ matcherEvents: ["preToolUse", "postToolUse"]
17321
17540
  }],
17322
17541
  ["pi", {
17323
17542
  class: PiHooks,
@@ -18991,6 +19210,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
18991
19210
  ["qwencode", { class: QwencodeIgnore }],
18992
19211
  ["reasonix", { class: ReasonixIgnore }],
18993
19212
  ["roo", { class: RooIgnore }],
19213
+ ["zoocode", { class: RooIgnore }],
18994
19214
  ["devin", { class: DevinIgnore }],
18995
19215
  ["vibe", { class: VibeIgnore }],
18996
19216
  ["warp", { class: WarpIgnore }],
@@ -20360,7 +20580,6 @@ const resolveCopilotcliServerType = (server) => {
20360
20580
  case "streamable-http": return "http";
20361
20581
  case "stdio":
20362
20582
  case "local": return declared;
20363
- default: break;
20364
20583
  }
20365
20584
  if (server.command !== void 0) return "stdio";
20366
20585
  return isRemoteMcpServer(server) ? "http" : "stdio";
@@ -20780,18 +20999,24 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
20780
20999
  /**
20781
21000
  * MCP generator for Devin Local (native `.devin/` configuration).
20782
21001
  *
20783
- * Devin reads MCP servers from the `mcpServers` key of its native config file:
20784
- * - Project scope: `.devin/config.json`
20785
- * - Global scope: `~/.config/devin/config.json`
21002
+ * Since v3000.3 (the Local 3.6 release), Devin reads MCP servers from a
21003
+ * dedicated `mcpServers`-keyed config file rather than the shared
21004
+ * `config.json`:
21005
+ * - Project scope: `.devin/mcp_config.json`
21006
+ * - Global scope: `~/.config/devin/mcp_config.json`
21007
+ *
21008
+ * Legacy `mcpServers` entries left in `config.json` are auto-migrated into
21009
+ * the dedicated file on Devin startup, so writing the legacy key would fight
21010
+ * the migration. Import still falls back to the legacy `config.json`
21011
+ * `mcpServers` key when no dedicated file exists, so pre-v3000.3 repos
21012
+ * migrate cleanly. The gitignored `.devin/mcp_config.local.json` override is
21013
+ * the user's personal territory and is never read or written.
20786
21014
  *
20787
- * The config file is shared with the permissions feature (`permissions` key)
20788
- * and, in global mode, the hooks feature (`hooks` key), so reads and writes
20789
- * merge into the existing JSON rather than overwriting it, and the file is
20790
- * never deleted. Each server is a stdio entry ({ command, args, env }) or a
20791
- * remote entry ({ serverUrl | url, headers }), and may carry an optional
20792
- * `disabledTools` array.
21015
+ * Each server is a stdio entry ({ command, args, env }) or a remote entry
21016
+ * ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
21017
+ * array.
20793
21018
  *
20794
- * @see https://docs.devin.ai/cli/extensibility/configuration
21019
+ * @see https://docs.devin.ai/cli/extensibility/mcp/configuration
20795
21020
  */
20796
21021
  var DevinMcp = class DevinMcp extends ToolMcp {
20797
21022
  json;
@@ -20809,31 +21034,27 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20809
21034
  throw new Error(`Failed to parse Devin MCP config at ${join(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
20810
21035
  }
20811
21036
  }
20812
- /**
20813
- * config.json may carry the permissions/hooks features' keys, so it is never
20814
- * deleted; only the managed `mcpServers` key is rewritten.
20815
- */
20816
- isDeletable() {
20817
- return false;
20818
- }
20819
21037
  static getSettablePaths({ global = false } = {}) {
20820
21038
  if (global) return {
20821
21039
  relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
20822
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
21040
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20823
21041
  };
20824
21042
  return {
20825
21043
  relativeDirPath: DEVIN_DIR,
20826
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
21044
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20827
21045
  };
20828
21046
  }
20829
21047
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20830
21048
  const paths = this.getSettablePaths({ global });
20831
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
20832
- const json = this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath);
20833
- const newJson = {
20834
- ...json,
20835
- mcpServers: json.mcpServers ?? {}
20836
- };
21049
+ let fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
21050
+ if (fileContent === null) {
21051
+ const legacyContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME));
21052
+ if (legacyContent !== null) {
21053
+ const legacyJson = this.parseJsonOrThrow(legacyContent, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME);
21054
+ fileContent = JSON.stringify({ mcpServers: legacyJson.mcpServers ?? {} }, null, 2);
21055
+ }
21056
+ }
21057
+ const newJson = { mcpServers: this.parseJsonOrThrow(fileContent ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath).mcpServers ?? {} };
20837
21058
  return new DevinMcp({
20838
21059
  outputRoot,
20839
21060
  relativeDirPath: paths.relativeDirPath,
@@ -20843,21 +21064,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20843
21064
  global
20844
21065
  });
20845
21066
  }
20846
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
21067
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
20847
21068
  const paths = this.getSettablePaths({ global });
20848
- const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20849
- const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({ mcpServers: {} }, null, 2);
21069
+ const existingContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
21070
+ if (existingContent !== null) {
21071
+ const existingJson = this.parseJsonOrThrow(existingContent, paths.relativeDirPath, paths.relativeFilePath);
21072
+ const existingServers = existingJson.mcpServers && typeof existingJson.mcpServers === "object" ? Object.keys(existingJson.mcpServers) : [];
21073
+ const managedServers = new Set(Object.keys(rulesyncMcp.getMcpServers()));
21074
+ const dropped = existingServers.filter((name) => !managedServers.has(name));
21075
+ if (dropped.length > 0) logger?.warn(`Devin MCP servers not managed by rulesync will be removed from ${join(paths.relativeDirPath, paths.relativeFilePath)}: ${dropped.join(", ")}. Run 'rulesync import' first to keep them, or move them to mcp_config.local.json.`);
21076
+ }
20850
21077
  return new DevinMcp({
20851
21078
  outputRoot,
20852
21079
  relativeDirPath: paths.relativeDirPath,
20853
21080
  relativeFilePath: paths.relativeFilePath,
20854
- fileContent: applySharedConfigPatch({
20855
- fileKey: sharedConfigFileKey(paths),
20856
- feature: "mcp",
20857
- existingContent,
20858
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
20859
- filePath
20860
- }),
21081
+ fileContent: JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2),
20861
21082
  validate,
20862
21083
  global
20863
21084
  });
@@ -22131,9 +22352,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22131
22352
  fileContent = await readFileContentOrNull(jsonPath);
22132
22353
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
22133
22354
  }
22355
+ if (instructions.length === 0 && fileContent === null) return null;
22134
22356
  const json = fileContent ? parse(fileContent) : {};
22135
22357
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22136
- const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
22358
+ const managedPrefix = `${toPosixPath(join(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
22359
+ const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
22360
+ const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...preservedInstructions, ...instructions])).toSorted();
22137
22361
  return new KiloMcp({
22138
22362
  outputRoot,
22139
22363
  relativeDirPath: basePaths.relativeDirPath,
@@ -22142,7 +22366,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22142
22366
  fileKey: sharedConfigFileKey(basePaths),
22143
22367
  feature: "rules",
22144
22368
  existingContent: fileContent ?? "",
22145
- patch: { instructions: mergedInstructions },
22369
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22146
22370
  filePath: join(jsonDir, relativeFilePath)
22147
22371
  }),
22148
22372
  validate
@@ -22864,6 +23088,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22864
23088
  static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
22865
23089
  const basePaths = this.getSettablePaths({ global });
22866
23090
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
23091
+ const configDirPrefix = `${toPosixPath(basePaths.relativeDirPath).replace(/\/+$/, "")}/`;
23092
+ const normalizedInstructions = instructions.map((path) => global && path.startsWith(configDirPrefix) ? path.slice(configDirPrefix.length) : path);
22867
23093
  let fileContent = null;
22868
23094
  let relativeFilePath = OPENCODE_JSONC_FILE_NAME;
22869
23095
  const jsoncPath = join(jsonDir, OPENCODE_JSONC_FILE_NAME);
@@ -22873,9 +23099,15 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22873
23099
  fileContent = await readFileContentOrNull(jsonPath);
22874
23100
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
22875
23101
  }
23102
+ if (instructions.length === 0 && fileContent === null) return null;
22876
23103
  const json = fileContent ? parse(fileContent) : {};
22877
23104
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22878
- const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
23105
+ const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
23106
+ const preservedInstructions = existingInstructions.filter((entry) => {
23107
+ const normalized = toPosixPath(entry).replace(/^\.\//, "");
23108
+ return !managedPrefixes.some((prefix) => normalized.startsWith(prefix));
23109
+ });
23110
+ const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...preservedInstructions, ...normalizedInstructions])).toSorted();
22879
23111
  return new OpencodeMcp({
22880
23112
  outputRoot,
22881
23113
  relativeDirPath: basePaths.relativeDirPath,
@@ -22884,7 +23116,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22884
23116
  fileKey: sharedConfigFileKey(basePaths),
22885
23117
  feature: "rules",
22886
23118
  existingContent: fileContent ?? "",
22887
- patch: { instructions: mergedInstructions },
23119
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22888
23120
  filePath: join(jsonDir, relativeFilePath)
22889
23121
  }),
22890
23122
  validate
@@ -23324,9 +23556,13 @@ function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
23324
23556
  return parsed;
23325
23557
  }
23326
23558
  /**
23327
- * Rovodev MCP: global only at ~/.rovodev/mcp.json.
23328
- * Same shape as Cursor: { mcpServers: { ... } }. See Rovodev MCP docs.
23329
- * Project-level MCP is not supported; use --global when generating.
23559
+ * Rovodev MCP: `~/.rovodev/mcp.json` (global) and the repo-committed project
23560
+ * `.rovodev/mcp.json` documented by the Bitbucket Cloud Agentic Pipelines
23561
+ * guide ("Register your MCP server in `.rovodev/mcp.json`", referenced via
23562
+ * `mcp.mcpConfigPath`). Same shape as Cursor: { mcpServers: { ... } }.
23563
+ * A server the canonical config marks `disabled: true` is still written here
23564
+ * and switched off through `mcp.disabledMcpServers` in the sibling
23565
+ * `config.yml` — the file Rovo Dev actually consults for disabling.
23330
23566
  */
23331
23567
  /**
23332
23568
  * Rovo Dev documents the per-server transport key as `transport`, with the
@@ -23352,11 +23588,7 @@ function lookupTransport(map, key) {
23352
23588
  return Object.hasOwn(map, key) ? map[key] : void 0;
23353
23589
  }
23354
23590
  function toRovodevServer(name, server, logger) {
23355
- const { type, transport, disabled, ...rest } = server;
23356
- if (disabled === true) {
23357
- logger?.warn(`Rovo Dev MCP: skipping "${name}" because it is disabled and mcp.json has no disable flag.`);
23358
- return null;
23359
- }
23591
+ const { type, transport, disabled: _disabled, ...rest } = server;
23360
23592
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
23361
23593
  if (declared === void 0) return rest;
23362
23594
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -23378,6 +23610,44 @@ function fromRovodevServer(server) {
23378
23610
  type: mapped
23379
23611
  };
23380
23612
  }
23613
+ /**
23614
+ * Read the sibling `config.yml` (same scope root as `mcp.json`) and return the
23615
+ * parsed document, `null` when the file does not exist. A malformed file
23616
+ * **throws**: the disable toggle lives here, so pretending a broken file says
23617
+ * nothing would silently re-enable servers on import and leave disabled ones
23618
+ * running on generate — both flip a security toggle the wrong way.
23619
+ */
23620
+ async function readRovodevConfigYaml({ outputRoot }) {
23621
+ const content = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME));
23622
+ if (content === null) return null;
23623
+ return parseSharedConfig({
23624
+ format: "yaml",
23625
+ fileContent: content,
23626
+ filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23627
+ });
23628
+ }
23629
+ function disabledNamesOf(config) {
23630
+ const mcpBlock = config && isRecord(config.mcp) ? config.mcp : {};
23631
+ return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
23632
+ }
23633
+ /**
23634
+ * Auxiliary writer for the `mcp:` block of `.rovodev/config.yml` (project) /
23635
+ * `~/.rovodev/config.yml` (global). Carries `disabledMcpServers` — the key
23636
+ * Rovo Dev actually consults to switch a server off — recomputed from the
23637
+ * existing block so user keys (`mcpConfigPath`, `allowedMcpServers`, ...) and
23638
+ * disabled names for servers rulesync does not manage survive.
23639
+ */
23640
+ var RovodevMcpConfigYaml = class extends ToolFile {
23641
+ isDeletable() {
23642
+ return false;
23643
+ }
23644
+ validate() {
23645
+ return {
23646
+ success: true,
23647
+ error: null
23648
+ };
23649
+ }
23650
+ };
23381
23651
  var RovodevMcp = class RovodevMcp extends ToolMcp {
23382
23652
  json;
23383
23653
  constructor(params) {
@@ -23398,13 +23668,24 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23398
23668
  };
23399
23669
  }
23400
23670
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
23401
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23402
23671
  const paths = this.getSettablePaths({ global });
23403
23672
  const json = parseRovodevMcpJson(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
23404
23673
  const newJson = {
23405
23674
  ...json,
23406
23675
  mcpServers: json.mcpServers ?? {}
23407
23676
  };
23677
+ const disabledNames = disabledNamesOf(await readRovodevConfigYaml({ outputRoot }));
23678
+ if (disabledNames.length > 0 && isMcpServers(newJson.mcpServers)) {
23679
+ const servers = newJson.mcpServers;
23680
+ for (const name of disabledNames) {
23681
+ if (!Object.hasOwn(servers, name)) continue;
23682
+ const server = servers[name];
23683
+ if (isPlainObject$1(server)) servers[name] = {
23684
+ ...server,
23685
+ disabled: true
23686
+ };
23687
+ }
23688
+ }
23408
23689
  return new RovodevMcp({
23409
23690
  outputRoot,
23410
23691
  relativeDirPath: paths.relativeDirPath,
@@ -23415,11 +23696,21 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23415
23696
  });
23416
23697
  }
23417
23698
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23418
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23419
23699
  const paths = this.getSettablePaths({ global });
23420
23700
  const json = parseRovodevMcpJson(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2), paths.relativeDirPath, paths.relativeFilePath);
23701
+ let canWriteDisableToggle = true;
23702
+ try {
23703
+ await readRovodevConfigYaml({ outputRoot });
23704
+ } catch {
23705
+ canWriteDisableToggle = false;
23706
+ }
23421
23707
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
23422
- const converted = toRovodevServer(name, server, logger);
23708
+ const record = server;
23709
+ if (record.disabled === true && !canWriteDisableToggle) {
23710
+ 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.`);
23711
+ return null;
23712
+ }
23713
+ const converted = toRovodevServer(name, record, logger);
23423
23714
  return converted === null ? null : [name, converted];
23424
23715
  }).filter((entry) => entry !== null));
23425
23716
  const rovodevConfig = {
@@ -23435,6 +23726,64 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23435
23726
  global
23436
23727
  });
23437
23728
  }
23729
+ /**
23730
+ * `mcp.disabledMcpServers` lives in the shared `config.yml` the permissions
23731
+ * feature also writes. Declared here so the write-order derivation sees this
23732
+ * feature as one of that file's writers — it is not a settable path, since
23733
+ * the servers themselves live in `mcp.json`.
23734
+ */
23735
+ static getExtraSharedWritePaths() {
23736
+ return [{
23737
+ relativeDirPath: ROVODEV_DIR,
23738
+ relativeFilePath: ROVODEV_CONFIG_FILE_NAME
23739
+ }];
23740
+ }
23741
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
23742
+ const servers = rulesyncMcp.forTarget({
23743
+ toolTarget: "rovodev",
23744
+ logger
23745
+ }).getMcpServers();
23746
+ const managedNames = Object.keys(servers).filter((name) => toRovodevServer(name, servers[name]) !== null);
23747
+ const disabledNames = managedNames.filter((name) => {
23748
+ const server = servers[name];
23749
+ return isRecord(server) && server.disabled === true;
23750
+ });
23751
+ const existingContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)) ?? "";
23752
+ let existingParsed;
23753
+ try {
23754
+ existingParsed = parseSharedConfig({
23755
+ format: "yaml",
23756
+ fileContent: existingContent,
23757
+ filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23758
+ });
23759
+ } catch (error) {
23760
+ logger?.warn(`Skipping the Rovo Dev mcp.disabledMcpServers update: ${formatError(error)}`);
23761
+ return [];
23762
+ }
23763
+ const existingMcp = isRecord(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
23764
+ const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
23765
+ const managedNameSet = new Set(managedNames);
23766
+ const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
23767
+ 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.`);
23768
+ const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
23769
+ if (mergedDisabled.length > 0) existingMcp.disabledMcpServers = mergedDisabled;
23770
+ else delete existingMcp.disabledMcpServers;
23771
+ if (mergedDisabled.length === 0 && existingContent.trim() === "") return [];
23772
+ const fileContent = applySharedConfigPatch({
23773
+ fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
23774
+ feature: "mcp",
23775
+ existingContent,
23776
+ patch: { mcp: Object.keys(existingMcp).length > 0 ? existingMcp : void 0 },
23777
+ filePath: join(ROVODEV_DIR, ROVODEV_CONFIG_FILE_NAME)
23778
+ });
23779
+ return [new RovodevMcpConfigYaml({
23780
+ outputRoot,
23781
+ relativeDirPath: ROVODEV_DIR,
23782
+ relativeFilePath: ROVODEV_CONFIG_FILE_NAME,
23783
+ fileContent,
23784
+ global
23785
+ })];
23786
+ }
23438
23787
  toRulesyncMcp() {
23439
23788
  const rawServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
23440
23789
  const mcpServers = Object.fromEntries(Object.entries(rawServers).filter(([, server]) => isPlainObject$1(server)).map(([name, server]) => [name, fromRovodevServer(server)]));
@@ -23583,7 +23932,6 @@ function transportOf(server) {
23583
23932
  case "http":
23584
23933
  case "streamable-http":
23585
23934
  case "ws": return "http";
23586
- default: break;
23587
23935
  }
23588
23936
  if (server.command !== void 0) return "stdio";
23589
23937
  if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
@@ -23611,6 +23959,20 @@ function deriveTransportAllowlist(servers) {
23611
23959
  return allowlist;
23612
23960
  }
23613
23961
  //#endregion
23962
+ //#region src/features/shared/vibe-config-scope.ts
23963
+ /**
23964
+ * Vibe selects exactly **one** persistence TOML and does not merge scopes: the
23965
+ * trusted project `.vibe/config.toml` when one is discovered, otherwise
23966
+ * `~/.vibe/config.toml` (single code path since v2.22.0's ConfigOrchestrator
23967
+ * migration). A `--global` run that writes the home file is therefore inert in
23968
+ * any project that has its own config — worth a heads-up, since the other Vibe
23969
+ * surfaces (rules, hooks, agents, skills) genuinely combine scopes.
23970
+ */
23971
+ async function warnIfGlobalVibeConfigIsShadowed(logger) {
23972
+ if (!await fileExists(join(process.cwd(), ".vibe", "config.toml"))) return;
23973
+ 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.");
23974
+ }
23975
+ //#endregion
23614
23976
  //#region src/features/mcp/vibe-mcp.ts
23615
23977
  const VIBE_MCP_SERVER_FIELDS = [
23616
23978
  "transport",
@@ -23679,8 +24041,9 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23679
24041
  global
23680
24042
  });
23681
24043
  }
23682
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
24044
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger, global = false }) {
23683
24045
  const paths = this.getSettablePaths({ global });
24046
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
23684
24047
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
23685
24048
  const existingContent = await readFileContentOrNull(filePath) ?? "";
23686
24049
  const existingServers = normalizeMcpServersArray(parseSharedConfig({
@@ -23689,7 +24052,10 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23689
24052
  filePath
23690
24053
  }).mcp_servers);
23691
24054
  const existingByName = new Map(existingServers.map((server) => [server.name, server]));
23692
- const mcpServers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
24055
+ const rulesyncServers = rulesyncMcp.getMcpServers();
24056
+ const managedNames = new Set(Object.keys(rulesyncServers));
24057
+ const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
24058
+ const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
23693
24059
  return new VibeMcp({
23694
24060
  outputRoot,
23695
24061
  relativeDirPath: paths.relativeDirPath,
@@ -23698,7 +24064,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23698
24064
  fileKey: sharedConfigFileKey(paths),
23699
24065
  feature: "mcp",
23700
24066
  existingContent,
23701
- patch: { mcp_servers: mcpServers },
24067
+ patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
23702
24068
  filePath
23703
24069
  }),
23704
24070
  validate,
@@ -24400,10 +24766,19 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
24400
24766
  supportsDisabledTools: false
24401
24767
  }
24402
24768
  }],
24769
+ ["zoocode", {
24770
+ class: RooMcp,
24771
+ meta: {
24772
+ supportsProject: true,
24773
+ supportsGlobal: false,
24774
+ supportsEnabledTools: false,
24775
+ supportsDisabledTools: false
24776
+ }
24777
+ }],
24403
24778
  ["rovodev", {
24404
24779
  class: RovodevMcp,
24405
24780
  meta: {
24406
- supportsProject: false,
24781
+ supportsProject: true,
24407
24782
  supportsGlobal: true,
24408
24783
  supportsEnabledTools: false,
24409
24784
  supportsDisabledTools: false
@@ -25269,9 +25644,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
25269
25644
  case "ask":
25270
25645
  ask.push(entry);
25271
25646
  break;
25272
- case "deny":
25273
- deny.push(entry);
25274
- break;
25647
+ case "deny": deny.push(entry);
25275
25648
  }
25276
25649
  }
25277
25650
  }
@@ -25482,9 +25855,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
25482
25855
  case "ask":
25483
25856
  ask.push(entry);
25484
25857
  break;
25485
- case "deny":
25486
- deny.push(entry);
25487
- break;
25858
+ case "deny": deny.push(entry);
25488
25859
  }
25489
25860
  }
25490
25861
  }
@@ -26216,9 +26587,7 @@ function convertRulesyncToClaudePermissions({ config, logger }) {
26216
26587
  case "ask":
26217
26588
  ask.push(entry);
26218
26589
  break;
26219
- case "deny":
26220
- deny.push(entry);
26221
- break;
26590
+ case "deny": deny.push(entry);
26222
26591
  }
26223
26592
  }
26224
26593
  }
@@ -27468,9 +27837,7 @@ function convertRulesyncToCursorPermissions(config, logger) {
27468
27837
  case "ask":
27469
27838
  logger?.warn(`Cursor CLI permissions do not support the "ask" action. Skipping ${category} rule: ${pattern}`);
27470
27839
  break;
27471
- case "deny":
27472
- deny.push(entry);
27473
- break;
27840
+ case "deny": deny.push(entry);
27474
27841
  }
27475
27842
  }
27476
27843
  }
@@ -27575,9 +27942,10 @@ function buildDevinPermissionEntry(scope, pattern) {
27575
27942
  * - Project scope: `.devin/config.json`
27576
27943
  * - Global scope: `~/.config/devin/config.json`
27577
27944
  *
27578
- * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
27579
- * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
27580
- * the file is never deleted; only the managed `permissions` key is rewritten.
27945
+ * In global mode the config file is shared with the hooks (`hooks`) feature
27946
+ * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
27947
+ * merge into the existing JSON and the file is never deleted; only the managed
27948
+ * `permissions` key is rewritten.
27581
27949
  *
27582
27950
  * @see https://docs.devin.ai/cli/reference/permissions
27583
27951
  */
@@ -27706,9 +28074,7 @@ function convertRulesyncToDevinPermissions(config) {
27706
28074
  case "ask":
27707
28075
  ask.push(entry);
27708
28076
  break;
27709
- case "deny":
27710
- deny.push(entry);
27711
- break;
28077
+ case "deny": deny.push(entry);
27712
28078
  }
27713
28079
  }
27714
28080
  }
@@ -27896,10 +28262,7 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
27896
28262
  case "allow":
27897
28263
  allow.push(pattern);
27898
28264
  break;
27899
- case "deny":
27900
- deny.push(pattern);
27901
- break;
27902
- case "ask": break;
28265
+ case "deny": deny.push(pattern);
27903
28266
  }
27904
28267
  }
27905
28268
  return {
@@ -28018,10 +28381,11 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28018
28381
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28019
28382
  }
28020
28383
  const config = isRecord(parsed) ? { ...parsed } : {};
28021
- config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
28384
+ const userPermission = convertRulesyncToGoosePermissionConfig({
28022
28385
  config: rulesyncPermissions.getJson(),
28023
28386
  logger
28024
28387
  });
28388
+ config[GOOSE_USER_KEY] = userPermission;
28025
28389
  return new GoosePermissions({
28026
28390
  outputRoot,
28027
28391
  relativeDirPath: paths.relativeDirPath,
@@ -30239,9 +30603,7 @@ function convertRulesyncToQwenPermissions(config) {
30239
30603
  case "ask":
30240
30604
  ask.push(entry);
30241
30605
  break;
30242
- case "deny":
30243
- deny.push(entry);
30244
- break;
30606
+ case "deny": deny.push(entry);
30245
30607
  }
30246
30608
  }
30247
30609
  }
@@ -30314,6 +30676,20 @@ function toCanonicalToolName$2(reasonixName) {
30314
30676
  return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
30315
30677
  }
30316
30678
  /**
30679
+ * Whether an entry uses the first-class `Tool=<literal>` exact-command form
30680
+ * (SPEC §3.7, v1.18.0): metacharacters in the literal are ordinary characters
30681
+ * and only the identical complete command matches. These entries have no
30682
+ * canonical tool→pattern→action equivalent — glob-style `Tool(...)` matches
30683
+ * differently by design — so they are preserved verbatim and round-trip
30684
+ * through the `reasonix` override's rawAllow/rawAsk/rawDeny arrays.
30685
+ */
30686
+ function isReasonixExactCommandEntry(entry) {
30687
+ const eqIndex = entry.indexOf("=");
30688
+ if (eqIndex <= 0) return false;
30689
+ const parenIndex = entry.indexOf("(");
30690
+ return parenIndex === -1 || eqIndex < parenIndex;
30691
+ }
30692
+ /**
30317
30693
  * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
30318
30694
  * If no parentheses, returns the tool name with "*" as the pattern.
30319
30695
  */
@@ -30402,10 +30778,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30402
30778
  const config = rulesyncPermissions.getJson();
30403
30779
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
30404
30780
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
30781
+ const override = config.reasonix;
30782
+ const rawAllow = toReasonixStringArray(override?.rawAllow);
30783
+ const rawAsk = toReasonixStringArray(override?.rawAsk);
30784
+ const rawDeny = toReasonixStringArray(override?.rawDeny);
30785
+ const rawOwnedEntries = /* @__PURE__ */ new Set([
30786
+ ...rawAllow,
30787
+ ...rawAsk,
30788
+ ...rawDeny
30789
+ ]);
30405
30790
  const existingPermissions = toReasonixTable(parsed.permissions);
30406
- const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30407
- const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30408
- const preservedDeny = toReasonixStringArray(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30791
+ const { allow: preservedAllow, ask: preservedAsk, deny: preservedDeny } = preserveExistingEntries({
30792
+ existingPermissions,
30793
+ managedToolNames,
30794
+ rawOwnedEntries,
30795
+ logger
30796
+ });
30409
30797
  if (logger && managedToolNames.has("Read")) {
30410
30798
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
30411
30799
  const { toolName } = parseReasonixPermissionEntry(entry);
@@ -30414,17 +30802,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30414
30802
  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.`);
30415
30803
  }
30416
30804
  const mergedPermissions = { ...existingPermissions };
30417
- const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
30418
- const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
30419
- const mergedDeny = uniq([...preservedDeny, ...deny].toSorted());
30420
- if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
30421
- else delete mergedPermissions.allow;
30422
- if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
30423
- else delete mergedPermissions.ask;
30424
- if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
30425
- else delete mergedPermissions.deny;
30805
+ setOrDeleteEntries(mergedPermissions, "allow", [
30806
+ ...preservedAllow,
30807
+ ...allow,
30808
+ ...rawAllow
30809
+ ]);
30810
+ setOrDeleteEntries(mergedPermissions, "ask", [
30811
+ ...preservedAsk,
30812
+ ...ask,
30813
+ ...rawAsk
30814
+ ]);
30815
+ setOrDeleteEntries(mergedPermissions, "deny", [
30816
+ ...preservedDeny,
30817
+ ...deny,
30818
+ ...rawDeny
30819
+ ]);
30426
30820
  const patch = { permissions: mergedPermissions };
30427
- const override = config.reasonix;
30428
30821
  if (override?.sandbox !== void 0) patch.sandbox = {
30429
30822
  ...asReasonixRecord(parsed.sandbox),
30430
30823
  ...asReasonixRecord(override.sandbox)
@@ -30455,16 +30848,29 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30455
30848
  }
30456
30849
  toRulesyncPermissions() {
30457
30850
  const permissions = toReasonixTable(this.toml.permissions);
30851
+ const splitExact = (entries) => {
30852
+ const exact = entries.filter((entry) => isReasonixExactCommandEntry(entry));
30853
+ return {
30854
+ translated: entries.filter((entry) => !isReasonixExactCommandEntry(entry)),
30855
+ exact
30856
+ };
30857
+ };
30858
+ const allowSplit = splitExact(toReasonixStringArray(permissions.allow));
30859
+ const askSplit = splitExact(toReasonixStringArray(permissions.ask));
30860
+ const denySplit = splitExact(toReasonixStringArray(permissions.deny));
30458
30861
  const config = convertReasonixToRulesyncPermissions({
30459
- allow: toReasonixStringArray(permissions.allow),
30460
- ask: toReasonixStringArray(permissions.ask),
30461
- deny: toReasonixStringArray(permissions.deny)
30862
+ allow: allowSplit.translated,
30863
+ ask: askSplit.translated,
30864
+ deny: denySplit.translated
30462
30865
  });
30463
30866
  const sandbox = asReasonixRecord(this.toml.sandbox);
30464
30867
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
30465
30868
  const reasonixOverride = {};
30466
30869
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
30467
30870
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
30871
+ if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
30872
+ if (askSplit.exact.length > 0) reasonixOverride.rawAsk = askSplit.exact;
30873
+ if (denySplit.exact.length > 0) reasonixOverride.rawDeny = denySplit.exact;
30468
30874
  const result = { ...config };
30469
30875
  if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
30470
30876
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -30494,6 +30900,41 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30494
30900
  }
30495
30901
  };
30496
30902
  /**
30903
+ * Preserve existing permission entries rulesync does not manage: entries for
30904
+ * tools NOT in the permissions config, plus exact `Tool=<literal>` entries not
30905
+ * owned by the override's raw arrays — Reasonix writes those itself as
30906
+ * remembered approvals, and dropping them would revoke grants the canonical
30907
+ * glob rules cannot re-express. Remove one by deleting it from `reasonix.toml`
30908
+ * (or by naming it in a raw array, which takes ownership). Preserved exact
30909
+ * entries are logged so the kept-alive grants stay visible.
30910
+ */
30911
+ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwnedEntries, logger }) {
30912
+ const isPreserved = (entry) => {
30913
+ if (isReasonixExactCommandEntry(entry)) return !rawOwnedEntries.has(entry);
30914
+ return !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName);
30915
+ };
30916
+ const allow = toReasonixStringArray(existingPermissions.allow).filter(isPreserved);
30917
+ const ask = toReasonixStringArray(existingPermissions.ask).filter(isPreserved);
30918
+ const deny = toReasonixStringArray(existingPermissions.deny).filter(isPreserved);
30919
+ const preservedExact = [
30920
+ ...allow,
30921
+ ...ask,
30922
+ ...deny
30923
+ ].filter((entry) => isReasonixExactCommandEntry(entry));
30924
+ if (preservedExact.length > 0) logger?.info(`Preserving ${preservedExact.length} exact Reasonix permission entries (remembered approvals): ${preservedExact.join(", ")}`);
30925
+ return {
30926
+ allow,
30927
+ ask,
30928
+ deny
30929
+ };
30930
+ }
30931
+ /** Sort, dedupe and set a permissions array key, deleting it when empty. */
30932
+ function setOrDeleteEntries(table, key, entries) {
30933
+ const merged = uniq(entries.toSorted());
30934
+ if (merged.length > 0) table[key] = merged;
30935
+ else delete table[key];
30936
+ }
30937
+ /**
30497
30938
  * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
30498
30939
  */
30499
30940
  function convertRulesyncToReasonixPermissions(config) {
@@ -30511,9 +30952,7 @@ function convertRulesyncToReasonixPermissions(config) {
30511
30952
  case "ask":
30512
30953
  ask.push(entry);
30513
30954
  break;
30514
- case "deny":
30515
- deny.push(entry);
30516
- break;
30955
+ case "deny": deny.push(entry);
30517
30956
  }
30518
30957
  }
30519
30958
  }
@@ -30543,7 +30982,6 @@ function convertReasonixToRulesyncPermissions(params) {
30543
30982
  }
30544
30983
  //#endregion
30545
30984
  //#region src/features/permissions/rovodev-permissions.ts
30546
- const ROVODEV_GLOBAL_ONLY_MESSAGE = "Rovodev permissions are global-only; use --global to sync ~/.rovodev/config.yml";
30547
30985
  const CATCH_ALL_PATTERN$1 = "*";
30548
30986
  const CATEGORY_TO_TOOL_KEYS = {
30549
30987
  read: [
@@ -30603,9 +31041,12 @@ const OWNED_TOOL_PERMISSION_KEYS = [
30603
31041
  /**
30604
31042
  * Permissions adapter for Rovo Dev CLI.
30605
31043
  *
30606
- * Rovo Dev reads tool permissions from the `toolPermissions` block of the global
30607
- * `~/.rovodev/config.yml`. This surface is **global only** there is no
30608
- * project-scoped Rovo Dev permissions file (mirrors the Rovodev MCP adapter).
31044
+ * Rovo Dev reads tool permissions from the `toolPermissions` block of
31045
+ * `config.yml` the global `~/.rovodev/config.yml`, and since the Bitbucket
31046
+ * Cloud Agentic Pipelines docs, also the repo-committed project
31047
+ * `.rovodev/config.yml` (referenced from `bitbucket-pipelines.yml` via
31048
+ * `config.path`, or the `--config-file` CLI flag). Both scopes use the
31049
+ * same relative path, resolved against the project root or home directory.
30609
31050
  *
30610
31051
  * Rovo Dev's three levels (`allow`/`ask`/`deny`) are an exact 1:1 with rulesync's
30611
31052
  * canonical action enum, so action values pass through verbatim.
@@ -30652,7 +31093,6 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30652
31093
  };
30653
31094
  }
30654
31095
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30655
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30656
31096
  const paths = RovodevPermissions.getSettablePaths({ global });
30657
31097
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
30658
31098
  return new RovodevPermissions({
@@ -30661,11 +31101,10 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30661
31101
  relativeFilePath: paths.relativeFilePath,
30662
31102
  fileContent,
30663
31103
  validate,
30664
- global: true
31104
+ global
30665
31105
  });
30666
31106
  }
30667
31107
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
30668
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30669
31108
  const paths = RovodevPermissions.getSettablePaths({ global });
30670
31109
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30671
31110
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -30688,14 +31127,20 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30688
31127
  filePath,
30689
31128
  logger
30690
31129
  });
30691
- if (resolvedToolPermissions !== void 0) config.toolPermissions = resolvedToolPermissions;
31130
+ const fileContent = resolvedToolPermissions !== void 0 ? applySharedConfigPatch({
31131
+ fileKey: ROVODEV_CONFIG_SHARED_FILE_KEY,
31132
+ feature: "permissions",
31133
+ existingContent,
31134
+ patch: { toolPermissions: resolvedToolPermissions },
31135
+ filePath
31136
+ }) : dump(config);
30692
31137
  return new RovodevPermissions({
30693
31138
  outputRoot,
30694
31139
  relativeDirPath: paths.relativeDirPath,
30695
31140
  relativeFilePath: paths.relativeFilePath,
30696
- fileContent: dump(config),
31141
+ fileContent,
30697
31142
  validate: true,
30698
- global: true
31143
+ global
30699
31144
  });
30700
31145
  }
30701
31146
  toRulesyncPermissions() {
@@ -31232,6 +31677,7 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
31232
31677
  write: "write_file",
31233
31678
  webfetch: "web_fetch",
31234
31679
  websearch: "web_search",
31680
+ grep: "grep",
31235
31681
  agent: "task"
31236
31682
  };
31237
31683
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
@@ -31241,6 +31687,7 @@ const VIBE_TO_CANONICAL_TOOL_NAMES = {
31241
31687
  write_file: "write",
31242
31688
  web_fetch: "webfetch",
31243
31689
  web_search: "websearch",
31690
+ grep: "grep",
31244
31691
  task: "agent"
31245
31692
  };
31246
31693
  var VibePermissions = class VibePermissions extends ToolPermissions {
@@ -31278,6 +31725,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31278
31725
  }
31279
31726
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
31280
31727
  const paths = this.getSettablePaths({ global });
31728
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
31281
31729
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
31282
31730
  const existingContent = await readFileContentOrNull(filePath) ?? "";
31283
31731
  const config = parseVibeConfig(existingContent);
@@ -31285,46 +31733,31 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31285
31733
  const tools = toVibeToolsRecord(config.tools);
31286
31734
  const enabledTools = new Set(toStringArray(config.enabled_tools));
31287
31735
  const disabledTools = new Set(toStringArray(config.disabled_tools));
31736
+ const removedEnabledTools = [];
31288
31737
  for (const category of Object.keys(permission)) {
31738
+ if (!hasVibeToolName(category)) continue;
31289
31739
  const vibeToolName = toVibeToolName(category);
31290
- enabledTools.delete(vibeToolName);
31740
+ if (enabledTools.delete(vibeToolName)) removedEnabledTools.push(vibeToolName);
31291
31741
  disabledTools.delete(vibeToolName);
31292
31742
  }
31293
31743
  for (const [category, rules] of Object.entries(permission)) {
31294
- const vibeToolName = toVibeToolName(category);
31295
- const existingTool = toVibeToolConfig(tools[vibeToolName]);
31296
- const nextTool = { ...existingTool };
31297
- const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31298
- const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31299
- for (const [pattern, action] of Object.entries(rules)) {
31300
- if (pattern === "*") {
31301
- applyWildcardPermission({
31302
- action,
31303
- toolConfig: nextTool
31304
- });
31305
- if (action === "deny") {
31306
- disabledTools.add(vibeToolName);
31307
- enabledTools.delete(vibeToolName);
31308
- } else if (action === "allow") {
31309
- enabledTools.add(vibeToolName);
31310
- disabledTools.delete(vibeToolName);
31311
- }
31312
- continue;
31313
- }
31314
- if (action === "ask") {
31315
- logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31316
- continue;
31317
- }
31318
- if (action === "allow") allow.add(pattern);
31319
- else deny.add(pattern);
31744
+ if (!hasVibeToolName(category)) {
31745
+ const ruleCount = Object.keys(rules).length;
31746
+ 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.`);
31747
+ continue;
31320
31748
  }
31321
- delete nextTool.allow;
31322
- delete nextTool.deny;
31323
- if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31324
- if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31325
- tools[vibeToolName] = nextTool;
31749
+ applyCategoryRules({
31750
+ category,
31751
+ rules,
31752
+ tools,
31753
+ enabledTools,
31754
+ disabledTools,
31755
+ logger
31756
+ });
31326
31757
  }
31327
- applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
31758
+ const vibeOverride = rulesyncPermissions.getJson().vibe;
31759
+ applyVibeSensitivePatterns(tools, vibeOverride, logger);
31760
+ 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.`);
31328
31761
  return new VibePermissions({
31329
31762
  outputRoot,
31330
31763
  relativeDirPath: paths.relativeDirPath,
@@ -31335,7 +31768,10 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31335
31768
  existingContent,
31336
31769
  patch: {
31337
31770
  tools,
31338
- enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
31771
+ enabled_tools: resolveEnabledToolsPatch({
31772
+ vibeOverride,
31773
+ enabledTools
31774
+ }),
31339
31775
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
31340
31776
  },
31341
31777
  filePath
@@ -31347,7 +31783,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31347
31783
  toRulesyncPermissions() {
31348
31784
  const permission = {};
31349
31785
  const vibeOverridePermission = {};
31350
- for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
31786
+ const enabledToolsList = toStringArray(this.toml.enabled_tools);
31351
31787
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
31352
31788
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
31353
31789
  const category = toCanonicalToolName$1(vibeToolName);
@@ -31360,9 +31796,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31360
31796
  if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
31361
31797
  }
31362
31798
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
31363
- const json = Object.keys(vibeOverridePermission).length > 0 ? {
31799
+ const vibeOverride = {};
31800
+ if (Object.keys(vibeOverridePermission).length > 0) vibeOverride.permission = vibeOverridePermission;
31801
+ if (enabledToolsList.length > 0) vibeOverride.enabled_tools = enabledToolsList;
31802
+ const json = Object.keys(vibeOverride).length > 0 ? {
31364
31803
  permission,
31365
- vibe: { permission: vibeOverridePermission }
31804
+ vibe: vibeOverride
31366
31805
  } : { permission };
31367
31806
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
31368
31807
  }
@@ -31397,8 +31836,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31397
31836
  * list for any category the override names: a present list is set, an empty
31398
31837
  * one clears it. Categories not named keep whatever the existing file had.
31399
31838
  */
31400
- function applyVibeSensitivePatterns(tools, vibeOverride) {
31839
+ function applyVibeSensitivePatterns(tools, vibeOverride, logger) {
31401
31840
  for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
31841
+ if (!hasVibeToolName(category)) {
31842
+ logger?.warn(`Vibe has no builtin tool for the '${category}' category; skipping its vibe.permission.${category}.sensitive_patterns override.`);
31843
+ continue;
31844
+ }
31402
31845
  const vibeToolName = toVibeToolName(category);
31403
31846
  const nextTool = toVibeToolConfig(tools[vibeToolName]);
31404
31847
  const patterns = toStringArray(toolOverride.sensitive_patterns);
@@ -31407,6 +31850,57 @@ function applyVibeSensitivePatterns(tools, vibeOverride) {
31407
31850
  tools[vibeToolName] = nextTool;
31408
31851
  }
31409
31852
  }
31853
+ /**
31854
+ * Translate one canonical category's rules into the tool's `[tools.<name>]`
31855
+ * table and the top-level `disabled_tools` filter, mutating `tools` and the
31856
+ * filter sets in place.
31857
+ */
31858
+ function applyCategoryRules({ category, rules, tools, enabledTools, disabledTools, logger }) {
31859
+ const vibeToolName = toVibeToolName(category);
31860
+ const existingTool = toVibeToolConfig(tools[vibeToolName]);
31861
+ const nextTool = { ...existingTool };
31862
+ const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31863
+ const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31864
+ for (const [pattern, action] of Object.entries(rules)) {
31865
+ if (pattern === "*") {
31866
+ applyWildcardPermission({
31867
+ action,
31868
+ toolConfig: nextTool
31869
+ });
31870
+ if (action === "deny") {
31871
+ disabledTools.add(vibeToolName);
31872
+ enabledTools.delete(vibeToolName);
31873
+ } else if (action === "allow") disabledTools.delete(vibeToolName);
31874
+ continue;
31875
+ }
31876
+ if (action === "ask") {
31877
+ logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31878
+ continue;
31879
+ }
31880
+ if (action === "allow") allow.add(pattern);
31881
+ else deny.add(pattern);
31882
+ }
31883
+ delete nextTool.allow;
31884
+ delete nextTool.deny;
31885
+ if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31886
+ if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31887
+ tools[vibeToolName] = nextTool;
31888
+ }
31889
+ /**
31890
+ * `enabled_tools` is an exclusive allowlist and therefore only ever
31891
+ * rulesync-authored explicitly, through `vibe.enabled_tools`. When the
31892
+ * override declares it (even empty), rulesync owns the whole list; when it
31893
+ * does not, the on-disk list survives minus the entries the caller's cleanup
31894
+ * loop removed for rulesync-configured tools (which also migrates away the
31895
+ * exclusive entries earlier rulesync versions wrote for allow rules).
31896
+ */
31897
+ function resolveEnabledToolsPatch({ vibeOverride, enabledTools }) {
31898
+ if (vibeOverride?.enabled_tools !== void 0) {
31899
+ const declared = [...new Set(toStringArray(vibeOverride.enabled_tools))];
31900
+ return declared.length > 0 ? declared.toSorted() : void 0;
31901
+ }
31902
+ return enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0;
31903
+ }
31410
31904
  function parseVibeConfig(fileContent) {
31411
31905
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
31412
31906
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -31415,6 +31909,17 @@ function parseVibeConfig(fileContent) {
31415
31909
  function toVibeToolName(category) {
31416
31910
  return CANONICAL_TO_VIBE_TOOL_NAMES[category] ?? category;
31417
31911
  }
31912
+ /**
31913
+ * Whether a canonical category has a Vibe builtin tool to land on. Categories
31914
+ * without one (e.g. `glob`, `notebookedit`) must not emit a
31915
+ * `[tools.<category>]` table: Vibe has no such tool, so a `deny` authored
31916
+ * there would look applied while being silently inert — the dangerous
31917
+ * direction. Unlike `agent` → `task` there is no correct name to rename to,
31918
+ * so the category is skipped with a warning (the grokcli adapter's pattern).
31919
+ */
31920
+ function hasVibeToolName(category) {
31921
+ return Object.hasOwn(CANONICAL_TO_VIBE_TOOL_NAMES, category);
31922
+ }
31418
31923
  function toCanonicalToolName$1(vibeToolName) {
31419
31924
  return VIBE_TO_CANONICAL_TOOL_NAMES[vibeToolName] ?? vibeToolName;
31420
31925
  }
@@ -31432,7 +31937,7 @@ function toStringArray(value) {
31432
31937
  function applyWildcardPermission({ action, toolConfig }) {
31433
31938
  if (action === "allow") toolConfig.permission = "always";
31434
31939
  else if (action === "ask") toolConfig.permission = "ask";
31435
- else if (toolConfig.permission !== void 0) delete toolConfig.permission;
31940
+ else toolConfig.permission = "never";
31436
31941
  }
31437
31942
  function fromVibePermission(value) {
31438
31943
  if (value === "always" || value === "allow") return "allow";
@@ -31451,11 +31956,29 @@ function ensurePermission(permission, category) {
31451
31956
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
31452
31957
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
31453
31958
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
31959
+ const EXECUTION_PROFILES_KEY = "execution_profiles";
31960
+ const DEFAULT_PROFILE_KEY = "default";
31961
+ const PROFILE_ALLOWLIST_KEY = "command_allowlist";
31962
+ const PROFILE_DENYLIST_KEY = "command_denylist";
31454
31963
  const WARP_OVERRIDE_KEYS = [
31455
31964
  "agent_mode_coding_permissions",
31456
31965
  "agent_mode_coding_file_read_allowlist",
31457
31966
  "agent_mode_execute_readonly_commands"
31458
31967
  ];
31968
+ const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
31969
+ const WARP_EXECUTION_PROFILE_KEYS = [
31970
+ "read_files",
31971
+ "apply_code_diffs",
31972
+ "execute_commands",
31973
+ "mcp_permissions",
31974
+ "write_to_pty",
31975
+ "ask_user_question",
31976
+ "run_agents",
31977
+ "computer_use",
31978
+ "directory_allowlist",
31979
+ "mcp_allowlist",
31980
+ "mcp_denylist"
31981
+ ];
31459
31982
  /**
31460
31983
  * Warp's `settings.toml` lives in a different directory per platform (Stable
31461
31984
  * channel). The home directory is resolved by the processor through
@@ -31478,11 +32001,21 @@ function warpSettingsDir() {
31478
32001
  /**
31479
32002
  * Permissions adapter for Warp.
31480
32003
  *
31481
- * Warp gates **shell command** execution through two regex arrays under the
31482
- * `[agents.profiles]` table of the global user `settings.toml`:
31483
- * - `agent_mode_command_execution_allowlist` commands that auto-execute.
31484
- * - `agent_mode_command_execution_denylist` — commands that always require
31485
- * permission (the denylist wins over the allowlist).
32004
+ * Warp gates **shell command** execution through two regex arrays in the
32005
+ * global user `settings.toml`. Since file-backed execution profiles went
32006
+ * Stable (2026-07-28) the authoritative surface is the `default` record of the
32007
+ * `[agents.execution_profiles.<id>]` collection:
32008
+ * - `command_allowlist` commands that auto-execute.
32009
+ * - `command_denylist` — commands that always require permission (the denylist
32010
+ * wins over the allowlist).
32011
+ *
32012
+ * The legacy `[agents.profiles]` keys
32013
+ * (`agent_mode_command_execution_allowlist` / `denylist`) are consumed only
32014
+ * once by Warp's one-shot migration and are ignored afterwards. Both surfaces
32015
+ * are written: the legacy block keeps un-migrated installs and old clients
32016
+ * working, and the `default` execution profile (merged in place, only when the
32017
+ * collection already exists) keeps migrated installs enforcing the lists.
32018
+ * Import prefers the execution profile and falls back to the legacy keys.
31486
32019
  *
31487
32020
  * This surface is **global only** — there is no project-scoped Warp permissions
31488
32021
  * file. rulesync's canonical `permission.bash` patterns map directly (`allow` →
@@ -31500,7 +32033,14 @@ function warpSettingsDir() {
31500
32033
  * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
31501
32034
  * on **import** they are lifted from `settings.toml` into the override, and on
31502
32035
  * **export** they are merged back into `[agents.profiles]` (the override wins).
31503
- * MCP allow/deny is a separate surface not modeled here.
32036
+ *
32037
+ * On migrated installs those legacy keys are inert; their execution-profile
32038
+ * counterparts (`read_files` / `apply_code_diffs` / `execute_commands`
32039
+ * action permissions, `directory_allowlist`, `mcp_allowlist` /
32040
+ * `mcp_denylist`) are authored through the nested `warp.execution_profile`
32041
+ * override, merged into the `default` record of
32042
+ * `[agents.execution_profiles.<id>]` under the same collection-exists guard as
32043
+ * the command lists, and lifted back from the `default` profile on import.
31504
32044
  *
31505
32045
  * The `settings.toml` file holds all of Warp's settings, so the
31506
32046
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -31553,7 +32093,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31553
32093
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
31554
32094
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
31555
32095
  const override = config.warp;
31556
- if (isRecord(override)) Object.assign(profiles, override);
32096
+ const executionProfileOverride = isRecord(override) && isRecord(override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
32097
+ if (isRecord(override)) {
32098
+ const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
32099
+ Object.assign(profiles, legacyOverride);
32100
+ }
31557
32101
  const mergedAllow = uniq(allow.toSorted());
31558
32102
  const mergedDeny = uniq(deny.toSorted());
31559
32103
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -31561,6 +32105,13 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31561
32105
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31562
32106
  else delete profiles[DENYLIST_KEY];
31563
32107
  agents.profiles = profiles;
32108
+ mergeIntoDefaultExecutionProfile({
32109
+ agents,
32110
+ mergedAllow,
32111
+ mergedDeny,
32112
+ executionProfileOverride,
32113
+ logger
32114
+ });
31564
32115
  settings.agents = agents;
31565
32116
  return new WarpPermissions({
31566
32117
  outputRoot,
@@ -31580,12 +32131,19 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31580
32131
  }
31581
32132
  const agents = isRecord(settings.agents) ? settings.agents : {};
31582
32133
  const profiles = isRecord(agents.profiles) ? agents.profiles : {};
32134
+ const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
32135
+ const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
31583
32136
  const config = convertWarpToRulesyncPermissions({
31584
- allow: isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
31585
- deny: isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
32137
+ allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
32138
+ deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
31586
32139
  });
31587
32140
  const warpOverride = {};
31588
32141
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
32142
+ if (defaultProfile) {
32143
+ const executionProfileOverride = {};
32144
+ for (const key of WARP_EXECUTION_PROFILE_KEYS) if (defaultProfile[key] !== void 0) executionProfileOverride[key] = defaultProfile[key];
32145
+ if (Object.keys(executionProfileOverride).length > 0) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
32146
+ }
31589
32147
  const result = { ...config };
31590
32148
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
31591
32149
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -31608,6 +32166,36 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31608
32166
  }
31609
32167
  };
31610
32168
  /**
32169
+ * On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
32170
+ * enforcement reads the active `[agents.execution_profiles.<id>]` record.
32171
+ * Merge the command lists (and the `warp.execution_profile` override's
32172
+ * autonomy keys) into the `default` profile in place so every other profile
32173
+ * key and profile ID survives. When the collection does not exist yet the
32174
+ * install is un-migrated — the legacy keys are still live there, and creating
32175
+ * the collection ourselves would mark Warp's one-shot migration complete early
32176
+ * and strand the user's other legacy settings (file-read allowlist, preferred
32177
+ * model), so it is deliberately left unset and the override is skipped with a
32178
+ * warning instead.
32179
+ */
32180
+ function mergeIntoDefaultExecutionProfile({ agents, mergedAllow, mergedDeny, executionProfileOverride, logger }) {
32181
+ const hasOverrideKeys = executionProfileOverride !== void 0 && Object.keys(executionProfileOverride).length > 0;
32182
+ if (!isRecord(agents[EXECUTION_PROFILES_KEY])) {
32183
+ 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.");
32184
+ return;
32185
+ }
32186
+ const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
32187
+ const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
32188
+ if (executionProfileOverride) Object.assign(defaultProfile, executionProfileOverride);
32189
+ if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
32190
+ else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
32191
+ if (mergedDeny.length > 0) defaultProfile[PROFILE_DENYLIST_KEY] = mergedDeny;
32192
+ else delete defaultProfile[PROFILE_DENYLIST_KEY];
32193
+ executionProfiles[DEFAULT_PROFILE_KEY] = defaultProfile;
32194
+ agents[EXECUTION_PROFILES_KEY] = executionProfiles;
32195
+ const otherProfileIds = Object.keys(executionProfiles).filter((id) => id !== DEFAULT_PROFILE_KEY);
32196
+ 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.`);
32197
+ }
32198
+ /**
31611
32199
  * Convert rulesync permissions config to Warp command allow/deny regex lists.
31612
32200
  * Only the `bash` category maps; `ask` rules and non-`bash` categories are
31613
32201
  * dropped (the latter with a warning when they carry `deny` rules).
@@ -31624,10 +32212,7 @@ function convertRulesyncToWarpPermissions({ config, logger }) {
31624
32212
  case "allow":
31625
32213
  allow.push(pattern);
31626
32214
  break;
31627
- case "deny":
31628
- deny.push(pattern);
31629
- break;
31630
- case "ask": break;
32215
+ case "deny": deny.push(pattern);
31631
32216
  }
31632
32217
  }
31633
32218
  return {
@@ -32082,7 +32667,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
32082
32667
  ["rovodev", {
32083
32668
  class: RovodevPermissions,
32084
32669
  meta: {
32085
- supportsProject: false,
32670
+ supportsProject: true,
32086
32671
  supportsGlobal: true,
32087
32672
  supportsImport: true
32088
32673
  }
@@ -33279,6 +33864,38 @@ var AugmentcodeSkill = class AugmentcodeSkill extends ToolSkill {
33279
33864
  }
33280
33865
  };
33281
33866
  //#endregion
33867
+ //#region src/features/rules/nested-scan-exclusions.ts
33868
+ /**
33869
+ * Shared exclusion lists for the nested rule-file scans (`AGENTS.md`,
33870
+ * `REASONIX.md`), which walk the whole project tree on import rather than a
33871
+ * rulesync-owned directory.
33872
+ */
33873
+ /**
33874
+ * Dependency trees never scanned for nested rule files, at any depth. A rule
33875
+ * file there describes somebody else's project, and neither name is ever a
33876
+ * package name. Hidden directories are excluded separately, because a rule
33877
+ * file inside one is another tool's generated output (rulesync writes several
33878
+ * itself).
33879
+ */
33880
+ const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
33881
+ /**
33882
+ * Build, vendoring and scratch directories, excluded at the **project root
33883
+ * only**. A top-level `build/` is a build directory; `packages/build/` is a
33884
+ * package, and dropping it silently would lose a real subproject.
33885
+ */
33886
+ const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
33887
+ "vendor",
33888
+ "third_party",
33889
+ "dist",
33890
+ "build",
33891
+ "out",
33892
+ "target",
33893
+ "coverage",
33894
+ "tmp",
33895
+ "temp",
33896
+ "venv"
33897
+ ];
33898
+ //#endregion
33282
33899
  //#region src/features/skills/claudecode-skill.ts
33283
33900
  const ClaudecodeSkillFrontmatterSchema = z.looseObject({
33284
33901
  name: z.string(),
@@ -33364,6 +33981,41 @@ var ClaudecodeSkill = class extends ToolSkill {
33364
33981
  alternativeSkillRoots: [CLAUDECODE_SCHEDULED_TASKS_DIR_PATH]
33365
33982
  };
33366
33983
  }
33984
+ /**
33985
+ * Claude Code v2.1.178+ also loads skills from **nested** `.claude/skills/`
33986
+ * directories below the working directory (a skill in
33987
+ * `apps/web/.claude/skills/` becomes available when working on files
33988
+ * there). Discover those directories so `rulesync import` sees them —
33989
+ * import-only and lenient, like every configured root; the project-root
33990
+ * `.claude/skills/` stays the sole generation target. The scan reuses the
33991
+ * nested-rule-file exclusions (dependency trees at any depth, build/vendor
33992
+ * dirs at the root, hidden dirs other than the `.claude` being matched) and
33993
+ * never runs in global mode. On a name clash with a root skill, the root
33994
+ * one wins the import (Claude Code itself keeps both under a
33995
+ * directory-qualified name, which rulesync's flat skill namespace cannot
33996
+ * express).
33997
+ *
33998
+ * @see https://code.claude.com/docs/en/skills
33999
+ */
34000
+ static async getConfiguredImportRoots({ outputRoot, global = false }) {
34001
+ if (global) return [];
34002
+ const root = toPosixPath(outputRoot);
34003
+ return filterOutPathsInGitIgnoredDirectories({
34004
+ rootDir: outputRoot,
34005
+ filePaths: await findFilesByGlobs([`${root}/*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`], {
34006
+ type: "dir",
34007
+ followSymbolicLinks: false,
34008
+ ignore: [
34009
+ `${root}/**/.*/**/${toPosixPath(CLAUDECODE_SKILLS_DIR_PATH)}`,
34010
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
34011
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
34012
+ ]
34013
+ })
34014
+ }).toSorted().map((dirPath) => ({
34015
+ outputRoot,
34016
+ relativeDirPath: relative(outputRoot, dirPath)
34017
+ }));
34018
+ }
33367
34019
  getFrontmatter() {
33368
34020
  return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
33369
34021
  }
@@ -36762,7 +37414,8 @@ var TaktSkill = class TaktSkill extends ToolSkill {
36762
37414
  getDirPath() {
36763
37415
  const fullPath = join(this.outputRoot, this.relativeDirPath);
36764
37416
  const resolvedFull = resolve(fullPath);
36765
- const rel = relative(resolve(this.outputRoot), resolvedFull);
37417
+ const resolvedBase = resolve(this.outputRoot);
37418
+ const rel = relative(resolvedBase, resolvedFull);
36766
37419
  if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}"`);
36767
37420
  return fullPath;
36768
37421
  }
@@ -37312,6 +37965,23 @@ var ZedSkill = class ZedSkill extends ToolSkill {
37312
37965
  }
37313
37966
  };
37314
37967
  //#endregion
37968
+ //#region src/features/skills/zoocode-skill.ts
37969
+ /**
37970
+ * Skill generator for **Zoo Code** (the community continuation of Roo Code).
37971
+ * Zoo Code keeps Roo's skills layout (`.roo/skills/` project,
37972
+ * `~/.roo/skills/` global) and the `roo:` frontmatter section (`modeSlugs`),
37973
+ * so this target reuses {@link RooSkill} verbatim and only narrows the
37974
+ * targeting.
37975
+ *
37976
+ * @see https://docs.zoocode.dev/features/skills
37977
+ */
37978
+ var ZoocodeSkill = class extends RooSkill {
37979
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
37980
+ const targets = rulesyncSkill.getFrontmatter().targets;
37981
+ return targets.includes("*") || targets.includes("zoocode");
37982
+ }
37983
+ };
37984
+ //#endregion
37315
37985
  //#region src/features/skills/skills-processor.ts
37316
37986
  const SkillsProcessorToolTargetSchema = z.enum(skillsProcessorToolTargetTuple);
37317
37987
  /**
@@ -37332,7 +38002,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37332
38002
  meta: {
37333
38003
  supportsProject: true,
37334
38004
  supportsSimulated: false,
37335
- supportsGlobal: true
38005
+ supportsGlobal: true,
38006
+ lenientImport: true
37336
38007
  }
37337
38008
  }],
37338
38009
  ["aiassistant", {
@@ -37583,6 +38254,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37583
38254
  supportsGlobal: true
37584
38255
  }
37585
38256
  }],
38257
+ ["zoocode", {
38258
+ class: ZoocodeSkill,
38259
+ meta: {
38260
+ supportsProject: true,
38261
+ supportsSimulated: false,
38262
+ supportsGlobal: true
38263
+ }
38264
+ }],
37586
38265
  ["rovodev", {
37587
38266
  class: RovodevSkill,
37588
38267
  meta: {
@@ -37745,7 +38424,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37745
38424
  for (const root of roots) {
37746
38425
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
37747
38426
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37748
- const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
38427
+ const isLenientRoot = configuredRootPaths.has(relativeDirPath) || factory.meta.lenientImport === true || isAgentSkillsInteropRoot(relativeDirPath);
37749
38428
  const skillsDirPath = join(rootOutputRoot, relativeDirPath);
37750
38429
  if (!await directoryExists(skillsDirPath)) continue;
37751
38430
  const dirPaths = await findFilesByGlobs(join(skillsDirPath, "*"), { type: "dir" });
@@ -37769,7 +38448,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37769
38448
  global: this.global
37770
38449
  });
37771
38450
  } catch (error) {
37772
- if (!isConfiguredRoot) throw error;
38451
+ if (!isLenientRoot) throw error;
37773
38452
  this.logger.warn(`Skipping ${join(relativeDirPath, dirName)}: ${formatError(error)}`);
37774
38453
  return null;
37775
38454
  }
@@ -37784,12 +38463,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37784
38463
  const fromFlatFile = factory.class.fromFlatFile;
37785
38464
  const directoryStems = new Set(ownedDirNames);
37786
38465
  const flatFilePaths = (await findFilesByGlobs(join(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has(basename(filePath, ".md")));
37787
- const flatSkills = await Promise.all(flatFilePaths.map((filePath) => fromFlatFile({
37788
- outputRoot: rootOutputRoot,
37789
- relativeDirPath,
37790
- relativeFilePath: basename(filePath),
37791
- global: this.global
37792
- })));
38466
+ const flatSkills = (await Promise.all(flatFilePaths.map(async (filePath) => {
38467
+ try {
38468
+ return await fromFlatFile({
38469
+ outputRoot: rootOutputRoot,
38470
+ relativeDirPath,
38471
+ relativeFilePath: basename(filePath),
38472
+ global: this.global
38473
+ });
38474
+ } catch (error) {
38475
+ if (!isLenientRoot) throw error;
38476
+ this.logger.warn(`Skipping ${join(relativeDirPath, basename(filePath))}: ${formatError(error)}`);
38477
+ return null;
38478
+ }
38479
+ }))).filter((skill) => skill !== null);
37793
38480
  for (const skill of flatSkills) {
37794
38481
  const skillName = skill.getImportIdentity();
37795
38482
  if (seenSkillNames.has(skillName)) continue;
@@ -37862,14 +38549,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37862
38549
  */
37863
38550
  static getSimulatedConventionSection({ skillList }) {
37864
38551
  if (!skillList || skillList.length === 0) return "";
38552
+ const skillListWithAtPrefix = skillList.map((skill) => ({
38553
+ ...skill,
38554
+ path: `@${skill.path}`
38555
+ }));
37865
38556
  return `## Simulated Skills
37866
38557
 
37867
38558
  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.
37868
38559
 
37869
- ${encode({ skillList: skillList.map((skill) => ({
37870
- ...skill,
37871
- path: `@${skill.path}`
37872
- })) })}`;
38560
+ ${encode({ skillList: skillListWithAtPrefix })}`;
37873
38561
  }
37874
38562
  /**
37875
38563
  * Return the tool targets that this processor supports in global mode
@@ -40473,7 +41161,8 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
40473
41161
  return getHermesagentSharedConfigWritePaths();
40474
41162
  }
40475
41163
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
40476
- return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
41164
+ const slug = subagentSlug(rulesyncSubagent.getRelativePathFromCwd());
41165
+ return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${slug}.json`)];
40477
41166
  }
40478
41167
  toRulesyncSubagent() {
40479
41168
  const slug = basename(this.getRelativeFilePath(), ".json");
@@ -41708,7 +42397,7 @@ function stringifyRooModes(modes) {
41708
42397
  *
41709
42398
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
41710
42399
  */
41711
- var RooSubagent = class RooSubagent extends ToolSubagent {
42400
+ var RooSubagent = class extends ToolSubagent {
41712
42401
  modes;
41713
42402
  constructor({ modes, ...rest }) {
41714
42403
  if (rest.validate !== false) {
@@ -41764,7 +42453,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41764
42453
  const mode = this.toRooMode(rulesyncSubagent);
41765
42454
  bySlug.set(mode.slug, mode);
41766
42455
  }
41767
- return new RooSubagent({
42456
+ return new this({
41768
42457
  outputRoot,
41769
42458
  relativeDirPath: this.getSettablePaths().relativeDirPath,
41770
42459
  relativeFilePath: ROO_MODES_FILE_NAME,
@@ -41843,7 +42532,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41843
42532
  }
41844
42533
  const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
41845
42534
  if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
41846
- return new RooSubagent({
42535
+ return new this({
41847
42536
  outputRoot,
41848
42537
  relativeDirPath: paths.relativeDirPath,
41849
42538
  relativeFilePath: basename(relativeFilePath),
@@ -41852,7 +42541,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41852
42541
  });
41853
42542
  }
41854
42543
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
41855
- return new RooSubagent({
42544
+ return new this({
41856
42545
  outputRoot,
41857
42546
  relativeDirPath,
41858
42547
  relativeFilePath,
@@ -42098,6 +42787,60 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
42098
42787
  }
42099
42788
  };
42100
42789
  //#endregion
42790
+ //#region src/features/subagents/zoocode-subagent.ts
42791
+ /**
42792
+ * Subagent (custom-mode) generator for **Zoo Code**, the community
42793
+ * continuation of Roo Code. Zoo Code keeps the aggregated `.roomodes` file and
42794
+ * Roo's mode schema, and the shared mode fields keep riding the `roo:`
42795
+ * frontmatter section — one rulesync source must not produce two different
42796
+ * spellings of the same field.
42797
+ *
42798
+ * The post-fork divergence is carried by the `zoocode:` section:
42799
+ * `allowedMcpServers` (Zoo Code v3.60.0+) — a per-mode MCP server allowlist.
42800
+ * "When omitted, all servers are available. When set, only the listed servers
42801
+ * are injected." On import the key is lifted back into `zoocode:` so it
42802
+ * round-trips (and never leaks into the `roo:` section, where the archived
42803
+ * Roo Code would not understand it).
42804
+ *
42805
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code
42806
+ * @see https://docs.zoocode.dev/features/custom-modes
42807
+ */
42808
+ var ZoocodeSubagent = class extends RooSubagent {
42809
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
42810
+ return this.isTargetedByRulesyncSubagentDefault({
42811
+ rulesyncSubagent,
42812
+ toolTarget: "zoocode"
42813
+ });
42814
+ }
42815
+ static toRooMode(rulesyncSubagent) {
42816
+ const mode = super.toRooMode(rulesyncSubagent);
42817
+ const frontmatter = rulesyncSubagent.getFrontmatter();
42818
+ const zoocodeSection = isRecord(frontmatter.zoocode) ? frontmatter.zoocode : {};
42819
+ if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
42820
+ return mode;
42821
+ }
42822
+ toRulesyncSubagents() {
42823
+ return super.toRulesyncSubagents().map((subagent) => {
42824
+ const frontmatter = subagent.getFrontmatter();
42825
+ const { allowedMcpServers, ...restRooSection } = isRecord(frontmatter.roo) ? { ...frontmatter.roo } : {};
42826
+ const rebuilt = {
42827
+ ...frontmatter,
42828
+ targets: ["zoocode"],
42829
+ roo: restRooSection
42830
+ };
42831
+ if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
42832
+ return new RulesyncSubagent({
42833
+ outputRoot: ".",
42834
+ frontmatter: rebuilt,
42835
+ body: subagent.getBody(),
42836
+ relativeDirPath: subagent.getRelativeDirPath(),
42837
+ relativeFilePath: subagent.getRelativeFilePath(),
42838
+ validate: true
42839
+ });
42840
+ });
42841
+ }
42842
+ };
42843
+ //#endregion
42101
42844
  //#region src/features/subagents/subagents-processor.ts
42102
42845
  const SubagentsProcessorToolTargetSchema = z.enum(subagentsProcessorToolTargetTuple);
42103
42846
  /**
@@ -42337,6 +43080,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
42337
43080
  filePattern: ".roomodes"
42338
43081
  }
42339
43082
  }],
43083
+ ["zoocode", {
43084
+ class: ZoocodeSubagent,
43085
+ meta: {
43086
+ supportsSimulated: false,
43087
+ supportsGlobal: false,
43088
+ filePattern: ".roomodes"
43089
+ }
43090
+ }],
42340
43091
  ["rovodev", {
42341
43092
  class: RovodevSubagent,
42342
43093
  meta: {
@@ -42597,11 +43348,13 @@ For example, if the user instructs \`Call planner subagent to plan the refactori
42597
43348
  //#region src/features/rules/tool-rule.ts
42598
43349
  var ToolRule = class extends ToolFile {
42599
43350
  root;
43351
+ localRoot;
42600
43352
  description;
42601
43353
  globs;
42602
- constructor({ root = false, description, globs, ...rest }) {
43354
+ constructor({ root = false, localRoot = false, description, globs, ...rest }) {
42603
43355
  super(rest);
42604
43356
  this.root = root;
43357
+ this.localRoot = localRoot;
42605
43358
  this.description = description;
42606
43359
  this.globs = globs;
42607
43360
  }
@@ -42684,6 +43437,38 @@ var ToolRule = class extends ToolFile {
42684
43437
  isRoot() {
42685
43438
  return this.root;
42686
43439
  }
43440
+ isLocalRoot() {
43441
+ return this.localRoot;
43442
+ }
43443
+ /**
43444
+ * Convert a tool's separate personal local-root file (see
43445
+ * {@link ToolRuleParams.localRoot}) back to a canonical `localRoot: true`
43446
+ * rulesync rule. The rulesync file keeps the tool-side basename
43447
+ * (`CLAUDE.local.md`, `AGENTS.local.md`, ...), which the derived `.gitignore`
43448
+ * already covers via `.rulesync/rules/*.local.md`, so personal content stays
43449
+ * untracked after import.
43450
+ *
43451
+ * `targets` is scoped to the importing tool rather than `"*"`. A wildcard
43452
+ * would spread the personal content to every tool on the next generate —
43453
+ * including being appended into the committed root file of tools without a
43454
+ * separate local file — and a multi-target import would produce several
43455
+ * wildcard localRoot rules, which the per-target validation rejects.
43456
+ */
43457
+ toLocalRootRulesyncRule({ targets }) {
43458
+ return new RulesyncRule({
43459
+ outputRoot: this.getOutputRoot(),
43460
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
43461
+ relativeFilePath: this.getRelativeFilePath(),
43462
+ frontmatter: {
43463
+ root: false,
43464
+ localRoot: true,
43465
+ targets,
43466
+ globs: []
43467
+ },
43468
+ body: this.getFileContent(),
43469
+ validate: true
43470
+ });
43471
+ }
42687
43472
  /**
42688
43473
  * Whether this rule must be left out of the root rule's reference/MCP
42689
43474
  * instruction listings even though it is a non-root survivor. Used by files
@@ -42716,31 +43501,6 @@ function buildToolPath(toolDir, subDir, excludeToolDir) {
42716
43501
  }
42717
43502
  //#endregion
42718
43503
  //#region src/features/rules/agentsmd-rule.ts
42719
- /**
42720
- * Dependency trees never scanned for nested `AGENTS.md` files, at any depth. An
42721
- * `AGENTS.md` there describes somebody else's project, and neither name is ever
42722
- * a package name. Hidden directories are excluded separately, because an
42723
- * `AGENTS.md` inside one is another tool's generated output (rulesync writes
42724
- * several itself).
42725
- */
42726
- const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
42727
- /**
42728
- * Build, vendoring and scratch directories, excluded at the **project root
42729
- * only**. A top-level `build/` is a build directory; `packages/build/` is a
42730
- * package, and dropping it silently would lose a real subproject.
42731
- */
42732
- const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
42733
- "vendor",
42734
- "third_party",
42735
- "dist",
42736
- "build",
42737
- "out",
42738
- "target",
42739
- "coverage",
42740
- "tmp",
42741
- "temp",
42742
- "venv"
42743
- ];
42744
43504
  var AgentsMdRule = class AgentsMdRule extends ToolRule {
42745
43505
  constructor({ fileContent, root, ...rest }) {
42746
43506
  super({
@@ -42797,7 +43557,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
42797
43557
  const normalizedDirPath = relativeDirPath === void 0 ? "." : toPosixPath(relativeDirPath);
42798
43558
  const isNested = relativeFilePath === "AGENTS.md" && normalizedDirPath !== "." && normalizedDirPath !== "" && !normalizedDirPath.startsWith(".");
42799
43559
  const isRoot = !isNested && relativeFilePath === "AGENTS.md" && (normalizedDirPath === "." || normalizedDirPath === "");
42800
- const fileContent = await readFileContent(join(outputRoot, isNested ? join(normalizedDirPath, relativeFilePath) : isRoot ? AGENTSMD_RULE_FILE_NAME : join(AGENTSMD_MEMORIES_DIR_PATH, relativeFilePath)));
43560
+ const relativePath = isNested ? join(normalizedDirPath, relativeFilePath) : isRoot ? AGENTSMD_RULE_FILE_NAME : join(AGENTSMD_MEMORIES_DIR_PATH, relativeFilePath);
43561
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
42801
43562
  return new AgentsMdRule({
42802
43563
  outputRoot,
42803
43564
  relativeDirPath: isNested ? join(normalizedDirPath) : isRoot ? this.getSettablePaths().root.relativeDirPath : this.getSettablePaths().nonRoot.relativeDirPath,
@@ -43104,7 +43865,8 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
43104
43865
  });
43105
43866
  }
43106
43867
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43107
- const fileContent = await readFileContent(join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath)));
43868
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
43869
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
43108
43870
  return new AntigravityCliRule({
43109
43871
  outputRoot,
43110
43872
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43565,7 +44327,8 @@ var AugmentcodeLegacyRule = class AugmentcodeLegacyRule extends ToolRule {
43565
44327
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
43566
44328
  const settablePaths = this.getSettablePaths();
43567
44329
  const isRoot = relativeFilePath === settablePaths.root.relativeFilePath;
43568
- const fileContent = await readFileContent(join(outputRoot, isRoot ? settablePaths.root.relativeFilePath : join(settablePaths.nonRoot.relativeDirPath, relativeFilePath)));
44330
+ const relativePath = isRoot ? settablePaths.root.relativeFilePath : join(settablePaths.nonRoot.relativeDirPath, relativeFilePath);
44331
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
43569
44332
  return new AugmentcodeLegacyRule({
43570
44333
  outputRoot,
43571
44334
  relativeDirPath: isRoot ? settablePaths.root.relativeDirPath : settablePaths.nonRoot.relativeDirPath,
@@ -43789,7 +44552,8 @@ var ClaudecodeLegacyRule = class ClaudecodeLegacyRule extends ToolRule {
43789
44552
  });
43790
44553
  }
43791
44554
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43792
- const fileContent = await readFileContent(join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath)));
44555
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
44556
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
43793
44557
  return new ClaudecodeLegacyRule({
43794
44558
  outputRoot,
43795
44559
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43910,7 +44674,8 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
43910
44674
  });
43911
44675
  }
43912
44676
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43913
- const filePath = join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath));
44677
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
44678
+ const filePath = join(outputRoot, relativePath);
43914
44679
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
43915
44680
  const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
43916
44681
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44297,7 +45062,8 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
44297
45062
  }
44298
45063
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44299
45064
  const { root } = this.getSettablePaths({ global });
44300
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
45065
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
45066
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
44301
45067
  return new CodexcliRule({
44302
45068
  outputRoot,
44303
45069
  relativeDirPath: root.relativeDirPath,
@@ -44466,7 +45232,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44466
45232
  }) : relativeFilePath === paths.root.relativeFilePath;
44467
45233
  const resolvedRelativeDirPath = relativeDirPath ?? (isRoot ? paths.root.relativeDirPath : paths.nonRoot?.relativeDirPath ?? paths.root.relativeDirPath);
44468
45234
  if (isRoot) {
44469
- const fileContent = await readFileContent(join(outputRoot, join(paths.root.relativeDirPath, paths.root.relativeFilePath)));
45235
+ const relativePath = join(paths.root.relativeDirPath, paths.root.relativeFilePath);
45236
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
44470
45237
  return new CopilotRule({
44471
45238
  outputRoot,
44472
45239
  relativeDirPath: paths.root.relativeDirPath,
@@ -44478,7 +45245,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44478
45245
  });
44479
45246
  }
44480
45247
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
44481
- const filePath = join(outputRoot, join(resolvedRelativeDirPath, relativeFilePath));
45248
+ const relativePath = join(resolvedRelativeDirPath, relativeFilePath);
45249
+ const filePath = join(outputRoot, relativePath);
44482
45250
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
44483
45251
  const result = CopilotRuleFrontmatterSchema.safeParse(frontmatter);
44484
45252
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44750,7 +45518,8 @@ var DeepagentsRule = class DeepagentsRule extends ToolRule {
44750
45518
  }
44751
45519
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44752
45520
  const settablePaths = this.getSettablePaths({ global });
44753
- const fileContent = await readFileContent(join(outputRoot, join(settablePaths.root.relativeDirPath, settablePaths.root.relativeFilePath)));
45521
+ const relativePath = join(settablePaths.root.relativeDirPath, settablePaths.root.relativeFilePath);
45522
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
44754
45523
  return new DeepagentsRule({
44755
45524
  outputRoot,
44756
45525
  relativeDirPath: settablePaths.root.relativeDirPath,
@@ -45177,7 +45946,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45177
45946
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
45178
45947
  const paths = this.getSettablePaths({ global });
45179
45948
  if (relativeFilePath === paths.root.relativeFilePath) {
45180
- const fileContent = await readFileContent(join(outputRoot, join(paths.root.relativeDirPath, paths.root.relativeFilePath)));
45949
+ const relativePath = join(paths.root.relativeDirPath, paths.root.relativeFilePath);
45950
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45181
45951
  return new FactorydroidRule({
45182
45952
  outputRoot,
45183
45953
  relativeDirPath: paths.root.relativeDirPath,
@@ -45188,7 +45958,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45188
45958
  });
45189
45959
  }
45190
45960
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45191
- const fileContent = await readFileContent(join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath)));
45961
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
45962
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45192
45963
  return new FactorydroidRule({
45193
45964
  outputRoot,
45194
45965
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45418,7 +46189,8 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
45418
46189
  }
45419
46190
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
45420
46191
  const { root } = this.getSettablePaths();
45421
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
46192
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
46193
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45422
46194
  return new HermesagentRule({
45423
46195
  outputRoot,
45424
46196
  relativeDirPath: root.relativeDirPath,
@@ -45529,7 +46301,8 @@ var JunieRule = class JunieRule extends ToolRule {
45529
46301
  });
45530
46302
  }
45531
46303
  const relativeDirPath = this.getSettablePaths().root.relativeDirPath;
45532
- const fileContent = await readFileContent(join(outputRoot, join(relativeDirPath, relativeFilePath)));
46304
+ const relativePath = join(relativeDirPath, relativeFilePath);
46305
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45533
46306
  return new JunieRule({
45534
46307
  outputRoot,
45535
46308
  relativeDirPath,
@@ -45629,7 +46402,8 @@ var KiloRule = class KiloRule extends ToolRule {
45629
46402
  });
45630
46403
  }
45631
46404
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45632
- const fileContent = await readFileContent(join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath)));
46405
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
46406
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45633
46407
  return new KiloRule({
45634
46408
  outputRoot,
45635
46409
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45966,10 +46740,13 @@ var KiroIdeRule = class extends KiroRule {
45966
46740
  //#region src/features/rules/opencode-rule.ts
45967
46741
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
45968
46742
  static getSettablePaths({ global, excludeToolDir } = {}) {
45969
- if (global) return { root: {
45970
- relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
45971
- relativeFilePath: OPENCODE_RULE_FILE_NAME
45972
- } };
46743
+ if (global) return {
46744
+ root: {
46745
+ relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
46746
+ relativeFilePath: OPENCODE_RULE_FILE_NAME
46747
+ },
46748
+ nonRoot: { relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, "memories", excludeToolDir) }
46749
+ };
45973
46750
  return {
45974
46751
  root: {
45975
46752
  relativeDirPath: ".",
@@ -45979,7 +46756,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
45979
46756
  };
45980
46757
  }
45981
46758
  static getExtraSharedWritePaths({ global = false } = {}) {
45982
- return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
46759
+ return [OpencodeMcp.getSettablePaths({ global })];
45983
46760
  }
45984
46761
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
45985
46762
  const paths = this.getSettablePaths({ global });
@@ -45996,7 +46773,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
45996
46773
  });
45997
46774
  }
45998
46775
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45999
- const fileContent = await readFileContent(join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath)));
46776
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
46777
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
46000
46778
  return new OpenCodeRule({
46001
46779
  outputRoot,
46002
46780
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46111,7 +46889,8 @@ var PiRule = class PiRule extends ToolRule {
46111
46889
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46112
46890
  const { root, appendSystemPrompt } = this.getSettablePaths({ global });
46113
46891
  if (relativeFilePath === "APPEND_SYSTEM.md") {
46114
- const fileContent = await readFileContent(join(outputRoot, join(appendSystemPrompt.relativeDirPath, appendSystemPrompt.relativeFilePath)));
46892
+ const relativePath = join(appendSystemPrompt.relativeDirPath, appendSystemPrompt.relativeFilePath);
46893
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
46115
46894
  return new PiRule({
46116
46895
  outputRoot,
46117
46896
  relativeDirPath: appendSystemPrompt.relativeDirPath,
@@ -46122,7 +46901,8 @@ var PiRule = class PiRule extends ToolRule {
46122
46901
  appendSystemPrompt: true
46123
46902
  });
46124
46903
  }
46125
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
46904
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
46905
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
46126
46906
  return new PiRule({
46127
46907
  outputRoot,
46128
46908
  relativeDirPath: root.relativeDirPath,
@@ -46418,6 +47198,13 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46418
47198
  toolTarget: "qwencode"
46419
47199
  });
46420
47200
  }
47201
+ /**
47202
+ * The personal local context file lives under `.qwen/`, not at the project
47203
+ * root where the settable root path points, so override the deletion glob.
47204
+ */
47205
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
47206
+ return join(outputRoot, QWENCODE_DIR, fileName);
47207
+ }
46421
47208
  };
46422
47209
  //#endregion
46423
47210
  //#region src/features/rules/reasonix-rule.ts
@@ -46435,9 +47222,51 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46435
47222
  relativeFilePath: REASONIX_RULE_FILE_NAME
46436
47223
  } };
46437
47224
  }
46438
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47225
+ /**
47226
+ * Context Engine v2 (v1.18.0) walks from the workspace root to the target
47227
+ * path loading per-directory instruction files, so nested `REASONIX.md`
47228
+ * files are a real scoping surface: "Deeper directories beat broader
47229
+ * directories." The scan mirrors the AGENTS.md standard's nested discovery
47230
+ * (same exclusions, import-only, project scope).
47231
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/v1.18.0/docs/SESSION_MEMORY_RETRIEVAL.md
47232
+ */
47233
+ static getNestedFilePatterns({ outputRoot }) {
47234
+ const root = toPosixPath(outputRoot);
47235
+ return {
47236
+ include: [`${root}/**/${REASONIX_RULE_FILE_NAME}`],
47237
+ ignore: [
47238
+ `${root}/${REASONIX_RULE_FILE_NAME}`,
47239
+ `${root}/**/.*/**`,
47240
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `${root}/**/${dir}/**`),
47241
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${root}/${dir}/**`)
47242
+ ]
47243
+ };
47244
+ }
47245
+ /**
47246
+ * The subproject directory this rule scopes, or `undefined` for the root
47247
+ * file (project or global).
47248
+ */
47249
+ getSubprojectPath() {
47250
+ if (this.isRoot()) return;
47251
+ const relativeDirPath = toPosixPath(this.getRelativeDirPath());
47252
+ if (relativeDirPath === "." || relativeDirPath === "" || relativeDirPath.startsWith(".")) return;
47253
+ return relativeDirPath;
47254
+ }
47255
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
46439
47256
  const { root } = this.getSettablePaths({ global });
46440
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
47257
+ if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
47258
+ const fileContent = await readFileContent(join(outputRoot, overrideDirPath, REASONIX_RULE_FILE_NAME));
47259
+ return new ReasonixRule({
47260
+ outputRoot,
47261
+ relativeDirPath: overrideDirPath,
47262
+ relativeFilePath: REASONIX_RULE_FILE_NAME,
47263
+ fileContent,
47264
+ validate,
47265
+ root: false
47266
+ });
47267
+ }
47268
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
47269
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
46441
47270
  return new ReasonixRule({
46442
47271
  outputRoot,
46443
47272
  relativeDirPath: root.relativeDirPath,
@@ -46449,7 +47278,17 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46449
47278
  }
46450
47279
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
46451
47280
  const { root } = this.getSettablePaths({ global });
46452
- const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47281
+ const frontmatter = rulesyncRule.getFrontmatter();
47282
+ const isRoot = frontmatter.root ?? false;
47283
+ const subprojectPath = frontmatter.agentsmd?.subprojectPath;
47284
+ if (!global && !isRoot && subprojectPath) return new ReasonixRule({
47285
+ outputRoot,
47286
+ relativeDirPath: join(subprojectPath),
47287
+ relativeFilePath: REASONIX_RULE_FILE_NAME,
47288
+ fileContent: rulesyncRule.getBody(),
47289
+ validate,
47290
+ root: false
47291
+ });
46453
47292
  return new ReasonixRule({
46454
47293
  outputRoot,
46455
47294
  relativeDirPath: root.relativeDirPath,
@@ -46460,7 +47299,22 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46460
47299
  });
46461
47300
  }
46462
47301
  toRulesyncRule() {
46463
- return this.toRulesyncRuleDefault();
47302
+ const subprojectPath = this.getSubprojectPath();
47303
+ if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
47304
+ const slug = subprojectPath.replaceAll("/", "-");
47305
+ return new RulesyncRule({
47306
+ outputRoot: this.getOutputRoot(),
47307
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
47308
+ relativeFilePath: `${slug}-reasonix.md`,
47309
+ frontmatter: {
47310
+ targets: ["reasonix"],
47311
+ root: false,
47312
+ globs: [`${subprojectPath}/**/*`],
47313
+ agentsmd: { subprojectPath }
47314
+ },
47315
+ body: this.getFileContent(),
47316
+ validate: true
47317
+ });
46464
47318
  }
46465
47319
  validate() {
46466
47320
  return {
@@ -46630,7 +47484,7 @@ var RooRule = class RooRule extends ToolRule {
46630
47484
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
46631
47485
  * at the project root, not under `.roo/` (mirrors rovodev).
46632
47486
  */
46633
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47487
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46634
47488
  return join(outputRoot, fileName);
46635
47489
  }
46636
47490
  };
@@ -46762,19 +47616,6 @@ var RovodevRule = class RovodevRule extends ToolRule {
46762
47616
  }));
46763
47617
  }
46764
47618
  toRulesyncRule() {
46765
- if (this.getRelativeFilePath() === "AGENTS.local.md") return new RulesyncRule({
46766
- outputRoot: this.getOutputRoot(),
46767
- relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
46768
- relativeFilePath: ROVODEV_LEGACY_RULE_FILE_NAME,
46769
- frontmatter: {
46770
- targets: ["rovodev"],
46771
- root: false,
46772
- localRoot: true,
46773
- globs: []
46774
- },
46775
- body: this.getFileContent(),
46776
- validate: true
46777
- });
46778
47619
  if (!this.isRoot()) return new RulesyncRule({
46779
47620
  outputRoot: this.getOutputRoot(),
46780
47621
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -46831,7 +47672,7 @@ var RovodevRule = class RovodevRule extends ToolRule {
46831
47672
  };
46832
47673
  }
46833
47674
  /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
46834
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47675
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46835
47676
  return join(outputRoot, fileName);
46836
47677
  }
46837
47678
  };
@@ -47021,15 +47862,20 @@ var WarpRule = class WarpRule extends ToolRule {
47021
47862
  root: root ?? false
47022
47863
  });
47023
47864
  }
47024
- static getSettablePaths(_options = {}) {
47865
+ static getSettablePaths({ global = false } = {}) {
47866
+ if (global) return { root: {
47867
+ relativeDirPath: WARP_GLOBAL_RULE_DIR,
47868
+ relativeFilePath: WARP_RULE_FILE_NAME
47869
+ } };
47025
47870
  return { root: {
47026
47871
  relativeDirPath: ".",
47027
47872
  relativeFilePath: WARP_RULE_FILE_NAME
47028
47873
  } };
47029
47874
  }
47030
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
47031
- const { root } = this.getSettablePaths();
47032
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
47875
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47876
+ const { root } = this.getSettablePaths({ global });
47877
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
47878
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
47033
47879
  return new WarpRule({
47034
47880
  outputRoot,
47035
47881
  relativeDirPath: root.relativeDirPath,
@@ -47039,8 +47885,8 @@ var WarpRule = class WarpRule extends ToolRule {
47039
47885
  root: true
47040
47886
  });
47041
47887
  }
47042
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
47043
- const { root } = this.getSettablePaths();
47888
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
47889
+ const { root } = this.getSettablePaths({ global });
47044
47890
  const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47045
47891
  return new WarpRule({
47046
47892
  outputRoot,
@@ -47060,8 +47906,8 @@ var WarpRule = class WarpRule extends ToolRule {
47060
47906
  error: null
47061
47907
  };
47062
47908
  }
47063
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
47064
- const { root } = this.getSettablePaths();
47909
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
47910
+ const { root } = this.getSettablePaths({ global });
47065
47911
  const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
47066
47912
  return new WarpRule({
47067
47913
  outputRoot,
@@ -47154,6 +48000,27 @@ var ZedRule = class ZedRule extends ToolRule {
47154
48000
  }
47155
48001
  };
47156
48002
  //#endregion
48003
+ //#region src/features/rules/zoocode-rule.ts
48004
+ /**
48005
+ * Rule generator for **Zoo Code**, the community continuation of Roo Code
48006
+ * (named by the Roo shutdown notice; releases continue Roo's numbering from
48007
+ * v3.54.0). Zoo Code still resolves `~/.roo` and the project `.roo/` layout —
48008
+ * the `.zoo` renaming is confined to provider/auth code — so this target
48009
+ * reuses {@link RooRule}'s behavior verbatim and only narrows the targeting to
48010
+ * the `zoocode` tool name.
48011
+ *
48012
+ * @see https://github.com/Zoo-Code-Org/Zoo-Code
48013
+ * @see https://docs.zoocode.dev
48014
+ */
48015
+ var ZoocodeRule = class extends RooRule {
48016
+ static isTargetedByRulesyncRule(rulesyncRule) {
48017
+ return this.isTargetedByRulesyncRuleDefault({
48018
+ rulesyncRule,
48019
+ toolTarget: "zoocode"
48020
+ });
48021
+ }
48022
+ };
48023
+ //#endregion
47157
48024
  //#region src/features/rules/rules-processor.ts
47158
48025
  const RulesProcessorToolTargetSchema = z.enum(rulesProcessorToolTargetTuple);
47159
48026
  const formatRulePaths = (rules) => rules.map((r) => join(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
@@ -47416,6 +48283,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47416
48283
  supportsGlobal: true,
47417
48284
  ruleDiscoveryMode: "toon",
47418
48285
  mcpInstructionsRegistrar: OpencodeMcp,
48286
+ mcpInstructionsRegistrarGlobal: true,
47419
48287
  collisionPolicy: "compose"
47420
48288
  }
47421
48289
  }],
@@ -47434,6 +48302,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47434
48302
  extension: "md",
47435
48303
  supportsGlobal: true,
47436
48304
  ruleDiscoveryMode: "auto",
48305
+ localRootMode: "separate-local-file",
48306
+ localRootFileName: QWENCODE_LOCAL_RULE_FILE_NAME,
47437
48307
  additionalConventions: { subagents: { subagentClass: QwencodeSubagent } }
47438
48308
  }
47439
48309
  }],
@@ -47478,6 +48348,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47478
48348
  localRootFileName: "AGENTS.local.md"
47479
48349
  }
47480
48350
  }],
48351
+ ["zoocode", {
48352
+ class: ZoocodeRule,
48353
+ meta: {
48354
+ extension: "md",
48355
+ supportsGlobal: true,
48356
+ ruleDiscoveryMode: "auto",
48357
+ localRootMode: "separate-local-file",
48358
+ localRootFileName: "AGENTS.local.md"
48359
+ }
48360
+ }],
47481
48361
  ["takt", {
47482
48362
  class: TaktRule,
47483
48363
  meta: {
@@ -47498,7 +48378,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47498
48378
  class: WarpRule,
47499
48379
  meta: {
47500
48380
  extension: "md",
47501
- supportsGlobal: false,
48381
+ supportsGlobal: true,
47502
48382
  ruleDiscoveryMode: "toon",
47503
48383
  collisionPolicy: "fold"
47504
48384
  }
@@ -47650,18 +48530,19 @@ var RulesProcessor = class extends FeatureProcessor {
47650
48530
  /**
47651
48531
  * Non-root rules of some tools are not auto-loaded; the tool's MCP feature
47652
48532
  * registers them in its shared config's `instructions` key. The root rule is
47653
- * auto-loaded and never registered. Project scope only.
48533
+ * auto-loaded and never registered. Global scope opt-in via
48534
+ * `mcpInstructionsRegistrarGlobal`.
47654
48535
  */
47655
48536
  async buildMcpInstructionFiles({ toolRules, meta }) {
47656
- if (!meta.mcpInstructionsRegistrar || this.global) return [];
48537
+ if (!meta.mcpInstructionsRegistrar || this.global && !meta.mcpInstructionsRegistrarGlobal) return [];
47657
48538
  const instructionPaths = toolRules.filter((rule) => !rule.isRoot() && !rule.isExcludedFromRootReferences()).map((rule) => toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
47658
- if (instructionPaths.length === 0) return [];
47659
- return [await meta.mcpInstructionsRegistrar.fromInstructions({
48539
+ const registered = await meta.mcpInstructionsRegistrar.fromInstructions({
47660
48540
  outputRoot: this.outputRoot,
47661
48541
  instructions: instructionPaths,
47662
48542
  validate: true,
47663
48543
  global: this.global
47664
- })];
48544
+ });
48545
+ return registered ? [registered] : [];
47665
48546
  }
47666
48547
  /**
47667
48548
  * For tools that don't create a separate conventions rule, prepend the
@@ -47792,45 +48673,58 @@ var RulesProcessor = class extends FeatureProcessor {
47792
48673
  const rootRule = toolRules.find((rule) => rule.isRoot());
47793
48674
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
47794
48675
  }
47795
- buildLocalRootFile({ factory, fileName, body }) {
48676
+ buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
47796
48677
  if (factory.class === ClaudecodeRule) {
47797
48678
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
47798
48679
  return new ClaudecodeRule({
47799
48680
  outputRoot: this.outputRoot,
47800
- relativeDirPath: paths.root.relativeDirPath,
48681
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47801
48682
  relativeFilePath: fileName,
47802
48683
  frontmatter: {},
47803
48684
  body,
47804
48685
  validate: true,
47805
- root: true
48686
+ root: true,
48687
+ localRoot
47806
48688
  });
47807
48689
  }
47808
48690
  if (factory.class === ClaudecodeLegacyRule) {
47809
48691
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
47810
48692
  return new ClaudecodeLegacyRule({
47811
48693
  outputRoot: this.outputRoot,
47812
- relativeDirPath: paths.root.relativeDirPath,
48694
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47813
48695
  relativeFilePath: fileName,
47814
48696
  fileContent: body,
47815
48697
  validate: true,
47816
- root: true
48698
+ root: true,
48699
+ localRoot
47817
48700
  });
47818
48701
  }
47819
48702
  if (factory.class === RovodevRule) return new RovodevRule({
47820
48703
  outputRoot: this.outputRoot,
47821
- relativeDirPath: ".",
48704
+ relativeDirPath: relativeDirPath ?? ".",
47822
48705
  relativeFilePath: fileName,
47823
48706
  fileContent: body,
47824
48707
  validate: true,
47825
- root: true
48708
+ root: true,
48709
+ localRoot
47826
48710
  });
47827
48711
  if (factory.class === RooRule) return new RooRule({
47828
48712
  outputRoot: this.outputRoot,
47829
- relativeDirPath: ".",
48713
+ relativeDirPath: relativeDirPath ?? ".",
47830
48714
  relativeFilePath: fileName,
47831
48715
  fileContent: body,
47832
48716
  validate: true,
47833
- root: true
48717
+ root: true,
48718
+ localRoot
48719
+ });
48720
+ if (factory.class === QwencodeRule) return new QwencodeRule({
48721
+ outputRoot: this.outputRoot,
48722
+ relativeDirPath: relativeDirPath ?? ".qwen",
48723
+ relativeFilePath: fileName,
48724
+ fileContent: body,
48725
+ validate: true,
48726
+ root: true,
48727
+ localRoot
47834
48728
  });
47835
48729
  return null;
47836
48730
  }
@@ -47868,6 +48762,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
47868
48762
  async convertToolFilesToRulesyncFiles(toolFiles) {
47869
48763
  const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
47870
48764
  const rulesyncRules = toolRules.map((toolRule) => {
48765
+ if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
47871
48766
  return toolRule.toRulesyncRule();
47872
48767
  });
47873
48768
  const claimedBy = /* @__PURE__ */ new Map();
@@ -48001,20 +48896,34 @@ As this project's AI coding tool, you must follow the additional conventions bel
48001
48896
  })();
48002
48897
  this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
48003
48898
  const localRootToolRules = await (async () => {
48004
- if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
48899
+ if (this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
48005
48900
  const fileName = factory.meta.localRootFileName;
48006
- if (factory.class.getLocalRootDeletionGlob) {
48007
- const filePaths = await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
48901
+ const filePaths = await (async () => {
48902
+ if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
48008
48903
  outputRoot: this.outputRoot,
48009
48904
  fileName
48010
48905
  }));
48011
- return buildDeletionRulesFromPaths(filePaths);
48012
- }
48013
- if (!settablePaths.root) return [];
48014
- const filePaths = await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName));
48015
- return buildDeletionRulesFromPaths(filePaths);
48906
+ if (!settablePaths.root) return [];
48907
+ return await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName));
48908
+ })();
48909
+ if (forDeletion) return buildDeletionRulesFromPaths(filePaths);
48910
+ return (await Promise.all(filePaths.map(async (filePath) => {
48911
+ const relativeDirPath = resolveRelativeDirPath(filePath);
48912
+ checkPathTraversal({
48913
+ relativePath: relativeDirPath,
48914
+ intendedRootDir: this.outputRoot
48915
+ });
48916
+ const body = await readFileContent(filePath);
48917
+ return this.buildLocalRootFile({
48918
+ factory,
48919
+ fileName: basename(filePath),
48920
+ body,
48921
+ relativeDirPath,
48922
+ localRoot: true
48923
+ });
48924
+ }))).filter((rule) => rule !== null);
48016
48925
  })();
48017
- this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
48926
+ this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files`);
48018
48927
  const rootMirrorDeletionRules = await (async () => {
48019
48928
  const rootMirror = factory.class.getRootMirror?.();
48020
48929
  if (!forDeletion || this.global || !rootMirror) return [];
@@ -48140,13 +49049,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
48140
49049
  const lines = [];
48141
49050
  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.");
48142
49051
  lines.push("");
48143
- const toonContent = encode({ rules: toolRulesWithoutRoot.map((toolRule) => {
49052
+ const rules = toolRulesWithoutRoot.map((toolRule) => {
48144
49053
  const frontmatter = toolRule.toRulesyncRule().getFrontmatter();
48145
49054
  const rule = { path: `@${toolRule.getRelativePathFromCwd()}` };
48146
49055
  if (frontmatter.description) rule.description = frontmatter.description;
48147
49056
  if (frontmatter.globs && frontmatter.globs.length > 0) rule.applyTo = frontmatter.globs;
48148
49057
  return rule;
48149
- }) });
49058
+ });
49059
+ const toonContent = encode({ rules });
48150
49060
  lines.push(toonContent);
48151
49061
  return lines.join("\n") + "\n\n";
48152
49062
  }
@@ -49308,8 +50218,8 @@ async function generateIgnoreCore(params) {
49308
50218
  allPaths.push(...result.paths);
49309
50219
  if (result.hasDiff) hasDiff = true;
49310
50220
  } catch (error) {
49311
- logger.warn(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
49312
- continue;
50221
+ logger.error(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
50222
+ throw error;
49313
50223
  }
49314
50224
  }
49315
50225
  return {
@@ -49581,8 +50491,8 @@ async function generatePermissionsCore(params) {
49581
50491
  allPaths.push(...result.paths);
49582
50492
  if (result.hasDiff) hasDiff = true;
49583
50493
  } catch (error) {
49584
- logger.warn(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
49585
- continue;
50494
+ logger.error(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
50495
+ throw error;
49586
50496
  }
49587
50497
  }
49588
50498
  return {
@@ -49984,6 +50894,6 @@ async function importChecksCore(params) {
49984
50894
  return writtenCount;
49985
50895
  }
49986
50896
  //#endregion
49987
- export { warnOnConflictingFlags as $, RULESYNC_RELATIVE_DIR_PATH as $t, RulesyncMcp as A, MAX_FILE_SIZE as At, stringifyFrontmatter as B, RULESYNC_HOOKS_LEGACY_FILE_NAME as Bt, RulesyncSubagent as C, runWithDirectoryRollback as Ct, RulesyncRule as D, ALL_TOOL_TARGETS_WITH_WILDCARD as Dt, RulesyncSkillFrontmatterSchema as E, ALL_TOOL_TARGETS as Et, parseJsonc as F, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Ft, CONFLICTING_TARGET_PAIRS as G, RULESYNC_MCP_LEGACY_FILE_NAME as Gt, SHARED_USER_MANAGED_CONFIG_PATHS as H, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Ht, RulesyncCommand as I, RULESYNC_CONFIG_SCHEMA_URL as It, SourceEntrySchema as J, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Jt, ConfigFileSchema as K, RULESYNC_MCP_RELATIVE_FILE_PATH as Kt, RulesyncCommandFrontmatterSchema as L, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Lt, RulesyncHooks as M, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Mt, getRulesyncSourceCandidates as N, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Nt, RulesyncRuleFrontmatterSchema as O, PACKAGING_TOOL_TARGETS as Ot, resolveRulesyncSourceWritePath as P, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Pt, fallbackLogger as Q, RULESYNC_PERMISSIONS_SCHEMA_URL as Qt, RulesyncCheck as R, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Rt, getLocalSkillDirNames as S, resolvePath as St, RulesyncSkill as T, writeFileContent as Tt, SKILL_FILE_NAME as U, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Ut, loadYaml as V, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Vt, ConfigResolver as W, RULESYNC_MCP_FILE_NAME as Wt, ConsoleLogger as X, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Xt, findControlCharacter as Y, RULESYNC_PERMISSIONS_FILE_NAME as Yt, JsonLogger as Z, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Zt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as _, removeDirectory as _t, convertFromTool as a, ALL_FEATURES_WITH_WILDCARD as an, checkPathTraversal as at, CODEXCLI_BASH_RULES_FILE_NAME as b, removeFileStrict as bt, SubagentsProcessor as c, ensureDir as ct, IgnoreProcessor as d, getFileSize as dt, RULESYNC_RULES_RELATIVE_DIR_PATH as en, CLIError as et, HooksProcessor as f, getHomeDirectory as ft, CLAUDECODE_MEMORIES_DIR_NAME as g, readFileContentOrNull as gt, CLAUDECODE_LOCAL_RULE_FILE_NAME as h, readFileContent as ht, getProcessorRegistryEntry as i, ALL_FEATURES as in, assertWritablePathInsideRoot as it, RulesyncIgnore as j, RULESYNC_AIIGNORE_FILE_NAME as jt, RulesyncPermissions as k, ToolTargetSchema as kt, SkillsProcessor as l, fileExists as lt, CLAUDECODE_DIR as m, listDirectoryFiles as mt, checkRulesyncDirExists as n, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as nn, assertDirectoryIfExists as nt, isPackagingToolTarget as o, DEPRECATED_FEATURE_REPLACEMENTS as on, createTempDirectory as ot, CommandsProcessor as p, isSymlink as pt, GITIGNORE_DESTINATION_KEY as q, RULESYNC_MCP_SCHEMA_URL as qt, generate as r, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as rn, assertTreeContainsNoSymlinks as rt, RulesProcessor as s, formatError as sn, directoryExists as st, importFromTool as t, RULESYNC_SKILLS_RELATIVE_DIR_PATH as tn, ErrorCodes as tt, McpProcessor as u, findFilesByGlobs as ut, CLAUDECODE_SKILLS_DIR_PATH as v, removeDirectoryStrict as vt, RulesyncSubagentFrontmatterSchema as w, toPosixPath as wt, CODEXCLI_DIR as x, removeTempDirectory as xt, ChecksProcessor as y, removeFile as yt, RulesyncCheckFrontmatterSchema as z, RULESYNC_HOOKS_FILE_NAME as zt };
50897
+ export { JsonLogger as $, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as $t, RulesyncRuleFrontmatterSchema as A, PACKAGING_TOOL_TARGETS as At, RulesyncCheck as B, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Bt, CODEXCLI_DIR as C, removeTempDirectory as Ct, RulesyncSkill as D, writeFileContent as Dt, RulesyncSubagentFrontmatterSchema as E, toPosixPath as Et, getRulesyncSourceCandidates as F, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Ft, SKILL_FILE_NAME as G, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Gt, stringifyFrontmatter as H, RULESYNC_HOOKS_LEGACY_FILE_NAME as Ht, resolveRulesyncSourceWritePath as I, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as It, ConfigFileSchema as J, RULESYNC_MCP_RELATIVE_FILE_PATH as Jt, ConfigResolver as K, RULESYNC_MCP_FILE_NAME as Kt, parseJsonc as L, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Lt, RulesyncMcp as M, MAX_FILE_SIZE as Mt, RulesyncIgnore as N, RULESYNC_AIIGNORE_FILE_NAME as Nt, RulesyncSkillFrontmatterSchema as O, ALL_TOOL_TARGETS as Ot, RulesyncHooks as P, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Pt, ConsoleLogger as Q, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Qt, RulesyncCommand as R, RULESYNC_CONFIG_SCHEMA_URL as Rt, CODEXCLI_BASH_RULES_FILE_NAME as S, removeFileStrict as St, RulesyncSubagent as T, runWithDirectoryRollback as Tt, loadYaml as U, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ut, RulesyncCheckFrontmatterSchema as V, RULESYNC_HOOKS_FILE_NAME as Vt, SHARED_USER_MANAGED_CONFIG_PATHS as W, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Wt, SourceEntrySchema as X, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Xt, GITIGNORE_DESTINATION_KEY as Y, RULESYNC_MCP_SCHEMA_URL as Yt, findControlCharacter as Z, RULESYNC_PERMISSIONS_FILE_NAME as Zt, CLAUDECODE_LOCAL_RULE_FILE_NAME as _, readFileContent as _t, convertFromTool as a, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as an, assertTreeContainsNoSymlinks as at, CLAUDECODE_SKILLS_DIR_PATH as b, removeDirectoryStrict as bt, SubagentsProcessor as c, DEPRECATED_FEATURE_REPLACEMENTS as cn, createTempDirectory as ct, IgnoreProcessor as d, fileExists as dt, RULESYNC_PERMISSIONS_SCHEMA_URL as en, fallbackLogger as et, HooksProcessor as f, findFilesByGlobs as ft, CLAUDECODE_DIR as g, listDirectoryFiles as gt, QWENCODE_LOCAL_RULE_FILE_NAME as h, isSymlink as ht, getProcessorRegistryEntry as i, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as in, assertDirectoryIfExists as it, RulesyncPermissions as j, ToolTargetSchema as jt, RulesyncRule as k, ALL_TOOL_TARGETS_WITH_WILDCARD as kt, SkillsProcessor as l, formatError as ln, directoryExists as lt, QWENCODE_DIR as m, getHomeDirectory as mt, checkRulesyncDirExists as n, RULESYNC_RULES_RELATIVE_DIR_PATH as nn, CLIError as nt, isPackagingToolTarget as o, ALL_FEATURES as on, assertWritablePathInsideRoot as ot, CommandsProcessor as p, getFileSize as pt, CONFLICTING_TARGET_PAIRS as q, RULESYNC_MCP_LEGACY_FILE_NAME as qt, generate as r, RULESYNC_SKILLS_RELATIVE_DIR_PATH as rn, ErrorCodes as rt, RulesProcessor as s, ALL_FEATURES_WITH_WILDCARD as sn, checkPathTraversal as st, importFromTool as t, RULESYNC_RELATIVE_DIR_PATH as tn, warnOnConflictingFlags as tt, McpProcessor as u, ensureDir as ut, CLAUDECODE_MEMORIES_DIR_NAME as v, readFileContentOrNull as vt, getLocalSkillDirNames as w, resolvePath as wt, ChecksProcessor as x, removeFile as xt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as y, removeDirectory as yt, RulesyncCommandFrontmatterSchema as z, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as zt };
49988
50898
 
49989
- //# sourceMappingURL=import-Bj5HxY9p.js.map
50899
+ //# sourceMappingURL=import-BlwypG9v.js.map