rulesync 16.3.0 → 16.4.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.
@@ -2381,6 +2381,7 @@ const PI_HOOK_EVENTS = [
2381
2381
  "preToolUse",
2382
2382
  "postToolUse",
2383
2383
  "preModelInvocation",
2384
+ "postModelInvocation",
2384
2385
  "beforeSubmitPrompt",
2385
2386
  "stop",
2386
2387
  "preCompact",
@@ -3087,6 +3088,7 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
3087
3088
  preToolUse: "tool_call",
3088
3089
  postToolUse: "tool_result",
3089
3090
  preModelInvocation: "context",
3091
+ postModelInvocation: "message_end",
3090
3092
  beforeSubmitPrompt: "input",
3091
3093
  stop: "agent_end",
3092
3094
  preCompact: "session_before_compact",
@@ -6852,24 +6854,14 @@ const SHARED_CONFIG_OWNERSHIP = {
6852
6854
  },
6853
6855
  ".devin/config.json": {
6854
6856
  format: "json",
6855
- features: {
6856
- mcp: {
6857
- kind: "replace-owned-keys",
6858
- ownedKeys: ["mcpServers"]
6859
- },
6860
- permissions: {
6861
- kind: "replace-owned-keys",
6862
- ownedKeys: ["permissions"]
6863
- }
6864
- }
6857
+ features: { permissions: {
6858
+ kind: "replace-owned-keys",
6859
+ ownedKeys: ["permissions"]
6860
+ } }
6865
6861
  },
6866
6862
  ".config/devin/config.json": {
6867
6863
  format: "json",
6868
6864
  features: {
6869
- mcp: {
6870
- kind: "replace-owned-keys",
6871
- ownedKeys: ["mcpServers"]
6872
- },
6873
6865
  hooks: {
6874
6866
  kind: "replace-owned-keys",
6875
6867
  ownedKeys: ["hooks"]
@@ -9069,6 +9061,7 @@ const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
9069
9061
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
9070
9062
  const DEVIN_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
9071
9063
  const DEVIN_CONFIG_FILE_NAME = "config.json";
9064
+ const DEVIN_MCP_CONFIG_FILE_NAME = "mcp_config.json";
9072
9065
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9073
9066
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9074
9067
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
@@ -11326,6 +11319,7 @@ const QWENCODE_COMMANDS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "commands")
11326
11319
  const QWENCODE_AGENTS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "agents");
11327
11320
  const QWENCODE_SKILLS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "skills");
11328
11321
  const QWENCODE_RULE_FILE_NAME = "QWEN.md";
11322
+ const QWENCODE_LOCAL_RULE_FILE_NAME = "QWEN.local.md";
11329
11323
  const QWENCODE_IGNORE_FILE_NAME = ".qwenignore";
11330
11324
  const QWENCODE_SETTINGS_FILE_NAME = "settings.json";
11331
11325
  //#endregion
@@ -12072,6 +12066,7 @@ const WARP_SKILLS_DIR_PATH = (0, node_path.join)(WARP_DIR, "skills");
12072
12066
  const WARP_LINUX_DIR = (0, node_path.join)(".config", "warp-terminal");
12073
12067
  const WARP_WIN32_DIR = (0, node_path.join)("AppData", "Local", "warp", "Warp", "config");
12074
12068
  const WARP_RULE_FILE_NAME = "AGENTS.md";
12069
+ const WARP_GLOBAL_RULE_DIR = ".agents";
12075
12070
  const WARP_MCP_FILE_NAME = ".mcp.json";
12076
12071
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
12077
12072
  const WARP_IGNORE_FILE_NAME = ".warpindexingignore";
@@ -14807,9 +14802,9 @@ const DEVIN_CONVERTER_CONFIG = {
14807
14802
  * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
14808
14803
  * level IS the event map (no wrapper key).
14809
14804
  * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
14810
- * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
14811
- * features, so reads and writes merge into the existing JSON and the file is
14812
- * never deleted in global mode.
14805
+ * file is shared with the permissions (`permissions`) feature (MCP moved to
14806
+ * the dedicated mcp_config.json in v3000.3), so reads and writes merge into
14807
+ * the existing JSON and the file is never deleted in global mode.
14813
14808
  *
14814
14809
  * @see https://docs.devin.ai/cli/extensibility/hooks/overview
14815
14810
  */
@@ -16421,6 +16416,13 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
16421
16416
  */
16422
16417
  const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
16423
16418
  /**
16419
+ * Pi extension events that fire for every message role (user, assistant,
16420
+ * toolResult). The generated handler gates these on the assistant role so a
16421
+ * `postModelInvocation` hook runs once per finalized model response rather
16422
+ * than for every message in the conversation.
16423
+ */
16424
+ const PI_ASSISTANT_MESSAGE_EVENTS = /* @__PURE__ */ new Set(["message_end"]);
16425
+ /**
16424
16426
  * Validate a hook matcher as a regular expression and return it as a JS
16425
16427
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
16426
16428
  */
@@ -16461,7 +16463,10 @@ function buildSubscriptionLines(handlerGroups) {
16461
16463
  const lines = [];
16462
16464
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
16463
16465
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
16464
- lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesToolName ? "event" : ""}) => {`);
16466
+ const gatesOnAssistant = PI_ASSISTANT_MESSAGE_EVENTS.has(piEvent);
16467
+ const usesEvent = usesToolName || gatesOnAssistant;
16468
+ lines.push(` pi.on(${JSON.stringify(piEvent)}, async (${usesEvent ? "event" : ""}) => {`);
16469
+ if (gatesOnAssistant) lines.push(` if (event.message.role !== "assistant") return;`);
16465
16470
  for (const handler of handlers) {
16466
16471
  const embeddedCommand = JSON.stringify(handler.command);
16467
16472
  if (usesToolName && handler.matcher) {
@@ -16596,6 +16601,7 @@ function canonicalDefToQwencodeHook(def) {
16596
16601
  const type = def.type ?? "command";
16597
16602
  const isHttp = type === "http";
16598
16603
  const isCommand = type === "command";
16604
+ const isPrompt = type === "prompt";
16599
16605
  return {
16600
16606
  type,
16601
16607
  ...compact({
@@ -16605,6 +16611,8 @@ function canonicalDefToQwencodeHook(def) {
16605
16611
  name: def.name,
16606
16612
  description: def.description,
16607
16613
  statusMessage: def.statusMessage,
16614
+ prompt: isPrompt ? def.prompt : void 0,
16615
+ model: isPrompt ? def.model : void 0,
16608
16616
  async: isCommand ? def.async : void 0,
16609
16617
  env: isCommand ? def.env : void 0,
16610
16618
  shell: isCommand ? def.shell : void 0,
@@ -16623,7 +16631,7 @@ function canonicalDefToQwencodeHook(def) {
16623
16631
  * is needed here — commands are passed through verbatim and any such variable
16624
16632
  * reference stays intact for Qwen Code to expand.
16625
16633
  */
16626
- function canonicalToQwencodeHooks(config) {
16634
+ function canonicalToQwencodeHooks(config, logger) {
16627
16635
  const qwencodeSupported = new Set(QWENCODE_HOOK_EVENTS);
16628
16636
  const sharedHooks = {};
16629
16637
  for (const [event, defs] of Object.entries(config.hooks)) if (qwencodeSupported.has(event)) sharedHooks[event] = defs;
@@ -16643,6 +16651,7 @@ function canonicalToQwencodeHooks(config) {
16643
16651
  const byMatcher = /* @__PURE__ */ new Map();
16644
16652
  for (const def of definitions) {
16645
16653
  if (!qwencodeSupportedTypes.has(def.type ?? "command")) continue;
16654
+ if (def.type === "prompt" && !def.prompt) logger?.warn(`Qwen Code prompt hook on '${eventName}' has no 'prompt' field; Qwen Code will load it and fail it at runtime.`);
16646
16655
  const key = def.matcher ?? "";
16647
16656
  const list = byMatcher.get(key);
16648
16657
  if (list) list.push(def);
@@ -16681,7 +16690,9 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
16681
16690
  shell: zod_mini.z.optional(zod_mini.z.string()),
16682
16691
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16683
16692
  allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16684
- once: zod_mini.z.optional(zod_mini.z.boolean())
16693
+ once: zod_mini.z.optional(zod_mini.z.boolean()),
16694
+ prompt: zod_mini.z.optional(zod_mini.z.string()),
16695
+ model: zod_mini.z.optional(zod_mini.z.string())
16685
16696
  });
16686
16697
  /**
16687
16698
  * A matcher group entry in a Qwen Code event array.
@@ -16696,37 +16707,43 @@ const QwencodeMatcherEntrySchema = zod_mini.z.looseObject({
16696
16707
  /**
16697
16708
  * Convert a single parsed Qwen Code matcher group into canonical hook definitions.
16698
16709
  */
16710
+ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
16711
+ const h = hook;
16712
+ const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16713
+ const isHttp = hookType === "http";
16714
+ const isCommand = hookType === "command";
16715
+ const isPrompt = hookType === "prompt";
16716
+ const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16717
+ return {
16718
+ type: hookType,
16719
+ ...compact({
16720
+ command: h.command,
16721
+ url: h.url,
16722
+ timeout: h.timeout,
16723
+ name: h.name,
16724
+ description: h.description,
16725
+ statusMessage: h.statusMessage,
16726
+ async: isCommand ? h.async : void 0,
16727
+ env: isCommand ? h.env : void 0,
16728
+ shell: isCommand ? shell : void 0,
16729
+ headers: isHttp ? h.headers : void 0,
16730
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16731
+ once: isHttp ? h.once : void 0,
16732
+ prompt: isPrompt ? h.prompt : void 0,
16733
+ model: isPrompt ? h.model : void 0,
16734
+ sequential: sequential ? true : void 0,
16735
+ matcher
16736
+ })
16737
+ };
16738
+ }
16699
16739
  function qwencodeMatcherEntryToCanonical(entry) {
16700
- const defs = [];
16701
- const hooks = entry.hooks ?? [];
16702
16740
  const sequential = entry.sequential === true;
16703
16741
  const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
16704
- for (const h of hooks) {
16705
- const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16706
- const isHttp = hookType === "http";
16707
- const isCommand = hookType === "command";
16708
- const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16709
- defs.push({
16710
- type: hookType,
16711
- ...compact({
16712
- command: h.command,
16713
- url: h.url,
16714
- timeout: h.timeout,
16715
- name: h.name,
16716
- description: h.description,
16717
- statusMessage: h.statusMessage,
16718
- async: isCommand ? h.async : void 0,
16719
- env: isCommand ? h.env : void 0,
16720
- shell: isCommand ? shell : void 0,
16721
- headers: isHttp ? h.headers : void 0,
16722
- allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16723
- once: isHttp ? h.once : void 0,
16724
- sequential: sequential ? true : void 0,
16725
- matcher
16726
- })
16727
- });
16728
- }
16729
- return defs;
16742
+ return (entry.hooks ?? []).map((hook) => qwencodeHookEntryToCanonical({
16743
+ hook,
16744
+ sequential,
16745
+ matcher
16746
+ }));
16730
16747
  }
16731
16748
  /**
16732
16749
  * Extract hooks from Qwen Code settings.json into canonical format.
@@ -16774,12 +16791,12 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
16774
16791
  validate
16775
16792
  });
16776
16793
  }
16777
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
16794
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16778
16795
  const paths = QwencodeHooks.getSettablePaths({ global });
16779
16796
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16780
16797
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16781
16798
  const config = rulesyncHooks.getJson();
16782
- const patch = { hooks: canonicalToQwencodeHooks(config) };
16799
+ const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
16783
16800
  const disableAllHooks = config.qwencode?.disableAllHooks;
16784
16801
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
16785
16802
  const fileContent = applySharedConfigPatch({
@@ -20805,18 +20822,24 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
20805
20822
  /**
20806
20823
  * MCP generator for Devin Local (native `.devin/` configuration).
20807
20824
  *
20808
- * Devin reads MCP servers from the `mcpServers` key of its native config file:
20809
- * - Project scope: `.devin/config.json`
20810
- * - Global scope: `~/.config/devin/config.json`
20825
+ * Since v3000.3 (the Local 3.6 release), Devin reads MCP servers from a
20826
+ * dedicated `mcpServers`-keyed config file rather than the shared
20827
+ * `config.json`:
20828
+ * - Project scope: `.devin/mcp_config.json`
20829
+ * - Global scope: `~/.config/devin/mcp_config.json`
20811
20830
  *
20812
- * The config file is shared with the permissions feature (`permissions` key)
20813
- * and, in global mode, the hooks feature (`hooks` key), so reads and writes
20814
- * merge into the existing JSON rather than overwriting it, and the file is
20815
- * never deleted. Each server is a stdio entry ({ command, args, env }) or a
20816
- * remote entry ({ serverUrl | url, headers }), and may carry an optional
20817
- * `disabledTools` array.
20831
+ * Legacy `mcpServers` entries left in `config.json` are auto-migrated into
20832
+ * the dedicated file on Devin startup, so writing the legacy key would fight
20833
+ * the migration. Import still falls back to the legacy `config.json`
20834
+ * `mcpServers` key when no dedicated file exists, so pre-v3000.3 repos
20835
+ * migrate cleanly. The gitignored `.devin/mcp_config.local.json` override is
20836
+ * the user's personal territory and is never read or written.
20818
20837
  *
20819
- * @see https://docs.devin.ai/cli/extensibility/configuration
20838
+ * Each server is a stdio entry ({ command, args, env }) or a remote entry
20839
+ * ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
20840
+ * array.
20841
+ *
20842
+ * @see https://docs.devin.ai/cli/extensibility/mcp/configuration
20820
20843
  */
20821
20844
  var DevinMcp = class DevinMcp extends ToolMcp {
20822
20845
  json;
@@ -20834,31 +20857,27 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20834
20857
  throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
20835
20858
  }
20836
20859
  }
20837
- /**
20838
- * config.json may carry the permissions/hooks features' keys, so it is never
20839
- * deleted; only the managed `mcpServers` key is rewritten.
20840
- */
20841
- isDeletable() {
20842
- return false;
20843
- }
20844
20860
  static getSettablePaths({ global = false } = {}) {
20845
20861
  if (global) return {
20846
20862
  relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
20847
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
20863
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20848
20864
  };
20849
20865
  return {
20850
20866
  relativeDirPath: DEVIN_DIR,
20851
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
20867
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20852
20868
  };
20853
20869
  }
20854
20870
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20855
20871
  const paths = this.getSettablePaths({ global });
20856
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
20857
- const json = this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath);
20858
- const newJson = {
20859
- ...json,
20860
- mcpServers: json.mcpServers ?? {}
20861
- };
20872
+ let fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
20873
+ if (fileContent === null) {
20874
+ const legacyContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME));
20875
+ if (legacyContent !== null) {
20876
+ const legacyJson = this.parseJsonOrThrow(legacyContent, paths.relativeDirPath, DEVIN_CONFIG_FILE_NAME);
20877
+ fileContent = JSON.stringify({ mcpServers: legacyJson.mcpServers ?? {} }, null, 2);
20878
+ }
20879
+ }
20880
+ const newJson = { mcpServers: this.parseJsonOrThrow(fileContent ?? "{\"mcpServers\":{}}", paths.relativeDirPath, paths.relativeFilePath).mcpServers ?? {} };
20862
20881
  return new DevinMcp({
20863
20882
  outputRoot,
20864
20883
  relativeDirPath: paths.relativeDirPath,
@@ -20868,21 +20887,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20868
20887
  global
20869
20888
  });
20870
20889
  }
20871
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
20890
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
20872
20891
  const paths = this.getSettablePaths({ global });
20873
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20874
- const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({ mcpServers: {} }, null, 2);
20892
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
20893
+ if (existingContent !== null) {
20894
+ const existingJson = this.parseJsonOrThrow(existingContent, paths.relativeDirPath, paths.relativeFilePath);
20895
+ const existingServers = existingJson.mcpServers && typeof existingJson.mcpServers === "object" ? Object.keys(existingJson.mcpServers) : [];
20896
+ const managedServers = new Set(Object.keys(rulesyncMcp.getMcpServers()));
20897
+ const dropped = existingServers.filter((name) => !managedServers.has(name));
20898
+ if (dropped.length > 0) logger?.warn(`Devin MCP servers not managed by rulesync will be removed from ${(0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)}: ${dropped.join(", ")}. Run 'rulesync import' first to keep them, or move them to mcp_config.local.json.`);
20899
+ }
20875
20900
  return new DevinMcp({
20876
20901
  outputRoot,
20877
20902
  relativeDirPath: paths.relativeDirPath,
20878
20903
  relativeFilePath: paths.relativeFilePath,
20879
- fileContent: applySharedConfigPatch({
20880
- fileKey: sharedConfigFileKey(paths),
20881
- feature: "mcp",
20882
- existingContent,
20883
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
20884
- filePath
20885
- }),
20904
+ fileContent: JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2),
20886
20905
  validate,
20887
20906
  global
20888
20907
  });
@@ -27600,9 +27619,10 @@ function buildDevinPermissionEntry(scope, pattern) {
27600
27619
  * - Project scope: `.devin/config.json`
27601
27620
  * - Global scope: `~/.config/devin/config.json`
27602
27621
  *
27603
- * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
27604
- * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
27605
- * the file is never deleted; only the managed `permissions` key is rewritten.
27622
+ * In global mode the config file is shared with the hooks (`hooks`) feature
27623
+ * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
27624
+ * merge into the existing JSON and the file is never deleted; only the managed
27625
+ * `permissions` key is rewritten.
27606
27626
  *
27607
27627
  * @see https://docs.devin.ai/cli/reference/permissions
27608
27628
  */
@@ -31476,6 +31496,10 @@ function ensurePermission(permission, category) {
31476
31496
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
31477
31497
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
31478
31498
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
31499
+ const EXECUTION_PROFILES_KEY = "execution_profiles";
31500
+ const DEFAULT_PROFILE_KEY = "default";
31501
+ const PROFILE_ALLOWLIST_KEY = "command_allowlist";
31502
+ const PROFILE_DENYLIST_KEY = "command_denylist";
31479
31503
  const WARP_OVERRIDE_KEYS = [
31480
31504
  "agent_mode_coding_permissions",
31481
31505
  "agent_mode_coding_file_read_allowlist",
@@ -31503,11 +31527,21 @@ function warpSettingsDir() {
31503
31527
  /**
31504
31528
  * Permissions adapter for Warp.
31505
31529
  *
31506
- * Warp gates **shell command** execution through two regex arrays under the
31507
- * `[agents.profiles]` table of the global user `settings.toml`:
31508
- * - `agent_mode_command_execution_allowlist` commands that auto-execute.
31509
- * - `agent_mode_command_execution_denylist` — commands that always require
31510
- * permission (the denylist wins over the allowlist).
31530
+ * Warp gates **shell command** execution through two regex arrays in the
31531
+ * global user `settings.toml`. Since file-backed execution profiles went
31532
+ * Stable (2026-07-28) the authoritative surface is the `default` record of the
31533
+ * `[agents.execution_profiles.<id>]` collection:
31534
+ * - `command_allowlist` commands that auto-execute.
31535
+ * - `command_denylist` — commands that always require permission (the denylist
31536
+ * wins over the allowlist).
31537
+ *
31538
+ * The legacy `[agents.profiles]` keys
31539
+ * (`agent_mode_command_execution_allowlist` / `denylist`) are consumed only
31540
+ * once by Warp's one-shot migration and are ignored afterwards. Both surfaces
31541
+ * are written: the legacy block keeps un-migrated installs and old clients
31542
+ * working, and the `default` execution profile (merged in place, only when the
31543
+ * collection already exists) keeps migrated installs enforcing the lists.
31544
+ * Import prefers the execution profile and falls back to the legacy keys.
31511
31545
  *
31512
31546
  * This surface is **global only** — there is no project-scoped Warp permissions
31513
31547
  * file. rulesync's canonical `permission.bash` patterns map directly (`allow` →
@@ -31586,6 +31620,18 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31586
31620
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31587
31621
  else delete profiles[DENYLIST_KEY];
31588
31622
  agents.profiles = profiles;
31623
+ if (isRecord(agents[EXECUTION_PROFILES_KEY])) {
31624
+ const executionProfiles = { ...agents[EXECUTION_PROFILES_KEY] };
31625
+ const defaultProfile = isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? { ...executionProfiles[DEFAULT_PROFILE_KEY] } : {};
31626
+ if (mergedAllow.length > 0) defaultProfile[PROFILE_ALLOWLIST_KEY] = mergedAllow;
31627
+ else delete defaultProfile[PROFILE_ALLOWLIST_KEY];
31628
+ if (mergedDeny.length > 0) defaultProfile[PROFILE_DENYLIST_KEY] = mergedDeny;
31629
+ else delete defaultProfile[PROFILE_DENYLIST_KEY];
31630
+ executionProfiles[DEFAULT_PROFILE_KEY] = defaultProfile;
31631
+ agents[EXECUTION_PROFILES_KEY] = executionProfiles;
31632
+ const otherProfileIds = Object.keys(executionProfiles).filter((id) => id !== DEFAULT_PROFILE_KEY);
31633
+ if (mergedDeny.length > 0 && otherProfileIds.length > 0 && logger) logger.warn(`Warp command deny rules were written to the 'default' execution profile only; they are not enforced while another profile (${otherProfileIds.join(", ")}) is active.`);
31634
+ }
31589
31635
  settings.agents = agents;
31590
31636
  return new WarpPermissions({
31591
31637
  outputRoot,
@@ -31605,9 +31651,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31605
31651
  }
31606
31652
  const agents = isRecord(settings.agents) ? settings.agents : {};
31607
31653
  const profiles = isRecord(agents.profiles) ? agents.profiles : {};
31654
+ const executionProfiles = isRecord(agents[EXECUTION_PROFILES_KEY]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
31655
+ const defaultProfile = executionProfiles && isRecord(executionProfiles[DEFAULT_PROFILE_KEY]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
31608
31656
  const config = convertWarpToRulesyncPermissions({
31609
- allow: isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
31610
- deny: isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
31657
+ allow: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_ALLOWLIST_KEY]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
31658
+ deny: defaultProfile ? isStringArray$1(defaultProfile[PROFILE_DENYLIST_KEY]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
31611
31659
  });
31612
31660
  const warpOverride = {};
31613
31661
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -37357,7 +37405,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37357
37405
  meta: {
37358
37406
  supportsProject: true,
37359
37407
  supportsSimulated: false,
37360
- supportsGlobal: true
37408
+ supportsGlobal: true,
37409
+ lenientImport: true
37361
37410
  }
37362
37411
  }],
37363
37412
  ["aiassistant", {
@@ -37794,7 +37843,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37794
37843
  global: this.global
37795
37844
  });
37796
37845
  } catch (error) {
37797
- if (!isConfiguredRoot) throw error;
37846
+ if (!isConfiguredRoot && !factory.meta.lenientImport) throw error;
37798
37847
  this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
37799
37848
  return null;
37800
37849
  }
@@ -46443,6 +46492,13 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46443
46492
  toolTarget: "qwencode"
46444
46493
  });
46445
46494
  }
46495
+ /**
46496
+ * The personal local context file lives under `.qwen/`, not at the project
46497
+ * root where the settable root path points, so override the deletion glob.
46498
+ */
46499
+ static getLocalRootDeletionGlob({ outputRoot, fileName }) {
46500
+ return (0, node_path.join)(outputRoot, QWENCODE_DIR, fileName);
46501
+ }
46446
46502
  };
46447
46503
  //#endregion
46448
46504
  //#region src/features/rules/reasonix-rule.ts
@@ -47046,14 +47102,18 @@ var WarpRule = class WarpRule extends ToolRule {
47046
47102
  root: root ?? false
47047
47103
  });
47048
47104
  }
47049
- static getSettablePaths(_options = {}) {
47105
+ static getSettablePaths({ global = false } = {}) {
47106
+ if (global) return { root: {
47107
+ relativeDirPath: WARP_GLOBAL_RULE_DIR,
47108
+ relativeFilePath: WARP_RULE_FILE_NAME
47109
+ } };
47050
47110
  return { root: {
47051
47111
  relativeDirPath: ".",
47052
47112
  relativeFilePath: WARP_RULE_FILE_NAME
47053
47113
  } };
47054
47114
  }
47055
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
47056
- const { root } = this.getSettablePaths();
47115
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47116
+ const { root } = this.getSettablePaths({ global });
47057
47117
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
47058
47118
  return new WarpRule({
47059
47119
  outputRoot,
@@ -47064,8 +47124,8 @@ var WarpRule = class WarpRule extends ToolRule {
47064
47124
  root: true
47065
47125
  });
47066
47126
  }
47067
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
47068
- const { root } = this.getSettablePaths();
47127
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
47128
+ const { root } = this.getSettablePaths({ global });
47069
47129
  const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47070
47130
  return new WarpRule({
47071
47131
  outputRoot,
@@ -47085,8 +47145,8 @@ var WarpRule = class WarpRule extends ToolRule {
47085
47145
  error: null
47086
47146
  };
47087
47147
  }
47088
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
47089
- const { root } = this.getSettablePaths();
47148
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
47149
+ const { root } = this.getSettablePaths({ global });
47090
47150
  const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
47091
47151
  return new WarpRule({
47092
47152
  outputRoot,
@@ -47459,6 +47519,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47459
47519
  extension: "md",
47460
47520
  supportsGlobal: true,
47461
47521
  ruleDiscoveryMode: "auto",
47522
+ localRootMode: "separate-local-file",
47523
+ localRootFileName: QWENCODE_LOCAL_RULE_FILE_NAME,
47462
47524
  additionalConventions: { subagents: { subagentClass: QwencodeSubagent } }
47463
47525
  }
47464
47526
  }],
@@ -47523,7 +47585,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47523
47585
  class: WarpRule,
47524
47586
  meta: {
47525
47587
  extension: "md",
47526
- supportsGlobal: false,
47588
+ supportsGlobal: true,
47527
47589
  ruleDiscoveryMode: "toon",
47528
47590
  collisionPolicy: "fold"
47529
47591
  }
@@ -47857,6 +47919,14 @@ var RulesProcessor = class extends FeatureProcessor {
47857
47919
  validate: true,
47858
47920
  root: true
47859
47921
  });
47922
+ if (factory.class === QwencodeRule) return new QwencodeRule({
47923
+ outputRoot: this.outputRoot,
47924
+ relativeDirPath: QWENCODE_DIR,
47925
+ relativeFilePath: fileName,
47926
+ fileContent: body,
47927
+ validate: true,
47928
+ root: true
47929
+ });
47860
47930
  return null;
47861
47931
  }
47862
47932
  /**
@@ -50171,6 +50241,18 @@ Object.defineProperty(exports, "PACKAGING_TOOL_TARGETS", {
50171
50241
  return PACKAGING_TOOL_TARGETS;
50172
50242
  }
50173
50243
  });
50244
+ Object.defineProperty(exports, "QWENCODE_DIR", {
50245
+ enumerable: true,
50246
+ get: function() {
50247
+ return QWENCODE_DIR;
50248
+ }
50249
+ });
50250
+ Object.defineProperty(exports, "QWENCODE_LOCAL_RULE_FILE_NAME", {
50251
+ enumerable: true,
50252
+ get: function() {
50253
+ return QWENCODE_LOCAL_RULE_FILE_NAME;
50254
+ }
50255
+ });
50174
50256
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
50175
50257
  enumerable: true,
50176
50258
  get: function() {
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_import = require("./import-DD-GRmB9.cjs");
2
+ const require_import = require("./import-C9wxpZey.cjs");
3
3
  //#region src/index.ts
4
4
  async function generate(options = {}) {
5
5
  const { silent = true, verbose = false, ...rest } = options;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Et as ALL_TOOL_TARGETS, W as ConfigResolver, X as ConsoleLogger, a as convertFromTool$1, in as ALL_FEATURES, n as checkRulesyncDirExists, r as generate$1, t as importFromTool$1 } from "./import-Bj5HxY9p.js";
1
+ import { K as ConfigResolver, Ot as ALL_TOOL_TARGETS, Q as ConsoleLogger, a as convertFromTool$1, n as checkRulesyncDirExists, on as ALL_FEATURES, r as generate$1, t as importFromTool$1 } from "./import-BwakMyyf.js";
2
2
  //#region src/index.ts
3
3
  async function generate(options = {}) {
4
4
  const { silent = true, verbose = false, ...rest } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "16.3.0",
3
+ "version": "16.4.0",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",
@@ -61,6 +61,7 @@
61
61
  "gray-matter": "4.0.3",
62
62
  "js-yaml": "5.2.1",
63
63
  "jsonc-parser": "3.3.1",
64
+ "minisearch": "7.2.0",
64
65
  "smol-toml": "1.7.0",
65
66
  "sury": "10.0.4",
66
67
  "zod": "4.4.3"
@@ -101,12 +102,13 @@
101
102
  "scripts": {
102
103
  "build": "tsdown",
103
104
  "check": "pnpm run fmt:check && pnpm run oxlint && pnpm run typecheck",
105
+ "check:docs-content": "pnpm run generate:docs-content && git diff --exit-code src/generated/docs-content.ts",
104
106
  "check:gitignore": "pnpm run dev gitignore && git diff --exit-code .gitignore .gitattributes",
105
107
  "check:supported-tools": "tsx scripts/generate-supported-tools-tables.ts && git diff --exit-code README.md docs/reference/supported-tools.md",
106
108
  "check:sync-skill-docs": "tsx scripts/check-skill-docs-sync.ts",
107
109
  "cicheck": "pnpm run cicheck:code && pnpm run cicheck:content",
108
110
  "cicheck:code": "pnpm run check && pnpm run test",
109
- "cicheck:content": "pnpm run check:sync-skill-docs && pnpm run check:gitignore && pnpm run check:supported-tools && pnpm run cspell && pnpm run secretlint",
111
+ "cicheck:content": "pnpm run check:sync-skill-docs && pnpm run check:docs-content && pnpm run check:gitignore && pnpm run check:supported-tools && pnpm run cspell && pnpm run secretlint",
110
112
  "cspell": "cspell --no-progress --gitignore .",
111
113
  "dev": "tsx src/cli/index.ts",
112
114
  "docs:build": "vitepress build docs",
@@ -116,6 +118,7 @@
116
118
  "fmt": "oxfmt .",
117
119
  "fmt:check": "oxfmt --check .",
118
120
  "generate": "pnpm run dev generate",
121
+ "generate:docs-content": "tsx scripts/generate-docs-content.ts",
119
122
  "generate:schema": "tsx scripts/generate-json-schema.ts",
120
123
  "generate:tables": "tsx scripts/generate-supported-tools-tables.ts",
121
124
  "knip": "knip",