rulesync 38.0.0 → 39.0.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.
@@ -522,6 +522,7 @@ const rulesProcessorToolTargetTuple = [
522
522
  "cursor",
523
523
  "deepagents",
524
524
  "factorydroid",
525
+ "firebender",
525
526
  "goose",
526
527
  "grokcli",
527
528
  "gitlabduo",
@@ -551,6 +552,7 @@ const rulesProcessorToolTargetTuple = [
551
552
  "trae",
552
553
  "vibe",
553
554
  "warp",
555
+ "xum",
554
556
  "devin",
555
557
  "devin-plugin",
556
558
  "zcode",
@@ -560,6 +562,7 @@ const rulesProcessorToolTargetTuple = [
560
562
  ];
561
563
  const ignoreProcessorToolTargetTuple = [
562
564
  "aiassistant",
565
+ "androidstudio",
563
566
  "antigravity-cli",
564
567
  "augmentcode",
565
568
  "bob",
@@ -570,6 +573,7 @@ const ignoreProcessorToolTargetTuple = [
570
573
  "continue",
571
574
  "crush",
572
575
  "cursor",
576
+ "firebender",
573
577
  "grokcli",
574
578
  "hermesagent",
575
579
  "junie",
@@ -617,6 +621,7 @@ const mcpProcessorToolTargetTuple = [
617
621
  "deepagents",
618
622
  "dsh",
619
623
  "factorydroid",
624
+ "firebender",
620
625
  "goose",
621
626
  "grokcli",
622
627
  "gitlabduo",
@@ -646,6 +651,7 @@ const mcpProcessorToolTargetTuple = [
646
651
  "vibe-plugin",
647
652
  "warp",
648
653
  "warpcli",
654
+ "xum",
649
655
  "devin",
650
656
  "devin-plugin",
651
657
  "zcode",
@@ -672,6 +678,7 @@ const commandsProcessorToolTargetTuple = [
672
678
  "copilot",
673
679
  "cursor",
674
680
  "factorydroid",
681
+ "firebender",
675
682
  "goose",
676
683
  "grokcli",
677
684
  "gitlabduo",
@@ -727,6 +734,7 @@ const subagentsProcessorToolTargetTuple = [
727
734
  "devin",
728
735
  "devin-plugin",
729
736
  "factorydroid",
737
+ "firebender",
730
738
  "goose",
731
739
  "grokcli",
732
740
  "hermesagent",
@@ -751,6 +759,7 @@ const subagentsProcessorToolTargetTuple = [
751
759
  "takt",
752
760
  "vibe",
753
761
  "vibe-plugin",
762
+ "xum",
754
763
  "zcode",
755
764
  "zcode-plugin"
756
765
  ];
@@ -758,6 +767,7 @@ const skillsProcessorToolTargetTuple = [
758
767
  "agentsmd",
759
768
  "agentsskills",
760
769
  "aiassistant",
770
+ "androidstudio",
761
771
  "amp",
762
772
  "antigravity-cli",
763
773
  "antigravity-ide",
@@ -781,6 +791,7 @@ const skillsProcessorToolTargetTuple = [
781
791
  "cursor",
782
792
  "deepagents",
783
793
  "factorydroid",
794
+ "firebender",
784
795
  "goose",
785
796
  "grokcli",
786
797
  "gitlabduo",
@@ -810,6 +821,7 @@ const skillsProcessorToolTargetTuple = [
810
821
  "vibe",
811
822
  "vibe-plugin",
812
823
  "warp",
824
+ "xum",
813
825
  "devin",
814
826
  "devin-plugin",
815
827
  "zcode",
@@ -844,6 +856,7 @@ const hooksProcessorToolTargetTuple = [
844
856
  "pi",
845
857
  "pool",
846
858
  "factorydroid",
859
+ "firebender",
847
860
  "goose",
848
861
  "hermesagent",
849
862
  "kimi-code",
@@ -3758,6 +3771,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3758
3771
  ".devin/config.json",
3759
3772
  ".dsh/cordis.patch.yml",
3760
3773
  ".factory/settings.json",
3774
+ "firebender.json",
3775
+ ".firebender/firebender.json",
3761
3776
  ".grok/config.toml",
3762
3777
  ".grok/sandbox.toml",
3763
3778
  ".letta/settings.json",
@@ -4219,14 +4234,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
4219
4234
  } } });
4220
4235
  return matter.stringify(file, cleanFrontmatter);
4221
4236
  }
4237
+ /**
4238
+ * Thrown in place of gray-matter's built-in JavaScript engine.
4239
+ *
4240
+ * gray-matter picks the parser from the text after the opening fence, so a
4241
+ * file that opens with `---js` or `---javascript` has its frontmatter handed
4242
+ * to `eval()`. Frontmatter here comes from third-party skills, fetched sources
4243
+ * and untrusted repositories, so that would run arbitrary code on the machine
4244
+ * of whoever generates or imports it. Every engine name that resolves to code
4245
+ * execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
4246
+ * refuses those fences earlier still, so this is the second line of defense.
4247
+ */
4248
+ function refuseExecutableFrontmatter() {
4249
+ throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
4250
+ }
4251
+ const REFUSED_FRONTMATTER_ENGINE = {
4252
+ parse: refuseExecutableFrontmatter,
4253
+ stringify: refuseExecutableFrontmatter
4254
+ };
4255
+ /**
4256
+ * Engines passed to every gray-matter parse. Supplying them merges over
4257
+ * gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
4258
+ * a plugin could register) can never be reached through an alias.
4259
+ *
4260
+ * `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
4261
+ * pnpm patch only applies inside this repository: gray-matter is an external
4262
+ * dependency of the published package, so an installed rulesync runs the
4263
+ * unpatched gray-matter, and this override plus the language check are what
4264
+ * protect it. Exported for the test that pins that.
4265
+ */
4266
+ const FRONTMATTER_PARSE_OPTIONS = { engines: {
4267
+ javascript: REFUSED_FRONTMATTER_ENGINE,
4268
+ js: REFUSED_FRONTMATTER_ENGINE,
4269
+ coffee: REFUSED_FRONTMATTER_ENGINE,
4270
+ coffeescript: REFUSED_FRONTMATTER_ENGINE,
4271
+ cson: REFUSED_FRONTMATTER_ENGINE
4272
+ } };
4273
+ /** Frontmatter languages accepted after the opening `---` fence. */
4274
+ const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
4275
+ /**
4276
+ * Read the opening frontmatter fence the way gray-matter does, or return
4277
+ * `undefined` when gray-matter would see no frontmatter at all.
4278
+ *
4279
+ * gray-matter strips one byte order mark, treats `---` as a fence only when a
4280
+ * fourth `-` does not follow, and takes the language from the text between the
4281
+ * fence and the first `\r?\n`, trimmed — dropping the last character when
4282
+ * there is no line break at all. Every check below that has to agree with
4283
+ * gray-matter on where the block is (the language check, the size guard, the
4284
+ * repair pass) goes through this one reading, so none of them can be bypassed
4285
+ * by a fence line gray-matter accepts and a stricter pattern would not (a
4286
+ * stray `\r` before the line break, say).
4287
+ */
4288
+ function readFrontmatterFence(content) {
4289
+ const offset = content.startsWith("") ? 1 : 0;
4290
+ const text = content.slice(offset);
4291
+ if (!text.startsWith("---") || text.charAt(3) === "-") return;
4292
+ const afterFence = text.slice(3);
4293
+ const lineBreak = /\r?\n/.exec(afterFence);
4294
+ const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
4295
+ const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
4296
+ return {
4297
+ language,
4298
+ lineBreakStart,
4299
+ lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
4300
+ };
4301
+ }
4302
+ /**
4303
+ * Refuse a frontmatter fence that names any language other than YAML.
4304
+ *
4305
+ * Only YAML is ever written by rulesync or by the tools it targets, so anything
4306
+ * else — `---js` above all, whose block gray-matter would `eval()` — is
4307
+ * rejected before gray-matter sees the content.
4308
+ */
4309
+ function assertYamlFrontmatterLanguage(content) {
4310
+ const fence = readFrontmatterFence(content);
4311
+ if (fence && !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) throw new Error(`Unsupported frontmatter language ${JSON.stringify(fence.language.slice(0, 40))}; only YAML frontmatter is supported`);
4312
+ }
4313
+ /**
4314
+ * The number of characters gray-matter would hand to the YAML parser as the
4315
+ * frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
4316
+ * closing fence does not make this zero: gray-matter then parses everything
4317
+ * after the fence line, so that is what the size guard has to measure.
4318
+ */
4319
+ function measureFrontmatterBlockChars(content) {
4320
+ const fence = readFrontmatterFence(content);
4321
+ if (fence?.lineBreakStart === void 0) return 0;
4322
+ const rest = content.slice(fence.lineBreakStart);
4323
+ const closeIndex = rest.indexOf("\n---");
4324
+ return closeIndex === -1 ? rest.length : closeIndex;
4325
+ }
4222
4326
  function parseFrontmatter(content, filePath) {
4223
4327
  let frontmatter;
4224
4328
  let body;
4225
4329
  let hasFrontmatter;
4226
4330
  try {
4227
- const bounds = findFrontmatterBlockBounds(content);
4228
- if (bounds && bounds.blockEnd - bounds.blockStart > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
4229
- const result = matter(content, {});
4331
+ assertYamlFrontmatterLanguage(content);
4332
+ if (measureFrontmatterBlockChars(content) > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
4333
+ const result = matter(content, FRONTMATTER_PARSE_OPTIONS);
4230
4334
  frontmatter = deepRemoveNullishObject(result.data);
4231
4335
  body = result.content;
4232
4336
  hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
@@ -4285,22 +4389,28 @@ function repairFrontmatterLine(line) {
4285
4389
  }
4286
4390
  /**
4287
4391
  * Locate a raw `---`-delimited frontmatter block's bounds within `content`,
4288
- * without parsing it. Shared by the size guard in {@link parseFrontmatter} and
4289
- * the YAML repair pass below, so both agree on exactly what gray-matter would
4290
- * treat as the block: gray-matter ends it at the first `\n---`, with no
4291
- * requirement that the delimiter be alone on its line, so a stricter pattern
4292
- * here would run past gray-matter's delimiter and act on text that is really
4293
- * the body.
4392
+ * without parsing it, for the YAML repair pass below and the rule loaders that
4393
+ * pre-process `globs:`. The opening fence is read exactly as gray-matter reads
4394
+ * it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
4395
+ * still YAML, any other language yields no bounds. gray-matter ends the block
4396
+ * at the first `\n---`, with no requirement that the delimiter be alone on its
4397
+ * line, so a stricter pattern here would run past gray-matter's delimiter and
4398
+ * act on text that is really the body. A block with no closing fence has no
4399
+ * bounds; the size guard measures that case separately.
4294
4400
  */
4295
4401
  function findFrontmatterBlockBounds(content) {
4296
- const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
4297
- if (!opening) return;
4298
- const blockStart = opening[0].length;
4299
- const closing = /\r?\n---/.exec(content.slice(blockStart));
4402
+ const fence = readFrontmatterFence(content);
4403
+ if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
4404
+ const blockStart = fence.lineEnd;
4405
+ const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
4300
4406
  if (!closing) return;
4407
+ if (closing.index === 0) return {
4408
+ blockStart,
4409
+ blockEnd: blockStart
4410
+ };
4301
4411
  return {
4302
4412
  blockStart,
4303
- blockEnd: blockStart + closing.index
4413
+ blockEnd: fence.lineBreakStart + closing.index
4304
4414
  };
4305
4415
  }
4306
4416
  /**
@@ -4332,8 +4442,9 @@ function repairMalformedFrontmatterYaml(content) {
4332
4442
  * narrow — one pass, top-level entries only, and the original error is what
4333
4443
  * surfaces if it does not help, so a genuinely broken file still fails with the
4334
4444
  * message that describes what is actually wrong with it. A file with no closing
4335
- * `---`, or one whose opening fence carries a language tag, is not repaired at
4336
- * all: neither is a frontmatter block gray-matter would have read.
4445
+ * `---` is not repaired at all, and one whose opening fence names a language
4446
+ * other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
4447
+ * is repaired like a plain one).
4337
4448
  *
4338
4449
  * @see https://agentskills.io/client-implementation/adding-skills-support
4339
4450
  */
@@ -4698,6 +4809,29 @@ const CURSOR_HOOK_EVENTS = [
4698
4809
  "workspaceOpen"
4699
4810
  ];
4700
4811
  /**
4812
+ * Hook events supported by Firebender.
4813
+ *
4814
+ * Firebender's `hooks.json` follows Cursor's layout, and every event it fires
4815
+ * is spelled like its canonical counterpart, so no name mapping is needed.
4816
+ * @see https://docs.firebender.com/multi-agent/hooks
4817
+ */
4818
+ const FIREBENDER_HOOK_EVENTS = [
4819
+ "sessionStart",
4820
+ "sessionEnd",
4821
+ "subagentStart",
4822
+ "subagentStop",
4823
+ "preToolUse",
4824
+ "postToolUse",
4825
+ "beforeShellExecution",
4826
+ "afterShellExecution",
4827
+ "beforeMCPExecution",
4828
+ "afterMCPExecution",
4829
+ "beforeReadFile",
4830
+ "afterFileEdit",
4831
+ "preCompact",
4832
+ "stop"
4833
+ ];
4834
+ /**
4701
4835
  * Hook events supported by Claude Code.
4702
4836
  *
4703
4837
  * Covers the full documented event surface.
@@ -6045,6 +6179,7 @@ const HooksConfigSchema = z.looseObject({
6045
6179
  version: z.optional(z.number()),
6046
6180
  hooks: canonicalHooksRecordSchema,
6047
6181
  cursor: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6182
+ firebender: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6048
6183
  claudecode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6049
6184
  copilot: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6050
6185
  copilotcli: z.optional(z.looseObject({
@@ -7148,7 +7283,7 @@ const McpServerSchema = z.looseObject({
7148
7283
  crushSessionless: z.optional(z.boolean()),
7149
7284
  crushChannelEnabled: z.optional(z.boolean()),
7150
7285
  crushChannelReply: z.optional(CrushChannelReplySchema),
7151
- zcodeOauth: z.optional(z.looseObject({ type: z.string() })),
7286
+ zcodeOauth: z.optional(z.looseObject({})),
7152
7287
  poolAllow: z.optional(z.array(z.string())),
7153
7288
  headers: z.optional(z.record(z.string(), z.string())),
7154
7289
  /**
@@ -7250,6 +7385,7 @@ const RulesyncMcpFileSchema = z.looseObject({
7250
7385
  devin: z.optional(toolScopedMcpSchema),
7251
7386
  dsh: z.optional(toolScopedMcpSchema),
7252
7387
  factorydroid: z.optional(toolScopedMcpSchema),
7388
+ firebender: z.optional(toolScopedMcpSchema),
7253
7389
  gitlabduo: z.optional(toolScopedMcpSchema),
7254
7390
  goose: z.optional(toolScopedMcpSchema),
7255
7391
  grokcli: z.optional(toolScopedMcpSchema),
@@ -7274,6 +7410,7 @@ const RulesyncMcpFileSchema = z.looseObject({
7274
7410
  vibe: z.optional(toolScopedMcpSchema),
7275
7411
  warp: z.optional(toolScopedMcpSchema),
7276
7412
  warpcli: z.optional(toolScopedMcpSchema),
7413
+ xum: z.optional(toolScopedMcpSchema),
7277
7414
  zed: z.optional(toolScopedMcpSchema)
7278
7415
  });
7279
7416
  /**
@@ -13069,6 +13206,35 @@ const CRUSH_CONFIG_DECLARATION = {
13069
13206
  }
13070
13207
  };
13071
13208
  /**
13209
+ * Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
13210
+ * and personal (`~/.firebender/firebender.json`) spellings so a policy edit
13211
+ * lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
13212
+ * `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
13213
+ * refuses to read-modify-write a file it could not parse. Each feature owns
13214
+ * one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
13215
+ * `mcpServers` (the complete server map) and `agents` (recomputed from the
13216
+ * existing list, with the entries for rulesync's own agent directory replaced).
13217
+ * https://docs.firebender.com/api-reference/syntax
13218
+ */
13219
+ const FIREBENDER_CONFIG_DECLARATION = {
13220
+ format: "json",
13221
+ invalidRootPolicy: "error",
13222
+ features: {
13223
+ ignore: {
13224
+ kind: "replace-owned-keys",
13225
+ ownedKeys: ["ignore"]
13226
+ },
13227
+ subagents: {
13228
+ kind: "replace-owned-keys",
13229
+ ownedKeys: ["agents"]
13230
+ },
13231
+ mcp: {
13232
+ kind: "replace-owned-keys",
13233
+ ownedKeys: ["mcpServers"]
13234
+ }
13235
+ }
13236
+ };
13237
+ /**
13072
13238
  * What the two Claude Code settings files have in common: both are plain JSON,
13073
13239
  * and both validate against the one published schema, which the gateway
13074
13240
  * points every file it writes at. `.claude/settings.local.json` is the same
@@ -13308,6 +13474,15 @@ const SHARED_CONFIG_OWNERSHIP = {
13308
13474
  ownedKeys: ["servers"]
13309
13475
  } }
13310
13476
  },
13477
+ ".xum/mcp.jsonc": {
13478
+ format: "jsonc",
13479
+ invalidRootPolicy: "error",
13480
+ jsoncParseErrors: "error",
13481
+ features: { mcp: {
13482
+ kind: "replace-owned-keys",
13483
+ ownedKeys: ["servers"]
13484
+ } }
13485
+ },
13311
13486
  ".qwen/settings.json": {
13312
13487
  format: "json",
13313
13488
  features: {
@@ -13395,6 +13570,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13395
13570
  ".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
13396
13571
  "crush.json": CRUSH_CONFIG_DECLARATION,
13397
13572
  ".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
13573
+ "firebender.json": FIREBENDER_CONFIG_DECLARATION,
13574
+ ".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
13398
13575
  ".bob/settings.json": {
13399
13576
  format: "json",
13400
13577
  invalidRootPolicy: "error",
@@ -20229,6 +20406,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
20229
20406
  }
20230
20407
  };
20231
20408
  //#endregion
20409
+ //#region src/constants/firebender-paths.ts
20410
+ /**
20411
+ * Firebender (the coding agent plugin for Android Studio and other JetBrains
20412
+ * IDEs) keeps project configuration under `.firebender/` plus a project-root
20413
+ * `firebender.json`, and user configuration under `~/.firebender/` (including
20414
+ * `~/.firebender/firebender.json`).
20415
+ *
20416
+ * @see https://docs.firebender.com/api-reference/syntax
20417
+ * @see https://docs.firebender.com/api-reference/rules
20418
+ * @see https://docs.firebender.com/api-reference/commands
20419
+ * @see https://docs.firebender.com/api-reference/agents
20420
+ * @see https://docs.firebender.com/api-reference/skills
20421
+ * @see https://docs.firebender.com/api-reference/agents-md
20422
+ * @see https://docs.firebender.com/multi-agent/hooks
20423
+ */
20424
+ const FIREBENDER_DIR = ".firebender";
20425
+ /**
20426
+ * `firebender.json`: at the project root for project scope, and under
20427
+ * `~/.firebender/` for the personal config. It is shared by the ignore
20428
+ * (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
20429
+ */
20430
+ const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
20431
+ /** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
20432
+ const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
20433
+ join(FIREBENDER_DIR, "rules");
20434
+ /** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
20435
+ const FIREBENDER_COMMANDS_DIR_PATH = join(FIREBENDER_DIR, "commands");
20436
+ /** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
20437
+ const FIREBENDER_AGENTS_DIR_NAME = "agents";
20438
+ const FIREBENDER_AGENTS_DIR_PATH = join(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
20439
+ /** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
20440
+ const FIREBENDER_SKILLS_DIR_PATH = join(FIREBENDER_DIR, "skills");
20441
+ /** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
20442
+ const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
20443
+ /**
20444
+ * The directory holding `firebender.json` for a scope: the project root, or
20445
+ * `~/.firebender/` for the personal config.
20446
+ */
20447
+ function getFirebenderConfigDir({ global }) {
20448
+ return global ? FIREBENDER_DIR : ".";
20449
+ }
20450
+ //#endregion
20451
+ //#region src/features/commands/firebender-command.ts
20452
+ /**
20453
+ * Frontmatter of a Firebender command. Every field is optional: `name`
20454
+ * defaults to the file name, `model` is `default`, `quick` or a model ID, and
20455
+ * `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
20456
+ * preserved.
20457
+ *
20458
+ * @see https://docs.firebender.com/api-reference/commands
20459
+ */
20460
+ const FirebenderCommandFrontmatterSchema = z.looseObject({
20461
+ name: z.optional(z.string()),
20462
+ description: z.optional(z.string()),
20463
+ model: z.optional(z.string()),
20464
+ mode: z.optional(z.string())
20465
+ });
20466
+ const FIREBENDER_COMMAND_EXTENSION = ".mdc";
20467
+ /**
20468
+ * Command generator for Firebender: one `.mdc` file per command under
20469
+ * `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
20470
+ * with optional `name` / `description` / `model` / `mode` frontmatter. The
20471
+ * canonical `description` is written as is; `name`, `model` and `mode` come
20472
+ * from the `firebender:` section of the rulesync command.
20473
+ *
20474
+ * @see https://docs.firebender.com/api-reference/commands
20475
+ */
20476
+ var FirebenderCommand = class FirebenderCommand extends ToolCommand {
20477
+ frontmatter;
20478
+ body;
20479
+ constructor({ frontmatter, body, ...rest }) {
20480
+ if (rest.validate) {
20481
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20482
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
20483
+ }
20484
+ super({
20485
+ ...rest,
20486
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
20487
+ });
20488
+ this.frontmatter = frontmatter;
20489
+ this.body = body;
20490
+ }
20491
+ static getSettablePaths(_options = {}) {
20492
+ return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
20493
+ }
20494
+ getBody() {
20495
+ return this.body;
20496
+ }
20497
+ getFrontmatter() {
20498
+ return this.frontmatter;
20499
+ }
20500
+ toRulesyncCommand() {
20501
+ const { description, ...restFields } = this.frontmatter;
20502
+ const rulesyncFrontmatter = {
20503
+ targets: ["*"],
20504
+ description,
20505
+ ...Object.keys(restFields).length > 0 && { firebender: restFields }
20506
+ };
20507
+ return new RulesyncCommand({
20508
+ outputRoot: ".",
20509
+ frontmatter: rulesyncFrontmatter,
20510
+ body: this.body,
20511
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
20512
+ relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
20513
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
20514
+ validate: true
20515
+ });
20516
+ }
20517
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
20518
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
20519
+ const firebenderFields = rulesyncFrontmatter.firebender ?? {};
20520
+ const frontmatter = {
20521
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
20522
+ ...firebenderFields
20523
+ };
20524
+ return new FirebenderCommand({
20525
+ outputRoot,
20526
+ frontmatter,
20527
+ body: rulesyncCommand.getBody(),
20528
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
20529
+ relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
20530
+ validate
20531
+ });
20532
+ }
20533
+ validate() {
20534
+ if (!this.frontmatter) return {
20535
+ success: true,
20536
+ error: null
20537
+ };
20538
+ const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
20539
+ if (result.success) return {
20540
+ success: true,
20541
+ error: null
20542
+ };
20543
+ return {
20544
+ success: false,
20545
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
20546
+ };
20547
+ }
20548
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
20549
+ return this.isTargetedByRulesyncCommandDefault({
20550
+ rulesyncCommand,
20551
+ toolTarget: "firebender"
20552
+ });
20553
+ }
20554
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
20555
+ const paths = this.getSettablePaths({ global });
20556
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
20557
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
20558
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20559
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
20560
+ return new FirebenderCommand({
20561
+ outputRoot,
20562
+ relativeDirPath: paths.relativeDirPath,
20563
+ relativeFilePath,
20564
+ frontmatter: result.data,
20565
+ body: body.trim(),
20566
+ validate
20567
+ });
20568
+ }
20569
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
20570
+ return new FirebenderCommand({
20571
+ outputRoot,
20572
+ relativeDirPath,
20573
+ relativeFilePath,
20574
+ frontmatter: {},
20575
+ body: "",
20576
+ validate: false
20577
+ });
20578
+ }
20579
+ };
20580
+ //#endregion
20232
20581
  //#region src/features/commands/gitlabduo-command.ts
20233
20582
  /**
20234
20583
  * GitLab Duo CLI custom slash commands are Markdown files named after the
@@ -20821,6 +21170,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
20821
21170
  }
20822
21171
  };
20823
21172
  //#endregion
21173
+ //#region src/features/shared/hermes-plugin-commands.ts
21174
+ /**
21175
+ * Normalize a name into a Hermes Agent plugin slash command name.
21176
+ *
21177
+ * `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
21178
+ * looks plugin commands up with every `_` turned into `-`
21179
+ * (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
21180
+ * reachable from every surface.
21181
+ */
21182
+ function hermesSlashName(value) {
21183
+ return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21184
+ }
21185
+ /**
21186
+ * Python helpers shared by the generated Hermes command plugins.
21187
+ *
21188
+ * A plugin slash command runs outside any agent turn, so it cannot call
21189
+ * `delegate_task` (which requires a parent agent). Instead the handler queues
21190
+ * its prompt as a user message with `ctx.inject_message()`, passing the calling
21191
+ * session's key: a `session_key` handler keyword on newer Hermes, or
21192
+ * `HERMES_SESSION_KEY` from the session context the gateway binds around the
21193
+ * handler on older releases.
21194
+ */
21195
+ function getHermesQueueUserMessagePython({ pluginName }) {
21196
+ return `def _session_key(kwargs):
21197
+ session_key = kwargs.get("session_key")
21198
+ if session_key:
21199
+ return session_key
21200
+ try:
21201
+ from gateway.session_context import get_session_env
21202
+ except Exception:
21203
+ return None
21204
+ return get_session_env("HERMES_SESSION_KEY") or None
21205
+
21206
+
21207
+ def _queue_user_message(ctx, message, kwargs, label):
21208
+ """Queue *message* as the session's next user turn; return an error reply, or None."""
21209
+ session_key = _session_key(kwargs)
21210
+ try:
21211
+ if session_key:
21212
+ queued = ctx.inject_message(message, role="user", session_key=session_key)
21213
+ else:
21214
+ queued = ctx.inject_message(message, role="user")
21215
+ except Exception as error:
21216
+ return f"Could not queue {label}: {error}"
21217
+ if not queued:
21218
+ return (
21219
+ f"Hermes refused to queue {label}: there is no active session, or this plugin "
21220
+ "may not inject messages. In gateway sessions, set "
21221
+ "plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
21222
+ )
21223
+ return None
21224
+ `;
21225
+ }
21226
+ //#endregion
20824
21227
  //#region src/features/skills/agentsskills-skill.ts
20825
21228
  const AgentsSkillsSkillFrontmatterSchema = z.looseObject({
20826
21229
  name: z.string(),
@@ -21303,9 +21706,6 @@ const HermesCommandSpecSchema = z.object({
21303
21706
  description: z.string(),
21304
21707
  prompt: z.string()
21305
21708
  });
21306
- function hermesSlashName(value) {
21307
- return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21308
- }
21309
21709
  function getPluginManifestContent$2() {
21310
21710
  return [
21311
21711
  "name: rulesync-commands",
@@ -21339,6 +21739,8 @@ def _load_commands():
21339
21739
  return commands
21340
21740
 
21341
21741
 
21742
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
21743
+
21342
21744
  def _register_command(ctx, command):
21343
21745
  slug = command.get("slug")
21344
21746
  if not slug:
@@ -21348,31 +21750,22 @@ def _register_command(ctx, command):
21348
21750
  prompt = command.get("prompt") or ""
21349
21751
 
21350
21752
  def handler(args=None, **kwargs):
21351
- del kwargs
21352
21753
  raw_args = ""
21353
21754
  if isinstance(args, dict):
21354
21755
  raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
21355
21756
  elif args is not None:
21356
21757
  raw_args = str(args)
21357
21758
 
21358
- context_parts = [prompt] if prompt else []
21759
+ parts = [prompt] if prompt else []
21359
21760
  if raw_args:
21360
- context_parts.append(f"User arguments:\\n{raw_args}")
21761
+ parts.append(f"User arguments:\\n{raw_args}")
21361
21762
 
21362
- # delegate_task takes no model-facing "toolsets" argument: the child
21363
- # inherits the parent's enabled toolsets. "tasks" is the only shape its
21364
- # schema advertises; a top-level "goal" is an unadvertised legacy path.
21365
- return ctx.dispatch_tool(
21366
- "delegate_task",
21367
- {
21368
- "tasks": [
21369
- {
21370
- "goal": description,
21371
- "context": "\\n\\n".join(context_parts),
21372
- },
21373
- ],
21374
- },
21375
- )
21763
+ # A slash command runs outside any agent turn, so calling delegate_task
21764
+ # here fails with "delegate_task requires a parent agent context".
21765
+ # Queue the command prompt as the session's next user message instead.
21766
+ label = f"the /{slug} command"
21767
+ error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
21768
+ return error or f"Queued {label}."
21376
21769
 
21377
21770
  ctx.register_command(slug, handler, description)
21378
21771
 
@@ -25244,6 +25637,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
25244
25637
  supportsSubdirectory: false
25245
25638
  }
25246
25639
  }],
25640
+ ["firebender", {
25641
+ class: FirebenderCommand,
25642
+ meta: {
25643
+ extension: "mdc",
25644
+ supportsProject: true,
25645
+ supportsGlobal: true,
25646
+ isSimulated: false,
25647
+ supportsSubdirectory: false
25648
+ }
25649
+ }],
25247
25650
  ["goose", {
25248
25651
  class: GooseCommand,
25249
25652
  meta: {
@@ -32527,6 +32930,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
32527
32930
  }
32528
32931
  };
32529
32932
  //#endregion
32933
+ //#region src/features/hooks/firebender-hooks.ts
32934
+ /**
32935
+ * Hooks generator for Firebender.
32936
+ *
32937
+ * Firebender reads `.firebender/hooks.json` (project) and
32938
+ * `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
32939
+ * `version` plus a `hooks` map from camelCase event names to lists of
32940
+ * `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
32941
+ * spelled like its canonical counterpart. The per-hook `type` accepts
32942
+ * `"command" | "prompt"`, but the docs never say what a prompt hook does, so
32943
+ * only command hooks are generated (the HooksProcessor warns about the rest).
32944
+ *
32945
+ * @see https://docs.firebender.com/multi-agent/hooks
32946
+ */
32947
+ var FirebenderHooks = class FirebenderHooks extends ToolHooks {
32948
+ constructor(params) {
32949
+ super({
32950
+ ...params,
32951
+ fileContent: params.fileContent ?? "{}"
32952
+ });
32953
+ }
32954
+ static getSettablePaths(_options = {}) {
32955
+ return {
32956
+ relativeDirPath: FIREBENDER_DIR,
32957
+ relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
32958
+ };
32959
+ }
32960
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
32961
+ const paths = FirebenderHooks.getSettablePaths({ global });
32962
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
32963
+ return new FirebenderHooks({
32964
+ outputRoot,
32965
+ relativeDirPath: paths.relativeDirPath,
32966
+ relativeFilePath: paths.relativeFilePath,
32967
+ fileContent,
32968
+ validate
32969
+ });
32970
+ }
32971
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
32972
+ const config = rulesyncHooks.getJson();
32973
+ const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
32974
+ const sharedHooks = {};
32975
+ for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
32976
+ const mergedHooks = {
32977
+ ...sharedHooks,
32978
+ ...config.firebender?.hooks
32979
+ };
32980
+ const firebenderHooks = {};
32981
+ for (const [eventName, defs] of Object.entries(mergedHooks)) {
32982
+ const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
32983
+ command: def.command,
32984
+ ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
32985
+ ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
32986
+ ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
32987
+ }));
32988
+ if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
32989
+ }
32990
+ const paths = FirebenderHooks.getSettablePaths({ global });
32991
+ const merged = mergeGeneratedHookLists({
32992
+ existingContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
32993
+ generatedHooks: firebenderHooks,
32994
+ shape: "flat",
32995
+ preserveUnowned,
32996
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
32997
+ logger
32998
+ });
32999
+ const fileContent = JSON.stringify({
33000
+ version: config.version ?? 1,
33001
+ hooks: merged.hooks
33002
+ }, null, 2);
33003
+ return new FirebenderHooks({
33004
+ outputRoot,
33005
+ relativeDirPath: paths.relativeDirPath,
33006
+ relativeFilePath: paths.relativeFilePath,
33007
+ fileContent,
33008
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
33009
+ validate
33010
+ });
33011
+ }
33012
+ static supportsPreserveUnowned() {
33013
+ return true;
33014
+ }
33015
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
33016
+ return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
33017
+ }
33018
+ toRulesyncHooks() {
33019
+ const parsed = JSON.parse(this.getFileContent());
33020
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
33021
+ hooks: parsed.hooks ?? {},
33022
+ overrideKey: "firebender",
33023
+ version: parsed.version ?? 1
33024
+ }), null, 2) });
33025
+ }
33026
+ validate() {
33027
+ return {
33028
+ success: true,
33029
+ error: null
33030
+ };
33031
+ }
33032
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
33033
+ return new FirebenderHooks({
33034
+ outputRoot,
33035
+ relativeDirPath,
33036
+ relativeFilePath,
33037
+ fileContent: "{}",
33038
+ validate: false
33039
+ });
33040
+ }
33041
+ };
33042
+ //#endregion
32530
33043
  //#region src/features/hooks/gitlabduo-hooks.ts
32531
33044
  const GITLABDUO_CONVERTER_CONFIG = {
32532
33045
  supportedEvents: GITLABDUO_HOOK_EVENTS,
@@ -36937,6 +37450,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
36937
37450
  supportedHookTypes: ["command", "prompt"],
36938
37451
  supportsMatcher: true
36939
37452
  }],
37453
+ ["firebender", {
37454
+ class: FirebenderHooks,
37455
+ meta: {
37456
+ supportsProject: true,
37457
+ supportsGlobal: true,
37458
+ supportsImport: true
37459
+ },
37460
+ supportedEvents: FIREBENDER_HOOK_EVENTS,
37461
+ supportedHookTypes: ["command"],
37462
+ supportsMatcher: true
37463
+ }],
36940
37464
  ["claudecode", {
36941
37465
  class: ClaudecodeHooks,
36942
37466
  meta: {
@@ -37808,6 +38332,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
37808
38332
  }
37809
38333
  };
37810
38334
  //#endregion
38335
+ //#region src/constants/androidstudio-paths.ts
38336
+ /**
38337
+ * Gemini in Android Studio configuration-layout conventions.
38338
+ *
38339
+ * Only the Android Studio-specific files are owned by the `androidstudio`
38340
+ * target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
38341
+ * reads belong to the `agentsmd` and `agentsskills` targets.
38342
+ */
38343
+ const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
38344
+ const ANDROIDSTUDIO_SKILLS_DIR_PATH = join(".android-studio", "skills");
38345
+ //#endregion
38346
+ //#region src/features/ignore/androidstudio-ignore.ts
38347
+ /**
38348
+ * Ignore generator for Gemini in Android Studio.
38349
+ *
38350
+ * Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
38351
+ * each one applies to the files at or below its directory and blocks both the
38352
+ * chat and the editor AI features such as code completion. Rulesync writes the
38353
+ * project-root file only. There is no user-level variant.
38354
+ *
38355
+ * @see https://developer.android.com/studio/gemini/aiexclude
38356
+ */
38357
+ var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
38358
+ static getSettablePaths() {
38359
+ return {
38360
+ relativeDirPath: ".",
38361
+ relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
38362
+ };
38363
+ }
38364
+ toRulesyncIgnore() {
38365
+ return this.toRulesyncIgnoreDefault();
38366
+ }
38367
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
38368
+ return new AndroidstudioIgnore({
38369
+ outputRoot,
38370
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38371
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38372
+ fileContent: rulesyncIgnore.getFileContent()
38373
+ });
38374
+ }
38375
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
38376
+ const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
38377
+ return new AndroidstudioIgnore({
38378
+ outputRoot,
38379
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38380
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38381
+ fileContent,
38382
+ validate
38383
+ });
38384
+ }
38385
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
38386
+ return new AndroidstudioIgnore({
38387
+ outputRoot,
38388
+ relativeDirPath,
38389
+ relativeFilePath,
38390
+ fileContent: "",
38391
+ validate: false
38392
+ });
38393
+ }
38394
+ };
38395
+ //#endregion
37811
38396
  //#region src/features/ignore/antigravity-cli-ignore.ts
37812
38397
  /**
37813
38398
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -38469,6 +39054,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
38469
39054
  }
38470
39055
  };
38471
39056
  //#endregion
39057
+ //#region src/features/ignore/firebender-ignore.ts
39058
+ function readIgnorePatterns(fileContent) {
39059
+ const ignore = parseSharedConfig({
39060
+ format: "json",
39061
+ fileContent
39062
+ }).ignore;
39063
+ if (!Array.isArray(ignore)) return [];
39064
+ return ignore.filter((pattern) => typeof pattern === "string");
39065
+ }
39066
+ /**
39067
+ * Ignore generator for Firebender.
39068
+ *
39069
+ * Firebender reads gitignore-style patterns from the `ignore` array of
39070
+ * `firebender.json` (project root) and `~/.firebender/firebender.json`
39071
+ * (personal); the two lists are appended together. The file is shared with the
39072
+ * MCP (`mcpServers`) and subagents (`agents`) features and carries settings
39073
+ * rulesync does not own, so only the `ignore` key is written and the file is
39074
+ * never deleted.
39075
+ *
39076
+ * @see https://docs.firebender.com/api-reference/syntax
39077
+ */
39078
+ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
39079
+ constructor(params) {
39080
+ super(params);
39081
+ this.patterns = readIgnorePatterns(this.fileContent || "{}");
39082
+ }
39083
+ static getSettablePaths({ global = false } = {}) {
39084
+ return {
39085
+ relativeDirPath: getFirebenderConfigDir({ global }),
39086
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
39087
+ };
39088
+ }
39089
+ /** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
39090
+ isDeletable() {
39091
+ return false;
39092
+ }
39093
+ toRulesyncIgnore() {
39094
+ return new RulesyncIgnore({
39095
+ outputRoot: ".",
39096
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
39097
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
39098
+ fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
39099
+ });
39100
+ }
39101
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
39102
+ const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
39103
+ const paths = this.getSettablePaths({ global });
39104
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
39105
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
39106
+ const managedPatterns = [...new Set(patterns)];
39107
+ return new FirebenderIgnore({
39108
+ outputRoot,
39109
+ relativeDirPath: paths.relativeDirPath,
39110
+ relativeFilePath: paths.relativeFilePath,
39111
+ fileContent: applySharedConfigPatch({
39112
+ fileKey: sharedConfigFileKey(paths),
39113
+ feature: "ignore",
39114
+ existingContent,
39115
+ patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
39116
+ filePath
39117
+ }),
39118
+ validate: true,
39119
+ global
39120
+ });
39121
+ }
39122
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
39123
+ const paths = this.getSettablePaths({ global });
39124
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
39125
+ return new FirebenderIgnore({
39126
+ outputRoot,
39127
+ relativeDirPath: paths.relativeDirPath,
39128
+ relativeFilePath: paths.relativeFilePath,
39129
+ fileContent,
39130
+ validate,
39131
+ global
39132
+ });
39133
+ }
39134
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
39135
+ return new FirebenderIgnore({
39136
+ outputRoot,
39137
+ relativeDirPath,
39138
+ relativeFilePath,
39139
+ fileContent: "{}",
39140
+ validate: false,
39141
+ global
39142
+ });
39143
+ }
39144
+ };
39145
+ //#endregion
38472
39146
  //#region src/features/ignore/grokcli-ignore.ts
38473
39147
  /** The base built-in profile the rulesync profile extends (Grok's own default). */
38474
39148
  const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
@@ -39786,6 +40460,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
39786
40460
  const IgnoreProcessorToolTargetSchema = z.enum(ignoreProcessorToolTargetTuple);
39787
40461
  const toolIgnoreFactories = /* @__PURE__ */ new Map([
39788
40462
  ["aiassistant", { class: AiassistantIgnore }],
40463
+ ["androidstudio", { class: AndroidstudioIgnore }],
39789
40464
  ["antigravity-cli", { class: AntigravityCliIgnore }],
39790
40465
  ["augmentcode", { class: AugmentcodeIgnore }],
39791
40466
  ["bob", { class: BobIgnore }],
@@ -39796,6 +40471,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
39796
40471
  ["continue", { class: ContinueIgnore }],
39797
40472
  ["crush", { class: CrushIgnore }],
39798
40473
  ["cursor", { class: CursorIgnore }],
40474
+ ["firebender", { class: FirebenderIgnore }],
39799
40475
  ["grokcli", { class: GrokcliIgnore }],
39800
40476
  ["hermesagent", { class: HermesagentIgnore }],
39801
40477
  ["junie", { class: JunieIgnore }],
@@ -39820,6 +40496,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
39820
40496
  const ignoreProcessorGlobalToolTargets = [
39821
40497
  "continue",
39822
40498
  "devin",
40499
+ "firebender",
39823
40500
  "grokcli",
39824
40501
  "kilo",
39825
40502
  "kiro",
@@ -45317,6 +45994,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
45317
45994
  }
45318
45995
  };
45319
45996
  //#endregion
45997
+ //#region src/features/mcp/firebender-mcp.ts
45998
+ /**
45999
+ * The keys of the canonical server schema. Firebender reads only `command`,
46000
+ * `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
46001
+ * NOT on this list is unknown to rulesync and passed through untouched, so a
46002
+ * Firebender-native field such as `deferLoading` written in the tool-scoped
46003
+ * `firebender.mcpServers` block (or on a shared server) survives.
46004
+ */
46005
+ const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
46006
+ function passthroughNonCanonicalFields$1(serverConfig) {
46007
+ return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
46008
+ }
46009
+ /**
46010
+ * Translate one canonical server into Firebender's `mcpServers` entry, or
46011
+ * return `null` (with a warning) for a server Firebender cannot express. A
46012
+ * stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
46013
+ * Firebender has no `type` field and no per-server disable switch.
46014
+ *
46015
+ * @see https://docs.firebender.com/api-reference/syntax
46016
+ */
46017
+ function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
46018
+ if (serverConfig.disabled === true) return warnAndSkipMcpServer({
46019
+ toolName: "Firebender",
46020
+ serverName,
46021
+ reason: "`disabled: true`, and Firebender has no per-server disable switch",
46022
+ logger
46023
+ });
46024
+ if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
46025
+ toolName: "Firebender",
46026
+ serverName,
46027
+ reason: "no command or url",
46028
+ logger
46029
+ });
46030
+ const passthrough = passthroughNonCanonicalFields$1(serverConfig);
46031
+ if (isRemoteMcpServer(serverConfig)) {
46032
+ const url = resolveRemoteMcpUrl(serverConfig);
46033
+ if (url === void 0) return warnAndSkipMcpServer({
46034
+ toolName: "Firebender",
46035
+ serverName,
46036
+ reason: "a remote transport but no url",
46037
+ logger
46038
+ });
46039
+ return {
46040
+ ...passthrough,
46041
+ url,
46042
+ ...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
46043
+ };
46044
+ }
46045
+ const [command, ...args] = resolveLocalMcpCommand(serverConfig);
46046
+ if (command === void 0) return warnAndSkipMcpServer({
46047
+ toolName: "Firebender",
46048
+ serverName,
46049
+ reason: "a local transport but no command",
46050
+ logger
46051
+ });
46052
+ return {
46053
+ ...passthrough,
46054
+ command,
46055
+ ...args.length > 0 && { args },
46056
+ ...serverConfig.env !== void 0 && { env: serverConfig.env }
46057
+ };
46058
+ }
46059
+ /**
46060
+ * MCP generator for Firebender.
46061
+ *
46062
+ * Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
46063
+ * (project root) and `~/.firebender/firebender.json` (personal; it wins on
46064
+ * name clashes). The file is shared with the ignore (`ignore`) and subagents
46065
+ * (`agents`) features and carries settings rulesync does not own, so only the
46066
+ * `mcpServers` key is written and the file is never deleted.
46067
+ *
46068
+ * @see https://docs.firebender.com/api-reference/syntax
46069
+ */
46070
+ var FirebenderMcp = class FirebenderMcp extends ToolMcp {
46071
+ json;
46072
+ constructor(params) {
46073
+ super(params);
46074
+ this.json = parseSharedConfig({
46075
+ format: "json",
46076
+ fileContent: this.fileContent || "{}"
46077
+ });
46078
+ }
46079
+ getJson() {
46080
+ return this.json;
46081
+ }
46082
+ static getSettablePaths({ global = false } = {}) {
46083
+ return {
46084
+ relativeDirPath: getFirebenderConfigDir({ global }),
46085
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
46086
+ };
46087
+ }
46088
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
46089
+ const paths = this.getSettablePaths({ global });
46090
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
46091
+ return new FirebenderMcp({
46092
+ outputRoot,
46093
+ relativeDirPath: paths.relativeDirPath,
46094
+ relativeFilePath: paths.relativeFilePath,
46095
+ fileContent,
46096
+ validate,
46097
+ global
46098
+ });
46099
+ }
46100
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
46101
+ const paths = this.getSettablePaths({ global });
46102
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
46103
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
46104
+ const mcpServers = {};
46105
+ for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
46106
+ const converted = convertServerToFirebenderFormat({
46107
+ serverName,
46108
+ serverConfig,
46109
+ logger
46110
+ });
46111
+ if (converted !== null) mcpServers[serverName] = converted;
46112
+ }
46113
+ return new FirebenderMcp({
46114
+ outputRoot,
46115
+ relativeDirPath: paths.relativeDirPath,
46116
+ relativeFilePath: paths.relativeFilePath,
46117
+ fileContent: applySharedConfigPatch({
46118
+ fileKey: sharedConfigFileKey(paths),
46119
+ feature: "mcp",
46120
+ existingContent,
46121
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
46122
+ filePath
46123
+ }),
46124
+ validate,
46125
+ global
46126
+ });
46127
+ }
46128
+ toRulesyncMcp() {
46129
+ const servers = this.json.mcpServers;
46130
+ const mcpServers = {};
46131
+ if (isMcpServers(servers)) {
46132
+ for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
46133
+ }
46134
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
46135
+ }
46136
+ validate() {
46137
+ return {
46138
+ success: true,
46139
+ error: null
46140
+ };
46141
+ }
46142
+ /**
46143
+ * `firebender.json` is a user-managed file shared with other features (the
46144
+ * ignore feature's `ignore`, the subagents feature's `agents`), so it must
46145
+ * not be deleted.
46146
+ */
46147
+ isDeletable() {
46148
+ return false;
46149
+ }
46150
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46151
+ return new FirebenderMcp({
46152
+ outputRoot,
46153
+ relativeDirPath,
46154
+ relativeFilePath,
46155
+ fileContent: "{}",
46156
+ validate: false,
46157
+ global
46158
+ });
46159
+ }
46160
+ };
46161
+ //#endregion
45320
46162
  //#region src/features/mcp/gitlabduo-mcp.ts
45321
46163
  const CANONICAL_TO_GITLABDUO_TRANSPORT = {
45322
46164
  local: "stdio",
@@ -52008,6 +52850,248 @@ var WarpcliMcp = class extends WarpMcp {
52008
52850
  }
52009
52851
  };
52010
52852
  //#endregion
52853
+ //#region src/constants/xum-paths.ts
52854
+ /**
52855
+ * Xum configuration-layout conventions.
52856
+ *
52857
+ * Xum (Coder's desktop app and `xum run` CLI for running coding agents in
52858
+ * parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
52859
+ * directory at the project root and the matching user assets under `~/.xum/`.
52860
+ * Legacy `.mux/` files are still read when the matching `.xum/` file is
52861
+ * missing; rulesync writes only the `.xum/` paths.
52862
+ *
52863
+ * @see https://xum.cdr.dev/reference/mux-compatibility
52864
+ */
52865
+ const XUM_DIR = ".xum";
52866
+ const XUM_RULE_FILE_NAME = "AGENTS.md";
52867
+ const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
52868
+ const XUM_MCP_FILE_NAME = "mcp.jsonc";
52869
+ const XUM_AGENTS_DIR_PATH = join(XUM_DIR, "agents");
52870
+ const XUM_SKILLS_DIR_PATH = join(XUM_DIR, "skills");
52871
+ //#endregion
52872
+ //#region src/features/mcp/xum-mcp.ts
52873
+ const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
52874
+ /** Characters that never need quoting in a POSIX shell word. */
52875
+ const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
52876
+ /**
52877
+ * A command string free of quotes, escapes, expansions and operators, which
52878
+ * can be split on whitespace back into argv without changing its meaning.
52879
+ */
52880
+ const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
52881
+ /** Single-quote one argv element for the POSIX shell Xum runs commands in. */
52882
+ function quoteShellWord(value) {
52883
+ if (SHELL_SAFE_WORD.test(value)) return value;
52884
+ return `'${value.replaceAll("'", `'\\''`)}'`;
52885
+ }
52886
+ /**
52887
+ * Build the shell command string a Xum stdio server is launched with. A
52888
+ * `command` string is kept verbatim (Xum itself treats it as a raw shell
52889
+ * string, so `npx -y pkg` authored as one string keeps working); an explicit
52890
+ * argv (`command` as an array) and every `args` element are quoted where
52891
+ * needed, so `/opt/my tools/srv` stays one word.
52892
+ */
52893
+ function buildCommandString(server) {
52894
+ const args = server.args ?? [];
52895
+ if (Array.isArray(server.command)) {
52896
+ const [executable] = server.command;
52897
+ if (executable === void 0 || executable.trim() === "") return void 0;
52898
+ return [...server.command, ...args].map(quoteShellWord).join(" ");
52899
+ }
52900
+ if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
52901
+ return [server.command, ...args.map(quoteShellWord)].join(" ");
52902
+ }
52903
+ /** Split a plain command string back into `command` + `args` when it is safe to. */
52904
+ function splitCommandString(command) {
52905
+ const trimmed = command.trim();
52906
+ if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
52907
+ const [head, ...rest] = trimmed.split(/\s+/);
52908
+ if (head === void 0) return { command };
52909
+ return rest.length > 0 ? {
52910
+ command: head,
52911
+ args: rest
52912
+ } : { command: head };
52913
+ }
52914
+ function resolveXumServerKind(server) {
52915
+ const candidate = server.type ?? server.transport;
52916
+ if (candidate === "stdio" || candidate === "local") return "stdio";
52917
+ if (candidate === "http" || candidate === "streamable-http") return "http";
52918
+ if (candidate === "sse") return "sse";
52919
+ if (candidate !== void 0) return { unsupported: candidate };
52920
+ if (server.command !== void 0) return "stdio";
52921
+ const url = server.url ?? server.httpUrl;
52922
+ if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
52923
+ }
52924
+ /** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
52925
+ function convertServerToXum({ name, server, logger }) {
52926
+ const skip = (reason) => {
52927
+ warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
52928
+ return null;
52929
+ };
52930
+ const kind = resolveXumServerKind(server);
52931
+ if (kind === void 0) return skip("it has neither a command nor a URL");
52932
+ if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
52933
+ const settings = {};
52934
+ if (server.disabled === true) settings.disabled = true;
52935
+ if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
52936
+ if (kind !== "stdio") {
52937
+ const url = server.url ?? server.httpUrl;
52938
+ if (typeof url !== "string" || url === "") return skip("it has no URL");
52939
+ return {
52940
+ ...kind !== "auto" && { transport: kind },
52941
+ url,
52942
+ ...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
52943
+ ...settings
52944
+ };
52945
+ }
52946
+ const command = buildCommandString(server);
52947
+ if (command === void 0) return skip("it has no command");
52948
+ const dropped = [...isPlainObject$1(server.env) && Object.keys(server.env).length > 0 ? ["env"] : [], ...typeof server.cwd === "string" ? ["cwd"] : []];
52949
+ if (dropped.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets ${dropped.join(" and ")}, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers with your shell environment in the workspace directory).`);
52950
+ return Object.keys(settings).length > 0 ? {
52951
+ command,
52952
+ ...settings
52953
+ } : command;
52954
+ }
52955
+ /** Copy only the string values of a header map (secret references have no canonical form). */
52956
+ function copyStringHeaders(value) {
52957
+ if (!isPlainObject$1(value)) return void 0;
52958
+ const result = {};
52959
+ for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
52960
+ return Object.keys(result).length > 0 ? result : void 0;
52961
+ }
52962
+ /** Read a Xum `servers` map back into the canonical shape. */
52963
+ function convertFromXumFormat(servers) {
52964
+ if (!isPlainObject$1(servers)) return {};
52965
+ const result = {};
52966
+ for (const [name, entry] of Object.entries(servers)) {
52967
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
52968
+ if (typeof entry === "string") {
52969
+ result[name] = splitCommandString(entry);
52970
+ continue;
52971
+ }
52972
+ if (!isPlainObject$1(entry)) continue;
52973
+ const server = {};
52974
+ if (typeof entry.url === "string") {
52975
+ if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
52976
+ server.url = entry.url;
52977
+ const headers = copyStringHeaders(entry.headers);
52978
+ if (headers !== void 0) server.headers = headers;
52979
+ } else if (typeof entry.command === "string") Object.assign(server, splitCommandString(entry.command));
52980
+ else continue;
52981
+ if (entry.disabled === true) server.disabled = true;
52982
+ if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
52983
+ result[name] = server;
52984
+ }
52985
+ return result;
52986
+ }
52987
+ /**
52988
+ * Xum MCP configuration.
52989
+ *
52990
+ * Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
52991
+ * (project; a server there wins over the user one of the same name). Both are
52992
+ * a JSONC document with a `servers` map:
52993
+ *
52994
+ * - a stdio server is a shell command string (the object form `{ command }`
52995
+ * carries `disabled` / `toolAllowlist`); Xum's mcp.jsonc has no `env` or
52996
+ * `cwd`, so those are dropped with a warning;
52997
+ * - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
52998
+ * (the transport defaults to auto-detection).
52999
+ *
53000
+ * rulesync owns only `servers`; other top-level keys (Xum writes
53001
+ * `enabledPluginServers` into the user file) are preserved. The project file
53002
+ * may be removed by `--delete`; the user file never is.
53003
+ *
53004
+ * @see https://xum.cdr.dev/config/mcp-servers
53005
+ * @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
53006
+ */
53007
+ var XumMcp = class XumMcp extends ToolMcp {
53008
+ json;
53009
+ constructor(params) {
53010
+ super(params);
53011
+ this.json = parseSharedConfig({
53012
+ format: "jsonc",
53013
+ fileContent: this.fileContent,
53014
+ filePath: join(this.relativeDirPath, this.relativeFilePath),
53015
+ invalidRootPolicy: "error",
53016
+ jsoncParseErrors: "error"
53017
+ });
53018
+ }
53019
+ getJson() {
53020
+ return this.json;
53021
+ }
53022
+ isDeletable() {
53023
+ return !this.global;
53024
+ }
53025
+ static getSettablePaths(_options = {}) {
53026
+ return {
53027
+ relativeDirPath: XUM_DIR,
53028
+ relativeFilePath: XUM_MCP_FILE_NAME
53029
+ };
53030
+ }
53031
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
53032
+ const paths = this.getSettablePaths({ global });
53033
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
53034
+ return new XumMcp({
53035
+ outputRoot,
53036
+ relativeDirPath: paths.relativeDirPath,
53037
+ relativeFilePath: paths.relativeFilePath,
53038
+ fileContent,
53039
+ validate,
53040
+ global
53041
+ });
53042
+ }
53043
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
53044
+ const paths = this.getSettablePaths({ global });
53045
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53046
+ const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
53047
+ const servers = {};
53048
+ for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
53049
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53050
+ const entry = convertServerToXum({
53051
+ name,
53052
+ server,
53053
+ logger
53054
+ });
53055
+ if (entry !== null) servers[name] = entry;
53056
+ }
53057
+ return new XumMcp({
53058
+ outputRoot,
53059
+ relativeDirPath: paths.relativeDirPath,
53060
+ relativeFilePath: paths.relativeFilePath,
53061
+ fileContent: applySharedConfigPatch({
53062
+ fileKey: sharedConfigFileKey(paths),
53063
+ feature: "mcp",
53064
+ existingContent,
53065
+ patch: { servers },
53066
+ filePath,
53067
+ logger
53068
+ }),
53069
+ validate,
53070
+ global
53071
+ });
53072
+ }
53073
+ toRulesyncMcp() {
53074
+ const mcpServers = convertFromXumFormat(this.json.servers);
53075
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
53076
+ }
53077
+ validate() {
53078
+ return {
53079
+ success: true,
53080
+ error: null
53081
+ };
53082
+ }
53083
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
53084
+ return new XumMcp({
53085
+ outputRoot,
53086
+ relativeDirPath,
53087
+ relativeFilePath,
53088
+ fileContent: "{}",
53089
+ validate: false,
53090
+ global
53091
+ });
53092
+ }
53093
+ };
53094
+ //#endregion
52011
53095
  //#region src/features/mcp/zcode-mcp.ts
52012
53096
  /**
52013
53097
  * Single spelling of the config.json codec/policy, matching the
@@ -52269,8 +53353,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
52269
53353
  * and unknown keys pass through untouched so an import keeps whatever a
52270
53354
  * hand-authored entry declared. `timeoutMs` is lifted back into the canonical
52271
53355
  * `timeout` (milliseconds); when the server also states a `timeout`, ZCode
52272
- * has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
52273
- * `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
53356
+ * has already dropped that one, so `timeoutMs` replaces it. Any record
53357
+ * `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
52274
53358
  * never emitted to another tool. Generation back out is a whitelist, so only
52275
53359
  * the keys `convertToZcodeFormat` writes are re-emitted.
52276
53360
  *
@@ -52296,7 +53380,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
52296
53380
  continue;
52297
53381
  }
52298
53382
  if (key === "timeout" && typeof config.timeoutMs === "number") continue;
52299
- if (key === "oauth" && isZcodeOauth(value)) {
53383
+ if (key === "oauth" && isRecord$1(value)) {
52300
53384
  converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
52301
53385
  continue;
52302
53386
  }
@@ -52942,6 +54026,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
52942
54026
  supportsDisabledTools: true
52943
54027
  }
52944
54028
  }],
54029
+ ["firebender", {
54030
+ class: FirebenderMcp,
54031
+ meta: {
54032
+ supportsProject: true,
54033
+ supportsGlobal: true,
54034
+ supportsEnabledTools: false,
54035
+ supportsDisabledTools: false
54036
+ }
54037
+ }],
52945
54038
  ["goose", {
52946
54039
  class: GooseMcp,
52947
54040
  meta: {
@@ -53212,6 +54305,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
53212
54305
  supportsDisabledTools: false
53213
54306
  }
53214
54307
  }],
54308
+ ["xum", {
54309
+ class: XumMcp,
54310
+ meta: {
54311
+ supportsProject: true,
54312
+ supportsGlobal: true,
54313
+ supportsEnabledTools: true,
54314
+ supportsDisabledTools: false
54315
+ }
54316
+ }],
53215
54317
  ["devin", {
53216
54318
  class: DevinMcp,
53217
54319
  meta: {
@@ -76423,6 +77525,11 @@ const TOOL_DISPLAY = [
76423
77525
  label: "Factory Droid",
76424
77526
  group: "ai"
76425
77527
  },
77528
+ {
77529
+ key: "firebender",
77530
+ label: "Firebender",
77531
+ group: "ai"
77532
+ },
76426
77533
  {
76427
77534
  key: "opencode",
76428
77535
  label: "OpenCode",
@@ -76578,6 +77685,11 @@ const TOOL_DISPLAY = [
76578
77685
  label: "JetBrains AI Assistant",
76579
77686
  group: "ai"
76580
77687
  },
77688
+ {
77689
+ key: "androidstudio",
77690
+ label: "Gemini in Android Studio",
77691
+ group: "ai"
77692
+ },
76581
77693
  {
76582
77694
  key: "junie",
76583
77695
  label: "JetBrains Junie",
@@ -76613,6 +77725,11 @@ const TOOL_DISPLAY = [
76613
77725
  label: "Warp Agent CLI",
76614
77726
  group: "ai"
76615
77727
  },
77728
+ {
77729
+ key: "xum",
77730
+ label: "Xum",
77731
+ group: "ai"
77732
+ },
76616
77733
  {
76617
77734
  key: "replit",
76618
77735
  label: "Replit",
@@ -77757,6 +78874,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
77757
78874
  }
77758
78875
  };
77759
78876
  //#endregion
78877
+ //#region src/features/skills/androidstudio-skill.ts
78878
+ /**
78879
+ * Gemini in Android Studio skills follow the Agent Skills open standard
78880
+ * (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
78881
+ * reuses {@link AgentsSkillsSkill} with Android Studio's own location:
78882
+ * `.android-studio/skills/` at the project root (project) or in the home
78883
+ * directory (global). The shared `.agents/skills/` location Android Studio also
78884
+ * reads belongs to the `agentsskills` target.
78885
+ *
78886
+ * @see https://developer.android.com/studio/gemini/skills
78887
+ */
78888
+ var AndroidstudioSkill = class extends AgentsSkillsSkill {
78889
+ static getSettablePaths(_options = {}) {
78890
+ return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
78891
+ }
78892
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
78893
+ const targets = rulesyncSkill.getFrontmatter().targets;
78894
+ return targets.includes("*") || targets.includes("androidstudio");
78895
+ }
78896
+ };
78897
+ //#endregion
77760
78898
  //#region src/features/skills/antigravity-plugin-skill.ts
77761
78899
  var AntigravityPluginSkill = class extends AntigravityIdeSkill {
77762
78900
  static isTargetedByRulesyncSkill(rulesyncSkill) {
@@ -81225,6 +82363,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
81225
82363
  }
81226
82364
  };
81227
82365
  //#endregion
82366
+ //#region src/features/skills/firebender-skill.ts
82367
+ /**
82368
+ * Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
82369
+ * required `name` and `description` frontmatter, plus optional supporting
82370
+ * files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
82371
+ * recommended locations: `.firebender/skills/` (project) and
82372
+ * `~/.firebender/skills/` (user).
82373
+ *
82374
+ * @see https://docs.firebender.com/api-reference/skills
82375
+ */
82376
+ var FirebenderSkill = class extends AgentsSkillsSkill {
82377
+ static getSettablePaths(_options = {}) {
82378
+ return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
82379
+ }
82380
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
82381
+ const targets = rulesyncSkill.getFrontmatter().targets;
82382
+ return targets.includes("*") || targets.includes("firebender");
82383
+ }
82384
+ };
82385
+ //#endregion
81228
82386
  //#region src/features/skills/gitlabduo-skill.ts
81229
82387
  /**
81230
82388
  * GitLab Duo skills follow the Agent Skills standard (`name` and `description`
@@ -84760,6 +85918,27 @@ var WarpSkill = class WarpSkill extends ToolSkill {
84760
85918
  }
84761
85919
  };
84762
85920
  //#endregion
85921
+ //#region src/features/skills/xum-skill.ts
85922
+ /**
85923
+ * Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
85924
+ * required `name` and `description` frontmatter; the directory name is
85925
+ * lowercase letters, digits and single hyphens up to 64 characters, and
85926
+ * `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
85927
+ * Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
85928
+ * Within one directory `.xum/skills` wins over `.agents/skills`.
85929
+ *
85930
+ * @see https://xum.cdr.dev/agents/agent-skills
85931
+ */
85932
+ var XumSkill = class extends AgentsSkillsSkill {
85933
+ static getSettablePaths(_options = {}) {
85934
+ return { relativeDirPath: XUM_SKILLS_DIR_PATH };
85935
+ }
85936
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
85937
+ const targets = rulesyncSkill.getFrontmatter().targets;
85938
+ return targets.includes("*") || targets.includes("xum");
85939
+ }
85940
+ };
85941
+ //#endregion
84763
85942
  //#region src/features/skills/zcode-skill.ts
84764
85943
  const ZcodeSkillFrontmatterSchema = z.looseObject({
84765
85944
  name: z.string(),
@@ -85175,6 +86354,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85175
86354
  supportsGlobal: false
85176
86355
  }
85177
86356
  }],
86357
+ ["androidstudio", {
86358
+ class: AndroidstudioSkill,
86359
+ meta: {
86360
+ supportsProject: true,
86361
+ supportsSimulated: false,
86362
+ supportsGlobal: true
86363
+ }
86364
+ }],
85178
86365
  ["amp", {
85179
86366
  class: AmpSkill,
85180
86367
  meta: {
@@ -85360,6 +86547,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85360
86547
  supportsGlobal: true
85361
86548
  }
85362
86549
  }],
86550
+ ["firebender", {
86551
+ class: FirebenderSkill,
86552
+ meta: {
86553
+ supportsProject: true,
86554
+ supportsSimulated: false,
86555
+ supportsGlobal: true
86556
+ }
86557
+ }],
85363
86558
  ["goose", {
85364
86559
  class: GooseSkill,
85365
86560
  meta: {
@@ -85610,6 +86805,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85610
86805
  supportsGlobal: true
85611
86806
  }
85612
86807
  }],
86808
+ ["xum", {
86809
+ class: XumSkill,
86810
+ meta: {
86811
+ supportsProject: true,
86812
+ supportsSimulated: false,
86813
+ supportsGlobal: true
86814
+ }
86815
+ }],
85613
86816
  ["devin", {
85614
86817
  class: DevinSkill,
85615
86818
  meta: {
@@ -89456,6 +90659,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
89456
90659
  }
89457
90660
  };
89458
90661
  //#endregion
90662
+ //#region src/features/subagents/firebender-subagent.ts
90663
+ /**
90664
+ * Frontmatter of a Firebender agent file. `tools` is a comma-separated string
90665
+ * of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
90666
+ * (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
90667
+ * model ID; `callable` lets the main agent delegate to it. Unknown keys are
90668
+ * preserved.
90669
+ *
90670
+ * @see https://docs.firebender.com/api-reference/agents
90671
+ */
90672
+ const FirebenderSubagentFrontmatterSchema = z.looseObject({
90673
+ name: z.optional(z.string()),
90674
+ description: z.optional(z.string()),
90675
+ color: z.optional(z.string()),
90676
+ icon: z.optional(z.string()),
90677
+ tools: z.optional(z.string()),
90678
+ model: z.optional(z.string()),
90679
+ callable: z.optional(z.boolean())
90680
+ });
90681
+ /**
90682
+ * The `agents` entry Firebender resolves to an agent file rulesync writes:
90683
+ * relative to the project root for the project config, and to `~/.firebender`
90684
+ * for the personal one.
90685
+ */
90686
+ function toAgentsEntry({ relativeFilePath, global }) {
90687
+ const fileName = toPosixPath(relativeFilePath);
90688
+ return global ? posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
90689
+ }
90690
+ /**
90691
+ * The agent file name an existing `agents` entry points at when it lies
90692
+ * directly inside the directory rulesync writes agents to
90693
+ * (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
90694
+ * in the personal config), or `undefined` for any other entry
90695
+ * (`~/firebender-agents/...`, an absolute path, a nested path).
90696
+ */
90697
+ function managedAgentFileName({ entry, global }) {
90698
+ const normalized = toPosixPath(entry).replace(/^\.\//, "");
90699
+ const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
90700
+ const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
90701
+ const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
90702
+ return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
90703
+ }
90704
+ /**
90705
+ * Subagent generator for Firebender.
90706
+ *
90707
+ * Each agent is a Markdown file with YAML frontmatter, written to
90708
+ * `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
90709
+ * (global). Firebender only loads agents listed in the `agents` array of
90710
+ * `firebender.json`, so generation also registers every file there — see
90711
+ * {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
90712
+ * delegated to, so `callable: true` is written unless the `firebender:`
90713
+ * section says otherwise.
90714
+ *
90715
+ * @see https://docs.firebender.com/api-reference/agents
90716
+ */
90717
+ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
90718
+ frontmatter;
90719
+ body;
90720
+ constructor({ frontmatter, body, ...rest }) {
90721
+ if (rest.validate !== false) {
90722
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
90723
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
90724
+ }
90725
+ super({ ...rest });
90726
+ this.frontmatter = frontmatter;
90727
+ this.body = body;
90728
+ }
90729
+ static getSettablePaths(_options = {}) {
90730
+ return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
90731
+ }
90732
+ /**
90733
+ * Generation also read-modify-writes the `agents` key of `firebender.json`,
90734
+ * so the write is declared for the shared-file order derivation.
90735
+ */
90736
+ static getExtraSharedWritePaths({ global = false } = {}) {
90737
+ return [{
90738
+ relativeDirPath: getFirebenderConfigDir({ global }),
90739
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
90740
+ }];
90741
+ }
90742
+ getFrontmatter() {
90743
+ return this.frontmatter;
90744
+ }
90745
+ getBody() {
90746
+ return this.body;
90747
+ }
90748
+ toRulesyncSubagent() {
90749
+ const { name, description, callable, ...rest } = this.frontmatter;
90750
+ const firebenderSection = {
90751
+ ...rest,
90752
+ ...callable !== true && { callable: callable ?? false }
90753
+ };
90754
+ return new RulesyncSubagent({
90755
+ outputRoot: ".",
90756
+ frontmatter: {
90757
+ targets: ["*"],
90758
+ name: name ?? basename(this.getRelativeFilePath(), ".md"),
90759
+ description,
90760
+ ...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
90761
+ },
90762
+ body: this.body,
90763
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
90764
+ relativeFilePath: this.getRelativeFilePath(),
90765
+ validate: true
90766
+ });
90767
+ }
90768
+ /**
90769
+ * Every targeted subagent becomes its own agent file, plus one
90770
+ * {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
90771
+ * The registry is emitted even when no subagent is targeted, so agents a
90772
+ * previous run registered are retracted once their files are gone. The
90773
+ * agent directory is listed up front so the registry can tell a registered
90774
+ * file that still exists (kept, whoever wrote it) from a stale one.
90775
+ */
90776
+ static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
90777
+ const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
90778
+ const agentsDirEscapesRoot = await writablePathEscapesRoot({
90779
+ rootPath: outputRoot,
90780
+ targetPath: join(outputRoot, agentsDirPath, "agent.md")
90781
+ });
90782
+ const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames(join(outputRoot, agentsDirPath)));
90783
+ const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
90784
+ outputRoot,
90785
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
90786
+ rulesyncSubagent,
90787
+ global
90788
+ }));
90789
+ const registry = new FirebenderAgentsRegistry({
90790
+ outputRoot,
90791
+ relativeDirPath: getFirebenderConfigDir({ global }),
90792
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
90793
+ entries: subagents.map((subagent) => toAgentsEntry({
90794
+ relativeFilePath: subagent.getRelativeFilePath(),
90795
+ global
90796
+ })),
90797
+ existingAgentFileNames,
90798
+ deleteOrphans,
90799
+ frozen: agentsDirEscapesRoot,
90800
+ global,
90801
+ logger
90802
+ });
90803
+ return [...subagents, registry];
90804
+ }
90805
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
90806
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
90807
+ const firebenderSection = rulesyncFrontmatter.firebender ?? {};
90808
+ const frontmatter = {
90809
+ name: rulesyncFrontmatter.name,
90810
+ ...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
90811
+ callable: true,
90812
+ ...firebenderSection
90813
+ };
90814
+ const body = rulesyncSubagent.getBody();
90815
+ return new FirebenderSubagent({
90816
+ outputRoot,
90817
+ frontmatter,
90818
+ body,
90819
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
90820
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
90821
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
90822
+ validate,
90823
+ global
90824
+ });
90825
+ }
90826
+ validate() {
90827
+ if (!this.frontmatter) return {
90828
+ success: true,
90829
+ error: null
90830
+ };
90831
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
90832
+ if (result.success) return {
90833
+ success: true,
90834
+ error: null
90835
+ };
90836
+ return {
90837
+ success: false,
90838
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
90839
+ };
90840
+ }
90841
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
90842
+ return this.isTargetedByRulesyncSubagentDefault({
90843
+ rulesyncSubagent,
90844
+ toolTarget: "firebender"
90845
+ });
90846
+ }
90847
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
90848
+ const paths = this.getSettablePaths({ global });
90849
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
90850
+ const fileContent = await readFileContent(filePath);
90851
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
90852
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
90853
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
90854
+ return new FirebenderSubagent({
90855
+ outputRoot,
90856
+ relativeDirPath: paths.relativeDirPath,
90857
+ relativeFilePath,
90858
+ frontmatter: result.data,
90859
+ body: body.trim(),
90860
+ fileContent,
90861
+ validate,
90862
+ global
90863
+ });
90864
+ }
90865
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
90866
+ return new FirebenderSubagent({
90867
+ outputRoot,
90868
+ relativeDirPath,
90869
+ relativeFilePath,
90870
+ frontmatter: {},
90871
+ body: "",
90872
+ fileContent: "",
90873
+ validate: false
90874
+ });
90875
+ }
90876
+ };
90877
+ /**
90878
+ * The `agents` array of `firebender.json`, which Firebender requires to load
90879
+ * an agent file. Every generated agent is registered. An existing entry that
90880
+ * points into rulesync's agent directory is kept while its file exists (a
90881
+ * hand-written agent there stays registered) and dropped, with a warning, once
90882
+ * the file is gone. Under `--delete` the orphan sweep removes every agent file
90883
+ * there that this run does not generate, but only after this file is written,
90884
+ * so in that mode such entries are dropped here too and a single run
90885
+ * converges. Every other entry and every other key of the file is kept.
90886
+ * The array is merged into the file on disk at write time and the file itself
90887
+ * is never deleted.
90888
+ */
90889
+ var FirebenderAgentsRegistry = class extends ToolSubagent {
90890
+ entries;
90891
+ existingAgentFileNames;
90892
+ deleteOrphans;
90893
+ frozen;
90894
+ logger;
90895
+ mergedContent = null;
90896
+ constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
90897
+ super({
90898
+ ...rest,
90899
+ fileContent: ""
90900
+ });
90901
+ this.entries = entries;
90902
+ this.existingAgentFileNames = existingAgentFileNames;
90903
+ this.deleteOrphans = deleteOrphans;
90904
+ this.frozen = frozen;
90905
+ this.logger = logger;
90906
+ }
90907
+ getEntries() {
90908
+ return this.entries;
90909
+ }
90910
+ isDeletable() {
90911
+ return false;
90912
+ }
90913
+ shouldMergeExistingFileContent() {
90914
+ return true;
90915
+ }
90916
+ setFileContent(newFileContent) {
90917
+ this.mergedContent = this.mergeInto(newFileContent);
90918
+ }
90919
+ getFileContent() {
90920
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
90921
+ return this.mergedContent;
90922
+ }
90923
+ mergeInto(existingContent) {
90924
+ const paths = {
90925
+ relativeDirPath: this.getRelativeDirPath(),
90926
+ relativeFilePath: this.getRelativeFilePath()
90927
+ };
90928
+ const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
90929
+ const existing = parseSharedConfig({
90930
+ format: "json",
90931
+ fileContent: existingContent,
90932
+ filePath,
90933
+ invalidRootPolicy: "error"
90934
+ });
90935
+ if (this.frozen) return applySharedConfigPatch({
90936
+ fileKey: sharedConfigFileKey(paths),
90937
+ feature: "subagents",
90938
+ existingContent,
90939
+ patch: { agents: existing.agents },
90940
+ filePath,
90941
+ logger: this.logger
90942
+ });
90943
+ const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
90944
+ const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
90945
+ entry,
90946
+ global: this.global
90947
+ }) ?? entry, entry]));
90948
+ const agents = [];
90949
+ const placed = /* @__PURE__ */ new Set();
90950
+ const dropped = [];
90951
+ for (const entry of existingEntries) {
90952
+ const fileName = typeof entry === "string" ? managedAgentFileName({
90953
+ entry,
90954
+ global: this.global
90955
+ }) : void 0;
90956
+ if (fileName === void 0) {
90957
+ agents.push(entry);
90958
+ continue;
90959
+ }
90960
+ const generated = generatedByFileName.get(fileName);
90961
+ if (generated !== void 0) {
90962
+ if (!placed.has(fileName)) {
90963
+ agents.push(generated);
90964
+ placed.add(fileName);
90965
+ }
90966
+ } else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
90967
+ else dropped.push(String(entry));
90968
+ }
90969
+ for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
90970
+ agents.push(entry);
90971
+ placed.add(fileName);
90972
+ }
90973
+ if (dropped.length > 0) this.logger?.warn(`Unregistered Firebender agent(s) ${dropped.map((entry) => quoteValueForWarning(entry)).join(", ")} from ${filePath}: ` + (this.deleteOrphans ? "--delete removes agent files that .rulesync/subagents/ no longer generates." : "the agent file no longer exists."));
90974
+ return applySharedConfigPatch({
90975
+ fileKey: sharedConfigFileKey(paths),
90976
+ feature: "subagents",
90977
+ existingContent,
90978
+ patch: { agents: agents.length > 0 ? agents : void 0 },
90979
+ filePath,
90980
+ logger: this.logger
90981
+ });
90982
+ }
90983
+ toRulesyncSubagent() {
90984
+ throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
90985
+ }
90986
+ validate() {
90987
+ return {
90988
+ success: true,
90989
+ error: null
90990
+ };
90991
+ }
90992
+ };
90993
+ //#endregion
89459
90994
  //#region src/features/subagents/goose-subagent.ts
89460
90995
  /**
89461
90996
  * Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
@@ -89743,13 +91278,13 @@ function subagentSlug(relativeFilePath) {
89743
91278
  return basename(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
89744
91279
  }
89745
91280
  function hermesCommandName(slug) {
89746
- return `rulesync_subagent_${slug}`;
91281
+ return hermesSlashName(`rulesync-subagent-${slug}`);
89747
91282
  }
89748
91283
  function getPluginManifestContent() {
89749
91284
  return [
89750
91285
  "name: rulesync-subagents",
89751
91286
  "version: \"1.0.0\"",
89752
- "description: Exposes RuleSync subagents as Hermes native delegation commands.",
91287
+ "description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
89753
91288
  ""
89754
91289
  ].join("\n");
89755
91290
  }
@@ -89757,9 +91292,12 @@ function getPluginInitContent() {
89757
91292
  return `"""RuleSync-generated Hermes subagent commands."""
89758
91293
 
89759
91294
  import json
91295
+ import logging
91296
+ import re
89760
91297
  from pathlib import Path
89761
91298
 
89762
91299
 
91300
+ logger = logging.getLogger(__name__)
89763
91301
  SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
89764
91302
 
89765
91303
 
@@ -89779,52 +91317,71 @@ def _load_subagents():
89779
91317
  return subagents
89780
91318
 
89781
91319
 
89782
- def _register_subagent(ctx, subagent):
91320
+ def _command_name(subagent, slug):
91321
+ # The spec carries the name Rulesync normalized; Hermes' messaging gateway
91322
+ # resolves plugin commands with "_" turned into "-", so keep it hyphenated.
91323
+ hermes = subagent.get("hermes")
91324
+ name = hermes.get("command") if isinstance(hermes, dict) else None
91325
+ name = name or f"rulesync-subagent-{slug}"
91326
+ # Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
91327
+ name = re.sub(r"[ _]", "-", str(name).lower())
91328
+ name = re.sub(r"[^a-z0-9-]", "", name)
91329
+ return re.sub(r"-{2,}", "-", name).strip("-")
91330
+
91331
+
91332
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
91333
+
91334
+ def _register_subagent(ctx, subagent, registered):
89783
91335
  slug = subagent.get("slug")
89784
91336
  if not slug:
89785
91337
  return
89786
91338
 
89787
- command_name = f"rulesync_subagent_{slug}"
91339
+ command_name = _command_name(subagent, slug)
91340
+ if command_name in registered:
91341
+ logger.warning(
91342
+ "rulesync-subagents: %s and %s both map to /%s; keeping the first.",
91343
+ registered[command_name],
91344
+ subagent.get("_path"),
91345
+ command_name,
91346
+ )
91347
+ return
91348
+ registered[command_name] = subagent.get("_path")
91349
+
89788
91350
  name = subagent.get("name") or slug
89789
91351
  description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
89790
91352
  system_prompt = subagent.get("prompt") or ""
89791
91353
 
89792
91354
  def handler(args=None, **kwargs):
89793
- del kwargs
89794
91355
  user_context = ""
89795
91356
  if isinstance(args, dict):
89796
91357
  user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
89797
91358
  elif args is not None:
89798
91359
  user_context = str(args)
89799
91360
 
89800
- context_parts = []
91361
+ # A slash command runs outside any agent turn, so calling delegate_task
91362
+ # here fails with "delegate_task requires a parent agent context".
91363
+ # Queue the brief as a user message instead; the agent then delegates.
91364
+ parts = [
91365
+ f"Use the delegate_task tool to hand the following task to a subagent "
91366
+ f"acting as the {name} RuleSync subagent.",
91367
+ f"Goal: {description}",
91368
+ ]
89801
91369
  if system_prompt:
89802
- context_parts.append(system_prompt)
91370
+ parts.append(f"Subagent instructions:\\n{system_prompt}")
89803
91371
  if user_context:
89804
- context_parts.append(user_context)
91372
+ parts.append(f"Task details:\\n{user_context}")
89805
91373
 
89806
- # delegate_task takes no model-facing "toolsets" argument: a subagent
89807
- # inherits the parent's enabled toolsets (role="orchestrator" is the
89808
- # only knob that changes them). "tasks" is the only shape its schema
89809
- # advertises; a top-level "goal" is an unadvertised legacy path.
89810
- return ctx.dispatch_tool(
89811
- "delegate_task",
89812
- {
89813
- "tasks": [
89814
- {
89815
- "goal": description,
89816
- "context": "\\n\\n".join(context_parts),
89817
- },
89818
- ],
89819
- },
89820
- )
91374
+ label = f"the {name} subagent task"
91375
+ error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
91376
+ return error or f"Queued {label}; the agent will delegate it."
89821
91377
 
89822
91378
  ctx.register_command(command_name, handler, description)
89823
91379
 
89824
91380
 
89825
91381
  def register(ctx):
91382
+ registered = {}
89826
91383
  for subagent in _load_subagents():
89827
- _register_subagent(ctx, subagent)
91384
+ _register_subagent(ctx, subagent, registered)
89828
91385
  `;
89829
91386
  }
89830
91387
  function getEnabledPluginConfigContent({ currentContent, global }) {
@@ -89855,7 +91412,7 @@ function getSubagentSpec(rulesyncSubagent) {
89855
91412
  prompt: rulesyncSubagent.getBody(),
89856
91413
  hermes: {
89857
91414
  command: hermesCommandName(slug),
89858
- dispatch: "delegate_task"
91415
+ dispatch: "inject_message"
89859
91416
  }
89860
91417
  };
89861
91418
  }
@@ -92647,6 +94204,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
92647
94204
  }
92648
94205
  };
92649
94206
  //#endregion
94207
+ //#region src/features/subagents/xum-subagent.ts
94208
+ /**
94209
+ * The built-in agent a rulesync subagent inherits from when its `xum` block
94210
+ * names neither a `base` nor its own `tools`. Xum gives an agent no tools at
94211
+ * all unless some agent in its `base` chain declares them, so a definition
94212
+ * without either would be a prompt that cannot read or edit anything.
94213
+ */
94214
+ const XUM_DEFAULT_BASE_AGENT = "exec";
94215
+ /**
94216
+ * Frontmatter of a Xum agent definition. `name` (the display name) is
94217
+ * required; the other documented fields are typed loosely and unknown keys are
94218
+ * kept (looseObject) so a round trip never drops them.
94219
+ *
94220
+ * @see https://xum.cdr.dev/agents
94221
+ */
94222
+ const XumSubagentFrontmatterSchema = z.looseObject({
94223
+ name: z.string(),
94224
+ description: z.optional(z.string()),
94225
+ base: z.optional(z.string()),
94226
+ disabled: z.optional(z.boolean()),
94227
+ ui: z.optional(z.looseObject({})),
94228
+ prompt: z.optional(z.looseObject({})),
94229
+ subagent: z.optional(z.looseObject({})),
94230
+ ai: z.optional(z.looseObject({})),
94231
+ tools: z.optional(z.looseObject({}))
94232
+ });
94233
+ /**
94234
+ * Subagent generator for Xum (formerly Mux).
94235
+ *
94236
+ * Xum reads custom agent definitions — Markdown with YAML frontmatter — from
94237
+ * the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
94238
+ * body is the agent's instruction prompt. The file name (without `.md`) is the
94239
+ * agent id, so it must be lowercase letters and digits with single `-` / `_`
94240
+ * separators; Xum skips other files with a warning.
94241
+ *
94242
+ * rulesync writes `name` and `description`, spreads the `xum` section (`base`,
94243
+ * `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
94244
+ * defaults so the definition works as a delegated subagent out of the box:
94245
+ * `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
94246
+ * when the section sets neither `base` nor `tools`, `base: exec`.
94247
+ *
94248
+ * @see https://xum.cdr.dev/agents
94249
+ */
94250
+ var XumSubagent = class XumSubagent extends ToolSubagent {
94251
+ frontmatter;
94252
+ body;
94253
+ constructor({ frontmatter, body, ...rest }) {
94254
+ if (rest.validate !== false) {
94255
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
94256
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
94257
+ }
94258
+ super({ ...rest });
94259
+ this.frontmatter = frontmatter;
94260
+ this.body = body;
94261
+ }
94262
+ static getSettablePaths(_options = {}) {
94263
+ return { relativeDirPath: XUM_AGENTS_DIR_PATH };
94264
+ }
94265
+ getFrontmatter() {
94266
+ return this.frontmatter;
94267
+ }
94268
+ getBody() {
94269
+ return this.body;
94270
+ }
94271
+ toRulesyncSubagent() {
94272
+ const { name, description, ...restFields } = this.frontmatter;
94273
+ return new RulesyncSubagent({
94274
+ outputRoot: ".",
94275
+ frontmatter: {
94276
+ targets: ["*"],
94277
+ name,
94278
+ description,
94279
+ ...Object.keys(restFields).length > 0 && { xum: restFields }
94280
+ },
94281
+ body: this.body,
94282
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
94283
+ relativeFilePath: this.getRelativeFilePath(),
94284
+ validate: true
94285
+ });
94286
+ }
94287
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
94288
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
94289
+ const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
94290
+ const subagentSection = xumSection.subagent;
94291
+ const result = XumSubagentFrontmatterSchema.safeParse({
94292
+ name: rulesyncFrontmatter.name,
94293
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
94294
+ ...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
94295
+ ...xumSection,
94296
+ subagent: {
94297
+ runnable: true,
94298
+ ...isPlainObject$1(subagentSection) ? subagentSection : {}
94299
+ }
94300
+ });
94301
+ if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
94302
+ const body = rulesyncSubagent.getBody();
94303
+ return new XumSubagent({
94304
+ outputRoot,
94305
+ frontmatter: result.data,
94306
+ body,
94307
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
94308
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
94309
+ fileContent: stringifyFrontmatter(body, result.data),
94310
+ validate
94311
+ });
94312
+ }
94313
+ validate() {
94314
+ if (!this.frontmatter) return {
94315
+ success: true,
94316
+ error: null
94317
+ };
94318
+ const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
94319
+ if (result.success) return {
94320
+ success: true,
94321
+ error: null
94322
+ };
94323
+ return {
94324
+ success: false,
94325
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
94326
+ };
94327
+ }
94328
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
94329
+ return this.isTargetedByRulesyncSubagentDefault({
94330
+ rulesyncSubagent,
94331
+ toolTarget: "xum"
94332
+ });
94333
+ }
94334
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
94335
+ const paths = this.getSettablePaths({ global });
94336
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
94337
+ const fileContent = await readFileContent(filePath);
94338
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
94339
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
94340
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
94341
+ return new XumSubagent({
94342
+ outputRoot,
94343
+ relativeDirPath: paths.relativeDirPath,
94344
+ relativeFilePath,
94345
+ frontmatter: result.data,
94346
+ body: body.trim(),
94347
+ fileContent,
94348
+ validate
94349
+ });
94350
+ }
94351
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
94352
+ return new XumSubagent({
94353
+ outputRoot,
94354
+ relativeDirPath,
94355
+ relativeFilePath,
94356
+ frontmatter: {
94357
+ name: "",
94358
+ description: ""
94359
+ },
94360
+ body: "",
94361
+ fileContent: "",
94362
+ validate: false
94363
+ });
94364
+ }
94365
+ };
94366
+ //#endregion
92650
94367
  //#region src/features/subagents/zcode-subagent.ts
92651
94368
  const ZcodeSubagentFrontmatterSchema = z.looseObject({
92652
94369
  name: z.string(),
@@ -93112,6 +94829,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93112
94829
  filePattern: "*.md"
93113
94830
  }
93114
94831
  }],
94832
+ ["firebender", {
94833
+ class: FirebenderSubagent,
94834
+ meta: {
94835
+ supportsProject: true,
94836
+ supportsSimulated: false,
94837
+ supportsGlobal: true,
94838
+ emitsEmptyAggregate: true,
94839
+ emitsEmptyAggregateOnDelete: true,
94840
+ filePattern: "*.md"
94841
+ }
94842
+ }],
93115
94843
  ["goose", {
93116
94844
  class: GooseSubagent,
93117
94845
  meta: {
@@ -93339,6 +95067,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93339
95067
  filePattern: "*.toml"
93340
95068
  }
93341
95069
  }],
95070
+ ["xum", {
95071
+ class: XumSubagent,
95072
+ meta: {
95073
+ supportsProject: true,
95074
+ supportsSimulated: false,
95075
+ supportsGlobal: true,
95076
+ filePattern: "*.md"
95077
+ }
95078
+ }],
93342
95079
  ["zcode", {
93343
95080
  class: ZcodeSubagent,
93344
95081
  meta: {
@@ -93393,6 +95130,7 @@ function compareRelativePathsShallowestFirst(left, right) {
93393
95130
  var SubagentsProcessor = class extends FeatureProcessor {
93394
95131
  toolTarget;
93395
95132
  global;
95133
+ deleteOrphans;
93396
95134
  getFactory;
93397
95135
  /**
93398
95136
  * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
@@ -93400,13 +95138,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
93400
95138
  * agents in its Pool settings are its own and must not be retracted.
93401
95139
  */
93402
95140
  rulesyncSourceDirFound = false;
93403
- constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
95141
+ constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
93404
95142
  super({
93405
95143
  outputRoot,
93406
95144
  inputRoots,
93407
95145
  dryRun,
93408
95146
  logger
93409
95147
  });
95148
+ this.deleteOrphans = deleteOrphans;
93410
95149
  const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
93411
95150
  if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
93412
95151
  this.toolTarget = result.data;
@@ -93414,7 +95153,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
93414
95153
  this.getFactory = getFactory;
93415
95154
  }
93416
95155
  emitsToolFilesForEmptySource() {
93417
- return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
95156
+ const { meta } = this.getFactory(this.toolTarget);
95157
+ if (meta.emitsEmptyAggregate !== true) return false;
95158
+ return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
93418
95159
  }
93419
95160
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
93420
95161
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
@@ -93430,11 +95171,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
93430
95171
  });
93431
95172
  if (factory.class.fromRulesyncSubagents) {
93432
95173
  if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
93433
- const toolSubagents = factory.class.fromRulesyncSubagents({
95174
+ const toolSubagents = await factory.class.fromRulesyncSubagents({
93434
95175
  outputRoot: this.outputRoot,
93435
95176
  rulesyncSubagents: targeted,
93436
95177
  global: this.global,
93437
- logger: this.logger
95178
+ logger: this.logger,
95179
+ deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
93438
95180
  });
93439
95181
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
93440
95182
  }
@@ -94180,7 +95922,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
94180
95922
  /**
94181
95923
  * Globs that match the whole project and so say nothing beyond "always".
94182
95924
  */
94183
- const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
95925
+ const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
94184
95926
  const METADATA_DELIMITER = "---";
94185
95927
  /**
94186
95928
  * Splits text into lines that keep their own line terminator, so the body can
@@ -94305,7 +96047,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
94305
96047
  */
94306
96048
  static buildMetadata(frontmatter) {
94307
96049
  const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
94308
- const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$5.has(glob));
96050
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
94309
96051
  const instructions = frontmatter.description && flattenValue(frontmatter.description);
94310
96052
  const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
94311
96053
  const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
@@ -96959,7 +98701,7 @@ function normalizePatternList(value) {
96959
98701
  * an always-on rule into a "when any file is in context" rule. Dropped on
96960
98702
  * generate and mapped back on import.
96961
98703
  */
96962
- const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
98704
+ const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
96963
98705
  /**
96964
98706
  * Rule generator for Continue (continuedev/continue), the open-source IDE
96965
98707
  * extension and CLI (`cn`).
@@ -97073,7 +98815,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
97073
98815
  if (alwaysApply === true) return;
97074
98816
  const targetGlobs = continueGlobs ?? parentGlobs;
97075
98817
  if (!targetGlobs || targetGlobs.length === 0) return;
97076
- if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
98818
+ if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
97077
98819
  return targetGlobs;
97078
98820
  }
97079
98821
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
@@ -97565,7 +99307,7 @@ const CursorRuleFrontmatterSchema = z.object({
97565
99307
  * does not already say. Same pair the Cline and Qwen Code adapters treat as
97566
99308
  * universal.
97567
99309
  */
97568
- const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
99310
+ const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
97569
99311
  var CursorRule = class CursorRule extends ToolRule {
97570
99312
  frontmatter;
97571
99313
  body;
@@ -97606,7 +99348,8 @@ var CursorRule = class CursorRule extends ToolRule {
97606
99348
  }
97607
99349
  /**
97608
99350
  * Custom parse function for Cursor MDC files
97609
- * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially
99351
+ * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
99352
+ * Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
97610
99353
  */
97611
99354
  static parseCursorFrontmatter(fileContent, filePath) {
97612
99355
  const bounds = findFrontmatterBlockBounds(fileContent);
@@ -97669,7 +99412,7 @@ var CursorRule = class CursorRule extends ToolRule {
97669
99412
  static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
97670
99413
  const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
97671
99414
  if (!targetGlobs || targetGlobs.length === 0) return;
97672
- if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$3.has(glob.trim()))) return;
99415
+ if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
97673
99416
  return targetGlobs.join(",");
97674
99417
  }
97675
99418
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
@@ -98583,6 +100326,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
98583
100326
  }
98584
100327
  };
98585
100328
  //#endregion
100329
+ //#region src/features/rules/firebender-rule.ts
100330
+ /**
100331
+ * Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
100332
+ * is one comma-separated string, and a YAML list is tolerated on import.
100333
+ */
100334
+ const FirebenderRuleFrontmatterSchema = z.looseObject({
100335
+ description: z.optional(z.nullable(z.string())),
100336
+ globs: z.optional(z.nullable(z.union([z.string(), z.array(z.string())]))),
100337
+ alwaysApply: z.optional(z.nullable(z.boolean()))
100338
+ });
100339
+ /** Globs that match every file, and so add nothing to an always-applied rule. */
100340
+ const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
100341
+ function normalizeGlobs$2(globs) {
100342
+ if (globs === null || globs === void 0) return [];
100343
+ return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
100344
+ }
100345
+ /**
100346
+ * Rule generator for Firebender.
100347
+ *
100348
+ * Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
100349
+ * project-root `AGENTS.md`, then the same pair in parent directories, then
100350
+ * `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
100351
+ * is written to `.firebender/AGENTS.md` in the project and to
100352
+ * `~/.firebender/AGENTS.md` globally, where it does not collide with the
100353
+ * project-root `AGENTS.md` the `agentsmd` target owns.
100354
+ *
100355
+ * Non-root rules: `.firebender/rules/*.mdc` (project) and
100356
+ * `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
100357
+ * frontmatter: `description`, `globs` (one comma-separated string) and
100358
+ * `alwaysApply`. A rule with specific globs is written with
100359
+ * `alwaysApply: false` and those globs; every other rule is always applied.
100360
+ *
100361
+ * @see https://docs.firebender.com/api-reference/rules
100362
+ * @see https://docs.firebender.com/api-reference/agents-md
100363
+ */
100364
+ var FirebenderRule = class FirebenderRule extends ToolRule {
100365
+ frontmatter;
100366
+ body;
100367
+ constructor({ frontmatter = {}, body, ...rest }) {
100368
+ if (rest.validate && !rest.root) {
100369
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
100370
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
100371
+ }
100372
+ const resolvedBody = body ?? rest.fileContent;
100373
+ super({
100374
+ ...rest,
100375
+ fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
100376
+ });
100377
+ this.frontmatter = frontmatter;
100378
+ this.body = resolvedBody;
100379
+ }
100380
+ static getSettablePaths({ excludeToolDir } = {}) {
100381
+ return {
100382
+ root: {
100383
+ relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
100384
+ relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
100385
+ },
100386
+ nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
100387
+ };
100388
+ }
100389
+ /**
100390
+ * The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
100391
+ * flattened `description`, then the globs joined the way Firebender's docs
100392
+ * spell them (`"*.kt, *.kts"`).
100393
+ */
100394
+ static toOutputFrontmatter(frontmatter) {
100395
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
100396
+ const globs = normalizeGlobs$2(frontmatter.globs);
100397
+ return {
100398
+ ...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
100399
+ ...description !== "" && { description },
100400
+ ...globs.length > 0 && { globs: globs.join(", ") }
100401
+ };
100402
+ }
100403
+ /**
100404
+ * Derives the Firebender frontmatter from the canonical one: specific globs
100405
+ * make a file-scoped rule, everything else (no globs, universal globs) is
100406
+ * always applied. Firebender splits `globs` on every comma, so a brace
100407
+ * alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
100408
+ */
100409
+ static buildFrontmatter(frontmatter) {
100410
+ const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
100411
+ const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
100412
+ const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
100413
+ const description = frontmatter.description?.trim() || void 0;
100414
+ return {
100415
+ alwaysApply: !isFileScoped,
100416
+ ...description !== void 0 && { description },
100417
+ ...isFileScoped && { globs: specificGlobs.join(", ") }
100418
+ };
100419
+ }
100420
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
100421
+ const paths = this.getSettablePaths();
100422
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
100423
+ if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
100424
+ outputRoot,
100425
+ relativeDirPath: paths.root.relativeDirPath,
100426
+ relativeFilePath: paths.root.relativeFilePath,
100427
+ fileContent: rulesyncRule.getBody(),
100428
+ validate,
100429
+ root: true,
100430
+ description: rulesyncFrontmatter.description,
100431
+ globs: rulesyncFrontmatter.globs
100432
+ });
100433
+ return new FirebenderRule({
100434
+ outputRoot,
100435
+ relativeDirPath: paths.nonRoot.relativeDirPath,
100436
+ relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
100437
+ frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
100438
+ body: rulesyncRule.getBody(),
100439
+ fileContent: "",
100440
+ validate,
100441
+ root: false
100442
+ });
100443
+ }
100444
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
100445
+ const paths = this.getSettablePaths();
100446
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
100447
+ const fileContent = await readFileContent(join(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
100448
+ return new FirebenderRule({
100449
+ outputRoot,
100450
+ relativeDirPath: paths.root.relativeDirPath,
100451
+ relativeFilePath: paths.root.relativeFilePath,
100452
+ fileContent,
100453
+ validate,
100454
+ root: true
100455
+ });
100456
+ }
100457
+ const filePath = join(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
100458
+ const fileContent = await readFileContent(filePath);
100459
+ const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
100460
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
100461
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
100462
+ return new FirebenderRule({
100463
+ outputRoot,
100464
+ relativeDirPath: paths.nonRoot.relativeDirPath,
100465
+ relativeFilePath,
100466
+ frontmatter: result.data,
100467
+ body: body.trim(),
100468
+ fileContent: "",
100469
+ validate,
100470
+ root: false
100471
+ });
100472
+ }
100473
+ toRulesyncRule() {
100474
+ if (this.isRoot()) return this.toRulesyncRuleDefault();
100475
+ const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
100476
+ const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
100477
+ const description = this.frontmatter.description?.trim() || void 0;
100478
+ const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
100479
+ return new RulesyncRule({
100480
+ outputRoot: process.cwd(),
100481
+ frontmatter: {
100482
+ targets: ["*"],
100483
+ root: false,
100484
+ ...description !== void 0 && { description },
100485
+ globs
100486
+ },
100487
+ body: this.body,
100488
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
100489
+ relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
100490
+ validate: true
100491
+ });
100492
+ }
100493
+ validate() {
100494
+ if (this.isRoot() || !this.frontmatter) return {
100495
+ success: true,
100496
+ error: null
100497
+ };
100498
+ const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
100499
+ if (result.success) return {
100500
+ success: true,
100501
+ error: null
100502
+ };
100503
+ return {
100504
+ success: false,
100505
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
100506
+ };
100507
+ }
100508
+ getFrontmatter() {
100509
+ return this.frontmatter;
100510
+ }
100511
+ getBody() {
100512
+ return this.body;
100513
+ }
100514
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
100515
+ const paths = this.getSettablePaths();
100516
+ const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
100517
+ return new FirebenderRule({
100518
+ outputRoot,
100519
+ relativeDirPath,
100520
+ relativeFilePath,
100521
+ fileContent: "",
100522
+ validate: false,
100523
+ root: isRoot
100524
+ });
100525
+ }
100526
+ static isTargetedByRulesyncRule(rulesyncRule) {
100527
+ return this.isTargetedByRulesyncRuleDefault({
100528
+ rulesyncRule,
100529
+ toolTarget: "firebender"
100530
+ });
100531
+ }
100532
+ };
100533
+ //#endregion
98586
100534
  //#region src/features/rules/gitlabduo-rule.ts
98587
100535
  /**
98588
100536
  * GitLab Duo CLI reads custom rules from a single plain-Markdown file:
@@ -101864,6 +103812,117 @@ var WarpRule = class extends NestedAgentsmdRule {
101864
103812
  }
101865
103813
  };
101866
103814
  //#endregion
103815
+ //#region src/features/rules/xum-rule.ts
103816
+ /** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
103817
+ const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
103818
+ /**
103819
+ * Make `body` start at a level-1 heading, adding `# <heading>` when it does not
103820
+ * already open with one.
103821
+ *
103822
+ * Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
103823
+ * document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
103824
+ * section that runs until the next heading of the same or a higher level.
103825
+ * Fragments rulesync concatenates into that document — folded topic rules and
103826
+ * the local overlay — would otherwise fall inside a scoped section the
103827
+ * preceding fragment ends with, and Xum would inject them only in that mode.
103828
+ * Only an H1 closes a section of any level, so each such fragment opens with
103829
+ * one.
103830
+ *
103831
+ * @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
103832
+ */
103833
+ function ensureXumSectionBoundary({ body, heading }) {
103834
+ const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
103835
+ if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
103836
+ const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
103837
+ return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
103838
+ }
103839
+ /** The heading a folded rule opens with: its file name without the extension. */
103840
+ function xumRuleHeading(rulesyncRule) {
103841
+ const fileName = basename(rulesyncRule.getRelativeFilePath());
103842
+ return fileName.slice(0, fileName.length - extname(fileName).length);
103843
+ }
103844
+ /**
103845
+ * Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
103846
+ *
103847
+ * Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
103848
+ * on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
103849
+ * `agentsmd` target already writes, so this target writes only the
103850
+ * Xum-dedicated file. Only these files honor the `## Model: <regex>` and
103851
+ * `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
103852
+ * so such sections authored in a rule keep working. The personal
103853
+ * `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
103854
+ *
103855
+ * @see https://xum.cdr.dev/agents/instruction-files
103856
+ */
103857
+ var XumRule = class XumRule extends ToolRule {
103858
+ constructor({ fileContent, root, ...rest }) {
103859
+ super({
103860
+ ...rest,
103861
+ fileContent,
103862
+ root: root ?? false
103863
+ });
103864
+ }
103865
+ static getSettablePaths(_options = {}) {
103866
+ return { root: {
103867
+ relativeDirPath: XUM_DIR,
103868
+ relativeFilePath: XUM_RULE_FILE_NAME
103869
+ } };
103870
+ }
103871
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
103872
+ const { root } = this.getSettablePaths({ global });
103873
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
103874
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
103875
+ return new XumRule({
103876
+ outputRoot,
103877
+ relativeDirPath: root.relativeDirPath,
103878
+ relativeFilePath: root.relativeFilePath,
103879
+ fileContent,
103880
+ validate,
103881
+ root: true
103882
+ });
103883
+ }
103884
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
103885
+ const { root } = this.getSettablePaths({ global });
103886
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
103887
+ return new XumRule({
103888
+ outputRoot,
103889
+ relativeDirPath: root.relativeDirPath,
103890
+ relativeFilePath: root.relativeFilePath,
103891
+ fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
103892
+ body: rulesyncRule.getBody(),
103893
+ heading: xumRuleHeading(rulesyncRule)
103894
+ }),
103895
+ validate,
103896
+ root: isRoot
103897
+ });
103898
+ }
103899
+ toRulesyncRule() {
103900
+ return this.toRulesyncRuleDefault();
103901
+ }
103902
+ validate() {
103903
+ return {
103904
+ success: true,
103905
+ error: null
103906
+ };
103907
+ }
103908
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
103909
+ return new XumRule({
103910
+ outputRoot,
103911
+ relativeDirPath,
103912
+ relativeFilePath,
103913
+ fileContent: "",
103914
+ validate: false,
103915
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
103916
+ });
103917
+ }
103918
+ static isTargetedByRulesyncRule(rulesyncRule) {
103919
+ return this.isTargetedByRulesyncRuleDefault({
103920
+ rulesyncRule,
103921
+ toolTarget: "xum"
103922
+ });
103923
+ }
103924
+ };
103925
+ //#endregion
101867
103926
  //#region src/features/rules/zcode-rule.ts
101868
103927
  var ZcodeRule = class ZcodeRule extends ToolRule {
101869
103928
  constructor({ fileContent, root, ...rest }) {
@@ -102352,6 +104411,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102352
104411
  collisionPolicy: "compose"
102353
104412
  }
102354
104413
  }],
104414
+ ["firebender", {
104415
+ class: FirebenderRule,
104416
+ meta: {
104417
+ extension: "mdc",
104418
+ supportsGlobal: true,
104419
+ ruleDiscoveryMode: "auto"
104420
+ }
104421
+ }],
102355
104422
  ["goose", {
102356
104423
  class: GooseRule,
102357
104424
  meta: {
@@ -102635,6 +104702,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102635
104702
  collisionPolicy: "fold"
102636
104703
  }
102637
104704
  }],
104705
+ ["xum", {
104706
+ class: XumRule,
104707
+ meta: {
104708
+ extension: "md",
104709
+ supportsGlobal: true,
104710
+ ruleDiscoveryMode: "auto",
104711
+ collisionPolicy: "fold",
104712
+ localRootMode: "separate-local-file",
104713
+ localRootFileName: XUM_LOCAL_RULE_FILE_NAME
104714
+ }
104715
+ }],
102638
104716
  ["devin", {
102639
104717
  class: DevinRule,
102640
104718
  meta: {
@@ -103405,18 +105483,39 @@ var RulesProcessor = class extends FeatureProcessor {
103405
105483
  root: true,
103406
105484
  localRoot
103407
105485
  });
103408
- if (isClassOrSubclassOf({
103409
- candidate: factory.class,
103410
- base: QwencodeRule
103411
- })) return new QwencodeRule({
103412
- outputRoot: this.outputRoot,
103413
- relativeDirPath: relativeDirPath ?? ".qwen",
103414
- relativeFilePath: fileName,
103415
- fileContent: body,
103416
- validate: true,
103417
- root: true,
105486
+ return this.buildToolDirLocalRootFile({
105487
+ factory,
105488
+ fileName,
105489
+ body,
105490
+ relativeDirPath,
103418
105491
  localRoot
103419
105492
  });
105493
+ }
105494
+ /**
105495
+ * Qwen Code and Xum read the personal local file from their tool directory
105496
+ * (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
105497
+ * (project scope only; global handling never reaches here).
105498
+ */
105499
+ buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
105500
+ const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
105501
+ candidate: factory.class,
105502
+ base
105503
+ }));
105504
+ if (toolDirEntry) {
105505
+ const [toolDirClass, toolDir] = toolDirEntry;
105506
+ return new toolDirClass({
105507
+ outputRoot: this.outputRoot,
105508
+ relativeDirPath: relativeDirPath ?? toolDir,
105509
+ relativeFilePath: fileName,
105510
+ fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
105511
+ body,
105512
+ heading: "Local instructions"
105513
+ }) : body,
105514
+ validate: true,
105515
+ root: true,
105516
+ localRoot
105517
+ });
105518
+ }
103420
105519
  return null;
103421
105520
  }
103422
105521
  /**
@@ -106168,6 +108267,7 @@ async function generateSubagentsCore(params) {
106168
108267
  toolTarget,
106169
108268
  global: config.getGlobal(),
106170
108269
  dryRun: config.isPreviewMode(),
108270
+ deleteOrphans: config.getDelete(),
106171
108271
  logger
106172
108272
  });
106173
108273
  const result = await processFeatureWithRulesyncFiles({
@@ -106792,4 +108892,4 @@ async function importChecksCore(params) {
106792
108892
  //#endregion
106793
108893
  export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
106794
108894
 
106795
- //# sourceMappingURL=import-CoFTCNq7.js.map
108895
+ //# sourceMappingURL=import-CfVOkld5.js.map