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.
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
457
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
458
458
  //#endregion
459
459
  //#region src/constants/rulesync-paths.ts
460
- const { join: join$426 } = node_path.posix;
460
+ const { join: join$439 } = node_path.posix;
461
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
462
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
463
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
464
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
465
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
466
466
  const RULES_FEATURE_SUBDIR = "rules";
467
- const CURATED_RULES_FEATURE_SUBDIR = join$426(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$439(RULES_FEATURE_SUBDIR, ".curated");
468
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
469
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
470
470
  const CHECKS_FEATURE_SUBDIR = "checks";
471
471
  const SKILLS_FEATURE_SUBDIR = "skills";
472
- const CURATED_SKILLS_FEATURE_SUBDIR = join$426(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$426(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$439(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
486
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
487
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
488
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$426(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
490
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
491
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
492
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -544,6 +544,7 @@ const rulesProcessorToolTargetTuple = [
544
544
  "cursor",
545
545
  "deepagents",
546
546
  "factorydroid",
547
+ "firebender",
547
548
  "goose",
548
549
  "grokcli",
549
550
  "gitlabduo",
@@ -573,6 +574,7 @@ const rulesProcessorToolTargetTuple = [
573
574
  "trae",
574
575
  "vibe",
575
576
  "warp",
577
+ "xum",
576
578
  "devin",
577
579
  "devin-plugin",
578
580
  "zcode",
@@ -582,6 +584,7 @@ const rulesProcessorToolTargetTuple = [
582
584
  ];
583
585
  const ignoreProcessorToolTargetTuple = [
584
586
  "aiassistant",
587
+ "androidstudio",
585
588
  "antigravity-cli",
586
589
  "augmentcode",
587
590
  "bob",
@@ -592,6 +595,7 @@ const ignoreProcessorToolTargetTuple = [
592
595
  "continue",
593
596
  "crush",
594
597
  "cursor",
598
+ "firebender",
595
599
  "grokcli",
596
600
  "hermesagent",
597
601
  "junie",
@@ -639,6 +643,7 @@ const mcpProcessorToolTargetTuple = [
639
643
  "deepagents",
640
644
  "dsh",
641
645
  "factorydroid",
646
+ "firebender",
642
647
  "goose",
643
648
  "grokcli",
644
649
  "gitlabduo",
@@ -668,6 +673,7 @@ const mcpProcessorToolTargetTuple = [
668
673
  "vibe-plugin",
669
674
  "warp",
670
675
  "warpcli",
676
+ "xum",
671
677
  "devin",
672
678
  "devin-plugin",
673
679
  "zcode",
@@ -694,6 +700,7 @@ const commandsProcessorToolTargetTuple = [
694
700
  "copilot",
695
701
  "cursor",
696
702
  "factorydroid",
703
+ "firebender",
697
704
  "goose",
698
705
  "grokcli",
699
706
  "gitlabduo",
@@ -749,6 +756,7 @@ const subagentsProcessorToolTargetTuple = [
749
756
  "devin",
750
757
  "devin-plugin",
751
758
  "factorydroid",
759
+ "firebender",
752
760
  "goose",
753
761
  "grokcli",
754
762
  "hermesagent",
@@ -773,6 +781,7 @@ const subagentsProcessorToolTargetTuple = [
773
781
  "takt",
774
782
  "vibe",
775
783
  "vibe-plugin",
784
+ "xum",
776
785
  "zcode",
777
786
  "zcode-plugin"
778
787
  ];
@@ -780,6 +789,7 @@ const skillsProcessorToolTargetTuple = [
780
789
  "agentsmd",
781
790
  "agentsskills",
782
791
  "aiassistant",
792
+ "androidstudio",
783
793
  "amp",
784
794
  "antigravity-cli",
785
795
  "antigravity-ide",
@@ -803,6 +813,7 @@ const skillsProcessorToolTargetTuple = [
803
813
  "cursor",
804
814
  "deepagents",
805
815
  "factorydroid",
816
+ "firebender",
806
817
  "goose",
807
818
  "grokcli",
808
819
  "gitlabduo",
@@ -832,6 +843,7 @@ const skillsProcessorToolTargetTuple = [
832
843
  "vibe",
833
844
  "vibe-plugin",
834
845
  "warp",
846
+ "xum",
835
847
  "devin",
836
848
  "devin-plugin",
837
849
  "zcode",
@@ -866,6 +878,7 @@ const hooksProcessorToolTargetTuple = [
866
878
  "pi",
867
879
  "pool",
868
880
  "factorydroid",
881
+ "firebender",
869
882
  "goose",
870
883
  "hermesagent",
871
884
  "kimi-code",
@@ -3780,6 +3793,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3780
3793
  ".devin/config.json",
3781
3794
  ".dsh/cordis.patch.yml",
3782
3795
  ".factory/settings.json",
3796
+ "firebender.json",
3797
+ ".firebender/firebender.json",
3783
3798
  ".grok/config.toml",
3784
3799
  ".grok/sandbox.toml",
3785
3800
  ".letta/settings.json",
@@ -4241,14 +4256,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
4241
4256
  } } });
4242
4257
  return gray_matter.default.stringify(file, cleanFrontmatter);
4243
4258
  }
4259
+ /**
4260
+ * Thrown in place of gray-matter's built-in JavaScript engine.
4261
+ *
4262
+ * gray-matter picks the parser from the text after the opening fence, so a
4263
+ * file that opens with `---js` or `---javascript` has its frontmatter handed
4264
+ * to `eval()`. Frontmatter here comes from third-party skills, fetched sources
4265
+ * and untrusted repositories, so that would run arbitrary code on the machine
4266
+ * of whoever generates or imports it. Every engine name that resolves to code
4267
+ * execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
4268
+ * refuses those fences earlier still, so this is the second line of defense.
4269
+ */
4270
+ function refuseExecutableFrontmatter() {
4271
+ throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
4272
+ }
4273
+ const REFUSED_FRONTMATTER_ENGINE = {
4274
+ parse: refuseExecutableFrontmatter,
4275
+ stringify: refuseExecutableFrontmatter
4276
+ };
4277
+ /**
4278
+ * Engines passed to every gray-matter parse. Supplying them merges over
4279
+ * gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
4280
+ * a plugin could register) can never be reached through an alias.
4281
+ *
4282
+ * `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
4283
+ * pnpm patch only applies inside this repository: gray-matter is an external
4284
+ * dependency of the published package, so an installed rulesync runs the
4285
+ * unpatched gray-matter, and this override plus the language check are what
4286
+ * protect it. Exported for the test that pins that.
4287
+ */
4288
+ const FRONTMATTER_PARSE_OPTIONS = { engines: {
4289
+ javascript: REFUSED_FRONTMATTER_ENGINE,
4290
+ js: REFUSED_FRONTMATTER_ENGINE,
4291
+ coffee: REFUSED_FRONTMATTER_ENGINE,
4292
+ coffeescript: REFUSED_FRONTMATTER_ENGINE,
4293
+ cson: REFUSED_FRONTMATTER_ENGINE
4294
+ } };
4295
+ /** Frontmatter languages accepted after the opening `---` fence. */
4296
+ const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
4297
+ /**
4298
+ * Read the opening frontmatter fence the way gray-matter does, or return
4299
+ * `undefined` when gray-matter would see no frontmatter at all.
4300
+ *
4301
+ * gray-matter strips one byte order mark, treats `---` as a fence only when a
4302
+ * fourth `-` does not follow, and takes the language from the text between the
4303
+ * fence and the first `\r?\n`, trimmed — dropping the last character when
4304
+ * there is no line break at all. Every check below that has to agree with
4305
+ * gray-matter on where the block is (the language check, the size guard, the
4306
+ * repair pass) goes through this one reading, so none of them can be bypassed
4307
+ * by a fence line gray-matter accepts and a stricter pattern would not (a
4308
+ * stray `\r` before the line break, say).
4309
+ */
4310
+ function readFrontmatterFence(content) {
4311
+ const offset = content.startsWith("") ? 1 : 0;
4312
+ const text = content.slice(offset);
4313
+ if (!text.startsWith("---") || text.charAt(3) === "-") return;
4314
+ const afterFence = text.slice(3);
4315
+ const lineBreak = /\r?\n/.exec(afterFence);
4316
+ const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
4317
+ const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
4318
+ return {
4319
+ language,
4320
+ lineBreakStart,
4321
+ lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
4322
+ };
4323
+ }
4324
+ /**
4325
+ * Refuse a frontmatter fence that names any language other than YAML.
4326
+ *
4327
+ * Only YAML is ever written by rulesync or by the tools it targets, so anything
4328
+ * else — `---js` above all, whose block gray-matter would `eval()` — is
4329
+ * rejected before gray-matter sees the content.
4330
+ */
4331
+ function assertYamlFrontmatterLanguage(content) {
4332
+ const fence = readFrontmatterFence(content);
4333
+ 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`);
4334
+ }
4335
+ /**
4336
+ * The number of characters gray-matter would hand to the YAML parser as the
4337
+ * frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
4338
+ * closing fence does not make this zero: gray-matter then parses everything
4339
+ * after the fence line, so that is what the size guard has to measure.
4340
+ */
4341
+ function measureFrontmatterBlockChars(content) {
4342
+ const fence = readFrontmatterFence(content);
4343
+ if (fence?.lineBreakStart === void 0) return 0;
4344
+ const rest = content.slice(fence.lineBreakStart);
4345
+ const closeIndex = rest.indexOf("\n---");
4346
+ return closeIndex === -1 ? rest.length : closeIndex;
4347
+ }
4244
4348
  function parseFrontmatter(content, filePath) {
4245
4349
  let frontmatter;
4246
4350
  let body;
4247
4351
  let hasFrontmatter;
4248
4352
  try {
4249
- const bounds = findFrontmatterBlockBounds(content);
4250
- 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)`);
4251
- const result = (0, gray_matter.default)(content, {});
4353
+ assertYamlFrontmatterLanguage(content);
4354
+ 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)`);
4355
+ const result = (0, gray_matter.default)(content, FRONTMATTER_PARSE_OPTIONS);
4252
4356
  frontmatter = deepRemoveNullishObject(result.data);
4253
4357
  body = result.content;
4254
4358
  hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
@@ -4307,22 +4411,28 @@ function repairFrontmatterLine(line) {
4307
4411
  }
4308
4412
  /**
4309
4413
  * Locate a raw `---`-delimited frontmatter block's bounds within `content`,
4310
- * without parsing it. Shared by the size guard in {@link parseFrontmatter} and
4311
- * the YAML repair pass below, so both agree on exactly what gray-matter would
4312
- * treat as the block: gray-matter ends it at the first `\n---`, with no
4313
- * requirement that the delimiter be alone on its line, so a stricter pattern
4314
- * here would run past gray-matter's delimiter and act on text that is really
4315
- * the body.
4414
+ * without parsing it, for the YAML repair pass below and the rule loaders that
4415
+ * pre-process `globs:`. The opening fence is read exactly as gray-matter reads
4416
+ * it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
4417
+ * still YAML, any other language yields no bounds. gray-matter ends the block
4418
+ * at the first `\n---`, with no requirement that the delimiter be alone on its
4419
+ * line, so a stricter pattern here would run past gray-matter's delimiter and
4420
+ * act on text that is really the body. A block with no closing fence has no
4421
+ * bounds; the size guard measures that case separately.
4316
4422
  */
4317
4423
  function findFrontmatterBlockBounds(content) {
4318
- const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
4319
- if (!opening) return;
4320
- const blockStart = opening[0].length;
4321
- const closing = /\r?\n---/.exec(content.slice(blockStart));
4424
+ const fence = readFrontmatterFence(content);
4425
+ if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
4426
+ const blockStart = fence.lineEnd;
4427
+ const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
4322
4428
  if (!closing) return;
4429
+ if (closing.index === 0) return {
4430
+ blockStart,
4431
+ blockEnd: blockStart
4432
+ };
4323
4433
  return {
4324
4434
  blockStart,
4325
- blockEnd: blockStart + closing.index
4435
+ blockEnd: fence.lineBreakStart + closing.index
4326
4436
  };
4327
4437
  }
4328
4438
  /**
@@ -4354,8 +4464,9 @@ function repairMalformedFrontmatterYaml(content) {
4354
4464
  * narrow — one pass, top-level entries only, and the original error is what
4355
4465
  * surfaces if it does not help, so a genuinely broken file still fails with the
4356
4466
  * message that describes what is actually wrong with it. A file with no closing
4357
- * `---`, or one whose opening fence carries a language tag, is not repaired at
4358
- * all: neither is a frontmatter block gray-matter would have read.
4467
+ * `---` is not repaired at all, and one whose opening fence names a language
4468
+ * other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
4469
+ * is repaired like a plain one).
4359
4470
  *
4360
4471
  * @see https://agentskills.io/client-implementation/adding-skills-support
4361
4472
  */
@@ -4720,6 +4831,29 @@ const CURSOR_HOOK_EVENTS = [
4720
4831
  "workspaceOpen"
4721
4832
  ];
4722
4833
  /**
4834
+ * Hook events supported by Firebender.
4835
+ *
4836
+ * Firebender's `hooks.json` follows Cursor's layout, and every event it fires
4837
+ * is spelled like its canonical counterpart, so no name mapping is needed.
4838
+ * @see https://docs.firebender.com/multi-agent/hooks
4839
+ */
4840
+ const FIREBENDER_HOOK_EVENTS = [
4841
+ "sessionStart",
4842
+ "sessionEnd",
4843
+ "subagentStart",
4844
+ "subagentStop",
4845
+ "preToolUse",
4846
+ "postToolUse",
4847
+ "beforeShellExecution",
4848
+ "afterShellExecution",
4849
+ "beforeMCPExecution",
4850
+ "afterMCPExecution",
4851
+ "beforeReadFile",
4852
+ "afterFileEdit",
4853
+ "preCompact",
4854
+ "stop"
4855
+ ];
4856
+ /**
4723
4857
  * Hook events supported by Claude Code.
4724
4858
  *
4725
4859
  * Covers the full documented event surface.
@@ -6067,6 +6201,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
6067
6201
  version: zod_mini.z.optional(zod_mini.z.number()),
6068
6202
  hooks: canonicalHooksRecordSchema,
6069
6203
  cursor: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6204
+ firebender: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6070
6205
  claudecode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6071
6206
  copilot: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6072
6207
  copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -7170,7 +7305,7 @@ const McpServerSchema = zod_mini.z.looseObject({
7170
7305
  crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
7171
7306
  crushChannelEnabled: zod_mini.z.optional(zod_mini.z.boolean()),
7172
7307
  crushChannelReply: zod_mini.z.optional(CrushChannelReplySchema),
7173
- zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({ type: zod_mini.z.string() })),
7308
+ zcodeOauth: zod_mini.z.optional(zod_mini.z.looseObject({})),
7174
7309
  poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7175
7310
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
7176
7311
  /**
@@ -7272,6 +7407,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
7272
7407
  devin: zod_mini.z.optional(toolScopedMcpSchema),
7273
7408
  dsh: zod_mini.z.optional(toolScopedMcpSchema),
7274
7409
  factorydroid: zod_mini.z.optional(toolScopedMcpSchema),
7410
+ firebender: zod_mini.z.optional(toolScopedMcpSchema),
7275
7411
  gitlabduo: zod_mini.z.optional(toolScopedMcpSchema),
7276
7412
  goose: zod_mini.z.optional(toolScopedMcpSchema),
7277
7413
  grokcli: zod_mini.z.optional(toolScopedMcpSchema),
@@ -7296,6 +7432,7 @@ const RulesyncMcpFileSchema = zod_mini.z.looseObject({
7296
7432
  vibe: zod_mini.z.optional(toolScopedMcpSchema),
7297
7433
  warp: zod_mini.z.optional(toolScopedMcpSchema),
7298
7434
  warpcli: zod_mini.z.optional(toolScopedMcpSchema),
7435
+ xum: zod_mini.z.optional(toolScopedMcpSchema),
7299
7436
  zed: zod_mini.z.optional(toolScopedMcpSchema)
7300
7437
  });
7301
7438
  /**
@@ -13091,6 +13228,35 @@ const CRUSH_CONFIG_DECLARATION = {
13091
13228
  }
13092
13229
  };
13093
13230
  /**
13231
+ * Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
13232
+ * and personal (`~/.firebender/firebender.json`) spellings so a policy edit
13233
+ * lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
13234
+ * `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
13235
+ * refuses to read-modify-write a file it could not parse. Each feature owns
13236
+ * one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
13237
+ * `mcpServers` (the complete server map) and `agents` (recomputed from the
13238
+ * existing list, with the entries for rulesync's own agent directory replaced).
13239
+ * https://docs.firebender.com/api-reference/syntax
13240
+ */
13241
+ const FIREBENDER_CONFIG_DECLARATION = {
13242
+ format: "json",
13243
+ invalidRootPolicy: "error",
13244
+ features: {
13245
+ ignore: {
13246
+ kind: "replace-owned-keys",
13247
+ ownedKeys: ["ignore"]
13248
+ },
13249
+ subagents: {
13250
+ kind: "replace-owned-keys",
13251
+ ownedKeys: ["agents"]
13252
+ },
13253
+ mcp: {
13254
+ kind: "replace-owned-keys",
13255
+ ownedKeys: ["mcpServers"]
13256
+ }
13257
+ }
13258
+ };
13259
+ /**
13094
13260
  * What the two Claude Code settings files have in common: both are plain JSON,
13095
13261
  * and both validate against the one published schema, which the gateway
13096
13262
  * points every file it writes at. `.claude/settings.local.json` is the same
@@ -13330,6 +13496,15 @@ const SHARED_CONFIG_OWNERSHIP = {
13330
13496
  ownedKeys: ["servers"]
13331
13497
  } }
13332
13498
  },
13499
+ ".xum/mcp.jsonc": {
13500
+ format: "jsonc",
13501
+ invalidRootPolicy: "error",
13502
+ jsoncParseErrors: "error",
13503
+ features: { mcp: {
13504
+ kind: "replace-owned-keys",
13505
+ ownedKeys: ["servers"]
13506
+ } }
13507
+ },
13333
13508
  ".qwen/settings.json": {
13334
13509
  format: "json",
13335
13510
  features: {
@@ -13417,6 +13592,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13417
13592
  ".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
13418
13593
  "crush.json": CRUSH_CONFIG_DECLARATION,
13419
13594
  ".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
13595
+ "firebender.json": FIREBENDER_CONFIG_DECLARATION,
13596
+ ".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
13420
13597
  ".bob/settings.json": {
13421
13598
  format: "json",
13422
13599
  invalidRootPolicy: "error",
@@ -20251,6 +20428,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
20251
20428
  }
20252
20429
  };
20253
20430
  //#endregion
20431
+ //#region src/constants/firebender-paths.ts
20432
+ /**
20433
+ * Firebender (the coding agent plugin for Android Studio and other JetBrains
20434
+ * IDEs) keeps project configuration under `.firebender/` plus a project-root
20435
+ * `firebender.json`, and user configuration under `~/.firebender/` (including
20436
+ * `~/.firebender/firebender.json`).
20437
+ *
20438
+ * @see https://docs.firebender.com/api-reference/syntax
20439
+ * @see https://docs.firebender.com/api-reference/rules
20440
+ * @see https://docs.firebender.com/api-reference/commands
20441
+ * @see https://docs.firebender.com/api-reference/agents
20442
+ * @see https://docs.firebender.com/api-reference/skills
20443
+ * @see https://docs.firebender.com/api-reference/agents-md
20444
+ * @see https://docs.firebender.com/multi-agent/hooks
20445
+ */
20446
+ const FIREBENDER_DIR = ".firebender";
20447
+ /**
20448
+ * `firebender.json`: at the project root for project scope, and under
20449
+ * `~/.firebender/` for the personal config. It is shared by the ignore
20450
+ * (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
20451
+ */
20452
+ const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
20453
+ /** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
20454
+ const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
20455
+ (0, node_path.join)(FIREBENDER_DIR, "rules");
20456
+ /** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
20457
+ const FIREBENDER_COMMANDS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "commands");
20458
+ /** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
20459
+ const FIREBENDER_AGENTS_DIR_NAME = "agents";
20460
+ const FIREBENDER_AGENTS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
20461
+ /** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
20462
+ const FIREBENDER_SKILLS_DIR_PATH = (0, node_path.join)(FIREBENDER_DIR, "skills");
20463
+ /** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
20464
+ const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
20465
+ /**
20466
+ * The directory holding `firebender.json` for a scope: the project root, or
20467
+ * `~/.firebender/` for the personal config.
20468
+ */
20469
+ function getFirebenderConfigDir({ global }) {
20470
+ return global ? FIREBENDER_DIR : ".";
20471
+ }
20472
+ //#endregion
20473
+ //#region src/features/commands/firebender-command.ts
20474
+ /**
20475
+ * Frontmatter of a Firebender command. Every field is optional: `name`
20476
+ * defaults to the file name, `model` is `default`, `quick` or a model ID, and
20477
+ * `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
20478
+ * preserved.
20479
+ *
20480
+ * @see https://docs.firebender.com/api-reference/commands
20481
+ */
20482
+ const FirebenderCommandFrontmatterSchema = zod_mini.z.looseObject({
20483
+ name: zod_mini.z.optional(zod_mini.z.string()),
20484
+ description: zod_mini.z.optional(zod_mini.z.string()),
20485
+ model: zod_mini.z.optional(zod_mini.z.string()),
20486
+ mode: zod_mini.z.optional(zod_mini.z.string())
20487
+ });
20488
+ const FIREBENDER_COMMAND_EXTENSION = ".mdc";
20489
+ /**
20490
+ * Command generator for Firebender: one `.mdc` file per command under
20491
+ * `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
20492
+ * with optional `name` / `description` / `model` / `mode` frontmatter. The
20493
+ * canonical `description` is written as is; `name`, `model` and `mode` come
20494
+ * from the `firebender:` section of the rulesync command.
20495
+ *
20496
+ * @see https://docs.firebender.com/api-reference/commands
20497
+ */
20498
+ var FirebenderCommand = class FirebenderCommand extends ToolCommand {
20499
+ frontmatter;
20500
+ body;
20501
+ constructor({ frontmatter, body, ...rest }) {
20502
+ if (rest.validate) {
20503
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20504
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
20505
+ }
20506
+ super({
20507
+ ...rest,
20508
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
20509
+ });
20510
+ this.frontmatter = frontmatter;
20511
+ this.body = body;
20512
+ }
20513
+ static getSettablePaths(_options = {}) {
20514
+ return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
20515
+ }
20516
+ getBody() {
20517
+ return this.body;
20518
+ }
20519
+ getFrontmatter() {
20520
+ return this.frontmatter;
20521
+ }
20522
+ toRulesyncCommand() {
20523
+ const { description, ...restFields } = this.frontmatter;
20524
+ const rulesyncFrontmatter = {
20525
+ targets: ["*"],
20526
+ description,
20527
+ ...Object.keys(restFields).length > 0 && { firebender: restFields }
20528
+ };
20529
+ return new RulesyncCommand({
20530
+ outputRoot: ".",
20531
+ frontmatter: rulesyncFrontmatter,
20532
+ body: this.body,
20533
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
20534
+ relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
20535
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
20536
+ validate: true
20537
+ });
20538
+ }
20539
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
20540
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
20541
+ const firebenderFields = rulesyncFrontmatter.firebender ?? {};
20542
+ const frontmatter = {
20543
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
20544
+ ...firebenderFields
20545
+ };
20546
+ return new FirebenderCommand({
20547
+ outputRoot,
20548
+ frontmatter,
20549
+ body: rulesyncCommand.getBody(),
20550
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
20551
+ relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
20552
+ validate
20553
+ });
20554
+ }
20555
+ validate() {
20556
+ if (!this.frontmatter) return {
20557
+ success: true,
20558
+ error: null
20559
+ };
20560
+ const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
20561
+ if (result.success) return {
20562
+ success: true,
20563
+ error: null
20564
+ };
20565
+ return {
20566
+ success: false,
20567
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
20568
+ };
20569
+ }
20570
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
20571
+ return this.isTargetedByRulesyncCommandDefault({
20572
+ rulesyncCommand,
20573
+ toolTarget: "firebender"
20574
+ });
20575
+ }
20576
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
20577
+ const paths = this.getSettablePaths({ global });
20578
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
20579
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
20580
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20581
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
20582
+ return new FirebenderCommand({
20583
+ outputRoot,
20584
+ relativeDirPath: paths.relativeDirPath,
20585
+ relativeFilePath,
20586
+ frontmatter: result.data,
20587
+ body: body.trim(),
20588
+ validate
20589
+ });
20590
+ }
20591
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
20592
+ return new FirebenderCommand({
20593
+ outputRoot,
20594
+ relativeDirPath,
20595
+ relativeFilePath,
20596
+ frontmatter: {},
20597
+ body: "",
20598
+ validate: false
20599
+ });
20600
+ }
20601
+ };
20602
+ //#endregion
20254
20603
  //#region src/features/commands/gitlabduo-command.ts
20255
20604
  /**
20256
20605
  * GitLab Duo CLI custom slash commands are Markdown files named after the
@@ -20843,6 +21192,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
20843
21192
  }
20844
21193
  };
20845
21194
  //#endregion
21195
+ //#region src/features/shared/hermes-plugin-commands.ts
21196
+ /**
21197
+ * Normalize a name into a Hermes Agent plugin slash command name.
21198
+ *
21199
+ * `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
21200
+ * looks plugin commands up with every `_` turned into `-`
21201
+ * (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
21202
+ * reachable from every surface.
21203
+ */
21204
+ function hermesSlashName(value) {
21205
+ return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21206
+ }
21207
+ /**
21208
+ * Python helpers shared by the generated Hermes command plugins.
21209
+ *
21210
+ * A plugin slash command runs outside any agent turn, so it cannot call
21211
+ * `delegate_task` (which requires a parent agent). Instead the handler queues
21212
+ * its prompt as a user message with `ctx.inject_message()`, passing the calling
21213
+ * session's key: a `session_key` handler keyword on newer Hermes, or
21214
+ * `HERMES_SESSION_KEY` from the session context the gateway binds around the
21215
+ * handler on older releases.
21216
+ */
21217
+ function getHermesQueueUserMessagePython({ pluginName }) {
21218
+ return `def _session_key(kwargs):
21219
+ session_key = kwargs.get("session_key")
21220
+ if session_key:
21221
+ return session_key
21222
+ try:
21223
+ from gateway.session_context import get_session_env
21224
+ except Exception:
21225
+ return None
21226
+ return get_session_env("HERMES_SESSION_KEY") or None
21227
+
21228
+
21229
+ def _queue_user_message(ctx, message, kwargs, label):
21230
+ """Queue *message* as the session's next user turn; return an error reply, or None."""
21231
+ session_key = _session_key(kwargs)
21232
+ try:
21233
+ if session_key:
21234
+ queued = ctx.inject_message(message, role="user", session_key=session_key)
21235
+ else:
21236
+ queued = ctx.inject_message(message, role="user")
21237
+ except Exception as error:
21238
+ return f"Could not queue {label}: {error}"
21239
+ if not queued:
21240
+ return (
21241
+ f"Hermes refused to queue {label}: there is no active session, or this plugin "
21242
+ "may not inject messages. In gateway sessions, set "
21243
+ "plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
21244
+ )
21245
+ return None
21246
+ `;
21247
+ }
21248
+ //#endregion
20846
21249
  //#region src/features/skills/agentsskills-skill.ts
20847
21250
  const AgentsSkillsSkillFrontmatterSchema = zod_mini.z.looseObject({
20848
21251
  name: zod_mini.z.string(),
@@ -21325,9 +21728,6 @@ const HermesCommandSpecSchema = zod_mini.z.object({
21325
21728
  description: zod_mini.z.string(),
21326
21729
  prompt: zod_mini.z.string()
21327
21730
  });
21328
- function hermesSlashName(value) {
21329
- return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21330
- }
21331
21731
  function getPluginManifestContent$2() {
21332
21732
  return [
21333
21733
  "name: rulesync-commands",
@@ -21361,6 +21761,8 @@ def _load_commands():
21361
21761
  return commands
21362
21762
 
21363
21763
 
21764
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
21765
+
21364
21766
  def _register_command(ctx, command):
21365
21767
  slug = command.get("slug")
21366
21768
  if not slug:
@@ -21370,31 +21772,22 @@ def _register_command(ctx, command):
21370
21772
  prompt = command.get("prompt") or ""
21371
21773
 
21372
21774
  def handler(args=None, **kwargs):
21373
- del kwargs
21374
21775
  raw_args = ""
21375
21776
  if isinstance(args, dict):
21376
21777
  raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
21377
21778
  elif args is not None:
21378
21779
  raw_args = str(args)
21379
21780
 
21380
- context_parts = [prompt] if prompt else []
21781
+ parts = [prompt] if prompt else []
21381
21782
  if raw_args:
21382
- context_parts.append(f"User arguments:\\n{raw_args}")
21783
+ parts.append(f"User arguments:\\n{raw_args}")
21383
21784
 
21384
- # delegate_task takes no model-facing "toolsets" argument: the child
21385
- # inherits the parent's enabled toolsets. "tasks" is the only shape its
21386
- # schema advertises; a top-level "goal" is an unadvertised legacy path.
21387
- return ctx.dispatch_tool(
21388
- "delegate_task",
21389
- {
21390
- "tasks": [
21391
- {
21392
- "goal": description,
21393
- "context": "\\n\\n".join(context_parts),
21394
- },
21395
- ],
21396
- },
21397
- )
21785
+ # A slash command runs outside any agent turn, so calling delegate_task
21786
+ # here fails with "delegate_task requires a parent agent context".
21787
+ # Queue the command prompt as the session's next user message instead.
21788
+ label = f"the /{slug} command"
21789
+ error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
21790
+ return error or f"Queued {label}."
21398
21791
 
21399
21792
  ctx.register_command(slug, handler, description)
21400
21793
 
@@ -25266,6 +25659,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
25266
25659
  supportsSubdirectory: false
25267
25660
  }
25268
25661
  }],
25662
+ ["firebender", {
25663
+ class: FirebenderCommand,
25664
+ meta: {
25665
+ extension: "mdc",
25666
+ supportsProject: true,
25667
+ supportsGlobal: true,
25668
+ isSimulated: false,
25669
+ supportsSubdirectory: false
25670
+ }
25671
+ }],
25269
25672
  ["goose", {
25270
25673
  class: GooseCommand,
25271
25674
  meta: {
@@ -32549,6 +32952,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
32549
32952
  }
32550
32953
  };
32551
32954
  //#endregion
32955
+ //#region src/features/hooks/firebender-hooks.ts
32956
+ /**
32957
+ * Hooks generator for Firebender.
32958
+ *
32959
+ * Firebender reads `.firebender/hooks.json` (project) and
32960
+ * `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
32961
+ * `version` plus a `hooks` map from camelCase event names to lists of
32962
+ * `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
32963
+ * spelled like its canonical counterpart. The per-hook `type` accepts
32964
+ * `"command" | "prompt"`, but the docs never say what a prompt hook does, so
32965
+ * only command hooks are generated (the HooksProcessor warns about the rest).
32966
+ *
32967
+ * @see https://docs.firebender.com/multi-agent/hooks
32968
+ */
32969
+ var FirebenderHooks = class FirebenderHooks extends ToolHooks {
32970
+ constructor(params) {
32971
+ super({
32972
+ ...params,
32973
+ fileContent: params.fileContent ?? "{}"
32974
+ });
32975
+ }
32976
+ static getSettablePaths(_options = {}) {
32977
+ return {
32978
+ relativeDirPath: FIREBENDER_DIR,
32979
+ relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
32980
+ };
32981
+ }
32982
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
32983
+ const paths = FirebenderHooks.getSettablePaths({ global });
32984
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
32985
+ return new FirebenderHooks({
32986
+ outputRoot,
32987
+ relativeDirPath: paths.relativeDirPath,
32988
+ relativeFilePath: paths.relativeFilePath,
32989
+ fileContent,
32990
+ validate
32991
+ });
32992
+ }
32993
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
32994
+ const config = rulesyncHooks.getJson();
32995
+ const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
32996
+ const sharedHooks = {};
32997
+ for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
32998
+ const mergedHooks = {
32999
+ ...sharedHooks,
33000
+ ...config.firebender?.hooks
33001
+ };
33002
+ const firebenderHooks = {};
33003
+ for (const [eventName, defs] of Object.entries(mergedHooks)) {
33004
+ const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
33005
+ command: def.command,
33006
+ ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
33007
+ ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
33008
+ ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
33009
+ }));
33010
+ if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
33011
+ }
33012
+ const paths = FirebenderHooks.getSettablePaths({ global });
33013
+ const merged = mergeGeneratedHookLists({
33014
+ existingContent: await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
33015
+ generatedHooks: firebenderHooks,
33016
+ shape: "flat",
33017
+ preserveUnowned,
33018
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
33019
+ logger
33020
+ });
33021
+ const fileContent = JSON.stringify({
33022
+ version: config.version ?? 1,
33023
+ hooks: merged.hooks
33024
+ }, null, 2);
33025
+ return new FirebenderHooks({
33026
+ outputRoot,
33027
+ relativeDirPath: paths.relativeDirPath,
33028
+ relativeFilePath: paths.relativeFilePath,
33029
+ fileContent,
33030
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
33031
+ validate
33032
+ });
33033
+ }
33034
+ static supportsPreserveUnowned() {
33035
+ return true;
33036
+ }
33037
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
33038
+ return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
33039
+ }
33040
+ toRulesyncHooks() {
33041
+ const parsed = JSON.parse(this.getFileContent());
33042
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
33043
+ hooks: parsed.hooks ?? {},
33044
+ overrideKey: "firebender",
33045
+ version: parsed.version ?? 1
33046
+ }), null, 2) });
33047
+ }
33048
+ validate() {
33049
+ return {
33050
+ success: true,
33051
+ error: null
33052
+ };
33053
+ }
33054
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
33055
+ return new FirebenderHooks({
33056
+ outputRoot,
33057
+ relativeDirPath,
33058
+ relativeFilePath,
33059
+ fileContent: "{}",
33060
+ validate: false
33061
+ });
33062
+ }
33063
+ };
33064
+ //#endregion
32552
33065
  //#region src/features/hooks/gitlabduo-hooks.ts
32553
33066
  const GITLABDUO_CONVERTER_CONFIG = {
32554
33067
  supportedEvents: GITLABDUO_HOOK_EVENTS,
@@ -36959,6 +37472,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
36959
37472
  supportedHookTypes: ["command", "prompt"],
36960
37473
  supportsMatcher: true
36961
37474
  }],
37475
+ ["firebender", {
37476
+ class: FirebenderHooks,
37477
+ meta: {
37478
+ supportsProject: true,
37479
+ supportsGlobal: true,
37480
+ supportsImport: true
37481
+ },
37482
+ supportedEvents: FIREBENDER_HOOK_EVENTS,
37483
+ supportedHookTypes: ["command"],
37484
+ supportsMatcher: true
37485
+ }],
36962
37486
  ["claudecode", {
36963
37487
  class: ClaudecodeHooks,
36964
37488
  meta: {
@@ -37830,6 +38354,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
37830
38354
  }
37831
38355
  };
37832
38356
  //#endregion
38357
+ //#region src/constants/androidstudio-paths.ts
38358
+ /**
38359
+ * Gemini in Android Studio configuration-layout conventions.
38360
+ *
38361
+ * Only the Android Studio-specific files are owned by the `androidstudio`
38362
+ * target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
38363
+ * reads belong to the `agentsmd` and `agentsskills` targets.
38364
+ */
38365
+ const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
38366
+ const ANDROIDSTUDIO_SKILLS_DIR_PATH = (0, node_path.join)(".android-studio", "skills");
38367
+ //#endregion
38368
+ //#region src/features/ignore/androidstudio-ignore.ts
38369
+ /**
38370
+ * Ignore generator for Gemini in Android Studio.
38371
+ *
38372
+ * Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
38373
+ * each one applies to the files at or below its directory and blocks both the
38374
+ * chat and the editor AI features such as code completion. Rulesync writes the
38375
+ * project-root file only. There is no user-level variant.
38376
+ *
38377
+ * @see https://developer.android.com/studio/gemini/aiexclude
38378
+ */
38379
+ var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
38380
+ static getSettablePaths() {
38381
+ return {
38382
+ relativeDirPath: ".",
38383
+ relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
38384
+ };
38385
+ }
38386
+ toRulesyncIgnore() {
38387
+ return this.toRulesyncIgnoreDefault();
38388
+ }
38389
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
38390
+ return new AndroidstudioIgnore({
38391
+ outputRoot,
38392
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38393
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38394
+ fileContent: rulesyncIgnore.getFileContent()
38395
+ });
38396
+ }
38397
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
38398
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
38399
+ return new AndroidstudioIgnore({
38400
+ outputRoot,
38401
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38402
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38403
+ fileContent,
38404
+ validate
38405
+ });
38406
+ }
38407
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
38408
+ return new AndroidstudioIgnore({
38409
+ outputRoot,
38410
+ relativeDirPath,
38411
+ relativeFilePath,
38412
+ fileContent: "",
38413
+ validate: false
38414
+ });
38415
+ }
38416
+ };
38417
+ //#endregion
37833
38418
  //#region src/features/ignore/antigravity-cli-ignore.ts
37834
38419
  /**
37835
38420
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -38491,6 +39076,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
38491
39076
  }
38492
39077
  };
38493
39078
  //#endregion
39079
+ //#region src/features/ignore/firebender-ignore.ts
39080
+ function readIgnorePatterns(fileContent) {
39081
+ const ignore = parseSharedConfig({
39082
+ format: "json",
39083
+ fileContent
39084
+ }).ignore;
39085
+ if (!Array.isArray(ignore)) return [];
39086
+ return ignore.filter((pattern) => typeof pattern === "string");
39087
+ }
39088
+ /**
39089
+ * Ignore generator for Firebender.
39090
+ *
39091
+ * Firebender reads gitignore-style patterns from the `ignore` array of
39092
+ * `firebender.json` (project root) and `~/.firebender/firebender.json`
39093
+ * (personal); the two lists are appended together. The file is shared with the
39094
+ * MCP (`mcpServers`) and subagents (`agents`) features and carries settings
39095
+ * rulesync does not own, so only the `ignore` key is written and the file is
39096
+ * never deleted.
39097
+ *
39098
+ * @see https://docs.firebender.com/api-reference/syntax
39099
+ */
39100
+ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
39101
+ constructor(params) {
39102
+ super(params);
39103
+ this.patterns = readIgnorePatterns(this.fileContent || "{}");
39104
+ }
39105
+ static getSettablePaths({ global = false } = {}) {
39106
+ return {
39107
+ relativeDirPath: getFirebenderConfigDir({ global }),
39108
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
39109
+ };
39110
+ }
39111
+ /** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
39112
+ isDeletable() {
39113
+ return false;
39114
+ }
39115
+ toRulesyncIgnore() {
39116
+ return new RulesyncIgnore({
39117
+ outputRoot: ".",
39118
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
39119
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
39120
+ fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
39121
+ });
39122
+ }
39123
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
39124
+ const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
39125
+ const paths = this.getSettablePaths({ global });
39126
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
39127
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
39128
+ const managedPatterns = [...new Set(patterns)];
39129
+ return new FirebenderIgnore({
39130
+ outputRoot,
39131
+ relativeDirPath: paths.relativeDirPath,
39132
+ relativeFilePath: paths.relativeFilePath,
39133
+ fileContent: applySharedConfigPatch({
39134
+ fileKey: sharedConfigFileKey(paths),
39135
+ feature: "ignore",
39136
+ existingContent,
39137
+ patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
39138
+ filePath
39139
+ }),
39140
+ validate: true,
39141
+ global
39142
+ });
39143
+ }
39144
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
39145
+ const paths = this.getSettablePaths({ global });
39146
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
39147
+ return new FirebenderIgnore({
39148
+ outputRoot,
39149
+ relativeDirPath: paths.relativeDirPath,
39150
+ relativeFilePath: paths.relativeFilePath,
39151
+ fileContent,
39152
+ validate,
39153
+ global
39154
+ });
39155
+ }
39156
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
39157
+ return new FirebenderIgnore({
39158
+ outputRoot,
39159
+ relativeDirPath,
39160
+ relativeFilePath,
39161
+ fileContent: "{}",
39162
+ validate: false,
39163
+ global
39164
+ });
39165
+ }
39166
+ };
39167
+ //#endregion
38494
39168
  //#region src/features/ignore/grokcli-ignore.ts
38495
39169
  /** The base built-in profile the rulesync profile extends (Grok's own default). */
38496
39170
  const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
@@ -39808,6 +40482,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
39808
40482
  const IgnoreProcessorToolTargetSchema = zod_mini.z.enum(ignoreProcessorToolTargetTuple);
39809
40483
  const toolIgnoreFactories = /* @__PURE__ */ new Map([
39810
40484
  ["aiassistant", { class: AiassistantIgnore }],
40485
+ ["androidstudio", { class: AndroidstudioIgnore }],
39811
40486
  ["antigravity-cli", { class: AntigravityCliIgnore }],
39812
40487
  ["augmentcode", { class: AugmentcodeIgnore }],
39813
40488
  ["bob", { class: BobIgnore }],
@@ -39818,6 +40493,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
39818
40493
  ["continue", { class: ContinueIgnore }],
39819
40494
  ["crush", { class: CrushIgnore }],
39820
40495
  ["cursor", { class: CursorIgnore }],
40496
+ ["firebender", { class: FirebenderIgnore }],
39821
40497
  ["grokcli", { class: GrokcliIgnore }],
39822
40498
  ["hermesagent", { class: HermesagentIgnore }],
39823
40499
  ["junie", { class: JunieIgnore }],
@@ -39842,6 +40518,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
39842
40518
  const ignoreProcessorGlobalToolTargets = [
39843
40519
  "continue",
39844
40520
  "devin",
40521
+ "firebender",
39845
40522
  "grokcli",
39846
40523
  "kilo",
39847
40524
  "kiro",
@@ -45339,6 +46016,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
45339
46016
  }
45340
46017
  };
45341
46018
  //#endregion
46019
+ //#region src/features/mcp/firebender-mcp.ts
46020
+ /**
46021
+ * The keys of the canonical server schema. Firebender reads only `command`,
46022
+ * `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
46023
+ * NOT on this list is unknown to rulesync and passed through untouched, so a
46024
+ * Firebender-native field such as `deferLoading` written in the tool-scoped
46025
+ * `firebender.mcpServers` block (or on a shared server) survives.
46026
+ */
46027
+ const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
46028
+ function passthroughNonCanonicalFields$1(serverConfig) {
46029
+ return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
46030
+ }
46031
+ /**
46032
+ * Translate one canonical server into Firebender's `mcpServers` entry, or
46033
+ * return `null` (with a warning) for a server Firebender cannot express. A
46034
+ * stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
46035
+ * Firebender has no `type` field and no per-server disable switch.
46036
+ *
46037
+ * @see https://docs.firebender.com/api-reference/syntax
46038
+ */
46039
+ function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
46040
+ if (serverConfig.disabled === true) return warnAndSkipMcpServer({
46041
+ toolName: "Firebender",
46042
+ serverName,
46043
+ reason: "`disabled: true`, and Firebender has no per-server disable switch",
46044
+ logger
46045
+ });
46046
+ if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
46047
+ toolName: "Firebender",
46048
+ serverName,
46049
+ reason: "no command or url",
46050
+ logger
46051
+ });
46052
+ const passthrough = passthroughNonCanonicalFields$1(serverConfig);
46053
+ if (isRemoteMcpServer(serverConfig)) {
46054
+ const url = resolveRemoteMcpUrl(serverConfig);
46055
+ if (url === void 0) return warnAndSkipMcpServer({
46056
+ toolName: "Firebender",
46057
+ serverName,
46058
+ reason: "a remote transport but no url",
46059
+ logger
46060
+ });
46061
+ return {
46062
+ ...passthrough,
46063
+ url,
46064
+ ...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
46065
+ };
46066
+ }
46067
+ const [command, ...args] = resolveLocalMcpCommand(serverConfig);
46068
+ if (command === void 0) return warnAndSkipMcpServer({
46069
+ toolName: "Firebender",
46070
+ serverName,
46071
+ reason: "a local transport but no command",
46072
+ logger
46073
+ });
46074
+ return {
46075
+ ...passthrough,
46076
+ command,
46077
+ ...args.length > 0 && { args },
46078
+ ...serverConfig.env !== void 0 && { env: serverConfig.env }
46079
+ };
46080
+ }
46081
+ /**
46082
+ * MCP generator for Firebender.
46083
+ *
46084
+ * Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
46085
+ * (project root) and `~/.firebender/firebender.json` (personal; it wins on
46086
+ * name clashes). The file is shared with the ignore (`ignore`) and subagents
46087
+ * (`agents`) features and carries settings rulesync does not own, so only the
46088
+ * `mcpServers` key is written and the file is never deleted.
46089
+ *
46090
+ * @see https://docs.firebender.com/api-reference/syntax
46091
+ */
46092
+ var FirebenderMcp = class FirebenderMcp extends ToolMcp {
46093
+ json;
46094
+ constructor(params) {
46095
+ super(params);
46096
+ this.json = parseSharedConfig({
46097
+ format: "json",
46098
+ fileContent: this.fileContent || "{}"
46099
+ });
46100
+ }
46101
+ getJson() {
46102
+ return this.json;
46103
+ }
46104
+ static getSettablePaths({ global = false } = {}) {
46105
+ return {
46106
+ relativeDirPath: getFirebenderConfigDir({ global }),
46107
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
46108
+ };
46109
+ }
46110
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
46111
+ const paths = this.getSettablePaths({ global });
46112
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
46113
+ return new FirebenderMcp({
46114
+ outputRoot,
46115
+ relativeDirPath: paths.relativeDirPath,
46116
+ relativeFilePath: paths.relativeFilePath,
46117
+ fileContent,
46118
+ validate,
46119
+ global
46120
+ });
46121
+ }
46122
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
46123
+ const paths = this.getSettablePaths({ global });
46124
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
46125
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
46126
+ const mcpServers = {};
46127
+ for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
46128
+ const converted = convertServerToFirebenderFormat({
46129
+ serverName,
46130
+ serverConfig,
46131
+ logger
46132
+ });
46133
+ if (converted !== null) mcpServers[serverName] = converted;
46134
+ }
46135
+ return new FirebenderMcp({
46136
+ outputRoot,
46137
+ relativeDirPath: paths.relativeDirPath,
46138
+ relativeFilePath: paths.relativeFilePath,
46139
+ fileContent: applySharedConfigPatch({
46140
+ fileKey: sharedConfigFileKey(paths),
46141
+ feature: "mcp",
46142
+ existingContent,
46143
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
46144
+ filePath
46145
+ }),
46146
+ validate,
46147
+ global
46148
+ });
46149
+ }
46150
+ toRulesyncMcp() {
46151
+ const servers = this.json.mcpServers;
46152
+ const mcpServers = {};
46153
+ if (isMcpServers(servers)) {
46154
+ for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
46155
+ }
46156
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
46157
+ }
46158
+ validate() {
46159
+ return {
46160
+ success: true,
46161
+ error: null
46162
+ };
46163
+ }
46164
+ /**
46165
+ * `firebender.json` is a user-managed file shared with other features (the
46166
+ * ignore feature's `ignore`, the subagents feature's `agents`), so it must
46167
+ * not be deleted.
46168
+ */
46169
+ isDeletable() {
46170
+ return false;
46171
+ }
46172
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46173
+ return new FirebenderMcp({
46174
+ outputRoot,
46175
+ relativeDirPath,
46176
+ relativeFilePath,
46177
+ fileContent: "{}",
46178
+ validate: false,
46179
+ global
46180
+ });
46181
+ }
46182
+ };
46183
+ //#endregion
45342
46184
  //#region src/features/mcp/gitlabduo-mcp.ts
45343
46185
  const CANONICAL_TO_GITLABDUO_TRANSPORT = {
45344
46186
  local: "stdio",
@@ -52030,6 +52872,248 @@ var WarpcliMcp = class extends WarpMcp {
52030
52872
  }
52031
52873
  };
52032
52874
  //#endregion
52875
+ //#region src/constants/xum-paths.ts
52876
+ /**
52877
+ * Xum configuration-layout conventions.
52878
+ *
52879
+ * Xum (Coder's desktop app and `xum run` CLI for running coding agents in
52880
+ * parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
52881
+ * directory at the project root and the matching user assets under `~/.xum/`.
52882
+ * Legacy `.mux/` files are still read when the matching `.xum/` file is
52883
+ * missing; rulesync writes only the `.xum/` paths.
52884
+ *
52885
+ * @see https://xum.cdr.dev/reference/mux-compatibility
52886
+ */
52887
+ const XUM_DIR = ".xum";
52888
+ const XUM_RULE_FILE_NAME = "AGENTS.md";
52889
+ const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
52890
+ const XUM_MCP_FILE_NAME = "mcp.jsonc";
52891
+ const XUM_AGENTS_DIR_PATH = (0, node_path.join)(XUM_DIR, "agents");
52892
+ const XUM_SKILLS_DIR_PATH = (0, node_path.join)(XUM_DIR, "skills");
52893
+ //#endregion
52894
+ //#region src/features/mcp/xum-mcp.ts
52895
+ const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
52896
+ /** Characters that never need quoting in a POSIX shell word. */
52897
+ const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
52898
+ /**
52899
+ * A command string free of quotes, escapes, expansions and operators, which
52900
+ * can be split on whitespace back into argv without changing its meaning.
52901
+ */
52902
+ const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
52903
+ /** Single-quote one argv element for the POSIX shell Xum runs commands in. */
52904
+ function quoteShellWord(value) {
52905
+ if (SHELL_SAFE_WORD.test(value)) return value;
52906
+ return `'${value.replaceAll("'", `'\\''`)}'`;
52907
+ }
52908
+ /**
52909
+ * Build the shell command string a Xum stdio server is launched with. A
52910
+ * `command` string is kept verbatim (Xum itself treats it as a raw shell
52911
+ * string, so `npx -y pkg` authored as one string keeps working); an explicit
52912
+ * argv (`command` as an array) and every `args` element are quoted where
52913
+ * needed, so `/opt/my tools/srv` stays one word.
52914
+ */
52915
+ function buildCommandString(server) {
52916
+ const args = server.args ?? [];
52917
+ if (Array.isArray(server.command)) {
52918
+ const [executable] = server.command;
52919
+ if (executable === void 0 || executable.trim() === "") return void 0;
52920
+ return [...server.command, ...args].map(quoteShellWord).join(" ");
52921
+ }
52922
+ if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
52923
+ return [server.command, ...args.map(quoteShellWord)].join(" ");
52924
+ }
52925
+ /** Split a plain command string back into `command` + `args` when it is safe to. */
52926
+ function splitCommandString(command) {
52927
+ const trimmed = command.trim();
52928
+ if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
52929
+ const [head, ...rest] = trimmed.split(/\s+/);
52930
+ if (head === void 0) return { command };
52931
+ return rest.length > 0 ? {
52932
+ command: head,
52933
+ args: rest
52934
+ } : { command: head };
52935
+ }
52936
+ function resolveXumServerKind(server) {
52937
+ const candidate = server.type ?? server.transport;
52938
+ if (candidate === "stdio" || candidate === "local") return "stdio";
52939
+ if (candidate === "http" || candidate === "streamable-http") return "http";
52940
+ if (candidate === "sse") return "sse";
52941
+ if (candidate !== void 0) return { unsupported: candidate };
52942
+ if (server.command !== void 0) return "stdio";
52943
+ const url = server.url ?? server.httpUrl;
52944
+ if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
52945
+ }
52946
+ /** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
52947
+ function convertServerToXum({ name, server, logger }) {
52948
+ const skip = (reason) => {
52949
+ warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
52950
+ return null;
52951
+ };
52952
+ const kind = resolveXumServerKind(server);
52953
+ if (kind === void 0) return skip("it has neither a command nor a URL");
52954
+ if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
52955
+ const settings = {};
52956
+ if (server.disabled === true) settings.disabled = true;
52957
+ if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
52958
+ if (kind !== "stdio") {
52959
+ const url = server.url ?? server.httpUrl;
52960
+ if (typeof url !== "string" || url === "") return skip("it has no URL");
52961
+ return {
52962
+ ...kind !== "auto" && { transport: kind },
52963
+ url,
52964
+ ...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
52965
+ ...settings
52966
+ };
52967
+ }
52968
+ const command = buildCommandString(server);
52969
+ if (command === void 0) return skip("it has no command");
52970
+ const dropped = [...isPlainObject$1(server.env) && Object.keys(server.env).length > 0 ? ["env"] : [], ...typeof server.cwd === "string" ? ["cwd"] : []];
52971
+ 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).`);
52972
+ return Object.keys(settings).length > 0 ? {
52973
+ command,
52974
+ ...settings
52975
+ } : command;
52976
+ }
52977
+ /** Copy only the string values of a header map (secret references have no canonical form). */
52978
+ function copyStringHeaders(value) {
52979
+ if (!isPlainObject$1(value)) return void 0;
52980
+ const result = {};
52981
+ for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
52982
+ return Object.keys(result).length > 0 ? result : void 0;
52983
+ }
52984
+ /** Read a Xum `servers` map back into the canonical shape. */
52985
+ function convertFromXumFormat(servers) {
52986
+ if (!isPlainObject$1(servers)) return {};
52987
+ const result = {};
52988
+ for (const [name, entry] of Object.entries(servers)) {
52989
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
52990
+ if (typeof entry === "string") {
52991
+ result[name] = splitCommandString(entry);
52992
+ continue;
52993
+ }
52994
+ if (!isPlainObject$1(entry)) continue;
52995
+ const server = {};
52996
+ if (typeof entry.url === "string") {
52997
+ if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
52998
+ server.url = entry.url;
52999
+ const headers = copyStringHeaders(entry.headers);
53000
+ if (headers !== void 0) server.headers = headers;
53001
+ } else if (typeof entry.command === "string") Object.assign(server, splitCommandString(entry.command));
53002
+ else continue;
53003
+ if (entry.disabled === true) server.disabled = true;
53004
+ if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
53005
+ result[name] = server;
53006
+ }
53007
+ return result;
53008
+ }
53009
+ /**
53010
+ * Xum MCP configuration.
53011
+ *
53012
+ * Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
53013
+ * (project; a server there wins over the user one of the same name). Both are
53014
+ * a JSONC document with a `servers` map:
53015
+ *
53016
+ * - a stdio server is a shell command string (the object form `{ command }`
53017
+ * carries `disabled` / `toolAllowlist`); Xum's mcp.jsonc has no `env` or
53018
+ * `cwd`, so those are dropped with a warning;
53019
+ * - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
53020
+ * (the transport defaults to auto-detection).
53021
+ *
53022
+ * rulesync owns only `servers`; other top-level keys (Xum writes
53023
+ * `enabledPluginServers` into the user file) are preserved. The project file
53024
+ * may be removed by `--delete`; the user file never is.
53025
+ *
53026
+ * @see https://xum.cdr.dev/config/mcp-servers
53027
+ * @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
53028
+ */
53029
+ var XumMcp = class XumMcp extends ToolMcp {
53030
+ json;
53031
+ constructor(params) {
53032
+ super(params);
53033
+ this.json = parseSharedConfig({
53034
+ format: "jsonc",
53035
+ fileContent: this.fileContent,
53036
+ filePath: (0, node_path.join)(this.relativeDirPath, this.relativeFilePath),
53037
+ invalidRootPolicy: "error",
53038
+ jsoncParseErrors: "error"
53039
+ });
53040
+ }
53041
+ getJson() {
53042
+ return this.json;
53043
+ }
53044
+ isDeletable() {
53045
+ return !this.global;
53046
+ }
53047
+ static getSettablePaths(_options = {}) {
53048
+ return {
53049
+ relativeDirPath: XUM_DIR,
53050
+ relativeFilePath: XUM_MCP_FILE_NAME
53051
+ };
53052
+ }
53053
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
53054
+ const paths = this.getSettablePaths({ global });
53055
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
53056
+ return new XumMcp({
53057
+ outputRoot,
53058
+ relativeDirPath: paths.relativeDirPath,
53059
+ relativeFilePath: paths.relativeFilePath,
53060
+ fileContent,
53061
+ validate,
53062
+ global
53063
+ });
53064
+ }
53065
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
53066
+ const paths = this.getSettablePaths({ global });
53067
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53068
+ const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
53069
+ const servers = {};
53070
+ for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
53071
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53072
+ const entry = convertServerToXum({
53073
+ name,
53074
+ server,
53075
+ logger
53076
+ });
53077
+ if (entry !== null) servers[name] = entry;
53078
+ }
53079
+ return new XumMcp({
53080
+ outputRoot,
53081
+ relativeDirPath: paths.relativeDirPath,
53082
+ relativeFilePath: paths.relativeFilePath,
53083
+ fileContent: applySharedConfigPatch({
53084
+ fileKey: sharedConfigFileKey(paths),
53085
+ feature: "mcp",
53086
+ existingContent,
53087
+ patch: { servers },
53088
+ filePath,
53089
+ logger
53090
+ }),
53091
+ validate,
53092
+ global
53093
+ });
53094
+ }
53095
+ toRulesyncMcp() {
53096
+ const mcpServers = convertFromXumFormat(this.json.servers);
53097
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
53098
+ }
53099
+ validate() {
53100
+ return {
53101
+ success: true,
53102
+ error: null
53103
+ };
53104
+ }
53105
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
53106
+ return new XumMcp({
53107
+ outputRoot,
53108
+ relativeDirPath,
53109
+ relativeFilePath,
53110
+ fileContent: "{}",
53111
+ validate: false,
53112
+ global
53113
+ });
53114
+ }
53115
+ };
53116
+ //#endregion
52033
53117
  //#region src/features/mcp/zcode-mcp.ts
52034
53118
  /**
52035
53119
  * Single spelling of the config.json codec/policy, matching the
@@ -52291,8 +53375,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
52291
53375
  * and unknown keys pass through untouched so an import keeps whatever a
52292
53376
  * hand-authored entry declared. `timeoutMs` is lifted back into the canonical
52293
53377
  * `timeout` (milliseconds); when the server also states a `timeout`, ZCode
52294
- * has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
52295
- * `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
53378
+ * has already dropped that one, so `timeoutMs` replaces it. Any record
53379
+ * `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
52296
53380
  * never emitted to another tool. Generation back out is a whitelist, so only
52297
53381
  * the keys `convertToZcodeFormat` writes are re-emitted.
52298
53382
  *
@@ -52318,7 +53402,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
52318
53402
  continue;
52319
53403
  }
52320
53404
  if (key === "timeout" && typeof config.timeoutMs === "number") continue;
52321
- if (key === "oauth" && isZcodeOauth(value)) {
53405
+ if (key === "oauth" && isRecord$1(value)) {
52322
53406
  converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
52323
53407
  continue;
52324
53408
  }
@@ -52964,6 +54048,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
52964
54048
  supportsDisabledTools: true
52965
54049
  }
52966
54050
  }],
54051
+ ["firebender", {
54052
+ class: FirebenderMcp,
54053
+ meta: {
54054
+ supportsProject: true,
54055
+ supportsGlobal: true,
54056
+ supportsEnabledTools: false,
54057
+ supportsDisabledTools: false
54058
+ }
54059
+ }],
52967
54060
  ["goose", {
52968
54061
  class: GooseMcp,
52969
54062
  meta: {
@@ -53234,6 +54327,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
53234
54327
  supportsDisabledTools: false
53235
54328
  }
53236
54329
  }],
54330
+ ["xum", {
54331
+ class: XumMcp,
54332
+ meta: {
54333
+ supportsProject: true,
54334
+ supportsGlobal: true,
54335
+ supportsEnabledTools: true,
54336
+ supportsDisabledTools: false
54337
+ }
54338
+ }],
53237
54339
  ["devin", {
53238
54340
  class: DevinMcp,
53239
54341
  meta: {
@@ -76445,6 +77547,11 @@ const TOOL_DISPLAY = [
76445
77547
  label: "Factory Droid",
76446
77548
  group: "ai"
76447
77549
  },
77550
+ {
77551
+ key: "firebender",
77552
+ label: "Firebender",
77553
+ group: "ai"
77554
+ },
76448
77555
  {
76449
77556
  key: "opencode",
76450
77557
  label: "OpenCode",
@@ -76600,6 +77707,11 @@ const TOOL_DISPLAY = [
76600
77707
  label: "JetBrains AI Assistant",
76601
77708
  group: "ai"
76602
77709
  },
77710
+ {
77711
+ key: "androidstudio",
77712
+ label: "Gemini in Android Studio",
77713
+ group: "ai"
77714
+ },
76603
77715
  {
76604
77716
  key: "junie",
76605
77717
  label: "JetBrains Junie",
@@ -76635,6 +77747,11 @@ const TOOL_DISPLAY = [
76635
77747
  label: "Warp Agent CLI",
76636
77748
  group: "ai"
76637
77749
  },
77750
+ {
77751
+ key: "xum",
77752
+ label: "Xum",
77753
+ group: "ai"
77754
+ },
76638
77755
  {
76639
77756
  key: "replit",
76640
77757
  label: "Replit",
@@ -77779,6 +78896,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
77779
78896
  }
77780
78897
  };
77781
78898
  //#endregion
78899
+ //#region src/features/skills/androidstudio-skill.ts
78900
+ /**
78901
+ * Gemini in Android Studio skills follow the Agent Skills open standard
78902
+ * (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
78903
+ * reuses {@link AgentsSkillsSkill} with Android Studio's own location:
78904
+ * `.android-studio/skills/` at the project root (project) or in the home
78905
+ * directory (global). The shared `.agents/skills/` location Android Studio also
78906
+ * reads belongs to the `agentsskills` target.
78907
+ *
78908
+ * @see https://developer.android.com/studio/gemini/skills
78909
+ */
78910
+ var AndroidstudioSkill = class extends AgentsSkillsSkill {
78911
+ static getSettablePaths(_options = {}) {
78912
+ return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
78913
+ }
78914
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
78915
+ const targets = rulesyncSkill.getFrontmatter().targets;
78916
+ return targets.includes("*") || targets.includes("androidstudio");
78917
+ }
78918
+ };
78919
+ //#endregion
77782
78920
  //#region src/features/skills/antigravity-plugin-skill.ts
77783
78921
  var AntigravityPluginSkill = class extends AntigravityIdeSkill {
77784
78922
  static isTargetedByRulesyncSkill(rulesyncSkill) {
@@ -81247,6 +82385,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
81247
82385
  }
81248
82386
  };
81249
82387
  //#endregion
82388
+ //#region src/features/skills/firebender-skill.ts
82389
+ /**
82390
+ * Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
82391
+ * required `name` and `description` frontmatter, plus optional supporting
82392
+ * files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
82393
+ * recommended locations: `.firebender/skills/` (project) and
82394
+ * `~/.firebender/skills/` (user).
82395
+ *
82396
+ * @see https://docs.firebender.com/api-reference/skills
82397
+ */
82398
+ var FirebenderSkill = class extends AgentsSkillsSkill {
82399
+ static getSettablePaths(_options = {}) {
82400
+ return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
82401
+ }
82402
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
82403
+ const targets = rulesyncSkill.getFrontmatter().targets;
82404
+ return targets.includes("*") || targets.includes("firebender");
82405
+ }
82406
+ };
82407
+ //#endregion
81250
82408
  //#region src/features/skills/gitlabduo-skill.ts
81251
82409
  /**
81252
82410
  * GitLab Duo skills follow the Agent Skills standard (`name` and `description`
@@ -84782,6 +85940,27 @@ var WarpSkill = class WarpSkill extends ToolSkill {
84782
85940
  }
84783
85941
  };
84784
85942
  //#endregion
85943
+ //#region src/features/skills/xum-skill.ts
85944
+ /**
85945
+ * Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
85946
+ * required `name` and `description` frontmatter; the directory name is
85947
+ * lowercase letters, digits and single hyphens up to 64 characters, and
85948
+ * `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
85949
+ * Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
85950
+ * Within one directory `.xum/skills` wins over `.agents/skills`.
85951
+ *
85952
+ * @see https://xum.cdr.dev/agents/agent-skills
85953
+ */
85954
+ var XumSkill = class extends AgentsSkillsSkill {
85955
+ static getSettablePaths(_options = {}) {
85956
+ return { relativeDirPath: XUM_SKILLS_DIR_PATH };
85957
+ }
85958
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
85959
+ const targets = rulesyncSkill.getFrontmatter().targets;
85960
+ return targets.includes("*") || targets.includes("xum");
85961
+ }
85962
+ };
85963
+ //#endregion
84785
85964
  //#region src/features/skills/zcode-skill.ts
84786
85965
  const ZcodeSkillFrontmatterSchema = zod_mini.z.looseObject({
84787
85966
  name: zod_mini.z.string(),
@@ -85197,6 +86376,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85197
86376
  supportsGlobal: false
85198
86377
  }
85199
86378
  }],
86379
+ ["androidstudio", {
86380
+ class: AndroidstudioSkill,
86381
+ meta: {
86382
+ supportsProject: true,
86383
+ supportsSimulated: false,
86384
+ supportsGlobal: true
86385
+ }
86386
+ }],
85200
86387
  ["amp", {
85201
86388
  class: AmpSkill,
85202
86389
  meta: {
@@ -85382,6 +86569,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85382
86569
  supportsGlobal: true
85383
86570
  }
85384
86571
  }],
86572
+ ["firebender", {
86573
+ class: FirebenderSkill,
86574
+ meta: {
86575
+ supportsProject: true,
86576
+ supportsSimulated: false,
86577
+ supportsGlobal: true
86578
+ }
86579
+ }],
85385
86580
  ["goose", {
85386
86581
  class: GooseSkill,
85387
86582
  meta: {
@@ -85632,6 +86827,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85632
86827
  supportsGlobal: true
85633
86828
  }
85634
86829
  }],
86830
+ ["xum", {
86831
+ class: XumSkill,
86832
+ meta: {
86833
+ supportsProject: true,
86834
+ supportsSimulated: false,
86835
+ supportsGlobal: true
86836
+ }
86837
+ }],
85635
86838
  ["devin", {
85636
86839
  class: DevinSkill,
85637
86840
  meta: {
@@ -89478,6 +90681,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
89478
90681
  }
89479
90682
  };
89480
90683
  //#endregion
90684
+ //#region src/features/subagents/firebender-subagent.ts
90685
+ /**
90686
+ * Frontmatter of a Firebender agent file. `tools` is a comma-separated string
90687
+ * of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
90688
+ * (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
90689
+ * model ID; `callable` lets the main agent delegate to it. Unknown keys are
90690
+ * preserved.
90691
+ *
90692
+ * @see https://docs.firebender.com/api-reference/agents
90693
+ */
90694
+ const FirebenderSubagentFrontmatterSchema = zod_mini.z.looseObject({
90695
+ name: zod_mini.z.optional(zod_mini.z.string()),
90696
+ description: zod_mini.z.optional(zod_mini.z.string()),
90697
+ color: zod_mini.z.optional(zod_mini.z.string()),
90698
+ icon: zod_mini.z.optional(zod_mini.z.string()),
90699
+ tools: zod_mini.z.optional(zod_mini.z.string()),
90700
+ model: zod_mini.z.optional(zod_mini.z.string()),
90701
+ callable: zod_mini.z.optional(zod_mini.z.boolean())
90702
+ });
90703
+ /**
90704
+ * The `agents` entry Firebender resolves to an agent file rulesync writes:
90705
+ * relative to the project root for the project config, and to `~/.firebender`
90706
+ * for the personal one.
90707
+ */
90708
+ function toAgentsEntry({ relativeFilePath, global }) {
90709
+ const fileName = toPosixPath(relativeFilePath);
90710
+ return global ? node_path.posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : node_path.posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
90711
+ }
90712
+ /**
90713
+ * The agent file name an existing `agents` entry points at when it lies
90714
+ * directly inside the directory rulesync writes agents to
90715
+ * (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
90716
+ * in the personal config), or `undefined` for any other entry
90717
+ * (`~/firebender-agents/...`, an absolute path, a nested path).
90718
+ */
90719
+ function managedAgentFileName({ entry, global }) {
90720
+ const normalized = toPosixPath(entry).replace(/^\.\//, "");
90721
+ const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
90722
+ const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
90723
+ const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
90724
+ return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
90725
+ }
90726
+ /**
90727
+ * Subagent generator for Firebender.
90728
+ *
90729
+ * Each agent is a Markdown file with YAML frontmatter, written to
90730
+ * `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
90731
+ * (global). Firebender only loads agents listed in the `agents` array of
90732
+ * `firebender.json`, so generation also registers every file there — see
90733
+ * {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
90734
+ * delegated to, so `callable: true` is written unless the `firebender:`
90735
+ * section says otherwise.
90736
+ *
90737
+ * @see https://docs.firebender.com/api-reference/agents
90738
+ */
90739
+ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
90740
+ frontmatter;
90741
+ body;
90742
+ constructor({ frontmatter, body, ...rest }) {
90743
+ if (rest.validate !== false) {
90744
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
90745
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
90746
+ }
90747
+ super({ ...rest });
90748
+ this.frontmatter = frontmatter;
90749
+ this.body = body;
90750
+ }
90751
+ static getSettablePaths(_options = {}) {
90752
+ return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
90753
+ }
90754
+ /**
90755
+ * Generation also read-modify-writes the `agents` key of `firebender.json`,
90756
+ * so the write is declared for the shared-file order derivation.
90757
+ */
90758
+ static getExtraSharedWritePaths({ global = false } = {}) {
90759
+ return [{
90760
+ relativeDirPath: getFirebenderConfigDir({ global }),
90761
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
90762
+ }];
90763
+ }
90764
+ getFrontmatter() {
90765
+ return this.frontmatter;
90766
+ }
90767
+ getBody() {
90768
+ return this.body;
90769
+ }
90770
+ toRulesyncSubagent() {
90771
+ const { name, description, callable, ...rest } = this.frontmatter;
90772
+ const firebenderSection = {
90773
+ ...rest,
90774
+ ...callable !== true && { callable: callable ?? false }
90775
+ };
90776
+ return new RulesyncSubagent({
90777
+ outputRoot: ".",
90778
+ frontmatter: {
90779
+ targets: ["*"],
90780
+ name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), ".md"),
90781
+ description,
90782
+ ...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
90783
+ },
90784
+ body: this.body,
90785
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
90786
+ relativeFilePath: this.getRelativeFilePath(),
90787
+ validate: true
90788
+ });
90789
+ }
90790
+ /**
90791
+ * Every targeted subagent becomes its own agent file, plus one
90792
+ * {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
90793
+ * The registry is emitted even when no subagent is targeted, so agents a
90794
+ * previous run registered are retracted once their files are gone. The
90795
+ * agent directory is listed up front so the registry can tell a registered
90796
+ * file that still exists (kept, whoever wrote it) from a stale one.
90797
+ */
90798
+ static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
90799
+ const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
90800
+ const agentsDirEscapesRoot = await writablePathEscapesRoot({
90801
+ rootPath: outputRoot,
90802
+ targetPath: (0, node_path.join)(outputRoot, agentsDirPath, "agent.md")
90803
+ });
90804
+ const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames((0, node_path.join)(outputRoot, agentsDirPath)));
90805
+ const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
90806
+ outputRoot,
90807
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
90808
+ rulesyncSubagent,
90809
+ global
90810
+ }));
90811
+ const registry = new FirebenderAgentsRegistry({
90812
+ outputRoot,
90813
+ relativeDirPath: getFirebenderConfigDir({ global }),
90814
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
90815
+ entries: subagents.map((subagent) => toAgentsEntry({
90816
+ relativeFilePath: subagent.getRelativeFilePath(),
90817
+ global
90818
+ })),
90819
+ existingAgentFileNames,
90820
+ deleteOrphans,
90821
+ frozen: agentsDirEscapesRoot,
90822
+ global,
90823
+ logger
90824
+ });
90825
+ return [...subagents, registry];
90826
+ }
90827
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
90828
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
90829
+ const firebenderSection = rulesyncFrontmatter.firebender ?? {};
90830
+ const frontmatter = {
90831
+ name: rulesyncFrontmatter.name,
90832
+ ...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
90833
+ callable: true,
90834
+ ...firebenderSection
90835
+ };
90836
+ const body = rulesyncSubagent.getBody();
90837
+ return new FirebenderSubagent({
90838
+ outputRoot,
90839
+ frontmatter,
90840
+ body,
90841
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
90842
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
90843
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
90844
+ validate,
90845
+ global
90846
+ });
90847
+ }
90848
+ validate() {
90849
+ if (!this.frontmatter) return {
90850
+ success: true,
90851
+ error: null
90852
+ };
90853
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
90854
+ if (result.success) return {
90855
+ success: true,
90856
+ error: null
90857
+ };
90858
+ return {
90859
+ success: false,
90860
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
90861
+ };
90862
+ }
90863
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
90864
+ return this.isTargetedByRulesyncSubagentDefault({
90865
+ rulesyncSubagent,
90866
+ toolTarget: "firebender"
90867
+ });
90868
+ }
90869
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
90870
+ const paths = this.getSettablePaths({ global });
90871
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
90872
+ const fileContent = await readFileContent(filePath);
90873
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
90874
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
90875
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
90876
+ return new FirebenderSubagent({
90877
+ outputRoot,
90878
+ relativeDirPath: paths.relativeDirPath,
90879
+ relativeFilePath,
90880
+ frontmatter: result.data,
90881
+ body: body.trim(),
90882
+ fileContent,
90883
+ validate,
90884
+ global
90885
+ });
90886
+ }
90887
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
90888
+ return new FirebenderSubagent({
90889
+ outputRoot,
90890
+ relativeDirPath,
90891
+ relativeFilePath,
90892
+ frontmatter: {},
90893
+ body: "",
90894
+ fileContent: "",
90895
+ validate: false
90896
+ });
90897
+ }
90898
+ };
90899
+ /**
90900
+ * The `agents` array of `firebender.json`, which Firebender requires to load
90901
+ * an agent file. Every generated agent is registered. An existing entry that
90902
+ * points into rulesync's agent directory is kept while its file exists (a
90903
+ * hand-written agent there stays registered) and dropped, with a warning, once
90904
+ * the file is gone. Under `--delete` the orphan sweep removes every agent file
90905
+ * there that this run does not generate, but only after this file is written,
90906
+ * so in that mode such entries are dropped here too and a single run
90907
+ * converges. Every other entry and every other key of the file is kept.
90908
+ * The array is merged into the file on disk at write time and the file itself
90909
+ * is never deleted.
90910
+ */
90911
+ var FirebenderAgentsRegistry = class extends ToolSubagent {
90912
+ entries;
90913
+ existingAgentFileNames;
90914
+ deleteOrphans;
90915
+ frozen;
90916
+ logger;
90917
+ mergedContent = null;
90918
+ constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
90919
+ super({
90920
+ ...rest,
90921
+ fileContent: ""
90922
+ });
90923
+ this.entries = entries;
90924
+ this.existingAgentFileNames = existingAgentFileNames;
90925
+ this.deleteOrphans = deleteOrphans;
90926
+ this.frozen = frozen;
90927
+ this.logger = logger;
90928
+ }
90929
+ getEntries() {
90930
+ return this.entries;
90931
+ }
90932
+ isDeletable() {
90933
+ return false;
90934
+ }
90935
+ shouldMergeExistingFileContent() {
90936
+ return true;
90937
+ }
90938
+ setFileContent(newFileContent) {
90939
+ this.mergedContent = this.mergeInto(newFileContent);
90940
+ }
90941
+ getFileContent() {
90942
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
90943
+ return this.mergedContent;
90944
+ }
90945
+ mergeInto(existingContent) {
90946
+ const paths = {
90947
+ relativeDirPath: this.getRelativeDirPath(),
90948
+ relativeFilePath: this.getRelativeFilePath()
90949
+ };
90950
+ const filePath = (0, node_path.join)(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
90951
+ const existing = parseSharedConfig({
90952
+ format: "json",
90953
+ fileContent: existingContent,
90954
+ filePath,
90955
+ invalidRootPolicy: "error"
90956
+ });
90957
+ if (this.frozen) return applySharedConfigPatch({
90958
+ fileKey: sharedConfigFileKey(paths),
90959
+ feature: "subagents",
90960
+ existingContent,
90961
+ patch: { agents: existing.agents },
90962
+ filePath,
90963
+ logger: this.logger
90964
+ });
90965
+ const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
90966
+ const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
90967
+ entry,
90968
+ global: this.global
90969
+ }) ?? entry, entry]));
90970
+ const agents = [];
90971
+ const placed = /* @__PURE__ */ new Set();
90972
+ const dropped = [];
90973
+ for (const entry of existingEntries) {
90974
+ const fileName = typeof entry === "string" ? managedAgentFileName({
90975
+ entry,
90976
+ global: this.global
90977
+ }) : void 0;
90978
+ if (fileName === void 0) {
90979
+ agents.push(entry);
90980
+ continue;
90981
+ }
90982
+ const generated = generatedByFileName.get(fileName);
90983
+ if (generated !== void 0) {
90984
+ if (!placed.has(fileName)) {
90985
+ agents.push(generated);
90986
+ placed.add(fileName);
90987
+ }
90988
+ } else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
90989
+ else dropped.push(String(entry));
90990
+ }
90991
+ for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
90992
+ agents.push(entry);
90993
+ placed.add(fileName);
90994
+ }
90995
+ 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."));
90996
+ return applySharedConfigPatch({
90997
+ fileKey: sharedConfigFileKey(paths),
90998
+ feature: "subagents",
90999
+ existingContent,
91000
+ patch: { agents: agents.length > 0 ? agents : void 0 },
91001
+ filePath,
91002
+ logger: this.logger
91003
+ });
91004
+ }
91005
+ toRulesyncSubagent() {
91006
+ throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
91007
+ }
91008
+ validate() {
91009
+ return {
91010
+ success: true,
91011
+ error: null
91012
+ };
91013
+ }
91014
+ };
91015
+ //#endregion
89481
91016
  //#region src/features/subagents/goose-subagent.ts
89482
91017
  /**
89483
91018
  * Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
@@ -89765,13 +91300,13 @@ function subagentSlug(relativeFilePath) {
89765
91300
  return (0, node_path.basename)(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
89766
91301
  }
89767
91302
  function hermesCommandName(slug) {
89768
- return `rulesync_subagent_${slug}`;
91303
+ return hermesSlashName(`rulesync-subagent-${slug}`);
89769
91304
  }
89770
91305
  function getPluginManifestContent() {
89771
91306
  return [
89772
91307
  "name: rulesync-subagents",
89773
91308
  "version: \"1.0.0\"",
89774
- "description: Exposes RuleSync subagents as Hermes native delegation commands.",
91309
+ "description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
89775
91310
  ""
89776
91311
  ].join("\n");
89777
91312
  }
@@ -89779,9 +91314,12 @@ function getPluginInitContent() {
89779
91314
  return `"""RuleSync-generated Hermes subagent commands."""
89780
91315
 
89781
91316
  import json
91317
+ import logging
91318
+ import re
89782
91319
  from pathlib import Path
89783
91320
 
89784
91321
 
91322
+ logger = logging.getLogger(__name__)
89785
91323
  SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
89786
91324
 
89787
91325
 
@@ -89801,52 +91339,71 @@ def _load_subagents():
89801
91339
  return subagents
89802
91340
 
89803
91341
 
89804
- def _register_subagent(ctx, subagent):
91342
+ def _command_name(subagent, slug):
91343
+ # The spec carries the name Rulesync normalized; Hermes' messaging gateway
91344
+ # resolves plugin commands with "_" turned into "-", so keep it hyphenated.
91345
+ hermes = subagent.get("hermes")
91346
+ name = hermes.get("command") if isinstance(hermes, dict) else None
91347
+ name = name or f"rulesync-subagent-{slug}"
91348
+ # Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
91349
+ name = re.sub(r"[ _]", "-", str(name).lower())
91350
+ name = re.sub(r"[^a-z0-9-]", "", name)
91351
+ return re.sub(r"-{2,}", "-", name).strip("-")
91352
+
91353
+
91354
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
91355
+
91356
+ def _register_subagent(ctx, subagent, registered):
89805
91357
  slug = subagent.get("slug")
89806
91358
  if not slug:
89807
91359
  return
89808
91360
 
89809
- command_name = f"rulesync_subagent_{slug}"
91361
+ command_name = _command_name(subagent, slug)
91362
+ if command_name in registered:
91363
+ logger.warning(
91364
+ "rulesync-subagents: %s and %s both map to /%s; keeping the first.",
91365
+ registered[command_name],
91366
+ subagent.get("_path"),
91367
+ command_name,
91368
+ )
91369
+ return
91370
+ registered[command_name] = subagent.get("_path")
91371
+
89810
91372
  name = subagent.get("name") or slug
89811
91373
  description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
89812
91374
  system_prompt = subagent.get("prompt") or ""
89813
91375
 
89814
91376
  def handler(args=None, **kwargs):
89815
- del kwargs
89816
91377
  user_context = ""
89817
91378
  if isinstance(args, dict):
89818
91379
  user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
89819
91380
  elif args is not None:
89820
91381
  user_context = str(args)
89821
91382
 
89822
- context_parts = []
91383
+ # A slash command runs outside any agent turn, so calling delegate_task
91384
+ # here fails with "delegate_task requires a parent agent context".
91385
+ # Queue the brief as a user message instead; the agent then delegates.
91386
+ parts = [
91387
+ f"Use the delegate_task tool to hand the following task to a subagent "
91388
+ f"acting as the {name} RuleSync subagent.",
91389
+ f"Goal: {description}",
91390
+ ]
89823
91391
  if system_prompt:
89824
- context_parts.append(system_prompt)
91392
+ parts.append(f"Subagent instructions:\\n{system_prompt}")
89825
91393
  if user_context:
89826
- context_parts.append(user_context)
91394
+ parts.append(f"Task details:\\n{user_context}")
89827
91395
 
89828
- # delegate_task takes no model-facing "toolsets" argument: a subagent
89829
- # inherits the parent's enabled toolsets (role="orchestrator" is the
89830
- # only knob that changes them). "tasks" is the only shape its schema
89831
- # advertises; a top-level "goal" is an unadvertised legacy path.
89832
- return ctx.dispatch_tool(
89833
- "delegate_task",
89834
- {
89835
- "tasks": [
89836
- {
89837
- "goal": description,
89838
- "context": "\\n\\n".join(context_parts),
89839
- },
89840
- ],
89841
- },
89842
- )
91396
+ label = f"the {name} subagent task"
91397
+ error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
91398
+ return error or f"Queued {label}; the agent will delegate it."
89843
91399
 
89844
91400
  ctx.register_command(command_name, handler, description)
89845
91401
 
89846
91402
 
89847
91403
  def register(ctx):
91404
+ registered = {}
89848
91405
  for subagent in _load_subagents():
89849
- _register_subagent(ctx, subagent)
91406
+ _register_subagent(ctx, subagent, registered)
89850
91407
  `;
89851
91408
  }
89852
91409
  function getEnabledPluginConfigContent({ currentContent, global }) {
@@ -89877,7 +91434,7 @@ function getSubagentSpec(rulesyncSubagent) {
89877
91434
  prompt: rulesyncSubagent.getBody(),
89878
91435
  hermes: {
89879
91436
  command: hermesCommandName(slug),
89880
- dispatch: "delegate_task"
91437
+ dispatch: "inject_message"
89881
91438
  }
89882
91439
  };
89883
91440
  }
@@ -92669,6 +94226,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
92669
94226
  }
92670
94227
  };
92671
94228
  //#endregion
94229
+ //#region src/features/subagents/xum-subagent.ts
94230
+ /**
94231
+ * The built-in agent a rulesync subagent inherits from when its `xum` block
94232
+ * names neither a `base` nor its own `tools`. Xum gives an agent no tools at
94233
+ * all unless some agent in its `base` chain declares them, so a definition
94234
+ * without either would be a prompt that cannot read or edit anything.
94235
+ */
94236
+ const XUM_DEFAULT_BASE_AGENT = "exec";
94237
+ /**
94238
+ * Frontmatter of a Xum agent definition. `name` (the display name) is
94239
+ * required; the other documented fields are typed loosely and unknown keys are
94240
+ * kept (looseObject) so a round trip never drops them.
94241
+ *
94242
+ * @see https://xum.cdr.dev/agents
94243
+ */
94244
+ const XumSubagentFrontmatterSchema = zod_mini.z.looseObject({
94245
+ name: zod_mini.z.string(),
94246
+ description: zod_mini.z.optional(zod_mini.z.string()),
94247
+ base: zod_mini.z.optional(zod_mini.z.string()),
94248
+ disabled: zod_mini.z.optional(zod_mini.z.boolean()),
94249
+ ui: zod_mini.z.optional(zod_mini.z.looseObject({})),
94250
+ prompt: zod_mini.z.optional(zod_mini.z.looseObject({})),
94251
+ subagent: zod_mini.z.optional(zod_mini.z.looseObject({})),
94252
+ ai: zod_mini.z.optional(zod_mini.z.looseObject({})),
94253
+ tools: zod_mini.z.optional(zod_mini.z.looseObject({}))
94254
+ });
94255
+ /**
94256
+ * Subagent generator for Xum (formerly Mux).
94257
+ *
94258
+ * Xum reads custom agent definitions — Markdown with YAML frontmatter — from
94259
+ * the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
94260
+ * body is the agent's instruction prompt. The file name (without `.md`) is the
94261
+ * agent id, so it must be lowercase letters and digits with single `-` / `_`
94262
+ * separators; Xum skips other files with a warning.
94263
+ *
94264
+ * rulesync writes `name` and `description`, spreads the `xum` section (`base`,
94265
+ * `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
94266
+ * defaults so the definition works as a delegated subagent out of the box:
94267
+ * `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
94268
+ * when the section sets neither `base` nor `tools`, `base: exec`.
94269
+ *
94270
+ * @see https://xum.cdr.dev/agents
94271
+ */
94272
+ var XumSubagent = class XumSubagent extends ToolSubagent {
94273
+ frontmatter;
94274
+ body;
94275
+ constructor({ frontmatter, body, ...rest }) {
94276
+ if (rest.validate !== false) {
94277
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
94278
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
94279
+ }
94280
+ super({ ...rest });
94281
+ this.frontmatter = frontmatter;
94282
+ this.body = body;
94283
+ }
94284
+ static getSettablePaths(_options = {}) {
94285
+ return { relativeDirPath: XUM_AGENTS_DIR_PATH };
94286
+ }
94287
+ getFrontmatter() {
94288
+ return this.frontmatter;
94289
+ }
94290
+ getBody() {
94291
+ return this.body;
94292
+ }
94293
+ toRulesyncSubagent() {
94294
+ const { name, description, ...restFields } = this.frontmatter;
94295
+ return new RulesyncSubagent({
94296
+ outputRoot: ".",
94297
+ frontmatter: {
94298
+ targets: ["*"],
94299
+ name,
94300
+ description,
94301
+ ...Object.keys(restFields).length > 0 && { xum: restFields }
94302
+ },
94303
+ body: this.body,
94304
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
94305
+ relativeFilePath: this.getRelativeFilePath(),
94306
+ validate: true
94307
+ });
94308
+ }
94309
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
94310
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
94311
+ const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
94312
+ const subagentSection = xumSection.subagent;
94313
+ const result = XumSubagentFrontmatterSchema.safeParse({
94314
+ name: rulesyncFrontmatter.name,
94315
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
94316
+ ...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
94317
+ ...xumSection,
94318
+ subagent: {
94319
+ runnable: true,
94320
+ ...isPlainObject$1(subagentSection) ? subagentSection : {}
94321
+ }
94322
+ });
94323
+ if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
94324
+ const body = rulesyncSubagent.getBody();
94325
+ return new XumSubagent({
94326
+ outputRoot,
94327
+ frontmatter: result.data,
94328
+ body,
94329
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
94330
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
94331
+ fileContent: stringifyFrontmatter(body, result.data),
94332
+ validate
94333
+ });
94334
+ }
94335
+ validate() {
94336
+ if (!this.frontmatter) return {
94337
+ success: true,
94338
+ error: null
94339
+ };
94340
+ const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
94341
+ if (result.success) return {
94342
+ success: true,
94343
+ error: null
94344
+ };
94345
+ return {
94346
+ success: false,
94347
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
94348
+ };
94349
+ }
94350
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
94351
+ return this.isTargetedByRulesyncSubagentDefault({
94352
+ rulesyncSubagent,
94353
+ toolTarget: "xum"
94354
+ });
94355
+ }
94356
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
94357
+ const paths = this.getSettablePaths({ global });
94358
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
94359
+ const fileContent = await readFileContent(filePath);
94360
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
94361
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
94362
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
94363
+ return new XumSubagent({
94364
+ outputRoot,
94365
+ relativeDirPath: paths.relativeDirPath,
94366
+ relativeFilePath,
94367
+ frontmatter: result.data,
94368
+ body: body.trim(),
94369
+ fileContent,
94370
+ validate
94371
+ });
94372
+ }
94373
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
94374
+ return new XumSubagent({
94375
+ outputRoot,
94376
+ relativeDirPath,
94377
+ relativeFilePath,
94378
+ frontmatter: {
94379
+ name: "",
94380
+ description: ""
94381
+ },
94382
+ body: "",
94383
+ fileContent: "",
94384
+ validate: false
94385
+ });
94386
+ }
94387
+ };
94388
+ //#endregion
92672
94389
  //#region src/features/subagents/zcode-subagent.ts
92673
94390
  const ZcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
92674
94391
  name: zod_mini.z.string(),
@@ -93134,6 +94851,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93134
94851
  filePattern: "*.md"
93135
94852
  }
93136
94853
  }],
94854
+ ["firebender", {
94855
+ class: FirebenderSubagent,
94856
+ meta: {
94857
+ supportsProject: true,
94858
+ supportsSimulated: false,
94859
+ supportsGlobal: true,
94860
+ emitsEmptyAggregate: true,
94861
+ emitsEmptyAggregateOnDelete: true,
94862
+ filePattern: "*.md"
94863
+ }
94864
+ }],
93137
94865
  ["goose", {
93138
94866
  class: GooseSubagent,
93139
94867
  meta: {
@@ -93361,6 +95089,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93361
95089
  filePattern: "*.toml"
93362
95090
  }
93363
95091
  }],
95092
+ ["xum", {
95093
+ class: XumSubagent,
95094
+ meta: {
95095
+ supportsProject: true,
95096
+ supportsSimulated: false,
95097
+ supportsGlobal: true,
95098
+ filePattern: "*.md"
95099
+ }
95100
+ }],
93364
95101
  ["zcode", {
93365
95102
  class: ZcodeSubagent,
93366
95103
  meta: {
@@ -93415,6 +95152,7 @@ function compareRelativePathsShallowestFirst(left, right) {
93415
95152
  var SubagentsProcessor = class extends FeatureProcessor {
93416
95153
  toolTarget;
93417
95154
  global;
95155
+ deleteOrphans;
93418
95156
  getFactory;
93419
95157
  /**
93420
95158
  * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
@@ -93422,13 +95160,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
93422
95160
  * agents in its Pool settings are its own and must not be retracted.
93423
95161
  */
93424
95162
  rulesyncSourceDirFound = false;
93425
- constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
95163
+ constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
93426
95164
  super({
93427
95165
  outputRoot,
93428
95166
  inputRoots,
93429
95167
  dryRun,
93430
95168
  logger
93431
95169
  });
95170
+ this.deleteOrphans = deleteOrphans;
93432
95171
  const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
93433
95172
  if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
93434
95173
  this.toolTarget = result.data;
@@ -93436,7 +95175,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
93436
95175
  this.getFactory = getFactory;
93437
95176
  }
93438
95177
  emitsToolFilesForEmptySource() {
93439
- return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
95178
+ const { meta } = this.getFactory(this.toolTarget);
95179
+ if (meta.emitsEmptyAggregate !== true) return false;
95180
+ return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
93440
95181
  }
93441
95182
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
93442
95183
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
@@ -93452,11 +95193,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
93452
95193
  });
93453
95194
  if (factory.class.fromRulesyncSubagents) {
93454
95195
  if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
93455
- const toolSubagents = factory.class.fromRulesyncSubagents({
95196
+ const toolSubagents = await factory.class.fromRulesyncSubagents({
93456
95197
  outputRoot: this.outputRoot,
93457
95198
  rulesyncSubagents: targeted,
93458
95199
  global: this.global,
93459
- logger: this.logger
95200
+ logger: this.logger,
95201
+ deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
93460
95202
  });
93461
95203
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
93462
95204
  }
@@ -94202,7 +95944,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
94202
95944
  /**
94203
95945
  * Globs that match the whole project and so say nothing beyond "always".
94204
95946
  */
94205
- const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
95947
+ const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
94206
95948
  const METADATA_DELIMITER = "---";
94207
95949
  /**
94208
95950
  * Splits text into lines that keep their own line terminator, so the body can
@@ -94327,7 +96069,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
94327
96069
  */
94328
96070
  static buildMetadata(frontmatter) {
94329
96071
  const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
94330
- const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$5.has(glob));
96072
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
94331
96073
  const instructions = frontmatter.description && flattenValue(frontmatter.description);
94332
96074
  const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
94333
96075
  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";
@@ -96981,7 +98723,7 @@ function normalizePatternList(value) {
96981
98723
  * an always-on rule into a "when any file is in context" rule. Dropped on
96982
98724
  * generate and mapped back on import.
96983
98725
  */
96984
- const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
98726
+ const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
96985
98727
  /**
96986
98728
  * Rule generator for Continue (continuedev/continue), the open-source IDE
96987
98729
  * extension and CLI (`cn`).
@@ -97095,7 +98837,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
97095
98837
  if (alwaysApply === true) return;
97096
98838
  const targetGlobs = continueGlobs ?? parentGlobs;
97097
98839
  if (!targetGlobs || targetGlobs.length === 0) return;
97098
- if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
98840
+ if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
97099
98841
  return targetGlobs;
97100
98842
  }
97101
98843
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
@@ -97587,7 +99329,7 @@ const CursorRuleFrontmatterSchema = zod_mini.z.object({
97587
99329
  * does not already say. Same pair the Cline and Qwen Code adapters treat as
97588
99330
  * universal.
97589
99331
  */
97590
- const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
99332
+ const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
97591
99333
  var CursorRule = class CursorRule extends ToolRule {
97592
99334
  frontmatter;
97593
99335
  body;
@@ -97628,7 +99370,8 @@ var CursorRule = class CursorRule extends ToolRule {
97628
99370
  }
97629
99371
  /**
97630
99372
  * Custom parse function for Cursor MDC files
97631
- * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially
99373
+ * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
99374
+ * Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
97632
99375
  */
97633
99376
  static parseCursorFrontmatter(fileContent, filePath) {
97634
99377
  const bounds = findFrontmatterBlockBounds(fileContent);
@@ -97691,7 +99434,7 @@ var CursorRule = class CursorRule extends ToolRule {
97691
99434
  static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
97692
99435
  const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
97693
99436
  if (!targetGlobs || targetGlobs.length === 0) return;
97694
- if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$3.has(glob.trim()))) return;
99437
+ if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
97695
99438
  return targetGlobs.join(",");
97696
99439
  }
97697
99440
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
@@ -98605,6 +100348,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
98605
100348
  }
98606
100349
  };
98607
100350
  //#endregion
100351
+ //#region src/features/rules/firebender-rule.ts
100352
+ /**
100353
+ * Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
100354
+ * is one comma-separated string, and a YAML list is tolerated on import.
100355
+ */
100356
+ const FirebenderRuleFrontmatterSchema = zod_mini.z.looseObject({
100357
+ description: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.string())),
100358
+ globs: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))),
100359
+ alwaysApply: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.boolean()))
100360
+ });
100361
+ /** Globs that match every file, and so add nothing to an always-applied rule. */
100362
+ const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
100363
+ function normalizeGlobs$2(globs) {
100364
+ if (globs === null || globs === void 0) return [];
100365
+ return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
100366
+ }
100367
+ /**
100368
+ * Rule generator for Firebender.
100369
+ *
100370
+ * Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
100371
+ * project-root `AGENTS.md`, then the same pair in parent directories, then
100372
+ * `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
100373
+ * is written to `.firebender/AGENTS.md` in the project and to
100374
+ * `~/.firebender/AGENTS.md` globally, where it does not collide with the
100375
+ * project-root `AGENTS.md` the `agentsmd` target owns.
100376
+ *
100377
+ * Non-root rules: `.firebender/rules/*.mdc` (project) and
100378
+ * `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
100379
+ * frontmatter: `description`, `globs` (one comma-separated string) and
100380
+ * `alwaysApply`. A rule with specific globs is written with
100381
+ * `alwaysApply: false` and those globs; every other rule is always applied.
100382
+ *
100383
+ * @see https://docs.firebender.com/api-reference/rules
100384
+ * @see https://docs.firebender.com/api-reference/agents-md
100385
+ */
100386
+ var FirebenderRule = class FirebenderRule extends ToolRule {
100387
+ frontmatter;
100388
+ body;
100389
+ constructor({ frontmatter = {}, body, ...rest }) {
100390
+ if (rest.validate && !rest.root) {
100391
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
100392
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
100393
+ }
100394
+ const resolvedBody = body ?? rest.fileContent;
100395
+ super({
100396
+ ...rest,
100397
+ fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
100398
+ });
100399
+ this.frontmatter = frontmatter;
100400
+ this.body = resolvedBody;
100401
+ }
100402
+ static getSettablePaths({ excludeToolDir } = {}) {
100403
+ return {
100404
+ root: {
100405
+ relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
100406
+ relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
100407
+ },
100408
+ nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
100409
+ };
100410
+ }
100411
+ /**
100412
+ * The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
100413
+ * flattened `description`, then the globs joined the way Firebender's docs
100414
+ * spell them (`"*.kt, *.kts"`).
100415
+ */
100416
+ static toOutputFrontmatter(frontmatter) {
100417
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
100418
+ const globs = normalizeGlobs$2(frontmatter.globs);
100419
+ return {
100420
+ ...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
100421
+ ...description !== "" && { description },
100422
+ ...globs.length > 0 && { globs: globs.join(", ") }
100423
+ };
100424
+ }
100425
+ /**
100426
+ * Derives the Firebender frontmatter from the canonical one: specific globs
100427
+ * make a file-scoped rule, everything else (no globs, universal globs) is
100428
+ * always applied. Firebender splits `globs` on every comma, so a brace
100429
+ * alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
100430
+ */
100431
+ static buildFrontmatter(frontmatter) {
100432
+ const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
100433
+ const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
100434
+ const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
100435
+ const description = frontmatter.description?.trim() || void 0;
100436
+ return {
100437
+ alwaysApply: !isFileScoped,
100438
+ ...description !== void 0 && { description },
100439
+ ...isFileScoped && { globs: specificGlobs.join(", ") }
100440
+ };
100441
+ }
100442
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
100443
+ const paths = this.getSettablePaths();
100444
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
100445
+ if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
100446
+ outputRoot,
100447
+ relativeDirPath: paths.root.relativeDirPath,
100448
+ relativeFilePath: paths.root.relativeFilePath,
100449
+ fileContent: rulesyncRule.getBody(),
100450
+ validate,
100451
+ root: true,
100452
+ description: rulesyncFrontmatter.description,
100453
+ globs: rulesyncFrontmatter.globs
100454
+ });
100455
+ return new FirebenderRule({
100456
+ outputRoot,
100457
+ relativeDirPath: paths.nonRoot.relativeDirPath,
100458
+ relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
100459
+ frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
100460
+ body: rulesyncRule.getBody(),
100461
+ fileContent: "",
100462
+ validate,
100463
+ root: false
100464
+ });
100465
+ }
100466
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
100467
+ const paths = this.getSettablePaths();
100468
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
100469
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
100470
+ return new FirebenderRule({
100471
+ outputRoot,
100472
+ relativeDirPath: paths.root.relativeDirPath,
100473
+ relativeFilePath: paths.root.relativeFilePath,
100474
+ fileContent,
100475
+ validate,
100476
+ root: true
100477
+ });
100478
+ }
100479
+ const filePath = (0, node_path.join)(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
100480
+ const fileContent = await readFileContent(filePath);
100481
+ const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
100482
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
100483
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
100484
+ return new FirebenderRule({
100485
+ outputRoot,
100486
+ relativeDirPath: paths.nonRoot.relativeDirPath,
100487
+ relativeFilePath,
100488
+ frontmatter: result.data,
100489
+ body: body.trim(),
100490
+ fileContent: "",
100491
+ validate,
100492
+ root: false
100493
+ });
100494
+ }
100495
+ toRulesyncRule() {
100496
+ if (this.isRoot()) return this.toRulesyncRuleDefault();
100497
+ const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
100498
+ const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
100499
+ const description = this.frontmatter.description?.trim() || void 0;
100500
+ const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
100501
+ return new RulesyncRule({
100502
+ outputRoot: process.cwd(),
100503
+ frontmatter: {
100504
+ targets: ["*"],
100505
+ root: false,
100506
+ ...description !== void 0 && { description },
100507
+ globs
100508
+ },
100509
+ body: this.body,
100510
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
100511
+ relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
100512
+ validate: true
100513
+ });
100514
+ }
100515
+ validate() {
100516
+ if (this.isRoot() || !this.frontmatter) return {
100517
+ success: true,
100518
+ error: null
100519
+ };
100520
+ const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
100521
+ if (result.success) return {
100522
+ success: true,
100523
+ error: null
100524
+ };
100525
+ return {
100526
+ success: false,
100527
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
100528
+ };
100529
+ }
100530
+ getFrontmatter() {
100531
+ return this.frontmatter;
100532
+ }
100533
+ getBody() {
100534
+ return this.body;
100535
+ }
100536
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
100537
+ const paths = this.getSettablePaths();
100538
+ const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
100539
+ return new FirebenderRule({
100540
+ outputRoot,
100541
+ relativeDirPath,
100542
+ relativeFilePath,
100543
+ fileContent: "",
100544
+ validate: false,
100545
+ root: isRoot
100546
+ });
100547
+ }
100548
+ static isTargetedByRulesyncRule(rulesyncRule) {
100549
+ return this.isTargetedByRulesyncRuleDefault({
100550
+ rulesyncRule,
100551
+ toolTarget: "firebender"
100552
+ });
100553
+ }
100554
+ };
100555
+ //#endregion
98608
100556
  //#region src/features/rules/gitlabduo-rule.ts
98609
100557
  /**
98610
100558
  * GitLab Duo CLI reads custom rules from a single plain-Markdown file:
@@ -101886,6 +103834,117 @@ var WarpRule = class extends NestedAgentsmdRule {
101886
103834
  }
101887
103835
  };
101888
103836
  //#endregion
103837
+ //#region src/features/rules/xum-rule.ts
103838
+ /** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
103839
+ const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
103840
+ /**
103841
+ * Make `body` start at a level-1 heading, adding `# <heading>` when it does not
103842
+ * already open with one.
103843
+ *
103844
+ * Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
103845
+ * document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
103846
+ * section that runs until the next heading of the same or a higher level.
103847
+ * Fragments rulesync concatenates into that document — folded topic rules and
103848
+ * the local overlay — would otherwise fall inside a scoped section the
103849
+ * preceding fragment ends with, and Xum would inject them only in that mode.
103850
+ * Only an H1 closes a section of any level, so each such fragment opens with
103851
+ * one.
103852
+ *
103853
+ * @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
103854
+ */
103855
+ function ensureXumSectionBoundary({ body, heading }) {
103856
+ const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
103857
+ if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
103858
+ const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
103859
+ return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
103860
+ }
103861
+ /** The heading a folded rule opens with: its file name without the extension. */
103862
+ function xumRuleHeading(rulesyncRule) {
103863
+ const fileName = (0, node_path.basename)(rulesyncRule.getRelativeFilePath());
103864
+ return fileName.slice(0, fileName.length - (0, node_path.extname)(fileName).length);
103865
+ }
103866
+ /**
103867
+ * Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
103868
+ *
103869
+ * Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
103870
+ * on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
103871
+ * `agentsmd` target already writes, so this target writes only the
103872
+ * Xum-dedicated file. Only these files honor the `## Model: <regex>` and
103873
+ * `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
103874
+ * so such sections authored in a rule keep working. The personal
103875
+ * `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
103876
+ *
103877
+ * @see https://xum.cdr.dev/agents/instruction-files
103878
+ */
103879
+ var XumRule = class XumRule extends ToolRule {
103880
+ constructor({ fileContent, root, ...rest }) {
103881
+ super({
103882
+ ...rest,
103883
+ fileContent,
103884
+ root: root ?? false
103885
+ });
103886
+ }
103887
+ static getSettablePaths(_options = {}) {
103888
+ return { root: {
103889
+ relativeDirPath: XUM_DIR,
103890
+ relativeFilePath: XUM_RULE_FILE_NAME
103891
+ } };
103892
+ }
103893
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
103894
+ const { root } = this.getSettablePaths({ global });
103895
+ const relativePath = (0, node_path.join)(root.relativeDirPath, root.relativeFilePath);
103896
+ const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
103897
+ return new XumRule({
103898
+ outputRoot,
103899
+ relativeDirPath: root.relativeDirPath,
103900
+ relativeFilePath: root.relativeFilePath,
103901
+ fileContent,
103902
+ validate,
103903
+ root: true
103904
+ });
103905
+ }
103906
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
103907
+ const { root } = this.getSettablePaths({ global });
103908
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
103909
+ return new XumRule({
103910
+ outputRoot,
103911
+ relativeDirPath: root.relativeDirPath,
103912
+ relativeFilePath: root.relativeFilePath,
103913
+ fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
103914
+ body: rulesyncRule.getBody(),
103915
+ heading: xumRuleHeading(rulesyncRule)
103916
+ }),
103917
+ validate,
103918
+ root: isRoot
103919
+ });
103920
+ }
103921
+ toRulesyncRule() {
103922
+ return this.toRulesyncRuleDefault();
103923
+ }
103924
+ validate() {
103925
+ return {
103926
+ success: true,
103927
+ error: null
103928
+ };
103929
+ }
103930
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
103931
+ return new XumRule({
103932
+ outputRoot,
103933
+ relativeDirPath,
103934
+ relativeFilePath,
103935
+ fileContent: "",
103936
+ validate: false,
103937
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
103938
+ });
103939
+ }
103940
+ static isTargetedByRulesyncRule(rulesyncRule) {
103941
+ return this.isTargetedByRulesyncRuleDefault({
103942
+ rulesyncRule,
103943
+ toolTarget: "xum"
103944
+ });
103945
+ }
103946
+ };
103947
+ //#endregion
101889
103948
  //#region src/features/rules/zcode-rule.ts
101890
103949
  var ZcodeRule = class ZcodeRule extends ToolRule {
101891
103950
  constructor({ fileContent, root, ...rest }) {
@@ -102374,6 +104433,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102374
104433
  collisionPolicy: "compose"
102375
104434
  }
102376
104435
  }],
104436
+ ["firebender", {
104437
+ class: FirebenderRule,
104438
+ meta: {
104439
+ extension: "mdc",
104440
+ supportsGlobal: true,
104441
+ ruleDiscoveryMode: "auto"
104442
+ }
104443
+ }],
102377
104444
  ["goose", {
102378
104445
  class: GooseRule,
102379
104446
  meta: {
@@ -102657,6 +104724,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102657
104724
  collisionPolicy: "fold"
102658
104725
  }
102659
104726
  }],
104727
+ ["xum", {
104728
+ class: XumRule,
104729
+ meta: {
104730
+ extension: "md",
104731
+ supportsGlobal: true,
104732
+ ruleDiscoveryMode: "auto",
104733
+ collisionPolicy: "fold",
104734
+ localRootMode: "separate-local-file",
104735
+ localRootFileName: XUM_LOCAL_RULE_FILE_NAME
104736
+ }
104737
+ }],
102660
104738
  ["devin", {
102661
104739
  class: DevinRule,
102662
104740
  meta: {
@@ -103427,18 +105505,39 @@ var RulesProcessor = class extends FeatureProcessor {
103427
105505
  root: true,
103428
105506
  localRoot
103429
105507
  });
103430
- if (isClassOrSubclassOf({
103431
- candidate: factory.class,
103432
- base: QwencodeRule
103433
- })) return new QwencodeRule({
103434
- outputRoot: this.outputRoot,
103435
- relativeDirPath: relativeDirPath ?? ".qwen",
103436
- relativeFilePath: fileName,
103437
- fileContent: body,
103438
- validate: true,
103439
- root: true,
105508
+ return this.buildToolDirLocalRootFile({
105509
+ factory,
105510
+ fileName,
105511
+ body,
105512
+ relativeDirPath,
103440
105513
  localRoot
103441
105514
  });
105515
+ }
105516
+ /**
105517
+ * Qwen Code and Xum read the personal local file from their tool directory
105518
+ * (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
105519
+ * (project scope only; global handling never reaches here).
105520
+ */
105521
+ buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
105522
+ const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
105523
+ candidate: factory.class,
105524
+ base
105525
+ }));
105526
+ if (toolDirEntry) {
105527
+ const [toolDirClass, toolDir] = toolDirEntry;
105528
+ return new toolDirClass({
105529
+ outputRoot: this.outputRoot,
105530
+ relativeDirPath: relativeDirPath ?? toolDir,
105531
+ relativeFilePath: fileName,
105532
+ fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
105533
+ body,
105534
+ heading: "Local instructions"
105535
+ }) : body,
105536
+ validate: true,
105537
+ root: true,
105538
+ localRoot
105539
+ });
105540
+ }
103442
105541
  return null;
103443
105542
  }
103444
105543
  /**
@@ -106190,6 +108289,7 @@ async function generateSubagentsCore(params) {
106190
108289
  toolTarget,
106191
108290
  global: config.getGlobal(),
106192
108291
  dryRun: config.isPreviewMode(),
108292
+ deleteOrphans: config.getDelete(),
106193
108293
  logger
106194
108294
  });
106195
108295
  const result = await processFeatureWithRulesyncFiles({