rulesync 39.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.
@@ -1048,6 +1048,57 @@ async function assertWritablePathInsideRoot(params) {
1048
1048
  existingPath = parentPath;
1049
1049
  }
1050
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
+ }
1051
1102
  async function assertTreeContainsNoSymlinks(dirPath) {
1052
1103
  for (const entry of await readdir(dirPath, { withFileTypes: true })) {
1053
1104
  const entryPath = join(dirPath, entry.name);
@@ -5112,6 +5163,40 @@ const COPILOT_HOOK_EVENTS = [
5112
5163
  "userPromptExpansion"
5113
5164
  ];
5114
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
+ /**
5115
5200
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
5116
5201
  *
5117
5202
  * The CLI documents a wider event surface than the shared cloud-agent set, so
@@ -5265,12 +5350,15 @@ const KIRO_IDE_HOOK_EVENTS = [
5265
5350
  /**
5266
5351
  * Hook events supported by Google Antigravity (both the IDE and the CLI).
5267
5352
  *
5268
- * Antigravity exposes a Claude-style hooks surface covering the five
5269
- * tool/model/turn lifecycle events it documents: `PreToolUse`, `PostToolUse`,
5270
- * `PreInvocation`, `PostInvocation`, and `Stop`. The model-invocation events
5271
- * (`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
5272
5359
  */
5273
5360
  const ANTIGRAVITY_HOOK_EVENTS = [
5361
+ "sessionStart",
5274
5362
  "preToolUse",
5275
5363
  "postToolUse",
5276
5364
  "preModelInvocation",
@@ -6043,8 +6131,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
6043
6131
  /**
6044
6132
  * Hook events supported by Hermes Agent's native Shell Hooks system.
6045
6133
  *
6046
- * Hermes validates hook events against a fixed `VALID_HOOKS` set — 44 entries as
6047
- * 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
6048
6136
  * full list. Only the events with a clean 1:1 canonical equivalent are mapped
6049
6137
  * here. All other native events round-trip through `hermesagent.hooks`.
6050
6138
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
@@ -6068,15 +6156,16 @@ const HERMESAGENT_HOOK_EVENTS = [
6068
6156
  * observers, `gateway_platform_event` and `transform_api_error_classification`.
6069
6157
  * Shell hooks are gated on the same set.
6070
6158
  *
6071
- * 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
6072
6160
  * from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16` (v0.20.2) — v0.21
6073
6161
  * added `agent_loop_stopped` and `on_room_member_activity`, v0.21.5 added
6074
- * `pre_auxiliary_call` and `post_auxiliary_call`, and v0.21.6 added
6162
+ * `pre_auxiliary_call` and `post_auxiliary_call`, v0.21.6 added
6075
6163
  * `post_gateway_admission`, `on_human_input_request` and
6076
- * `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
6077
6166
  * {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
6078
6167
  *
6079
- * @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
6080
6169
  */
6081
6170
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6082
6171
  "pre_tool_call",
@@ -6100,6 +6189,7 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6100
6189
  "on_session_end",
6101
6190
  "on_session_finalize",
6102
6191
  "on_session_reset",
6192
+ "on_session_delete",
6103
6193
  "on_skill_lifecycle",
6104
6194
  "subagent_start",
6105
6195
  "subagent_stop",
@@ -6121,7 +6211,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6121
6211
  "on_kanban_task_updated",
6122
6212
  "on_kanban_dispatch_tick",
6123
6213
  "gateway_platform_event",
6124
- "pre_command"
6214
+ "pre_command",
6215
+ "post_compaction"
6125
6216
  ];
6126
6217
  /**
6127
6218
  * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
@@ -6133,8 +6224,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6133
6224
  * rulesync only ever writes shell hooks, so these are excluded from
6134
6225
  * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
6135
6226
  *
6136
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
6137
- * @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`
6138
6229
  */
6139
6230
  const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
6140
6231
  /**
@@ -6316,10 +6407,11 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6316
6407
  const AUGMENTCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
6317
6408
  /**
6318
6409
  * Map canonical camelCase event names to Antigravity PascalCase.
6319
- * 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,
6320
6411
  * plus `PreInvocation`/`PostInvocation` for the model-invocation lifecycle.
6321
6412
  */
6322
6413
  const CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES = {
6414
+ sessionStart: "SessionStart",
6323
6415
  preToolUse: "PreToolUse",
6324
6416
  postToolUse: "PostToolUse",
6325
6417
  preModelInvocation: "PreInvocation",
@@ -7338,14 +7430,19 @@ const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.st
7338
7430
  /**
7339
7431
  * Kimi Code's tool-scoped block also carries the `[mcp]` defaults from its
7340
7432
  * shared user config: connect and per-tool-call timeouts that apply to *every*
7341
- * MCP server, including ones rulesync did not write. Per-server
7342
- * `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).
7343
7439
  *
7344
7440
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
7345
7441
  */
7346
7442
  const kimiCodeScopedMcpSchema = z.extend(toolScopedMcpSchema, {
7347
7443
  startupTimeoutMs: z.optional(z.number()),
7348
- toolTimeoutMs: z.optional(z.number())
7444
+ toolTimeoutMs: z.optional(z.number()),
7445
+ defaultDeferred: z.optional(z.boolean())
7349
7446
  });
7350
7447
  /**
7351
7448
  * Qwen Code's tool-scoped block also carries the server-level allow/deny lists
@@ -8114,13 +8211,16 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
8114
8211
  });
8115
8212
  /**
8116
8213
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
8117
- * before prompting. `always-ask` auto-approves only read-only tools, `write`
8118
- * 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.
8119
8218
  *
8120
8219
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
8121
8220
  */
8122
8221
  const OMP_APPROVAL_MODES = [
8123
8222
  "always-ask",
8223
+ "always-ask-cached",
8124
8224
  "write",
8125
8225
  "yolo"
8126
8226
  ];
@@ -8141,10 +8241,13 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
8141
8241
  ])),
8142
8242
  /**
8143
8243
  * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
8144
- * default `true`). `false` turns off the built-in dangerous-command policy
8145
- * (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.
8146
8248
  *
8147
8249
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
8250
+ * @see https://github.com/MoonshotAI/kimi-code/pull/4250
8148
8251
  */
8149
8252
  dangerousCommandGuard: z.optional(z.boolean()),
8150
8253
  rules: z.optional(z.array(z.looseObject({
@@ -8504,6 +8607,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
8504
8607
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8505
8608
  * @example
8506
8609
  * { "mode": "deny" }
8610
+ * @example
8611
+ * { "tools": { "disabled": ["web_fetch"] }, "askOnExternalOrigin": true }
8507
8612
  */
8508
8613
  const ReasonixPermissionsOverrideSchema = z.looseObject({
8509
8614
  permission: z.optional(ToolScopedPermissionSchema),
@@ -8517,7 +8622,9 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
8517
8622
  "ask",
8518
8623
  "allow",
8519
8624
  "deny"
8520
- ]))
8625
+ ])),
8626
+ askOnExternalOrigin: z.optional(z.boolean()),
8627
+ tools: z.optional(z.strictObject({ disabled: z.optional(z.array(z.string())) }))
8521
8628
  });
8522
8629
  /**
8523
8630
  * Tool-scoped override block for Qoder. `outputStyle` is written to the
@@ -11331,6 +11438,15 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
11331
11438
  compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
11332
11439
  metadata: z.optional(z.looseObject({}))
11333
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
+ })),
11334
11450
  zcode: z.optional(z.looseObject({
11335
11451
  when_to_use: z.optional(z.string()),
11336
11452
  license: z.optional(z.string()),
@@ -11609,7 +11725,7 @@ async function getLocalSkillDirNames(sourceTree) {
11609
11725
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11610
11726
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11611
11727
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11612
- * 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
11613
11729
  * default with a per-target value. A defined section value (including `false`)
11614
11730
  * always wins over the root default.
11615
11731
  *
@@ -11629,7 +11745,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
11629
11745
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
11630
11746
  * that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11631
11747
  * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh, reasonix, commandcode,
11632
- * 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
11633
11749
  * per-target value. A defined section value (including `false`) always wins
11634
11750
  * over the root default.
11635
11751
  *
@@ -13944,7 +14060,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13944
14060
  ownedKeys: [
13945
14061
  "permissions",
13946
14062
  "sandbox",
13947
- "agent"
14063
+ "agent",
14064
+ "tools"
13948
14065
  ]
13949
14066
  }
13950
14067
  }
@@ -13965,7 +14082,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13965
14082
  ownedKeys: [
13966
14083
  "permissions",
13967
14084
  "sandbox",
13968
- "agent"
14085
+ "agent",
14086
+ "tools"
13969
14087
  ]
13970
14088
  }
13971
14089
  }
@@ -24090,6 +24208,7 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
24090
24208
  const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
24091
24209
  const REASONIX_GLOBAL_DIR = ".reasonix";
24092
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);
24093
24212
  const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
24094
24213
  const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
24095
24214
  const REASONIX_DIR = REASONIX_GLOBAL_DIR;
@@ -28437,25 +28556,65 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
28437
28556
  //#endregion
28438
28557
  //#region src/features/hooks/antigravity-hooks.ts
28439
28558
  /**
28440
- * Antigravity uses a Claude-Code-like `hooks.json` (matcher shape). The
28441
- * `PreToolUse`/`PostToolUse` events use the matcher-based array shape, while
28442
- * `PreInvocation`/`PostInvocation`/`Stop` are matcher-less handler lists.
28443
- * `projectDirVar` is empty because Antigravity does not document a
28444
- * 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.
28445
28586
  */
28446
28587
  const ANTIGRAVITY_CONVERTER_CONFIG = {
28447
28588
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28448
28589
  canonicalToToolEventNames: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
28449
28590
  toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
28450
28591
  projectDirVar: "",
28451
- noMatcherEvents: /* @__PURE__ */ new Set([
28452
- "preModelInvocation",
28453
- "postModelInvocation",
28454
- "stop"
28455
- ]),
28592
+ noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
28456
28593
  supportedHookTypes: /* @__PURE__ */ new Set(["command"])
28457
28594
  };
28458
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
+ /**
28459
28618
  * Antigravity's `hooks.json` is keyed by a named hook whose value holds the
28460
28619
  * per-event map (plus an optional `enabled` flag). rulesync writes a single
28461
28620
  * generated hook under this stable name.
@@ -28492,8 +28651,8 @@ function flattenAntigravityHooks(parsed) {
28492
28651
  * Hooks generator for Google Antigravity (both the IDE and the CLI).
28493
28652
  *
28494
28653
  * Antigravity writes a dedicated `hooks.json` keyed by a named hook whose
28495
- * value holds the Claude-Code-style event → matcher-entry map
28496
- * (`{ "<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
28497
28656
  * same shape; only the location differs (`.agents/hooks.json` vs
28498
28657
  * `~/.gemini/config/hooks.json`).
28499
28658
  *
@@ -28537,12 +28696,12 @@ var AntigravityHooks = class extends ToolHooks {
28537
28696
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28538
28697
  const paths = this.getSettablePaths({ global });
28539
28698
  const config = rulesyncHooks.getJson();
28540
- const eventMap = canonicalToToolHooks({
28699
+ const eventMap = toAntigravityEventMap(canonicalToToolHooks({
28541
28700
  config,
28542
28701
  toolOverrideHooks: config[this.getOverrideKey()]?.hooks,
28543
28702
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28544
28703
  logger
28545
- });
28704
+ }));
28546
28705
  const antigravityHooks = Object.keys(eventMap).length > 0 ? { [ANTIGRAVITY_HOOK_NAME]: eventMap } : {};
28547
28706
  const fileContent = JSON.stringify(antigravityHooks, null, 2);
28548
28707
  return new this({
@@ -28561,7 +28720,7 @@ var AntigravityHooks = class extends ToolHooks {
28561
28720
  throw new Error(`Failed to parse Antigravity hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28562
28721
  }
28563
28722
  const hooks = toolHooksToCanonical({
28564
- hooks: flattenAntigravityHooks(parsed),
28723
+ hooks: toMatcherGroupEventMap(flattenAntigravityHooks(parsed)),
28565
28724
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28566
28725
  logger
28567
28726
  });
@@ -31025,11 +31184,17 @@ var ContinueHooks = class extends SettingsJsonHooks {
31025
31184
  * entry that uses them instead of a shell field is not mistaken for one
31026
31185
  * without a command.
31027
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
+ *
31028
31191
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
31192
+ * @see https://code.visualstudio.com/docs/agent-customization/hooks
31029
31193
  * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
31030
31194
  */
31031
31195
  const CopilotHookEntrySchema = z.looseObject({
31032
31196
  type: z.string(),
31197
+ matcher: z.optional(z.string()),
31033
31198
  bash: z.optional(z.string()),
31034
31199
  powershell: z.optional(z.string()),
31035
31200
  command: z.optional(z.string()),
@@ -31063,8 +31228,10 @@ function canonicalToCopilotHooks(config) {
31063
31228
  ...sharedConfigHooks,
31064
31229
  ...config.copilot?.hooks
31065
31230
  };
31231
+ const matcherEvents = new Set(COPILOT_MATCHER_HOOK_EVENTS);
31066
31232
  const copilot = {};
31067
31233
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
31234
+ const matcherSupported = matcherEvents.has(eventName);
31068
31235
  const copilotEventName = lookupOwn({
31069
31236
  record: CANONICAL_TO_COPILOT_EVENT_NAMES,
31070
31237
  key: eventName
@@ -31072,7 +31239,7 @@ function canonicalToCopilotHooks(config) {
31072
31239
  const entries = [];
31073
31240
  for (const def of definitions) {
31074
31241
  const hookType = def.type ?? "command";
31075
- if (def.matcher) continue;
31242
+ if (def.matcher && !matcherSupported) continue;
31076
31243
  if (hookType !== "command") continue;
31077
31244
  const command = def.command;
31078
31245
  const timeout = def.timeout;
@@ -31080,6 +31247,7 @@ function canonicalToCopilotHooks(config) {
31080
31247
  const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
31081
31248
  entries.push({
31082
31249
  type: hookType,
31250
+ ...def.matcher && { matcher: def.matcher },
31083
31251
  ...command !== void 0 && command !== null && { [commandField]: command },
31084
31252
  ...def.env !== void 0 && { env: def.env },
31085
31253
  ...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
@@ -31174,6 +31342,7 @@ function copilotHooksToCanonical(copilotHooks, logger) {
31174
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.`);
31175
31343
  defs.push({
31176
31344
  type: "command",
31345
+ ...entry.matcher !== void 0 && entry.matcher !== "" && { matcher: entry.matcher },
31177
31346
  ...command !== void 0 && { command },
31178
31347
  ...shell !== void 0 && { shell },
31179
31348
  ...timeout !== void 0 && { timeout },
@@ -31317,21 +31486,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
31317
31486
  */
31318
31487
  /**
31319
31488
  * Canonical events whose Copilot CLI hook entries honor an optional `matcher`
31320
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`).
31321
- *
31322
- * Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
31323
- * honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
31324
- * `Edit|Write`) are now honored instead of silently dropped").
31325
- * @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}.
31326
31494
  */
31327
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
31328
- "preToolUse",
31329
- "postToolUse",
31330
- "notification",
31331
- "permissionRequest",
31332
- "preCompact",
31333
- "subagentStart"
31334
- ]);
31495
+ const COPILOTCLI_MATCHER_EVENTS = new Set(COPILOTCLI_MATCHER_HOOK_EVENTS);
31335
31496
  /** Human-readable list of the matcher-aware events, for the drop warning. */
31336
31497
  const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
31337
31498
  const CopilotCliHookEntrySchema = z.looseObject({
@@ -33467,7 +33628,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
33467
33628
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
33468
33629
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
33469
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.`);
33470
- 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.`);
33471
33632
  setHermesHookEntries({
33472
33633
  result,
33473
33634
  event: nativeEvent,
@@ -37557,7 +37718,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37557
37718
  },
37558
37719
  supportedEvents: COPILOT_HOOK_EVENTS,
37559
37720
  supportedHookTypes: ["command"],
37560
- supportsMatcher: false
37721
+ supportsMatcher: true,
37722
+ matcherEvents: COPILOT_MATCHER_HOOK_EVENTS
37561
37723
  }],
37562
37724
  ["copilotcli", {
37563
37725
  class: CopilotcliHooks,
@@ -37572,7 +37734,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37572
37734
  "prompt",
37573
37735
  "http"
37574
37736
  ],
37575
- supportsMatcher: false
37737
+ supportsMatcher: true
37576
37738
  }],
37577
37739
  ["kilo", {
37578
37740
  class: KiloHooks,
@@ -39962,6 +40124,31 @@ const REASONIX_READ_TOOL_NAME = "read_file";
39962
40124
  * treated as rulesync-owned on generate so the dormant copy is replaced.
39963
40125
  */
39964
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
+ }
39965
40152
  //#endregion
39966
40153
  //#region src/features/ignore/reasonix-ignore.ts
39967
40154
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -40045,7 +40232,12 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
40045
40232
  fileContent: rulesyncPatterns.join("\n")
40046
40233
  });
40047
40234
  }
40048
- 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
+ });
40049
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));
40050
40242
  const paths = this.getSettablePaths({ global });
40051
40243
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -40623,7 +40815,8 @@ var IgnoreProcessor = class extends FeatureProcessor {
40623
40815
  outputRoot: this.outputRoot,
40624
40816
  rulesyncIgnore,
40625
40817
  options: this.featureOptions,
40626
- global: this.global
40818
+ global: this.global,
40819
+ logger: this.logger
40627
40820
  });
40628
40821
  const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
40629
40822
  toolIgnore,
@@ -40973,13 +41166,31 @@ function renameServerField(servers, from, to) {
40973
41166
  return [name, Object.fromEntries(Object.entries(config).map(([key, value]) => key === from ? [to, value] : [key, value]))];
40974
41167
  }));
40975
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
+ }
40976
41173
  /**
40977
41174
  * Antigravity uses `serverUrl` (not the canonical `url`) for HTTP/SSE MCP
40978
41175
  * servers. Convert the canonical shape into Antigravity's on the way out, and
40979
- * 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
40980
41186
  */
40981
41187
  function toAntigravityMcpServers(servers) {
40982
- 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
+ }));
40983
41194
  }
40984
41195
  function toCanonicalMcpServers(servers) {
40985
41196
  return renameServerField(servers, "serverUrl", "url");
@@ -45169,6 +45380,60 @@ function applyDeepagentsCommand({ name, server, converted, logger }) {
45169
45380
  return true;
45170
45381
  }
45171
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
+ /**
45172
45437
  * Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
45173
45438
  * skip it.
45174
45439
  *
@@ -45191,7 +45456,7 @@ function toDeepagentsServer({ name, server, logger }) {
45191
45456
  reason: "the WebSocket transport, which deepagents does not support",
45192
45457
  logger
45193
45458
  });
45194
- const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, ...rest } = server;
45459
+ const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, oauth, ...rest } = server;
45195
45460
  const converted = { ...rest };
45196
45461
  if (!applyDeepagentsCommand({
45197
45462
  name,
@@ -45206,6 +45471,15 @@ function toDeepagentsServer({ name, server, logger }) {
45206
45471
  if (transport !== void 0) converted.transport = normalized;
45207
45472
  else converted.type = normalized;
45208
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
+ }
45209
45483
  if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
45210
45484
  toolName: TOOL_NAME,
45211
45485
  serverName: name,
@@ -45232,6 +45506,13 @@ function toRulesyncServer(server) {
45232
45506
  const converted = { ...rest };
45233
45507
  for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
45234
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
+ }
45235
45516
  return converted;
45236
45517
  }
45237
45518
  var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
@@ -46964,6 +47245,50 @@ function copyHermesOauth(source) {
46964
47245
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
46965
47246
  return Object.keys(oauth).length > 0 ? oauth : void 0;
46966
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
+ }
46967
47292
  const HERMES_OPAQUE_MAPPING_KEYS = [
46968
47293
  "sampling",
46969
47294
  "elicitation",
@@ -46975,13 +47300,18 @@ const HERMES_OPAQUE_MAPPING_KEYS = [
46975
47300
  * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
46976
47301
  * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
46977
47302
  * `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
46978
- * `lazy`, `keepalive_interval`, `elicitation`, `trust`, and `identity_header` —
47303
+ * `lazy`, `keepalive_interval`, `elicitation`, `trust`, `identity_header`, and
47304
+ * the cost keys (see {@link copyHermesCostFields}) —
46979
47305
  * verbatim from `source` to `target`. Field names are identical on both sides
46980
47306
  * (the canonical `McpServerSchema` is a `looseObject`), so this serves export
46981
47307
  * and import alike. See the Hermes mcp-config-reference.
46982
47308
  */
46983
- function copyHermesAdvancedFields(source, target) {
46984
- let copied = false;
47309
+ function copyHermesAdvancedFields(source, target, logger) {
47310
+ let copied = copyHermesCostFields({
47311
+ source,
47312
+ target,
47313
+ logger
47314
+ });
46985
47315
  if (typeof source.auth === "string") {
46986
47316
  target.auth = source.auth;
46987
47317
  copied = true;
@@ -47090,7 +47420,7 @@ function applyHermesToolsBlock(hermesTools, server) {
47090
47420
  * as Hermes's own `transport: sse`, without which Hermes would connect to it
47091
47421
  * over Streamable HTTP.
47092
47422
  */
47093
- function convertServerToHermes(config) {
47423
+ function convertServerToHermes(config, logger) {
47094
47424
  const out = {};
47095
47425
  const command = config.command;
47096
47426
  const url = resolveHermesUrl(config);
@@ -47113,7 +47443,7 @@ function convertServerToHermes(config) {
47113
47443
  if (config.disabled === true) out.enabled = false;
47114
47444
  const timeout = resolveHermesTimeout(config);
47115
47445
  if (timeout !== void 0) out.timeout = timeout;
47116
- copyHermesAdvancedFields(config, out);
47446
+ copyHermesAdvancedFields(config, out, logger);
47117
47447
  const tools = buildHermesToolsBlock(config);
47118
47448
  if (Object.keys(tools).length > 0) out.tools = tools;
47119
47449
  return out;
@@ -47121,11 +47451,11 @@ function convertServerToHermes(config) {
47121
47451
  /**
47122
47452
  * Converts rulesync canonical MCP servers into Hermes `mcp_servers:` entries.
47123
47453
  */
47124
- function convertToHermesFormat(mcpServers) {
47454
+ function convertToHermesFormat(mcpServers, logger) {
47125
47455
  const result = {};
47126
47456
  for (const [name, config] of Object.entries(mcpServers)) {
47127
47457
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
47128
- result[name] = convertServerToHermes(config);
47458
+ result[name] = convertServerToHermes(config, logger);
47129
47459
  }
47130
47460
  return result;
47131
47461
  }
@@ -47145,7 +47475,7 @@ function mergeHermesMcpServers(config, mcpServers) {
47145
47475
  * Mirrors {@link convertToHermesFormat}: `enabled: false` maps back to the
47146
47476
  * canonical `disabled: true`, and only recognized fields are carried over.
47147
47477
  */
47148
- function convertFromHermesFormat(mcpServers) {
47478
+ function convertFromHermesFormat(mcpServers, logger) {
47149
47479
  const result = {};
47150
47480
  const hermesOverrides = {};
47151
47481
  for (const [name, config] of Object.entries(mcpServers)) {
@@ -47162,7 +47492,7 @@ function convertFromHermesFormat(mcpServers) {
47162
47492
  if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
47163
47493
  result[name] = server;
47164
47494
  const hermesServer = { ...server };
47165
- if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
47495
+ if (copyHermesAdvancedFields(config, hermesServer, logger)) hermesOverrides[name] = hermesServer;
47166
47496
  }
47167
47497
  return {
47168
47498
  mcpServers: result,
@@ -47181,8 +47511,10 @@ function convertFromHermesFormat(mcpServers) {
47181
47511
  */
47182
47512
  var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47183
47513
  config;
47184
- constructor(params) {
47514
+ logger;
47515
+ constructor({ logger, ...params }) {
47185
47516
  super(params);
47517
+ this.logger = logger;
47186
47518
  this.config = this.fileContent !== void 0 ? parseSharedConfig({
47187
47519
  format: "yaml",
47188
47520
  fileContent: this.fileContent
@@ -47226,7 +47558,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47226
47558
  static getExtraSharedWritePaths() {
47227
47559
  return getHermesagentSharedConfigWritePaths();
47228
47560
  }
47229
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
47561
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
47230
47562
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
47231
47563
  const paths = this.getSettablePaths({ global });
47232
47564
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
@@ -47236,17 +47568,18 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47236
47568
  relativeFilePath: paths.relativeFilePath,
47237
47569
  fileContent,
47238
47570
  validate,
47239
- global
47571
+ global,
47572
+ logger
47240
47573
  });
47241
47574
  }
47242
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
47575
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
47243
47576
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
47244
47577
  const paths = this.getSettablePaths({ global });
47245
47578
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
47246
47579
  const merged = mergeHermesMcpServers(parseSharedConfig({
47247
47580
  format: "yaml",
47248
47581
  fileContent
47249
- }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
47582
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers(), logger));
47250
47583
  return new HermesagentMcp({
47251
47584
  outputRoot,
47252
47585
  relativeDirPath: paths.relativeDirPath,
@@ -47258,11 +47591,12 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47258
47591
  patch: { mcp_servers: merged.mcp_servers }
47259
47592
  }),
47260
47593
  validate,
47261
- global
47594
+ global,
47595
+ logger
47262
47596
  });
47263
47597
  }
47264
47598
  toRulesyncMcp() {
47265
- 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);
47266
47600
  return this.toRulesyncMcpDefault({
47267
47601
  outputRoot: getHermesagentRulesyncOutputRoot({
47268
47602
  nativeOutputRoot: this.outputRoot,
@@ -48146,14 +48480,15 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
48146
48480
  if (!config.parsed) return {};
48147
48481
  return {
48148
48482
  ...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
48149
- ...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 }
48150
48485
  };
48151
48486
  }
48152
48487
  /**
48153
48488
  * Kimi's `[mcp]` section in the shared user config sets the default connect and
48154
- * per-tool-call timeouts for *every* MCP server, including ones rulesync did not
48155
- * write. Per-server values in `mcp.json` still take precedence; this only moves
48156
- * 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.
48157
48492
  *
48158
48493
  * Written as an auxiliary file so it goes through the normal write phase and
48159
48494
  * respects `--dry-run`, and merged through the shared-config gateway so the
@@ -48161,7 +48496,8 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
48161
48496
  *
48162
48497
  * The gateway replaces an owned key wholesale, and `mcp` is a table, so the
48163
48498
  * section is recomputed from the existing file: authoring only
48164
- * `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
48165
48501
  * approach the `.vibe/config.toml` `tools` writer takes.
48166
48502
  *
48167
48503
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
@@ -48188,7 +48524,8 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
48188
48524
  patch: { mcp: {
48189
48525
  ...existingSection,
48190
48526
  ...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
48191
- ...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 }
48192
48529
  } },
48193
48530
  filePath: relativeConfigPath
48194
48531
  });
@@ -48286,18 +48623,20 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
48286
48623
  if (!isRecord$1(block)) return [];
48287
48624
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
48288
48625
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
48289
- 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 [];
48290
48628
  const existing = await readKimiCodeConfig({ outputRoot });
48291
48629
  if (!existing.parsed) {
48292
48630
  const paths = kimiCodeConfigRelativePath();
48293
- 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.`);
48294
48632
  return [];
48295
48633
  }
48296
48634
  return [KimiCodeMcpConfigToml.fromDefaults({
48297
48635
  outputRoot,
48298
48636
  defaults: {
48299
48637
  startupTimeoutMs,
48300
- toolTimeoutMs
48638
+ toolTimeoutMs,
48639
+ defaultDeferred
48301
48640
  },
48302
48641
  existing
48303
48642
  })];
@@ -50873,6 +51212,11 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
50873
51212
  });
50874
51213
  }
50875
51214
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
51215
+ await warnIfReasonixNestedProjectConfigShadowed({
51216
+ outputRoot,
51217
+ global,
51218
+ logger
51219
+ });
50876
51220
  const paths = this.getSettablePaths({ global });
50877
51221
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50878
51222
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -52900,6 +53244,144 @@ function buildCommandString(server) {
52900
53244
  if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
52901
53245
  return [server.command, ...args.map(quoteShellWord)].join(" ");
52902
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
+ }
52903
53385
  /** Split a plain command string back into `command` + `args` when it is safe to. */
52904
53386
  function splitCommandString(command) {
52905
53387
  const trimmed = command.trim();
@@ -52943,10 +53425,20 @@ function convertServerToXum({ name, server, logger }) {
52943
53425
  ...settings
52944
53426
  };
52945
53427
  }
52946
- const command = buildCommandString(server);
52947
- if (command === void 0) return skip("it has no command");
52948
- const dropped = [...isPlainObject$1(server.env) && Object.keys(server.env).length > 0 ? ["env"] : [], ...typeof server.cwd === "string" ? ["cwd"] : []];
52949
- if (dropped.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets ${dropped.join(" and ")}, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers with your shell environment in the workspace directory).`);
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).`);
52950
53442
  return Object.keys(settings).length > 0 ? {
52951
53443
  command,
52952
53444
  ...settings
@@ -52966,7 +53458,7 @@ function convertFromXumFormat(servers) {
52966
53458
  for (const [name, entry] of Object.entries(servers)) {
52967
53459
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
52968
53460
  if (typeof entry === "string") {
52969
- result[name] = splitCommandString(entry);
53461
+ result[name] = parseCommandString(entry);
52970
53462
  continue;
52971
53463
  }
52972
53464
  if (!isPlainObject$1(entry)) continue;
@@ -52976,7 +53468,7 @@ function convertFromXumFormat(servers) {
52976
53468
  server.url = entry.url;
52977
53469
  const headers = copyStringHeaders(entry.headers);
52978
53470
  if (headers !== void 0) server.headers = headers;
52979
- } else if (typeof entry.command === "string") Object.assign(server, splitCommandString(entry.command));
53471
+ } else if (typeof entry.command === "string") Object.assign(server, parseCommandString(entry.command));
52980
53472
  else continue;
52981
53473
  if (entry.disabled === true) server.disabled = true;
52982
53474
  if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
@@ -52991,9 +53483,10 @@ function convertFromXumFormat(servers) {
52991
53483
  * (project; a server there wins over the user one of the same name). Both are
52992
53484
  * a JSONC document with a `servers` map:
52993
53485
  *
52994
- * - a stdio server is a shell command string (the object form `{ command }`
52995
- * carries `disabled` / `toolAllowlist`); Xum's mcp.jsonc has no `env` or
52996
- * `cwd`, so those are dropped with a warning;
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;
52997
53490
  * - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
52998
53491
  * (the transport defaults to auto-detection).
52999
53492
  *
@@ -53125,7 +53618,7 @@ function asZcodeRemoteType(stated, url) {
53125
53618
  }
53126
53619
  /**
53127
53620
  * The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
53128
- * 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
53129
53622
  * runtime config schema, since ZCode's MCP page only says to "enable OAuth for
53130
53623
  * the service in its configuration". Both variants are strict, so any other
53131
53624
  * key would make ZCode skip the server.
@@ -53229,7 +53722,11 @@ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
53229
53722
  }
53230
53723
  /**
53231
53724
  * The per-server keys ZCode reads on every transport: `timeoutMs`,
53232
- * `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.
53233
53730
  */
53234
53731
  function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53235
53732
  const common = {};
@@ -53242,6 +53739,9 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53242
53739
  const { protocolVersion } = config;
53243
53740
  if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
53244
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.`);
53245
53745
  if (config.disabled === true) common[enableKey] = false;
53246
53746
  return common;
53247
53747
  }
@@ -53254,11 +53754,11 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53254
53754
  * absent.
53255
53755
  *
53256
53756
  * Every server also carries `timeoutMs` (from the canonical `timeout`, or an
53257
- * explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
53258
- * carry `cwd`, and remote servers an `oauth` built from the namespaced
53259
- * `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
53260
53760
  * is read from the authored server through `rawServerOf`. ZCode's docs do not
53261
- * 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
53262
53762
  * schema, which is strict: an unknown key makes ZCode skip the server, which
53263
53763
  * is why the output stays a whitelist.
53264
53764
  *
@@ -58395,6 +58895,11 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
58395
58895
  reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
58396
58896
  widens: isNotFalse
58397
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
+ },
58398
58903
  {
58399
58904
  path: ["allowUnsandboxedCommands"],
58400
58905
  reason: "controls whether Claude may retry a blocked command outside the sandbox",
@@ -68844,7 +69349,7 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
68844
69349
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
68845
69350
  *
68846
69351
  * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
68847
- * `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
68848
69353
  * import a recognized value is read back into the override.
68849
69354
  *
68850
69355
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
@@ -71116,6 +71621,8 @@ function buildReasonixPermissionEntry(toolName, pattern) {
71116
71621
  const REASONIX_TOOL_LABEL = "Reasonix";
71117
71622
  /** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
71118
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";
71119
71626
  /**
71120
71627
  * `[sandbox]` keys whose authored value loosens the enforcement layer beneath
71121
71628
  * the permission policy: they take Bash out of its OS jail, open that jail to
@@ -71318,6 +71825,95 @@ function applyPermissionsMode({ permissions, authored, global, relativeFilePath,
71318
71825
  reason: "lets every writer call that no rule matches run without a prompt"
71319
71826
  }];
71320
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
+ }
71321
71917
  function parseReasonixConfig(fileContent) {
71322
71918
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
71323
71919
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -71373,6 +71969,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71373
71969
  });
71374
71970
  }
71375
71971
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
71972
+ await warnIfReasonixNestedProjectConfigShadowed({
71973
+ outputRoot,
71974
+ global,
71975
+ logger
71976
+ });
71376
71977
  const paths = this.getSettablePaths({ global });
71377
71978
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
71378
71979
  const relativeFilePathForLog = toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath));
@@ -71432,7 +72033,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71432
72033
  relativeFilePath: relativeFilePathForLog,
71433
72034
  logger
71434
72035
  }));
72036
+ trustAffecting.push(...applyAskOnExternalOrigin({
72037
+ permissions: mergedPermissions,
72038
+ override,
72039
+ global,
72040
+ relativeFilePath: relativeFilePathForLog,
72041
+ logger
72042
+ }));
71435
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);
71436
72050
  if (override?.sandbox !== void 0) {
71437
72051
  const authoredSandbox = dropUserOnlySandboxKeys({
71438
72052
  sandbox: override.sandbox,
@@ -71506,6 +72120,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71506
72120
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
71507
72121
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
71508
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
+ }
71509
72131
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
71510
72132
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
71511
72133
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -73087,7 +73709,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
73087
73709
  const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
73088
73710
  const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
73089
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.`);
73090
- const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
73712
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : dropUntrustedProviderOptions({
73713
+ providerOptions: authoredProviderOptions,
73714
+ global,
73715
+ filePath,
73716
+ logger
73717
+ });
73091
73718
  const authoredPolicies = pickSecurityPolicies(override, {
73092
73719
  filePath,
73093
73720
  logger,
@@ -73591,6 +74218,68 @@ function taktModeToRulesyncConfig(mode) {
73591
74218
  default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
73592
74219
  }
73593
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
+ }
73594
74283
  /**
73595
74284
  * Lift Takt's security-policy keys out of a source object, keeping only the
73596
74285
  * shapes Takt itself accepts (a boolean, or a table of booleans). Used in both
@@ -85920,6 +86609,19 @@ var WarpSkill = class WarpSkill extends ToolSkill {
85920
86609
  //#endregion
85921
86610
  //#region src/features/skills/xum-skill.ts
85922
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
+ /**
85923
86625
  * Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
85924
86626
  * required `name` and `description` frontmatter; the directory name is
85925
86627
  * lowercase letters, digits and single hyphens up to 64 characters, and
@@ -85927,12 +86629,82 @@ var WarpSkill = class WarpSkill extends ToolSkill {
85927
86629
  * Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
85928
86630
  * Within one directory `.xum/skills` wins over `.agents/skills`.
85929
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
+ *
85930
86639
  * @see https://xum.cdr.dev/agents/agent-skills
85931
86640
  */
85932
- var XumSkill = class extends AgentsSkillsSkill {
86641
+ var XumSkill = class XumSkill extends AgentsSkillsSkill {
85933
86642
  static getSettablePaths(_options = {}) {
85934
86643
  return { relativeDirPath: XUM_SKILLS_DIR_PATH };
85935
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
+ }
85936
86708
  static isTargetedByRulesyncSkill(rulesyncSkill) {
85937
86709
  const targets = rulesyncSkill.getFrontmatter().targets;
85938
86710
  return targets.includes("*") || targets.includes("xum");
@@ -92088,6 +92860,15 @@ const KiroCliSubagentJsonSchema = z.looseObject({
92088
92860
  allowedTools: z.optional(z.nullable(z.array(z.string()))),
92089
92861
  includeMcpJson: z.optional(z.nullable(z.boolean()))
92090
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
+ }
92091
92872
  var KiroSubagent = class extends ToolSubagent {
92092
92873
  body;
92093
92874
  constructor({ body, ...rest }) {
@@ -92123,7 +92904,7 @@ var KiroSubagent = class extends ToolSubagent {
92123
92904
  outputRoot: ".",
92124
92905
  frontmatter: {
92125
92906
  targets: [toolTarget],
92126
- name: name ?? basename(this.getRelativeFilePath(), ".json"),
92907
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
92127
92908
  description: description ?? void 0,
92128
92909
  ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
92129
92910
  },
@@ -92273,7 +93054,7 @@ var KiroIdeSubagent = class KiroIdeSubagent extends ToolSubagent {
92273
93054
  outputRoot: ".",
92274
93055
  frontmatter: {
92275
93056
  targets: ["*"],
92276
- name: name ?? this.getRelativeFilePath().replace(/\.md$/, ""),
93057
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
92277
93058
  description: description ?? "",
92278
93059
  ...Object.keys(kiroIdeSection).length > 0 && { "kiro-ide": kiroIdeSection }
92279
93060
  },
@@ -94891,7 +95672,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
94891
95672
  supportsProject: true,
94892
95673
  supportsSimulated: false,
94893
95674
  supportsGlobal: true,
94894
- filePattern: "*.json"
95675
+ filePattern: "**/*.json",
95676
+ supportsNestedPaths: true
94895
95677
  }
94896
95678
  }],
94897
95679
  ["kiro-ide", {
@@ -94900,7 +95682,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
94900
95682
  supportsProject: true,
94901
95683
  supportsSimulated: false,
94902
95684
  supportsGlobal: true,
94903
- filePattern: "*.md"
95685
+ filePattern: "**/*.md",
95686
+ supportsNestedPaths: true
94904
95687
  }
94905
95688
  }],
94906
95689
  ["lettacode", {
@@ -108890,6 +109673,6 @@ async function importChecksCore(params) {
108890
109673
  return writtenCount;
108891
109674
  }
108892
109675
  //#endregion
108893
- export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, CRUSH_LOCAL_RULE_FILE_NAME as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, LETTACODE_DIR as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, CommandsProcessor as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, REASONIX_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
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 };
108894
109677
 
108895
- //# sourceMappingURL=import-CfVOkld5.js.map
109678
+ //# sourceMappingURL=import-Chn7Nqnm.js.map