rulesync 38.0.0 → 40.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -522,6 +522,7 @@ const rulesProcessorToolTargetTuple = [
522
522
  "cursor",
523
523
  "deepagents",
524
524
  "factorydroid",
525
+ "firebender",
525
526
  "goose",
526
527
  "grokcli",
527
528
  "gitlabduo",
@@ -551,6 +552,7 @@ const rulesProcessorToolTargetTuple = [
551
552
  "trae",
552
553
  "vibe",
553
554
  "warp",
555
+ "xum",
554
556
  "devin",
555
557
  "devin-plugin",
556
558
  "zcode",
@@ -560,6 +562,7 @@ const rulesProcessorToolTargetTuple = [
560
562
  ];
561
563
  const ignoreProcessorToolTargetTuple = [
562
564
  "aiassistant",
565
+ "androidstudio",
563
566
  "antigravity-cli",
564
567
  "augmentcode",
565
568
  "bob",
@@ -570,6 +573,7 @@ const ignoreProcessorToolTargetTuple = [
570
573
  "continue",
571
574
  "crush",
572
575
  "cursor",
576
+ "firebender",
573
577
  "grokcli",
574
578
  "hermesagent",
575
579
  "junie",
@@ -617,6 +621,7 @@ const mcpProcessorToolTargetTuple = [
617
621
  "deepagents",
618
622
  "dsh",
619
623
  "factorydroid",
624
+ "firebender",
620
625
  "goose",
621
626
  "grokcli",
622
627
  "gitlabduo",
@@ -646,6 +651,7 @@ const mcpProcessorToolTargetTuple = [
646
651
  "vibe-plugin",
647
652
  "warp",
648
653
  "warpcli",
654
+ "xum",
649
655
  "devin",
650
656
  "devin-plugin",
651
657
  "zcode",
@@ -672,6 +678,7 @@ const commandsProcessorToolTargetTuple = [
672
678
  "copilot",
673
679
  "cursor",
674
680
  "factorydroid",
681
+ "firebender",
675
682
  "goose",
676
683
  "grokcli",
677
684
  "gitlabduo",
@@ -727,6 +734,7 @@ const subagentsProcessorToolTargetTuple = [
727
734
  "devin",
728
735
  "devin-plugin",
729
736
  "factorydroid",
737
+ "firebender",
730
738
  "goose",
731
739
  "grokcli",
732
740
  "hermesagent",
@@ -751,6 +759,7 @@ const subagentsProcessorToolTargetTuple = [
751
759
  "takt",
752
760
  "vibe",
753
761
  "vibe-plugin",
762
+ "xum",
754
763
  "zcode",
755
764
  "zcode-plugin"
756
765
  ];
@@ -758,6 +767,7 @@ const skillsProcessorToolTargetTuple = [
758
767
  "agentsmd",
759
768
  "agentsskills",
760
769
  "aiassistant",
770
+ "androidstudio",
761
771
  "amp",
762
772
  "antigravity-cli",
763
773
  "antigravity-ide",
@@ -781,6 +791,7 @@ const skillsProcessorToolTargetTuple = [
781
791
  "cursor",
782
792
  "deepagents",
783
793
  "factorydroid",
794
+ "firebender",
784
795
  "goose",
785
796
  "grokcli",
786
797
  "gitlabduo",
@@ -810,6 +821,7 @@ const skillsProcessorToolTargetTuple = [
810
821
  "vibe",
811
822
  "vibe-plugin",
812
823
  "warp",
824
+ "xum",
813
825
  "devin",
814
826
  "devin-plugin",
815
827
  "zcode",
@@ -844,6 +856,7 @@ const hooksProcessorToolTargetTuple = [
844
856
  "pi",
845
857
  "pool",
846
858
  "factorydroid",
859
+ "firebender",
847
860
  "goose",
848
861
  "hermesagent",
849
862
  "kimi-code",
@@ -1035,6 +1048,57 @@ async function assertWritablePathInsideRoot(params) {
1035
1048
  existingPath = parentPath;
1036
1049
  }
1037
1050
  }
1051
+ /**
1052
+ * Thrown by {@link assertPathResolvesInsideRoot} when a symbolic link leads
1053
+ * the target outside the root, or the target is reached through a dangling
1054
+ * link. Any other error it throws comes from the filesystem itself.
1055
+ */
1056
+ var PathOutsideRootError = class extends Error {
1057
+ constructor(message, options) {
1058
+ super(message, options);
1059
+ this.name = "PathOutsideRootError";
1060
+ }
1061
+ };
1062
+ /**
1063
+ * Refuse a write or removal target whose real location is outside `rootPath`.
1064
+ *
1065
+ * Unlike {@link assertWritablePathInsideRoot}, a symbolic link on the way is
1066
+ * allowed as long as it resolves inside the root: projects deliberately link a
1067
+ * skill directory to a shared one next to it. What this stops is a link that
1068
+ * leads elsewhere — a cloned repository committing `.rulesync/skills/x ->
1069
+ * ~/.ssh`, say — which a textual check such as {@link checkPathTraversal}
1070
+ * cannot see. The deepest existing ancestor of `targetPath` is resolved, and
1071
+ * an existing target that is itself a link is resolved too.
1072
+ */
1073
+ async function assertPathResolvesInsideRoot(params) {
1074
+ const { rootPath, targetPath } = params;
1075
+ let realRoot;
1076
+ try {
1077
+ realRoot = await realpath(rootPath);
1078
+ } catch (error) {
1079
+ if (isFileNotFoundError(error)) {
1080
+ if (await isSymlink(rootPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${rootPath}.`, { cause: error });
1081
+ return;
1082
+ }
1083
+ throw error;
1084
+ }
1085
+ let existingPath = resolve(targetPath);
1086
+ while (true) {
1087
+ let realExisting;
1088
+ try {
1089
+ realExisting = await realpath(existingPath);
1090
+ } catch (error) {
1091
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
1092
+ if (await isSymlink(existingPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${targetPath}.`, { cause: error });
1093
+ const parentPath = dirname(existingPath);
1094
+ if (parentPath === existingPath) throw error;
1095
+ existingPath = parentPath;
1096
+ continue;
1097
+ }
1098
+ if (pathEscapesRoot(relative(realRoot, realExisting))) throw new PathOutsideRootError(`Path resolves outside ${rootPath} through a symbolic link: ${targetPath}.`);
1099
+ return;
1100
+ }
1101
+ }
1038
1102
  async function assertTreeContainsNoSymlinks(dirPath) {
1039
1103
  for (const entry of await readdir(dirPath, { withFileTypes: true })) {
1040
1104
  const entryPath = join(dirPath, entry.name);
@@ -3758,6 +3822,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
3758
3822
  ".devin/config.json",
3759
3823
  ".dsh/cordis.patch.yml",
3760
3824
  ".factory/settings.json",
3825
+ "firebender.json",
3826
+ ".firebender/firebender.json",
3761
3827
  ".grok/config.toml",
3762
3828
  ".grok/sandbox.toml",
3763
3829
  ".letta/settings.json",
@@ -4219,14 +4285,103 @@ function stringifyFrontmatter(body, frontmatter, options) {
4219
4285
  } } });
4220
4286
  return matter.stringify(file, cleanFrontmatter);
4221
4287
  }
4288
+ /**
4289
+ * Thrown in place of gray-matter's built-in JavaScript engine.
4290
+ *
4291
+ * gray-matter picks the parser from the text after the opening fence, so a
4292
+ * file that opens with `---js` or `---javascript` has its frontmatter handed
4293
+ * to `eval()`. Frontmatter here comes from third-party skills, fetched sources
4294
+ * and untrusted repositories, so that would run arbitrary code on the machine
4295
+ * of whoever generates or imports it. Every engine name that resolves to code
4296
+ * execution is mapped to this refusal; {@link assertYamlFrontmatterLanguage}
4297
+ * refuses those fences earlier still, so this is the second line of defense.
4298
+ */
4299
+ function refuseExecutableFrontmatter() {
4300
+ throw new Error("Frontmatter written in an executable language (JavaScript / CoffeeScript) is not supported; use YAML");
4301
+ }
4302
+ const REFUSED_FRONTMATTER_ENGINE = {
4303
+ parse: refuseExecutableFrontmatter,
4304
+ stringify: refuseExecutableFrontmatter
4305
+ };
4306
+ /**
4307
+ * Engines passed to every gray-matter parse. Supplying them merges over
4308
+ * gray-matter's defaults, so the JavaScript engine (and the CoffeeScript names
4309
+ * a plugin could register) can never be reached through an alias.
4310
+ *
4311
+ * `patches/gray-matter@4.0.3.patch` also deletes the JavaScript engine, but a
4312
+ * pnpm patch only applies inside this repository: gray-matter is an external
4313
+ * dependency of the published package, so an installed rulesync runs the
4314
+ * unpatched gray-matter, and this override plus the language check are what
4315
+ * protect it. Exported for the test that pins that.
4316
+ */
4317
+ const FRONTMATTER_PARSE_OPTIONS = { engines: {
4318
+ javascript: REFUSED_FRONTMATTER_ENGINE,
4319
+ js: REFUSED_FRONTMATTER_ENGINE,
4320
+ coffee: REFUSED_FRONTMATTER_ENGINE,
4321
+ coffeescript: REFUSED_FRONTMATTER_ENGINE,
4322
+ cson: REFUSED_FRONTMATTER_ENGINE
4323
+ } };
4324
+ /** Frontmatter languages accepted after the opening `---` fence. */
4325
+ const ALLOWED_FRONTMATTER_LANGUAGE_PATTERN = /^(?:ya?ml)?$/i;
4326
+ /**
4327
+ * Read the opening frontmatter fence the way gray-matter does, or return
4328
+ * `undefined` when gray-matter would see no frontmatter at all.
4329
+ *
4330
+ * gray-matter strips one byte order mark, treats `---` as a fence only when a
4331
+ * fourth `-` does not follow, and takes the language from the text between the
4332
+ * fence and the first `\r?\n`, trimmed — dropping the last character when
4333
+ * there is no line break at all. Every check below that has to agree with
4334
+ * gray-matter on where the block is (the language check, the size guard, the
4335
+ * repair pass) goes through this one reading, so none of them can be bypassed
4336
+ * by a fence line gray-matter accepts and a stricter pattern would not (a
4337
+ * stray `\r` before the line break, say).
4338
+ */
4339
+ function readFrontmatterFence(content) {
4340
+ const offset = content.startsWith("") ? 1 : 0;
4341
+ const text = content.slice(offset);
4342
+ if (!text.startsWith("---") || text.charAt(3) === "-") return;
4343
+ const afterFence = text.slice(3);
4344
+ const lineBreak = /\r?\n/.exec(afterFence);
4345
+ const language = afterFence.slice(0, lineBreak ? lineBreak.index : -1).trim();
4346
+ const lineBreakStart = lineBreak ? offset + 3 + lineBreak.index : void 0;
4347
+ return {
4348
+ language,
4349
+ lineBreakStart,
4350
+ lineEnd: lineBreak && lineBreakStart !== void 0 ? lineBreakStart + lineBreak[0].length : void 0
4351
+ };
4352
+ }
4353
+ /**
4354
+ * Refuse a frontmatter fence that names any language other than YAML.
4355
+ *
4356
+ * Only YAML is ever written by rulesync or by the tools it targets, so anything
4357
+ * else — `---js` above all, whose block gray-matter would `eval()` — is
4358
+ * rejected before gray-matter sees the content.
4359
+ */
4360
+ function assertYamlFrontmatterLanguage(content) {
4361
+ const fence = readFrontmatterFence(content);
4362
+ 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`);
4363
+ }
4364
+ /**
4365
+ * The number of characters gray-matter would hand to the YAML parser as the
4366
+ * frontmatter block. Unlike {@link findFrontmatterBlockBounds}, a missing
4367
+ * closing fence does not make this zero: gray-matter then parses everything
4368
+ * after the fence line, so that is what the size guard has to measure.
4369
+ */
4370
+ function measureFrontmatterBlockChars(content) {
4371
+ const fence = readFrontmatterFence(content);
4372
+ if (fence?.lineBreakStart === void 0) return 0;
4373
+ const rest = content.slice(fence.lineBreakStart);
4374
+ const closeIndex = rest.indexOf("\n---");
4375
+ return closeIndex === -1 ? rest.length : closeIndex;
4376
+ }
4222
4377
  function parseFrontmatter(content, filePath) {
4223
4378
  let frontmatter;
4224
4379
  let body;
4225
4380
  let hasFrontmatter;
4226
4381
  try {
4227
- const bounds = findFrontmatterBlockBounds(content);
4228
- if (bounds && bounds.blockEnd - bounds.blockStart > 65536) throw new Error(`Frontmatter block is larger than ${MAX_FRONTMATTER_RAW_CHARS} characters; refusing to parse it (a complex YAML key can cost memory while parsing, before any post-parse budget applies)`);
4229
- const result = matter(content, {});
4382
+ assertYamlFrontmatterLanguage(content);
4383
+ 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)`);
4384
+ const result = matter(content, FRONTMATTER_PARSE_OPTIONS);
4230
4385
  frontmatter = deepRemoveNullishObject(result.data);
4231
4386
  body = result.content;
4232
4387
  hasFrontmatter = result.matter !== "" || content.trimStart().startsWith("---");
@@ -4285,22 +4440,28 @@ function repairFrontmatterLine(line) {
4285
4440
  }
4286
4441
  /**
4287
4442
  * Locate a raw `---`-delimited frontmatter block's bounds within `content`,
4288
- * without parsing it. Shared by the size guard in {@link parseFrontmatter} and
4289
- * the YAML repair pass below, so both agree on exactly what gray-matter would
4290
- * treat as the block: gray-matter ends it at the first `\n---`, with no
4291
- * requirement that the delimiter be alone on its line, so a stricter pattern
4292
- * here would run past gray-matter's delimiter and act on text that is really
4293
- * the body.
4443
+ * without parsing it, for the YAML repair pass below and the rule loaders that
4444
+ * pre-process `globs:`. The opening fence is read exactly as gray-matter reads
4445
+ * it ({@link readFrontmatterFence}); an explicit `yaml` / `yml` language is
4446
+ * still YAML, any other language yields no bounds. gray-matter ends the block
4447
+ * at the first `\n---`, with no requirement that the delimiter be alone on its
4448
+ * line, so a stricter pattern here would run past gray-matter's delimiter and
4449
+ * act on text that is really the body. A block with no closing fence has no
4450
+ * bounds; the size guard measures that case separately.
4294
4451
  */
4295
4452
  function findFrontmatterBlockBounds(content) {
4296
- const opening = /^\uFEFF?---[^\S\r\n]*\r?\n/.exec(content);
4297
- if (!opening) return;
4298
- const blockStart = opening[0].length;
4299
- const closing = /\r?\n---/.exec(content.slice(blockStart));
4453
+ const fence = readFrontmatterFence(content);
4454
+ if (fence?.lineBreakStart === void 0 || fence.lineEnd === void 0 || !ALLOWED_FRONTMATTER_LANGUAGE_PATTERN.test(fence.language)) return;
4455
+ const blockStart = fence.lineEnd;
4456
+ const closing = /\r?\n---/.exec(content.slice(fence.lineBreakStart));
4300
4457
  if (!closing) return;
4458
+ if (closing.index === 0) return {
4459
+ blockStart,
4460
+ blockEnd: blockStart
4461
+ };
4301
4462
  return {
4302
4463
  blockStart,
4303
- blockEnd: blockStart + closing.index
4464
+ blockEnd: fence.lineBreakStart + closing.index
4304
4465
  };
4305
4466
  }
4306
4467
  /**
@@ -4332,8 +4493,9 @@ function repairMalformedFrontmatterYaml(content) {
4332
4493
  * narrow — one pass, top-level entries only, and the original error is what
4333
4494
  * surfaces if it does not help, so a genuinely broken file still fails with the
4334
4495
  * message that describes what is actually wrong with it. A file with no closing
4335
- * `---`, or one whose opening fence carries a language tag, is not repaired at
4336
- * all: neither is a frontmatter block gray-matter would have read.
4496
+ * `---` is not repaired at all, and one whose opening fence names a language
4497
+ * other than YAML is refused outright (an explicit `---yaml` / `---yml` fence
4498
+ * is repaired like a plain one).
4337
4499
  *
4338
4500
  * @see https://agentskills.io/client-implementation/adding-skills-support
4339
4501
  */
@@ -4698,6 +4860,29 @@ const CURSOR_HOOK_EVENTS = [
4698
4860
  "workspaceOpen"
4699
4861
  ];
4700
4862
  /**
4863
+ * Hook events supported by Firebender.
4864
+ *
4865
+ * Firebender's `hooks.json` follows Cursor's layout, and every event it fires
4866
+ * is spelled like its canonical counterpart, so no name mapping is needed.
4867
+ * @see https://docs.firebender.com/multi-agent/hooks
4868
+ */
4869
+ const FIREBENDER_HOOK_EVENTS = [
4870
+ "sessionStart",
4871
+ "sessionEnd",
4872
+ "subagentStart",
4873
+ "subagentStop",
4874
+ "preToolUse",
4875
+ "postToolUse",
4876
+ "beforeShellExecution",
4877
+ "afterShellExecution",
4878
+ "beforeMCPExecution",
4879
+ "afterMCPExecution",
4880
+ "beforeReadFile",
4881
+ "afterFileEdit",
4882
+ "preCompact",
4883
+ "stop"
4884
+ ];
4885
+ /**
4701
4886
  * Hook events supported by Claude Code.
4702
4887
  *
4703
4888
  * Covers the full documented event surface.
@@ -4978,6 +5163,40 @@ const COPILOT_HOOK_EVENTS = [
4978
5163
  "userPromptExpansion"
4979
5164
  ];
4980
5165
  /**
5166
+ * The {@link COPILOT_HOOK_EVENTS} whose hook entries honor an optional
5167
+ * `matcher` regex. The hooks reference documents matcher filtering on
5168
+ * `preToolUse` / `postToolUse` / `permissionRequest` (tool name), `preCompact`
5169
+ * (trigger) and `subagentStart` (agent name), plus `notification`; the last
5170
+ * two are not authorable for `copilot`, so they are not listed here. VS Code's
5171
+ * migration guide confirms the Copilot harness "applies documented matcher
5172
+ * rules" (only the legacy Local harness ignores them).
5173
+ *
5174
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
5175
+ * @see https://code.visualstudio.com/docs/agent-customization/migrate-customizations
5176
+ */
5177
+ const COPILOT_MATCHER_HOOK_EVENTS = [
5178
+ "preToolUse",
5179
+ "postToolUse",
5180
+ "preCompact",
5181
+ "subagentStart"
5182
+ ];
5183
+ /**
5184
+ * The {@link COPILOTCLI_HOOK_EVENTS} whose hook entries honor an optional
5185
+ * `matcher` regex: the shared {@link COPILOT_MATCHER_HOOK_EVENTS} plus the two
5186
+ * CLI-only events, `permissionRequest` (tool name) and `notification`
5187
+ * (notification type).
5188
+ *
5189
+ * Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
5190
+ * honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
5191
+ * `Edit|Write`) are now honored instead of silently dropped").
5192
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
5193
+ */
5194
+ const COPILOTCLI_MATCHER_HOOK_EVENTS = [
5195
+ ...COPILOT_MATCHER_HOOK_EVENTS,
5196
+ "permissionRequest",
5197
+ "notification"
5198
+ ];
5199
+ /**
4981
5200
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
4982
5201
  *
4983
5202
  * The CLI documents a wider event surface than the shared cloud-agent set, so
@@ -5131,12 +5350,15 @@ const KIRO_IDE_HOOK_EVENTS = [
5131
5350
  /**
5132
5351
  * Hook events supported by Google Antigravity (both the IDE and the CLI).
5133
5352
  *
5134
- * Antigravity exposes a Claude-style hooks surface covering the five
5135
- * tool/model/turn lifecycle events it documents: `PreToolUse`, `PostToolUse`,
5136
- * `PreInvocation`, `PostInvocation`, and `Stop`. The model-invocation events
5137
- * (`PreInvocation`/`PostInvocation`) and `Stop` are matcher-less handler lists.
5353
+ * Antigravity exposes a Claude-style hooks surface covering the six
5354
+ * session/tool/model/turn lifecycle events it documents: `SessionStart`,
5355
+ * `PreToolUse`, `PostToolUse`, `PreInvocation`, `PostInvocation`, and `Stop`.
5356
+ * `SessionStart`, the model-invocation events (`PreInvocation`/`PostInvocation`)
5357
+ * and `Stop` are matcher-less handler lists.
5358
+ * https://antigravity.google/docs/hooks
5138
5359
  */
5139
5360
  const ANTIGRAVITY_HOOK_EVENTS = [
5361
+ "sessionStart",
5140
5362
  "preToolUse",
5141
5363
  "postToolUse",
5142
5364
  "preModelInvocation",
@@ -5909,8 +6131,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
5909
6131
  /**
5910
6132
  * Hook events supported by Hermes Agent's native Shell Hooks system.
5911
6133
  *
5912
- * Hermes validates hook events against a fixed `VALID_HOOKS` set — 44 entries as
5913
- * of v0.21.6 (tag `v0.21.6`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
6134
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 46 entries as
6135
+ * of v0.22.3 (tag `v0.22.3`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
5914
6136
  * full list. Only the events with a clean 1:1 canonical equivalent are mapped
5915
6137
  * here. All other native events round-trip through `hermesagent.hooks`.
5916
6138
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
@@ -5934,15 +6156,16 @@ const HERMESAGENT_HOOK_EVENTS = [
5934
6156
  * observers, `gateway_platform_event` and `transform_api_error_classification`.
5935
6157
  * Shell hooks are gated on the same set.
5936
6158
  *
5937
- * Verified against the tag `v0.21.6`: `VALID_HOOKS` holds 44 entries, grown
6159
+ * Verified against the tag `v0.22.3`: `VALID_HOOKS` holds 46 entries, grown
5938
6160
  * from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16` (v0.20.2) — v0.21
5939
6161
  * added `agent_loop_stopped` and `on_room_member_activity`, v0.21.5 added
5940
- * `pre_auxiliary_call` and `post_auxiliary_call`, and v0.21.6 added
6162
+ * `pre_auxiliary_call` and `post_auxiliary_call`, v0.21.6 added
5941
6163
  * `post_gateway_admission`, `on_human_input_request` and
5942
- * `on_human_input_resolved`. This list holds 43 of them — see
6164
+ * `on_human_input_resolved`, and v0.22.3 added `on_session_delete` and
6165
+ * `post_compaction`. This list holds 45 of them — see
5943
6166
  * {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
5944
6167
  *
5945
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/hermes_cli/plugins.py
6168
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py
5946
6169
  */
5947
6170
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5948
6171
  "pre_tool_call",
@@ -5966,6 +6189,7 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5966
6189
  "on_session_end",
5967
6190
  "on_session_finalize",
5968
6191
  "on_session_reset",
6192
+ "on_session_delete",
5969
6193
  "on_skill_lifecycle",
5970
6194
  "subagent_start",
5971
6195
  "subagent_stop",
@@ -5987,7 +6211,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5987
6211
  "on_kanban_task_updated",
5988
6212
  "on_kanban_dispatch_tick",
5989
6213
  "gateway_platform_event",
5990
- "pre_command"
6214
+ "pre_command",
6215
+ "post_compaction"
5991
6216
  ];
5992
6217
  /**
5993
6218
  * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
@@ -5999,8 +6224,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
5999
6224
  * rulesync only ever writes shell hooks, so these are excluded from
6000
6225
  * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
6001
6226
  *
6002
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
6003
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/agent/shell_hooks.py — `_parse_hooks_block`
6227
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
6228
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/agent/shell_hooks.py — `_parse_hooks_block`
6004
6229
  */
6005
6230
  const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
6006
6231
  /**
@@ -6045,6 +6270,7 @@ const HooksConfigSchema = z.looseObject({
6045
6270
  version: z.optional(z.number()),
6046
6271
  hooks: canonicalHooksRecordSchema,
6047
6272
  cursor: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6273
+ firebender: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6048
6274
  claudecode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6049
6275
  copilot: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6050
6276
  copilotcli: z.optional(z.looseObject({
@@ -6181,10 +6407,11 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6181
6407
  const AUGMENTCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
6182
6408
  /**
6183
6409
  * Map canonical camelCase event names to Antigravity PascalCase.
6184
- * Antigravity uses the same PascalCase names as Claude for its tool/turn events,
6410
+ * Antigravity uses the same PascalCase names as Claude for its session/tool/turn events,
6185
6411
  * plus `PreInvocation`/`PostInvocation` for the model-invocation lifecycle.
6186
6412
  */
6187
6413
  const CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES = {
6414
+ sessionStart: "SessionStart",
6188
6415
  preToolUse: "PreToolUse",
6189
6416
  postToolUse: "PostToolUse",
6190
6417
  preModelInvocation: "PreInvocation",
@@ -7148,7 +7375,7 @@ const McpServerSchema = z.looseObject({
7148
7375
  crushSessionless: z.optional(z.boolean()),
7149
7376
  crushChannelEnabled: z.optional(z.boolean()),
7150
7377
  crushChannelReply: z.optional(CrushChannelReplySchema),
7151
- zcodeOauth: z.optional(z.looseObject({ type: z.string() })),
7378
+ zcodeOauth: z.optional(z.looseObject({})),
7152
7379
  poolAllow: z.optional(z.array(z.string())),
7153
7380
  headers: z.optional(z.record(z.string(), z.string())),
7154
7381
  /**
@@ -7203,14 +7430,19 @@ const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.st
7203
7430
  /**
7204
7431
  * Kimi Code's tool-scoped block also carries the `[mcp]` defaults from its
7205
7432
  * shared user config: connect and per-tool-call timeouts that apply to *every*
7206
- * MCP server, including ones rulesync did not write. Per-server
7207
- * `startupTimeoutMs` / `toolTimeoutMs` in `mcpServers` still win.
7433
+ * MCP server, including ones rulesync did not write, and (Kimi Code 2.3.0)
7434
+ * `defaultDeferred`, which loads every server's tools on demand unless the
7435
+ * server opts out. Per-server `startupTimeoutMs` / `toolTimeoutMs` / `deferred`
7436
+ * in `mcpServers` still win. `defaultDeferred` only takes effect when Kimi's
7437
+ * experimental `tool-select` flag is on, and is captured when a server connects
7438
+ * (MoonshotAI/kimi-code#4210).
7208
7439
  *
7209
7440
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
7210
7441
  */
7211
7442
  const kimiCodeScopedMcpSchema = z.extend(toolScopedMcpSchema, {
7212
7443
  startupTimeoutMs: z.optional(z.number()),
7213
- toolTimeoutMs: z.optional(z.number())
7444
+ toolTimeoutMs: z.optional(z.number()),
7445
+ defaultDeferred: z.optional(z.boolean())
7214
7446
  });
7215
7447
  /**
7216
7448
  * Qwen Code's tool-scoped block also carries the server-level allow/deny lists
@@ -7250,6 +7482,7 @@ const RulesyncMcpFileSchema = z.looseObject({
7250
7482
  devin: z.optional(toolScopedMcpSchema),
7251
7483
  dsh: z.optional(toolScopedMcpSchema),
7252
7484
  factorydroid: z.optional(toolScopedMcpSchema),
7485
+ firebender: z.optional(toolScopedMcpSchema),
7253
7486
  gitlabduo: z.optional(toolScopedMcpSchema),
7254
7487
  goose: z.optional(toolScopedMcpSchema),
7255
7488
  grokcli: z.optional(toolScopedMcpSchema),
@@ -7274,6 +7507,7 @@ const RulesyncMcpFileSchema = z.looseObject({
7274
7507
  vibe: z.optional(toolScopedMcpSchema),
7275
7508
  warp: z.optional(toolScopedMcpSchema),
7276
7509
  warpcli: z.optional(toolScopedMcpSchema),
7510
+ xum: z.optional(toolScopedMcpSchema),
7277
7511
  zed: z.optional(toolScopedMcpSchema)
7278
7512
  });
7279
7513
  /**
@@ -7977,13 +8211,16 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
7977
8211
  });
7978
8212
  /**
7979
8213
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
7980
- * before prompting. `always-ask` auto-approves only read-only tools, `write`
7981
- * also workspace writes, and `yolo` (oh-my-pi's default) every tier.
8214
+ * before prompting. `always-ask` auto-approves only read-only tools,
8215
+ * `always-ask-cached` (Ask and remember) also prompts before file reads and
8216
+ * lets a prompt remember the granted file or folder, `write` also
8217
+ * auto-approves workspace writes, and `yolo` (oh-my-pi's default) every tier.
7982
8218
  *
7983
8219
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
7984
8220
  */
7985
8221
  const OMP_APPROVAL_MODES = [
7986
8222
  "always-ask",
8223
+ "always-ask-cached",
7987
8224
  "write",
7988
8225
  "yolo"
7989
8226
  ];
@@ -8004,10 +8241,13 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
8004
8241
  ])),
8005
8242
  /**
8006
8243
  * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
8007
- * default `true`). `false` turns off the built-in dangerous-command policy
8008
- * (no confirmation for `rm -rf`, `shutdown` and the like).
8244
+ * default `true`). Since Kimi Code 2.3.0, `false` turns off every built-in
8245
+ * guard confirmation policy: dangerous commands (`rm -rf`, `shutdown` and the
8246
+ * like), sensitive-file access (`.env`, `id_rsa`, ...) and git control-path
8247
+ * access. Before 2.3.0 it only covered dangerous commands.
8009
8248
  *
8010
8249
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
8250
+ * @see https://github.com/MoonshotAI/kimi-code/pull/4250
8011
8251
  */
8012
8252
  dangerousCommandGuard: z.optional(z.boolean()),
8013
8253
  rules: z.optional(z.array(z.looseObject({
@@ -8367,6 +8607,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
8367
8607
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8368
8608
  * @example
8369
8609
  * { "mode": "deny" }
8610
+ * @example
8611
+ * { "tools": { "disabled": ["web_fetch"] }, "askOnExternalOrigin": true }
8370
8612
  */
8371
8613
  const ReasonixPermissionsOverrideSchema = z.looseObject({
8372
8614
  permission: z.optional(ToolScopedPermissionSchema),
@@ -8380,7 +8622,9 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
8380
8622
  "ask",
8381
8623
  "allow",
8382
8624
  "deny"
8383
- ]))
8625
+ ])),
8626
+ askOnExternalOrigin: z.optional(z.boolean()),
8627
+ tools: z.optional(z.strictObject({ disabled: z.optional(z.array(z.string())) }))
8384
8628
  });
8385
8629
  /**
8386
8630
  * Tool-scoped override block for Qoder. `outputStyle` is written to the
@@ -11194,6 +11438,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
11194
11438
  compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
11195
11439
  metadata: z.optional(z.looseObject({}))
11196
11440
  })),
11441
+ xum: z.optional(z.looseObject({
11442
+ "disable-model-invocation": z.optional(z.boolean()),
11443
+ advertise: z.optional(z.boolean()),
11444
+ "user-invocable": z.optional(z.boolean()),
11445
+ "argument-hint": z.optional(z.string()),
11446
+ when_to_use: z.optional(z.string()),
11447
+ "when-to-use": z.optional(z.string()),
11448
+ paths: z.optional(z.union([z.string(), z.array(z.string())]))
11449
+ })),
11197
11450
  zcode: z.optional(z.looseObject({
11198
11451
  when_to_use: z.optional(z.string()),
11199
11452
  license: z.optional(z.string()),
@@ -11472,7 +11725,7 @@ async function getLocalSkillDirNames(sourceTree) {
11472
11725
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11473
11726
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11474
11727
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11475
- * commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
11728
+ * commandcode, lettacode, opencode, mimocode, xum). Each tool's own section may override that
11476
11729
  * default with a per-target value. A defined section value (including `false`)
11477
11730
  * always wins over the root default.
11478
11731
  *
@@ -11492,7 +11745,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
11492
11745
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
11493
11746
  * that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11494
11747
  * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh, reasonix, commandcode,
11495
- * lettacode). Each tool's own section may override that default with a
11748
+ * lettacode, xum). Each tool's own section may override that default with a
11496
11749
  * per-target value. A defined section value (including `false`) always wins
11497
11750
  * over the root default.
11498
11751
  *
@@ -13069,6 +13322,35 @@ const CRUSH_CONFIG_DECLARATION = {
13069
13322
  }
13070
13323
  };
13071
13324
  /**
13325
+ * Firebender's `firebender.json`, shared by the project (`<project>/firebender.json`)
13326
+ * and personal (`~/.firebender/firebender.json`) spellings so a policy edit
13327
+ * lands on both. The file also carries `rules`, `commands`, `mcpEnvFile`,
13328
+ * `worktrees` and `useCursorRules` keys rulesync does not own, so every writer
13329
+ * refuses to read-modify-write a file it could not parse. Each feature owns
13330
+ * one whole top-level key: `ignore` (recomputed from `.rulesync/.aiignore`),
13331
+ * `mcpServers` (the complete server map) and `agents` (recomputed from the
13332
+ * existing list, with the entries for rulesync's own agent directory replaced).
13333
+ * https://docs.firebender.com/api-reference/syntax
13334
+ */
13335
+ const FIREBENDER_CONFIG_DECLARATION = {
13336
+ format: "json",
13337
+ invalidRootPolicy: "error",
13338
+ features: {
13339
+ ignore: {
13340
+ kind: "replace-owned-keys",
13341
+ ownedKeys: ["ignore"]
13342
+ },
13343
+ subagents: {
13344
+ kind: "replace-owned-keys",
13345
+ ownedKeys: ["agents"]
13346
+ },
13347
+ mcp: {
13348
+ kind: "replace-owned-keys",
13349
+ ownedKeys: ["mcpServers"]
13350
+ }
13351
+ }
13352
+ };
13353
+ /**
13072
13354
  * What the two Claude Code settings files have in common: both are plain JSON,
13073
13355
  * and both validate against the one published schema, which the gateway
13074
13356
  * points every file it writes at. `.claude/settings.local.json` is the same
@@ -13308,6 +13590,15 @@ const SHARED_CONFIG_OWNERSHIP = {
13308
13590
  ownedKeys: ["servers"]
13309
13591
  } }
13310
13592
  },
13593
+ ".xum/mcp.jsonc": {
13594
+ format: "jsonc",
13595
+ invalidRootPolicy: "error",
13596
+ jsoncParseErrors: "error",
13597
+ features: { mcp: {
13598
+ kind: "replace-owned-keys",
13599
+ ownedKeys: ["servers"]
13600
+ } }
13601
+ },
13311
13602
  ".qwen/settings.json": {
13312
13603
  format: "json",
13313
13604
  features: {
@@ -13395,6 +13686,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13395
13686
  ".zcode/cli/config.json": ZCODE_CONFIG_DECLARATION,
13396
13687
  "crush.json": CRUSH_CONFIG_DECLARATION,
13397
13688
  ".config/crush/crush.json": CRUSH_CONFIG_DECLARATION,
13689
+ "firebender.json": FIREBENDER_CONFIG_DECLARATION,
13690
+ ".firebender/firebender.json": FIREBENDER_CONFIG_DECLARATION,
13398
13691
  ".bob/settings.json": {
13399
13692
  format: "json",
13400
13693
  invalidRootPolicy: "error",
@@ -13767,7 +14060,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13767
14060
  ownedKeys: [
13768
14061
  "permissions",
13769
14062
  "sandbox",
13770
- "agent"
14063
+ "agent",
14064
+ "tools"
13771
14065
  ]
13772
14066
  }
13773
14067
  }
@@ -13788,7 +14082,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13788
14082
  ownedKeys: [
13789
14083
  "permissions",
13790
14084
  "sandbox",
13791
- "agent"
14085
+ "agent",
14086
+ "tools"
13792
14087
  ]
13793
14088
  }
13794
14089
  }
@@ -20229,6 +20524,178 @@ var FactorydroidCommand = class FactorydroidCommand extends ToolCommand {
20229
20524
  }
20230
20525
  };
20231
20526
  //#endregion
20527
+ //#region src/constants/firebender-paths.ts
20528
+ /**
20529
+ * Firebender (the coding agent plugin for Android Studio and other JetBrains
20530
+ * IDEs) keeps project configuration under `.firebender/` plus a project-root
20531
+ * `firebender.json`, and user configuration under `~/.firebender/` (including
20532
+ * `~/.firebender/firebender.json`).
20533
+ *
20534
+ * @see https://docs.firebender.com/api-reference/syntax
20535
+ * @see https://docs.firebender.com/api-reference/rules
20536
+ * @see https://docs.firebender.com/api-reference/commands
20537
+ * @see https://docs.firebender.com/api-reference/agents
20538
+ * @see https://docs.firebender.com/api-reference/skills
20539
+ * @see https://docs.firebender.com/api-reference/agents-md
20540
+ * @see https://docs.firebender.com/multi-agent/hooks
20541
+ */
20542
+ const FIREBENDER_DIR = ".firebender";
20543
+ /**
20544
+ * `firebender.json`: at the project root for project scope, and under
20545
+ * `~/.firebender/` for the personal config. It is shared by the ignore
20546
+ * (`ignore`), MCP (`mcpServers`) and subagents (`agents`) features.
20547
+ */
20548
+ const FIREBENDER_CONFIG_FILE_NAME = "firebender.json";
20549
+ /** `.firebender/AGENTS.md`, the first file Firebender looks for in both scopes. */
20550
+ const FIREBENDER_AGENTS_MD_FILE_NAME = "AGENTS.md";
20551
+ join(FIREBENDER_DIR, "rules");
20552
+ /** Commands: `.firebender/commands/*.mdc` (project) and `~/.firebender/commands/*.mdc` (global). */
20553
+ const FIREBENDER_COMMANDS_DIR_PATH = join(FIREBENDER_DIR, "commands");
20554
+ /** Agents: Markdown files registered through the `agents` array of `firebender.json`. */
20555
+ const FIREBENDER_AGENTS_DIR_NAME = "agents";
20556
+ const FIREBENDER_AGENTS_DIR_PATH = join(FIREBENDER_DIR, FIREBENDER_AGENTS_DIR_NAME);
20557
+ /** Skills: `.firebender/skills/<name>/SKILL.md` (project) and `~/.firebender/skills/` (global). */
20558
+ const FIREBENDER_SKILLS_DIR_PATH = join(FIREBENDER_DIR, "skills");
20559
+ /** Hooks: `.firebender/hooks.json` (project) and `~/.firebender/hooks.json` (global). */
20560
+ const FIREBENDER_HOOKS_FILE_NAME = "hooks.json";
20561
+ /**
20562
+ * The directory holding `firebender.json` for a scope: the project root, or
20563
+ * `~/.firebender/` for the personal config.
20564
+ */
20565
+ function getFirebenderConfigDir({ global }) {
20566
+ return global ? FIREBENDER_DIR : ".";
20567
+ }
20568
+ //#endregion
20569
+ //#region src/features/commands/firebender-command.ts
20570
+ /**
20571
+ * Frontmatter of a Firebender command. Every field is optional: `name`
20572
+ * defaults to the file name, `model` is `default`, `quick` or a model ID, and
20573
+ * `mode` is one of `auto`, `read`, `write` or `composer`. Unknown keys are
20574
+ * preserved.
20575
+ *
20576
+ * @see https://docs.firebender.com/api-reference/commands
20577
+ */
20578
+ const FirebenderCommandFrontmatterSchema = z.looseObject({
20579
+ name: z.optional(z.string()),
20580
+ description: z.optional(z.string()),
20581
+ model: z.optional(z.string()),
20582
+ mode: z.optional(z.string())
20583
+ });
20584
+ const FIREBENDER_COMMAND_EXTENSION = ".mdc";
20585
+ /**
20586
+ * Command generator for Firebender: one `.mdc` file per command under
20587
+ * `.firebender/commands/` (project) and `~/.firebender/commands/` (global),
20588
+ * with optional `name` / `description` / `model` / `mode` frontmatter. The
20589
+ * canonical `description` is written as is; `name`, `model` and `mode` come
20590
+ * from the `firebender:` section of the rulesync command.
20591
+ *
20592
+ * @see https://docs.firebender.com/api-reference/commands
20593
+ */
20594
+ var FirebenderCommand = class FirebenderCommand extends ToolCommand {
20595
+ frontmatter;
20596
+ body;
20597
+ constructor({ frontmatter, body, ...rest }) {
20598
+ if (rest.validate) {
20599
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20600
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
20601
+ }
20602
+ super({
20603
+ ...rest,
20604
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
20605
+ });
20606
+ this.frontmatter = frontmatter;
20607
+ this.body = body;
20608
+ }
20609
+ static getSettablePaths(_options = {}) {
20610
+ return { relativeDirPath: FIREBENDER_COMMANDS_DIR_PATH };
20611
+ }
20612
+ getBody() {
20613
+ return this.body;
20614
+ }
20615
+ getFrontmatter() {
20616
+ return this.frontmatter;
20617
+ }
20618
+ toRulesyncCommand() {
20619
+ const { description, ...restFields } = this.frontmatter;
20620
+ const rulesyncFrontmatter = {
20621
+ targets: ["*"],
20622
+ description,
20623
+ ...Object.keys(restFields).length > 0 && { firebender: restFields }
20624
+ };
20625
+ return new RulesyncCommand({
20626
+ outputRoot: ".",
20627
+ frontmatter: rulesyncFrontmatter,
20628
+ body: this.body,
20629
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
20630
+ relativeFilePath: this.relativeFilePath.replace(/\.mdc$/, ".md"),
20631
+ fileContent: stringifyFrontmatter(this.body, rulesyncFrontmatter),
20632
+ validate: true
20633
+ });
20634
+ }
20635
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
20636
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
20637
+ const firebenderFields = rulesyncFrontmatter.firebender ?? {};
20638
+ const frontmatter = {
20639
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
20640
+ ...firebenderFields
20641
+ };
20642
+ return new FirebenderCommand({
20643
+ outputRoot,
20644
+ frontmatter,
20645
+ body: rulesyncCommand.getBody(),
20646
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
20647
+ relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(/\.md$/, FIREBENDER_COMMAND_EXTENSION),
20648
+ validate
20649
+ });
20650
+ }
20651
+ validate() {
20652
+ if (!this.frontmatter) return {
20653
+ success: true,
20654
+ error: null
20655
+ };
20656
+ const result = FirebenderCommandFrontmatterSchema.safeParse(this.frontmatter);
20657
+ if (result.success) return {
20658
+ success: true,
20659
+ error: null
20660
+ };
20661
+ return {
20662
+ success: false,
20663
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
20664
+ };
20665
+ }
20666
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
20667
+ return this.isTargetedByRulesyncCommandDefault({
20668
+ rulesyncCommand,
20669
+ toolTarget: "firebender"
20670
+ });
20671
+ }
20672
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
20673
+ const paths = this.getSettablePaths({ global });
20674
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
20675
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
20676
+ const result = FirebenderCommandFrontmatterSchema.safeParse(frontmatter);
20677
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
20678
+ return new FirebenderCommand({
20679
+ outputRoot,
20680
+ relativeDirPath: paths.relativeDirPath,
20681
+ relativeFilePath,
20682
+ frontmatter: result.data,
20683
+ body: body.trim(),
20684
+ validate
20685
+ });
20686
+ }
20687
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
20688
+ return new FirebenderCommand({
20689
+ outputRoot,
20690
+ relativeDirPath,
20691
+ relativeFilePath,
20692
+ frontmatter: {},
20693
+ body: "",
20694
+ validate: false
20695
+ });
20696
+ }
20697
+ };
20698
+ //#endregion
20232
20699
  //#region src/features/commands/gitlabduo-command.ts
20233
20700
  /**
20234
20701
  * GitLab Duo CLI custom slash commands are Markdown files named after the
@@ -20821,6 +21288,60 @@ var GrokcliCommand = class GrokcliCommand extends ToolCommand {
20821
21288
  }
20822
21289
  };
20823
21290
  //#endregion
21291
+ //#region src/features/shared/hermes-plugin-commands.ts
21292
+ /**
21293
+ * Normalize a name into a Hermes Agent plugin slash command name.
21294
+ *
21295
+ * `ctx.register_command()` lowercases the name, and Hermes' messaging gateway
21296
+ * looks plugin commands up with every `_` turned into `-`
21297
+ * (`gateway/run_inbound_hooks.py`), so only a lowercase, hyphenated name is
21298
+ * reachable from every surface.
21299
+ */
21300
+ function hermesSlashName(value) {
21301
+ return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21302
+ }
21303
+ /**
21304
+ * Python helpers shared by the generated Hermes command plugins.
21305
+ *
21306
+ * A plugin slash command runs outside any agent turn, so it cannot call
21307
+ * `delegate_task` (which requires a parent agent). Instead the handler queues
21308
+ * its prompt as a user message with `ctx.inject_message()`, passing the calling
21309
+ * session's key: a `session_key` handler keyword on newer Hermes, or
21310
+ * `HERMES_SESSION_KEY` from the session context the gateway binds around the
21311
+ * handler on older releases.
21312
+ */
21313
+ function getHermesQueueUserMessagePython({ pluginName }) {
21314
+ return `def _session_key(kwargs):
21315
+ session_key = kwargs.get("session_key")
21316
+ if session_key:
21317
+ return session_key
21318
+ try:
21319
+ from gateway.session_context import get_session_env
21320
+ except Exception:
21321
+ return None
21322
+ return get_session_env("HERMES_SESSION_KEY") or None
21323
+
21324
+
21325
+ def _queue_user_message(ctx, message, kwargs, label):
21326
+ """Queue *message* as the session's next user turn; return an error reply, or None."""
21327
+ session_key = _session_key(kwargs)
21328
+ try:
21329
+ if session_key:
21330
+ queued = ctx.inject_message(message, role="user", session_key=session_key)
21331
+ else:
21332
+ queued = ctx.inject_message(message, role="user")
21333
+ except Exception as error:
21334
+ return f"Could not queue {label}: {error}"
21335
+ if not queued:
21336
+ return (
21337
+ f"Hermes refused to queue {label}: there is no active session, or this plugin "
21338
+ "may not inject messages. In gateway sessions, set "
21339
+ "plugins.entries.${pluginName}.allow_gateway_injection: true in config.yaml."
21340
+ )
21341
+ return None
21342
+ `;
21343
+ }
21344
+ //#endregion
20824
21345
  //#region src/features/skills/agentsskills-skill.ts
20825
21346
  const AgentsSkillsSkillFrontmatterSchema = z.looseObject({
20826
21347
  name: z.string(),
@@ -21303,9 +21824,6 @@ const HermesCommandSpecSchema = z.object({
21303
21824
  description: z.string(),
21304
21825
  prompt: z.string()
21305
21826
  });
21306
- function hermesSlashName(value) {
21307
- return value.toLowerCase().replace(/[ _]/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
21308
- }
21309
21827
  function getPluginManifestContent$2() {
21310
21828
  return [
21311
21829
  "name: rulesync-commands",
@@ -21339,6 +21857,8 @@ def _load_commands():
21339
21857
  return commands
21340
21858
 
21341
21859
 
21860
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-commands" })}
21861
+
21342
21862
  def _register_command(ctx, command):
21343
21863
  slug = command.get("slug")
21344
21864
  if not slug:
@@ -21348,31 +21868,22 @@ def _register_command(ctx, command):
21348
21868
  prompt = command.get("prompt") or ""
21349
21869
 
21350
21870
  def handler(args=None, **kwargs):
21351
- del kwargs
21352
21871
  raw_args = ""
21353
21872
  if isinstance(args, dict):
21354
21873
  raw_args = args.get("args") or args.get("context") or args.get("prompt") or ""
21355
21874
  elif args is not None:
21356
21875
  raw_args = str(args)
21357
21876
 
21358
- context_parts = [prompt] if prompt else []
21877
+ parts = [prompt] if prompt else []
21359
21878
  if raw_args:
21360
- context_parts.append(f"User arguments:\\n{raw_args}")
21879
+ parts.append(f"User arguments:\\n{raw_args}")
21361
21880
 
21362
- # delegate_task takes no model-facing "toolsets" argument: the child
21363
- # inherits the parent's enabled toolsets. "tasks" is the only shape its
21364
- # schema advertises; a top-level "goal" is an unadvertised legacy path.
21365
- return ctx.dispatch_tool(
21366
- "delegate_task",
21367
- {
21368
- "tasks": [
21369
- {
21370
- "goal": description,
21371
- "context": "\\n\\n".join(context_parts),
21372
- },
21373
- ],
21374
- },
21375
- )
21881
+ # A slash command runs outside any agent turn, so calling delegate_task
21882
+ # here fails with "delegate_task requires a parent agent context".
21883
+ # Queue the command prompt as the session's next user message instead.
21884
+ label = f"the /{slug} command"
21885
+ error = _queue_user_message(ctx, "\\n\\n".join(parts) or description, kwargs, label)
21886
+ return error or f"Queued {label}."
21376
21887
 
21377
21888
  ctx.register_command(slug, handler, description)
21378
21889
 
@@ -23697,6 +24208,7 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
23697
24208
  const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
23698
24209
  const REASONIX_GLOBAL_DIR = ".reasonix";
23699
24210
  const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
24211
+ const REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH = join(".reasonix", REASONIX_PROJECT_MCP_FILE_NAME);
23700
24212
  const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
23701
24213
  const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
23702
24214
  const REASONIX_DIR = REASONIX_GLOBAL_DIR;
@@ -25244,6 +25756,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
25244
25756
  supportsSubdirectory: false
25245
25757
  }
25246
25758
  }],
25759
+ ["firebender", {
25760
+ class: FirebenderCommand,
25761
+ meta: {
25762
+ extension: "mdc",
25763
+ supportsProject: true,
25764
+ supportsGlobal: true,
25765
+ isSimulated: false,
25766
+ supportsSubdirectory: false
25767
+ }
25768
+ }],
25247
25769
  ["goose", {
25248
25770
  class: GooseCommand,
25249
25771
  meta: {
@@ -28034,25 +28556,65 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
28034
28556
  //#endregion
28035
28557
  //#region src/features/hooks/antigravity-hooks.ts
28036
28558
  /**
28037
- * Antigravity uses a Claude-Code-like `hooks.json` (matcher shape). The
28038
- * `PreToolUse`/`PostToolUse` events use the matcher-based array shape, while
28039
- * `PreInvocation`/`PostInvocation`/`Stop` are matcher-less handler lists.
28040
- * `projectDirVar` is empty because Antigravity does not document a
28041
- * project-directory variable for commands.
28559
+ * Canonical events whose Antigravity counterparts (`SessionStart`,
28560
+ * `PreInvocation`, `PostInvocation`, `Stop`) take no matcher.
28561
+ */
28562
+ const ANTIGRAVITY_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
28563
+ "sessionStart",
28564
+ "preModelInvocation",
28565
+ "postModelInvocation",
28566
+ "stop"
28567
+ ]);
28568
+ /**
28569
+ * Antigravity event names whose value is a flat list of handlers
28570
+ * (`"Stop": [ { "type": "command", "command": "..." } ]`) rather than a list
28571
+ * of `{ "matcher", "hooks": [...] }` groups. Antigravity's loader rejects a
28572
+ * wrapped group under these events ("command hook must specify 'command'")
28573
+ * and then skips the whole `hooks.json`.
28574
+ * https://antigravity.google/docs/hooks
28575
+ */
28576
+ const ANTIGRAVITY_FLAT_HANDLER_EVENTS = new Set([...ANTIGRAVITY_NO_MATCHER_EVENTS].map((event) => lookupOwn({
28577
+ record: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
28578
+ key: event
28579
+ }) ?? event));
28580
+ /**
28581
+ * Antigravity uses a Claude-Code-like `hooks.json`. The `PreToolUse` /
28582
+ * `PostToolUse` events use the matcher-based group shape, while `SessionStart`,
28583
+ * `PreInvocation`, `PostInvocation` and `Stop` are flat handler lists (see
28584
+ * {@link toAntigravityEventMap}). `projectDirVar` is empty because Antigravity
28585
+ * does not document a project-directory variable for commands.
28042
28586
  */
28043
28587
  const ANTIGRAVITY_CONVERTER_CONFIG = {
28044
28588
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28045
28589
  canonicalToToolEventNames: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
28046
28590
  toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
28047
28591
  projectDirVar: "",
28048
- noMatcherEvents: /* @__PURE__ */ new Set([
28049
- "preModelInvocation",
28050
- "postModelInvocation",
28051
- "stop"
28052
- ]),
28592
+ noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
28053
28593
  supportedHookTypes: /* @__PURE__ */ new Set(["command"])
28054
28594
  };
28055
28595
  /**
28596
+ * Turn the shared converter's matcher-group output into Antigravity's shape:
28597
+ * the matcher-less events get their handlers directly under the event key.
28598
+ */
28599
+ function toAntigravityEventMap(eventMap) {
28600
+ return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
28601
+ if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event)) return [event, entries];
28602
+ return [event, entries.flatMap((entry) => isPlainObject$1(entry) && Array.isArray(entry.hooks) ? entry.hooks : [])];
28603
+ }));
28604
+ }
28605
+ /**
28606
+ * Reverse of {@link toAntigravityEventMap} for import: wrap each flat handler
28607
+ * of a matcher-less event in a `{ hooks: [...] }` group so the shared converter
28608
+ * can read it. An entry that is already a group (`{ hooks: [...] }`, as older
28609
+ * rulesync versions wrote) is kept as is.
28610
+ */
28611
+ function toMatcherGroupEventMap(eventMap) {
28612
+ return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
28613
+ if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event) || !Array.isArray(entries)) return [event, entries];
28614
+ return [event, entries.map((entry) => isPlainObject$1(entry) && !("hooks" in entry) ? { hooks: [entry] } : entry)];
28615
+ }));
28616
+ }
28617
+ /**
28056
28618
  * Antigravity's `hooks.json` is keyed by a named hook whose value holds the
28057
28619
  * per-event map (plus an optional `enabled` flag). rulesync writes a single
28058
28620
  * generated hook under this stable name.
@@ -28089,8 +28651,8 @@ function flattenAntigravityHooks(parsed) {
28089
28651
  * Hooks generator for Google Antigravity (both the IDE and the CLI).
28090
28652
  *
28091
28653
  * Antigravity writes a dedicated `hooks.json` keyed by a named hook whose
28092
- * value holds the Claude-Code-style event → matcher-entry map
28093
- * (`{ "<name>": { "<Event>": [...] } }`). Project and global modes share the
28654
+ * value holds the event map (`{ "<name>": { "<Event>": [...] } }`): matcher
28655
+ * groups for the tool events, flat handler lists for the others. Project and global modes share the
28094
28656
  * same shape; only the location differs (`.agents/hooks.json` vs
28095
28657
  * `~/.gemini/config/hooks.json`).
28096
28658
  *
@@ -28134,12 +28696,12 @@ var AntigravityHooks = class extends ToolHooks {
28134
28696
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28135
28697
  const paths = this.getSettablePaths({ global });
28136
28698
  const config = rulesyncHooks.getJson();
28137
- const eventMap = canonicalToToolHooks({
28699
+ const eventMap = toAntigravityEventMap(canonicalToToolHooks({
28138
28700
  config,
28139
28701
  toolOverrideHooks: config[this.getOverrideKey()]?.hooks,
28140
28702
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28141
28703
  logger
28142
- });
28704
+ }));
28143
28705
  const antigravityHooks = Object.keys(eventMap).length > 0 ? { [ANTIGRAVITY_HOOK_NAME]: eventMap } : {};
28144
28706
  const fileContent = JSON.stringify(antigravityHooks, null, 2);
28145
28707
  return new this({
@@ -28158,7 +28720,7 @@ var AntigravityHooks = class extends ToolHooks {
28158
28720
  throw new Error(`Failed to parse Antigravity hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28159
28721
  }
28160
28722
  const hooks = toolHooksToCanonical({
28161
- hooks: flattenAntigravityHooks(parsed),
28723
+ hooks: toMatcherGroupEventMap(flattenAntigravityHooks(parsed)),
28162
28724
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28163
28725
  logger
28164
28726
  });
@@ -30622,11 +31184,17 @@ var ContinueHooks = class extends SettingsJsonHooks {
30622
31184
  * entry that uses them instead of a shell field is not mistaken for one
30623
31185
  * without a command.
30624
31186
  *
31187
+ * An optional `matcher` regex is honored on the events in
31188
+ * {@link COPILOT_MATCHER_HOOK_EVENTS}. The VS Code per-OS override fields come
31189
+ * from the legacy Local hooks reference and are kept for existing files.
31190
+ *
30625
31191
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
31192
+ * @see https://code.visualstudio.com/docs/agent-customization/hooks
30626
31193
  * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
30627
31194
  */
30628
31195
  const CopilotHookEntrySchema = z.looseObject({
30629
31196
  type: z.string(),
31197
+ matcher: z.optional(z.string()),
30630
31198
  bash: z.optional(z.string()),
30631
31199
  powershell: z.optional(z.string()),
30632
31200
  command: z.optional(z.string()),
@@ -30660,8 +31228,10 @@ function canonicalToCopilotHooks(config) {
30660
31228
  ...sharedConfigHooks,
30661
31229
  ...config.copilot?.hooks
30662
31230
  };
31231
+ const matcherEvents = new Set(COPILOT_MATCHER_HOOK_EVENTS);
30663
31232
  const copilot = {};
30664
31233
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
31234
+ const matcherSupported = matcherEvents.has(eventName);
30665
31235
  const copilotEventName = lookupOwn({
30666
31236
  record: CANONICAL_TO_COPILOT_EVENT_NAMES,
30667
31237
  key: eventName
@@ -30669,7 +31239,7 @@ function canonicalToCopilotHooks(config) {
30669
31239
  const entries = [];
30670
31240
  for (const def of definitions) {
30671
31241
  const hookType = def.type ?? "command";
30672
- if (def.matcher) continue;
31242
+ if (def.matcher && !matcherSupported) continue;
30673
31243
  if (hookType !== "command") continue;
30674
31244
  const command = def.command;
30675
31245
  const timeout = def.timeout;
@@ -30677,6 +31247,7 @@ function canonicalToCopilotHooks(config) {
30677
31247
  const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
30678
31248
  entries.push({
30679
31249
  type: hookType,
31250
+ ...def.matcher && { matcher: def.matcher },
30680
31251
  ...command !== void 0 && command !== null && { [commandField]: command },
30681
31252
  ...def.env !== void 0 && { env: def.env },
30682
31253
  ...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
@@ -30771,6 +31342,7 @@ function copilotHooksToCanonical(copilotHooks, logger) {
30771
31342
  if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
30772
31343
  defs.push({
30773
31344
  type: "command",
31345
+ ...entry.matcher !== void 0 && entry.matcher !== "" && { matcher: entry.matcher },
30774
31346
  ...command !== void 0 && { command },
30775
31347
  ...shell !== void 0 && { shell },
30776
31348
  ...timeout !== void 0 && { timeout },
@@ -30914,21 +31486,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
30914
31486
  */
30915
31487
  /**
30916
31488
  * Canonical events whose Copilot CLI hook entries honor an optional `matcher`
30917
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`).
30918
- *
30919
- * Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
30920
- * honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
30921
- * `Edit|Write`) are now honored instead of silently dropped").
30922
- * @see https://docs.github.com/en/copilot/reference/hooks-reference
31489
+ * field (a regex compiled as `^(?:PATTERN)$`). The hooks reference filters each
31490
+ * on a different field: the tool name for `preToolUse` / `postToolUse` /
31491
+ * `permissionRequest`, `notification_type`, the `preCompact` trigger
31492
+ * (`manual` | `auto`) and the `subagentStart` agent name.
31493
+ * See {@link COPILOTCLI_MATCHER_HOOK_EVENTS}.
30923
31494
  */
30924
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
30925
- "preToolUse",
30926
- "postToolUse",
30927
- "notification",
30928
- "permissionRequest",
30929
- "preCompact",
30930
- "subagentStart"
30931
- ]);
31495
+ const COPILOTCLI_MATCHER_EVENTS = new Set(COPILOTCLI_MATCHER_HOOK_EVENTS);
30932
31496
  /** Human-readable list of the matcher-aware events, for the drop warning. */
30933
31497
  const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
30934
31498
  const CopilotCliHookEntrySchema = z.looseObject({
@@ -32527,6 +33091,116 @@ var FactorydroidHooks = class FactorydroidHooks extends ToolHooks {
32527
33091
  }
32528
33092
  };
32529
33093
  //#endregion
33094
+ //#region src/features/hooks/firebender-hooks.ts
33095
+ /**
33096
+ * Hooks generator for Firebender.
33097
+ *
33098
+ * Firebender reads `.firebender/hooks.json` (project) and
33099
+ * `~/.firebender/hooks.json` (global). The file follows Cursor's layout — a
33100
+ * `version` plus a `hooks` map from camelCase event names to lists of
33101
+ * `{ command, timeout?, loop_limit?, matcher? }` entries — and every event is
33102
+ * spelled like its canonical counterpart. The per-hook `type` accepts
33103
+ * `"command" | "prompt"`, but the docs never say what a prompt hook does, so
33104
+ * only command hooks are generated (the HooksProcessor warns about the rest).
33105
+ *
33106
+ * @see https://docs.firebender.com/multi-agent/hooks
33107
+ */
33108
+ var FirebenderHooks = class FirebenderHooks extends ToolHooks {
33109
+ constructor(params) {
33110
+ super({
33111
+ ...params,
33112
+ fileContent: params.fileContent ?? "{}"
33113
+ });
33114
+ }
33115
+ static getSettablePaths(_options = {}) {
33116
+ return {
33117
+ relativeDirPath: FIREBENDER_DIR,
33118
+ relativeFilePath: FIREBENDER_HOOKS_FILE_NAME
33119
+ };
33120
+ }
33121
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
33122
+ const paths = FirebenderHooks.getSettablePaths({ global });
33123
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
33124
+ return new FirebenderHooks({
33125
+ outputRoot,
33126
+ relativeDirPath: paths.relativeDirPath,
33127
+ relativeFilePath: paths.relativeFilePath,
33128
+ fileContent,
33129
+ validate
33130
+ });
33131
+ }
33132
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
33133
+ const config = rulesyncHooks.getJson();
33134
+ const supportedEvents = new Set(FIREBENDER_HOOK_EVENTS);
33135
+ const sharedHooks = {};
33136
+ for (const [event, defs] of Object.entries(config.hooks)) if (supportedEvents.has(event)) sharedHooks[event] = defs;
33137
+ const mergedHooks = {
33138
+ ...sharedHooks,
33139
+ ...config.firebender?.hooks
33140
+ };
33141
+ const firebenderHooks = {};
33142
+ for (const [eventName, defs] of Object.entries(mergedHooks)) {
33143
+ const mappedDefs = defs.filter((def) => (def.type ?? "command") === "command").filter((def) => typeof def.command === "string" && def.command.length > 0).map((def) => ({
33144
+ command: def.command,
33145
+ ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
33146
+ ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
33147
+ ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher }
33148
+ }));
33149
+ if (mappedDefs.length > 0) firebenderHooks[eventName] = mappedDefs;
33150
+ }
33151
+ const paths = FirebenderHooks.getSettablePaths({ global });
33152
+ const merged = mergeGeneratedHookLists({
33153
+ existingContent: await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "",
33154
+ generatedHooks: firebenderHooks,
33155
+ shape: "flat",
33156
+ preserveUnowned,
33157
+ previouslyOwned: preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0,
33158
+ logger
33159
+ });
33160
+ const fileContent = JSON.stringify({
33161
+ version: config.version ?? 1,
33162
+ hooks: merged.hooks
33163
+ }, null, 2);
33164
+ return new FirebenderHooks({
33165
+ outputRoot,
33166
+ relativeDirPath: paths.relativeDirPath,
33167
+ relativeFilePath: paths.relativeFilePath,
33168
+ fileContent,
33169
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
33170
+ validate
33171
+ });
33172
+ }
33173
+ static supportsPreserveUnowned() {
33174
+ return true;
33175
+ }
33176
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
33177
+ return toolHooks instanceof FirebenderHooks ? toolHooks.getOwnershipLockFiles() : [];
33178
+ }
33179
+ toRulesyncHooks() {
33180
+ const parsed = JSON.parse(this.getFileContent());
33181
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
33182
+ hooks: parsed.hooks ?? {},
33183
+ overrideKey: "firebender",
33184
+ version: parsed.version ?? 1
33185
+ }), null, 2) });
33186
+ }
33187
+ validate() {
33188
+ return {
33189
+ success: true,
33190
+ error: null
33191
+ };
33192
+ }
33193
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
33194
+ return new FirebenderHooks({
33195
+ outputRoot,
33196
+ relativeDirPath,
33197
+ relativeFilePath,
33198
+ fileContent: "{}",
33199
+ validate: false
33200
+ });
33201
+ }
33202
+ };
33203
+ //#endregion
32530
33204
  //#region src/features/hooks/gitlabduo-hooks.ts
32531
33205
  const GITLABDUO_CONVERTER_CONFIG = {
32532
33206
  supportedEvents: GITLABDUO_HOOK_EVENTS,
@@ -32954,7 +33628,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
32954
33628
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
32955
33629
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
32956
33630
  if (HERMESAGENT_SHELL_UNSUPPORTED_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is Python-plugin-only; Hermes Agent refuses it for shell hooks, so the generated entry will never run.`);
32957
- else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.21.6; preserving it for forward compatibility.`);
33631
+ else if (!HERMESAGENT_NATIVE_EVENTS.has(nativeEvent)) logger?.warn(`Hermes hook event "${nativeEvent}" is not documented by Hermes Agent v0.22.3; preserving it for forward compatibility.`);
32958
33632
  setHermesHookEntries({
32959
33633
  result,
32960
33634
  event: nativeEvent,
@@ -36937,6 +37611,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
36937
37611
  supportedHookTypes: ["command", "prompt"],
36938
37612
  supportsMatcher: true
36939
37613
  }],
37614
+ ["firebender", {
37615
+ class: FirebenderHooks,
37616
+ meta: {
37617
+ supportsProject: true,
37618
+ supportsGlobal: true,
37619
+ supportsImport: true
37620
+ },
37621
+ supportedEvents: FIREBENDER_HOOK_EVENTS,
37622
+ supportedHookTypes: ["command"],
37623
+ supportsMatcher: true
37624
+ }],
36940
37625
  ["claudecode", {
36941
37626
  class: ClaudecodeHooks,
36942
37627
  meta: {
@@ -37033,7 +37718,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37033
37718
  },
37034
37719
  supportedEvents: COPILOT_HOOK_EVENTS,
37035
37720
  supportedHookTypes: ["command"],
37036
- supportsMatcher: false
37721
+ supportsMatcher: true,
37722
+ matcherEvents: COPILOT_MATCHER_HOOK_EVENTS
37037
37723
  }],
37038
37724
  ["copilotcli", {
37039
37725
  class: CopilotcliHooks,
@@ -37048,7 +37734,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37048
37734
  "prompt",
37049
37735
  "http"
37050
37736
  ],
37051
- supportsMatcher: false
37737
+ supportsMatcher: true
37052
37738
  }],
37053
37739
  ["kilo", {
37054
37740
  class: KiloHooks,
@@ -37808,6 +38494,67 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
37808
38494
  }
37809
38495
  };
37810
38496
  //#endregion
38497
+ //#region src/constants/androidstudio-paths.ts
38498
+ /**
38499
+ * Gemini in Android Studio configuration-layout conventions.
38500
+ *
38501
+ * Only the Android Studio-specific files are owned by the `androidstudio`
38502
+ * target. The shared `AGENTS.md` and `.agents/skills/` surfaces Gemini also
38503
+ * reads belong to the `agentsmd` and `agentsskills` targets.
38504
+ */
38505
+ const ANDROIDSTUDIO_IGNORE_FILE_NAME = ".aiexclude";
38506
+ const ANDROIDSTUDIO_SKILLS_DIR_PATH = join(".android-studio", "skills");
38507
+ //#endregion
38508
+ //#region src/features/ignore/androidstudio-ignore.ts
38509
+ /**
38510
+ * Ignore generator for Gemini in Android Studio.
38511
+ *
38512
+ * Gemini reads `.aiexclude` files (gitignore syntax) anywhere in the project;
38513
+ * each one applies to the files at or below its directory and blocks both the
38514
+ * chat and the editor AI features such as code completion. Rulesync writes the
38515
+ * project-root file only. There is no user-level variant.
38516
+ *
38517
+ * @see https://developer.android.com/studio/gemini/aiexclude
38518
+ */
38519
+ var AndroidstudioIgnore = class AndroidstudioIgnore extends ToolIgnore {
38520
+ static getSettablePaths() {
38521
+ return {
38522
+ relativeDirPath: ".",
38523
+ relativeFilePath: ANDROIDSTUDIO_IGNORE_FILE_NAME
38524
+ };
38525
+ }
38526
+ toRulesyncIgnore() {
38527
+ return this.toRulesyncIgnoreDefault();
38528
+ }
38529
+ static fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore }) {
38530
+ return new AndroidstudioIgnore({
38531
+ outputRoot,
38532
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38533
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38534
+ fileContent: rulesyncIgnore.getFileContent()
38535
+ });
38536
+ }
38537
+ static async fromFile({ outputRoot = process.cwd(), validate = true }) {
38538
+ const fileContent = await readFileContent(join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath));
38539
+ return new AndroidstudioIgnore({
38540
+ outputRoot,
38541
+ relativeDirPath: this.getSettablePaths().relativeDirPath,
38542
+ relativeFilePath: this.getSettablePaths().relativeFilePath,
38543
+ fileContent,
38544
+ validate
38545
+ });
38546
+ }
38547
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
38548
+ return new AndroidstudioIgnore({
38549
+ outputRoot,
38550
+ relativeDirPath,
38551
+ relativeFilePath,
38552
+ fileContent: "",
38553
+ validate: false
38554
+ });
38555
+ }
38556
+ };
38557
+ //#endregion
37811
38558
  //#region src/features/ignore/antigravity-cli-ignore.ts
37812
38559
  /**
37813
38560
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -38469,6 +39216,95 @@ var DevinIgnore = class DevinIgnore extends ToolIgnore {
38469
39216
  }
38470
39217
  };
38471
39218
  //#endregion
39219
+ //#region src/features/ignore/firebender-ignore.ts
39220
+ function readIgnorePatterns(fileContent) {
39221
+ const ignore = parseSharedConfig({
39222
+ format: "json",
39223
+ fileContent
39224
+ }).ignore;
39225
+ if (!Array.isArray(ignore)) return [];
39226
+ return ignore.filter((pattern) => typeof pattern === "string");
39227
+ }
39228
+ /**
39229
+ * Ignore generator for Firebender.
39230
+ *
39231
+ * Firebender reads gitignore-style patterns from the `ignore` array of
39232
+ * `firebender.json` (project root) and `~/.firebender/firebender.json`
39233
+ * (personal); the two lists are appended together. The file is shared with the
39234
+ * MCP (`mcpServers`) and subagents (`agents`) features and carries settings
39235
+ * rulesync does not own, so only the `ignore` key is written and the file is
39236
+ * never deleted.
39237
+ *
39238
+ * @see https://docs.firebender.com/api-reference/syntax
39239
+ */
39240
+ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
39241
+ constructor(params) {
39242
+ super(params);
39243
+ this.patterns = readIgnorePatterns(this.fileContent || "{}");
39244
+ }
39245
+ static getSettablePaths({ global = false } = {}) {
39246
+ return {
39247
+ relativeDirPath: getFirebenderConfigDir({ global }),
39248
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
39249
+ };
39250
+ }
39251
+ /** `firebender.json` is a user-managed config file, so rulesync never deletes it. */
39252
+ isDeletable() {
39253
+ return false;
39254
+ }
39255
+ toRulesyncIgnore() {
39256
+ return new RulesyncIgnore({
39257
+ outputRoot: ".",
39258
+ relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
39259
+ relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
39260
+ fileContent: this.patterns.filter((pattern) => pattern.length > 0).join("\n")
39261
+ });
39262
+ }
39263
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
39264
+ const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
39265
+ const paths = this.getSettablePaths({ global });
39266
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
39267
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
39268
+ const managedPatterns = [...new Set(patterns)];
39269
+ return new FirebenderIgnore({
39270
+ outputRoot,
39271
+ relativeDirPath: paths.relativeDirPath,
39272
+ relativeFilePath: paths.relativeFilePath,
39273
+ fileContent: applySharedConfigPatch({
39274
+ fileKey: sharedConfigFileKey(paths),
39275
+ feature: "ignore",
39276
+ existingContent,
39277
+ patch: { ignore: managedPatterns.length > 0 ? managedPatterns : void 0 },
39278
+ filePath
39279
+ }),
39280
+ validate: true,
39281
+ global
39282
+ });
39283
+ }
39284
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
39285
+ const paths = this.getSettablePaths({ global });
39286
+ const fileContent = await readFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
39287
+ return new FirebenderIgnore({
39288
+ outputRoot,
39289
+ relativeDirPath: paths.relativeDirPath,
39290
+ relativeFilePath: paths.relativeFilePath,
39291
+ fileContent,
39292
+ validate,
39293
+ global
39294
+ });
39295
+ }
39296
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
39297
+ return new FirebenderIgnore({
39298
+ outputRoot,
39299
+ relativeDirPath,
39300
+ relativeFilePath,
39301
+ fileContent: "{}",
39302
+ validate: false,
39303
+ global
39304
+ });
39305
+ }
39306
+ };
39307
+ //#endregion
38472
39308
  //#region src/features/ignore/grokcli-ignore.ts
38473
39309
  /** The base built-in profile the rulesync profile extends (Grok's own default). */
38474
39310
  const GROKCLI_SANDBOX_BASE_PROFILE = "workspace";
@@ -39288,6 +40124,31 @@ const REASONIX_READ_TOOL_NAME = "read_file";
39288
40124
  * treated as rulesync-owned on generate so the dormant copy is replaced.
39289
40125
  */
39290
40126
  const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
40127
+ async function isPathPresent(path) {
40128
+ return await fileExists(path) || await isPresentButUnresolvable(path);
40129
+ }
40130
+ /**
40131
+ * Warns when a project-scope generate is about to create `./reasonix.toml`
40132
+ * next to an existing `.reasonix/reasonix.toml`.
40133
+ *
40134
+ * Since v2.46.0 Reasonix reads `.reasonix/reasonix.toml` as the project config
40135
+ * when `./reasonix.toml` is absent, and never merges the two: once the root
40136
+ * file exists it wins, so every setting in the nested file (permissions,
40137
+ * sandbox, disabled tools, MCP servers, ...) silently stops applying. rulesync
40138
+ * writes only the root file, so it cannot keep the nested one in effect; this
40139
+ * at least names the problem, once per run however many of the features that
40140
+ * write the file (mcp, permissions, ignore) are generated. Like Reasonix, anything at the root path
40141
+ * (including a broken symlink) counts as the root file being there.
40142
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/internal/contract/config/paths.go
40143
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/CONFIG_PATHS.md
40144
+ */
40145
+ async function warnIfReasonixNestedProjectConfigShadowed({ outputRoot, global, logger }) {
40146
+ if (global) return;
40147
+ if (await isPathPresent(join(outputRoot, "reasonix.toml"))) return;
40148
+ if (!await isPathPresent(join(outputRoot, REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH))) return;
40149
+ const nested = toPosixPath(REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH);
40150
+ warnOnceWithFallback(logger, `Reasonix: ${nested} exists but ${REASONIX_PROJECT_MCP_FILE_NAME} does not. Generating creates ${REASONIX_PROJECT_MCP_FILE_NAME}, and Reasonix then reads it instead of ${nested} (the two are never merged), so every setting in ${nested} stops applying. Move those settings into ${REASONIX_PROJECT_MCP_FILE_NAME} or the rulesync sources.`);
40151
+ }
39291
40152
  //#endregion
39292
40153
  //#region src/features/ignore/reasonix-ignore.ts
39293
40154
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -39371,7 +40232,12 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
39371
40232
  fileContent: rulesyncPatterns.join("\n")
39372
40233
  });
39373
40234
  }
39374
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
40235
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
40236
+ await warnIfReasonixNestedProjectConfigShadowed({
40237
+ outputRoot,
40238
+ global,
40239
+ logger
40240
+ });
39375
40241
  const readDenies = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern, REASONIX_READ_TOOL_NAME));
39376
40242
  const paths = this.getSettablePaths({ global });
39377
40243
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -39786,6 +40652,7 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
39786
40652
  const IgnoreProcessorToolTargetSchema = z.enum(ignoreProcessorToolTargetTuple);
39787
40653
  const toolIgnoreFactories = /* @__PURE__ */ new Map([
39788
40654
  ["aiassistant", { class: AiassistantIgnore }],
40655
+ ["androidstudio", { class: AndroidstudioIgnore }],
39789
40656
  ["antigravity-cli", { class: AntigravityCliIgnore }],
39790
40657
  ["augmentcode", { class: AugmentcodeIgnore }],
39791
40658
  ["bob", { class: BobIgnore }],
@@ -39796,6 +40663,7 @@ const toolIgnoreFactories = /* @__PURE__ */ new Map([
39796
40663
  ["continue", { class: ContinueIgnore }],
39797
40664
  ["crush", { class: CrushIgnore }],
39798
40665
  ["cursor", { class: CursorIgnore }],
40666
+ ["firebender", { class: FirebenderIgnore }],
39799
40667
  ["grokcli", { class: GrokcliIgnore }],
39800
40668
  ["hermesagent", { class: HermesagentIgnore }],
39801
40669
  ["junie", { class: JunieIgnore }],
@@ -39820,6 +40688,7 @@ const ignoreProcessorToolTargets = [...toolIgnoreFactories.keys()];
39820
40688
  const ignoreProcessorGlobalToolTargets = [
39821
40689
  "continue",
39822
40690
  "devin",
40691
+ "firebender",
39823
40692
  "grokcli",
39824
40693
  "kilo",
39825
40694
  "kiro",
@@ -39946,7 +40815,8 @@ var IgnoreProcessor = class extends FeatureProcessor {
39946
40815
  outputRoot: this.outputRoot,
39947
40816
  rulesyncIgnore,
39948
40817
  options: this.featureOptions,
39949
- global: this.global
40818
+ global: this.global,
40819
+ logger: this.logger
39950
40820
  });
39951
40821
  const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
39952
40822
  toolIgnore,
@@ -40296,13 +41166,31 @@ function renameServerField(servers, from, to) {
40296
41166
  return [name, Object.fromEntries(Object.entries(config).map(([key, value]) => key === from ? [to, value] : [key, value]))];
40297
41167
  }));
40298
41168
  }
41169
+ function isEmptyValue(value) {
41170
+ if (Array.isArray(value)) return value.length === 0;
41171
+ return isPlainObject$1(value) && Object.keys(value).length === 0;
41172
+ }
40299
41173
  /**
40300
41174
  * Antigravity uses `serverUrl` (not the canonical `url`) for HTTP/SSE MCP
40301
41175
  * servers. Convert the canonical shape into Antigravity's on the way out, and
40302
- * back to canonical on the way in, so round-tripping is lossless.
41176
+ * back to canonical on the way in.
41177
+ *
41178
+ * Antigravity documents no `type` field: the transport follows from `command`
41179
+ * (stdio) or `serverUrl` (Streamable HTTP / SSE), and `agy plugin validate`
41180
+ * reports the canonical `type` as an unknown field, so it is not written. A
41181
+ * remote server that carries `command`, `args` or `env` was also observed to be
41182
+ * refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
41183
+ * not documented), so an empty `args` / `env` left on a `serverUrl` server is
41184
+ * dropped.
41185
+ * https://antigravity.google/docs/mcp
40303
41186
  */
40304
41187
  function toAntigravityMcpServers(servers) {
40305
- return renameServerField(servers, "url", "serverUrl");
41188
+ const renamed = renameServerField(servers, "url", "serverUrl");
41189
+ return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
41190
+ if (!isPlainObject$1(config)) return [name, config];
41191
+ const isRemote = typeof config.serverUrl === "string";
41192
+ return [name, Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value))))];
41193
+ }));
40306
41194
  }
40307
41195
  function toCanonicalMcpServers(servers) {
40308
41196
  return renameServerField(servers, "serverUrl", "url");
@@ -44492,6 +45380,60 @@ function applyDeepagentsCommand({ name, server, converted, logger }) {
44492
45380
  return true;
44493
45381
  }
44494
45382
  /**
45383
+ * Translate a canonical `oauth` block into dcode's custom Slack OAuth app
45384
+ * settings, or `undefined` (after warning) when it has to be left out.
45385
+ *
45386
+ * Since deepagents-code 0.1.87, `_validate_slack_oauth_config` accepts `oauth`
45387
+ * only as `{ client_id, client_secret? }` of non-empty strings, and only on a
45388
+ * `slack.com` / `*.slack.com` server with `auth: "oauth"`; anything else makes
45389
+ * dcode skip the whole server. The canonical block usually carries Claude
45390
+ * Code's `{ clientId, callbackPort, ... }` shape, so `clientId` becomes
45391
+ * `client_id` (a native `client_id` wins), `client_secret` passes through, and
45392
+ * every other key is dropped.
45393
+ *
45394
+ * @see https://github.com/langchain-ai/deepagents/blob/deepagents-code%3D%3D0.1.87/libs/code/deepagents_code/mcp_config.py
45395
+ */
45396
+ function toDeepagentsOauth({ name, oauth, converted, logger }) {
45397
+ if (!isSlackOauthServer(converted)) {
45398
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents reads it only on a Slack (slack.com) server with auth: "oauth" and skips any other server that sets it.`);
45399
+ return;
45400
+ }
45401
+ const source = isRecord$1(oauth) ? oauth : {};
45402
+ const result = {};
45403
+ for (const [key, value] of Object.entries(source)) {
45404
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
45405
+ const nativeKey = key === "clientId" ? "client_id" : key;
45406
+ if (nativeKey !== "client_id" && nativeKey !== "client_secret") {
45407
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents accepts only client_id and client_secret.`);
45408
+ continue;
45409
+ }
45410
+ if (nativeKey !== key && isDeepagentsOauthValue(source[nativeKey])) continue;
45411
+ if (!isDeepagentsOauthValue(value)) {
45412
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents requires a non-empty string without control characters.`);
45413
+ continue;
45414
+ }
45415
+ result[nativeKey] = value;
45416
+ }
45417
+ if (result.client_id === void 0) {
45418
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents requires oauth.client_id (canonical clientId) and skips a server without it.`);
45419
+ return;
45420
+ }
45421
+ return result;
45422
+ }
45423
+ function isDeepagentsOauthValue(value) {
45424
+ return typeof value === "string" && value.trim() !== "" && !/[\x00-\x1f\x7f]/.test(value);
45425
+ }
45426
+ function isSlackOauthServer(server) {
45427
+ if (server.auth !== "oauth" || typeof server.url !== "string") return false;
45428
+ let host;
45429
+ try {
45430
+ host = new URL(server.url).hostname.toLowerCase();
45431
+ } catch {
45432
+ return false;
45433
+ }
45434
+ return host === "slack.com" || host.endsWith(".slack.com");
45435
+ }
45436
+ /**
44495
45437
  * Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
44496
45438
  * skip it.
44497
45439
  *
@@ -44514,7 +45456,7 @@ function toDeepagentsServer({ name, server, logger }) {
44514
45456
  reason: "the WebSocket transport, which deepagents does not support",
44515
45457
  logger
44516
45458
  });
44517
- const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, ...rest } = server;
45459
+ const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, oauth, ...rest } = server;
44518
45460
  const converted = { ...rest };
44519
45461
  if (!applyDeepagentsCommand({
44520
45462
  name,
@@ -44529,6 +45471,15 @@ function toDeepagentsServer({ name, server, logger }) {
44529
45471
  if (transport !== void 0) converted.transport = normalized;
44530
45472
  else converted.type = normalized;
44531
45473
  }
45474
+ if (oauth !== void 0) {
45475
+ const deepagentsOauth = toDeepagentsOauth({
45476
+ name,
45477
+ oauth,
45478
+ converted,
45479
+ logger
45480
+ });
45481
+ if (deepagentsOauth !== void 0) converted.oauth = deepagentsOauth;
45482
+ }
44532
45483
  if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
44533
45484
  toolName: TOOL_NAME,
44534
45485
  serverName: name,
@@ -44555,6 +45506,13 @@ function toRulesyncServer(server) {
44555
45506
  const converted = { ...rest };
44556
45507
  for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
44557
45508
  if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
45509
+ if (isRecord$1(converted.oauth) && Object.hasOwn(converted.oauth, "client_id")) {
45510
+ const { client_id: clientId, ...oauthRest } = converted.oauth;
45511
+ converted.oauth = Object.hasOwn(oauthRest, "clientId") ? oauthRest : {
45512
+ clientId,
45513
+ ...oauthRest
45514
+ };
45515
+ }
44558
45516
  return converted;
44559
45517
  }
44560
45518
  var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
@@ -45317,6 +46275,171 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
45317
46275
  }
45318
46276
  };
45319
46277
  //#endregion
46278
+ //#region src/features/mcp/firebender-mcp.ts
46279
+ /**
46280
+ * The keys of the canonical server schema. Firebender reads only `command`,
46281
+ * `args`, `env`, `url` and `headers` of them, so the rest are dropped; a key
46282
+ * NOT on this list is unknown to rulesync and passed through untouched, so a
46283
+ * Firebender-native field such as `deferLoading` written in the tool-scoped
46284
+ * `firebender.mcpServers` block (or on a shared server) survives.
46285
+ */
46286
+ const CANONICAL_MCP_SERVER_KEYS$1 = new Set(Object.keys(McpServerSchema.def.shape));
46287
+ function passthroughNonCanonicalFields$1(serverConfig) {
46288
+ return Object.fromEntries(Object.entries(serverConfig).filter(([key]) => !CANONICAL_MCP_SERVER_KEYS$1.has(key)));
46289
+ }
46290
+ /**
46291
+ * Translate one canonical server into Firebender's `mcpServers` entry, or
46292
+ * return `null` (with a warning) for a server Firebender cannot express. A
46293
+ * stdio server is `{ command, args?, env? }`, a remote one `{ url, headers? }`;
46294
+ * Firebender has no `type` field and no per-server disable switch.
46295
+ *
46296
+ * @see https://docs.firebender.com/api-reference/syntax
46297
+ */
46298
+ function convertServerToFirebenderFormat({ serverName, serverConfig, logger }) {
46299
+ if (serverConfig.disabled === true) return warnAndSkipMcpServer({
46300
+ toolName: "Firebender",
46301
+ serverName,
46302
+ reason: "`disabled: true`, and Firebender has no per-server disable switch",
46303
+ logger
46304
+ });
46305
+ if (declaresNoTransport(serverConfig)) return warnAndSkipMcpServer({
46306
+ toolName: "Firebender",
46307
+ serverName,
46308
+ reason: "no command or url",
46309
+ logger
46310
+ });
46311
+ const passthrough = passthroughNonCanonicalFields$1(serverConfig);
46312
+ if (isRemoteMcpServer(serverConfig)) {
46313
+ const url = resolveRemoteMcpUrl(serverConfig);
46314
+ if (url === void 0) return warnAndSkipMcpServer({
46315
+ toolName: "Firebender",
46316
+ serverName,
46317
+ reason: "a remote transport but no url",
46318
+ logger
46319
+ });
46320
+ return {
46321
+ ...passthrough,
46322
+ url,
46323
+ ...serverConfig.headers !== void 0 && { headers: serverConfig.headers }
46324
+ };
46325
+ }
46326
+ const [command, ...args] = resolveLocalMcpCommand(serverConfig);
46327
+ if (command === void 0) return warnAndSkipMcpServer({
46328
+ toolName: "Firebender",
46329
+ serverName,
46330
+ reason: "a local transport but no command",
46331
+ logger
46332
+ });
46333
+ return {
46334
+ ...passthrough,
46335
+ command,
46336
+ ...args.length > 0 && { args },
46337
+ ...serverConfig.env !== void 0 && { env: serverConfig.env }
46338
+ };
46339
+ }
46340
+ /**
46341
+ * MCP generator for Firebender.
46342
+ *
46343
+ * Firebender reads MCP servers from the `mcpServers` key of `firebender.json`
46344
+ * (project root) and `~/.firebender/firebender.json` (personal; it wins on
46345
+ * name clashes). The file is shared with the ignore (`ignore`) and subagents
46346
+ * (`agents`) features and carries settings rulesync does not own, so only the
46347
+ * `mcpServers` key is written and the file is never deleted.
46348
+ *
46349
+ * @see https://docs.firebender.com/api-reference/syntax
46350
+ */
46351
+ var FirebenderMcp = class FirebenderMcp extends ToolMcp {
46352
+ json;
46353
+ constructor(params) {
46354
+ super(params);
46355
+ this.json = parseSharedConfig({
46356
+ format: "json",
46357
+ fileContent: this.fileContent || "{}"
46358
+ });
46359
+ }
46360
+ getJson() {
46361
+ return this.json;
46362
+ }
46363
+ static getSettablePaths({ global = false } = {}) {
46364
+ return {
46365
+ relativeDirPath: getFirebenderConfigDir({ global }),
46366
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
46367
+ };
46368
+ }
46369
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
46370
+ const paths = this.getSettablePaths({ global });
46371
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
46372
+ return new FirebenderMcp({
46373
+ outputRoot,
46374
+ relativeDirPath: paths.relativeDirPath,
46375
+ relativeFilePath: paths.relativeFilePath,
46376
+ fileContent,
46377
+ validate,
46378
+ global
46379
+ });
46380
+ }
46381
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
46382
+ const paths = this.getSettablePaths({ global });
46383
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
46384
+ const existingContent = await readFileContentOrNull(filePath) ?? "{}";
46385
+ const mcpServers = {};
46386
+ for (const [serverName, serverConfig] of Object.entries(rulesyncMcp.getMcpServers())) {
46387
+ const converted = convertServerToFirebenderFormat({
46388
+ serverName,
46389
+ serverConfig,
46390
+ logger
46391
+ });
46392
+ if (converted !== null) mcpServers[serverName] = converted;
46393
+ }
46394
+ return new FirebenderMcp({
46395
+ outputRoot,
46396
+ relativeDirPath: paths.relativeDirPath,
46397
+ relativeFilePath: paths.relativeFilePath,
46398
+ fileContent: applySharedConfigPatch({
46399
+ fileKey: sharedConfigFileKey(paths),
46400
+ feature: "mcp",
46401
+ existingContent,
46402
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
46403
+ filePath
46404
+ }),
46405
+ validate,
46406
+ global
46407
+ });
46408
+ }
46409
+ toRulesyncMcp() {
46410
+ const servers = this.json.mcpServers;
46411
+ const mcpServers = {};
46412
+ if (isMcpServers(servers)) {
46413
+ for (const [serverName, server] of Object.entries(servers)) if (server !== null && typeof server === "object" && !Array.isArray(server)) mcpServers[serverName] = server;
46414
+ }
46415
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
46416
+ }
46417
+ validate() {
46418
+ return {
46419
+ success: true,
46420
+ error: null
46421
+ };
46422
+ }
46423
+ /**
46424
+ * `firebender.json` is a user-managed file shared with other features (the
46425
+ * ignore feature's `ignore`, the subagents feature's `agents`), so it must
46426
+ * not be deleted.
46427
+ */
46428
+ isDeletable() {
46429
+ return false;
46430
+ }
46431
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
46432
+ return new FirebenderMcp({
46433
+ outputRoot,
46434
+ relativeDirPath,
46435
+ relativeFilePath,
46436
+ fileContent: "{}",
46437
+ validate: false,
46438
+ global
46439
+ });
46440
+ }
46441
+ };
46442
+ //#endregion
45320
46443
  //#region src/features/mcp/gitlabduo-mcp.ts
45321
46444
  const CANONICAL_TO_GITLABDUO_TRANSPORT = {
45322
46445
  local: "stdio",
@@ -46122,6 +47245,50 @@ function copyHermesOauth(source) {
46122
47245
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
46123
47246
  return Object.keys(oauth).length > 0 ? oauth : void 0;
46124
47247
  }
47248
+ const HERMES_COST_UNIT_MAX_LENGTH = 32;
47249
+ function describeHermesValue(value) {
47250
+ return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
47251
+ }
47252
+ /**
47253
+ * Copies the per-server MCP cost keys Hermes v0.22.3 reads in
47254
+ * `tools/mcp_tool_costs.py`: `tool_costs` (declared per-tool costs keyed by
47255
+ * MCP tool name; each a number, a `[min, max]` range or `{credits, note}`),
47256
+ * `spend_limit` (per-chat budget for the server's costed tools; `0`/absent =
47257
+ * no limit) and `cost_unit` (label shown with costs; default `credits`).
47258
+ *
47259
+ * A costed call that would exceed `spend_limit` asks first even on a
47260
+ * `trust: full` server, so dropping the key on regenerate would silently
47261
+ * remove the guard. Values Hermes would reject when loading the server are
47262
+ * dropped with a warning instead of being written into a config Hermes then
47263
+ * refuses: `spend_limit` must be a finite non-negative number, `cost_unit` a
47264
+ * non-blank string of at most 32 characters, and `tool_costs` a mapping. The
47265
+ * mapping is rebuilt as an opaque object (so no reference is shared with the
47266
+ * source), with prototype-pollution keys removed at every depth since its
47267
+ * values may themselves be objects.
47268
+ */
47269
+ function copyHermesCostFields({ source, target, logger }) {
47270
+ let copied = false;
47271
+ const { spend_limit: spendLimit, cost_unit: costUnit, tool_costs: toolCosts } = source;
47272
+ if (spendLimit !== void 0) {
47273
+ if (typeof spendLimit === "number" && Number.isFinite(spendLimit) && spendLimit >= 0) {
47274
+ target.spend_limit = spendLimit;
47275
+ copied = true;
47276
+ } else logger?.warn(`Hermes MCP: dropping spend_limit ${describeHermesValue(spendLimit)}; Hermes requires a non-negative number.`);
47277
+ }
47278
+ if (costUnit !== void 0) {
47279
+ if (typeof costUnit === "string" && costUnit.trim() !== "" && costUnit.length <= HERMES_COST_UNIT_MAX_LENGTH) {
47280
+ target.cost_unit = costUnit;
47281
+ copied = true;
47282
+ } else logger?.warn(`Hermes MCP: dropping cost_unit ${describeHermesValue(costUnit)}; Hermes requires a non-empty string of at most ${HERMES_COST_UNIT_MAX_LENGTH} characters.`);
47283
+ }
47284
+ if (toolCosts !== void 0) {
47285
+ if (isPlainObject$1(toolCosts)) {
47286
+ target.tool_costs = omitPrototypePollutionKeysDeep(toolCosts);
47287
+ copied = true;
47288
+ } else logger?.warn("Hermes MCP: dropping tool_costs; Hermes requires a mapping of MCP tool names to costs.");
47289
+ }
47290
+ return copied;
47291
+ }
46125
47292
  const HERMES_OPAQUE_MAPPING_KEYS = [
46126
47293
  "sampling",
46127
47294
  "elicitation",
@@ -46133,13 +47300,18 @@ const HERMES_OPAQUE_MAPPING_KEYS = [
46133
47300
  * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
46134
47301
  * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
46135
47302
  * `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
46136
- * `lazy`, `keepalive_interval`, `elicitation`, `trust`, and `identity_header` —
47303
+ * `lazy`, `keepalive_interval`, `elicitation`, `trust`, `identity_header`, and
47304
+ * the cost keys (see {@link copyHermesCostFields}) —
46137
47305
  * verbatim from `source` to `target`. Field names are identical on both sides
46138
47306
  * (the canonical `McpServerSchema` is a `looseObject`), so this serves export
46139
47307
  * and import alike. See the Hermes mcp-config-reference.
46140
47308
  */
46141
- function copyHermesAdvancedFields(source, target) {
46142
- let copied = false;
47309
+ function copyHermesAdvancedFields(source, target, logger) {
47310
+ let copied = copyHermesCostFields({
47311
+ source,
47312
+ target,
47313
+ logger
47314
+ });
46143
47315
  if (typeof source.auth === "string") {
46144
47316
  target.auth = source.auth;
46145
47317
  copied = true;
@@ -46248,7 +47420,7 @@ function applyHermesToolsBlock(hermesTools, server) {
46248
47420
  * as Hermes's own `transport: sse`, without which Hermes would connect to it
46249
47421
  * over Streamable HTTP.
46250
47422
  */
46251
- function convertServerToHermes(config) {
47423
+ function convertServerToHermes(config, logger) {
46252
47424
  const out = {};
46253
47425
  const command = config.command;
46254
47426
  const url = resolveHermesUrl(config);
@@ -46271,7 +47443,7 @@ function convertServerToHermes(config) {
46271
47443
  if (config.disabled === true) out.enabled = false;
46272
47444
  const timeout = resolveHermesTimeout(config);
46273
47445
  if (timeout !== void 0) out.timeout = timeout;
46274
- copyHermesAdvancedFields(config, out);
47446
+ copyHermesAdvancedFields(config, out, logger);
46275
47447
  const tools = buildHermesToolsBlock(config);
46276
47448
  if (Object.keys(tools).length > 0) out.tools = tools;
46277
47449
  return out;
@@ -46279,11 +47451,11 @@ function convertServerToHermes(config) {
46279
47451
  /**
46280
47452
  * Converts rulesync canonical MCP servers into Hermes `mcp_servers:` entries.
46281
47453
  */
46282
- function convertToHermesFormat(mcpServers) {
47454
+ function convertToHermesFormat(mcpServers, logger) {
46283
47455
  const result = {};
46284
47456
  for (const [name, config] of Object.entries(mcpServers)) {
46285
47457
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
46286
- result[name] = convertServerToHermes(config);
47458
+ result[name] = convertServerToHermes(config, logger);
46287
47459
  }
46288
47460
  return result;
46289
47461
  }
@@ -46303,7 +47475,7 @@ function mergeHermesMcpServers(config, mcpServers) {
46303
47475
  * Mirrors {@link convertToHermesFormat}: `enabled: false` maps back to the
46304
47476
  * canonical `disabled: true`, and only recognized fields are carried over.
46305
47477
  */
46306
- function convertFromHermesFormat(mcpServers) {
47478
+ function convertFromHermesFormat(mcpServers, logger) {
46307
47479
  const result = {};
46308
47480
  const hermesOverrides = {};
46309
47481
  for (const [name, config] of Object.entries(mcpServers)) {
@@ -46320,7 +47492,7 @@ function convertFromHermesFormat(mcpServers) {
46320
47492
  if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
46321
47493
  result[name] = server;
46322
47494
  const hermesServer = { ...server };
46323
- if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
47495
+ if (copyHermesAdvancedFields(config, hermesServer, logger)) hermesOverrides[name] = hermesServer;
46324
47496
  }
46325
47497
  return {
46326
47498
  mcpServers: result,
@@ -46339,8 +47511,10 @@ function convertFromHermesFormat(mcpServers) {
46339
47511
  */
46340
47512
  var HermesagentMcp = class HermesagentMcp extends ToolMcp {
46341
47513
  config;
46342
- constructor(params) {
47514
+ logger;
47515
+ constructor({ logger, ...params }) {
46343
47516
  super(params);
47517
+ this.logger = logger;
46344
47518
  this.config = this.fileContent !== void 0 ? parseSharedConfig({
46345
47519
  format: "yaml",
46346
47520
  fileContent: this.fileContent
@@ -46384,7 +47558,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
46384
47558
  static getExtraSharedWritePaths() {
46385
47559
  return getHermesagentSharedConfigWritePaths();
46386
47560
  }
46387
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
47561
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
46388
47562
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
46389
47563
  const paths = this.getSettablePaths({ global });
46390
47564
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
@@ -46394,17 +47568,18 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
46394
47568
  relativeFilePath: paths.relativeFilePath,
46395
47569
  fileContent,
46396
47570
  validate,
46397
- global
47571
+ global,
47572
+ logger
46398
47573
  });
46399
47574
  }
46400
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
47575
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
46401
47576
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
46402
47577
  const paths = this.getSettablePaths({ global });
46403
47578
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
46404
47579
  const merged = mergeHermesMcpServers(parseSharedConfig({
46405
47580
  format: "yaml",
46406
47581
  fileContent
46407
- }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
47582
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers(), logger));
46408
47583
  return new HermesagentMcp({
46409
47584
  outputRoot,
46410
47585
  relativeDirPath: paths.relativeDirPath,
@@ -46416,11 +47591,12 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
46416
47591
  patch: { mcp_servers: merged.mcp_servers }
46417
47592
  }),
46418
47593
  validate,
46419
- global
47594
+ global,
47595
+ logger
46420
47596
  });
46421
47597
  }
46422
47598
  toRulesyncMcp() {
46423
- const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
47599
+ const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {}, this.logger);
46424
47600
  return this.toRulesyncMcpDefault({
46425
47601
  outputRoot: getHermesagentRulesyncOutputRoot({
46426
47602
  nativeOutputRoot: this.outputRoot,
@@ -47304,14 +48480,15 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
47304
48480
  if (!config.parsed) return {};
47305
48481
  return {
47306
48482
  ...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
47307
- ...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms }
48483
+ ...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms },
48484
+ ...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred }
47308
48485
  };
47309
48486
  }
47310
48487
  /**
47311
48488
  * Kimi's `[mcp]` section in the shared user config sets the default connect and
47312
- * per-tool-call timeouts for *every* MCP server, including ones rulesync did not
47313
- * write. Per-server values in `mcp.json` still take precedence; this only moves
47314
- * the floor.
48489
+ * per-tool-call timeouts, and whether tools load on demand, for *every* MCP
48490
+ * server, including ones rulesync did not write. Per-server values in
48491
+ * `mcp.json` still take precedence; this only moves the default.
47315
48492
  *
47316
48493
  * Written as an auxiliary file so it goes through the normal write phase and
47317
48494
  * respects `--dry-run`, and merged through the shared-config gateway so the
@@ -47319,7 +48496,8 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
47319
48496
  *
47320
48497
  * The gateway replaces an owned key wholesale, and `mcp` is a table, so the
47321
48498
  * section is recomputed from the existing file: authoring only
47322
- * `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms`. Same
48499
+ * `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms` or
48500
+ * `default_deferred`. Same
47323
48501
  * approach the `.vibe/config.toml` `tools` writer takes.
47324
48502
  *
47325
48503
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
@@ -47346,7 +48524,8 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
47346
48524
  patch: { mcp: {
47347
48525
  ...existingSection,
47348
48526
  ...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
47349
- ...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs }
48527
+ ...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs },
48528
+ ...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred }
47350
48529
  } },
47351
48530
  filePath: relativeConfigPath
47352
48531
  });
@@ -47444,18 +48623,20 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
47444
48623
  if (!isRecord$1(block)) return [];
47445
48624
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
47446
48625
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
47447
- if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
48626
+ const defaultDeferred = typeof block.defaultDeferred === "boolean" ? block.defaultDeferred : void 0;
48627
+ if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0) return [];
47448
48628
  const existing = await readKimiCodeConfig({ outputRoot });
47449
48629
  if (!existing.parsed) {
47450
48630
  const paths = kimiCodeConfigRelativePath();
47451
- warnWithFallback(logger, `Skipping the Kimi Code MCP timeout defaults: ${join(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
48631
+ warnWithFallback(logger, `Skipping the Kimi Code MCP [mcp] defaults: ${join(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
47452
48632
  return [];
47453
48633
  }
47454
48634
  return [KimiCodeMcpConfigToml.fromDefaults({
47455
48635
  outputRoot,
47456
48636
  defaults: {
47457
48637
  startupTimeoutMs,
47458
- toolTimeoutMs
48638
+ toolTimeoutMs,
48639
+ defaultDeferred
47459
48640
  },
47460
48641
  existing
47461
48642
  })];
@@ -50031,6 +51212,11 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
50031
51212
  });
50032
51213
  }
50033
51214
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
51215
+ await warnIfReasonixNestedProjectConfigShadowed({
51216
+ outputRoot,
51217
+ global,
51218
+ logger
51219
+ });
50034
51220
  const paths = this.getSettablePaths({ global });
50035
51221
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50036
51222
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -52008,6 +53194,397 @@ var WarpcliMcp = class extends WarpMcp {
52008
53194
  }
52009
53195
  };
52010
53196
  //#endregion
53197
+ //#region src/constants/xum-paths.ts
53198
+ /**
53199
+ * Xum configuration-layout conventions.
53200
+ *
53201
+ * Xum (Coder's desktop app and `xum run` CLI for running coding agents in
53202
+ * parallel workspaces, formerly Mux) keeps its project assets under a `.xum/`
53203
+ * directory at the project root and the matching user assets under `~/.xum/`.
53204
+ * Legacy `.mux/` files are still read when the matching `.xum/` file is
53205
+ * missing; rulesync writes only the `.xum/` paths.
53206
+ *
53207
+ * @see https://xum.cdr.dev/reference/mux-compatibility
53208
+ */
53209
+ const XUM_DIR = ".xum";
53210
+ const XUM_RULE_FILE_NAME = "AGENTS.md";
53211
+ const XUM_LOCAL_RULE_FILE_NAME = "AGENTS.local.md";
53212
+ const XUM_MCP_FILE_NAME = "mcp.jsonc";
53213
+ const XUM_AGENTS_DIR_PATH = join(XUM_DIR, "agents");
53214
+ const XUM_SKILLS_DIR_PATH = join(XUM_DIR, "skills");
53215
+ //#endregion
53216
+ //#region src/features/mcp/xum-mcp.ts
53217
+ const EMPTY_XUM_MCP_CONFIG = JSON.stringify({ servers: {} }, null, 2);
53218
+ /** Characters that never need quoting in a POSIX shell word. */
53219
+ const SHELL_SAFE_WORD = /^[A-Za-z0-9_\-.,:/@%+=]+$/;
53220
+ /**
53221
+ * A command string free of quotes, escapes, expansions and operators, which
53222
+ * can be split on whitespace back into argv without changing its meaning.
53223
+ */
53224
+ const SPLIT_SAFE_COMMAND = /^[^'"\\$`|&;<>(){}*?[\]~#!\n]+$/;
53225
+ /** Single-quote one argv element for the POSIX shell Xum runs commands in. */
53226
+ function quoteShellWord(value) {
53227
+ if (SHELL_SAFE_WORD.test(value)) return value;
53228
+ return `'${value.replaceAll("'", `'\\''`)}'`;
53229
+ }
53230
+ /**
53231
+ * Build the shell command string a Xum stdio server is launched with. A
53232
+ * `command` string is kept verbatim (Xum itself treats it as a raw shell
53233
+ * string, so `npx -y pkg` authored as one string keeps working); an explicit
53234
+ * argv (`command` as an array) and every `args` element are quoted where
53235
+ * needed, so `/opt/my tools/srv` stays one word.
53236
+ */
53237
+ function buildCommandString(server) {
53238
+ const args = server.args ?? [];
53239
+ if (Array.isArray(server.command)) {
53240
+ const [executable] = server.command;
53241
+ if (executable === void 0 || executable.trim() === "") return void 0;
53242
+ return [...server.command, ...args].map(quoteShellWord).join(" ");
53243
+ }
53244
+ if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
53245
+ return [server.command, ...args.map(quoteShellWord)].join(" ");
53246
+ }
53247
+ /** A name bash accepts on the left of a `NAME=value` assignment. */
53248
+ const SHELL_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
53249
+ /** A canonical `${VAR}` reference whose name bash can expand. */
53250
+ const ENV_VAR_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
53251
+ /**
53252
+ * Quote an env value for a `NAME=value` assignment. Literal text is
53253
+ * single-quoted, so nothing in it is expanded or run, while every canonical
53254
+ * `${VAR}` reference is emitted as `"${VAR}"` so bash still expands it from
53255
+ * the environment Xum starts the server with. The pieces are concatenated into
53256
+ * one shell word: `Bearer ${TOKEN}` becomes `'Bearer '"${TOKEN}"`.
53257
+ */
53258
+ function quoteEnvValue(value) {
53259
+ if (value === "") return "''";
53260
+ const parts = [];
53261
+ let last = 0;
53262
+ for (const match of value.matchAll(ENV_VAR_REFERENCE)) {
53263
+ if (match.index > last) parts.push(quoteShellWord(value.slice(last, match.index)));
53264
+ parts.push(`"\${${match[1]}}"`);
53265
+ last = match.index + match[0].length;
53266
+ }
53267
+ if (last < value.length) parts.push(quoteShellWord(value.slice(last)));
53268
+ return parts.join("");
53269
+ }
53270
+ /**
53271
+ * Build the `NAME=value ` prefix that sets a stdio server's `env` for its
53272
+ * command only (Xum runs the command string with bash). Returns the prefix
53273
+ * plus the entries left out: names that cannot be bash assignment targets and
53274
+ * entries whose value is not a string.
53275
+ */
53276
+ function buildEnvPrefix(env) {
53277
+ const assignments = [];
53278
+ const invalidNames = [];
53279
+ const nonStringNames = [];
53280
+ for (const [name, value] of Object.entries(env)) {
53281
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53282
+ if (!SHELL_ENV_NAME.test(name)) {
53283
+ invalidNames.push(name);
53284
+ continue;
53285
+ }
53286
+ if (typeof value !== "string") {
53287
+ nonStringNames.push(name);
53288
+ continue;
53289
+ }
53290
+ assignments.push(`${name}=${quoteEnvValue(value)}`);
53291
+ }
53292
+ return {
53293
+ prefix: assignments.map((assignment) => `${assignment} `).join(""),
53294
+ invalidNames,
53295
+ nonStringNames
53296
+ };
53297
+ }
53298
+ /** Bash reserved words; a command starting with one cannot take an assignment prefix. */
53299
+ const BASH_RESERVED_WORDS = /* @__PURE__ */ new Set([
53300
+ "!",
53301
+ "[[",
53302
+ "]]",
53303
+ "{",
53304
+ "}",
53305
+ "case",
53306
+ "coproc",
53307
+ "do",
53308
+ "done",
53309
+ "elif",
53310
+ "else",
53311
+ "esac",
53312
+ "fi",
53313
+ "for",
53314
+ "function",
53315
+ "if",
53316
+ "in",
53317
+ "select",
53318
+ "then",
53319
+ "time",
53320
+ "until",
53321
+ "while"
53322
+ ]);
53323
+ /**
53324
+ * Explain why a `NAME=value` prefix would not reach the server, or return
53325
+ * `undefined` when it would. Bash applies such assignments to the first simple
53326
+ * command only: with a list or pipeline (`cd x && srv`) the server would not
53327
+ * see them, and in front of a group, parenthesized list or reserved word (`( srv )`,
53328
+ * `{ srv; }`, `if ...`) they are a syntax error that keeps the server from
53329
+ * starting. Only the verbatim `command` string is checked for operators,
53330
+ * outside complete single-quoted words; argv elements are quoted by
53331
+ * {@link quoteShellWord}.
53332
+ */
53333
+ function envPrefixBlocker(server, baseCommand) {
53334
+ if (typeof server.command === "string") {
53335
+ const unquoted = server.command.replace(/'[^']*'/g, "");
53336
+ if (unquoted.includes("'") || /[;&|(){}\n]/.test(unquoted)) return "its command string contains shell operators, grouping or a newline, so bash would not pass NAME=value assignments in front of it to the server";
53337
+ }
53338
+ const [firstWord] = baseCommand.trimStart().split(/\s+/);
53339
+ if (firstWord !== void 0 && BASH_RESERVED_WORDS.has(firstWord)) return `its command starts with the bash reserved word ${quoteValueForWarning(firstWord)}, which cannot follow NAME=value assignments`;
53340
+ }
53341
+ /**
53342
+ * One leading `NAME=value` assignment in exactly the shape {@link quoteEnvValue}
53343
+ * writes: bare shell-safe characters, single-quoted text (with the `'\''`
53344
+ * escape) and `"${VAR}"` references, followed by spaces or tabs (a newline
53345
+ * would end the command, so the assignment would not apply to what follows). Bare characters
53346
+ * are matched one at a time, so every alternative starts with a distinct
53347
+ * character and the pattern cannot backtrack exponentially.
53348
+ */
53349
+ const LEADING_ENV_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=((?:[A-Za-z0-9_\-.,:/@%+=]|'[^']*'|\\'|"\$\{[A-Za-z_][A-Za-z0-9_]*\}")*)[ \t]+/;
53350
+ /** Decode a value written by {@link quoteEnvValue} back into its canonical form. */
53351
+ function unquoteEnvValue(quoted) {
53352
+ return quoted.replace(/'([^']*)'|\\'|"(\$\{[A-Za-z_][A-Za-z0-9_]*\})"/g, (_m, single, ref) => typeof single === "string" ? single : typeof ref === "string" ? ref : "'");
53353
+ }
53354
+ /**
53355
+ * Split leading `NAME=value` assignments off a command string, but only when
53356
+ * every one of them is in the shape rulesync writes and a plain command
53357
+ * follows (see {@link envPrefixBlocker}); anything else (for example a `$(...)` substitution) keeps the whole string as
53358
+ * the command.
53359
+ */
53360
+ function splitEnvPrefix(command) {
53361
+ const env = {};
53362
+ let rest = command.trimStart();
53363
+ for (;;) {
53364
+ const match = LEADING_ENV_ASSIGNMENT.exec(rest);
53365
+ if (match === null) break;
53366
+ const [whole, name, quoted] = match;
53367
+ if (name === void 0 || quoted === void 0 || PROTOTYPE_POLLUTION_KEYS.has(name)) break;
53368
+ env[name] = unquoteEnvValue(quoted);
53369
+ rest = rest.slice(whole.length);
53370
+ }
53371
+ if (Object.keys(env).length === 0 || rest.trim() === "" || /^[A-Za-z_][A-Za-z0-9_]*=/.test(rest) || envPrefixBlocker({ command: rest }, rest) !== void 0) return { rest: command };
53372
+ return {
53373
+ env,
53374
+ rest
53375
+ };
53376
+ }
53377
+ /** Read a command string back into `env` + `command` + `args`. */
53378
+ function parseCommandString(command) {
53379
+ const { env, rest } = splitEnvPrefix(command);
53380
+ return {
53381
+ ...env !== void 0 && { env },
53382
+ ...splitCommandString(rest)
53383
+ };
53384
+ }
53385
+ /** Split a plain command string back into `command` + `args` when it is safe to. */
53386
+ function splitCommandString(command) {
53387
+ const trimmed = command.trim();
53388
+ if (!SPLIT_SAFE_COMMAND.test(trimmed)) return { command };
53389
+ const [head, ...rest] = trimmed.split(/\s+/);
53390
+ if (head === void 0) return { command };
53391
+ return rest.length > 0 ? {
53392
+ command: head,
53393
+ args: rest
53394
+ } : { command: head };
53395
+ }
53396
+ function resolveXumServerKind(server) {
53397
+ const candidate = server.type ?? server.transport;
53398
+ if (candidate === "stdio" || candidate === "local") return "stdio";
53399
+ if (candidate === "http" || candidate === "streamable-http") return "http";
53400
+ if (candidate === "sse") return "sse";
53401
+ if (candidate !== void 0) return { unsupported: candidate };
53402
+ if (server.command !== void 0) return "stdio";
53403
+ const url = server.url ?? server.httpUrl;
53404
+ if (typeof url === "string") return /^wss?:\/\//i.test(url) ? { unsupported: "ws" } : "auto";
53405
+ }
53406
+ /** Convert one canonical server into a Xum `servers` entry, or `null` to skip it. */
53407
+ function convertServerToXum({ name, server, logger }) {
53408
+ const skip = (reason) => {
53409
+ warnWithFallback(logger, `Xum MCP: skipping ${quoteValueForWarning(name)} because ${reason}.`);
53410
+ return null;
53411
+ };
53412
+ const kind = resolveXumServerKind(server);
53413
+ if (kind === void 0) return skip("it has neither a command nor a URL");
53414
+ if (typeof kind === "object") return skip(`it uses the "${kind.unsupported}" transport, which Xum does not support (only stdio, http and sse)`);
53415
+ const settings = {};
53416
+ if (server.disabled === true) settings.disabled = true;
53417
+ if (isStringArray$1(server.enabledTools)) settings.toolAllowlist = server.enabledTools;
53418
+ if (kind !== "stdio") {
53419
+ const url = server.url ?? server.httpUrl;
53420
+ if (typeof url !== "string" || url === "") return skip("it has no URL");
53421
+ return {
53422
+ ...kind !== "auto" && { transport: kind },
53423
+ url,
53424
+ ...isPlainObject$1(server.headers) && Object.keys(server.headers).length > 0 && { headers: server.headers },
53425
+ ...settings
53426
+ };
53427
+ }
53428
+ const baseCommand = buildCommandString(server);
53429
+ if (baseCommand === void 0) return skip("it has no command");
53430
+ let command = baseCommand;
53431
+ if (isPlainObject$1(server.env) && Object.keys(server.env).length > 0) {
53432
+ const blocker = envPrefixBlocker(server, baseCommand);
53433
+ if (blocker !== void 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env, but ${blocker}; the server is written without env.`);
53434
+ else {
53435
+ const { prefix, invalidNames, nonStringNames } = buildEnvPrefix(server.env);
53436
+ command = `${prefix}${baseCommand}`;
53437
+ if (invalidNames.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env ${invalidNames.map(quoteValueForWarning).join(", ")}, which cannot be a bash variable name; the server is written without it.`);
53438
+ if (nonStringNames.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env ${nonStringNames.map(quoteValueForWarning).join(", ")} to a value that is not a string; the server is written without it.`);
53439
+ }
53440
+ }
53441
+ if (typeof server.cwd === "string") warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets cwd, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers in the workspace directory).`);
53442
+ return Object.keys(settings).length > 0 ? {
53443
+ command,
53444
+ ...settings
53445
+ } : command;
53446
+ }
53447
+ /** Copy only the string values of a header map (secret references have no canonical form). */
53448
+ function copyStringHeaders(value) {
53449
+ if (!isPlainObject$1(value)) return void 0;
53450
+ const result = {};
53451
+ for (const [key, entry] of Object.entries(value)) if (!PROTOTYPE_POLLUTION_KEYS.has(key) && typeof entry === "string") result[key] = entry;
53452
+ return Object.keys(result).length > 0 ? result : void 0;
53453
+ }
53454
+ /** Read a Xum `servers` map back into the canonical shape. */
53455
+ function convertFromXumFormat(servers) {
53456
+ if (!isPlainObject$1(servers)) return {};
53457
+ const result = {};
53458
+ for (const [name, entry] of Object.entries(servers)) {
53459
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53460
+ if (typeof entry === "string") {
53461
+ result[name] = parseCommandString(entry);
53462
+ continue;
53463
+ }
53464
+ if (!isPlainObject$1(entry)) continue;
53465
+ const server = {};
53466
+ if (typeof entry.url === "string") {
53467
+ if (entry.transport === "http" || entry.transport === "sse") server.type = entry.transport;
53468
+ server.url = entry.url;
53469
+ const headers = copyStringHeaders(entry.headers);
53470
+ if (headers !== void 0) server.headers = headers;
53471
+ } else if (typeof entry.command === "string") Object.assign(server, parseCommandString(entry.command));
53472
+ else continue;
53473
+ if (entry.disabled === true) server.disabled = true;
53474
+ if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
53475
+ result[name] = server;
53476
+ }
53477
+ return result;
53478
+ }
53479
+ /**
53480
+ * Xum MCP configuration.
53481
+ *
53482
+ * Xum reads `~/.xum/mcp.jsonc` (user) and the repo override `.xum/mcp.jsonc`
53483
+ * (project; a server there wins over the user one of the same name). Both are
53484
+ * a JSONC document with a `servers` map:
53485
+ *
53486
+ * - a stdio server is a shell command string run with bash (the object form
53487
+ * `{ command }` carries `disabled` / `toolAllowlist`); `env` is written as
53488
+ * `NAME=value` assignments in front of the command, and `cwd`, which has no
53489
+ * equivalent, is dropped with a warning;
53490
+ * - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
53491
+ * (the transport defaults to auto-detection).
53492
+ *
53493
+ * rulesync owns only `servers`; other top-level keys (Xum writes
53494
+ * `enabledPluginServers` into the user file) are preserved. The project file
53495
+ * may be removed by `--delete`; the user file never is.
53496
+ *
53497
+ * @see https://xum.cdr.dev/config/mcp-servers
53498
+ * @see https://github.com/coder/xum/blob/main/src/node/services/mcpConfigService.ts
53499
+ */
53500
+ var XumMcp = class XumMcp extends ToolMcp {
53501
+ json;
53502
+ constructor(params) {
53503
+ super(params);
53504
+ this.json = parseSharedConfig({
53505
+ format: "jsonc",
53506
+ fileContent: this.fileContent,
53507
+ filePath: join(this.relativeDirPath, this.relativeFilePath),
53508
+ invalidRootPolicy: "error",
53509
+ jsoncParseErrors: "error"
53510
+ });
53511
+ }
53512
+ getJson() {
53513
+ return this.json;
53514
+ }
53515
+ isDeletable() {
53516
+ return !this.global;
53517
+ }
53518
+ static getSettablePaths(_options = {}) {
53519
+ return {
53520
+ relativeDirPath: XUM_DIR,
53521
+ relativeFilePath: XUM_MCP_FILE_NAME
53522
+ };
53523
+ }
53524
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
53525
+ const paths = this.getSettablePaths({ global });
53526
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? EMPTY_XUM_MCP_CONFIG;
53527
+ return new XumMcp({
53528
+ outputRoot,
53529
+ relativeDirPath: paths.relativeDirPath,
53530
+ relativeFilePath: paths.relativeFilePath,
53531
+ fileContent,
53532
+ validate,
53533
+ global
53534
+ });
53535
+ }
53536
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
53537
+ const paths = this.getSettablePaths({ global });
53538
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
53539
+ const existingContent = await readFileContentOrNull(filePath) ?? EMPTY_XUM_MCP_CONFIG;
53540
+ const servers = {};
53541
+ for (const [name, server] of Object.entries(rulesyncMcp.getMcpServers())) {
53542
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53543
+ const entry = convertServerToXum({
53544
+ name,
53545
+ server,
53546
+ logger
53547
+ });
53548
+ if (entry !== null) servers[name] = entry;
53549
+ }
53550
+ return new XumMcp({
53551
+ outputRoot,
53552
+ relativeDirPath: paths.relativeDirPath,
53553
+ relativeFilePath: paths.relativeFilePath,
53554
+ fileContent: applySharedConfigPatch({
53555
+ fileKey: sharedConfigFileKey(paths),
53556
+ feature: "mcp",
53557
+ existingContent,
53558
+ patch: { servers },
53559
+ filePath,
53560
+ logger
53561
+ }),
53562
+ validate,
53563
+ global
53564
+ });
53565
+ }
53566
+ toRulesyncMcp() {
53567
+ const mcpServers = convertFromXumFormat(this.json.servers);
53568
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
53569
+ }
53570
+ validate() {
53571
+ return {
53572
+ success: true,
53573
+ error: null
53574
+ };
53575
+ }
53576
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
53577
+ return new XumMcp({
53578
+ outputRoot,
53579
+ relativeDirPath,
53580
+ relativeFilePath,
53581
+ fileContent: "{}",
53582
+ validate: false,
53583
+ global
53584
+ });
53585
+ }
53586
+ };
53587
+ //#endregion
52011
53588
  //#region src/features/mcp/zcode-mcp.ts
52012
53589
  /**
52013
53590
  * Single spelling of the config.json codec/policy, matching the
@@ -52041,7 +53618,7 @@ function asZcodeRemoteType(stated, url) {
52041
53618
  }
52042
53619
  /**
52043
53620
  * The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
52044
- * remote server, and which of them are required. Read off the 3.14.3/3.15.3
53621
+ * remote server, and which of them are required. Read off the 3.14.3–3.16.2
52045
53622
  * runtime config schema, since ZCode's MCP page only says to "enable OAuth for
52046
53623
  * the service in its configuration". Both variants are strict, so any other
52047
53624
  * key would make ZCode skip the server.
@@ -52145,7 +53722,11 @@ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
52145
53722
  }
52146
53723
  /**
52147
53724
  * The per-server keys ZCode reads on every transport: `timeoutMs`,
52148
- * `protocolVersion` and the enable toggle (`enableKey`).
53725
+ * `protocolVersion`, `alwaysLoad` and the enable toggle (`enableKey`).
53726
+ *
53727
+ * `alwaysLoad` (added in 3.16.2 alongside tool search) keeps a server's tools
53728
+ * loaded up front instead of deferring them behind tool search. ZCode's schema
53729
+ * types it as a boolean, so any other value is dropped with a warning.
52149
53730
  */
52150
53731
  function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
52151
53732
  const common = {};
@@ -52158,6 +53739,9 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
52158
53739
  const { protocolVersion } = config;
52159
53740
  if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
52160
53741
  else if (protocolVersion !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping protocolVersion ${JSON.stringify(protocolVersion)}, as ZCode accepts only ${[...ZCODE_PROTOCOL_VERSIONS].map((v) => JSON.stringify(v)).join(", ")}.`);
53742
+ const { alwaysLoad } = config;
53743
+ if (typeof alwaysLoad === "boolean") common.alwaysLoad = alwaysLoad;
53744
+ else if (alwaysLoad !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping alwaysLoad ${JSON.stringify(alwaysLoad)}, as it must be a boolean.`);
52161
53745
  if (config.disabled === true) common[enableKey] = false;
52162
53746
  return common;
52163
53747
  }
@@ -52170,11 +53754,11 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
52170
53754
  * absent.
52171
53755
  *
52172
53756
  * Every server also carries `timeoutMs` (from the canonical `timeout`, or an
52173
- * explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
52174
- * carry `cwd`, and remote servers an `oauth` built from the namespaced
52175
- * `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
53757
+ * explicit `timeoutMs`), an allow-listed `protocolVersion` and a boolean
53758
+ * `alwaysLoad`; stdio servers carry `cwd`, and remote servers an `oauth` built
53759
+ * from the namespaced `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
52176
53760
  * is read from the authored server through `rawServerOf`. ZCode's docs do not
52177
- * name these keys, so they follow the 3.14.3/3.15.3 runtime's per-server
53761
+ * name these keys, so they follow the 3.14.3–3.16.2 runtime's per-server
52178
53762
  * schema, which is strict: an unknown key makes ZCode skip the server, which
52179
53763
  * is why the output stays a whitelist.
52180
53764
  *
@@ -52269,8 +53853,8 @@ const ZCODE_ENABLE_KEYS = /* @__PURE__ */ new Set(["enable", "enabled"]);
52269
53853
  * and unknown keys pass through untouched so an import keeps whatever a
52270
53854
  * hand-authored entry declared. `timeoutMs` is lifted back into the canonical
52271
53855
  * `timeout` (milliseconds); when the server also states a `timeout`, ZCode
52272
- * has already dropped that one, so `timeoutMs` replaces it. A ZCode-shaped
52273
- * `oauth` is imported as the namespaced `zcodeOauth`, so its client secret is
53856
+ * has already dropped that one, so `timeoutMs` replaces it. Any record
53857
+ * `oauth` is imported as the namespaced `zcodeOauth`, so a client secret is
52274
53858
  * never emitted to another tool. Generation back out is a whitelist, so only
52275
53859
  * the keys `convertToZcodeFormat` writes are re-emitted.
52276
53860
  *
@@ -52296,7 +53880,7 @@ function convertFromZcodeFormat({ zcodeServers }) {
52296
53880
  continue;
52297
53881
  }
52298
53882
  if (key === "timeout" && typeof config.timeoutMs === "number") continue;
52299
- if (key === "oauth" && isZcodeOauth(value)) {
53883
+ if (key === "oauth" && isRecord$1(value)) {
52300
53884
  converted[ZCODE_OAUTH_CANONICAL_KEY] = value;
52301
53885
  continue;
52302
53886
  }
@@ -52942,6 +54526,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
52942
54526
  supportsDisabledTools: true
52943
54527
  }
52944
54528
  }],
54529
+ ["firebender", {
54530
+ class: FirebenderMcp,
54531
+ meta: {
54532
+ supportsProject: true,
54533
+ supportsGlobal: true,
54534
+ supportsEnabledTools: false,
54535
+ supportsDisabledTools: false
54536
+ }
54537
+ }],
52945
54538
  ["goose", {
52946
54539
  class: GooseMcp,
52947
54540
  meta: {
@@ -53212,6 +54805,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
53212
54805
  supportsDisabledTools: false
53213
54806
  }
53214
54807
  }],
54808
+ ["xum", {
54809
+ class: XumMcp,
54810
+ meta: {
54811
+ supportsProject: true,
54812
+ supportsGlobal: true,
54813
+ supportsEnabledTools: true,
54814
+ supportsDisabledTools: false
54815
+ }
54816
+ }],
53215
54817
  ["devin", {
53216
54818
  class: DevinMcp,
53217
54819
  meta: {
@@ -57293,6 +58895,11 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
57293
58895
  reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
57294
58896
  widens: isNotFalse
57295
58897
  },
58898
+ {
58899
+ path: ["allowNestedUserNamespaces"],
58900
+ reason: "lets sandboxed Linux commands create user namespaces, which gets around the sandbox's file protections",
58901
+ widens: isNotFalse
58902
+ },
57296
58903
  {
57297
58904
  path: ["allowUnsandboxedCommands"],
57298
58905
  reason: "controls whether Claude may retry a blocked command outside the sandbox",
@@ -67742,7 +69349,7 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
67742
69349
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
67743
69350
  *
67744
69351
  * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
67745
- * `write` or `yolo`); without it the key is left as it is in the file, and on
69352
+ * `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
67746
69353
  * import a recognized value is read back into the override.
67747
69354
  *
67748
69355
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
@@ -70014,6 +71621,8 @@ function buildReasonixPermissionEntry(toolName, pattern) {
70014
71621
  const REASONIX_TOOL_LABEL = "Reasonix";
70015
71622
  /** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
70016
71623
  const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
71624
+ /** The `[permissions]` key the override's `askOnExternalOrigin` writes and reads. */
71625
+ const REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY = "ask_on_external_origin";
70017
71626
  /**
70018
71627
  * `[sandbox]` keys whose authored value loosens the enforcement layer beneath
70019
71628
  * the permission policy: they take Bash out of its OS jail, open that jail to
@@ -70216,6 +71825,95 @@ function applyPermissionsMode({ permissions, authored, global, relativeFilePath,
70216
71825
  reason: "lets every writer call that no rule matches run without a prompt"
70217
71826
  }];
70218
71827
  }
71828
+ /**
71829
+ * Writes the override's `askOnExternalOrigin` into `[permissions]`. Reasonix
71830
+ * honors the key only in the user config (`narrowPermissions` in
71831
+ * `project_scope.go`), so a project-scope generate skips it with a warning and
71832
+ * leaves whatever the file had. Turning it on only adds prompts, so that is
71833
+ * quiet; turning off a value the file had on is reported, since Auto and YOLO
71834
+ * then run calls that repeat text from a web page or tool result without asking.
71835
+ * As with `applyAllowDynamicBash`, only a literal `true` counts as quiet, so a
71836
+ * value a `--no-validate` run let through is never written in silence.
71837
+ *
71838
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/GUIDE.md
71839
+ */
71840
+ function applyAskOnExternalOrigin({ permissions, override, global, relativeFilePath, logger }) {
71841
+ const authored = override?.askOnExternalOrigin;
71842
+ if (authored === void 0) return [];
71843
+ if (!global) {
71844
+ logger?.warn(`Reasonix permissions: skipping reasonix.askOnExternalOrigin for the project-scoped ${relativeFilePath}; Reasonix honors [permissions] ask_on_external_origin only in the user config and ignores a project value, so generate it with --global instead.`);
71845
+ return [];
71846
+ }
71847
+ const previous = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
71848
+ permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY] = authored;
71849
+ if (authored === true || previous !== true && authored === false) return [];
71850
+ return [{
71851
+ label: `permissions.${REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY}`,
71852
+ reason: "lets Auto and YOLO run a call that repeats text from a web page or tool result without asking first"
71853
+ }];
71854
+ }
71855
+ /**
71856
+ * The `tools` patch entry — the `[tools]` table with the override's `disabled`
71857
+ * list laid over it, or nothing when the override does not author one — and the
71858
+ * trust rows it carries. Adding a name only removes a tool, and a project file
71859
+ * can only add to the user's list, so that stays quiet. The list is replaced
71860
+ * whole, though, so a name the file had turned off and the override leaves out
71861
+ * is turned back on, and that is reported, as are a `[tools]` or a `disabled`
71862
+ * the file holds in a shape that cannot be read. Only `disabled` is written:
71863
+ * the rest of `[tools]` (timeouts, search backends, ...) stays as it is.
71864
+ */
71865
+ function applyToolsDisabled({ existing, override }) {
71866
+ const disabled = override?.tools?.disabled;
71867
+ if (disabled === void 0) return {
71868
+ patch: {},
71869
+ entries: []
71870
+ };
71871
+ const patch = { tools: {
71872
+ ...asReasonixRecord(existing),
71873
+ disabled
71874
+ } };
71875
+ if (existing === void 0) return {
71876
+ patch,
71877
+ entries: []
71878
+ };
71879
+ if (!isRecord$1(existing)) return {
71880
+ patch,
71881
+ entries: [{
71882
+ label: "tools",
71883
+ reason: replacedUnreadableReason({
71884
+ shape: "object",
71885
+ toolLabel: REASONIX_TOOL_LABEL
71886
+ })
71887
+ }]
71888
+ };
71889
+ const before = existing.disabled;
71890
+ if (before === void 0) return {
71891
+ patch,
71892
+ entries: []
71893
+ };
71894
+ if (!Array.isArray(before)) return {
71895
+ patch,
71896
+ entries: [{
71897
+ label: "tools.disabled",
71898
+ reason: replacedUnreadableReason({
71899
+ shape: "list",
71900
+ toolLabel: REASONIX_TOOL_LABEL
71901
+ })
71902
+ }]
71903
+ };
71904
+ const reEnabled = uniq(before.filter((name) => !disabled.includes(name)));
71905
+ if (reEnabled.length === 0) return {
71906
+ patch,
71907
+ entries: []
71908
+ };
71909
+ return {
71910
+ patch,
71911
+ entries: [{
71912
+ label: "tools.disabled",
71913
+ reason: `turns back on ${reEnabled.map((name) => quoteValueForWarning(name)).join(", ")}, which the file had turned off`
71914
+ }]
71915
+ };
71916
+ }
70219
71917
  function parseReasonixConfig(fileContent) {
70220
71918
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
70221
71919
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -70271,6 +71969,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
70271
71969
  });
70272
71970
  }
70273
71971
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
71972
+ await warnIfReasonixNestedProjectConfigShadowed({
71973
+ outputRoot,
71974
+ global,
71975
+ logger
71976
+ });
70274
71977
  const paths = this.getSettablePaths({ global });
70275
71978
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
70276
71979
  const relativeFilePathForLog = toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath));
@@ -70330,7 +72033,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
70330
72033
  relativeFilePath: relativeFilePathForLog,
70331
72034
  logger
70332
72035
  }));
72036
+ trustAffecting.push(...applyAskOnExternalOrigin({
72037
+ permissions: mergedPermissions,
72038
+ override,
72039
+ global,
72040
+ relativeFilePath: relativeFilePathForLog,
72041
+ logger
72042
+ }));
70333
72043
  const patch = { permissions: mergedPermissions };
72044
+ const toolsDisabled = applyToolsDisabled({
72045
+ existing: parsed.tools,
72046
+ override
72047
+ });
72048
+ Object.assign(patch, toolsDisabled.patch);
72049
+ trustAffecting.push(...toolsDisabled.entries);
70334
72050
  if (override?.sandbox !== void 0) {
70335
72051
  const authoredSandbox = dropUserOnlySandboxKeys({
70336
72052
  sandbox: override.sandbox,
@@ -70404,6 +72120,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
70404
72120
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
70405
72121
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
70406
72122
  if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
72123
+ const askOnExternalOrigin = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
72124
+ if (this.global && typeof askOnExternalOrigin === "boolean") reasonixOverride.askOnExternalOrigin = askOnExternalOrigin;
72125
+ const toolsDisabled = asReasonixRecord(this.toml.tools).disabled;
72126
+ if (Array.isArray(toolsDisabled)) {
72127
+ const names = toReasonixStringArray(toolsDisabled);
72128
+ if (names.length < toolsDisabled.length) warnWithFallback(void 0, `Reasonix permissions: ${this.getRelativePathFromCwd()} [tools] disabled holds ${toolsDisabled.length - names.length} non-string entries; they are not imported into reasonix.tools.disabled, so the next generate drops them.`);
72129
+ reasonixOverride.tools = { disabled: names };
72130
+ }
70407
72131
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
70408
72132
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
70409
72133
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -71985,7 +73709,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
71985
73709
  const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
71986
73710
  const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
71987
73711
  if (runtimeModeRefusesProviderOptions) logger?.warn(`Takt permissions: not writing "${TAKT_PROVIDER_OPTIONS_KEY}" to ${filePath} because ${runtime.filePaths.join(" + ")} puts Takt in runtime provider mode (Takt 0.56.0+), where any legacy provider setting in config.yaml makes Takt fail with "Mixed provider configuration detected". Move those options to \`provider.profiles.<profile>.options\` in runtime.yaml, and remove them from the \`takt\` block of the rulesync source. A "${TAKT_PROVIDER_OPTIONS_KEY}" already in ${filePath} is left untouched — rulesync does not own that key.`);
71988
- const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
73712
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : dropUntrustedProviderOptions({
73713
+ providerOptions: authoredProviderOptions,
73714
+ global,
73715
+ filePath,
73716
+ logger
73717
+ });
71989
73718
  const authoredPolicies = pickSecurityPolicies(override, {
71990
73719
  filePath,
71991
73720
  logger,
@@ -72489,6 +74218,68 @@ function taktModeToRulesyncConfig(mode) {
72489
74218
  default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
72490
74219
  }
72491
74220
  }
74221
+ const TAKT_BASE_URL_PROVIDERS = [
74222
+ "codex",
74223
+ "claude",
74224
+ "deepseek_harness"
74225
+ ];
74226
+ const TAKT_DEEPSEEK_HARNESS_KEY = "deepseek_harness";
74227
+ /** Mirrors Takt's `isLoopbackBaseUrl`: localhost, *.localhost, ::1 or 127.x.x.x. */
74228
+ function isLoopbackBaseUrl(value) {
74229
+ let hostname;
74230
+ try {
74231
+ hostname = new URL(value).hostname.toLowerCase();
74232
+ } catch {
74233
+ return false;
74234
+ }
74235
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "[::1]") return true;
74236
+ const octets = hostname.split(".");
74237
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d+$/.test(octet) && Number(octet) <= 255);
74238
+ }
74239
+ /** The keys of one `provider_options.<provider>` entry Takt rejects in config.yaml. */
74240
+ function untrustedProviderOptionKeys({ provider, options, global }) {
74241
+ const keys = [];
74242
+ if (provider === TAKT_DEEPSEEK_HARNESS_KEY) {
74243
+ if (options.reasoning_effort !== void 0) keys.push("reasoning_effort");
74244
+ const { plugins } = options;
74245
+ if (!global && plugins !== void 0 && (!Array.isArray(plugins) || plugins.length > 0)) keys.push("plugins");
74246
+ }
74247
+ const baseUrl = options.base_url;
74248
+ if (!global && TAKT_BASE_URL_PROVIDERS.includes(provider) && baseUrl !== void 0 && (typeof baseUrl !== "string" || !isLoopbackBaseUrl(baseUrl))) keys.push("base_url");
74249
+ return keys;
74250
+ }
74251
+ /**
74252
+ * Drop the `provider_options` entries Takt refuses to load from `config.yaml`
74253
+ * (it fails at startup with a `Configuration error`), warning for each:
74254
+ * - `deepseek_harness.reasoning_effort` in either scope — Takt accepts it only
74255
+ * in runtime profile options or `TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT`.
74256
+ * - at project scope, a non-empty `deepseek_harness.plugins` (Takt 0.71.0+,
74257
+ * global config only) and a non-loopback `{codex,claude,deepseek_harness}.base_url`.
74258
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/providerOptions.ts
74259
+ * https://github.com/nrslib/takt/blob/main/docs/configuration.md
74260
+ */
74261
+ function dropUntrustedProviderOptions({ providerOptions, global, filePath, logger }) {
74262
+ if (providerOptions === void 0) return;
74263
+ const dropped = [];
74264
+ const result = {};
74265
+ for (const [provider, options] of Object.entries(providerOptions)) {
74266
+ if (!isPlainObject$1(options)) {
74267
+ result[provider] = options;
74268
+ continue;
74269
+ }
74270
+ const untrusted = untrustedProviderOptionKeys({
74271
+ provider,
74272
+ options,
74273
+ global
74274
+ });
74275
+ const kept = Object.fromEntries(Object.entries(options).filter(([key]) => !untrusted.includes(key)));
74276
+ dropped.push(...untrusted.map((key) => `${TAKT_PROVIDER_OPTIONS_KEY}.${provider}.${key}`));
74277
+ if (Object.keys(kept).length > 0 || Object.keys(options).length === 0) result[provider] = kept;
74278
+ }
74279
+ if (dropped.length === 0) return providerOptions;
74280
+ logger?.warn(`Takt permissions: not writing ${dropped.map((key) => `"${key}"`).join(", ")} to ${filePath} because Takt refuses to load ${dropped.length === 1 ? "it" : "them"} from ${global ? "config.yaml" : "a project config.yaml"} and fails at startup with a "Configuration error". Set non-loopback base URLs and DeepSeek Harness plugins in the global ~/.takt/config.yaml (or the TAKT_PROVIDER_OPTIONS_* environment variables), and DeepSeek Harness reasoning_effort in a runtime.yaml profile's options or TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT, then remove them from the \`takt\` block of the rulesync source.`);
74281
+ return Object.keys(result).length > 0 ? result : void 0;
74282
+ }
72492
74283
  /**
72493
74284
  * Lift Takt's security-policy keys out of a source object, keeping only the
72494
74285
  * shapes Takt itself accepts (a boolean, or a table of booleans). Used in both
@@ -76423,6 +78214,11 @@ const TOOL_DISPLAY = [
76423
78214
  label: "Factory Droid",
76424
78215
  group: "ai"
76425
78216
  },
78217
+ {
78218
+ key: "firebender",
78219
+ label: "Firebender",
78220
+ group: "ai"
78221
+ },
76426
78222
  {
76427
78223
  key: "opencode",
76428
78224
  label: "OpenCode",
@@ -76578,6 +78374,11 @@ const TOOL_DISPLAY = [
76578
78374
  label: "JetBrains AI Assistant",
76579
78375
  group: "ai"
76580
78376
  },
78377
+ {
78378
+ key: "androidstudio",
78379
+ label: "Gemini in Android Studio",
78380
+ group: "ai"
78381
+ },
76581
78382
  {
76582
78383
  key: "junie",
76583
78384
  label: "JetBrains Junie",
@@ -76613,6 +78414,11 @@ const TOOL_DISPLAY = [
76613
78414
  label: "Warp Agent CLI",
76614
78415
  group: "ai"
76615
78416
  },
78417
+ {
78418
+ key: "xum",
78419
+ label: "Xum",
78420
+ group: "ai"
78421
+ },
76616
78422
  {
76617
78423
  key: "replit",
76618
78424
  label: "Replit",
@@ -77757,6 +79563,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
77757
79563
  }
77758
79564
  };
77759
79565
  //#endregion
79566
+ //#region src/features/skills/androidstudio-skill.ts
79567
+ /**
79568
+ * Gemini in Android Studio skills follow the Agent Skills open standard
79569
+ * (`<name>/SKILL.md` with `name` and `description` frontmatter), so this target
79570
+ * reuses {@link AgentsSkillsSkill} with Android Studio's own location:
79571
+ * `.android-studio/skills/` at the project root (project) or in the home
79572
+ * directory (global). The shared `.agents/skills/` location Android Studio also
79573
+ * reads belongs to the `agentsskills` target.
79574
+ *
79575
+ * @see https://developer.android.com/studio/gemini/skills
79576
+ */
79577
+ var AndroidstudioSkill = class extends AgentsSkillsSkill {
79578
+ static getSettablePaths(_options = {}) {
79579
+ return { relativeDirPath: ANDROIDSTUDIO_SKILLS_DIR_PATH };
79580
+ }
79581
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
79582
+ const targets = rulesyncSkill.getFrontmatter().targets;
79583
+ return targets.includes("*") || targets.includes("androidstudio");
79584
+ }
79585
+ };
79586
+ //#endregion
77760
79587
  //#region src/features/skills/antigravity-plugin-skill.ts
77761
79588
  var AntigravityPluginSkill = class extends AntigravityIdeSkill {
77762
79589
  static isTargetedByRulesyncSkill(rulesyncSkill) {
@@ -81225,6 +83052,26 @@ var FactorydroidSkill = class FactorydroidSkill extends ToolSkill {
81225
83052
  }
81226
83053
  };
81227
83054
  //#endregion
83055
+ //#region src/features/skills/firebender-skill.ts
83056
+ /**
83057
+ * Firebender skills follow the Agent Skills layout (`<name>/SKILL.md` with the
83058
+ * required `name` and `description` frontmatter, plus optional supporting
83059
+ * files), so this target reuses {@link AgentsSkillsSkill} with Firebender's
83060
+ * recommended locations: `.firebender/skills/` (project) and
83061
+ * `~/.firebender/skills/` (user).
83062
+ *
83063
+ * @see https://docs.firebender.com/api-reference/skills
83064
+ */
83065
+ var FirebenderSkill = class extends AgentsSkillsSkill {
83066
+ static getSettablePaths(_options = {}) {
83067
+ return { relativeDirPath: FIREBENDER_SKILLS_DIR_PATH };
83068
+ }
83069
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
83070
+ const targets = rulesyncSkill.getFrontmatter().targets;
83071
+ return targets.includes("*") || targets.includes("firebender");
83072
+ }
83073
+ };
83074
+ //#endregion
81228
83075
  //#region src/features/skills/gitlabduo-skill.ts
81229
83076
  /**
81230
83077
  * GitLab Duo skills follow the Agent Skills standard (`name` and `description`
@@ -84760,6 +86607,110 @@ var WarpSkill = class WarpSkill extends ToolSkill {
84760
86607
  }
84761
86608
  };
84762
86609
  //#endregion
86610
+ //#region src/features/skills/xum-skill.ts
86611
+ /**
86612
+ * The Agent Skills standard keys the `agentsskills` section carries. On import
86613
+ * they go back there, exactly as for the `agentsskills` target, and every other
86614
+ * key besides `name` / `description` goes into the `xum` section.
86615
+ */
86616
+ const AGENT_SKILLS_STANDARD_KEYS = /* @__PURE__ */ new Set([
86617
+ "name",
86618
+ "description",
86619
+ "license",
86620
+ "compatibility",
86621
+ "metadata",
86622
+ "allowed-tools"
86623
+ ]);
86624
+ /**
86625
+ * Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
86626
+ * required `name` and `description` frontmatter; the directory name is
86627
+ * lowercase letters, digits and single hyphens up to 64 characters, and
86628
+ * `name` must match it), so this target reuses {@link AgentsSkillsSkill} with
86629
+ * Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
86630
+ * Within one directory `.xum/skills` wins over `.agents/skills`.
86631
+ *
86632
+ * On top of the standard fields, Xum reads `disable-model-invocation` (and its
86633
+ * own `advertise: false` alias), `user-invocable`, `argument-hint`,
86634
+ * `when_to_use` / `when-to-use` and `paths` (globs that make the skill
86635
+ * path-scoped). Those come from the `xum` section of the rulesync skill and are
86636
+ * written verbatim; the two invocation gates fall back to the root-level
86637
+ * rulesync values.
86638
+ *
86639
+ * @see https://xum.cdr.dev/agents/agent-skills
86640
+ */
86641
+ var XumSkill = class XumSkill extends AgentsSkillsSkill {
86642
+ static getSettablePaths(_options = {}) {
86643
+ return { relativeDirPath: XUM_SKILLS_DIR_PATH };
86644
+ }
86645
+ toRulesyncSkill() {
86646
+ const base = super.toRulesyncSkill();
86647
+ const xumSection = Object.fromEntries(Object.entries(this.getFrontmatter()).filter(([key]) => !AGENT_SKILLS_STANDARD_KEYS.has(key)));
86648
+ if (Object.keys(xumSection).length === 0) return base;
86649
+ const rulesyncFrontmatter = {
86650
+ ...base.getFrontmatter(),
86651
+ xum: xumSection
86652
+ };
86653
+ return new RulesyncSkill({
86654
+ outputRoot: base.getOutputRoot(),
86655
+ relativeDirPath: base.getRelativeDirPath(),
86656
+ dirName: base.getDirName(),
86657
+ frontmatter: rulesyncFrontmatter,
86658
+ body: base.getBody(),
86659
+ otherFiles: base.getOtherFiles(),
86660
+ validate: true,
86661
+ global: this.global
86662
+ });
86663
+ }
86664
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
86665
+ const settablePaths = XumSkill.getSettablePaths({ global });
86666
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
86667
+ const dirName = rulesyncSkill.getDirName();
86668
+ const xumSection = {};
86669
+ const ignoredKeys = [];
86670
+ for (const [key, value] of Object.entries(rulesyncFrontmatter.xum ?? {})) if (AGENT_SKILLS_STANDARD_KEYS.has(key)) ignoredKeys.push(key);
86671
+ else xumSection[key] = value;
86672
+ if (ignoredKeys.length > 0) warnWithFallback(logger, `${join(outputRoot, settablePaths.relativeDirPath, dirName, SKILL_FILE_NAME)}: the xum section sets ${ignoredKeys.map(quoteValueForWarning).join(", ")}, which ${ignoredKeys.length === 1 ? "is" : "are"} ignored there; set name/description at the root and the Agent Skills standard fields at the root or in the agentsskills section.`);
86673
+ const userInvocable = resolveUserInvocable({
86674
+ rootFrontmatter: rulesyncFrontmatter,
86675
+ section: xumSection
86676
+ });
86677
+ const disableModelInvocation = resolveDisableModelInvocation({
86678
+ rootFrontmatter: rulesyncFrontmatter,
86679
+ section: xumSection
86680
+ });
86681
+ const frontmatter = {
86682
+ name: rulesyncFrontmatter.name,
86683
+ description: rulesyncFrontmatter.description,
86684
+ ...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter)),
86685
+ ...xumSection,
86686
+ ...userInvocable !== void 0 && { "user-invocable": userInvocable },
86687
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
86688
+ };
86689
+ AgentsSkillsSkill.reportSpecViolations({
86690
+ outputRoot,
86691
+ relativeDirPath: settablePaths.relativeDirPath,
86692
+ dirName,
86693
+ frontmatter,
86694
+ sourceAllowedTools: rulesyncFrontmatter.agentsskills?.["allowed-tools"],
86695
+ logger
86696
+ });
86697
+ return new XumSkill({
86698
+ outputRoot,
86699
+ relativeDirPath: settablePaths.relativeDirPath,
86700
+ dirName,
86701
+ frontmatter,
86702
+ body: rulesyncSkill.getBody(),
86703
+ otherFiles: rulesyncSkill.getOtherFiles(),
86704
+ validate,
86705
+ global
86706
+ });
86707
+ }
86708
+ static isTargetedByRulesyncSkill(rulesyncSkill) {
86709
+ const targets = rulesyncSkill.getFrontmatter().targets;
86710
+ return targets.includes("*") || targets.includes("xum");
86711
+ }
86712
+ };
86713
+ //#endregion
84763
86714
  //#region src/features/skills/zcode-skill.ts
84764
86715
  const ZcodeSkillFrontmatterSchema = z.looseObject({
84765
86716
  name: z.string(),
@@ -85175,6 +87126,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85175
87126
  supportsGlobal: false
85176
87127
  }
85177
87128
  }],
87129
+ ["androidstudio", {
87130
+ class: AndroidstudioSkill,
87131
+ meta: {
87132
+ supportsProject: true,
87133
+ supportsSimulated: false,
87134
+ supportsGlobal: true
87135
+ }
87136
+ }],
85178
87137
  ["amp", {
85179
87138
  class: AmpSkill,
85180
87139
  meta: {
@@ -85360,6 +87319,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85360
87319
  supportsGlobal: true
85361
87320
  }
85362
87321
  }],
87322
+ ["firebender", {
87323
+ class: FirebenderSkill,
87324
+ meta: {
87325
+ supportsProject: true,
87326
+ supportsSimulated: false,
87327
+ supportsGlobal: true
87328
+ }
87329
+ }],
85363
87330
  ["goose", {
85364
87331
  class: GooseSkill,
85365
87332
  meta: {
@@ -85610,6 +87577,14 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
85610
87577
  supportsGlobal: true
85611
87578
  }
85612
87579
  }],
87580
+ ["xum", {
87581
+ class: XumSkill,
87582
+ meta: {
87583
+ supportsProject: true,
87584
+ supportsSimulated: false,
87585
+ supportsGlobal: true
87586
+ }
87587
+ }],
85613
87588
  ["devin", {
85614
87589
  class: DevinSkill,
85615
87590
  meta: {
@@ -89456,6 +91431,338 @@ var FactorydroidSubagent = class FactorydroidSubagent extends ToolSubagent {
89456
91431
  }
89457
91432
  };
89458
91433
  //#endregion
91434
+ //#region src/features/subagents/firebender-subagent.ts
91435
+ /**
91436
+ * Frontmatter of a Firebender agent file. `tools` is a comma-separated string
91437
+ * of tool groups (`read`, `edit`, `execution`, `other`) and MCP tools
91438
+ * (`mcp_<server>_<tool>`); `model` is `small`, `medium`, `large`, `inherit` or a
91439
+ * model ID; `callable` lets the main agent delegate to it. Unknown keys are
91440
+ * preserved.
91441
+ *
91442
+ * @see https://docs.firebender.com/api-reference/agents
91443
+ */
91444
+ const FirebenderSubagentFrontmatterSchema = z.looseObject({
91445
+ name: z.optional(z.string()),
91446
+ description: z.optional(z.string()),
91447
+ color: z.optional(z.string()),
91448
+ icon: z.optional(z.string()),
91449
+ tools: z.optional(z.string()),
91450
+ model: z.optional(z.string()),
91451
+ callable: z.optional(z.boolean())
91452
+ });
91453
+ /**
91454
+ * The `agents` entry Firebender resolves to an agent file rulesync writes:
91455
+ * relative to the project root for the project config, and to `~/.firebender`
91456
+ * for the personal one.
91457
+ */
91458
+ function toAgentsEntry({ relativeFilePath, global }) {
91459
+ const fileName = toPosixPath(relativeFilePath);
91460
+ return global ? posix.join(FIREBENDER_AGENTS_DIR_NAME, fileName) : posix.join(toPosixPath(FIREBENDER_AGENTS_DIR_PATH), fileName);
91461
+ }
91462
+ /**
91463
+ * The agent file name an existing `agents` entry points at when it lies
91464
+ * directly inside the directory rulesync writes agents to
91465
+ * (`.firebender/agents/` in the project; `agents/` or `~/.firebender/agents/`
91466
+ * in the personal config), or `undefined` for any other entry
91467
+ * (`~/firebender-agents/...`, an absolute path, a nested path).
91468
+ */
91469
+ function managedAgentFileName({ entry, global }) {
91470
+ const normalized = toPosixPath(entry).replace(/^\.\//, "");
91471
+ const prefix = global ? `${FIREBENDER_AGENTS_DIR_NAME}/` : `${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
91472
+ const homePrefix = `~/${toPosixPath(FIREBENDER_AGENTS_DIR_PATH)}/`;
91473
+ const rest = normalized.startsWith(prefix) ? normalized.slice(prefix.length) : global && normalized.startsWith(homePrefix) ? normalized.slice(homePrefix.length) : void 0;
91474
+ return rest !== void 0 && rest.endsWith(".md") && !rest.includes("/") ? rest : void 0;
91475
+ }
91476
+ /**
91477
+ * Subagent generator for Firebender.
91478
+ *
91479
+ * Each agent is a Markdown file with YAML frontmatter, written to
91480
+ * `.firebender/agents/<name>.md` (project) or `~/.firebender/agents/<name>.md`
91481
+ * (global). Firebender only loads agents listed in the `agents` array of
91482
+ * `firebender.json`, so generation also registers every file there — see
91483
+ * {@link FirebenderAgentsRegistry}. Rulesync subagents are meant to be
91484
+ * delegated to, so `callable: true` is written unless the `firebender:`
91485
+ * section says otherwise.
91486
+ *
91487
+ * @see https://docs.firebender.com/api-reference/agents
91488
+ */
91489
+ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
91490
+ frontmatter;
91491
+ body;
91492
+ constructor({ frontmatter, body, ...rest }) {
91493
+ if (rest.validate !== false) {
91494
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
91495
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
91496
+ }
91497
+ super({ ...rest });
91498
+ this.frontmatter = frontmatter;
91499
+ this.body = body;
91500
+ }
91501
+ static getSettablePaths(_options = {}) {
91502
+ return { relativeDirPath: FIREBENDER_AGENTS_DIR_PATH };
91503
+ }
91504
+ /**
91505
+ * Generation also read-modify-writes the `agents` key of `firebender.json`,
91506
+ * so the write is declared for the shared-file order derivation.
91507
+ */
91508
+ static getExtraSharedWritePaths({ global = false } = {}) {
91509
+ return [{
91510
+ relativeDirPath: getFirebenderConfigDir({ global }),
91511
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME
91512
+ }];
91513
+ }
91514
+ getFrontmatter() {
91515
+ return this.frontmatter;
91516
+ }
91517
+ getBody() {
91518
+ return this.body;
91519
+ }
91520
+ toRulesyncSubagent() {
91521
+ const { name, description, callable, ...rest } = this.frontmatter;
91522
+ const firebenderSection = {
91523
+ ...rest,
91524
+ ...callable !== true && { callable: callable ?? false }
91525
+ };
91526
+ return new RulesyncSubagent({
91527
+ outputRoot: ".",
91528
+ frontmatter: {
91529
+ targets: ["*"],
91530
+ name: name ?? basename(this.getRelativeFilePath(), ".md"),
91531
+ description,
91532
+ ...Object.keys(firebenderSection).length > 0 && { firebender: firebenderSection }
91533
+ },
91534
+ body: this.body,
91535
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
91536
+ relativeFilePath: this.getRelativeFilePath(),
91537
+ validate: true
91538
+ });
91539
+ }
91540
+ /**
91541
+ * Every targeted subagent becomes its own agent file, plus one
91542
+ * {@link FirebenderAgentsRegistry} that lists them in `firebender.json`.
91543
+ * The registry is emitted even when no subagent is targeted, so agents a
91544
+ * previous run registered are retracted once their files are gone. The
91545
+ * agent directory is listed up front so the registry can tell a registered
91546
+ * file that still exists (kept, whoever wrote it) from a stale one.
91547
+ */
91548
+ static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
91549
+ const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
91550
+ const agentsDirEscapesRoot = await writablePathEscapesRoot({
91551
+ rootPath: outputRoot,
91552
+ targetPath: join(outputRoot, agentsDirPath, "agent.md")
91553
+ });
91554
+ const existingAgentFileNames = new Set(agentsDirEscapesRoot ? [] : await listDirectoryEntryNames(join(outputRoot, agentsDirPath)));
91555
+ const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
91556
+ outputRoot,
91557
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
91558
+ rulesyncSubagent,
91559
+ global
91560
+ }));
91561
+ const registry = new FirebenderAgentsRegistry({
91562
+ outputRoot,
91563
+ relativeDirPath: getFirebenderConfigDir({ global }),
91564
+ relativeFilePath: FIREBENDER_CONFIG_FILE_NAME,
91565
+ entries: subagents.map((subagent) => toAgentsEntry({
91566
+ relativeFilePath: subagent.getRelativeFilePath(),
91567
+ global
91568
+ })),
91569
+ existingAgentFileNames,
91570
+ deleteOrphans,
91571
+ frozen: agentsDirEscapesRoot,
91572
+ global,
91573
+ logger
91574
+ });
91575
+ return [...subagents, registry];
91576
+ }
91577
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
91578
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
91579
+ const firebenderSection = rulesyncFrontmatter.firebender ?? {};
91580
+ const frontmatter = {
91581
+ name: rulesyncFrontmatter.name,
91582
+ ...rulesyncFrontmatter.description !== void 0 && { description: rulesyncFrontmatter.description },
91583
+ callable: true,
91584
+ ...firebenderSection
91585
+ };
91586
+ const body = rulesyncSubagent.getBody();
91587
+ return new FirebenderSubagent({
91588
+ outputRoot,
91589
+ frontmatter,
91590
+ body,
91591
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
91592
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
91593
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true }),
91594
+ validate,
91595
+ global
91596
+ });
91597
+ }
91598
+ validate() {
91599
+ if (!this.frontmatter) return {
91600
+ success: true,
91601
+ error: null
91602
+ };
91603
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(this.frontmatter);
91604
+ if (result.success) return {
91605
+ success: true,
91606
+ error: null
91607
+ };
91608
+ return {
91609
+ success: false,
91610
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
91611
+ };
91612
+ }
91613
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
91614
+ return this.isTargetedByRulesyncSubagentDefault({
91615
+ rulesyncSubagent,
91616
+ toolTarget: "firebender"
91617
+ });
91618
+ }
91619
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
91620
+ const paths = this.getSettablePaths({ global });
91621
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
91622
+ const fileContent = await readFileContent(filePath);
91623
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
91624
+ const result = FirebenderSubagentFrontmatterSchema.safeParse(frontmatter);
91625
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
91626
+ return new FirebenderSubagent({
91627
+ outputRoot,
91628
+ relativeDirPath: paths.relativeDirPath,
91629
+ relativeFilePath,
91630
+ frontmatter: result.data,
91631
+ body: body.trim(),
91632
+ fileContent,
91633
+ validate,
91634
+ global
91635
+ });
91636
+ }
91637
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
91638
+ return new FirebenderSubagent({
91639
+ outputRoot,
91640
+ relativeDirPath,
91641
+ relativeFilePath,
91642
+ frontmatter: {},
91643
+ body: "",
91644
+ fileContent: "",
91645
+ validate: false
91646
+ });
91647
+ }
91648
+ };
91649
+ /**
91650
+ * The `agents` array of `firebender.json`, which Firebender requires to load
91651
+ * an agent file. Every generated agent is registered. An existing entry that
91652
+ * points into rulesync's agent directory is kept while its file exists (a
91653
+ * hand-written agent there stays registered) and dropped, with a warning, once
91654
+ * the file is gone. Under `--delete` the orphan sweep removes every agent file
91655
+ * there that this run does not generate, but only after this file is written,
91656
+ * so in that mode such entries are dropped here too and a single run
91657
+ * converges. Every other entry and every other key of the file is kept.
91658
+ * The array is merged into the file on disk at write time and the file itself
91659
+ * is never deleted.
91660
+ */
91661
+ var FirebenderAgentsRegistry = class extends ToolSubagent {
91662
+ entries;
91663
+ existingAgentFileNames;
91664
+ deleteOrphans;
91665
+ frozen;
91666
+ logger;
91667
+ mergedContent = null;
91668
+ constructor({ entries, existingAgentFileNames = /* @__PURE__ */ new Set(), deleteOrphans = false, frozen = false, logger, ...rest }) {
91669
+ super({
91670
+ ...rest,
91671
+ fileContent: ""
91672
+ });
91673
+ this.entries = entries;
91674
+ this.existingAgentFileNames = existingAgentFileNames;
91675
+ this.deleteOrphans = deleteOrphans;
91676
+ this.frozen = frozen;
91677
+ this.logger = logger;
91678
+ }
91679
+ getEntries() {
91680
+ return this.entries;
91681
+ }
91682
+ isDeletable() {
91683
+ return false;
91684
+ }
91685
+ shouldMergeExistingFileContent() {
91686
+ return true;
91687
+ }
91688
+ setFileContent(newFileContent) {
91689
+ this.mergedContent = this.mergeInto(newFileContent);
91690
+ }
91691
+ getFileContent() {
91692
+ if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
91693
+ return this.mergedContent;
91694
+ }
91695
+ mergeInto(existingContent) {
91696
+ const paths = {
91697
+ relativeDirPath: this.getRelativeDirPath(),
91698
+ relativeFilePath: this.getRelativeFilePath()
91699
+ };
91700
+ const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
91701
+ const existing = parseSharedConfig({
91702
+ format: "json",
91703
+ fileContent: existingContent,
91704
+ filePath,
91705
+ invalidRootPolicy: "error"
91706
+ });
91707
+ if (this.frozen) return applySharedConfigPatch({
91708
+ fileKey: sharedConfigFileKey(paths),
91709
+ feature: "subagents",
91710
+ existingContent,
91711
+ patch: { agents: existing.agents },
91712
+ filePath,
91713
+ logger: this.logger
91714
+ });
91715
+ const existingEntries = Array.isArray(existing.agents) ? existing.agents : [];
91716
+ const generatedByFileName = new Map(this.entries.map((entry) => [managedAgentFileName({
91717
+ entry,
91718
+ global: this.global
91719
+ }) ?? entry, entry]));
91720
+ const agents = [];
91721
+ const placed = /* @__PURE__ */ new Set();
91722
+ const dropped = [];
91723
+ for (const entry of existingEntries) {
91724
+ const fileName = typeof entry === "string" ? managedAgentFileName({
91725
+ entry,
91726
+ global: this.global
91727
+ }) : void 0;
91728
+ if (fileName === void 0) {
91729
+ agents.push(entry);
91730
+ continue;
91731
+ }
91732
+ const generated = generatedByFileName.get(fileName);
91733
+ if (generated !== void 0) {
91734
+ if (!placed.has(fileName)) {
91735
+ agents.push(generated);
91736
+ placed.add(fileName);
91737
+ }
91738
+ } else if (!this.deleteOrphans && this.existingAgentFileNames.has(fileName)) agents.push(entry);
91739
+ else dropped.push(String(entry));
91740
+ }
91741
+ for (const [fileName, entry] of generatedByFileName) if (!placed.has(fileName)) {
91742
+ agents.push(entry);
91743
+ placed.add(fileName);
91744
+ }
91745
+ 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."));
91746
+ return applySharedConfigPatch({
91747
+ fileKey: sharedConfigFileKey(paths),
91748
+ feature: "subagents",
91749
+ existingContent,
91750
+ patch: { agents: agents.length > 0 ? agents : void 0 },
91751
+ filePath,
91752
+ logger: this.logger
91753
+ });
91754
+ }
91755
+ toRulesyncSubagent() {
91756
+ throw new Error(`${this.getRelativePathFromCwd()} registers Firebender agents and is not a subagent itself.`);
91757
+ }
91758
+ validate() {
91759
+ return {
91760
+ success: true,
91761
+ error: null
91762
+ };
91763
+ }
91764
+ };
91765
+ //#endregion
89459
91766
  //#region src/features/subagents/goose-subagent.ts
89460
91767
  /**
89461
91768
  * Goose custom agents (v1.34.0+) are Markdown files with YAML frontmatter —
@@ -89743,13 +92050,13 @@ function subagentSlug(relativeFilePath) {
89743
92050
  return basename(relativeFilePath, ".md").replace(/[^a-zA-Z0-9_-]/g, "_");
89744
92051
  }
89745
92052
  function hermesCommandName(slug) {
89746
- return `rulesync_subagent_${slug}`;
92053
+ return hermesSlashName(`rulesync-subagent-${slug}`);
89747
92054
  }
89748
92055
  function getPluginManifestContent() {
89749
92056
  return [
89750
92057
  "name: rulesync-subagents",
89751
92058
  "version: \"1.0.0\"",
89752
- "description: Exposes RuleSync subagents as Hermes native delegation commands.",
92059
+ "description: Exposes RuleSync subagents as Hermes slash commands that queue a delegation prompt.",
89753
92060
  ""
89754
92061
  ].join("\n");
89755
92062
  }
@@ -89757,9 +92064,12 @@ function getPluginInitContent() {
89757
92064
  return `"""RuleSync-generated Hermes subagent commands."""
89758
92065
 
89759
92066
  import json
92067
+ import logging
92068
+ import re
89760
92069
  from pathlib import Path
89761
92070
 
89762
92071
 
92072
+ logger = logging.getLogger(__name__)
89763
92073
  SUBAGENTS_DIR = Path(__file__).resolve().parents[2] / "rulesync" / "subagents"
89764
92074
 
89765
92075
 
@@ -89779,52 +92089,71 @@ def _load_subagents():
89779
92089
  return subagents
89780
92090
 
89781
92091
 
89782
- def _register_subagent(ctx, subagent):
92092
+ def _command_name(subagent, slug):
92093
+ # The spec carries the name Rulesync normalized; Hermes' messaging gateway
92094
+ # resolves plugin commands with "_" turned into "-", so keep it hyphenated.
92095
+ hermes = subagent.get("hermes")
92096
+ name = hermes.get("command") if isinstance(hermes, dict) else None
92097
+ name = name or f"rulesync-subagent-{slug}"
92098
+ # Mirrors hermesSlashName() in src/features/shared/hermes-plugin-commands.ts.
92099
+ name = re.sub(r"[ _]", "-", str(name).lower())
92100
+ name = re.sub(r"[^a-z0-9-]", "", name)
92101
+ return re.sub(r"-{2,}", "-", name).strip("-")
92102
+
92103
+
92104
+ ${getHermesQueueUserMessagePython({ pluginName: "rulesync-subagents" })}
92105
+
92106
+ def _register_subagent(ctx, subagent, registered):
89783
92107
  slug = subagent.get("slug")
89784
92108
  if not slug:
89785
92109
  return
89786
92110
 
89787
- command_name = f"rulesync_subagent_{slug}"
92111
+ command_name = _command_name(subagent, slug)
92112
+ if command_name in registered:
92113
+ logger.warning(
92114
+ "rulesync-subagents: %s and %s both map to /%s; keeping the first.",
92115
+ registered[command_name],
92116
+ subagent.get("_path"),
92117
+ command_name,
92118
+ )
92119
+ return
92120
+ registered[command_name] = subagent.get("_path")
92121
+
89788
92122
  name = subagent.get("name") or slug
89789
92123
  description = subagent.get("description") or f"Delegate work to the {name} RuleSync subagent."
89790
92124
  system_prompt = subagent.get("prompt") or ""
89791
92125
 
89792
92126
  def handler(args=None, **kwargs):
89793
- del kwargs
89794
92127
  user_context = ""
89795
92128
  if isinstance(args, dict):
89796
92129
  user_context = args.get("context") or args.get("task") or args.get("prompt") or ""
89797
92130
  elif args is not None:
89798
92131
  user_context = str(args)
89799
92132
 
89800
- context_parts = []
92133
+ # A slash command runs outside any agent turn, so calling delegate_task
92134
+ # here fails with "delegate_task requires a parent agent context".
92135
+ # Queue the brief as a user message instead; the agent then delegates.
92136
+ parts = [
92137
+ f"Use the delegate_task tool to hand the following task to a subagent "
92138
+ f"acting as the {name} RuleSync subagent.",
92139
+ f"Goal: {description}",
92140
+ ]
89801
92141
  if system_prompt:
89802
- context_parts.append(system_prompt)
92142
+ parts.append(f"Subagent instructions:\\n{system_prompt}")
89803
92143
  if user_context:
89804
- context_parts.append(user_context)
92144
+ parts.append(f"Task details:\\n{user_context}")
89805
92145
 
89806
- # delegate_task takes no model-facing "toolsets" argument: a subagent
89807
- # inherits the parent's enabled toolsets (role="orchestrator" is the
89808
- # only knob that changes them). "tasks" is the only shape its schema
89809
- # advertises; a top-level "goal" is an unadvertised legacy path.
89810
- return ctx.dispatch_tool(
89811
- "delegate_task",
89812
- {
89813
- "tasks": [
89814
- {
89815
- "goal": description,
89816
- "context": "\\n\\n".join(context_parts),
89817
- },
89818
- ],
89819
- },
89820
- )
92146
+ label = f"the {name} subagent task"
92147
+ error = _queue_user_message(ctx, "\\n\\n".join(parts), kwargs, label)
92148
+ return error or f"Queued {label}; the agent will delegate it."
89821
92149
 
89822
92150
  ctx.register_command(command_name, handler, description)
89823
92151
 
89824
92152
 
89825
92153
  def register(ctx):
92154
+ registered = {}
89826
92155
  for subagent in _load_subagents():
89827
- _register_subagent(ctx, subagent)
92156
+ _register_subagent(ctx, subagent, registered)
89828
92157
  `;
89829
92158
  }
89830
92159
  function getEnabledPluginConfigContent({ currentContent, global }) {
@@ -89855,7 +92184,7 @@ function getSubagentSpec(rulesyncSubagent) {
89855
92184
  prompt: rulesyncSubagent.getBody(),
89856
92185
  hermes: {
89857
92186
  command: hermesCommandName(slug),
89858
- dispatch: "delegate_task"
92187
+ dispatch: "inject_message"
89859
92188
  }
89860
92189
  };
89861
92190
  }
@@ -90531,6 +92860,15 @@ const KiroCliSubagentJsonSchema = z.looseObject({
90531
92860
  allowedTools: z.optional(z.nullable(z.array(z.string()))),
90532
92861
  includeMcpJson: z.optional(z.nullable(z.boolean()))
90533
92862
  });
92863
+ /**
92864
+ * The name Kiro gives an agent file: its path relative to the agents directory
92865
+ * without the extension, so a nested `team/planner.md` is `team/planner`.
92866
+ * https://kiro.dev/docs/custom-agents/
92867
+ */
92868
+ function kiroAgentNameFromPath(relativeFilePath) {
92869
+ const posixPath = toPosixPath(relativeFilePath);
92870
+ return posixPath.slice(0, posixPath.length - extname(posixPath).length);
92871
+ }
90534
92872
  var KiroSubagent = class extends ToolSubagent {
90535
92873
  body;
90536
92874
  constructor({ body, ...rest }) {
@@ -90566,7 +92904,7 @@ var KiroSubagent = class extends ToolSubagent {
90566
92904
  outputRoot: ".",
90567
92905
  frontmatter: {
90568
92906
  targets: [toolTarget],
90569
- name: name ?? basename(this.getRelativeFilePath(), ".json"),
92907
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
90570
92908
  description: description ?? void 0,
90571
92909
  ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
90572
92910
  },
@@ -90716,7 +93054,7 @@ var KiroIdeSubagent = class KiroIdeSubagent extends ToolSubagent {
90716
93054
  outputRoot: ".",
90717
93055
  frontmatter: {
90718
93056
  targets: ["*"],
90719
- name: name ?? this.getRelativeFilePath().replace(/\.md$/, ""),
93057
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
90720
93058
  description: description ?? "",
90721
93059
  ...Object.keys(kiroIdeSection).length > 0 && { "kiro-ide": kiroIdeSection }
90722
93060
  },
@@ -92647,6 +94985,166 @@ var VibeSubagent = class VibeSubagent extends ToolSubagent {
92647
94985
  }
92648
94986
  };
92649
94987
  //#endregion
94988
+ //#region src/features/subagents/xum-subagent.ts
94989
+ /**
94990
+ * The built-in agent a rulesync subagent inherits from when its `xum` block
94991
+ * names neither a `base` nor its own `tools`. Xum gives an agent no tools at
94992
+ * all unless some agent in its `base` chain declares them, so a definition
94993
+ * without either would be a prompt that cannot read or edit anything.
94994
+ */
94995
+ const XUM_DEFAULT_BASE_AGENT = "exec";
94996
+ /**
94997
+ * Frontmatter of a Xum agent definition. `name` (the display name) is
94998
+ * required; the other documented fields are typed loosely and unknown keys are
94999
+ * kept (looseObject) so a round trip never drops them.
95000
+ *
95001
+ * @see https://xum.cdr.dev/agents
95002
+ */
95003
+ const XumSubagentFrontmatterSchema = z.looseObject({
95004
+ name: z.string(),
95005
+ description: z.optional(z.string()),
95006
+ base: z.optional(z.string()),
95007
+ disabled: z.optional(z.boolean()),
95008
+ ui: z.optional(z.looseObject({})),
95009
+ prompt: z.optional(z.looseObject({})),
95010
+ subagent: z.optional(z.looseObject({})),
95011
+ ai: z.optional(z.looseObject({})),
95012
+ tools: z.optional(z.looseObject({}))
95013
+ });
95014
+ /**
95015
+ * Subagent generator for Xum (formerly Mux).
95016
+ *
95017
+ * Xum reads custom agent definitions — Markdown with YAML frontmatter — from
95018
+ * the top level of `.xum/agents/` (project) and `~/.xum/agents/` (user); the
95019
+ * body is the agent's instruction prompt. The file name (without `.md`) is the
95020
+ * agent id, so it must be lowercase letters and digits with single `-` / `_`
95021
+ * separators; Xum skips other files with a warning.
95022
+ *
95023
+ * rulesync writes `name` and `description`, spreads the `xum` section (`base`,
95024
+ * `ui`, `prompt`, `subagent`, `ai`, `tools`, ...) on top, and fills two
95025
+ * defaults so the definition works as a delegated subagent out of the box:
95026
+ * `subagent.runnable: true` (required for `task({ agentId })` to spawn it) and,
95027
+ * when the section sets neither `base` nor `tools`, `base: exec`.
95028
+ *
95029
+ * @see https://xum.cdr.dev/agents
95030
+ */
95031
+ var XumSubagent = class XumSubagent extends ToolSubagent {
95032
+ frontmatter;
95033
+ body;
95034
+ constructor({ frontmatter, body, ...rest }) {
95035
+ if (rest.validate !== false) {
95036
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
95037
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
95038
+ }
95039
+ super({ ...rest });
95040
+ this.frontmatter = frontmatter;
95041
+ this.body = body;
95042
+ }
95043
+ static getSettablePaths(_options = {}) {
95044
+ return { relativeDirPath: XUM_AGENTS_DIR_PATH };
95045
+ }
95046
+ getFrontmatter() {
95047
+ return this.frontmatter;
95048
+ }
95049
+ getBody() {
95050
+ return this.body;
95051
+ }
95052
+ toRulesyncSubagent() {
95053
+ const { name, description, ...restFields } = this.frontmatter;
95054
+ return new RulesyncSubagent({
95055
+ outputRoot: ".",
95056
+ frontmatter: {
95057
+ targets: ["*"],
95058
+ name,
95059
+ description,
95060
+ ...Object.keys(restFields).length > 0 && { xum: restFields }
95061
+ },
95062
+ body: this.body,
95063
+ relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
95064
+ relativeFilePath: this.getRelativeFilePath(),
95065
+ validate: true
95066
+ });
95067
+ }
95068
+ static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false }) {
95069
+ const rulesyncFrontmatter = rulesyncSubagent.getFrontmatter();
95070
+ const xumSection = this.filterToolSpecificSection(rulesyncFrontmatter.xum ?? {}, ["name", "description"]);
95071
+ const subagentSection = xumSection.subagent;
95072
+ const result = XumSubagentFrontmatterSchema.safeParse({
95073
+ name: rulesyncFrontmatter.name,
95074
+ ...rulesyncFrontmatter.description && { description: rulesyncFrontmatter.description },
95075
+ ...xumSection.base === void 0 && xumSection.tools === void 0 && { base: XUM_DEFAULT_BASE_AGENT },
95076
+ ...xumSection,
95077
+ subagent: {
95078
+ runnable: true,
95079
+ ...isPlainObject$1(subagentSection) ? subagentSection : {}
95080
+ }
95081
+ });
95082
+ if (!result.success) throw new Error(`Invalid xum subagent frontmatter in ${rulesyncSubagent.getRelativeFilePath()}: ${formatError(result.error)}`);
95083
+ const body = rulesyncSubagent.getBody();
95084
+ return new XumSubagent({
95085
+ outputRoot,
95086
+ frontmatter: result.data,
95087
+ body,
95088
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
95089
+ relativeFilePath: rulesyncSubagent.getRelativeFilePath(),
95090
+ fileContent: stringifyFrontmatter(body, result.data),
95091
+ validate
95092
+ });
95093
+ }
95094
+ validate() {
95095
+ if (!this.frontmatter) return {
95096
+ success: true,
95097
+ error: null
95098
+ };
95099
+ const result = XumSubagentFrontmatterSchema.safeParse(this.frontmatter);
95100
+ if (result.success) return {
95101
+ success: true,
95102
+ error: null
95103
+ };
95104
+ return {
95105
+ success: false,
95106
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
95107
+ };
95108
+ }
95109
+ static isTargetedByRulesyncSubagent(rulesyncSubagent) {
95110
+ return this.isTargetedByRulesyncSubagentDefault({
95111
+ rulesyncSubagent,
95112
+ toolTarget: "xum"
95113
+ });
95114
+ }
95115
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
95116
+ const paths = this.getSettablePaths({ global });
95117
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
95118
+ const fileContent = await readFileContent(filePath);
95119
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
95120
+ const result = XumSubagentFrontmatterSchema.safeParse(frontmatter);
95121
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
95122
+ return new XumSubagent({
95123
+ outputRoot,
95124
+ relativeDirPath: paths.relativeDirPath,
95125
+ relativeFilePath,
95126
+ frontmatter: result.data,
95127
+ body: body.trim(),
95128
+ fileContent,
95129
+ validate
95130
+ });
95131
+ }
95132
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
95133
+ return new XumSubagent({
95134
+ outputRoot,
95135
+ relativeDirPath,
95136
+ relativeFilePath,
95137
+ frontmatter: {
95138
+ name: "",
95139
+ description: ""
95140
+ },
95141
+ body: "",
95142
+ fileContent: "",
95143
+ validate: false
95144
+ });
95145
+ }
95146
+ };
95147
+ //#endregion
92650
95148
  //#region src/features/subagents/zcode-subagent.ts
92651
95149
  const ZcodeSubagentFrontmatterSchema = z.looseObject({
92652
95150
  name: z.string(),
@@ -93112,6 +95610,17 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93112
95610
  filePattern: "*.md"
93113
95611
  }
93114
95612
  }],
95613
+ ["firebender", {
95614
+ class: FirebenderSubagent,
95615
+ meta: {
95616
+ supportsProject: true,
95617
+ supportsSimulated: false,
95618
+ supportsGlobal: true,
95619
+ emitsEmptyAggregate: true,
95620
+ emitsEmptyAggregateOnDelete: true,
95621
+ filePattern: "*.md"
95622
+ }
95623
+ }],
93115
95624
  ["goose", {
93116
95625
  class: GooseSubagent,
93117
95626
  meta: {
@@ -93163,7 +95672,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93163
95672
  supportsProject: true,
93164
95673
  supportsSimulated: false,
93165
95674
  supportsGlobal: true,
93166
- filePattern: "*.json"
95675
+ filePattern: "**/*.json",
95676
+ supportsNestedPaths: true
93167
95677
  }
93168
95678
  }],
93169
95679
  ["kiro-ide", {
@@ -93172,7 +95682,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93172
95682
  supportsProject: true,
93173
95683
  supportsSimulated: false,
93174
95684
  supportsGlobal: true,
93175
- filePattern: "*.md"
95685
+ filePattern: "**/*.md",
95686
+ supportsNestedPaths: true
93176
95687
  }
93177
95688
  }],
93178
95689
  ["lettacode", {
@@ -93339,6 +95850,15 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
93339
95850
  filePattern: "*.toml"
93340
95851
  }
93341
95852
  }],
95853
+ ["xum", {
95854
+ class: XumSubagent,
95855
+ meta: {
95856
+ supportsProject: true,
95857
+ supportsSimulated: false,
95858
+ supportsGlobal: true,
95859
+ filePattern: "*.md"
95860
+ }
95861
+ }],
93342
95862
  ["zcode", {
93343
95863
  class: ZcodeSubagent,
93344
95864
  meta: {
@@ -93393,6 +95913,7 @@ function compareRelativePathsShallowestFirst(left, right) {
93393
95913
  var SubagentsProcessor = class extends FeatureProcessor {
93394
95914
  toolTarget;
93395
95915
  global;
95916
+ deleteOrphans;
93396
95917
  getFactory;
93397
95918
  /**
93398
95919
  * Whether the last `loadRulesyncFiles` found a `subagents/` directory under
@@ -93400,13 +95921,14 @@ var SubagentsProcessor = class extends FeatureProcessor {
93400
95921
  * agents in its Pool settings are its own and must not be retracted.
93401
95922
  */
93402
95923
  rulesyncSourceDirFound = false;
93403
- constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
95924
+ constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, deleteOrphans = false, logger }) {
93404
95925
  super({
93405
95926
  outputRoot,
93406
95927
  inputRoots,
93407
95928
  dryRun,
93408
95929
  logger
93409
95930
  });
95931
+ this.deleteOrphans = deleteOrphans;
93410
95932
  const result = SubagentsProcessorToolTargetSchema.safeParse(toolTarget);
93411
95933
  if (!result.success) throw new Error(`Invalid tool target for SubagentsProcessor: ${toolTarget}. ${formatError(result.error)}`);
93412
95934
  this.toolTarget = result.data;
@@ -93414,7 +95936,9 @@ var SubagentsProcessor = class extends FeatureProcessor {
93414
95936
  this.getFactory = getFactory;
93415
95937
  }
93416
95938
  emitsToolFilesForEmptySource() {
93417
- return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
95939
+ const { meta } = this.getFactory(this.toolTarget);
95940
+ if (meta.emitsEmptyAggregate !== true) return false;
95941
+ return this.rulesyncSourceDirFound || this.deleteOrphans && meta.emitsEmptyAggregateOnDelete === true;
93418
95942
  }
93419
95943
  async convertRulesyncFilesToToolFiles(rulesyncFiles) {
93420
95944
  const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
@@ -93430,11 +95954,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
93430
95954
  });
93431
95955
  if (factory.class.fromRulesyncSubagents) {
93432
95956
  if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
93433
- const toolSubagents = factory.class.fromRulesyncSubagents({
95957
+ const toolSubagents = await factory.class.fromRulesyncSubagents({
93434
95958
  outputRoot: this.outputRoot,
93435
95959
  rulesyncSubagents: targeted,
93436
95960
  global: this.global,
93437
- logger: this.logger
95961
+ logger: this.logger,
95962
+ deleteOrphans: this.deleteOrphans && !this.hasRulesyncSourceLoadFailure()
93438
95963
  });
93439
95964
  return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
93440
95965
  }
@@ -94180,7 +96705,7 @@ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
94180
96705
  /**
94181
96706
  * Globs that match the whole project and so say nothing beyond "always".
94182
96707
  */
94183
- const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
96708
+ const UNIVERSAL_GLOBS$6 = /* @__PURE__ */ new Set(["**/*", "*"]);
94184
96709
  const METADATA_DELIMITER = "---";
94185
96710
  /**
94186
96711
  * Splits text into lines that keep their own line terminator, so the body can
@@ -94305,7 +96830,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
94305
96830
  */
94306
96831
  static buildMetadata(frontmatter) {
94307
96832
  const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
94308
- const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$5.has(glob));
96833
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$6.has(glob));
94309
96834
  const instructions = frontmatter.description && flattenValue(frontmatter.description);
94310
96835
  const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
94311
96836
  const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
@@ -96959,7 +99484,7 @@ function normalizePatternList(value) {
96959
99484
  * an always-on rule into a "when any file is in context" rule. Dropped on
96960
99485
  * generate and mapped back on import.
96961
99486
  */
96962
- const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
99487
+ const UNIVERSAL_GLOBS$5 = /* @__PURE__ */ new Set(["**/*", "*"]);
96963
99488
  /**
96964
99489
  * Rule generator for Continue (continuedev/continue), the open-source IDE
96965
99490
  * extension and CLI (`cn`).
@@ -97073,7 +99598,7 @@ var ContinueRule = class ContinueRule extends ToolRule {
97073
99598
  if (alwaysApply === true) return;
97074
99599
  const targetGlobs = continueGlobs ?? parentGlobs;
97075
99600
  if (!targetGlobs || targetGlobs.length === 0) return;
97076
- if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
99601
+ if (targetGlobs.every((glob) => UNIVERSAL_GLOBS$5.has(glob.trim()))) return;
97077
99602
  return targetGlobs;
97078
99603
  }
97079
99604
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
@@ -97565,7 +100090,7 @@ const CursorRuleFrontmatterSchema = z.object({
97565
100090
  * does not already say. Same pair the Cline and Qwen Code adapters treat as
97566
100091
  * universal.
97567
100092
  */
97568
- const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
100093
+ const UNIVERSAL_GLOBS$4 = /* @__PURE__ */ new Set(["**/*", "*"]);
97569
100094
  var CursorRule = class CursorRule extends ToolRule {
97570
100095
  frontmatter;
97571
100096
  body;
@@ -97606,7 +100131,8 @@ var CursorRule = class CursorRule extends ToolRule {
97606
100131
  }
97607
100132
  /**
97608
100133
  * Custom parse function for Cursor MDC files
97609
- * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially
100134
+ * MDC files don't support quotes in YAML, so we need to handle patterns like *.ts specially.
100135
+ * Also used by Firebender, whose `.firebender/rules/*.mdc` files carry the same frontmatter.
97610
100136
  */
97611
100137
  static parseCursorFrontmatter(fileContent, filePath) {
97612
100138
  const bounds = findFrontmatterBlockBounds(fileContent);
@@ -97669,7 +100195,7 @@ var CursorRule = class CursorRule extends ToolRule {
97669
100195
  static resolveCursorGlobs({ cursorSpecificGlobs, parentGlobs, alwaysApply }) {
97670
100196
  const targetGlobs = cursorSpecificGlobs !== void 0 ? cursorSpecificGlobs : parentGlobs;
97671
100197
  if (!targetGlobs || targetGlobs.length === 0) return;
97672
- if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$3.has(glob.trim()))) return;
100198
+ if (alwaysApply && targetGlobs.every((glob) => UNIVERSAL_GLOBS$4.has(glob.trim()))) return;
97673
100199
  return targetGlobs.join(",");
97674
100200
  }
97675
100201
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
@@ -98583,6 +101109,211 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
98583
101109
  }
98584
101110
  };
98585
101111
  //#endregion
101112
+ //#region src/features/rules/firebender-rule.ts
101113
+ /**
101114
+ * Frontmatter of a Firebender `.mdc` rule. Every field is optional; `globs`
101115
+ * is one comma-separated string, and a YAML list is tolerated on import.
101116
+ */
101117
+ const FirebenderRuleFrontmatterSchema = z.looseObject({
101118
+ description: z.optional(z.nullable(z.string())),
101119
+ globs: z.optional(z.nullable(z.union([z.string(), z.array(z.string())]))),
101120
+ alwaysApply: z.optional(z.nullable(z.boolean()))
101121
+ });
101122
+ /** Globs that match every file, and so add nothing to an always-applied rule. */
101123
+ const UNIVERSAL_GLOBS$3 = /* @__PURE__ */ new Set(["**/*", "*"]);
101124
+ function normalizeGlobs$2(globs) {
101125
+ if (globs === null || globs === void 0) return [];
101126
+ return (typeof globs === "string" ? globs.split(",") : globs).map((glob) => glob.trim()).filter((glob) => glob.length > 0);
101127
+ }
101128
+ /**
101129
+ * Rule generator for Firebender.
101130
+ *
101131
+ * Root rule: Firebender looks for `.firebender/AGENTS.md` first (then the
101132
+ * project-root `AGENTS.md`, then the same pair in parent directories, then
101133
+ * `~/.firebender/AGENTS.md`) and uses the first file it finds, so the root rule
101134
+ * is written to `.firebender/AGENTS.md` in the project and to
101135
+ * `~/.firebender/AGENTS.md` globally, where it does not collide with the
101136
+ * project-root `AGENTS.md` the `agentsmd` target owns.
101137
+ *
101138
+ * Non-root rules: `.firebender/rules/*.mdc` (project) and
101139
+ * `~/.firebender/rules/*.mdc` (personal), each with optional Cursor-style
101140
+ * frontmatter: `description`, `globs` (one comma-separated string) and
101141
+ * `alwaysApply`. A rule with specific globs is written with
101142
+ * `alwaysApply: false` and those globs; every other rule is always applied.
101143
+ *
101144
+ * @see https://docs.firebender.com/api-reference/rules
101145
+ * @see https://docs.firebender.com/api-reference/agents-md
101146
+ */
101147
+ var FirebenderRule = class FirebenderRule extends ToolRule {
101148
+ frontmatter;
101149
+ body;
101150
+ constructor({ frontmatter = {}, body, ...rest }) {
101151
+ if (rest.validate && !rest.root) {
101152
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
101153
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
101154
+ }
101155
+ const resolvedBody = body ?? rest.fileContent;
101156
+ super({
101157
+ ...rest,
101158
+ fileContent: rest.root ? resolvedBody : stringifyFrontmatter(resolvedBody, FirebenderRule.toOutputFrontmatter(frontmatter), { avoidBlockScalars: true })
101159
+ });
101160
+ this.frontmatter = frontmatter;
101161
+ this.body = resolvedBody;
101162
+ }
101163
+ static getSettablePaths({ excludeToolDir } = {}) {
101164
+ return {
101165
+ root: {
101166
+ relativeDirPath: buildToolPath(FIREBENDER_DIR, ".", excludeToolDir),
101167
+ relativeFilePath: FIREBENDER_AGENTS_MD_FILE_NAME
101168
+ },
101169
+ nonRoot: { relativeDirPath: buildToolPath(FIREBENDER_DIR, "rules", excludeToolDir) }
101170
+ };
101171
+ }
101172
+ /**
101173
+ * The frontmatter written to a `.mdc` file: `alwaysApply` first, then the
101174
+ * flattened `description`, then the globs joined the way Firebender's docs
101175
+ * spell them (`"*.kt, *.kts"`).
101176
+ */
101177
+ static toOutputFrontmatter(frontmatter) {
101178
+ const description = typeof frontmatter.description === "string" ? frontmatter.description.replace(/[\r\n\u0085\u2028\u2029]+/g, " ").trim() : "";
101179
+ const globs = normalizeGlobs$2(frontmatter.globs);
101180
+ return {
101181
+ ...typeof frontmatter.alwaysApply === "boolean" && { alwaysApply: frontmatter.alwaysApply },
101182
+ ...description !== "" && { description },
101183
+ ...globs.length > 0 && { globs: globs.join(", ") }
101184
+ };
101185
+ }
101186
+ /**
101187
+ * Derives the Firebender frontmatter from the canonical one: specific globs
101188
+ * make a file-scoped rule, everything else (no globs, universal globs) is
101189
+ * always applied. Firebender splits `globs` on every comma, so a brace
101190
+ * alternation such as `*.{ts,tsx}` is expanded into one glob per branch.
101191
+ */
101192
+ static buildFrontmatter(frontmatter) {
101193
+ const globs = normalizeGlobs$2(frontmatter.globs ?? []).flatMap((glob) => expandBraceAlternations(glob));
101194
+ const specificGlobs = globs.filter((glob) => !UNIVERSAL_GLOBS$3.has(glob));
101195
+ const isFileScoped = specificGlobs.length > 0 && specificGlobs.length === globs.length;
101196
+ const description = frontmatter.description?.trim() || void 0;
101197
+ return {
101198
+ alwaysApply: !isFileScoped,
101199
+ ...description !== void 0 && { description },
101200
+ ...isFileScoped && { globs: specificGlobs.join(", ") }
101201
+ };
101202
+ }
101203
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
101204
+ const paths = this.getSettablePaths();
101205
+ const rulesyncFrontmatter = rulesyncRule.getFrontmatter();
101206
+ if (rulesyncFrontmatter.root ?? false) return new FirebenderRule({
101207
+ outputRoot,
101208
+ relativeDirPath: paths.root.relativeDirPath,
101209
+ relativeFilePath: paths.root.relativeFilePath,
101210
+ fileContent: rulesyncRule.getBody(),
101211
+ validate,
101212
+ root: true,
101213
+ description: rulesyncFrontmatter.description,
101214
+ globs: rulesyncFrontmatter.globs
101215
+ });
101216
+ return new FirebenderRule({
101217
+ outputRoot,
101218
+ relativeDirPath: paths.nonRoot.relativeDirPath,
101219
+ relativeFilePath: rulesyncRule.getRelativeFilePath().replace(/\.md$/, ".mdc"),
101220
+ frontmatter: this.buildFrontmatter(rulesyncFrontmatter),
101221
+ body: rulesyncRule.getBody(),
101222
+ fileContent: "",
101223
+ validate,
101224
+ root: false
101225
+ });
101226
+ }
101227
+ static async fromFile({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true }) {
101228
+ const paths = this.getSettablePaths();
101229
+ if (relativeFilePath === paths.root.relativeFilePath && (relativeDirPath === void 0 || relativeDirPath === paths.root.relativeDirPath)) {
101230
+ const fileContent = await readFileContent(join(outputRoot, paths.root.relativeDirPath, paths.root.relativeFilePath));
101231
+ return new FirebenderRule({
101232
+ outputRoot,
101233
+ relativeDirPath: paths.root.relativeDirPath,
101234
+ relativeFilePath: paths.root.relativeFilePath,
101235
+ fileContent,
101236
+ validate,
101237
+ root: true
101238
+ });
101239
+ }
101240
+ const filePath = join(outputRoot, paths.nonRoot.relativeDirPath, relativeFilePath);
101241
+ const fileContent = await readFileContent(filePath);
101242
+ const { frontmatter, body } = CursorRule.parseCursorFrontmatter(fileContent, filePath);
101243
+ const result = FirebenderRuleFrontmatterSchema.safeParse(frontmatter);
101244
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
101245
+ return new FirebenderRule({
101246
+ outputRoot,
101247
+ relativeDirPath: paths.nonRoot.relativeDirPath,
101248
+ relativeFilePath,
101249
+ frontmatter: result.data,
101250
+ body: body.trim(),
101251
+ fileContent: "",
101252
+ validate,
101253
+ root: false
101254
+ });
101255
+ }
101256
+ toRulesyncRule() {
101257
+ if (this.isRoot()) return this.toRulesyncRuleDefault();
101258
+ const sourceGlobs = normalizeGlobs$2(this.frontmatter.globs);
101259
+ const alwaysApply = this.frontmatter.alwaysApply ?? void 0;
101260
+ const description = this.frontmatter.description?.trim() || void 0;
101261
+ const globs = alwaysApply === true || sourceGlobs.length === 0 ? ["**/*"] : sourceGlobs;
101262
+ return new RulesyncRule({
101263
+ outputRoot: process.cwd(),
101264
+ frontmatter: {
101265
+ targets: ["*"],
101266
+ root: false,
101267
+ ...description !== void 0 && { description },
101268
+ globs
101269
+ },
101270
+ body: this.body,
101271
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
101272
+ relativeFilePath: this.getRelativeFilePath().replace(/\.mdc$/, ".md"),
101273
+ validate: true
101274
+ });
101275
+ }
101276
+ validate() {
101277
+ if (this.isRoot() || !this.frontmatter) return {
101278
+ success: true,
101279
+ error: null
101280
+ };
101281
+ const result = FirebenderRuleFrontmatterSchema.safeParse(this.frontmatter);
101282
+ if (result.success) return {
101283
+ success: true,
101284
+ error: null
101285
+ };
101286
+ return {
101287
+ success: false,
101288
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
101289
+ };
101290
+ }
101291
+ getFrontmatter() {
101292
+ return this.frontmatter;
101293
+ }
101294
+ getBody() {
101295
+ return this.body;
101296
+ }
101297
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
101298
+ const paths = this.getSettablePaths();
101299
+ const isRoot = relativeFilePath === paths.root.relativeFilePath && relativeDirPath === paths.root.relativeDirPath;
101300
+ return new FirebenderRule({
101301
+ outputRoot,
101302
+ relativeDirPath,
101303
+ relativeFilePath,
101304
+ fileContent: "",
101305
+ validate: false,
101306
+ root: isRoot
101307
+ });
101308
+ }
101309
+ static isTargetedByRulesyncRule(rulesyncRule) {
101310
+ return this.isTargetedByRulesyncRuleDefault({
101311
+ rulesyncRule,
101312
+ toolTarget: "firebender"
101313
+ });
101314
+ }
101315
+ };
101316
+ //#endregion
98586
101317
  //#region src/features/rules/gitlabduo-rule.ts
98587
101318
  /**
98588
101319
  * GitLab Duo CLI reads custom rules from a single plain-Markdown file:
@@ -101864,6 +104595,117 @@ var WarpRule = class extends NestedAgentsmdRule {
101864
104595
  }
101865
104596
  };
101866
104597
  //#endregion
104598
+ //#region src/features/rules/xum-rule.ts
104599
+ /** An ATX level-1 heading line (`# Title`), which Xum's parser treats as an H1. */
104600
+ const ATX_H1_LINE = /^ {0,3}#(?:[ \t]|$)/;
104601
+ /**
104602
+ * Make `body` start at a level-1 heading, adding `# <heading>` when it does not
104603
+ * already open with one.
104604
+ *
104605
+ * Xum reads `.xum/AGENTS.md` (and appends `AGENTS.local.md`) as one Markdown
104606
+ * document and treats a `Mode:` / `Model:` / `Tool:` heading as a scoped
104607
+ * section that runs until the next heading of the same or a higher level.
104608
+ * Fragments rulesync concatenates into that document — folded topic rules and
104609
+ * the local overlay — would otherwise fall inside a scoped section the
104610
+ * preceding fragment ends with, and Xum would inject them only in that mode.
104611
+ * Only an H1 closes a section of any level, so each such fragment opens with
104612
+ * one.
104613
+ *
104614
+ * @see https://github.com/coder/xum/blob/main/src/node/utils/main/markdown.ts
104615
+ */
104616
+ function ensureXumSectionBoundary({ body, heading }) {
104617
+ const firstLine = body.split(/\r?\n/).find((line) => line.trim() !== "");
104618
+ if (firstLine !== void 0 && ATX_H1_LINE.test(firstLine)) return body;
104619
+ const safeHeading = heading.replaceAll(/[\r\n:]/g, " ").trim() || "Instructions";
104620
+ return body.trim() === "" ? `# ${safeHeading}\n` : `# ${safeHeading}\n\n${body}`;
104621
+ }
104622
+ /** The heading a folded rule opens with: its file name without the extension. */
104623
+ function xumRuleHeading(rulesyncRule) {
104624
+ const fileName = basename(rulesyncRule.getRelativeFilePath());
104625
+ return fileName.slice(0, fileName.length - extname(fileName).length);
104626
+ }
104627
+ /**
104628
+ * Rule generator for Xum (Coder's parallel coding-agent app, formerly Mux).
104629
+ *
104630
+ * Xum layers its own `.xum/AGENTS.md` (project) and `~/.xum/AGENTS.md` (user)
104631
+ * on top of the shared root `AGENTS.md` / `AGENT.md` / `CLAUDE.md`, which the
104632
+ * `agentsmd` target already writes, so this target writes only the
104633
+ * Xum-dedicated file. Only these files honor the `## Model: <regex>` and
104634
+ * `## Mode: <mode>` scoped sections; rulesync passes the body through as-is,
104635
+ * so such sections authored in a rule keep working. The personal
104636
+ * `.xum/AGENTS.local.md` overlay is written for a `localRoot: true` rule.
104637
+ *
104638
+ * @see https://xum.cdr.dev/agents/instruction-files
104639
+ */
104640
+ var XumRule = class XumRule extends ToolRule {
104641
+ constructor({ fileContent, root, ...rest }) {
104642
+ super({
104643
+ ...rest,
104644
+ fileContent,
104645
+ root: root ?? false
104646
+ });
104647
+ }
104648
+ static getSettablePaths(_options = {}) {
104649
+ return { root: {
104650
+ relativeDirPath: XUM_DIR,
104651
+ relativeFilePath: XUM_RULE_FILE_NAME
104652
+ } };
104653
+ }
104654
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
104655
+ const { root } = this.getSettablePaths({ global });
104656
+ const relativePath = join(root.relativeDirPath, root.relativeFilePath);
104657
+ const fileContent = await readFileContent(join(outputRoot, relativePath));
104658
+ return new XumRule({
104659
+ outputRoot,
104660
+ relativeDirPath: root.relativeDirPath,
104661
+ relativeFilePath: root.relativeFilePath,
104662
+ fileContent,
104663
+ validate,
104664
+ root: true
104665
+ });
104666
+ }
104667
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
104668
+ const { root } = this.getSettablePaths({ global });
104669
+ const isRoot = rulesyncRule.getFrontmatter().root ?? false;
104670
+ return new XumRule({
104671
+ outputRoot,
104672
+ relativeDirPath: root.relativeDirPath,
104673
+ relativeFilePath: root.relativeFilePath,
104674
+ fileContent: isRoot ? rulesyncRule.getBody() : ensureXumSectionBoundary({
104675
+ body: rulesyncRule.getBody(),
104676
+ heading: xumRuleHeading(rulesyncRule)
104677
+ }),
104678
+ validate,
104679
+ root: isRoot
104680
+ });
104681
+ }
104682
+ toRulesyncRule() {
104683
+ return this.toRulesyncRuleDefault();
104684
+ }
104685
+ validate() {
104686
+ return {
104687
+ success: true,
104688
+ error: null
104689
+ };
104690
+ }
104691
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
104692
+ return new XumRule({
104693
+ outputRoot,
104694
+ relativeDirPath,
104695
+ relativeFilePath,
104696
+ fileContent: "",
104697
+ validate: false,
104698
+ root: relativeFilePath === "AGENTS.md" && relativeDirPath === ".xum"
104699
+ });
104700
+ }
104701
+ static isTargetedByRulesyncRule(rulesyncRule) {
104702
+ return this.isTargetedByRulesyncRuleDefault({
104703
+ rulesyncRule,
104704
+ toolTarget: "xum"
104705
+ });
104706
+ }
104707
+ };
104708
+ //#endregion
101867
104709
  //#region src/features/rules/zcode-rule.ts
101868
104710
  var ZcodeRule = class ZcodeRule extends ToolRule {
101869
104711
  constructor({ fileContent, root, ...rest }) {
@@ -102352,6 +105194,14 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102352
105194
  collisionPolicy: "compose"
102353
105195
  }
102354
105196
  }],
105197
+ ["firebender", {
105198
+ class: FirebenderRule,
105199
+ meta: {
105200
+ extension: "mdc",
105201
+ supportsGlobal: true,
105202
+ ruleDiscoveryMode: "auto"
105203
+ }
105204
+ }],
102355
105205
  ["goose", {
102356
105206
  class: GooseRule,
102357
105207
  meta: {
@@ -102635,6 +105485,17 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
102635
105485
  collisionPolicy: "fold"
102636
105486
  }
102637
105487
  }],
105488
+ ["xum", {
105489
+ class: XumRule,
105490
+ meta: {
105491
+ extension: "md",
105492
+ supportsGlobal: true,
105493
+ ruleDiscoveryMode: "auto",
105494
+ collisionPolicy: "fold",
105495
+ localRootMode: "separate-local-file",
105496
+ localRootFileName: XUM_LOCAL_RULE_FILE_NAME
105497
+ }
105498
+ }],
102638
105499
  ["devin", {
102639
105500
  class: DevinRule,
102640
105501
  meta: {
@@ -103405,18 +106266,39 @@ var RulesProcessor = class extends FeatureProcessor {
103405
106266
  root: true,
103406
106267
  localRoot
103407
106268
  });
103408
- if (isClassOrSubclassOf({
103409
- candidate: factory.class,
103410
- base: QwencodeRule
103411
- })) return new QwencodeRule({
103412
- outputRoot: this.outputRoot,
103413
- relativeDirPath: relativeDirPath ?? ".qwen",
103414
- relativeFilePath: fileName,
103415
- fileContent: body,
103416
- validate: true,
103417
- root: true,
106269
+ return this.buildToolDirLocalRootFile({
106270
+ factory,
106271
+ fileName,
106272
+ body,
106273
+ relativeDirPath,
103418
106274
  localRoot
103419
106275
  });
106276
+ }
106277
+ /**
106278
+ * Qwen Code and Xum read the personal local file from their tool directory
106279
+ * (`.qwen/QWEN.local.md`, `.xum/AGENTS.local.md`), not the project root
106280
+ * (project scope only; global handling never reaches here).
106281
+ */
106282
+ buildToolDirLocalRootFile({ factory, fileName, body, relativeDirPath, localRoot }) {
106283
+ const toolDirEntry = [[QwencodeRule, QWENCODE_DIR], [XumRule, XUM_DIR]].find(([base]) => isClassOrSubclassOf({
106284
+ candidate: factory.class,
106285
+ base
106286
+ }));
106287
+ if (toolDirEntry) {
106288
+ const [toolDirClass, toolDir] = toolDirEntry;
106289
+ return new toolDirClass({
106290
+ outputRoot: this.outputRoot,
106291
+ relativeDirPath: relativeDirPath ?? toolDir,
106292
+ relativeFilePath: fileName,
106293
+ fileContent: toolDirClass === XumRule && !localRoot ? ensureXumSectionBoundary({
106294
+ body,
106295
+ heading: "Local instructions"
106296
+ }) : body,
106297
+ validate: true,
106298
+ root: true,
106299
+ localRoot
106300
+ });
106301
+ }
103420
106302
  return null;
103421
106303
  }
103422
106304
  /**
@@ -106168,6 +109050,7 @@ async function generateSubagentsCore(params) {
106168
109050
  toolTarget,
106169
109051
  global: config.getGlobal(),
106170
109052
  dryRun: config.isPreviewMode(),
109053
+ deleteOrphans: config.getDelete(),
106171
109054
  logger
106172
109055
  });
106173
109056
  const result = await processFeatureWithRulesyncFiles({
@@ -106790,6 +109673,6 @@ async function importChecksCore(params) {
106790
109673
  return writtenCount;
106791
109674
  }
106792
109675
  //#endregion
106793
- export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
109676
+ export { RulesyncHooks as $, stripHiddenCharacters as $n, readFileContentOrNull as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_MCP_RELATIVE_FILE_PATH as An, PathOutsideRootError as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_USER_CONFIG_DIR_NAME as Bn, fileExists as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_RELATIVE_DIR_PATH as Fn, assertWritablePathInsideRoot as Ft, RulesyncSubagentFrontmatterSchema as G, DEPRECATED_FEATURE_REPLACEMENTS as Gn, isFileSystemError as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, parseCommaSeparatedList as Hn, getFileSizeAndExecutableBit as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_RULES_RELATIVE_DIR_PATH as In, checkPathTraversal as It, RulesyncRule as J, hasDeceptiveHiddenCharacters as Jn, listFilePathsRecursively as Jt, RulesyncSkill as K, formatError as Kn, isSymlink as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Ln, createTempDirectory as Lt, CODEXCLI_DIR as M, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Mn, assertDirectoryIfExists as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Nn, assertPathResolvesInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_IGNORE_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_PERMISSIONS_SCHEMA_URL as Pn, assertTreeContainsNoSymlinks as Pt, RulesyncIgnore as Q, stripControlCharactersKeepingLineFeeds as Qn, readFileContent as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Rn, directoryExists as Rt, COMMANDCODE_DIR as S, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, ALL_FEATURES as Un, getHomeDirectory as Ut, AUGMENTCODE_DIR as V, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Vn, findFilesByGlobs as Vt, RulesyncSubagent as W, ALL_FEATURES_WITH_WILDCARD as Wn, isFileNotFoundError as Wt, RulesyncPermissions as X, quoteForLog as Xn, pathEscapesRoot as Xt, RulesyncRuleFrontmatterSchema as Y, hasEnclosingMarkOutsideKeycap as Yn, listSubdirectoryNames as Yt, RulesyncMcp as Z, stripControlCharacters as Zn, posixRelativePathEscapesRoot as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, CURATED_RULES_FEATURE_SUBDIR as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, resolvePath as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, splitPathSegments as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, writeFileContent as dn, ConfigResolver as dt, removeDirectory as en, getRulesyncSourceCandidates as et, displayWidthOf as f, mapWithConcurrency as fn, mergeInputRootConfigs as ft, HooksProcessor as g, ToolTargetSchema as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, PACKAGING_TOOL_TARGETS as hn, ConfigFileSchema as ht, inspectInputRoots as i, removeTempDirectory as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_MCP_SCHEMA_URL as jn, applyFileMode as jt, caseFoldIdentity as k, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as kn, ErrorCodes as kt, SubagentsProcessor as l, toPosixPath as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ALL_TOOL_TARGETS_WITH_WILDCARD as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeFile as nn, parseJsonc as nt, convertFromTool as o, resolvedRelativePath as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, ALL_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, truncateText as qn, listDirectoryEntryNames as qt, generate as r, removeFileStrict as rn, RulesyncCommand as rt, isPackagingToolTarget as s, runWithDirectoryRollback as sn, stringifyFrontmatter as st, importFromTool as t, removeDirectoryStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, writeFileBuffer as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, FEATURE_SOURCE_TREE_ENTRIES as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CONFIG_SCHEMA_URL as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CHECKS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, MAX_FILE_SIZE as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as zn, ensureDir as zt };
106794
109677
 
106795
- //# sourceMappingURL=import-CoFTCNq7.js.map
109678
+ //# sourceMappingURL=import-Chn7Nqnm.js.map