rulesync 16.2.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.
@@ -86,6 +86,13 @@ const ALL_FEATURES = [
86
86
  ...ACTIVE_FEATURES_AFTER_IGNORE
87
87
  ];
88
88
  const ALL_FEATURES_WITH_WILDCARD = [...ALL_FEATURES, "*"];
89
+ /**
90
+ * Features that remain accepted for compatibility but are superseded by a
91
+ * newer feature. Maps each deprecated feature to its replacement; consumers
92
+ * (e.g. `rulesync doctor`) derive their deprecation warnings from this map so
93
+ * the set cannot drift from the schema above.
94
+ */
95
+ const DEPRECATED_FEATURE_REPLACEMENTS = { ignore: "permissions" };
89
96
  const ACTIVE_FEATURES = [...ACTIVE_FEATURES_BEFORE_IGNORE, ...ACTIVE_FEATURES_AFTER_IGNORE];
90
97
  const DeprecatedIgnoreFeatureSchema = zod_mini.z.literal("ignore").check((0, zod_mini.meta)({
91
98
  deprecated: true,
@@ -835,6 +842,7 @@ const ErrorCodes = {
835
842
  GITIGNORE_FAILED: "GITIGNORE_FAILED",
836
843
  INIT_FAILED: "INIT_FAILED",
837
844
  MCP_FAILED: "MCP_FAILED",
845
+ DOCTOR_FAILED: "DOCTOR_FAILED",
838
846
  UNKNOWN_ERROR: "UNKNOWN_ERROR"
839
847
  };
840
848
  /**
@@ -843,10 +851,12 @@ const ErrorCodes = {
843
851
  var CLIError = class extends Error {
844
852
  code;
845
853
  exitCode;
846
- constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1) {
854
+ details;
855
+ constructor(message, code = ErrorCodes.UNKNOWN_ERROR, exitCode = 1, details) {
847
856
  super(message);
848
857
  this.code = code;
849
858
  this.exitCode = exitCode;
859
+ this.details = details;
850
860
  this.name = "CLIError";
851
861
  }
852
862
  };
@@ -970,6 +980,7 @@ var JsonLogger = class extends BaseLogger {
970
980
  message: errorMessage
971
981
  };
972
982
  if (this._verbose && message instanceof Error && message.stack) errorInfo.stack = message.stack;
983
+ if (message instanceof CLIError && message.details !== void 0) errorInfo.details = message.details;
973
984
  this.outputJson(false, errorInfo);
974
985
  }
975
986
  debug(_message, ..._args) {}
@@ -1030,6 +1041,10 @@ function hasControlCharacters(value) {
1030
1041
  }
1031
1042
  //#endregion
1032
1043
  //#region src/config/config.ts
1044
+ /**
1045
+ * Key accepted alongside feature names in the per-feature object form of
1046
+ * `targets`. Exported so `rulesync doctor` treats the same key as valid.
1047
+ */
1033
1048
  const GITIGNORE_DESTINATION_KEY = "gitignoreDestination";
1034
1049
  /**
1035
1050
  * Schema for a single source entry in the sources array.
@@ -1096,7 +1111,9 @@ function normalizeConfigFilePath({ configFilePath, inputRoot }) {
1096
1111
  return (0, node_path.isAbsolute)(configFilePath) ? configFilePath : (0, node_path.resolve)(configFilePath);
1097
1112
  }
1098
1113
  /**
1099
- * Conflicting target pairs that cannot be used together
1114
+ * Conflicting target pairs that cannot be used together.
1115
+ * Exported so `rulesync doctor` can report the same conflicts as diagnostics
1116
+ * without duplicating the list.
1100
1117
  */
1101
1118
  const CONFLICTING_TARGET_PAIRS = [["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"]];
1102
1119
  /**
@@ -2364,6 +2381,7 @@ const PI_HOOK_EVENTS = [
2364
2381
  "preToolUse",
2365
2382
  "postToolUse",
2366
2383
  "preModelInvocation",
2384
+ "postModelInvocation",
2367
2385
  "beforeSubmitPrompt",
2368
2386
  "stop",
2369
2387
  "preCompact",
@@ -3070,6 +3088,7 @@ const CANONICAL_TO_PI_EVENT_NAMES = {
3070
3088
  preToolUse: "tool_call",
3071
3089
  postToolUse: "tool_result",
3072
3090
  preModelInvocation: "context",
3091
+ postModelInvocation: "message_end",
3073
3092
  beforeSubmitPrompt: "input",
3074
3093
  stop: "agent_end",
3075
3094
  preCompact: "session_before_compact",
@@ -6835,24 +6854,14 @@ const SHARED_CONFIG_OWNERSHIP = {
6835
6854
  },
6836
6855
  ".devin/config.json": {
6837
6856
  format: "json",
6838
- features: {
6839
- mcp: {
6840
- kind: "replace-owned-keys",
6841
- ownedKeys: ["mcpServers"]
6842
- },
6843
- permissions: {
6844
- kind: "replace-owned-keys",
6845
- ownedKeys: ["permissions"]
6846
- }
6847
- }
6857
+ features: { permissions: {
6858
+ kind: "replace-owned-keys",
6859
+ ownedKeys: ["permissions"]
6860
+ } }
6848
6861
  },
6849
6862
  ".config/devin/config.json": {
6850
6863
  format: "json",
6851
6864
  features: {
6852
- mcp: {
6853
- kind: "replace-owned-keys",
6854
- ownedKeys: ["mcpServers"]
6855
- },
6856
6865
  hooks: {
6857
6866
  kind: "replace-owned-keys",
6858
6867
  ownedKeys: ["hooks"]
@@ -9052,6 +9061,7 @@ const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
9052
9061
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
9053
9062
  const DEVIN_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
9054
9063
  const DEVIN_CONFIG_FILE_NAME = "config.json";
9064
+ const DEVIN_MCP_CONFIG_FILE_NAME = "mcp_config.json";
9055
9065
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
9056
9066
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
9057
9067
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
@@ -11309,6 +11319,7 @@ const QWENCODE_COMMANDS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "commands")
11309
11319
  const QWENCODE_AGENTS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "agents");
11310
11320
  const QWENCODE_SKILLS_DIR_PATH = (0, node_path.join)(QWENCODE_DIR, "skills");
11311
11321
  const QWENCODE_RULE_FILE_NAME = "QWEN.md";
11322
+ const QWENCODE_LOCAL_RULE_FILE_NAME = "QWEN.local.md";
11312
11323
  const QWENCODE_IGNORE_FILE_NAME = ".qwenignore";
11313
11324
  const QWENCODE_SETTINGS_FILE_NAME = "settings.json";
11314
11325
  //#endregion
@@ -12055,6 +12066,7 @@ const WARP_SKILLS_DIR_PATH = (0, node_path.join)(WARP_DIR, "skills");
12055
12066
  const WARP_LINUX_DIR = (0, node_path.join)(".config", "warp-terminal");
12056
12067
  const WARP_WIN32_DIR = (0, node_path.join)("AppData", "Local", "warp", "Warp", "config");
12057
12068
  const WARP_RULE_FILE_NAME = "AGENTS.md";
12069
+ const WARP_GLOBAL_RULE_DIR = ".agents";
12058
12070
  const WARP_MCP_FILE_NAME = ".mcp.json";
12059
12071
  const WARP_PERMISSIONS_FILE_NAME = "settings.toml";
12060
12072
  const WARP_IGNORE_FILE_NAME = ".warpindexingignore";
@@ -14790,9 +14802,9 @@ const DEVIN_CONVERTER_CONFIG = {
14790
14802
  * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
14791
14803
  * level IS the event map (no wrapper key).
14792
14804
  * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
14793
- * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
14794
- * features, so reads and writes merge into the existing JSON and the file is
14795
- * 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.
14796
14808
  *
14797
14809
  * @see https://docs.devin.ai/cli/extensibility/hooks/overview
14798
14810
  */
@@ -16404,6 +16416,13 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
16404
16416
  */
16405
16417
  const PI_TOOL_EVENTS = /* @__PURE__ */ new Set(["tool_call", "tool_result"]);
16406
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
+ /**
16407
16426
  * Validate a hook matcher as a regular expression and return it as a JS
16408
16427
  * string-literal (JSON.stringify quoting) safe to embed in generated code.
16409
16428
  */
@@ -16444,7 +16463,10 @@ function buildSubscriptionLines(handlerGroups) {
16444
16463
  const lines = [];
16445
16464
  for (const [piEvent, handlers] of Object.entries(handlerGroups)) {
16446
16465
  const usesToolName = PI_TOOL_EVENTS.has(piEvent) && handlers.some((h) => h.matcher);
16447
- 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;`);
16448
16470
  for (const handler of handlers) {
16449
16471
  const embeddedCommand = JSON.stringify(handler.command);
16450
16472
  if (usesToolName && handler.matcher) {
@@ -16579,6 +16601,7 @@ function canonicalDefToQwencodeHook(def) {
16579
16601
  const type = def.type ?? "command";
16580
16602
  const isHttp = type === "http";
16581
16603
  const isCommand = type === "command";
16604
+ const isPrompt = type === "prompt";
16582
16605
  return {
16583
16606
  type,
16584
16607
  ...compact({
@@ -16588,6 +16611,8 @@ function canonicalDefToQwencodeHook(def) {
16588
16611
  name: def.name,
16589
16612
  description: def.description,
16590
16613
  statusMessage: def.statusMessage,
16614
+ prompt: isPrompt ? def.prompt : void 0,
16615
+ model: isPrompt ? def.model : void 0,
16591
16616
  async: isCommand ? def.async : void 0,
16592
16617
  env: isCommand ? def.env : void 0,
16593
16618
  shell: isCommand ? def.shell : void 0,
@@ -16606,7 +16631,7 @@ function canonicalDefToQwencodeHook(def) {
16606
16631
  * is needed here — commands are passed through verbatim and any such variable
16607
16632
  * reference stays intact for Qwen Code to expand.
16608
16633
  */
16609
- function canonicalToQwencodeHooks(config) {
16634
+ function canonicalToQwencodeHooks(config, logger) {
16610
16635
  const qwencodeSupported = new Set(QWENCODE_HOOK_EVENTS);
16611
16636
  const sharedHooks = {};
16612
16637
  for (const [event, defs] of Object.entries(config.hooks)) if (qwencodeSupported.has(event)) sharedHooks[event] = defs;
@@ -16626,6 +16651,7 @@ function canonicalToQwencodeHooks(config) {
16626
16651
  const byMatcher = /* @__PURE__ */ new Map();
16627
16652
  for (const def of definitions) {
16628
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.`);
16629
16655
  const key = def.matcher ?? "";
16630
16656
  const list = byMatcher.get(key);
16631
16657
  if (list) list.push(def);
@@ -16664,7 +16690,9 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
16664
16690
  shell: zod_mini.z.optional(zod_mini.z.string()),
16665
16691
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16666
16692
  allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16667
- 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())
16668
16696
  });
16669
16697
  /**
16670
16698
  * A matcher group entry in a Qwen Code event array.
@@ -16679,37 +16707,43 @@ const QwencodeMatcherEntrySchema = zod_mini.z.looseObject({
16679
16707
  /**
16680
16708
  * Convert a single parsed Qwen Code matcher group into canonical hook definitions.
16681
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
+ }
16682
16739
  function qwencodeMatcherEntryToCanonical(entry) {
16683
- const defs = [];
16684
- const hooks = entry.hooks ?? [];
16685
16740
  const sequential = entry.sequential === true;
16686
16741
  const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
16687
- for (const h of hooks) {
16688
- const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" || h.type === "function" ? h.type : "command";
16689
- const isHttp = hookType === "http";
16690
- const isCommand = hookType === "command";
16691
- const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
16692
- defs.push({
16693
- type: hookType,
16694
- ...compact({
16695
- command: h.command,
16696
- url: h.url,
16697
- timeout: h.timeout,
16698
- name: h.name,
16699
- description: h.description,
16700
- statusMessage: h.statusMessage,
16701
- async: isCommand ? h.async : void 0,
16702
- env: isCommand ? h.env : void 0,
16703
- shell: isCommand ? shell : void 0,
16704
- headers: isHttp ? h.headers : void 0,
16705
- allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
16706
- once: isHttp ? h.once : void 0,
16707
- sequential: sequential ? true : void 0,
16708
- matcher
16709
- })
16710
- });
16711
- }
16712
- return defs;
16742
+ return (entry.hooks ?? []).map((hook) => qwencodeHookEntryToCanonical({
16743
+ hook,
16744
+ sequential,
16745
+ matcher
16746
+ }));
16713
16747
  }
16714
16748
  /**
16715
16749
  * Extract hooks from Qwen Code settings.json into canonical format.
@@ -16757,12 +16791,12 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
16757
16791
  validate
16758
16792
  });
16759
16793
  }
16760
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
16794
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
16761
16795
  const paths = QwencodeHooks.getSettablePaths({ global });
16762
16796
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16763
16797
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
16764
16798
  const config = rulesyncHooks.getJson();
16765
- const patch = { hooks: canonicalToQwencodeHooks(config) };
16799
+ const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
16766
16800
  const disableAllHooks = config.qwencode?.disableAllHooks;
16767
16801
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
16768
16802
  const fileContent = applySharedConfigPatch({
@@ -20788,18 +20822,24 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
20788
20822
  /**
20789
20823
  * MCP generator for Devin Local (native `.devin/` configuration).
20790
20824
  *
20791
- * Devin reads MCP servers from the `mcpServers` key of its native config file:
20792
- * - Project scope: `.devin/config.json`
20793
- * - 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`
20830
+ *
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.
20794
20837
  *
20795
- * The config file is shared with the permissions feature (`permissions` key)
20796
- * and, in global mode, the hooks feature (`hooks` key), so reads and writes
20797
- * merge into the existing JSON rather than overwriting it, and the file is
20798
- * never deleted. Each server is a stdio entry ({ command, args, env }) or a
20799
- * remote entry ({ serverUrl | url, headers }), and may carry an optional
20800
- * `disabledTools` array.
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.
20801
20841
  *
20802
- * @see https://docs.devin.ai/cli/extensibility/configuration
20842
+ * @see https://docs.devin.ai/cli/extensibility/mcp/configuration
20803
20843
  */
20804
20844
  var DevinMcp = class DevinMcp extends ToolMcp {
20805
20845
  json;
@@ -20817,31 +20857,27 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20817
20857
  throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
20818
20858
  }
20819
20859
  }
20820
- /**
20821
- * config.json may carry the permissions/hooks features' keys, so it is never
20822
- * deleted; only the managed `mcpServers` key is rewritten.
20823
- */
20824
- isDeletable() {
20825
- return false;
20826
- }
20827
20860
  static getSettablePaths({ global = false } = {}) {
20828
20861
  if (global) return {
20829
20862
  relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
20830
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
20863
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20831
20864
  };
20832
20865
  return {
20833
20866
  relativeDirPath: DEVIN_DIR,
20834
- relativeFilePath: DEVIN_CONFIG_FILE_NAME
20867
+ relativeFilePath: DEVIN_MCP_CONFIG_FILE_NAME
20835
20868
  };
20836
20869
  }
20837
20870
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20838
20871
  const paths = this.getSettablePaths({ global });
20839
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"mcpServers\":{}}";
20840
- const json = this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath);
20841
- const newJson = {
20842
- ...json,
20843
- mcpServers: json.mcpServers ?? {}
20844
- };
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 ?? {} };
20845
20881
  return new DevinMcp({
20846
20882
  outputRoot,
20847
20883
  relativeDirPath: paths.relativeDirPath,
@@ -20851,21 +20887,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
20851
20887
  global
20852
20888
  });
20853
20889
  }
20854
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
20890
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
20855
20891
  const paths = this.getSettablePaths({ global });
20856
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20857
- 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
+ }
20858
20900
  return new DevinMcp({
20859
20901
  outputRoot,
20860
20902
  relativeDirPath: paths.relativeDirPath,
20861
20903
  relativeFilePath: paths.relativeFilePath,
20862
- fileContent: applySharedConfigPatch({
20863
- fileKey: sharedConfigFileKey(paths),
20864
- feature: "mcp",
20865
- existingContent,
20866
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
20867
- filePath
20868
- }),
20904
+ fileContent: JSON.stringify({ mcpServers: rulesyncMcp.getMcpServers() }, null, 2),
20869
20905
  validate,
20870
20906
  global
20871
20907
  });
@@ -27583,9 +27619,10 @@ function buildDevinPermissionEntry(scope, pattern) {
27583
27619
  * - Project scope: `.devin/config.json`
27584
27620
  * - Global scope: `~/.config/devin/config.json`
27585
27621
  *
27586
- * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
27587
- * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
27588
- * 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.
27589
27626
  *
27590
27627
  * @see https://docs.devin.ai/cli/reference/permissions
27591
27628
  */
@@ -31459,6 +31496,10 @@ function ensurePermission(permission, category) {
31459
31496
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
31460
31497
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
31461
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";
31462
31503
  const WARP_OVERRIDE_KEYS = [
31463
31504
  "agent_mode_coding_permissions",
31464
31505
  "agent_mode_coding_file_read_allowlist",
@@ -31486,11 +31527,21 @@ function warpSettingsDir() {
31486
31527
  /**
31487
31528
  * Permissions adapter for Warp.
31488
31529
  *
31489
- * Warp gates **shell command** execution through two regex arrays under the
31490
- * `[agents.profiles]` table of the global user `settings.toml`:
31491
- * - `agent_mode_command_execution_allowlist` commands that auto-execute.
31492
- * - `agent_mode_command_execution_denylist` — commands that always require
31493
- * 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.
31494
31545
  *
31495
31546
  * This surface is **global only** — there is no project-scoped Warp permissions
31496
31547
  * file. rulesync's canonical `permission.bash` patterns map directly (`allow` →
@@ -31569,6 +31620,18 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31569
31620
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
31570
31621
  else delete profiles[DENYLIST_KEY];
31571
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
+ }
31572
31635
  settings.agents = agents;
31573
31636
  return new WarpPermissions({
31574
31637
  outputRoot,
@@ -31588,9 +31651,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
31588
31651
  }
31589
31652
  const agents = isRecord(settings.agents) ? settings.agents : {};
31590
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;
31591
31656
  const config = convertWarpToRulesyncPermissions({
31592
- allow: isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
31593
- 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] : []
31594
31659
  });
31595
31660
  const warpOverride = {};
31596
31661
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -37340,7 +37405,8 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
37340
37405
  meta: {
37341
37406
  supportsProject: true,
37342
37407
  supportsSimulated: false,
37343
- supportsGlobal: true
37408
+ supportsGlobal: true,
37409
+ lenientImport: true
37344
37410
  }
37345
37411
  }],
37346
37412
  ["aiassistant", {
@@ -37777,7 +37843,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
37777
37843
  global: this.global
37778
37844
  });
37779
37845
  } catch (error) {
37780
- if (!isConfiguredRoot) throw error;
37846
+ if (!isConfiguredRoot && !factory.meta.lenientImport) throw error;
37781
37847
  this.logger.warn(`Skipping ${(0, node_path.join)(relativeDirPath, dirName)}: ${formatError(error)}`);
37782
37848
  return null;
37783
37849
  }
@@ -46426,6 +46492,13 @@ var QwencodeRule = class QwencodeRule extends ToolRule {
46426
46492
  toolTarget: "qwencode"
46427
46493
  });
46428
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
+ }
46429
46502
  };
46430
46503
  //#endregion
46431
46504
  //#region src/features/rules/reasonix-rule.ts
@@ -47029,14 +47102,18 @@ var WarpRule = class WarpRule extends ToolRule {
47029
47102
  root: root ?? false
47030
47103
  });
47031
47104
  }
47032
- 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
+ } };
47033
47110
  return { root: {
47034
47111
  relativeDirPath: ".",
47035
47112
  relativeFilePath: WARP_RULE_FILE_NAME
47036
47113
  } };
47037
47114
  }
47038
- static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true }) {
47039
- const { root } = this.getSettablePaths();
47115
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, validate = true, global = false }) {
47116
+ const { root } = this.getSettablePaths({ global });
47040
47117
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, (0, node_path.join)(root.relativeDirPath, root.relativeFilePath)));
47041
47118
  return new WarpRule({
47042
47119
  outputRoot,
@@ -47047,8 +47124,8 @@ var WarpRule = class WarpRule extends ToolRule {
47047
47124
  root: true
47048
47125
  });
47049
47126
  }
47050
- static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
47051
- const { root } = this.getSettablePaths();
47127
+ static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
47128
+ const { root } = this.getSettablePaths({ global });
47052
47129
  const isRoot = rulesyncRule.getFrontmatter().root ?? false;
47053
47130
  return new WarpRule({
47054
47131
  outputRoot,
@@ -47068,8 +47145,8 @@ var WarpRule = class WarpRule extends ToolRule {
47068
47145
  error: null
47069
47146
  };
47070
47147
  }
47071
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
47072
- const { root } = this.getSettablePaths();
47148
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
47149
+ const { root } = this.getSettablePaths({ global });
47073
47150
  const isRoot = relativeFilePath === root.relativeFilePath && relativeDirPath === root.relativeDirPath;
47074
47151
  return new WarpRule({
47075
47152
  outputRoot,
@@ -47442,6 +47519,8 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47442
47519
  extension: "md",
47443
47520
  supportsGlobal: true,
47444
47521
  ruleDiscoveryMode: "auto",
47522
+ localRootMode: "separate-local-file",
47523
+ localRootFileName: QWENCODE_LOCAL_RULE_FILE_NAME,
47445
47524
  additionalConventions: { subagents: { subagentClass: QwencodeSubagent } }
47446
47525
  }
47447
47526
  }],
@@ -47506,7 +47585,7 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
47506
47585
  class: WarpRule,
47507
47586
  meta: {
47508
47587
  extension: "md",
47509
- supportsGlobal: false,
47588
+ supportsGlobal: true,
47510
47589
  ruleDiscoveryMode: "toon",
47511
47590
  collisionPolicy: "fold"
47512
47591
  }
@@ -47840,6 +47919,14 @@ var RulesProcessor = class extends FeatureProcessor {
47840
47919
  validate: true,
47841
47920
  root: true
47842
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
+ });
47843
47930
  return null;
47844
47931
  }
47845
47932
  /**
@@ -50064,6 +50151,12 @@ Object.defineProperty(exports, "CODEXCLI_DIR", {
50064
50151
  return CODEXCLI_DIR;
50065
50152
  }
50066
50153
  });
50154
+ Object.defineProperty(exports, "CONFLICTING_TARGET_PAIRS", {
50155
+ enumerable: true,
50156
+ get: function() {
50157
+ return CONFLICTING_TARGET_PAIRS;
50158
+ }
50159
+ });
50067
50160
  Object.defineProperty(exports, "ChecksProcessor", {
50068
50161
  enumerable: true,
50069
50162
  get: function() {
@@ -50094,12 +50187,24 @@ Object.defineProperty(exports, "ConsoleLogger", {
50094
50187
  return ConsoleLogger;
50095
50188
  }
50096
50189
  });
50190
+ Object.defineProperty(exports, "DEPRECATED_FEATURE_REPLACEMENTS", {
50191
+ enumerable: true,
50192
+ get: function() {
50193
+ return DEPRECATED_FEATURE_REPLACEMENTS;
50194
+ }
50195
+ });
50097
50196
  Object.defineProperty(exports, "ErrorCodes", {
50098
50197
  enumerable: true,
50099
50198
  get: function() {
50100
50199
  return ErrorCodes;
50101
50200
  }
50102
50201
  });
50202
+ Object.defineProperty(exports, "GITIGNORE_DESTINATION_KEY", {
50203
+ enumerable: true,
50204
+ get: function() {
50205
+ return GITIGNORE_DESTINATION_KEY;
50206
+ }
50207
+ });
50103
50208
  Object.defineProperty(exports, "HooksProcessor", {
50104
50209
  enumerable: true,
50105
50210
  get: function() {
@@ -50136,6 +50241,18 @@ Object.defineProperty(exports, "PACKAGING_TOOL_TARGETS", {
50136
50241
  return PACKAGING_TOOL_TARGETS;
50137
50242
  }
50138
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
+ });
50139
50256
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
50140
50257
  enumerable: true,
50141
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-DNfxS6QZ.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;