rulesync 16.4.0 → 16.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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",
@@ -3030,7 +3044,6 @@ const CANONICAL_TO_OPENCODE_EVENT_NAMES = {
3030
3044
  postToolUse: "tool.execute.after",
3031
3045
  stop: "session.idle",
3032
3046
  afterFileEdit: "file.edited",
3033
- afterShellExecution: "command.executed",
3034
3047
  permissionRequest: "permission.asked",
3035
3048
  preCompact: "experimental.session.compacting",
3036
3049
  postCompact: "session.compacted",
@@ -4097,10 +4110,22 @@ const ClaudecodePermissionsOverrideSchema = z.looseObject({
4097
4110
  * category; the shared `permission` block still drives the base permission and
4098
4111
  * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
4099
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
+ *
4100
4120
  * @example
4101
4121
  * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
4122
+ * @example
4123
+ * { "enabled_tools": ["bash", "read_file", "grep"] }
4102
4124
  */
4103
- 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
+ });
4104
4129
  /**
4105
4130
  * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
4106
4131
  * autonomy settings with no canonical permission category — `approvalMode`
@@ -4172,7 +4197,10 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
4172
4197
  const ReasonixPermissionsOverrideSchema = z.looseObject({
4173
4198
  permission: z.optional(ToolScopedPermissionSchema),
4174
4199
  sandbox: z.optional(z.looseObject({})),
4175
- 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()))
4176
4204
  });
4177
4205
  /**
4178
4206
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -4203,12 +4231,69 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({
4203
4231
  * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
4204
4232
  * Fields placed here are merged into `[agents.profiles]` of Warp's global
4205
4233
  * `settings.toml`, while the shared `permission` block continues to drive the
4206
- * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
4207
- * 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.
4208
4240
  *
4209
4241
  * @example
4210
- * { "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).
4211
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
+ });
4212
4297
  const WarpPermissionsOverrideSchema = z.looseObject({
4213
4298
  permission: z.optional(ToolScopedPermissionSchema),
4214
4299
  agent_mode_coding_permissions: z.optional(z.enum([
@@ -4217,7 +4302,8 @@ const WarpPermissionsOverrideSchema = z.looseObject({
4217
4302
  "allow_reading_specific_files"
4218
4303
  ])),
4219
4304
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
4220
- 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)
4221
4307
  });
4222
4308
  /**
4223
4309
  * The actions Junie's allowlist accepts. Verified against the shipped Junie
@@ -4949,7 +5035,8 @@ var AiDir = class {
4949
5035
  getDirPath() {
4950
5036
  const fullPath = path.join(this.outputRoot, this.relativeDirPath, this.dirName);
4951
5037
  const resolvedFull = resolve(fullPath);
4952
- const rel = relative(resolve(this.outputRoot), resolvedFull);
5038
+ const resolvedBase = resolve(this.outputRoot);
5039
+ const rel = relative(resolvedBase, resolvedFull);
4953
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}"`);
4954
5041
  return fullPath;
4955
5042
  }
@@ -5232,6 +5319,9 @@ const RulesyncSubagentFrontmatterSchema = z.looseObject({
5232
5319
  customInstructions: z.optional(z.string()),
5233
5320
  groups: z.optional(z.array(z.unknown()))
5234
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())) })),
5235
5325
  vibe: z.optional(z.looseObject({
5236
5326
  agent_type: z.optional(z.enum(["agent", "subagent"])),
5237
5327
  display_name: z.optional(z.string()),
@@ -5371,7 +5461,6 @@ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
5371
5461
  const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
5372
5462
  const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
5373
5463
  const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
5374
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
5375
5464
  const ROVODEV_MCP_FILE_NAME = "mcp.json";
5376
5465
  const ROVODEV_CONFIG_FILE_NAME = "config.yml";
5377
5466
  const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
@@ -6590,6 +6679,7 @@ const KIMI_CODE_CONFIG_SHARED_FILE_KEY = ".kimi-code/config.toml";
6590
6679
  const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
6591
6680
  const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
6592
6681
  const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
6682
+ const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
6593
6683
  /**
6594
6684
  * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
6595
6685
  * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
@@ -6929,6 +7019,10 @@ const SHARED_CONFIG_OWNERSHIP = {
6929
7019
  permissions: {
6930
7020
  kind: "replace-owned-keys",
6931
7021
  ownedKeys: ["permission"]
7022
+ },
7023
+ rules: {
7024
+ kind: "replace-owned-keys",
7025
+ ownedKeys: ["instructions"]
6932
7026
  }
6933
7027
  }
6934
7028
  },
@@ -7046,6 +7140,19 @@ const SHARED_CONFIG_OWNERSHIP = {
7046
7140
  ]
7047
7141
  }
7048
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
+ }
7049
7156
  }
7050
7157
  };
7051
7158
  /**
@@ -7467,7 +7574,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7467
7574
  class: CursorCheck,
7468
7575
  meta: {
7469
7576
  supportsGlobal: false,
7470
- filePattern: CURSOR_BUGBOT_FILE_NAME
7577
+ filePattern: CURSOR_BUGBOT_FILE_NAME,
7578
+ committedOutput: true
7471
7579
  }
7472
7580
  }],
7473
7581
  ["hermesagent", {
@@ -7481,7 +7589,8 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
7481
7589
  class: RovodevCheck,
7482
7590
  meta: {
7483
7591
  supportsGlobal: false,
7484
- filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME
7592
+ filePattern: ROVODEV_REVIEW_AGENT_FILE_NAME,
7593
+ committedOutput: true
7485
7594
  }
7486
7595
  }],
7487
7596
  ["takt", {
@@ -9643,7 +9752,8 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9643
9752
  static async validateRulesyncCommands({ inputRoot, rulesyncCommands, logger }) {
9644
9753
  const commandNames = new Set(rulesyncCommands.filter((command) => this.isTargetedByRulesyncCommand(command)).map((command) => basename(command.getRelativeFilePath(), ".md")));
9645
9754
  if (commandNames.size === 0) return;
9646
- 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));
9647
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.`);
9648
9758
  }
9649
9759
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -9678,6 +9788,20 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
9678
9788
  function toolSkillSearchRoots(paths) {
9679
9789
  return [paths.relativeDirPath, ...paths.alternativeSkillRoots ?? []];
9680
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
+ }
9681
9805
  /** Ordered import roots: managed roots first, followed by read-only discovery roots. */
9682
9806
  function toolSkillImportRoots(paths) {
9683
9807
  return [...toolSkillSearchRoots(paths), ...paths.importOnlySkillRoots ?? []];
@@ -11621,7 +11745,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11621
11745
  };
11622
11746
  const body = rulesyncCommand.getBody();
11623
11747
  const fileContent = stringifyFrontmatter(body, rooFrontmatter);
11624
- return new RooCommand({
11748
+ return new this({
11625
11749
  outputRoot,
11626
11750
  frontmatter: rooFrontmatter,
11627
11751
  body,
@@ -11658,7 +11782,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11658
11782
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
11659
11783
  const result = RooCommandFrontmatterSchema.safeParse(frontmatter);
11660
11784
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
11661
- return new RooCommand({
11785
+ return new this({
11662
11786
  outputRoot,
11663
11787
  relativeDirPath: RooCommand.getSettablePaths().relativeDirPath,
11664
11788
  relativeFilePath,
@@ -11669,7 +11793,7 @@ var RooCommand = class RooCommand extends ToolCommand {
11669
11793
  });
11670
11794
  }
11671
11795
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
11672
- return new RooCommand({
11796
+ return new this({
11673
11797
  outputRoot,
11674
11798
  relativeDirPath,
11675
11799
  relativeFilePath,
@@ -12127,6 +12251,39 @@ var WarpCommand = class WarpCommand extends ToolCommand {
12127
12251
  }
12128
12252
  };
12129
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
12130
12287
  //#region src/features/commands/commands-processor.ts
12131
12288
  const CommandsProcessorToolTargetSchema = z.enum(commandsProcessorToolTargetTuple);
12132
12289
  /**
@@ -12386,6 +12543,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
12386
12543
  supportsSubdirectory: true
12387
12544
  }
12388
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
+ }],
12389
12556
  ["rovodev", {
12390
12557
  class: RovodevCommand,
12391
12558
  meta: {
@@ -13765,16 +13932,17 @@ var ClaudecodeHooks = class extends ToolHooks {
13765
13932
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13766
13933
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
13767
13934
  const config = rulesyncHooks.getJson();
13935
+ const claudeHooks = canonicalToToolHooks({
13936
+ config,
13937
+ toolOverrideHooks: config.claudecode?.hooks,
13938
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
13939
+ logger
13940
+ });
13768
13941
  const fileContent = applySharedConfigPatch({
13769
13942
  fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
13770
13943
  feature: "hooks",
13771
13944
  existingContent,
13772
- patch: { hooks: canonicalToToolHooks({
13773
- config,
13774
- toolOverrideHooks: config.claudecode?.hooks,
13775
- converterConfig: CLAUDE_CONVERTER_CONFIG,
13776
- logger
13777
- }) },
13945
+ patch: { hooks: claudeHooks },
13778
13946
  filePath
13779
13947
  });
13780
13948
  return new this({
@@ -15528,6 +15696,30 @@ const NAMED_HOOK_MATCHER_SUBJECTS = {
15528
15696
  "tool.execute.after": "input.tool",
15529
15697
  "experimental.session.compacting": null
15530
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
+ };
15531
15723
  function escapeForTemplateLiteral(command) {
15532
15724
  return command.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
15533
15725
  }
@@ -15548,15 +15740,19 @@ function validateAndSanitizeMatcher(matcher) {
15548
15740
  */
15549
15741
  function collectOpencodeStyleHandlers({ effectiveHooks, eventMap, namedEventHandlers, genericEventHandlers }) {
15550
15742
  for (const [canonicalEvent, definitions] of Object.entries(effectiveHooks)) {
15551
- const toolEvent = eventMap[canonicalEvent];
15743
+ const shellGate = SHELL_EVENT_TOOL_GATES[canonicalEvent];
15744
+ const toolEvent = shellGate?.toolEvent ?? eventMap[canonicalEvent];
15552
15745
  if (!toolEvent) continue;
15746
+ const matcherSupported = !shellGate && Object.hasOwn(NAMED_HOOK_MATCHER_SUBJECTS, toolEvent) && NAMED_HOOK_MATCHER_SUBJECTS[toolEvent] !== null;
15553
15747
  const handlers = [];
15554
15748
  for (const def of definitions) {
15555
15749
  if ((def.type ?? "command") !== "command") continue;
15556
15750
  if (!def.command) continue;
15751
+ if (def.matcher && !matcherSupported) continue;
15557
15752
  handlers.push({
15558
15753
  command: def.command,
15559
- matcher: def.matcher ? def.matcher : void 0
15754
+ matcher: def.matcher ? def.matcher : void 0,
15755
+ ...shellGate ? { toolGate: shellGate.tool } : {}
15560
15756
  });
15561
15757
  }
15562
15758
  if (handlers.length > 0) {
@@ -15593,7 +15789,11 @@ function buildNamedEventBodyLines(namedEventHandlers) {
15593
15789
  bodyLines.push(` "${eventName}": async (input) => {`);
15594
15790
  for (const handler of handlers) {
15595
15791
  const escapedCommand = escapeForTemplateLiteral(handler.command);
15596
- 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) {
15597
15797
  const safeMatcher = validateAndSanitizeMatcher(handler.matcher);
15598
15798
  bodyLines.push(" {");
15599
15799
  bodyLines.push(` const __re = new RegExp("${safeMatcher}");`);
@@ -17323,7 +17523,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17323
17523
  },
17324
17524
  supportedEvents: KILO_HOOK_EVENTS,
17325
17525
  supportedHookTypes: ["command"],
17326
- supportsMatcher: true
17526
+ supportsMatcher: true,
17527
+ matcherEvents: ["preToolUse", "postToolUse"]
17327
17528
  }],
17328
17529
  ["opencode", {
17329
17530
  class: OpencodeHooks,
@@ -17334,7 +17535,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
17334
17535
  },
17335
17536
  supportedEvents: OPENCODE_HOOK_EVENTS,
17336
17537
  supportedHookTypes: ["command"],
17337
- supportsMatcher: true
17538
+ supportsMatcher: true,
17539
+ matcherEvents: ["preToolUse", "postToolUse"]
17338
17540
  }],
17339
17541
  ["pi", {
17340
17542
  class: PiHooks,
@@ -19008,6 +19210,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
19008
19210
  ["qwencode", { class: QwencodeIgnore }],
19009
19211
  ["reasonix", { class: ReasonixIgnore }],
19010
19212
  ["roo", { class: RooIgnore }],
19213
+ ["zoocode", { class: RooIgnore }],
19011
19214
  ["devin", { class: DevinIgnore }],
19012
19215
  ["vibe", { class: VibeIgnore }],
19013
19216
  ["warp", { class: WarpIgnore }],
@@ -20377,7 +20580,6 @@ const resolveCopilotcliServerType = (server) => {
20377
20580
  case "streamable-http": return "http";
20378
20581
  case "stdio":
20379
20582
  case "local": return declared;
20380
- default: break;
20381
20583
  }
20382
20584
  if (server.command !== void 0) return "stdio";
20383
20585
  return isRemoteMcpServer(server) ? "http" : "stdio";
@@ -22150,9 +22352,12 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22150
22352
  fileContent = await readFileContentOrNull(jsonPath);
22151
22353
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
22152
22354
  }
22355
+ if (instructions.length === 0 && fileContent === null) return null;
22153
22356
  const json = fileContent ? parse(fileContent) : {};
22154
22357
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22155
- 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();
22156
22361
  return new KiloMcp({
22157
22362
  outputRoot,
22158
22363
  relativeDirPath: basePaths.relativeDirPath,
@@ -22161,7 +22366,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
22161
22366
  fileKey: sharedConfigFileKey(basePaths),
22162
22367
  feature: "rules",
22163
22368
  existingContent: fileContent ?? "",
22164
- patch: { instructions: mergedInstructions },
22369
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22165
22370
  filePath: join(jsonDir, relativeFilePath)
22166
22371
  }),
22167
22372
  validate
@@ -22883,6 +23088,8 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22883
23088
  static async fromInstructions({ outputRoot = process.cwd(), instructions, validate = true, global = false }) {
22884
23089
  const basePaths = this.getSettablePaths({ global });
22885
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);
22886
23093
  let fileContent = null;
22887
23094
  let relativeFilePath = OPENCODE_JSONC_FILE_NAME;
22888
23095
  const jsoncPath = join(jsonDir, OPENCODE_JSONC_FILE_NAME);
@@ -22892,9 +23099,15 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22892
23099
  fileContent = await readFileContentOrNull(jsonPath);
22893
23100
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
22894
23101
  }
23102
+ if (instructions.length === 0 && fileContent === null) return null;
22895
23103
  const json = fileContent ? parse(fileContent) : {};
22896
23104
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
22897
- 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();
22898
23111
  return new OpencodeMcp({
22899
23112
  outputRoot,
22900
23113
  relativeDirPath: basePaths.relativeDirPath,
@@ -22903,7 +23116,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
22903
23116
  fileKey: sharedConfigFileKey(basePaths),
22904
23117
  feature: "rules",
22905
23118
  existingContent: fileContent ?? "",
22906
- patch: { instructions: mergedInstructions },
23119
+ patch: { instructions: mergedInstructions.length > 0 ? mergedInstructions : void 0 },
22907
23120
  filePath: join(jsonDir, relativeFilePath)
22908
23121
  }),
22909
23122
  validate
@@ -23343,9 +23556,13 @@ function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
23343
23556
  return parsed;
23344
23557
  }
23345
23558
  /**
23346
- * Rovodev MCP: global only at ~/.rovodev/mcp.json.
23347
- * Same shape as Cursor: { mcpServers: { ... } }. See Rovodev MCP docs.
23348
- * 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.
23349
23566
  */
23350
23567
  /**
23351
23568
  * Rovo Dev documents the per-server transport key as `transport`, with the
@@ -23371,11 +23588,7 @@ function lookupTransport(map, key) {
23371
23588
  return Object.hasOwn(map, key) ? map[key] : void 0;
23372
23589
  }
23373
23590
  function toRovodevServer(name, server, logger) {
23374
- const { type, transport, disabled, ...rest } = server;
23375
- if (disabled === true) {
23376
- logger?.warn(`Rovo Dev MCP: skipping "${name}" because it is disabled and mcp.json has no disable flag.`);
23377
- return null;
23378
- }
23591
+ const { type, transport, disabled: _disabled, ...rest } = server;
23379
23592
  const declared = typeof transport === "string" ? transport : typeof type === "string" ? type : void 0;
23380
23593
  if (declared === void 0) return rest;
23381
23594
  const mapped = lookupTransport(CANONICAL_TO_ROVODEV_TRANSPORT, declared);
@@ -23397,6 +23610,44 @@ function fromRovodevServer(server) {
23397
23610
  type: mapped
23398
23611
  };
23399
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
+ };
23400
23651
  var RovodevMcp = class RovodevMcp extends ToolMcp {
23401
23652
  json;
23402
23653
  constructor(params) {
@@ -23417,13 +23668,24 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23417
23668
  };
23418
23669
  }
23419
23670
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
23420
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23421
23671
  const paths = this.getSettablePaths({ global });
23422
23672
  const json = parseRovodevMcpJson(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath);
23423
23673
  const newJson = {
23424
23674
  ...json,
23425
23675
  mcpServers: json.mcpServers ?? {}
23426
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
+ }
23427
23689
  return new RovodevMcp({
23428
23690
  outputRoot,
23429
23691
  relativeDirPath: paths.relativeDirPath,
@@ -23434,11 +23696,21 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23434
23696
  });
23435
23697
  }
23436
23698
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
23437
- if (!global) throw new Error("Rovodev MCP is global-only; use --global to sync ~/.rovodev/mcp.json");
23438
23699
  const paths = this.getSettablePaths({ global });
23439
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
+ }
23440
23707
  const mcpServers = Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => {
23441
- 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);
23442
23714
  return converted === null ? null : [name, converted];
23443
23715
  }).filter((entry) => entry !== null));
23444
23716
  const rovodevConfig = {
@@ -23454,6 +23726,64 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
23454
23726
  global
23455
23727
  });
23456
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
+ }
23457
23787
  toRulesyncMcp() {
23458
23788
  const rawServers = isMcpServers(this.json.mcpServers) ? this.json.mcpServers : {};
23459
23789
  const mcpServers = Object.fromEntries(Object.entries(rawServers).filter(([, server]) => isPlainObject$1(server)).map(([name, server]) => [name, fromRovodevServer(server)]));
@@ -23602,7 +23932,6 @@ function transportOf(server) {
23602
23932
  case "http":
23603
23933
  case "streamable-http":
23604
23934
  case "ws": return "http";
23605
- default: break;
23606
23935
  }
23607
23936
  if (server.command !== void 0) return "stdio";
23608
23937
  if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
@@ -23630,6 +23959,20 @@ function deriveTransportAllowlist(servers) {
23630
23959
  return allowlist;
23631
23960
  }
23632
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
23633
23976
  //#region src/features/mcp/vibe-mcp.ts
23634
23977
  const VIBE_MCP_SERVER_FIELDS = [
23635
23978
  "transport",
@@ -23698,8 +24041,9 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23698
24041
  global
23699
24042
  });
23700
24043
  }
23701
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
24044
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger, global = false }) {
23702
24045
  const paths = this.getSettablePaths({ global });
24046
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
23703
24047
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
23704
24048
  const existingContent = await readFileContentOrNull(filePath) ?? "";
23705
24049
  const existingServers = normalizeMcpServersArray(parseSharedConfig({
@@ -23708,7 +24052,10 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23708
24052
  filePath
23709
24053
  }).mcp_servers);
23710
24054
  const existingByName = new Map(existingServers.map((server) => [server.name, server]));
23711
- 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));
23712
24059
  return new VibeMcp({
23713
24060
  outputRoot,
23714
24061
  relativeDirPath: paths.relativeDirPath,
@@ -23717,7 +24064,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
23717
24064
  fileKey: sharedConfigFileKey(paths),
23718
24065
  feature: "mcp",
23719
24066
  existingContent,
23720
- patch: { mcp_servers: mcpServers },
24067
+ patch: { mcp_servers: [...mcpServers, ...unmanagedServers] },
23721
24068
  filePath
23722
24069
  }),
23723
24070
  validate,
@@ -24419,10 +24766,19 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
24419
24766
  supportsDisabledTools: false
24420
24767
  }
24421
24768
  }],
24769
+ ["zoocode", {
24770
+ class: RooMcp,
24771
+ meta: {
24772
+ supportsProject: true,
24773
+ supportsGlobal: false,
24774
+ supportsEnabledTools: false,
24775
+ supportsDisabledTools: false
24776
+ }
24777
+ }],
24422
24778
  ["rovodev", {
24423
24779
  class: RovodevMcp,
24424
24780
  meta: {
24425
- supportsProject: false,
24781
+ supportsProject: true,
24426
24782
  supportsGlobal: true,
24427
24783
  supportsEnabledTools: false,
24428
24784
  supportsDisabledTools: false
@@ -25288,9 +25644,7 @@ function convertRulesyncToAntigravityCliPermissions(config) {
25288
25644
  case "ask":
25289
25645
  ask.push(entry);
25290
25646
  break;
25291
- case "deny":
25292
- deny.push(entry);
25293
- break;
25647
+ case "deny": deny.push(entry);
25294
25648
  }
25295
25649
  }
25296
25650
  }
@@ -25501,9 +25855,7 @@ function convertRulesyncToAntigravityIdePermissions(config) {
25501
25855
  case "ask":
25502
25856
  ask.push(entry);
25503
25857
  break;
25504
- case "deny":
25505
- deny.push(entry);
25506
- break;
25858
+ case "deny": deny.push(entry);
25507
25859
  }
25508
25860
  }
25509
25861
  }
@@ -26235,9 +26587,7 @@ function convertRulesyncToClaudePermissions({ config, logger }) {
26235
26587
  case "ask":
26236
26588
  ask.push(entry);
26237
26589
  break;
26238
- case "deny":
26239
- deny.push(entry);
26240
- break;
26590
+ case "deny": deny.push(entry);
26241
26591
  }
26242
26592
  }
26243
26593
  }
@@ -27487,9 +27837,7 @@ function convertRulesyncToCursorPermissions(config, logger) {
27487
27837
  case "ask":
27488
27838
  logger?.warn(`Cursor CLI permissions do not support the "ask" action. Skipping ${category} rule: ${pattern}`);
27489
27839
  break;
27490
- case "deny":
27491
- deny.push(entry);
27492
- break;
27840
+ case "deny": deny.push(entry);
27493
27841
  }
27494
27842
  }
27495
27843
  }
@@ -27726,9 +28074,7 @@ function convertRulesyncToDevinPermissions(config) {
27726
28074
  case "ask":
27727
28075
  ask.push(entry);
27728
28076
  break;
27729
- case "deny":
27730
- deny.push(entry);
27731
- break;
28077
+ case "deny": deny.push(entry);
27732
28078
  }
27733
28079
  }
27734
28080
  }
@@ -27916,10 +28262,7 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
27916
28262
  case "allow":
27917
28263
  allow.push(pattern);
27918
28264
  break;
27919
- case "deny":
27920
- deny.push(pattern);
27921
- break;
27922
- case "ask": break;
28265
+ case "deny": deny.push(pattern);
27923
28266
  }
27924
28267
  }
27925
28268
  return {
@@ -28038,10 +28381,11 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
28038
28381
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
28039
28382
  }
28040
28383
  const config = isRecord(parsed) ? { ...parsed } : {};
28041
- config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
28384
+ const userPermission = convertRulesyncToGoosePermissionConfig({
28042
28385
  config: rulesyncPermissions.getJson(),
28043
28386
  logger
28044
28387
  });
28388
+ config[GOOSE_USER_KEY] = userPermission;
28045
28389
  return new GoosePermissions({
28046
28390
  outputRoot,
28047
28391
  relativeDirPath: paths.relativeDirPath,
@@ -30259,9 +30603,7 @@ function convertRulesyncToQwenPermissions(config) {
30259
30603
  case "ask":
30260
30604
  ask.push(entry);
30261
30605
  break;
30262
- case "deny":
30263
- deny.push(entry);
30264
- break;
30606
+ case "deny": deny.push(entry);
30265
30607
  }
30266
30608
  }
30267
30609
  }
@@ -30334,6 +30676,20 @@ function toCanonicalToolName$2(reasonixName) {
30334
30676
  return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
30335
30677
  }
30336
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
+ /**
30337
30693
  * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
30338
30694
  * If no parentheses, returns the tool name with "*" as the pattern.
30339
30695
  */
@@ -30422,10 +30778,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30422
30778
  const config = rulesyncPermissions.getJson();
30423
30779
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
30424
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
+ ]);
30425
30790
  const existingPermissions = toReasonixTable(parsed.permissions);
30426
- const preservedAllow = toReasonixStringArray(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30427
- const preservedAsk = toReasonixStringArray(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
30428
- 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
+ });
30429
30797
  if (logger && managedToolNames.has("Read")) {
30430
30798
  const droppedReadDenyEntries = toReasonixStringArray(existingPermissions.deny).filter((entry) => {
30431
30799
  const { toolName } = parseReasonixPermissionEntry(entry);
@@ -30434,17 +30802,22 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30434
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.`);
30435
30803
  }
30436
30804
  const mergedPermissions = { ...existingPermissions };
30437
- const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
30438
- const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
30439
- const mergedDeny = uniq([...preservedDeny, ...deny].toSorted());
30440
- if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
30441
- else delete mergedPermissions.allow;
30442
- if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
30443
- else delete mergedPermissions.ask;
30444
- if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
30445
- 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
+ ]);
30446
30820
  const patch = { permissions: mergedPermissions };
30447
- const override = config.reasonix;
30448
30821
  if (override?.sandbox !== void 0) patch.sandbox = {
30449
30822
  ...asReasonixRecord(parsed.sandbox),
30450
30823
  ...asReasonixRecord(override.sandbox)
@@ -30475,16 +30848,29 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30475
30848
  }
30476
30849
  toRulesyncPermissions() {
30477
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));
30478
30861
  const config = convertReasonixToRulesyncPermissions({
30479
- allow: toReasonixStringArray(permissions.allow),
30480
- ask: toReasonixStringArray(permissions.ask),
30481
- deny: toReasonixStringArray(permissions.deny)
30862
+ allow: allowSplit.translated,
30863
+ ask: askSplit.translated,
30864
+ deny: denySplit.translated
30482
30865
  });
30483
30866
  const sandbox = asReasonixRecord(this.toml.sandbox);
30484
30867
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
30485
30868
  const reasonixOverride = {};
30486
30869
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
30487
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;
30488
30874
  const result = { ...config };
30489
30875
  if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
30490
30876
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -30514,6 +30900,41 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
30514
30900
  }
30515
30901
  };
30516
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
+ /**
30517
30938
  * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
30518
30939
  */
30519
30940
  function convertRulesyncToReasonixPermissions(config) {
@@ -30531,9 +30952,7 @@ function convertRulesyncToReasonixPermissions(config) {
30531
30952
  case "ask":
30532
30953
  ask.push(entry);
30533
30954
  break;
30534
- case "deny":
30535
- deny.push(entry);
30536
- break;
30955
+ case "deny": deny.push(entry);
30537
30956
  }
30538
30957
  }
30539
30958
  }
@@ -30563,7 +30982,6 @@ function convertReasonixToRulesyncPermissions(params) {
30563
30982
  }
30564
30983
  //#endregion
30565
30984
  //#region src/features/permissions/rovodev-permissions.ts
30566
- const ROVODEV_GLOBAL_ONLY_MESSAGE = "Rovodev permissions are global-only; use --global to sync ~/.rovodev/config.yml";
30567
30985
  const CATCH_ALL_PATTERN$1 = "*";
30568
30986
  const CATEGORY_TO_TOOL_KEYS = {
30569
30987
  read: [
@@ -30623,9 +31041,12 @@ const OWNED_TOOL_PERMISSION_KEYS = [
30623
31041
  /**
30624
31042
  * Permissions adapter for Rovo Dev CLI.
30625
31043
  *
30626
- * Rovo Dev reads tool permissions from the `toolPermissions` block of the global
30627
- * `~/.rovodev/config.yml`. This surface is **global only** there is no
30628
- * 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.
30629
31050
  *
30630
31051
  * Rovo Dev's three levels (`allow`/`ask`/`deny`) are an exact 1:1 with rulesync's
30631
31052
  * canonical action enum, so action values pass through verbatim.
@@ -30672,7 +31093,6 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30672
31093
  };
30673
31094
  }
30674
31095
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
30675
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30676
31096
  const paths = RovodevPermissions.getSettablePaths({ global });
30677
31097
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
30678
31098
  return new RovodevPermissions({
@@ -30681,11 +31101,10 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30681
31101
  relativeFilePath: paths.relativeFilePath,
30682
31102
  fileContent,
30683
31103
  validate,
30684
- global: true
31104
+ global
30685
31105
  });
30686
31106
  }
30687
31107
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
30688
- if (!global) throw new Error(ROVODEV_GLOBAL_ONLY_MESSAGE);
30689
31108
  const paths = RovodevPermissions.getSettablePaths({ global });
30690
31109
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30691
31110
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -30708,14 +31127,20 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
30708
31127
  filePath,
30709
31128
  logger
30710
31129
  });
30711
- 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);
30712
31137
  return new RovodevPermissions({
30713
31138
  outputRoot,
30714
31139
  relativeDirPath: paths.relativeDirPath,
30715
31140
  relativeFilePath: paths.relativeFilePath,
30716
- fileContent: dump(config),
31141
+ fileContent,
30717
31142
  validate: true,
30718
- global: true
31143
+ global
30719
31144
  });
30720
31145
  }
30721
31146
  toRulesyncPermissions() {
@@ -31252,6 +31677,7 @@ const CANONICAL_TO_VIBE_TOOL_NAMES = {
31252
31677
  write: "write_file",
31253
31678
  webfetch: "web_fetch",
31254
31679
  websearch: "web_search",
31680
+ grep: "grep",
31255
31681
  agent: "task"
31256
31682
  };
31257
31683
  const VIBE_TO_CANONICAL_TOOL_NAMES = {
@@ -31261,6 +31687,7 @@ const VIBE_TO_CANONICAL_TOOL_NAMES = {
31261
31687
  write_file: "write",
31262
31688
  web_fetch: "webfetch",
31263
31689
  web_search: "websearch",
31690
+ grep: "grep",
31264
31691
  task: "agent"
31265
31692
  };
31266
31693
  var VibePermissions = class VibePermissions extends ToolPermissions {
@@ -31298,6 +31725,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31298
31725
  }
31299
31726
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
31300
31727
  const paths = this.getSettablePaths({ global });
31728
+ if (global) await warnIfGlobalVibeConfigIsShadowed(logger);
31301
31729
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
31302
31730
  const existingContent = await readFileContentOrNull(filePath) ?? "";
31303
31731
  const config = parseVibeConfig(existingContent);
@@ -31305,46 +31733,31 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31305
31733
  const tools = toVibeToolsRecord(config.tools);
31306
31734
  const enabledTools = new Set(toStringArray(config.enabled_tools));
31307
31735
  const disabledTools = new Set(toStringArray(config.disabled_tools));
31736
+ const removedEnabledTools = [];
31308
31737
  for (const category of Object.keys(permission)) {
31738
+ if (!hasVibeToolName(category)) continue;
31309
31739
  const vibeToolName = toVibeToolName(category);
31310
- enabledTools.delete(vibeToolName);
31740
+ if (enabledTools.delete(vibeToolName)) removedEnabledTools.push(vibeToolName);
31311
31741
  disabledTools.delete(vibeToolName);
31312
31742
  }
31313
31743
  for (const [category, rules] of Object.entries(permission)) {
31314
- const vibeToolName = toVibeToolName(category);
31315
- const existingTool = toVibeToolConfig(tools[vibeToolName]);
31316
- const nextTool = { ...existingTool };
31317
- const allow = new Set(toStringArray(existingTool.allow ?? existingTool.allowlist));
31318
- const deny = new Set(toStringArray(existingTool.deny ?? existingTool.denylist));
31319
- for (const [pattern, action] of Object.entries(rules)) {
31320
- if (pattern === "*") {
31321
- applyWildcardPermission({
31322
- action,
31323
- toolConfig: nextTool
31324
- });
31325
- if (action === "deny") {
31326
- disabledTools.add(vibeToolName);
31327
- enabledTools.delete(vibeToolName);
31328
- } else if (action === "allow") {
31329
- enabledTools.add(vibeToolName);
31330
- disabledTools.delete(vibeToolName);
31331
- }
31332
- continue;
31333
- }
31334
- if (action === "ask") {
31335
- logger?.warn(`Vibe permissions do not support pattern-level "ask" rules. Skipping ${category}: ${pattern}`);
31336
- continue;
31337
- }
31338
- if (action === "allow") allow.add(pattern);
31339
- 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;
31340
31748
  }
31341
- delete nextTool.allow;
31342
- delete nextTool.deny;
31343
- if (allow.size > 0) nextTool.allowlist = [...allow].toSorted();
31344
- if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
31345
- tools[vibeToolName] = nextTool;
31749
+ applyCategoryRules({
31750
+ category,
31751
+ rules,
31752
+ tools,
31753
+ enabledTools,
31754
+ disabledTools,
31755
+ logger
31756
+ });
31346
31757
  }
31347
- 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.`);
31348
31761
  return new VibePermissions({
31349
31762
  outputRoot,
31350
31763
  relativeDirPath: paths.relativeDirPath,
@@ -31355,7 +31768,10 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31355
31768
  existingContent,
31356
31769
  patch: {
31357
31770
  tools,
31358
- enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
31771
+ enabled_tools: resolveEnabledToolsPatch({
31772
+ vibeOverride,
31773
+ enabledTools
31774
+ }),
31359
31775
  disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
31360
31776
  },
31361
31777
  filePath
@@ -31367,7 +31783,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31367
31783
  toRulesyncPermissions() {
31368
31784
  const permission = {};
31369
31785
  const vibeOverridePermission = {};
31370
- for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
31786
+ const enabledToolsList = toStringArray(this.toml.enabled_tools);
31371
31787
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
31372
31788
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
31373
31789
  const category = toCanonicalToolName$1(vibeToolName);
@@ -31380,9 +31796,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31380
31796
  if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
31381
31797
  }
31382
31798
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
31383
- 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 ? {
31384
31803
  permission,
31385
- vibe: { permission: vibeOverridePermission }
31804
+ vibe: vibeOverride
31386
31805
  } : { permission };
31387
31806
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
31388
31807
  }
@@ -31417,8 +31836,12 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
31417
31836
  * list for any category the override names: a present list is set, an empty
31418
31837
  * one clears it. Categories not named keep whatever the existing file had.
31419
31838
  */
31420
- function applyVibeSensitivePatterns(tools, vibeOverride) {
31839
+ function applyVibeSensitivePatterns(tools, vibeOverride, logger) {
31421
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
+ }
31422
31845
  const vibeToolName = toVibeToolName(category);
31423
31846
  const nextTool = toVibeToolConfig(tools[vibeToolName]);
31424
31847
  const patterns = toStringArray(toolOverride.sensitive_patterns);
@@ -31427,6 +31850,57 @@ function applyVibeSensitivePatterns(tools, vibeOverride) {
31427
31850
  tools[vibeToolName] = nextTool;
31428
31851
  }
31429
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
+ }
31430
31904
  function parseVibeConfig(fileContent) {
31431
31905
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
31432
31906
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -31435,6 +31909,17 @@ function parseVibeConfig(fileContent) {
31435
31909
  function toVibeToolName(category) {
31436
31910
  return CANONICAL_TO_VIBE_TOOL_NAMES[category] ?? category;
31437
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
+ }
31438
31923
  function toCanonicalToolName$1(vibeToolName) {
31439
31924
  return VIBE_TO_CANONICAL_TOOL_NAMES[vibeToolName] ?? vibeToolName;
31440
31925
  }
@@ -31452,7 +31937,7 @@ function toStringArray(value) {
31452
31937
  function applyWildcardPermission({ action, toolConfig }) {
31453
31938
  if (action === "allow") toolConfig.permission = "always";
31454
31939
  else if (action === "ask") toolConfig.permission = "ask";
31455
- else if (toolConfig.permission !== void 0) delete toolConfig.permission;
31940
+ else toolConfig.permission = "never";
31456
31941
  }
31457
31942
  function fromVibePermission(value) {
31458
31943
  if (value === "always" || value === "allow") return "allow";
@@ -31480,6 +31965,20 @@ const WARP_OVERRIDE_KEYS = [
31480
31965
  "agent_mode_coding_file_read_allowlist",
31481
31966
  "agent_mode_execute_readonly_commands"
31482
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
+ ];
31483
31982
  /**
31484
31983
  * Warp's `settings.toml` lives in a different directory per platform (Stable
31485
31984
  * channel). The home directory is resolved by the processor through
@@ -31534,7 +32033,14 @@ function warpSettingsDir() {
31534
32033
  * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
31535
32034
  * on **import** they are lifted from `settings.toml` into the override, and on
31536
32035
  * **export** they are merged back into `[agents.profiles]` (the override wins).
31537
- * 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.
31538
32044
  *
31539
32045
  * The `settings.toml` file holds all of Warp's settings, so the
31540
32046
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -31587,7 +32093,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31587
32093
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
31588
32094
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
31589
32095
  const override = config.warp;
31590
- 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
+ }
31591
32101
  const mergedAllow = uniq(allow.toSorted());
31592
32102
  const mergedDeny = uniq(deny.toSorted());
31593
32103
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -31595,18 +32105,13 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31595
32105
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31596
32106
  else delete profiles[DENYLIST_KEY];
31597
32107
  agents.profiles = profiles;
31598
- if (isRecord(agents[EXECUTION_PROFILES_KEY])) {
31599
- const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
31600
- const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
31601
- if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
31602
- else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
31603
- if (mergedDeny.length > 0) defaultProfile[PROFILE_DENYLIST_KEY] = mergedDeny;
31604
- else delete defaultProfile[PROFILE_DENYLIST_KEY];
31605
- executionProfiles[DEFAULT_PROFILE_KEY] = defaultProfile;
31606
- agents[EXECUTION_PROFILES_KEY] = executionProfiles;
31607
- const otherProfileIds = Object.keys(executionProfiles).filter((id) => id !== DEFAULT_PROFILE_KEY);
31608
- if (mergedDeny.length > 0 && otherProfileIds.length > 0 && logger) logger.warn(`Warp command deny rules were written to the 'default' execution profile only; they are not enforced while another profile (${otherProfileIds.join(", ")}) is active.`);
31609
- }
32108
+ mergeIntoDefaultExecutionProfile({
32109
+ agents,
32110
+ mergedAllow,
32111
+ mergedDeny,
32112
+ executionProfileOverride,
32113
+ logger
32114
+ });
31610
32115
  settings.agents = agents;
31611
32116
  return new WarpPermissions({
31612
32117
  outputRoot,
@@ -31634,6 +32139,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31634
32139
  });
31635
32140
  const warpOverride = {};
31636
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
+ }
31637
32147
  const result = { ...config };
31638
32148
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
31639
32149
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -31656,6 +32166,36 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31656
32166
  }
31657
32167
  };
31658
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
+ /**
31659
32199
  * Convert rulesync permissions config to Warp command allow/deny regex lists.
31660
32200
  * Only the `bash` category maps; `ask` rules and non-`bash` categories are
31661
32201
  * dropped (the latter with a warning when they carry `deny` rules).
@@ -31672,10 +32212,7 @@ function convertRulesyncToWarpPermissions({ config, logger }) {
31672
32212
  case "allow":
31673
32213
  allow.push(pattern);
31674
32214
  break;
31675
- case "deny":
31676
- deny.push(pattern);
31677
- break;
31678
- case "ask": break;
32215
+ case "deny": deny.push(pattern);
31679
32216
  }
31680
32217
  }
31681
32218
  return {
@@ -32130,7 +32667,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
32130
32667
  ["rovodev", {
32131
32668
  class: RovodevPermissions,
32132
32669
  meta: {
32133
- supportsProject: false,
32670
+ supportsProject: true,
32134
32671
  supportsGlobal: true,
32135
32672
  supportsImport: true
32136
32673
  }
@@ -33327,6 +33864,38 @@ var AugmentcodeSkill = class AugmentcodeSkill extends ToolSkill {
33327
33864
  }
33328
33865
  };
33329
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
33330
33899
  //#region src/features/skills/claudecode-skill.ts
33331
33900
  const ClaudecodeSkillFrontmatterSchema = z.looseObject({
33332
33901
  name: z.string(),
@@ -33412,6 +33981,41 @@ var ClaudecodeSkill = class extends ToolSkill {
33412
33981
  alternativeSkillRoots: [CLAUDECODE_SCHEDULED_TASKS_DIR_PATH]
33413
33982
  };
33414
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
+ }
33415
34019
  getFrontmatter() {
33416
34020
  return ClaudecodeSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
33417
34021
  }
@@ -36810,7 +37414,8 @@ var TaktSkill = class TaktSkill extends ToolSkill {
36810
37414
  getDirPath() {
36811
37415
  const fullPath = join(this.outputRoot, this.relativeDirPath);
36812
37416
  const resolvedFull = resolve(fullPath);
36813
- const rel = relative(resolve(this.outputRoot), resolvedFull);
37417
+ const resolvedBase = resolve(this.outputRoot);
37418
+ const rel = relative(resolvedBase, resolvedFull);
36814
37419
  if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`Path traversal detected: Final path escapes outputRoot. outputRoot="${this.outputRoot}", relativeDirPath="${this.relativeDirPath}"`);
36815
37420
  return fullPath;
36816
37421
  }
@@ -37360,6 +37965,23 @@ var ZedSkill = class ZedSkill extends ToolSkill {
37360
37965
  }
37361
37966
  };
37362
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
37363
37985
  //#region src/features/skills/skills-processor.ts
37364
37986
  const SkillsProcessorToolTargetSchema = z.enum(skillsProcessorToolTargetTuple);
37365
37987
  /**
@@ -37632,6 +38254,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37632
38254
  supportsGlobal: true
37633
38255
  }
37634
38256
  }],
38257
+ ["zoocode", {
38258
+ class: ZoocodeSkill,
38259
+ meta: {
38260
+ supportsProject: true,
38261
+ supportsSimulated: false,
38262
+ supportsGlobal: true
38263
+ }
38264
+ }],
37635
38265
  ["rovodev", {
37636
38266
  class: RovodevSkill,
37637
38267
  meta: {
@@ -37794,7 +38424,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37794
38424
  for (const root of roots) {
37795
38425
  const rootOutputRoot = typeof root === "string" ? this.outputRoot : root.outputRoot;
37796
38426
  const relativeDirPath = typeof root === "string" ? root : root.relativeDirPath;
37797
- const isConfiguredRoot = configuredRootPaths.has(relativeDirPath);
38427
+ const isLenientRoot = configuredRootPaths.has(relativeDirPath) || factory.meta.lenientImport === true || isAgentSkillsInteropRoot(relativeDirPath);
37798
38428
  const skillsDirPath = join(rootOutputRoot, relativeDirPath);
37799
38429
  if (!await directoryExists(skillsDirPath)) continue;
37800
38430
  const dirPaths = await findFilesByGlobs(join(skillsDirPath, "*"), { type: "dir" });
@@ -37818,7 +38448,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37818
38448
  global: this.global
37819
38449
  });
37820
38450
  } catch (error) {
37821
- if (!isConfiguredRoot && !factory.meta.lenientImport) throw error;
38451
+ if (!isLenientRoot) throw error;
37822
38452
  this.logger.warn(`Skipping ${join(relativeDirPath, dirName)}: ${formatError(error)}`);
37823
38453
  return null;
37824
38454
  }
@@ -37833,12 +38463,20 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37833
38463
  const fromFlatFile = factory.class.fromFlatFile;
37834
38464
  const directoryStems = new Set(ownedDirNames);
37835
38465
  const flatFilePaths = (await findFilesByGlobs(join(skillsDirPath, "*.md"), { type: "file" })).filter((filePath) => !directoryStems.has(basename(filePath, ".md")));
37836
- const flatSkills = await Promise.all(flatFilePaths.map((filePath) => fromFlatFile({
37837
- outputRoot: rootOutputRoot,
37838
- relativeDirPath,
37839
- relativeFilePath: basename(filePath),
37840
- global: this.global
37841
- })));
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);
37842
38480
  for (const skill of flatSkills) {
37843
38481
  const skillName = skill.getImportIdentity();
37844
38482
  if (seenSkillNames.has(skillName)) continue;
@@ -37911,14 +38549,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37911
38549
  */
37912
38550
  static getSimulatedConventionSection({ skillList }) {
37913
38551
  if (!skillList || skillList.length === 0) return "";
38552
+ const skillListWithAtPrefix = skillList.map((skill) => ({
38553
+ ...skill,
38554
+ path: `@${skill.path}`
38555
+ }));
37914
38556
  return `## Simulated Skills
37915
38557
 
37916
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.
37917
38559
 
37918
- ${encode({ skillList: skillList.map((skill) => ({
37919
- ...skill,
37920
- path: `@${skill.path}`
37921
- })) })}`;
38560
+ ${encode({ skillList: skillListWithAtPrefix })}`;
37922
38561
  }
37923
38562
  /**
37924
38563
  * Return the tool targets that this processor supports in global mode
@@ -40522,7 +41161,8 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
40522
41161
  return getHermesagentSharedConfigWritePaths();
40523
41162
  }
40524
41163
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
40525
- 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`)];
40526
41166
  }
40527
41167
  toRulesyncSubagent() {
40528
41168
  const slug = basename(this.getRelativeFilePath(), ".json");
@@ -41757,7 +42397,7 @@ function stringifyRooModes(modes) {
41757
42397
  *
41758
42398
  * @see https://roocodeinc.github.io/Roo-Code/features/custom-modes
41759
42399
  */
41760
- var RooSubagent = class RooSubagent extends ToolSubagent {
42400
+ var RooSubagent = class extends ToolSubagent {
41761
42401
  modes;
41762
42402
  constructor({ modes, ...rest }) {
41763
42403
  if (rest.validate !== false) {
@@ -41813,7 +42453,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41813
42453
  const mode = this.toRooMode(rulesyncSubagent);
41814
42454
  bySlug.set(mode.slug, mode);
41815
42455
  }
41816
- return new RooSubagent({
42456
+ return new this({
41817
42457
  outputRoot,
41818
42458
  relativeDirPath: this.getSettablePaths().relativeDirPath,
41819
42459
  relativeFilePath: ROO_MODES_FILE_NAME,
@@ -41892,7 +42532,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41892
42532
  }
41893
42533
  const result = RooModesFileSchema.safeParse(parsed ?? { customModes: [] });
41894
42534
  if (!result.success) throw new Error(`Invalid .roomodes in ${filePath}: ${formatError(result.error)}`);
41895
- return new RooSubagent({
42535
+ return new this({
41896
42536
  outputRoot,
41897
42537
  relativeDirPath: paths.relativeDirPath,
41898
42538
  relativeFilePath: basename(relativeFilePath),
@@ -41901,7 +42541,7 @@ var RooSubagent = class RooSubagent extends ToolSubagent {
41901
42541
  });
41902
42542
  }
41903
42543
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
41904
- return new RooSubagent({
42544
+ return new this({
41905
42545
  outputRoot,
41906
42546
  relativeDirPath,
41907
42547
  relativeFilePath,
@@ -42147,6 +42787,60 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
42147
42787
  }
42148
42788
  };
42149
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
42150
42844
  //#region src/features/subagents/subagents-processor.ts
42151
42845
  const SubagentsProcessorToolTargetSchema = z.enum(subagentsProcessorToolTargetTuple);
42152
42846
  /**
@@ -42386,6 +43080,14 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
42386
43080
  filePattern: ".roomodes"
42387
43081
  }
42388
43082
  }],
43083
+ ["zoocode", {
43084
+ class: ZoocodeSubagent,
43085
+ meta: {
43086
+ supportsSimulated: false,
43087
+ supportsGlobal: false,
43088
+ filePattern: ".roomodes"
43089
+ }
43090
+ }],
42389
43091
  ["rovodev", {
42390
43092
  class: RovodevSubagent,
42391
43093
  meta: {
@@ -42646,11 +43348,13 @@ For example, if the user instructs \`Call planner subagent to plan the refactori
42646
43348
  //#region src/features/rules/tool-rule.ts
42647
43349
  var ToolRule = class extends ToolFile {
42648
43350
  root;
43351
+ localRoot;
42649
43352
  description;
42650
43353
  globs;
42651
- constructor({ root = false, description, globs, ...rest }) {
43354
+ constructor({ root = false, localRoot = false, description, globs, ...rest }) {
42652
43355
  super(rest);
42653
43356
  this.root = root;
43357
+ this.localRoot = localRoot;
42654
43358
  this.description = description;
42655
43359
  this.globs = globs;
42656
43360
  }
@@ -42733,6 +43437,38 @@ var ToolRule = class extends ToolFile {
42733
43437
  isRoot() {
42734
43438
  return this.root;
42735
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
+ }
42736
43472
  /**
42737
43473
  * Whether this rule must be left out of the root rule's reference/MCP
42738
43474
  * instruction listings even though it is a non-root survivor. Used by files
@@ -42765,31 +43501,6 @@ function buildToolPath(toolDir, subDir, excludeToolDir) {
42765
43501
  }
42766
43502
  //#endregion
42767
43503
  //#region src/features/rules/agentsmd-rule.ts
42768
- /**
42769
- * Dependency trees never scanned for nested `AGENTS.md` files, at any depth. An
42770
- * `AGENTS.md` there describes somebody else's project, and neither name is ever
42771
- * a package name. Hidden directories are excluded separately, because an
42772
- * `AGENTS.md` inside one is another tool's generated output (rulesync writes
42773
- * several itself).
42774
- */
42775
- const NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH = ["node_modules", "__pycache__"];
42776
- /**
42777
- * Build, vendoring and scratch directories, excluded at the **project root
42778
- * only**. A top-level `build/` is a build directory; `packages/build/` is a
42779
- * package, and dropping it silently would lose a real subproject.
42780
- */
42781
- const NESTED_SCAN_EXCLUDED_ROOT_DIRS = [
42782
- "vendor",
42783
- "third_party",
42784
- "dist",
42785
- "build",
42786
- "out",
42787
- "target",
42788
- "coverage",
42789
- "tmp",
42790
- "temp",
42791
- "venv"
42792
- ];
42793
43504
  var AgentsMdRule = class AgentsMdRule extends ToolRule {
42794
43505
  constructor({ fileContent, root, ...rest }) {
42795
43506
  super({
@@ -42846,7 +43557,8 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
42846
43557
  const normalizedDirPath = relativeDirPath === void 0 ? "." : toPosixPath(relativeDirPath);
42847
43558
  const isNested = relativeFilePath === "AGENTS.md" && normalizedDirPath !== "." && normalizedDirPath !== "" && !normalizedDirPath.startsWith(".");
42848
43559
  const isRoot = !isNested && relativeFilePath === "AGENTS.md" && (normalizedDirPath === "." || normalizedDirPath === "");
42849
- 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));
42850
43562
  return new AgentsMdRule({
42851
43563
  outputRoot,
42852
43564
  relativeDirPath: isNested ? join(normalizedDirPath) : isRoot ? this.getSettablePaths().root.relativeDirPath : this.getSettablePaths().nonRoot.relativeDirPath,
@@ -43153,7 +43865,8 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
43153
43865
  });
43154
43866
  }
43155
43867
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43156
- 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));
43157
43870
  return new AntigravityCliRule({
43158
43871
  outputRoot,
43159
43872
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43614,7 +44327,8 @@ var AugmentcodeLegacyRule = class AugmentcodeLegacyRule extends ToolRule {
43614
44327
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
43615
44328
  const settablePaths = this.getSettablePaths();
43616
44329
  const isRoot = relativeFilePath === settablePaths.root.relativeFilePath;
43617
- 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));
43618
44332
  return new AugmentcodeLegacyRule({
43619
44333
  outputRoot,
43620
44334
  relativeDirPath: isRoot ? settablePaths.root.relativeDirPath : settablePaths.nonRoot.relativeDirPath,
@@ -43838,7 +44552,8 @@ var ClaudecodeLegacyRule = class ClaudecodeLegacyRule extends ToolRule {
43838
44552
  });
43839
44553
  }
43840
44554
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43841
- 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));
43842
44557
  return new ClaudecodeLegacyRule({
43843
44558
  outputRoot,
43844
44559
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -43959,7 +44674,8 @@ var ClaudecodeRule = class ClaudecodeRule extends ToolRule {
43959
44674
  });
43960
44675
  }
43961
44676
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
43962
- const filePath = join(outputRoot, join(paths.nonRoot.relativeDirPath, relativeFilePath));
44677
+ const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
44678
+ const filePath = join(outputRoot, relativePath);
43963
44679
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
43964
44680
  const result = ClaudecodeRuleFrontmatterSchema.safeParse(frontmatter);
43965
44681
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44346,7 +45062,8 @@ var CodexcliRule = class CodexcliRule extends ToolRule {
44346
45062
  }
44347
45063
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44348
45064
  const { root } = this.getSettablePaths({ global });
44349
- 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));
44350
45067
  return new CodexcliRule({
44351
45068
  outputRoot,
44352
45069
  relativeDirPath: root.relativeDirPath,
@@ -44515,7 +45232,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44515
45232
  }) : relativeFilePath === paths.root.relativeFilePath;
44516
45233
  const resolvedRelativeDirPath = relativeDirPath ?? (isRoot ? paths.root.relativeDirPath : paths.nonRoot?.relativeDirPath ?? paths.root.relativeDirPath);
44517
45234
  if (isRoot) {
44518
- 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));
44519
45237
  return new CopilotRule({
44520
45238
  outputRoot,
44521
45239
  relativeDirPath: paths.root.relativeDirPath,
@@ -44527,7 +45245,8 @@ var CopilotRule = class CopilotRule extends ToolRule {
44527
45245
  });
44528
45246
  }
44529
45247
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
44530
- const filePath = join(outputRoot, join(resolvedRelativeDirPath, relativeFilePath));
45248
+ const relativePath = join(resolvedRelativeDirPath, relativeFilePath);
45249
+ const filePath = join(outputRoot, relativePath);
44531
45250
  const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
44532
45251
  const result = CopilotRuleFrontmatterSchema.safeParse(frontmatter);
44533
45252
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
@@ -44799,7 +45518,8 @@ var DeepagentsRule = class DeepagentsRule extends ToolRule {
44799
45518
  }
44800
45519
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
44801
45520
  const settablePaths = this.getSettablePaths({ global });
44802
- 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));
44803
45523
  return new DeepagentsRule({
44804
45524
  outputRoot,
44805
45525
  relativeDirPath: settablePaths.root.relativeDirPath,
@@ -45226,7 +45946,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45226
45946
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
45227
45947
  const paths = this.getSettablePaths({ global });
45228
45948
  if (relativeFilePath === paths.root.relativeFilePath) {
45229
- 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));
45230
45951
  return new FactorydroidRule({
45231
45952
  outputRoot,
45232
45953
  relativeDirPath: paths.root.relativeDirPath,
@@ -45237,7 +45958,8 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
45237
45958
  });
45238
45959
  }
45239
45960
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45240
- 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));
45241
45963
  return new FactorydroidRule({
45242
45964
  outputRoot,
45243
45965
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -45467,7 +46189,8 @@ var HermesagentRule = class HermesagentRule extends ToolRule {
45467
46189
  }
45468
46190
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
45469
46191
  const { root } = this.getSettablePaths();
45470
- 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));
45471
46194
  return new HermesagentRule({
45472
46195
  outputRoot,
45473
46196
  relativeDirPath: root.relativeDirPath,
@@ -45578,7 +46301,8 @@ var JunieRule = class JunieRule extends ToolRule {
45578
46301
  });
45579
46302
  }
45580
46303
  const relativeDirPath = this.getSettablePaths().root.relativeDirPath;
45581
- const fileContent = await readFileContent(join(outputRoot, join(relativeDirPath, relativeFilePath)));
46304
+ const relativePath = join(relativeDirPath, relativeFilePath);
46305
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
45582
46306
  return new JunieRule({
45583
46307
  outputRoot,
45584
46308
  relativeDirPath,
@@ -45678,7 +46402,8 @@ var KiloRule = class KiloRule extends ToolRule {
45678
46402
  });
45679
46403
  }
45680
46404
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
45681
- 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));
45682
46407
  return new KiloRule({
45683
46408
  outputRoot,
45684
46409
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46015,10 +46740,13 @@ var KiroIdeRule = class extends KiroRule {
46015
46740
  //#region src/features/rules/opencode-rule.ts
46016
46741
  var OpenCodeRule = class OpenCodeRule extends ToolRule {
46017
46742
  static getSettablePaths({ global, excludeToolDir } = {}) {
46018
- if (global) return { root: {
46019
- relativeDirPath: buildToolPath(OPENCODE_GLOBAL_DIR, ".", excludeToolDir),
46020
- relativeFilePath: OPENCODE_RULE_FILE_NAME
46021
- } };
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
+ };
46022
46750
  return {
46023
46751
  root: {
46024
46752
  relativeDirPath: ".",
@@ -46028,7 +46756,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46028
46756
  };
46029
46757
  }
46030
46758
  static getExtraSharedWritePaths({ global = false } = {}) {
46031
- return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
46759
+ return [OpencodeMcp.getSettablePaths({ global })];
46032
46760
  }
46033
46761
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46034
46762
  const paths = this.getSettablePaths({ global });
@@ -46045,7 +46773,8 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
46045
46773
  });
46046
46774
  }
46047
46775
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
46048
- 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));
46049
46778
  return new OpenCodeRule({
46050
46779
  outputRoot,
46051
46780
  relativeDirPath: paths.nonRoot.relativeDirPath,
@@ -46160,7 +46889,8 @@ var PiRule = class PiRule extends ToolRule {
46160
46889
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
46161
46890
  const { root, appendSystemPrompt } = this.getSettablePaths({ global });
46162
46891
  if (relativeFilePath === "APPEND_SYSTEM.md") {
46163
- 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));
46164
46894
  return new PiRule({
46165
46895
  outputRoot,
46166
46896
  relativeDirPath: appendSystemPrompt.relativeDirPath,
@@ -46171,7 +46901,8 @@ var PiRule = class PiRule extends ToolRule {
46171
46901
  appendSystemPrompt: true
46172
46902
  });
46173
46903
  }
46174
- 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));
46175
46906
  return new PiRule({
46176
46907
  outputRoot,
46177
46908
  relativeDirPath: root.relativeDirPath,
@@ -46471,7 +47202,7 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46471
47202
  * The personal local context file lives under `.qwen/`, not at the project
46472
47203
  * root where the settable root path points, so override the deletion glob.
46473
47204
  */
46474
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47205
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46475
47206
  return join(outputRoot, QWENCODE_DIR, fileName);
46476
47207
  }
46477
47208
  };
@@ -46491,9 +47222,51 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46491
47222
  relativeFilePath: REASONIX_RULE_FILE_NAME
46492
47223
  } };
46493
47224
  }
46494
- 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 }) {
46495
47256
  const { root } = this.getSettablePaths({ global });
46496
- 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));
46497
47270
  return new ReasonixRule({
46498
47271
  outputRoot,
46499
47272
  relativeDirPath: root.relativeDirPath,
@@ -46505,7 +47278,17 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46505
47278
  }
46506
47279
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
46507
47280
  const { root } = this.getSettablePaths({ global });
46508
- 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
+ });
46509
47292
  return new ReasonixRule({
46510
47293
  outputRoot,
46511
47294
  relativeDirPath: root.relativeDirPath,
@@ -46516,7 +47299,22 @@ var ReasonixRule = class ReasonixRule extends ToolRule {
46516
47299
  });
46517
47300
  }
46518
47301
  toRulesyncRule() {
46519
- 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
+ });
46520
47318
  }
46521
47319
  validate() {
46522
47320
  return {
@@ -46686,7 +47484,7 @@ var RooRule = class RooRule extends ToolRule {
46686
47484
  * Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
46687
47485
  * at the project root, not under `.roo/` (mirrors rovodev).
46688
47486
  */
46689
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47487
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46690
47488
  return join(outputRoot, fileName);
46691
47489
  }
46692
47490
  };
@@ -46818,19 +47616,6 @@ var RovodevRule = class RovodevRule extends ToolRule {
46818
47616
  }));
46819
47617
  }
46820
47618
  toRulesyncRule() {
46821
- if (this.getRelativeFilePath() === "AGENTS.local.md") return new RulesyncRule({
46822
- outputRoot: this.getOutputRoot(),
46823
- relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
46824
- relativeFilePath: ROVODEV_LEGACY_RULE_FILE_NAME,
46825
- frontmatter: {
46826
- targets: ["rovodev"],
46827
- root: false,
46828
- localRoot: true,
46829
- globs: []
46830
- },
46831
- body: this.getFileContent(),
46832
- validate: true
46833
- });
46834
47619
  if (!this.isRoot()) return new RulesyncRule({
46835
47620
  outputRoot: this.getOutputRoot(),
46836
47621
  relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
@@ -46887,7 +47672,7 @@ var RovodevRule = class RovodevRule extends ToolRule {
46887
47672
  };
46888
47673
  }
46889
47674
  /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
46890
- static getLocalRootDeletionGlob({ outputRoot, fileName }) {
47675
+ static getLocalRootFileGlob({ outputRoot, fileName }) {
46891
47676
  return join(outputRoot, fileName);
46892
47677
  }
46893
47678
  };
@@ -47089,7 +47874,8 @@ var WarpRule = class WarpRule extends ToolRule {
47089
47874
  }
47090
47875
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47091
47876
  const { root } = this.getSettablePaths({ global });
47092
- const fileContent = await readFileContent(join(outputRoot, join(root.relativeDirPath, root.relativeFilePath)));
47877
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
47878
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
47093
47879
  return new WarpRule({
47094
47880
  outputRoot,
47095
47881
  relativeDirPath: root.relativeDirPath,
@@ -47214,6 +48000,27 @@ var ZedRule = class ZedRule extends ToolRule {
47214
48000
  }
47215
48001
  };
47216
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
47217
48024
  //#region src/features/rules/rules-processor.ts
47218
48025
  const RulesProcessorToolTargetSchema = z.enum(rulesProcessorToolTargetTuple);
47219
48026
  const formatRulePaths = (rules) => rules.map((r) => join(r.getRelativeDirPath(), r.getRelativeFilePath())).join(", ");
@@ -47476,6 +48283,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47476
48283
  supportsGlobal: true,
47477
48284
  ruleDiscoveryMode: "toon",
47478
48285
  mcpInstructionsRegistrar: OpencodeMcp,
48286
+ mcpInstructionsRegistrarGlobal: true,
47479
48287
  collisionPolicy: "compose"
47480
48288
  }
47481
48289
  }],
@@ -47540,6 +48348,16 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47540
48348
  localRootFileName: "AGENTS.local.md"
47541
48349
  }
47542
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
+ }],
47543
48361
  ["takt", {
47544
48362
  class: TaktRule,
47545
48363
  meta: {
@@ -47712,18 +48530,19 @@ var RulesProcessor = class extends FeatureProcessor {
47712
48530
  /**
47713
48531
  * Non-root rules of some tools are not auto-loaded; the tool's MCP feature
47714
48532
  * registers them in its shared config's `instructions` key. The root rule is
47715
- * auto-loaded and never registered. Project scope only.
48533
+ * auto-loaded and never registered. Global scope opt-in via
48534
+ * `mcpInstructionsRegistrarGlobal`.
47716
48535
  */
47717
48536
  async buildMcpInstructionFiles({ toolRules, meta }) {
47718
- if (!meta.mcpInstructionsRegistrar || this.global) return [];
48537
+ if (!meta.mcpInstructionsRegistrar || this.global && !meta.mcpInstructionsRegistrarGlobal) return [];
47719
48538
  const instructionPaths = toolRules.filter((rule) => !rule.isRoot() && !rule.isExcludedFromRootReferences()).map((rule) => toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())));
47720
- if (instructionPaths.length === 0) return [];
47721
- return [await meta.mcpInstructionsRegistrar.fromInstructions({
48539
+ const registered = await meta.mcpInstructionsRegistrar.fromInstructions({
47722
48540
  outputRoot: this.outputRoot,
47723
48541
  instructions: instructionPaths,
47724
48542
  validate: true,
47725
48543
  global: this.global
47726
- })];
48544
+ });
48545
+ return registered ? [registered] : [];
47727
48546
  }
47728
48547
  /**
47729
48548
  * For tools that don't create a separate conventions rule, prepend the
@@ -47854,53 +48673,58 @@ var RulesProcessor = class extends FeatureProcessor {
47854
48673
  const rootRule = toolRules.find((rule) => rule.isRoot());
47855
48674
  if (rootRule) rootRule.setFileContent(rootRule.getFileContent() + "\n\n" + localRootBody);
47856
48675
  }
47857
- buildLocalRootFile({ factory, fileName, body }) {
48676
+ buildLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot = false }) {
47858
48677
  if (factory.class === ClaudecodeRule) {
47859
48678
  const paths = ClaudecodeRule.getSettablePaths({ global: this.global });
47860
48679
  return new ClaudecodeRule({
47861
48680
  outputRoot: this.outputRoot,
47862
- relativeDirPath: paths.root.relativeDirPath,
48681
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47863
48682
  relativeFilePath: fileName,
47864
48683
  frontmatter: {},
47865
48684
  body,
47866
48685
  validate: true,
47867
- root: true
48686
+ root: true,
48687
+ localRoot
47868
48688
  });
47869
48689
  }
47870
48690
  if (factory.class === ClaudecodeLegacyRule) {
47871
48691
  const paths = ClaudecodeLegacyRule.getSettablePaths({ global: this.global });
47872
48692
  return new ClaudecodeLegacyRule({
47873
48693
  outputRoot: this.outputRoot,
47874
- relativeDirPath: paths.root.relativeDirPath,
48694
+ relativeDirPath: relativeDirPath ?? paths.root.relativeDirPath,
47875
48695
  relativeFilePath: fileName,
47876
48696
  fileContent: body,
47877
48697
  validate: true,
47878
- root: true
48698
+ root: true,
48699
+ localRoot
47879
48700
  });
47880
48701
  }
47881
48702
  if (factory.class === RovodevRule) return new RovodevRule({
47882
48703
  outputRoot: this.outputRoot,
47883
- relativeDirPath: ".",
48704
+ relativeDirPath: relativeDirPath ?? ".",
47884
48705
  relativeFilePath: fileName,
47885
48706
  fileContent: body,
47886
48707
  validate: true,
47887
- root: true
48708
+ root: true,
48709
+ localRoot
47888
48710
  });
47889
48711
  if (factory.class === RooRule) return new RooRule({
47890
48712
  outputRoot: this.outputRoot,
47891
- relativeDirPath: ".",
48713
+ relativeDirPath: relativeDirPath ?? ".",
47892
48714
  relativeFilePath: fileName,
47893
48715
  fileContent: body,
47894
48716
  validate: true,
47895
- root: true
48717
+ root: true,
48718
+ localRoot
47896
48719
  });
47897
48720
  if (factory.class === QwencodeRule) return new QwencodeRule({
47898
48721
  outputRoot: this.outputRoot,
47899
- relativeDirPath: QWENCODE_DIR,
48722
+ relativeDirPath: relativeDirPath ?? ".qwen",
47900
48723
  relativeFilePath: fileName,
47901
48724
  fileContent: body,
47902
48725
  validate: true,
47903
- root: true
48726
+ root: true,
48727
+ localRoot
47904
48728
  });
47905
48729
  return null;
47906
48730
  }
@@ -47938,6 +48762,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
47938
48762
  async convertToolFilesToRulesyncFiles(toolFiles) {
47939
48763
  const toolRules = toolFiles.filter((file) => file instanceof ToolRule);
47940
48764
  const rulesyncRules = toolRules.map((toolRule) => {
48765
+ if (toolRule.isLocalRoot()) return toolRule.toLocalRootRulesyncRule({ targets: [this.toolTarget] });
47941
48766
  return toolRule.toRulesyncRule();
47942
48767
  });
47943
48768
  const claimedBy = /* @__PURE__ */ new Map();
@@ -48071,20 +48896,34 @@ As this project's AI coding tool, you must follow the additional conventions bel
48071
48896
  })();
48072
48897
  this.logger.debug(`Found ${rootToolRules.length} root tool rule files`);
48073
48898
  const localRootToolRules = await (async () => {
48074
- 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 [];
48075
48900
  const fileName = factory.meta.localRootFileName;
48076
- if (factory.class.getLocalRootDeletionGlob) {
48077
- const filePaths = await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
48901
+ const filePaths = await (async () => {
48902
+ if (factory.class.getLocalRootFileGlob) return await findFilesByGlobs(factory.class.getLocalRootFileGlob({
48078
48903
  outputRoot: this.outputRoot,
48079
48904
  fileName
48080
48905
  }));
48081
- return buildDeletionRulesFromPaths(filePaths);
48082
- }
48083
- if (!settablePaths.root) return [];
48084
- const filePaths = await findFilesWithFallback(join(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => join(this.outputRoot, alt.relativeDirPath, fileName));
48085
- 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);
48086
48925
  })();
48087
- 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`);
48088
48927
  const rootMirrorDeletionRules = await (async () => {
48089
48928
  const rootMirror = factory.class.getRootMirror?.();
48090
48929
  if (!forDeletion || this.global || !rootMirror) return [];
@@ -48210,13 +49049,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
48210
49049
  const lines = [];
48211
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.");
48212
49051
  lines.push("");
48213
- const toonContent = encode({ rules: toolRulesWithoutRoot.map((toolRule) => {
49052
+ const rules = toolRulesWithoutRoot.map((toolRule) => {
48214
49053
  const frontmatter = toolRule.toRulesyncRule().getFrontmatter();
48215
49054
  const rule = { path: `@${toolRule.getRelativePathFromCwd()}` };
48216
49055
  if (frontmatter.description) rule.description = frontmatter.description;
48217
49056
  if (frontmatter.globs && frontmatter.globs.length > 0) rule.applyTo = frontmatter.globs;
48218
49057
  return rule;
48219
- }) });
49058
+ });
49059
+ const toonContent = encode({ rules });
48220
49060
  lines.push(toonContent);
48221
49061
  return lines.join("\n") + "\n\n";
48222
49062
  }
@@ -49378,8 +50218,8 @@ async function generateIgnoreCore(params) {
49378
50218
  allPaths.push(...result.paths);
49379
50219
  if (result.hasDiff) hasDiff = true;
49380
50220
  } catch (error) {
49381
- logger.warn(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
49382
- continue;
50221
+ logger.error(`Failed to generate ${toolTarget} ignore files for ${outputRoot}: ${formatError(error)}`);
50222
+ throw error;
49383
50223
  }
49384
50224
  }
49385
50225
  return {
@@ -49651,8 +50491,8 @@ async function generatePermissionsCore(params) {
49651
50491
  allPaths.push(...result.paths);
49652
50492
  if (result.hasDiff) hasDiff = true;
49653
50493
  } catch (error) {
49654
- logger.warn(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
49655
- continue;
50494
+ logger.error(`Failed to generate ${toolTarget} permissions files for ${outputRoot}: ${formatError(error)}`);
50495
+ throw error;
49656
50496
  }
49657
50497
  }
49658
50498
  return {
@@ -50056,4 +50896,4 @@ async function importChecksCore(params) {
50056
50896
  //#endregion
50057
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 };
50058
50898
 
50059
- //# sourceMappingURL=import-BwakMyyf.js.map
50899
+ //# sourceMappingURL=import-BlwypG9v.js.map