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.
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
457
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
458
458
  //#endregion
459
459
  //#region src/constants/rulesync-paths.ts
460
- const { join: join$439 } = node_path.posix;
460
+ const { join: join$441 } = node_path.posix;
461
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
462
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
463
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
464
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
465
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
466
466
  const RULES_FEATURE_SUBDIR = "rules";
467
- const CURATED_RULES_FEATURE_SUBDIR = join$439(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$441(RULES_FEATURE_SUBDIR, ".curated");
468
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
469
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
470
470
  const CHECKS_FEATURE_SUBDIR = "checks";
471
471
  const SKILLS_FEATURE_SUBDIR = "skills";
472
- const CURATED_SKILLS_FEATURE_SUBDIR = join$439(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$439(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$441(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$441(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
486
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
487
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
488
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$439(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$441(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
490
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
491
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
492
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -1070,6 +1070,57 @@ async function assertWritablePathInsideRoot(params) {
1070
1070
  existingPath = parentPath;
1071
1071
  }
1072
1072
  }
1073
+ /**
1074
+ * Thrown by {@link assertPathResolvesInsideRoot} when a symbolic link leads
1075
+ * the target outside the root, or the target is reached through a dangling
1076
+ * link. Any other error it throws comes from the filesystem itself.
1077
+ */
1078
+ var PathOutsideRootError = class extends Error {
1079
+ constructor(message, options) {
1080
+ super(message, options);
1081
+ this.name = "PathOutsideRootError";
1082
+ }
1083
+ };
1084
+ /**
1085
+ * Refuse a write or removal target whose real location is outside `rootPath`.
1086
+ *
1087
+ * Unlike {@link assertWritablePathInsideRoot}, a symbolic link on the way is
1088
+ * allowed as long as it resolves inside the root: projects deliberately link a
1089
+ * skill directory to a shared one next to it. What this stops is a link that
1090
+ * leads elsewhere — a cloned repository committing `.rulesync/skills/x ->
1091
+ * ~/.ssh`, say — which a textual check such as {@link checkPathTraversal}
1092
+ * cannot see. The deepest existing ancestor of `targetPath` is resolved, and
1093
+ * an existing target that is itself a link is resolved too.
1094
+ */
1095
+ async function assertPathResolvesInsideRoot(params) {
1096
+ const { rootPath, targetPath } = params;
1097
+ let realRoot;
1098
+ try {
1099
+ realRoot = await (0, node_fs_promises.realpath)(rootPath);
1100
+ } catch (error) {
1101
+ if (isFileNotFoundError(error)) {
1102
+ if (await isSymlink(rootPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${rootPath}.`, { cause: error });
1103
+ return;
1104
+ }
1105
+ throw error;
1106
+ }
1107
+ let existingPath = (0, node_path.resolve)(targetPath);
1108
+ while (true) {
1109
+ let realExisting;
1110
+ try {
1111
+ realExisting = await (0, node_fs_promises.realpath)(existingPath);
1112
+ } catch (error) {
1113
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
1114
+ if (await isSymlink(existingPath)) throw new PathOutsideRootError(`Refusing to write through a dangling symbolic link: ${targetPath}.`, { cause: error });
1115
+ const parentPath = (0, node_path.dirname)(existingPath);
1116
+ if (parentPath === existingPath) throw error;
1117
+ existingPath = parentPath;
1118
+ continue;
1119
+ }
1120
+ if (pathEscapesRoot((0, node_path.relative)(realRoot, realExisting))) throw new PathOutsideRootError(`Path resolves outside ${rootPath} through a symbolic link: ${targetPath}.`);
1121
+ return;
1122
+ }
1123
+ }
1073
1124
  async function assertTreeContainsNoSymlinks(dirPath) {
1074
1125
  for (const entry of await (0, node_fs_promises.readdir)(dirPath, { withFileTypes: true })) {
1075
1126
  const entryPath = (0, node_path.join)(dirPath, entry.name);
@@ -5134,6 +5185,40 @@ const COPILOT_HOOK_EVENTS = [
5134
5185
  "userPromptExpansion"
5135
5186
  ];
5136
5187
  /**
5188
+ * The {@link COPILOT_HOOK_EVENTS} whose hook entries honor an optional
5189
+ * `matcher` regex. The hooks reference documents matcher filtering on
5190
+ * `preToolUse` / `postToolUse` / `permissionRequest` (tool name), `preCompact`
5191
+ * (trigger) and `subagentStart` (agent name), plus `notification`; the last
5192
+ * two are not authorable for `copilot`, so they are not listed here. VS Code's
5193
+ * migration guide confirms the Copilot harness "applies documented matcher
5194
+ * rules" (only the legacy Local harness ignores them).
5195
+ *
5196
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
5197
+ * @see https://code.visualstudio.com/docs/agent-customization/migrate-customizations
5198
+ */
5199
+ const COPILOT_MATCHER_HOOK_EVENTS = [
5200
+ "preToolUse",
5201
+ "postToolUse",
5202
+ "preCompact",
5203
+ "subagentStart"
5204
+ ];
5205
+ /**
5206
+ * The {@link COPILOTCLI_HOOK_EVENTS} whose hook entries honor an optional
5207
+ * `matcher` regex: the shared {@link COPILOT_MATCHER_HOOK_EVENTS} plus the two
5208
+ * CLI-only events, `permissionRequest` (tool name) and `notification`
5209
+ * (notification type).
5210
+ *
5211
+ * Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
5212
+ * honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
5213
+ * `Edit|Write`) are now honored instead of silently dropped").
5214
+ * @see https://docs.github.com/en/copilot/reference/hooks-reference
5215
+ */
5216
+ const COPILOTCLI_MATCHER_HOOK_EVENTS = [
5217
+ ...COPILOT_MATCHER_HOOK_EVENTS,
5218
+ "permissionRequest",
5219
+ "notification"
5220
+ ];
5221
+ /**
5137
5222
  * Hook events supported by the GitHub Copilot CLI (`copilotcli-hooks.ts`).
5138
5223
  *
5139
5224
  * The CLI documents a wider event surface than the shared cloud-agent set, so
@@ -5287,12 +5372,15 @@ const KIRO_IDE_HOOK_EVENTS = [
5287
5372
  /**
5288
5373
  * Hook events supported by Google Antigravity (both the IDE and the CLI).
5289
5374
  *
5290
- * Antigravity exposes a Claude-style hooks surface covering the five
5291
- * tool/model/turn lifecycle events it documents: `PreToolUse`, `PostToolUse`,
5292
- * `PreInvocation`, `PostInvocation`, and `Stop`. The model-invocation events
5293
- * (`PreInvocation`/`PostInvocation`) and `Stop` are matcher-less handler lists.
5375
+ * Antigravity exposes a Claude-style hooks surface covering the six
5376
+ * session/tool/model/turn lifecycle events it documents: `SessionStart`,
5377
+ * `PreToolUse`, `PostToolUse`, `PreInvocation`, `PostInvocation`, and `Stop`.
5378
+ * `SessionStart`, the model-invocation events (`PreInvocation`/`PostInvocation`)
5379
+ * and `Stop` are matcher-less handler lists.
5380
+ * https://antigravity.google/docs/hooks
5294
5381
  */
5295
5382
  const ANTIGRAVITY_HOOK_EVENTS = [
5383
+ "sessionStart",
5296
5384
  "preToolUse",
5297
5385
  "postToolUse",
5298
5386
  "preModelInvocation",
@@ -6065,8 +6153,8 @@ const CONTINUE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANO
6065
6153
  /**
6066
6154
  * Hook events supported by Hermes Agent's native Shell Hooks system.
6067
6155
  *
6068
- * Hermes validates hook events against a fixed `VALID_HOOKS` set — 44 entries as
6069
- * of v0.21.6 (tag `v0.21.6`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
6156
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set — 46 entries as
6157
+ * of v0.22.3 (tag `v0.22.3`); see {@link HERMESAGENT_NATIVE_HOOK_EVENTS} for the
6070
6158
  * full list. Only the events with a clean 1:1 canonical equivalent are mapped
6071
6159
  * here. All other native events round-trip through `hermesagent.hooks`.
6072
6160
  * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
@@ -6090,15 +6178,16 @@ const HERMESAGENT_HOOK_EVENTS = [
6090
6178
  * observers, `gateway_platform_event` and `transform_api_error_classification`.
6091
6179
  * Shell hooks are gated on the same set.
6092
6180
  *
6093
- * Verified against the tag `v0.21.6`: `VALID_HOOKS` holds 44 entries, grown
6181
+ * Verified against the tag `v0.22.3`: `VALID_HOOKS` holds 46 entries, grown
6094
6182
  * from 23 at `v2026.8.3` (v0.20.0) and 37 at `v2026.8.16` (v0.20.2) — v0.21
6095
6183
  * added `agent_loop_stopped` and `on_room_member_activity`, v0.21.5 added
6096
- * `pre_auxiliary_call` and `post_auxiliary_call`, and v0.21.6 added
6184
+ * `pre_auxiliary_call` and `post_auxiliary_call`, v0.21.6 added
6097
6185
  * `post_gateway_admission`, `on_human_input_request` and
6098
- * `on_human_input_resolved`. This list holds 43 of them — see
6186
+ * `on_human_input_resolved`, and v0.22.3 added `on_session_delete` and
6187
+ * `post_compaction`. This list holds 45 of them — see
6099
6188
  * {@link HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS} for the exclusion.
6100
6189
  *
6101
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/hermes_cli/plugins.py
6190
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py
6102
6191
  */
6103
6192
  const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6104
6193
  "pre_tool_call",
@@ -6122,6 +6211,7 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6122
6211
  "on_session_end",
6123
6212
  "on_session_finalize",
6124
6213
  "on_session_reset",
6214
+ "on_session_delete",
6125
6215
  "on_skill_lifecycle",
6126
6216
  "subagent_start",
6127
6217
  "subagent_stop",
@@ -6143,7 +6233,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6143
6233
  "on_kanban_task_updated",
6144
6234
  "on_kanban_dispatch_tick",
6145
6235
  "gateway_platform_event",
6146
- "pre_command"
6236
+ "pre_command",
6237
+ "post_compaction"
6147
6238
  ];
6148
6239
  /**
6149
6240
  * `VALID_HOOKS` entries a SHELL hook may not register for. `VALID_HOOKS` doubles
@@ -6155,8 +6246,8 @@ const HERMESAGENT_NATIVE_HOOK_EVENTS = [
6155
6246
  * rulesync only ever writes shell hooks, so these are excluded from
6156
6247
  * {@link HERMESAGENT_NATIVE_HOOK_EVENTS} and warned about with their own message.
6157
6248
  *
6158
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
6159
- * @see https://github.com/NousResearch/hermes-agent/blob/v0.21.6/agent/shell_hooks.py — `_parse_hooks_block`
6249
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/hermes_cli/plugins.py — `SHELL_UNSUPPORTED_HOOKS`
6250
+ * @see https://github.com/NousResearch/hermes-agent/blob/v0.22.3/agent/shell_hooks.py — `_parse_hooks_block`
6160
6251
  */
6161
6252
  const HERMESAGENT_SHELL_UNSUPPORTED_HOOK_EVENTS = ["transform_api_error_classification"];
6162
6253
  /**
@@ -6338,10 +6429,11 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6338
6429
  const AUGMENTCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENTCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
6339
6430
  /**
6340
6431
  * Map canonical camelCase event names to Antigravity PascalCase.
6341
- * Antigravity uses the same PascalCase names as Claude for its tool/turn events,
6432
+ * Antigravity uses the same PascalCase names as Claude for its session/tool/turn events,
6342
6433
  * plus `PreInvocation`/`PostInvocation` for the model-invocation lifecycle.
6343
6434
  */
6344
6435
  const CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES = {
6436
+ sessionStart: "SessionStart",
6345
6437
  preToolUse: "PreToolUse",
6346
6438
  postToolUse: "PostToolUse",
6347
6439
  preModelInvocation: "PreInvocation",
@@ -7360,14 +7452,19 @@ const toolScopedMcpSchema = zod_mini.z.looseObject({ mcpServers: zod_mini.z.opti
7360
7452
  /**
7361
7453
  * Kimi Code's tool-scoped block also carries the `[mcp]` defaults from its
7362
7454
  * shared user config: connect and per-tool-call timeouts that apply to *every*
7363
- * MCP server, including ones rulesync did not write. Per-server
7364
- * `startupTimeoutMs` / `toolTimeoutMs` in `mcpServers` still win.
7455
+ * MCP server, including ones rulesync did not write, and (Kimi Code 2.3.0)
7456
+ * `defaultDeferred`, which loads every server's tools on demand unless the
7457
+ * server opts out. Per-server `startupTimeoutMs` / `toolTimeoutMs` / `deferred`
7458
+ * in `mcpServers` still win. `defaultDeferred` only takes effect when Kimi's
7459
+ * experimental `tool-select` flag is on, and is captured when a server connects
7460
+ * (MoonshotAI/kimi-code#4210).
7365
7461
  *
7366
7462
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
7367
7463
  */
7368
7464
  const kimiCodeScopedMcpSchema = zod_mini.z.extend(toolScopedMcpSchema, {
7369
7465
  startupTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
7370
- toolTimeoutMs: zod_mini.z.optional(zod_mini.z.number())
7466
+ toolTimeoutMs: zod_mini.z.optional(zod_mini.z.number()),
7467
+ defaultDeferred: zod_mini.z.optional(zod_mini.z.boolean())
7371
7468
  });
7372
7469
  /**
7373
7470
  * Qwen Code's tool-scoped block also carries the server-level allow/deny lists
@@ -8136,13 +8233,16 @@ const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
8136
8233
  });
8137
8234
  /**
8138
8235
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
8139
- * before prompting. `always-ask` auto-approves only read-only tools, `write`
8140
- * also workspace writes, and `yolo` (oh-my-pi's default) every tier.
8236
+ * before prompting. `always-ask` auto-approves only read-only tools,
8237
+ * `always-ask-cached` (Ask and remember) also prompts before file reads and
8238
+ * lets a prompt remember the granted file or folder, `write` also
8239
+ * auto-approves workspace writes, and `yolo` (oh-my-pi's default) every tier.
8141
8240
  *
8142
8241
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
8143
8242
  */
8144
8243
  const OMP_APPROVAL_MODES = [
8145
8244
  "always-ask",
8245
+ "always-ask-cached",
8146
8246
  "write",
8147
8247
  "yolo"
8148
8248
  ];
@@ -8163,10 +8263,13 @@ const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
8163
8263
  ])),
8164
8264
  /**
8165
8265
  * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
8166
- * default `true`). `false` turns off the built-in dangerous-command policy
8167
- * (no confirmation for `rm -rf`, `shutdown` and the like).
8266
+ * default `true`). Since Kimi Code 2.3.0, `false` turns off every built-in
8267
+ * guard confirmation policy: dangerous commands (`rm -rf`, `shutdown` and the
8268
+ * like), sensitive-file access (`.env`, `id_rsa`, ...) and git control-path
8269
+ * access. Before 2.3.0 it only covered dangerous commands.
8168
8270
  *
8169
8271
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
8272
+ * @see https://github.com/MoonshotAI/kimi-code/pull/4250
8170
8273
  */
8171
8274
  dangerousCommandGuard: zod_mini.z.optional(zod_mini.z.boolean()),
8172
8275
  rules: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
@@ -8526,6 +8629,8 @@ const TabninePermissionsOverrideSchema = zod_mini.z.looseObject({
8526
8629
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8527
8630
  * @example
8528
8631
  * { "mode": "deny" }
8632
+ * @example
8633
+ * { "tools": { "disabled": ["web_fetch"] }, "askOnExternalOrigin": true }
8529
8634
  */
8530
8635
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8531
8636
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
@@ -8539,7 +8644,9 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8539
8644
  "ask",
8540
8645
  "allow",
8541
8646
  "deny"
8542
- ]))
8647
+ ])),
8648
+ askOnExternalOrigin: zod_mini.z.optional(zod_mini.z.boolean()),
8649
+ tools: zod_mini.z.optional(zod_mini.z.strictObject({ disabled: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))
8543
8650
  });
8544
8651
  /**
8545
8652
  * Tool-scoped override block for Qoder. `outputStyle` is written to the
@@ -11353,6 +11460,15 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
11353
11460
  compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
11354
11461
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
11355
11462
  })),
11463
+ xum: zod_mini.z.optional(zod_mini.z.looseObject({
11464
+ "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
11465
+ advertise: zod_mini.z.optional(zod_mini.z.boolean()),
11466
+ "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
11467
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string()),
11468
+ when_to_use: zod_mini.z.optional(zod_mini.z.string()),
11469
+ "when-to-use": zod_mini.z.optional(zod_mini.z.string()),
11470
+ paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
11471
+ })),
11356
11472
  zcode: zod_mini.z.optional(zod_mini.z.looseObject({
11357
11473
  when_to_use: zod_mini.z.optional(zod_mini.z.string()),
11358
11474
  license: zod_mini.z.optional(zod_mini.z.string()),
@@ -11631,7 +11747,7 @@ async function getLocalSkillDirNames(sourceTree) {
11631
11747
  * The rulesync skill frontmatter exposes a root-level `disable-model-invocation`
11632
11748
  * default that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11633
11749
  * copilotcli, crush, cursor, zed, pi, omp, qwencode, vibe, grokcli, factorydroid, dsh, reasonix,
11634
- * commandcode, lettacode, opencode, mimocode). Each tool's own section may override that
11750
+ * commandcode, lettacode, opencode, mimocode, xum). Each tool's own section may override that
11635
11751
  * default with a per-target value. A defined section value (including `false`)
11636
11752
  * always wins over the root default.
11637
11753
  *
@@ -11651,7 +11767,7 @@ function resolveDisableModelInvocation({ rootFrontmatter, section }) {
11651
11767
  * The rulesync skill frontmatter exposes a root-level `user-invocable` default
11652
11768
  * that applies to every tool supporting the flag (claudecode, codebuddy, copilot,
11653
11769
  * copilotcli, crush, cursor, qwencode, vibe, grokcli, factorydroid, dsh, reasonix, commandcode,
11654
- * lettacode). Each tool's own section may override that default with a
11770
+ * lettacode, xum). Each tool's own section may override that default with a
11655
11771
  * per-target value. A defined section value (including `false`) always wins
11656
11772
  * over the root default.
11657
11773
  *
@@ -13966,7 +14082,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13966
14082
  ownedKeys: [
13967
14083
  "permissions",
13968
14084
  "sandbox",
13969
- "agent"
14085
+ "agent",
14086
+ "tools"
13970
14087
  ]
13971
14088
  }
13972
14089
  }
@@ -13987,7 +14104,8 @@ const SHARED_CONFIG_OWNERSHIP = {
13987
14104
  ownedKeys: [
13988
14105
  "permissions",
13989
14106
  "sandbox",
13990
- "agent"
14107
+ "agent",
14108
+ "tools"
13991
14109
  ]
13992
14110
  }
13993
14111
  }
@@ -24112,6 +24230,7 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
24112
24230
  const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
24113
24231
  const REASONIX_GLOBAL_DIR = ".reasonix";
24114
24232
  const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
24233
+ const REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH = (0, node_path.join)(".reasonix", REASONIX_PROJECT_MCP_FILE_NAME);
24115
24234
  const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
24116
24235
  const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
24117
24236
  const REASONIX_DIR = REASONIX_GLOBAL_DIR;
@@ -28459,25 +28578,65 @@ function toolHooksToCanonical({ hooks, converterConfig, logger }) {
28459
28578
  //#endregion
28460
28579
  //#region src/features/hooks/antigravity-hooks.ts
28461
28580
  /**
28462
- * Antigravity uses a Claude-Code-like `hooks.json` (matcher shape). The
28463
- * `PreToolUse`/`PostToolUse` events use the matcher-based array shape, while
28464
- * `PreInvocation`/`PostInvocation`/`Stop` are matcher-less handler lists.
28465
- * `projectDirVar` is empty because Antigravity does not document a
28466
- * project-directory variable for commands.
28581
+ * Canonical events whose Antigravity counterparts (`SessionStart`,
28582
+ * `PreInvocation`, `PostInvocation`, `Stop`) take no matcher.
28583
+ */
28584
+ const ANTIGRAVITY_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
28585
+ "sessionStart",
28586
+ "preModelInvocation",
28587
+ "postModelInvocation",
28588
+ "stop"
28589
+ ]);
28590
+ /**
28591
+ * Antigravity event names whose value is a flat list of handlers
28592
+ * (`"Stop": [ { "type": "command", "command": "..." } ]`) rather than a list
28593
+ * of `{ "matcher", "hooks": [...] }` groups. Antigravity's loader rejects a
28594
+ * wrapped group under these events ("command hook must specify 'command'")
28595
+ * and then skips the whole `hooks.json`.
28596
+ * https://antigravity.google/docs/hooks
28597
+ */
28598
+ const ANTIGRAVITY_FLAT_HANDLER_EVENTS = new Set([...ANTIGRAVITY_NO_MATCHER_EVENTS].map((event) => lookupOwn({
28599
+ record: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
28600
+ key: event
28601
+ }) ?? event));
28602
+ /**
28603
+ * Antigravity uses a Claude-Code-like `hooks.json`. The `PreToolUse` /
28604
+ * `PostToolUse` events use the matcher-based group shape, while `SessionStart`,
28605
+ * `PreInvocation`, `PostInvocation` and `Stop` are flat handler lists (see
28606
+ * {@link toAntigravityEventMap}). `projectDirVar` is empty because Antigravity
28607
+ * does not document a project-directory variable for commands.
28467
28608
  */
28468
28609
  const ANTIGRAVITY_CONVERTER_CONFIG = {
28469
28610
  supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
28470
28611
  canonicalToToolEventNames: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
28471
28612
  toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
28472
28613
  projectDirVar: "",
28473
- noMatcherEvents: /* @__PURE__ */ new Set([
28474
- "preModelInvocation",
28475
- "postModelInvocation",
28476
- "stop"
28477
- ]),
28614
+ noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
28478
28615
  supportedHookTypes: /* @__PURE__ */ new Set(["command"])
28479
28616
  };
28480
28617
  /**
28618
+ * Turn the shared converter's matcher-group output into Antigravity's shape:
28619
+ * the matcher-less events get their handlers directly under the event key.
28620
+ */
28621
+ function toAntigravityEventMap(eventMap) {
28622
+ return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
28623
+ if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event)) return [event, entries];
28624
+ return [event, entries.flatMap((entry) => isPlainObject$1(entry) && Array.isArray(entry.hooks) ? entry.hooks : [])];
28625
+ }));
28626
+ }
28627
+ /**
28628
+ * Reverse of {@link toAntigravityEventMap} for import: wrap each flat handler
28629
+ * of a matcher-less event in a `{ hooks: [...] }` group so the shared converter
28630
+ * can read it. An entry that is already a group (`{ hooks: [...] }`, as older
28631
+ * rulesync versions wrote) is kept as is.
28632
+ */
28633
+ function toMatcherGroupEventMap(eventMap) {
28634
+ return Object.fromEntries(Object.entries(eventMap).map(([event, entries]) => {
28635
+ if (!ANTIGRAVITY_FLAT_HANDLER_EVENTS.has(event) || !Array.isArray(entries)) return [event, entries];
28636
+ return [event, entries.map((entry) => isPlainObject$1(entry) && !("hooks" in entry) ? { hooks: [entry] } : entry)];
28637
+ }));
28638
+ }
28639
+ /**
28481
28640
  * Antigravity's `hooks.json` is keyed by a named hook whose value holds the
28482
28641
  * per-event map (plus an optional `enabled` flag). rulesync writes a single
28483
28642
  * generated hook under this stable name.
@@ -28514,8 +28673,8 @@ function flattenAntigravityHooks(parsed) {
28514
28673
  * Hooks generator for Google Antigravity (both the IDE and the CLI).
28515
28674
  *
28516
28675
  * Antigravity writes a dedicated `hooks.json` keyed by a named hook whose
28517
- * value holds the Claude-Code-style event → matcher-entry map
28518
- * (`{ "<name>": { "<Event>": [...] } }`). Project and global modes share the
28676
+ * value holds the event map (`{ "<name>": { "<Event>": [...] } }`): matcher
28677
+ * groups for the tool events, flat handler lists for the others. Project and global modes share the
28519
28678
  * same shape; only the location differs (`.agents/hooks.json` vs
28520
28679
  * `~/.gemini/config/hooks.json`).
28521
28680
  *
@@ -28559,12 +28718,12 @@ var AntigravityHooks = class extends ToolHooks {
28559
28718
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
28560
28719
  const paths = this.getSettablePaths({ global });
28561
28720
  const config = rulesyncHooks.getJson();
28562
- const eventMap = canonicalToToolHooks({
28721
+ const eventMap = toAntigravityEventMap(canonicalToToolHooks({
28563
28722
  config,
28564
28723
  toolOverrideHooks: config[this.getOverrideKey()]?.hooks,
28565
28724
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28566
28725
  logger
28567
- });
28726
+ }));
28568
28727
  const antigravityHooks = Object.keys(eventMap).length > 0 ? { [ANTIGRAVITY_HOOK_NAME]: eventMap } : {};
28569
28728
  const fileContent = JSON.stringify(antigravityHooks, null, 2);
28570
28729
  return new this({
@@ -28583,7 +28742,7 @@ var AntigravityHooks = class extends ToolHooks {
28583
28742
  throw new Error(`Failed to parse Antigravity hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
28584
28743
  }
28585
28744
  const hooks = toolHooksToCanonical({
28586
- hooks: flattenAntigravityHooks(parsed),
28745
+ hooks: toMatcherGroupEventMap(flattenAntigravityHooks(parsed)),
28587
28746
  converterConfig: ANTIGRAVITY_CONVERTER_CONFIG,
28588
28747
  logger
28589
28748
  });
@@ -31047,11 +31206,17 @@ var ContinueHooks = class extends SettingsJsonHooks {
31047
31206
  * entry that uses them instead of a shell field is not mistaken for one
31048
31207
  * without a command.
31049
31208
  *
31209
+ * An optional `matcher` regex is honored on the events in
31210
+ * {@link COPILOT_MATCHER_HOOK_EVENTS}. The VS Code per-OS override fields come
31211
+ * from the legacy Local hooks reference and are kept for existing files.
31212
+ *
31050
31213
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
31214
+ * @see https://code.visualstudio.com/docs/agent-customization/hooks
31051
31215
  * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
31052
31216
  */
31053
31217
  const CopilotHookEntrySchema = zod_mini.z.looseObject({
31054
31218
  type: zod_mini.z.string(),
31219
+ matcher: zod_mini.z.optional(zod_mini.z.string()),
31055
31220
  bash: zod_mini.z.optional(zod_mini.z.string()),
31056
31221
  powershell: zod_mini.z.optional(zod_mini.z.string()),
31057
31222
  command: zod_mini.z.optional(zod_mini.z.string()),
@@ -31085,8 +31250,10 @@ function canonicalToCopilotHooks(config) {
31085
31250
  ...sharedConfigHooks,
31086
31251
  ...config.copilot?.hooks
31087
31252
  };
31253
+ const matcherEvents = new Set(COPILOT_MATCHER_HOOK_EVENTS);
31088
31254
  const copilot = {};
31089
31255
  for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
31256
+ const matcherSupported = matcherEvents.has(eventName);
31090
31257
  const copilotEventName = lookupOwn({
31091
31258
  record: CANONICAL_TO_COPILOT_EVENT_NAMES,
31092
31259
  key: eventName
@@ -31094,7 +31261,7 @@ function canonicalToCopilotHooks(config) {
31094
31261
  const entries = [];
31095
31262
  for (const def of definitions) {
31096
31263
  const hookType = def.type ?? "command";
31097
- if (def.matcher) continue;
31264
+ if (def.matcher && !matcherSupported) continue;
31098
31265
  if (hookType !== "command") continue;
31099
31266
  const command = def.command;
31100
31267
  const timeout = def.timeout;
@@ -31102,6 +31269,7 @@ function canonicalToCopilotHooks(config) {
31102
31269
  const rest = Object.fromEntries(Object.entries(def).filter(([k]) => !canonicalSchemaKeys.includes(k)));
31103
31270
  entries.push({
31104
31271
  type: hookType,
31272
+ ...def.matcher && { matcher: def.matcher },
31105
31273
  ...command !== void 0 && command !== null && { [commandField]: command },
31106
31274
  ...def.env !== void 0 && { env: def.env },
31107
31275
  ...timeout !== void 0 && timeout !== null && { timeoutSec: timeout },
@@ -31196,6 +31364,7 @@ function copilotHooksToCanonical(copilotHooks, logger) {
31196
31364
  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.`);
31197
31365
  defs.push({
31198
31366
  type: "command",
31367
+ ...entry.matcher !== void 0 && entry.matcher !== "" && { matcher: entry.matcher },
31199
31368
  ...command !== void 0 && { command },
31200
31369
  ...shell !== void 0 && { shell },
31201
31370
  ...timeout !== void 0 && { timeout },
@@ -31339,21 +31508,13 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
31339
31508
  */
31340
31509
  /**
31341
31510
  * Canonical events whose Copilot CLI hook entries honor an optional `matcher`
31342
- * field (a regex compiled as `^(?:PATTERN)$`, tested against `toolName`).
31343
- *
31344
- * Confirmed by the upstream changelog: v1.0.36 (2026-04-24, `preToolUse.matcher`
31345
- * honored) and v1.0.63 (2026-06-15, "PostToolUse hook matchers (e.g.
31346
- * `Edit|Write`) are now honored instead of silently dropped").
31347
- * @see https://docs.github.com/en/copilot/reference/hooks-reference
31511
+ * field (a regex compiled as `^(?:PATTERN)$`). The hooks reference filters each
31512
+ * on a different field: the tool name for `preToolUse` / `postToolUse` /
31513
+ * `permissionRequest`, `notification_type`, the `preCompact` trigger
31514
+ * (`manual` | `auto`) and the `subagentStart` agent name.
31515
+ * See {@link COPILOTCLI_MATCHER_HOOK_EVENTS}.
31348
31516
  */
31349
- const COPILOTCLI_MATCHER_EVENTS = /* @__PURE__ */ new Set([
31350
- "preToolUse",
31351
- "postToolUse",
31352
- "notification",
31353
- "permissionRequest",
31354
- "preCompact",
31355
- "subagentStart"
31356
- ]);
31517
+ const COPILOTCLI_MATCHER_EVENTS = new Set(COPILOTCLI_MATCHER_HOOK_EVENTS);
31357
31518
  /** Human-readable list of the matcher-aware events, for the drop warning. */
31358
31519
  const COPILOTCLI_MATCHER_EVENTS_LABEL = [...COPILOTCLI_MATCHER_EVENTS].join("/");
31359
31520
  const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
@@ -33489,7 +33650,7 @@ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
33489
33650
  for (const [nativeEvent, definitions] of Object.entries(toolOverrideHooks ?? {})) {
33490
33651
  if (HERMESAGENT_CANONICAL_EVENTS.has(nativeEvent)) continue;
33491
33652
  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.`);
33492
- 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.`);
33653
+ 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.`);
33493
33654
  setHermesHookEntries({
33494
33655
  result,
33495
33656
  event: nativeEvent,
@@ -37579,7 +37740,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37579
37740
  },
37580
37741
  supportedEvents: COPILOT_HOOK_EVENTS,
37581
37742
  supportedHookTypes: ["command"],
37582
- supportsMatcher: false
37743
+ supportsMatcher: true,
37744
+ matcherEvents: COPILOT_MATCHER_HOOK_EVENTS
37583
37745
  }],
37584
37746
  ["copilotcli", {
37585
37747
  class: CopilotcliHooks,
@@ -37594,7 +37756,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
37594
37756
  "prompt",
37595
37757
  "http"
37596
37758
  ],
37597
- supportsMatcher: false
37759
+ supportsMatcher: true
37598
37760
  }],
37599
37761
  ["kilo", {
37600
37762
  class: KiloHooks,
@@ -39984,6 +40146,31 @@ const REASONIX_READ_TOOL_NAME = "read_file";
39984
40146
  * treated as rulesync-owned on generate so the dormant copy is replaced.
39985
40147
  */
39986
40148
  const REASONIX_LEGACY_READ_TOOL_NAME = "Read";
40149
+ async function isPathPresent(path) {
40150
+ return await fileExists(path) || await isPresentButUnresolvable(path);
40151
+ }
40152
+ /**
40153
+ * Warns when a project-scope generate is about to create `./reasonix.toml`
40154
+ * next to an existing `.reasonix/reasonix.toml`.
40155
+ *
40156
+ * Since v2.46.0 Reasonix reads `.reasonix/reasonix.toml` as the project config
40157
+ * when `./reasonix.toml` is absent, and never merges the two: once the root
40158
+ * file exists it wins, so every setting in the nested file (permissions,
40159
+ * sandbox, disabled tools, MCP servers, ...) silently stops applying. rulesync
40160
+ * writes only the root file, so it cannot keep the nested one in effect; this
40161
+ * at least names the problem, once per run however many of the features that
40162
+ * write the file (mcp, permissions, ignore) are generated. Like Reasonix, anything at the root path
40163
+ * (including a broken symlink) counts as the root file being there.
40164
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/internal/contract/config/paths.go
40165
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/CONFIG_PATHS.md
40166
+ */
40167
+ async function warnIfReasonixNestedProjectConfigShadowed({ outputRoot, global, logger }) {
40168
+ if (global) return;
40169
+ if (await isPathPresent((0, node_path.join)(outputRoot, "reasonix.toml"))) return;
40170
+ if (!await isPathPresent((0, node_path.join)(outputRoot, REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH))) return;
40171
+ const nested = toPosixPath(REASONIX_NESTED_PROJECT_CONFIG_RELATIVE_FILE_PATH);
40172
+ 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.`);
40173
+ }
39987
40174
  //#endregion
39988
40175
  //#region src/features/ignore/reasonix-ignore.ts
39989
40176
  const permissionsTableOf = (document) => toReasonixTable(document.permissions);
@@ -40067,7 +40254,12 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
40067
40254
  fileContent: rulesyncPatterns.join("\n")
40068
40255
  });
40069
40256
  }
40070
- static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false }) {
40257
+ static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
40258
+ await warnIfReasonixNestedProjectConfigShadowed({
40259
+ outputRoot,
40260
+ global,
40261
+ logger
40262
+ });
40071
40263
  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));
40072
40264
  const paths = this.getSettablePaths({ global });
40073
40265
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -40645,7 +40837,8 @@ var IgnoreProcessor = class extends FeatureProcessor {
40645
40837
  outputRoot: this.outputRoot,
40646
40838
  rulesyncIgnore,
40647
40839
  options: this.featureOptions,
40648
- global: this.global
40840
+ global: this.global,
40841
+ logger: this.logger
40649
40842
  });
40650
40843
  const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
40651
40844
  toolIgnore,
@@ -40995,13 +41188,31 @@ function renameServerField(servers, from, to) {
40995
41188
  return [name, Object.fromEntries(Object.entries(config).map(([key, value]) => key === from ? [to, value] : [key, value]))];
40996
41189
  }));
40997
41190
  }
41191
+ function isEmptyValue(value) {
41192
+ if (Array.isArray(value)) return value.length === 0;
41193
+ return isPlainObject$1(value) && Object.keys(value).length === 0;
41194
+ }
40998
41195
  /**
40999
41196
  * Antigravity uses `serverUrl` (not the canonical `url`) for HTTP/SSE MCP
41000
41197
  * servers. Convert the canonical shape into Antigravity's on the way out, and
41001
- * back to canonical on the way in, so round-tripping is lossless.
41198
+ * back to canonical on the way in.
41199
+ *
41200
+ * Antigravity documents no `type` field: the transport follows from `command`
41201
+ * (stdio) or `serverUrl` (Streamable HTTP / SSE), and `agy plugin validate`
41202
+ * reports the canonical `type` as an unknown field, so it is not written. A
41203
+ * remote server that carries `command`, `args` or `env` was also observed to be
41204
+ * refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
41205
+ * not documented), so an empty `args` / `env` left on a `serverUrl` server is
41206
+ * dropped.
41207
+ * https://antigravity.google/docs/mcp
41002
41208
  */
41003
41209
  function toAntigravityMcpServers(servers) {
41004
- return renameServerField(servers, "url", "serverUrl");
41210
+ const renamed = renameServerField(servers, "url", "serverUrl");
41211
+ return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
41212
+ if (!isPlainObject$1(config)) return [name, config];
41213
+ const isRemote = typeof config.serverUrl === "string";
41214
+ return [name, Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value))))];
41215
+ }));
41005
41216
  }
41006
41217
  function toCanonicalMcpServers(servers) {
41007
41218
  return renameServerField(servers, "serverUrl", "url");
@@ -45191,6 +45402,60 @@ function applyDeepagentsCommand({ name, server, converted, logger }) {
45191
45402
  return true;
45192
45403
  }
45193
45404
  /**
45405
+ * Translate a canonical `oauth` block into dcode's custom Slack OAuth app
45406
+ * settings, or `undefined` (after warning) when it has to be left out.
45407
+ *
45408
+ * Since deepagents-code 0.1.87, `_validate_slack_oauth_config` accepts `oauth`
45409
+ * only as `{ client_id, client_secret? }` of non-empty strings, and only on a
45410
+ * `slack.com` / `*.slack.com` server with `auth: "oauth"`; anything else makes
45411
+ * dcode skip the whole server. The canonical block usually carries Claude
45412
+ * Code's `{ clientId, callbackPort, ... }` shape, so `clientId` becomes
45413
+ * `client_id` (a native `client_id` wins), `client_secret` passes through, and
45414
+ * every other key is dropped.
45415
+ *
45416
+ * @see https://github.com/langchain-ai/deepagents/blob/deepagents-code%3D%3D0.1.87/libs/code/deepagents_code/mcp_config.py
45417
+ */
45418
+ function toDeepagentsOauth({ name, oauth, converted, logger }) {
45419
+ if (!isSlackOauthServer(converted)) {
45420
+ 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.`);
45421
+ return;
45422
+ }
45423
+ const source = isRecord$1(oauth) ? oauth : {};
45424
+ const result = {};
45425
+ for (const [key, value] of Object.entries(source)) {
45426
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
45427
+ const nativeKey = key === "clientId" ? "client_id" : key;
45428
+ if (nativeKey !== "client_id" && nativeKey !== "client_secret") {
45429
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents accepts only client_id and client_secret.`);
45430
+ continue;
45431
+ }
45432
+ if (nativeKey !== key && isDeepagentsOauthValue(source[nativeKey])) continue;
45433
+ if (!isDeepagentsOauthValue(value)) {
45434
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth.${key} on "${name}"; deepagents requires a non-empty string without control characters.`);
45435
+ continue;
45436
+ }
45437
+ result[nativeKey] = value;
45438
+ }
45439
+ if (result.client_id === void 0) {
45440
+ logger?.warn(`${TOOL_NAME} MCP: dropping oauth on "${name}"; deepagents requires oauth.client_id (canonical clientId) and skips a server without it.`);
45441
+ return;
45442
+ }
45443
+ return result;
45444
+ }
45445
+ function isDeepagentsOauthValue(value) {
45446
+ return typeof value === "string" && value.trim() !== "" && !/[\x00-\x1f\x7f]/.test(value);
45447
+ }
45448
+ function isSlackOauthServer(server) {
45449
+ if (server.auth !== "oauth" || typeof server.url !== "string") return false;
45450
+ let host;
45451
+ try {
45452
+ host = new URL(server.url).hostname.toLowerCase();
45453
+ } catch {
45454
+ return false;
45455
+ }
45456
+ return host === "slack.com" || host.endsWith(".slack.com");
45457
+ }
45458
+ /**
45194
45459
  * Translate one canonical server into dcode's `.mcp.json` shape, or `null` to
45195
45460
  * skip it.
45196
45461
  *
@@ -45213,7 +45478,7 @@ function toDeepagentsServer({ name, server, logger }) {
45213
45478
  reason: "the WebSocket transport, which deepagents does not support",
45214
45479
  logger
45215
45480
  });
45216
- const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, ...rest } = server;
45481
+ const { enabledTools, disabledTools, type: _type, transport, disabled: _disabled, httpUrl, oauth, ...rest } = server;
45217
45482
  const converted = { ...rest };
45218
45483
  if (!applyDeepagentsCommand({
45219
45484
  name,
@@ -45228,6 +45493,15 @@ function toDeepagentsServer({ name, server, logger }) {
45228
45493
  if (transport !== void 0) converted.transport = normalized;
45229
45494
  else converted.type = normalized;
45230
45495
  }
45496
+ if (oauth !== void 0) {
45497
+ const deepagentsOauth = toDeepagentsOauth({
45498
+ name,
45499
+ oauth,
45500
+ converted,
45501
+ logger
45502
+ });
45503
+ if (deepagentsOauth !== void 0) converted.oauth = deepagentsOauth;
45504
+ }
45231
45505
  if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
45232
45506
  toolName: TOOL_NAME,
45233
45507
  serverName: name,
@@ -45254,6 +45528,13 @@ function toRulesyncServer(server) {
45254
45528
  const converted = { ...rest };
45255
45529
  for (const key of ["type", "transport"]) if (converted[key] === "streamable_http" || converted[key] === "streamable-http") converted[key] = "http";
45256
45530
  if (Array.isArray(allowedTools)) converted.enabledTools = allowedTools;
45531
+ if (isRecord$1(converted.oauth) && Object.hasOwn(converted.oauth, "client_id")) {
45532
+ const { client_id: clientId, ...oauthRest } = converted.oauth;
45533
+ converted.oauth = Object.hasOwn(oauthRest, "clientId") ? oauthRest : {
45534
+ clientId,
45535
+ ...oauthRest
45536
+ };
45537
+ }
45257
45538
  return converted;
45258
45539
  }
45259
45540
  var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
@@ -46986,6 +47267,50 @@ function copyHermesOauth(source) {
46986
47267
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
46987
47268
  return Object.keys(oauth).length > 0 ? oauth : void 0;
46988
47269
  }
47270
+ const HERMES_COST_UNIT_MAX_LENGTH = 32;
47271
+ function describeHermesValue(value) {
47272
+ return typeof value === "number" ? String(value) : JSON.stringify(value) ?? String(value);
47273
+ }
47274
+ /**
47275
+ * Copies the per-server MCP cost keys Hermes v0.22.3 reads in
47276
+ * `tools/mcp_tool_costs.py`: `tool_costs` (declared per-tool costs keyed by
47277
+ * MCP tool name; each a number, a `[min, max]` range or `{credits, note}`),
47278
+ * `spend_limit` (per-chat budget for the server's costed tools; `0`/absent =
47279
+ * no limit) and `cost_unit` (label shown with costs; default `credits`).
47280
+ *
47281
+ * A costed call that would exceed `spend_limit` asks first even on a
47282
+ * `trust: full` server, so dropping the key on regenerate would silently
47283
+ * remove the guard. Values Hermes would reject when loading the server are
47284
+ * dropped with a warning instead of being written into a config Hermes then
47285
+ * refuses: `spend_limit` must be a finite non-negative number, `cost_unit` a
47286
+ * non-blank string of at most 32 characters, and `tool_costs` a mapping. The
47287
+ * mapping is rebuilt as an opaque object (so no reference is shared with the
47288
+ * source), with prototype-pollution keys removed at every depth since its
47289
+ * values may themselves be objects.
47290
+ */
47291
+ function copyHermesCostFields({ source, target, logger }) {
47292
+ let copied = false;
47293
+ const { spend_limit: spendLimit, cost_unit: costUnit, tool_costs: toolCosts } = source;
47294
+ if (spendLimit !== void 0) {
47295
+ if (typeof spendLimit === "number" && Number.isFinite(spendLimit) && spendLimit >= 0) {
47296
+ target.spend_limit = spendLimit;
47297
+ copied = true;
47298
+ } else logger?.warn(`Hermes MCP: dropping spend_limit ${describeHermesValue(spendLimit)}; Hermes requires a non-negative number.`);
47299
+ }
47300
+ if (costUnit !== void 0) {
47301
+ if (typeof costUnit === "string" && costUnit.trim() !== "" && costUnit.length <= HERMES_COST_UNIT_MAX_LENGTH) {
47302
+ target.cost_unit = costUnit;
47303
+ copied = true;
47304
+ } 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.`);
47305
+ }
47306
+ if (toolCosts !== void 0) {
47307
+ if (isPlainObject$1(toolCosts)) {
47308
+ target.tool_costs = omitPrototypePollutionKeysDeep(toolCosts);
47309
+ copied = true;
47310
+ } else logger?.warn("Hermes MCP: dropping tool_costs; Hermes requires a mapping of MCP tool names to costs.");
47311
+ }
47312
+ return copied;
47313
+ }
46989
47314
  const HERMES_OPAQUE_MAPPING_KEYS = [
46990
47315
  "sampling",
46991
47316
  "elicitation",
@@ -46997,13 +47322,18 @@ const HERMES_OPAQUE_MAPPING_KEYS = [
46997
47322
  * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
46998
47323
  * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
46999
47324
  * `connect_timeout` (seconds), `supports_parallel_tool_calls`, `protocol`,
47000
- * `lazy`, `keepalive_interval`, `elicitation`, `trust`, and `identity_header` —
47325
+ * `lazy`, `keepalive_interval`, `elicitation`, `trust`, `identity_header`, and
47326
+ * the cost keys (see {@link copyHermesCostFields}) —
47001
47327
  * verbatim from `source` to `target`. Field names are identical on both sides
47002
47328
  * (the canonical `McpServerSchema` is a `looseObject`), so this serves export
47003
47329
  * and import alike. See the Hermes mcp-config-reference.
47004
47330
  */
47005
- function copyHermesAdvancedFields(source, target) {
47006
- let copied = false;
47331
+ function copyHermesAdvancedFields(source, target, logger) {
47332
+ let copied = copyHermesCostFields({
47333
+ source,
47334
+ target,
47335
+ logger
47336
+ });
47007
47337
  if (typeof source.auth === "string") {
47008
47338
  target.auth = source.auth;
47009
47339
  copied = true;
@@ -47112,7 +47442,7 @@ function applyHermesToolsBlock(hermesTools, server) {
47112
47442
  * as Hermes's own `transport: sse`, without which Hermes would connect to it
47113
47443
  * over Streamable HTTP.
47114
47444
  */
47115
- function convertServerToHermes(config) {
47445
+ function convertServerToHermes(config, logger) {
47116
47446
  const out = {};
47117
47447
  const command = config.command;
47118
47448
  const url = resolveHermesUrl(config);
@@ -47135,7 +47465,7 @@ function convertServerToHermes(config) {
47135
47465
  if (config.disabled === true) out.enabled = false;
47136
47466
  const timeout = resolveHermesTimeout(config);
47137
47467
  if (timeout !== void 0) out.timeout = timeout;
47138
- copyHermesAdvancedFields(config, out);
47468
+ copyHermesAdvancedFields(config, out, logger);
47139
47469
  const tools = buildHermesToolsBlock(config);
47140
47470
  if (Object.keys(tools).length > 0) out.tools = tools;
47141
47471
  return out;
@@ -47143,11 +47473,11 @@ function convertServerToHermes(config) {
47143
47473
  /**
47144
47474
  * Converts rulesync canonical MCP servers into Hermes `mcp_servers:` entries.
47145
47475
  */
47146
- function convertToHermesFormat(mcpServers) {
47476
+ function convertToHermesFormat(mcpServers, logger) {
47147
47477
  const result = {};
47148
47478
  for (const [name, config] of Object.entries(mcpServers)) {
47149
47479
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
47150
- result[name] = convertServerToHermes(config);
47480
+ result[name] = convertServerToHermes(config, logger);
47151
47481
  }
47152
47482
  return result;
47153
47483
  }
@@ -47167,7 +47497,7 @@ function mergeHermesMcpServers(config, mcpServers) {
47167
47497
  * Mirrors {@link convertToHermesFormat}: `enabled: false` maps back to the
47168
47498
  * canonical `disabled: true`, and only recognized fields are carried over.
47169
47499
  */
47170
- function convertFromHermesFormat(mcpServers) {
47500
+ function convertFromHermesFormat(mcpServers, logger) {
47171
47501
  const result = {};
47172
47502
  const hermesOverrides = {};
47173
47503
  for (const [name, config] of Object.entries(mcpServers)) {
@@ -47184,7 +47514,7 @@ function convertFromHermesFormat(mcpServers) {
47184
47514
  if (isRecord$1(config.tools)) applyHermesToolsBlock(config.tools, server);
47185
47515
  result[name] = server;
47186
47516
  const hermesServer = { ...server };
47187
- if (copyHermesAdvancedFields(config, hermesServer)) hermesOverrides[name] = hermesServer;
47517
+ if (copyHermesAdvancedFields(config, hermesServer, logger)) hermesOverrides[name] = hermesServer;
47188
47518
  }
47189
47519
  return {
47190
47520
  mcpServers: result,
@@ -47203,8 +47533,10 @@ function convertFromHermesFormat(mcpServers) {
47203
47533
  */
47204
47534
  var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47205
47535
  config;
47206
- constructor(params) {
47536
+ logger;
47537
+ constructor({ logger, ...params }) {
47207
47538
  super(params);
47539
+ this.logger = logger;
47208
47540
  this.config = this.fileContent !== void 0 ? parseSharedConfig({
47209
47541
  format: "yaml",
47210
47542
  fileContent: this.fileContent
@@ -47248,7 +47580,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47248
47580
  static getExtraSharedWritePaths() {
47249
47581
  return getHermesagentSharedConfigWritePaths();
47250
47582
  }
47251
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
47583
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
47252
47584
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
47253
47585
  const paths = this.getSettablePaths({ global });
47254
47586
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
@@ -47258,17 +47590,18 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47258
47590
  relativeFilePath: paths.relativeFilePath,
47259
47591
  fileContent,
47260
47592
  validate,
47261
- global
47593
+ global,
47594
+ logger
47262
47595
  });
47263
47596
  }
47264
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
47597
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
47265
47598
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
47266
47599
  const paths = this.getSettablePaths({ global });
47267
47600
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
47268
47601
  const merged = mergeHermesMcpServers(parseSharedConfig({
47269
47602
  format: "yaml",
47270
47603
  fileContent
47271
- }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
47604
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers(), logger));
47272
47605
  return new HermesagentMcp({
47273
47606
  outputRoot,
47274
47607
  relativeDirPath: paths.relativeDirPath,
@@ -47280,11 +47613,12 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
47280
47613
  patch: { mcp_servers: merged.mcp_servers }
47281
47614
  }),
47282
47615
  validate,
47283
- global
47616
+ global,
47617
+ logger
47284
47618
  });
47285
47619
  }
47286
47620
  toRulesyncMcp() {
47287
- const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
47621
+ const { mcpServers: servers, hermesOverrides } = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {}, this.logger);
47288
47622
  return this.toRulesyncMcpDefault({
47289
47623
  outputRoot: getHermesagentRulesyncOutputRoot({
47290
47624
  nativeOutputRoot: this.outputRoot,
@@ -48168,14 +48502,15 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
48168
48502
  if (!config.parsed) return {};
48169
48503
  return {
48170
48504
  ...typeof config.mcp.startup_timeout_ms === "number" && { startupTimeoutMs: config.mcp.startup_timeout_ms },
48171
- ...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms }
48505
+ ...typeof config.mcp.tool_timeout_ms === "number" && { toolTimeoutMs: config.mcp.tool_timeout_ms },
48506
+ ...typeof config.mcp.default_deferred === "boolean" && { defaultDeferred: config.mcp.default_deferred }
48172
48507
  };
48173
48508
  }
48174
48509
  /**
48175
48510
  * Kimi's `[mcp]` section in the shared user config sets the default connect and
48176
- * per-tool-call timeouts for *every* MCP server, including ones rulesync did not
48177
- * write. Per-server values in `mcp.json` still take precedence; this only moves
48178
- * the floor.
48511
+ * per-tool-call timeouts, and whether tools load on demand, for *every* MCP
48512
+ * server, including ones rulesync did not write. Per-server values in
48513
+ * `mcp.json` still take precedence; this only moves the default.
48179
48514
  *
48180
48515
  * Written as an auxiliary file so it goes through the normal write phase and
48181
48516
  * respects `--dry-run`, and merged through the shared-config gateway so the
@@ -48183,7 +48518,8 @@ async function readKimiCodeMcpDefaults({ outputRoot }) {
48183
48518
  *
48184
48519
  * The gateway replaces an owned key wholesale, and `mcp` is a table, so the
48185
48520
  * section is recomputed from the existing file: authoring only
48186
- * `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms`. Same
48521
+ * `startupTimeoutMs` must not delete a hand-written `tool_timeout_ms` or
48522
+ * `default_deferred`. Same
48187
48523
  * approach the `.vibe/config.toml` `tools` writer takes.
48188
48524
  *
48189
48525
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#mcp
@@ -48210,7 +48546,8 @@ var KimiCodeMcpConfigToml = class KimiCodeMcpConfigToml extends ToolFile {
48210
48546
  patch: { mcp: {
48211
48547
  ...existingSection,
48212
48548
  ...defaults.startupTimeoutMs !== void 0 && { startup_timeout_ms: defaults.startupTimeoutMs },
48213
- ...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs }
48549
+ ...defaults.toolTimeoutMs !== void 0 && { tool_timeout_ms: defaults.toolTimeoutMs },
48550
+ ...defaults.defaultDeferred !== void 0 && { default_deferred: defaults.defaultDeferred }
48214
48551
  } },
48215
48552
  filePath: relativeConfigPath
48216
48553
  });
@@ -48308,18 +48645,20 @@ var KimiCodeMcp = class KimiCodeMcp extends ToolMcp {
48308
48645
  if (!isRecord$1(block)) return [];
48309
48646
  const startupTimeoutMs = typeof block.startupTimeoutMs === "number" ? block.startupTimeoutMs : void 0;
48310
48647
  const toolTimeoutMs = typeof block.toolTimeoutMs === "number" ? block.toolTimeoutMs : void 0;
48311
- if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0) return [];
48648
+ const defaultDeferred = typeof block.defaultDeferred === "boolean" ? block.defaultDeferred : void 0;
48649
+ if (startupTimeoutMs === void 0 && toolTimeoutMs === void 0 && defaultDeferred === void 0) return [];
48312
48650
  const existing = await readKimiCodeConfig({ outputRoot });
48313
48651
  if (!existing.parsed) {
48314
48652
  const paths = kimiCodeConfigRelativePath();
48315
- warnWithFallback(logger, `Skipping the Kimi Code MCP timeout defaults: ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
48653
+ warnWithFallback(logger, `Skipping the Kimi Code MCP [mcp] defaults: ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)} is not valid TOML.`);
48316
48654
  return [];
48317
48655
  }
48318
48656
  return [KimiCodeMcpConfigToml.fromDefaults({
48319
48657
  outputRoot,
48320
48658
  defaults: {
48321
48659
  startupTimeoutMs,
48322
- toolTimeoutMs
48660
+ toolTimeoutMs,
48661
+ defaultDeferred
48323
48662
  },
48324
48663
  existing
48325
48664
  })];
@@ -50895,6 +51234,11 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
50895
51234
  });
50896
51235
  }
50897
51236
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
51237
+ await warnIfReasonixNestedProjectConfigShadowed({
51238
+ outputRoot,
51239
+ global,
51240
+ logger
51241
+ });
50898
51242
  const paths = this.getSettablePaths({ global });
50899
51243
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
50900
51244
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -52922,6 +53266,144 @@ function buildCommandString(server) {
52922
53266
  if (typeof server.command !== "string" || server.command.trim() === "") return void 0;
52923
53267
  return [server.command, ...args.map(quoteShellWord)].join(" ");
52924
53268
  }
53269
+ /** A name bash accepts on the left of a `NAME=value` assignment. */
53270
+ const SHELL_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
53271
+ /** A canonical `${VAR}` reference whose name bash can expand. */
53272
+ const ENV_VAR_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
53273
+ /**
53274
+ * Quote an env value for a `NAME=value` assignment. Literal text is
53275
+ * single-quoted, so nothing in it is expanded or run, while every canonical
53276
+ * `${VAR}` reference is emitted as `"${VAR}"` so bash still expands it from
53277
+ * the environment Xum starts the server with. The pieces are concatenated into
53278
+ * one shell word: `Bearer ${TOKEN}` becomes `'Bearer '"${TOKEN}"`.
53279
+ */
53280
+ function quoteEnvValue(value) {
53281
+ if (value === "") return "''";
53282
+ const parts = [];
53283
+ let last = 0;
53284
+ for (const match of value.matchAll(ENV_VAR_REFERENCE)) {
53285
+ if (match.index > last) parts.push(quoteShellWord(value.slice(last, match.index)));
53286
+ parts.push(`"\${${match[1]}}"`);
53287
+ last = match.index + match[0].length;
53288
+ }
53289
+ if (last < value.length) parts.push(quoteShellWord(value.slice(last)));
53290
+ return parts.join("");
53291
+ }
53292
+ /**
53293
+ * Build the `NAME=value ` prefix that sets a stdio server's `env` for its
53294
+ * command only (Xum runs the command string with bash). Returns the prefix
53295
+ * plus the entries left out: names that cannot be bash assignment targets and
53296
+ * entries whose value is not a string.
53297
+ */
53298
+ function buildEnvPrefix(env) {
53299
+ const assignments = [];
53300
+ const invalidNames = [];
53301
+ const nonStringNames = [];
53302
+ for (const [name, value] of Object.entries(env)) {
53303
+ if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
53304
+ if (!SHELL_ENV_NAME.test(name)) {
53305
+ invalidNames.push(name);
53306
+ continue;
53307
+ }
53308
+ if (typeof value !== "string") {
53309
+ nonStringNames.push(name);
53310
+ continue;
53311
+ }
53312
+ assignments.push(`${name}=${quoteEnvValue(value)}`);
53313
+ }
53314
+ return {
53315
+ prefix: assignments.map((assignment) => `${assignment} `).join(""),
53316
+ invalidNames,
53317
+ nonStringNames
53318
+ };
53319
+ }
53320
+ /** Bash reserved words; a command starting with one cannot take an assignment prefix. */
53321
+ const BASH_RESERVED_WORDS = /* @__PURE__ */ new Set([
53322
+ "!",
53323
+ "[[",
53324
+ "]]",
53325
+ "{",
53326
+ "}",
53327
+ "case",
53328
+ "coproc",
53329
+ "do",
53330
+ "done",
53331
+ "elif",
53332
+ "else",
53333
+ "esac",
53334
+ "fi",
53335
+ "for",
53336
+ "function",
53337
+ "if",
53338
+ "in",
53339
+ "select",
53340
+ "then",
53341
+ "time",
53342
+ "until",
53343
+ "while"
53344
+ ]);
53345
+ /**
53346
+ * Explain why a `NAME=value` prefix would not reach the server, or return
53347
+ * `undefined` when it would. Bash applies such assignments to the first simple
53348
+ * command only: with a list or pipeline (`cd x && srv`) the server would not
53349
+ * see them, and in front of a group, parenthesized list or reserved word (`( srv )`,
53350
+ * `{ srv; }`, `if ...`) they are a syntax error that keeps the server from
53351
+ * starting. Only the verbatim `command` string is checked for operators,
53352
+ * outside complete single-quoted words; argv elements are quoted by
53353
+ * {@link quoteShellWord}.
53354
+ */
53355
+ function envPrefixBlocker(server, baseCommand) {
53356
+ if (typeof server.command === "string") {
53357
+ const unquoted = server.command.replace(/'[^']*'/g, "");
53358
+ 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";
53359
+ }
53360
+ const [firstWord] = baseCommand.trimStart().split(/\s+/);
53361
+ 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`;
53362
+ }
53363
+ /**
53364
+ * One leading `NAME=value` assignment in exactly the shape {@link quoteEnvValue}
53365
+ * writes: bare shell-safe characters, single-quoted text (with the `'\''`
53366
+ * escape) and `"${VAR}"` references, followed by spaces or tabs (a newline
53367
+ * would end the command, so the assignment would not apply to what follows). Bare characters
53368
+ * are matched one at a time, so every alternative starts with a distinct
53369
+ * character and the pattern cannot backtrack exponentially.
53370
+ */
53371
+ const LEADING_ENV_ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=((?:[A-Za-z0-9_\-.,:/@%+=]|'[^']*'|\\'|"\$\{[A-Za-z_][A-Za-z0-9_]*\}")*)[ \t]+/;
53372
+ /** Decode a value written by {@link quoteEnvValue} back into its canonical form. */
53373
+ function unquoteEnvValue(quoted) {
53374
+ return quoted.replace(/'([^']*)'|\\'|"(\$\{[A-Za-z_][A-Za-z0-9_]*\})"/g, (_m, single, ref) => typeof single === "string" ? single : typeof ref === "string" ? ref : "'");
53375
+ }
53376
+ /**
53377
+ * Split leading `NAME=value` assignments off a command string, but only when
53378
+ * every one of them is in the shape rulesync writes and a plain command
53379
+ * follows (see {@link envPrefixBlocker}); anything else (for example a `$(...)` substitution) keeps the whole string as
53380
+ * the command.
53381
+ */
53382
+ function splitEnvPrefix(command) {
53383
+ const env = {};
53384
+ let rest = command.trimStart();
53385
+ for (;;) {
53386
+ const match = LEADING_ENV_ASSIGNMENT.exec(rest);
53387
+ if (match === null) break;
53388
+ const [whole, name, quoted] = match;
53389
+ if (name === void 0 || quoted === void 0 || PROTOTYPE_POLLUTION_KEYS.has(name)) break;
53390
+ env[name] = unquoteEnvValue(quoted);
53391
+ rest = rest.slice(whole.length);
53392
+ }
53393
+ 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 };
53394
+ return {
53395
+ env,
53396
+ rest
53397
+ };
53398
+ }
53399
+ /** Read a command string back into `env` + `command` + `args`. */
53400
+ function parseCommandString(command) {
53401
+ const { env, rest } = splitEnvPrefix(command);
53402
+ return {
53403
+ ...env !== void 0 && { env },
53404
+ ...splitCommandString(rest)
53405
+ };
53406
+ }
52925
53407
  /** Split a plain command string back into `command` + `args` when it is safe to. */
52926
53408
  function splitCommandString(command) {
52927
53409
  const trimmed = command.trim();
@@ -52965,10 +53447,20 @@ function convertServerToXum({ name, server, logger }) {
52965
53447
  ...settings
52966
53448
  };
52967
53449
  }
52968
- const command = buildCommandString(server);
52969
- if (command === void 0) return skip("it has no command");
52970
- const dropped = [...isPlainObject$1(server.env) && Object.keys(server.env).length > 0 ? ["env"] : [], ...typeof server.cwd === "string" ? ["cwd"] : []];
52971
- if (dropped.length > 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets ${dropped.join(" and ")}, which Xum's mcp.jsonc cannot express; the server is written without it (Xum starts stdio servers with your shell environment in the workspace directory).`);
53450
+ const baseCommand = buildCommandString(server);
53451
+ if (baseCommand === void 0) return skip("it has no command");
53452
+ let command = baseCommand;
53453
+ if (isPlainObject$1(server.env) && Object.keys(server.env).length > 0) {
53454
+ const blocker = envPrefixBlocker(server, baseCommand);
53455
+ if (blocker !== void 0) warnWithFallback(logger, `Xum MCP: ${quoteValueForWarning(name)} sets env, but ${blocker}; the server is written without env.`);
53456
+ else {
53457
+ const { prefix, invalidNames, nonStringNames } = buildEnvPrefix(server.env);
53458
+ command = `${prefix}${baseCommand}`;
53459
+ 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.`);
53460
+ 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.`);
53461
+ }
53462
+ }
53463
+ 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).`);
52972
53464
  return Object.keys(settings).length > 0 ? {
52973
53465
  command,
52974
53466
  ...settings
@@ -52988,7 +53480,7 @@ function convertFromXumFormat(servers) {
52988
53480
  for (const [name, entry] of Object.entries(servers)) {
52989
53481
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
52990
53482
  if (typeof entry === "string") {
52991
- result[name] = splitCommandString(entry);
53483
+ result[name] = parseCommandString(entry);
52992
53484
  continue;
52993
53485
  }
52994
53486
  if (!isPlainObject$1(entry)) continue;
@@ -52998,7 +53490,7 @@ function convertFromXumFormat(servers) {
52998
53490
  server.url = entry.url;
52999
53491
  const headers = copyStringHeaders(entry.headers);
53000
53492
  if (headers !== void 0) server.headers = headers;
53001
- } else if (typeof entry.command === "string") Object.assign(server, splitCommandString(entry.command));
53493
+ } else if (typeof entry.command === "string") Object.assign(server, parseCommandString(entry.command));
53002
53494
  else continue;
53003
53495
  if (entry.disabled === true) server.disabled = true;
53004
53496
  if (isStringArray$1(entry.toolAllowlist)) server.enabledTools = entry.toolAllowlist;
@@ -53013,9 +53505,10 @@ function convertFromXumFormat(servers) {
53013
53505
  * (project; a server there wins over the user one of the same name). Both are
53014
53506
  * a JSONC document with a `servers` map:
53015
53507
  *
53016
- * - a stdio server is a shell command string (the object form `{ command }`
53017
- * carries `disabled` / `toolAllowlist`); Xum's mcp.jsonc has no `env` or
53018
- * `cwd`, so those are dropped with a warning;
53508
+ * - a stdio server is a shell command string run with bash (the object form
53509
+ * `{ command }` carries `disabled` / `toolAllowlist`); `env` is written as
53510
+ * `NAME=value` assignments in front of the command, and `cwd`, which has no
53511
+ * equivalent, is dropped with a warning;
53019
53512
  * - a remote server is `{ transport: "http" | "sse" | "auto", url, headers }`
53020
53513
  * (the transport defaults to auto-detection).
53021
53514
  *
@@ -53147,7 +53640,7 @@ function asZcodeRemoteType(stated, url) {
53147
53640
  }
53148
53641
  /**
53149
53642
  * The keys each OAuth flow of ZCode's per-server `oauth` union accepts on a
53150
- * remote server, and which of them are required. Read off the 3.14.3/3.15.3
53643
+ * remote server, and which of them are required. Read off the 3.14.3–3.16.2
53151
53644
  * runtime config schema, since ZCode's MCP page only says to "enable OAuth for
53152
53645
  * the service in its configuration". Both variants are strict, so any other
53153
53646
  * key would make ZCode skip the server.
@@ -53251,7 +53744,11 @@ function resolveZcodeTimeoutMs({ serverName, config, logger }) {
53251
53744
  }
53252
53745
  /**
53253
53746
  * The per-server keys ZCode reads on every transport: `timeoutMs`,
53254
- * `protocolVersion` and the enable toggle (`enableKey`).
53747
+ * `protocolVersion`, `alwaysLoad` and the enable toggle (`enableKey`).
53748
+ *
53749
+ * `alwaysLoad` (added in 3.16.2 alongside tool search) keeps a server's tools
53750
+ * loaded up front instead of deferring them behind tool search. ZCode's schema
53751
+ * types it as a boolean, so any other value is dropped with a warning.
53255
53752
  */
53256
53753
  function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53257
53754
  const common = {};
@@ -53264,6 +53761,9 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53264
53761
  const { protocolVersion } = config;
53265
53762
  if (ZCODE_PROTOCOL_VERSIONS.has(protocolVersion)) common.protocolVersion = protocolVersion;
53266
53763
  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(", ")}.`);
53764
+ const { alwaysLoad } = config;
53765
+ if (typeof alwaysLoad === "boolean") common.alwaysLoad = alwaysLoad;
53766
+ else if (alwaysLoad !== void 0) logger?.warn(`ZCode MCP server "${serverName}": dropping alwaysLoad ${JSON.stringify(alwaysLoad)}, as it must be a boolean.`);
53267
53767
  if (config.disabled === true) common[enableKey] = false;
53268
53768
  return common;
53269
53769
  }
@@ -53276,11 +53776,11 @@ function toZcodeCommonKeys({ serverName, config, enableKey, logger }) {
53276
53776
  * absent.
53277
53777
  *
53278
53778
  * Every server also carries `timeoutMs` (from the canonical `timeout`, or an
53279
- * explicit `timeoutMs`) and an allow-listed `protocolVersion`; stdio servers
53280
- * carry `cwd`, and remote servers an `oauth` built from the namespaced
53281
- * `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
53779
+ * explicit `timeoutMs`), an allow-listed `protocolVersion` and a boolean
53780
+ * `alwaysLoad`; stdio servers carry `cwd`, and remote servers an `oauth` built
53781
+ * from the namespaced `zcodeOauth`. That key is stripped by `RulesyncMcp.getMcpServers()`, so it
53282
53782
  * is read from the authored server through `rawServerOf`. ZCode's docs do not
53283
- * name these keys, so they follow the 3.14.3/3.15.3 runtime's per-server
53783
+ * name these keys, so they follow the 3.14.3–3.16.2 runtime's per-server
53284
53784
  * schema, which is strict: an unknown key makes ZCode skip the server, which
53285
53785
  * is why the output stays a whitelist.
53286
53786
  *
@@ -58417,6 +58917,11 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
58417
58917
  reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
58418
58918
  widens: isNotFalse
58419
58919
  },
58920
+ {
58921
+ path: ["allowNestedUserNamespaces"],
58922
+ reason: "lets sandboxed Linux commands create user namespaces, which gets around the sandbox's file protections",
58923
+ widens: isNotFalse
58924
+ },
58420
58925
  {
58421
58926
  path: ["allowUnsandboxedCommands"],
58422
58927
  reason: "controls whether Claude may retry a blocked command outside the sandbox",
@@ -68866,7 +69371,7 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
68866
69371
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
68867
69372
  *
68868
69373
  * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
68869
- * `write` or `yolo`); without it the key is left as it is in the file, and on
69374
+ * `always-ask-cached`, `write` or `yolo`); without it the key is left as it is in the file, and on
68870
69375
  * import a recognized value is read back into the override.
68871
69376
  *
68872
69377
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
@@ -71138,6 +71643,8 @@ function buildReasonixPermissionEntry(toolName, pattern) {
71138
71643
  const REASONIX_TOOL_LABEL = "Reasonix";
71139
71644
  /** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
71140
71645
  const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
71646
+ /** The `[permissions]` key the override's `askOnExternalOrigin` writes and reads. */
71647
+ const REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY = "ask_on_external_origin";
71141
71648
  /**
71142
71649
  * `[sandbox]` keys whose authored value loosens the enforcement layer beneath
71143
71650
  * the permission policy: they take Bash out of its OS jail, open that jail to
@@ -71340,6 +71847,95 @@ function applyPermissionsMode({ permissions, authored, global, relativeFilePath,
71340
71847
  reason: "lets every writer call that no rule matches run without a prompt"
71341
71848
  }];
71342
71849
  }
71850
+ /**
71851
+ * Writes the override's `askOnExternalOrigin` into `[permissions]`. Reasonix
71852
+ * honors the key only in the user config (`narrowPermissions` in
71853
+ * `project_scope.go`), so a project-scope generate skips it with a warning and
71854
+ * leaves whatever the file had. Turning it on only adds prompts, so that is
71855
+ * quiet; turning off a value the file had on is reported, since Auto and YOLO
71856
+ * then run calls that repeat text from a web page or tool result without asking.
71857
+ * As with `applyAllowDynamicBash`, only a literal `true` counts as quiet, so a
71858
+ * value a `--no-validate` run let through is never written in silence.
71859
+ *
71860
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.47.1/docs/GUIDE.md
71861
+ */
71862
+ function applyAskOnExternalOrigin({ permissions, override, global, relativeFilePath, logger }) {
71863
+ const authored = override?.askOnExternalOrigin;
71864
+ if (authored === void 0) return [];
71865
+ if (!global) {
71866
+ 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.`);
71867
+ return [];
71868
+ }
71869
+ const previous = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
71870
+ permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY] = authored;
71871
+ if (authored === true || previous !== true && authored === false) return [];
71872
+ return [{
71873
+ label: `permissions.${REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY}`,
71874
+ reason: "lets Auto and YOLO run a call that repeats text from a web page or tool result without asking first"
71875
+ }];
71876
+ }
71877
+ /**
71878
+ * The `tools` patch entry — the `[tools]` table with the override's `disabled`
71879
+ * list laid over it, or nothing when the override does not author one — and the
71880
+ * trust rows it carries. Adding a name only removes a tool, and a project file
71881
+ * can only add to the user's list, so that stays quiet. The list is replaced
71882
+ * whole, though, so a name the file had turned off and the override leaves out
71883
+ * is turned back on, and that is reported, as are a `[tools]` or a `disabled`
71884
+ * the file holds in a shape that cannot be read. Only `disabled` is written:
71885
+ * the rest of `[tools]` (timeouts, search backends, ...) stays as it is.
71886
+ */
71887
+ function applyToolsDisabled({ existing, override }) {
71888
+ const disabled = override?.tools?.disabled;
71889
+ if (disabled === void 0) return {
71890
+ patch: {},
71891
+ entries: []
71892
+ };
71893
+ const patch = { tools: {
71894
+ ...asReasonixRecord(existing),
71895
+ disabled
71896
+ } };
71897
+ if (existing === void 0) return {
71898
+ patch,
71899
+ entries: []
71900
+ };
71901
+ if (!isRecord$1(existing)) return {
71902
+ patch,
71903
+ entries: [{
71904
+ label: "tools",
71905
+ reason: replacedUnreadableReason({
71906
+ shape: "object",
71907
+ toolLabel: REASONIX_TOOL_LABEL
71908
+ })
71909
+ }]
71910
+ };
71911
+ const before = existing.disabled;
71912
+ if (before === void 0) return {
71913
+ patch,
71914
+ entries: []
71915
+ };
71916
+ if (!Array.isArray(before)) return {
71917
+ patch,
71918
+ entries: [{
71919
+ label: "tools.disabled",
71920
+ reason: replacedUnreadableReason({
71921
+ shape: "list",
71922
+ toolLabel: REASONIX_TOOL_LABEL
71923
+ })
71924
+ }]
71925
+ };
71926
+ const reEnabled = (0, es_toolkit.uniq)(before.filter((name) => !disabled.includes(name)));
71927
+ if (reEnabled.length === 0) return {
71928
+ patch,
71929
+ entries: []
71930
+ };
71931
+ return {
71932
+ patch,
71933
+ entries: [{
71934
+ label: "tools.disabled",
71935
+ reason: `turns back on ${reEnabled.map((name) => quoteValueForWarning(name)).join(", ")}, which the file had turned off`
71936
+ }]
71937
+ };
71938
+ }
71343
71939
  function parseReasonixConfig(fileContent) {
71344
71940
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
71345
71941
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -71395,6 +71991,11 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71395
71991
  });
71396
71992
  }
71397
71993
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
71994
+ await warnIfReasonixNestedProjectConfigShadowed({
71995
+ outputRoot,
71996
+ global,
71997
+ logger
71998
+ });
71398
71999
  const paths = this.getSettablePaths({ global });
71399
72000
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
71400
72001
  const relativeFilePathForLog = toPosixPath((0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath));
@@ -71454,7 +72055,20 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71454
72055
  relativeFilePath: relativeFilePathForLog,
71455
72056
  logger
71456
72057
  }));
72058
+ trustAffecting.push(...applyAskOnExternalOrigin({
72059
+ permissions: mergedPermissions,
72060
+ override,
72061
+ global,
72062
+ relativeFilePath: relativeFilePathForLog,
72063
+ logger
72064
+ }));
71457
72065
  const patch = { permissions: mergedPermissions };
72066
+ const toolsDisabled = applyToolsDisabled({
72067
+ existing: parsed.tools,
72068
+ override
72069
+ });
72070
+ Object.assign(patch, toolsDisabled.patch);
72071
+ trustAffecting.push(...toolsDisabled.entries);
71458
72072
  if (override?.sandbox !== void 0) {
71459
72073
  const authoredSandbox = dropUserOnlySandboxKeys({
71460
72074
  sandbox: override.sandbox,
@@ -71528,6 +72142,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
71528
72142
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
71529
72143
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
71530
72144
  if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
72145
+ const askOnExternalOrigin = permissions[REASONIX_ASK_ON_EXTERNAL_ORIGIN_KEY];
72146
+ if (this.global && typeof askOnExternalOrigin === "boolean") reasonixOverride.askOnExternalOrigin = askOnExternalOrigin;
72147
+ const toolsDisabled = asReasonixRecord(this.toml.tools).disabled;
72148
+ if (Array.isArray(toolsDisabled)) {
72149
+ const names = toReasonixStringArray(toolsDisabled);
72150
+ 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.`);
72151
+ reasonixOverride.tools = { disabled: names };
72152
+ }
71531
72153
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
71532
72154
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
71533
72155
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -73109,7 +73731,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
73109
73731
  const authoredProviderOptions = isPlainObject$1(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
73110
73732
  const runtimeModeRefusesProviderOptions = authoredProviderOptions !== void 0 && runtime.active;
73111
73733
  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.`);
73112
- const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : authoredProviderOptions;
73734
+ const overrideProviderOptions = runtimeModeRefusesProviderOptions ? void 0 : dropUntrustedProviderOptions({
73735
+ providerOptions: authoredProviderOptions,
73736
+ global,
73737
+ filePath,
73738
+ logger
73739
+ });
73113
73740
  const authoredPolicies = pickSecurityPolicies(override, {
73114
73741
  filePath,
73115
73742
  logger,
@@ -73613,6 +74240,68 @@ function taktModeToRulesyncConfig(mode) {
73613
74240
  default: return { permission: { bash: { [CATCH_ALL_PATTERN$1]: "deny" } } };
73614
74241
  }
73615
74242
  }
74243
+ const TAKT_BASE_URL_PROVIDERS = [
74244
+ "codex",
74245
+ "claude",
74246
+ "deepseek_harness"
74247
+ ];
74248
+ const TAKT_DEEPSEEK_HARNESS_KEY = "deepseek_harness";
74249
+ /** Mirrors Takt's `isLoopbackBaseUrl`: localhost, *.localhost, ::1 or 127.x.x.x. */
74250
+ function isLoopbackBaseUrl(value) {
74251
+ let hostname;
74252
+ try {
74253
+ hostname = new URL(value).hostname.toLowerCase();
74254
+ } catch {
74255
+ return false;
74256
+ }
74257
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "[::1]") return true;
74258
+ const octets = hostname.split(".");
74259
+ return octets.length === 4 && octets[0] === "127" && octets.every((octet) => /^\d+$/.test(octet) && Number(octet) <= 255);
74260
+ }
74261
+ /** The keys of one `provider_options.<provider>` entry Takt rejects in config.yaml. */
74262
+ function untrustedProviderOptionKeys({ provider, options, global }) {
74263
+ const keys = [];
74264
+ if (provider === TAKT_DEEPSEEK_HARNESS_KEY) {
74265
+ if (options.reasoning_effort !== void 0) keys.push("reasoning_effort");
74266
+ const { plugins } = options;
74267
+ if (!global && plugins !== void 0 && (!Array.isArray(plugins) || plugins.length > 0)) keys.push("plugins");
74268
+ }
74269
+ const baseUrl = options.base_url;
74270
+ if (!global && TAKT_BASE_URL_PROVIDERS.includes(provider) && baseUrl !== void 0 && (typeof baseUrl !== "string" || !isLoopbackBaseUrl(baseUrl))) keys.push("base_url");
74271
+ return keys;
74272
+ }
74273
+ /**
74274
+ * Drop the `provider_options` entries Takt refuses to load from `config.yaml`
74275
+ * (it fails at startup with a `Configuration error`), warning for each:
74276
+ * - `deepseek_harness.reasoning_effort` in either scope — Takt accepts it only
74277
+ * in runtime profile options or `TAKT_PROVIDER_OPTIONS_DEEPSEEK_HARNESS_REASONING_EFFORT`.
74278
+ * - at project scope, a non-empty `deepseek_harness.plugins` (Takt 0.71.0+,
74279
+ * global config only) and a non-loopback `{codex,claude,deepseek_harness}.base_url`.
74280
+ * https://github.com/nrslib/takt/blob/main/src/infra/config/providerOptions.ts
74281
+ * https://github.com/nrslib/takt/blob/main/docs/configuration.md
74282
+ */
74283
+ function dropUntrustedProviderOptions({ providerOptions, global, filePath, logger }) {
74284
+ if (providerOptions === void 0) return;
74285
+ const dropped = [];
74286
+ const result = {};
74287
+ for (const [provider, options] of Object.entries(providerOptions)) {
74288
+ if (!isPlainObject$1(options)) {
74289
+ result[provider] = options;
74290
+ continue;
74291
+ }
74292
+ const untrusted = untrustedProviderOptionKeys({
74293
+ provider,
74294
+ options,
74295
+ global
74296
+ });
74297
+ const kept = Object.fromEntries(Object.entries(options).filter(([key]) => !untrusted.includes(key)));
74298
+ dropped.push(...untrusted.map((key) => `${TAKT_PROVIDER_OPTIONS_KEY}.${provider}.${key}`));
74299
+ if (Object.keys(kept).length > 0 || Object.keys(options).length === 0) result[provider] = kept;
74300
+ }
74301
+ if (dropped.length === 0) return providerOptions;
74302
+ 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.`);
74303
+ return Object.keys(result).length > 0 ? result : void 0;
74304
+ }
73616
74305
  /**
73617
74306
  * Lift Takt's security-policy keys out of a source object, keeping only the
73618
74307
  * shapes Takt itself accepts (a boolean, or a table of booleans). Used in both
@@ -85942,6 +86631,19 @@ var WarpSkill = class WarpSkill extends ToolSkill {
85942
86631
  //#endregion
85943
86632
  //#region src/features/skills/xum-skill.ts
85944
86633
  /**
86634
+ * The Agent Skills standard keys the `agentsskills` section carries. On import
86635
+ * they go back there, exactly as for the `agentsskills` target, and every other
86636
+ * key besides `name` / `description` goes into the `xum` section.
86637
+ */
86638
+ const AGENT_SKILLS_STANDARD_KEYS = /* @__PURE__ */ new Set([
86639
+ "name",
86640
+ "description",
86641
+ "license",
86642
+ "compatibility",
86643
+ "metadata",
86644
+ "allowed-tools"
86645
+ ]);
86646
+ /**
85945
86647
  * Xum skills follow the Agent Skills layout (`<name>/SKILL.md` with the
85946
86648
  * required `name` and `description` frontmatter; the directory name is
85947
86649
  * lowercase letters, digits and single hyphens up to 64 characters, and
@@ -85949,12 +86651,82 @@ var WarpSkill = class WarpSkill extends ToolSkill {
85949
86651
  * Xum's locations: `.xum/skills/` (project) and `~/.xum/skills/` (user).
85950
86652
  * Within one directory `.xum/skills` wins over `.agents/skills`.
85951
86653
  *
86654
+ * On top of the standard fields, Xum reads `disable-model-invocation` (and its
86655
+ * own `advertise: false` alias), `user-invocable`, `argument-hint`,
86656
+ * `when_to_use` / `when-to-use` and `paths` (globs that make the skill
86657
+ * path-scoped). Those come from the `xum` section of the rulesync skill and are
86658
+ * written verbatim; the two invocation gates fall back to the root-level
86659
+ * rulesync values.
86660
+ *
85952
86661
  * @see https://xum.cdr.dev/agents/agent-skills
85953
86662
  */
85954
- var XumSkill = class extends AgentsSkillsSkill {
86663
+ var XumSkill = class XumSkill extends AgentsSkillsSkill {
85955
86664
  static getSettablePaths(_options = {}) {
85956
86665
  return { relativeDirPath: XUM_SKILLS_DIR_PATH };
85957
86666
  }
86667
+ toRulesyncSkill() {
86668
+ const base = super.toRulesyncSkill();
86669
+ const xumSection = Object.fromEntries(Object.entries(this.getFrontmatter()).filter(([key]) => !AGENT_SKILLS_STANDARD_KEYS.has(key)));
86670
+ if (Object.keys(xumSection).length === 0) return base;
86671
+ const rulesyncFrontmatter = {
86672
+ ...base.getFrontmatter(),
86673
+ xum: xumSection
86674
+ };
86675
+ return new RulesyncSkill({
86676
+ outputRoot: base.getOutputRoot(),
86677
+ relativeDirPath: base.getRelativeDirPath(),
86678
+ dirName: base.getDirName(),
86679
+ frontmatter: rulesyncFrontmatter,
86680
+ body: base.getBody(),
86681
+ otherFiles: base.getOtherFiles(),
86682
+ validate: true,
86683
+ global: this.global
86684
+ });
86685
+ }
86686
+ static fromRulesyncSkill({ outputRoot = process.cwd(), rulesyncSkill, validate = true, global = false, logger }) {
86687
+ const settablePaths = XumSkill.getSettablePaths({ global });
86688
+ const rulesyncFrontmatter = rulesyncSkill.getFrontmatter();
86689
+ const dirName = rulesyncSkill.getDirName();
86690
+ const xumSection = {};
86691
+ const ignoredKeys = [];
86692
+ for (const [key, value] of Object.entries(rulesyncFrontmatter.xum ?? {})) if (AGENT_SKILLS_STANDARD_KEYS.has(key)) ignoredKeys.push(key);
86693
+ else xumSection[key] = value;
86694
+ if (ignoredKeys.length > 0) warnWithFallback(logger, `${(0, node_path.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.`);
86695
+ const userInvocable = resolveUserInvocable({
86696
+ rootFrontmatter: rulesyncFrontmatter,
86697
+ section: xumSection
86698
+ });
86699
+ const disableModelInvocation = resolveDisableModelInvocation({
86700
+ rootFrontmatter: rulesyncFrontmatter,
86701
+ section: xumSection
86702
+ });
86703
+ const frontmatter = {
86704
+ name: rulesyncFrontmatter.name,
86705
+ description: rulesyncFrontmatter.description,
86706
+ ...toSpecConformantAgentSkillFields(resolveAgentsSkillsSection(rulesyncFrontmatter)),
86707
+ ...xumSection,
86708
+ ...userInvocable !== void 0 && { "user-invocable": userInvocable },
86709
+ ...disableModelInvocation !== void 0 && { "disable-model-invocation": disableModelInvocation }
86710
+ };
86711
+ AgentsSkillsSkill.reportSpecViolations({
86712
+ outputRoot,
86713
+ relativeDirPath: settablePaths.relativeDirPath,
86714
+ dirName,
86715
+ frontmatter,
86716
+ sourceAllowedTools: rulesyncFrontmatter.agentsskills?.["allowed-tools"],
86717
+ logger
86718
+ });
86719
+ return new XumSkill({
86720
+ outputRoot,
86721
+ relativeDirPath: settablePaths.relativeDirPath,
86722
+ dirName,
86723
+ frontmatter,
86724
+ body: rulesyncSkill.getBody(),
86725
+ otherFiles: rulesyncSkill.getOtherFiles(),
86726
+ validate,
86727
+ global
86728
+ });
86729
+ }
85958
86730
  static isTargetedByRulesyncSkill(rulesyncSkill) {
85959
86731
  const targets = rulesyncSkill.getFrontmatter().targets;
85960
86732
  return targets.includes("*") || targets.includes("xum");
@@ -92110,6 +92882,15 @@ const KiroCliSubagentJsonSchema = zod_mini.z.looseObject({
92110
92882
  allowedTools: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.array(zod_mini.z.string()))),
92111
92883
  includeMcpJson: zod_mini.z.optional(zod_mini.z.nullable(zod_mini.z.boolean()))
92112
92884
  });
92885
+ /**
92886
+ * The name Kiro gives an agent file: its path relative to the agents directory
92887
+ * without the extension, so a nested `team/planner.md` is `team/planner`.
92888
+ * https://kiro.dev/docs/custom-agents/
92889
+ */
92890
+ function kiroAgentNameFromPath(relativeFilePath) {
92891
+ const posixPath = toPosixPath(relativeFilePath);
92892
+ return posixPath.slice(0, posixPath.length - (0, node_path.extname)(posixPath).length);
92893
+ }
92113
92894
  var KiroSubagent = class extends ToolSubagent {
92114
92895
  body;
92115
92896
  constructor({ body, ...rest }) {
@@ -92145,7 +92926,7 @@ var KiroSubagent = class extends ToolSubagent {
92145
92926
  outputRoot: ".",
92146
92927
  frontmatter: {
92147
92928
  targets: [toolTarget],
92148
- name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), ".json"),
92929
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
92149
92930
  description: description ?? void 0,
92150
92931
  ...Object.keys(kiroSection).length > 0 && { kiro: kiroSection }
92151
92932
  },
@@ -92295,7 +93076,7 @@ var KiroIdeSubagent = class KiroIdeSubagent extends ToolSubagent {
92295
93076
  outputRoot: ".",
92296
93077
  frontmatter: {
92297
93078
  targets: ["*"],
92298
- name: name ?? this.getRelativeFilePath().replace(/\.md$/, ""),
93079
+ name: name ?? kiroAgentNameFromPath(this.getRelativeFilePath()),
92299
93080
  description: description ?? "",
92300
93081
  ...Object.keys(kiroIdeSection).length > 0 && { "kiro-ide": kiroIdeSection }
92301
93082
  },
@@ -94913,7 +95694,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
94913
95694
  supportsProject: true,
94914
95695
  supportsSimulated: false,
94915
95696
  supportsGlobal: true,
94916
- filePattern: "*.json"
95697
+ filePattern: "**/*.json",
95698
+ supportsNestedPaths: true
94917
95699
  }
94918
95700
  }],
94919
95701
  ["kiro-ide", {
@@ -94922,7 +95704,8 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
94922
95704
  supportsProject: true,
94923
95705
  supportsSimulated: false,
94924
95706
  supportsGlobal: true,
94925
- filePattern: "*.md"
95707
+ filePattern: "**/*.md",
95708
+ supportsNestedPaths: true
94926
95709
  }
94927
95710
  }],
94928
95711
  ["lettacode", {
@@ -109164,6 +109947,12 @@ Object.defineProperty(exports, "PACKAGING_TOOL_TARGETS", {
109164
109947
  return PACKAGING_TOOL_TARGETS;
109165
109948
  }
109166
109949
  });
109950
+ Object.defineProperty(exports, "PathOutsideRootError", {
109951
+ enumerable: true,
109952
+ get: function() {
109953
+ return PathOutsideRootError;
109954
+ }
109955
+ });
109167
109956
  Object.defineProperty(exports, "QWENCODE_DIR", {
109168
109957
  enumerable: true,
109169
109958
  get: function() {
@@ -109464,6 +110253,12 @@ Object.defineProperty(exports, "assertDirectoryIfExists", {
109464
110253
  return assertDirectoryIfExists;
109465
110254
  }
109466
110255
  });
110256
+ Object.defineProperty(exports, "assertPathResolvesInsideRoot", {
110257
+ enumerable: true,
110258
+ get: function() {
110259
+ return assertPathResolvesInsideRoot;
110260
+ }
110261
+ });
109467
110262
  Object.defineProperty(exports, "assertTargetsFeaturesExclusive", {
109468
110263
  enumerable: true,
109469
110264
  get: function() {