rulesync 36.0.2 → 37.1.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.
@@ -4040,7 +4040,7 @@ function isPlainObject$1(value) {
4040
4040
  /**
4041
4041
  * Type guard to check if a value is an array of strings.
4042
4042
  */
4043
- function isStringArray$2(value) {
4043
+ function isStringArray$1(value) {
4044
4044
  return Array.isArray(value) && value.every((item) => typeof item === "string");
4045
4045
  }
4046
4046
  //#endregion
@@ -7895,14 +7895,53 @@ const COPILOT_DEFAULT_PERMISSION_MODES = [
7895
7895
  "autoApprove",
7896
7896
  "autopilot"
7897
7897
  ];
7898
+ /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
7899
+ const CopilotSandboxEnabledSchema = z.enum(["off", "on"]);
7900
+ /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
7901
+ const CopilotSandboxUserConfiguredPathsSchema = z.strictObject({
7902
+ readwritePaths: z.optional(z.array(z.string())),
7903
+ readonlyPaths: z.optional(z.array(z.string())),
7904
+ deniedPaths: z.optional(z.array(z.string()))
7905
+ });
7906
+ /**
7907
+ * The Agent Host sandbox settings VS Code 1.141 settled on, as a nested object
7908
+ * whose path mirrors the dotted setting key under `chat.agent.sandbox.` — e.g.
7909
+ * `network.allowNetwork` writes `chat.agent.sandbox.network.allowNetwork`.
7910
+ * Only the documented, registered keys are accepted; the deprecated 1.138
7911
+ * shapes (`chat.agent.sandbox.allowNetwork`, `.fileSystem.linux|mac|windows`)
7912
+ * are not modeled, since the Agent Host ignores them.
7913
+ *
7914
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
7915
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
7916
+ */
7917
+ const CopilotSandboxSchema = z.strictObject({
7918
+ enabled: z.optional(CopilotSandboxEnabledSchema),
7919
+ network: z.optional(z.strictObject({
7920
+ allowNetwork: z.optional(z.boolean()),
7921
+ allowLocalNetwork: z.optional(z.boolean())
7922
+ })),
7923
+ allowUnsandboxedCommands: z.optional(z.boolean()),
7924
+ mcpServers: z.optional(z.boolean()),
7925
+ lspServers: z.optional(z.boolean()),
7926
+ credentials: z.optional(z.strictObject({
7927
+ authenticategit: z.optional(z.boolean()),
7928
+ authenticategh: z.optional(z.boolean())
7929
+ })),
7930
+ fileSystem: z.optional(z.strictObject({
7931
+ allowDevToolAccess: z.optional(z.boolean()),
7932
+ userConfiguredPaths: z.optional(CopilotSandboxUserConfiguredPathsSchema)
7933
+ }))
7934
+ });
7898
7935
  /**
7899
7936
  * Tool-scoped override block for GitHub Copilot in VS Code.
7900
7937
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
7901
- * setting, which has no canonical category.
7938
+ * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
7939
+ * has a canonical category.
7902
7940
  */
7903
7941
  const CopilotPermissionsOverrideSchema = z.looseObject({
7904
7942
  permission: z.optional(ToolScopedPermissionSchema),
7905
- defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7943
+ defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES)),
7944
+ sandbox: z.optional(CopilotSandboxSchema)
7906
7945
  });
7907
7946
  /**
7908
7947
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
@@ -12835,6 +12874,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12835
12874
  kind: "replace-owned-keys",
12836
12875
  ownedKeys: ["disableAllHooks"]
12837
12876
  },
12877
+ mcp: {
12878
+ kind: "replace-owned-keys",
12879
+ ownedKeys: ["disabledMcpServers"]
12880
+ },
12838
12881
  permissions: {
12839
12882
  kind: "replace-owned-keys",
12840
12883
  ownedKeys: ["deniedUrls"]
@@ -12849,6 +12892,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12849
12892
  kind: "replace-owned-keys",
12850
12893
  ownedKeys: ["disableAllHooks"]
12851
12894
  },
12895
+ mcp: {
12896
+ kind: "replace-owned-keys",
12897
+ ownedKeys: ["disabledMcpServers"]
12898
+ },
12852
12899
  permissions: {
12853
12900
  kind: "replace-owned-keys",
12854
12901
  ownedKeys: ["allowedUrls", "deniedUrls"]
@@ -12866,6 +12913,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12866
12913
  "chat.tools.edits.autoApprove",
12867
12914
  "chat.tools.urls.autoApprove",
12868
12915
  "chat.permissions.default",
12916
+ "chat.agent.sandbox.enabled",
12917
+ "chat.agent.sandbox.network.allowNetwork",
12918
+ "chat.agent.sandbox.network.allowLocalNetwork",
12919
+ "chat.agent.sandbox.allowUnsandboxedCommands",
12920
+ "chat.agent.sandbox.mcpServers",
12921
+ "chat.agent.sandbox.lspServers",
12922
+ "chat.agent.sandbox.credentials.authenticategit",
12923
+ "chat.agent.sandbox.credentials.authenticategh",
12924
+ "chat.agent.sandbox.fileSystem.allowDevToolAccess",
12925
+ "chat.agent.sandbox.fileSystem.userConfiguredPaths",
12869
12926
  "zoo-code.allowedCommands",
12870
12927
  "zoo-code.deniedCommands",
12871
12928
  "roo-cline.allowedCommands",
@@ -22113,7 +22170,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
22113
22170
  */
22114
22171
  function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
22115
22172
  if (Array.isArray(config.skills)) {
22116
- if (!layout.readsV2ConfigSpellings || !isStringArray$2(config.skills)) return [];
22173
+ if (!layout.readsV2ConfigSpellings || !isStringArray$1(config.skills)) return [];
22117
22174
  return config.skills.filter((value) => !/^https?:\/\//i.test(value));
22118
22175
  }
22119
22176
  const skills = asOpencodeEntries(config.skills);
@@ -26359,7 +26416,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
26359
26416
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
26360
26417
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
26361
26418
  const isEmittableString = ({ value }) => isNonEmptyString(value);
26362
- const isEmittableArray = ({ value }) => isStringArray$1(value);
26419
+ const isEmittableArray = ({ value }) => isStringArray(value);
26363
26420
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
26364
26421
  const isImportableArray = ({ value }) => isSafeStringArray(value);
26365
26422
  const isEmittableObject = ({ value }) => isPlainObject$1(value);
@@ -26801,7 +26858,7 @@ function isStringRecord(value) {
26801
26858
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
26802
26859
  return Object.values(value).every((v) => typeof v === "string");
26803
26860
  }
26804
- function isStringArray$1(value) {
26861
+ function isStringArray(value) {
26805
26862
  return Array.isArray(value) && value.every((v) => typeof v === "string");
26806
26863
  }
26807
26864
  /** Compare object values without letting key order decide the answer. */
@@ -26846,7 +26903,7 @@ function isSafeEnvEntry(entry) {
26846
26903
  * skipped wholesale when that read fails.
26847
26904
  */
26848
26905
  function isSafeStringArray(value) {
26849
- return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26906
+ return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26850
26907
  }
26851
26908
  /**
26852
26909
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -26888,7 +26945,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
26888
26945
  return {
26889
26946
  ...url !== void 0 && { url },
26890
26947
  ...isStringRecord(headers) && { headers },
26891
- ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26948
+ ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26892
26949
  };
26893
26950
  }
26894
26951
  if (hookType === "mcp_tool") {
@@ -28028,8 +28085,34 @@ function collectOwnedRefs({ generatedHooks, shape }) {
28028
28085
  }
28029
28086
  return owned;
28030
28087
  }
28088
+ /**
28089
+ * The merge keeps every unowned existing handler where it is. Some tools key
28090
+ * state by position — Codex CLI persists a hook's trust review under its event,
28091
+ * matcher group index and handler index — so moving a handler the user already
28092
+ * reviewed would silently change which hook that review belongs to. Hence:
28093
+ * - an unowned existing handler stays at its index;
28094
+ * - a newly generated handler is appended to the first existing group with its
28095
+ * matcher and group-level keys, or in a new group after the existing ones;
28096
+ * - within a group, the slots rulesync's own handlers occupy (including the
28097
+ * appended ones) are filled in generated order, so reordering the sources
28098
+ * still reorders the generated handlers, which matters wherever handlers
28099
+ * run in order (a Qwen Code `sequential` group, a Cursor list).
28100
+ *
28101
+ * A retracted handler, or a group left empty by retractions, is still removed,
28102
+ * which shifts whatever follows it in the same list; that is unavoidable
28103
+ * without leaving a placeholder behind.
28104
+ */
28031
28105
  function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
28032
- const merged = [...generated];
28106
+ const pending = createPendingGenerated();
28107
+ for (const group of generated) if (isMatcherGroup(group)) addPending({
28108
+ pending,
28109
+ handlers: group.hooks,
28110
+ event,
28111
+ matcher: matcherKey(group.matcher),
28112
+ attributes: groupAttributesKey(group)
28113
+ });
28114
+ const merged = [];
28115
+ const homes = /* @__PURE__ */ new Map();
28033
28116
  for (const group of existing) {
28034
28117
  if (!isMatcherGroup(group)) {
28035
28118
  warnSkip({
@@ -28040,26 +28123,57 @@ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned,
28040
28123
  continue;
28041
28124
  }
28042
28125
  const matcher = matcherKey(group.matcher);
28043
- const preserved = group.hooks.filter((handler) => shouldPreserve({
28044
- handler,
28126
+ const hooks = keepExistingHandlers({
28127
+ handlers: group.hooks,
28045
28128
  event,
28046
28129
  matcher,
28130
+ attributes: groupAttributesKey(group),
28131
+ pending,
28047
28132
  seen,
28048
28133
  previouslyOwned,
28049
28134
  logger
28050
- })).map((handler) => cloneHandler(handler));
28051
- if (preserved.length === 0) continue;
28052
- const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher);
28053
- if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
28054
- else merged.push({
28135
+ });
28136
+ if (hooks.length === 0) continue;
28137
+ const kept = {
28055
28138
  ...omitPrototypePollutionKeys(group),
28056
- hooks: preserved
28139
+ hooks
28140
+ };
28141
+ merged.push(kept);
28142
+ const home = groupSlotKey(group);
28143
+ if (!homes.has(home)) homes.set(home, kept);
28144
+ }
28145
+ for (const group of generated) {
28146
+ if (!isMatcherGroup(group)) {
28147
+ merged.push(group);
28148
+ continue;
28149
+ }
28150
+ const remaining = group.hooks.filter((handler) => !pending.placed.has(handler));
28151
+ if (remaining.length === 0) continue;
28152
+ const home = homes.get(groupSlotKey(group));
28153
+ if (home === void 0) merged.push({
28154
+ ...group,
28155
+ hooks: remaining
28057
28156
  });
28157
+ else home.hooks.push(...remaining);
28058
28158
  }
28159
+ const order = generatedOrder(generated.flatMap((group) => isMatcherGroup(group) ? group.hooks : []));
28160
+ for (const group of merged) if (isMatcherGroup(group)) orderGeneratedSlots({
28161
+ handlers: group.hooks,
28162
+ order
28163
+ });
28059
28164
  return merged;
28060
28165
  }
28166
+ /** The flat-list counterpart of {@link mergeMatcherGroups}, with the same positional rules. */
28061
28167
  function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
28062
- const preserved = [];
28168
+ const pending = createPendingGenerated();
28169
+ addPending({
28170
+ pending,
28171
+ handlers: generated,
28172
+ event,
28173
+ matcher: void 0,
28174
+ attributes: ""
28175
+ });
28176
+ const flat = [];
28063
28177
  for (const handler of existing) {
28064
28178
  if (isMatcherGroup(handler)) {
28065
28179
  warnSkip({
@@ -28069,17 +28183,93 @@ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned,
28069
28183
  });
28070
28184
  continue;
28071
28185
  }
28072
- if (!shouldPreserve({
28186
+ flat.push(handler);
28187
+ }
28188
+ const merged = keepExistingHandlers({
28189
+ handlers: flat,
28190
+ event,
28191
+ matcher: void 0,
28192
+ attributes: "",
28193
+ pending,
28194
+ seen,
28195
+ previouslyOwned,
28196
+ logger
28197
+ });
28198
+ merged.push(...generated.filter((handler) => !pending.placed.has(handler)));
28199
+ orderGeneratedSlots({
28200
+ handlers: merged,
28201
+ order: generatedOrder(generated)
28202
+ });
28203
+ return merged;
28204
+ }
28205
+ /** Each generated handler's position in the generated list, by object identity. */
28206
+ function generatedOrder(handlers) {
28207
+ const order = /* @__PURE__ */ new Map();
28208
+ for (const [index, handler] of handlers.entries()) if (isPlainObject$1(handler)) order.set(handler, index);
28209
+ return order;
28210
+ }
28211
+ /**
28212
+ * Rewrite, in place, the slots of `handlers` that hold generated handlers so
28213
+ * they follow the generated order. Every other slot — an unowned handler — is
28214
+ * left at its index.
28215
+ */
28216
+ function orderGeneratedSlots({ handlers, order }) {
28217
+ const slots = [];
28218
+ for (const [index, handler] of handlers.entries()) if (order.has(handler)) slots.push(index);
28219
+ const sorted = slots.map((index) => handlers[index]).toSorted((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
28220
+ for (const [position, index] of slots.entries()) handlers[index] = sorted[position];
28221
+ }
28222
+ function createPendingGenerated() {
28223
+ return {
28224
+ byKey: /* @__PURE__ */ new Map(),
28225
+ placed: /* @__PURE__ */ new Set()
28226
+ };
28227
+ }
28228
+ function addPending({ pending, handlers, event, matcher, attributes }) {
28229
+ for (const handler of handlers) {
28230
+ if (!isPlainObject$1(handler)) continue;
28231
+ const key = pendingKey({
28232
+ event,
28233
+ matcher,
28234
+ attributes,
28235
+ handler
28236
+ });
28237
+ const queue = pending.byKey.get(key);
28238
+ if (queue === void 0) pending.byKey.set(key, [handler]);
28239
+ else queue.push(handler);
28240
+ }
28241
+ }
28242
+ /**
28243
+ * Walk one existing list in order and decide each handler: replaced in place
28244
+ * by its generated copy, kept as unowned, or dropped (retracted or duplicate).
28245
+ */
28246
+ function keepExistingHandlers({ handlers, event, matcher, attributes, pending, seen, previouslyOwned, logger }) {
28247
+ const kept = [];
28248
+ for (const handler of handlers) {
28249
+ if (isPlainObject$1(handler)) {
28250
+ const key = pendingKey({
28251
+ event,
28252
+ matcher,
28253
+ attributes,
28254
+ handler
28255
+ });
28256
+ const generated = pending.byKey.get(key)?.shift();
28257
+ if (generated !== void 0) {
28258
+ pending.placed.add(generated);
28259
+ kept.push(generated);
28260
+ continue;
28261
+ }
28262
+ }
28263
+ if (shouldPreserve({
28073
28264
  handler,
28074
28265
  event,
28075
- matcher: void 0,
28266
+ matcher,
28076
28267
  seen,
28077
28268
  previouslyOwned,
28078
28269
  logger
28079
- })) continue;
28080
- preserved.push(cloneHandler(handler));
28270
+ })) kept.push(cloneHandler(handler));
28081
28271
  }
28082
- return [...generated, ...preserved];
28272
+ return kept;
28083
28273
  }
28084
28274
  function isMatcherGroup(value) {
28085
28275
  return isPlainObject$1(value) && Array.isArray(value.hooks);
@@ -28170,6 +28360,36 @@ function stableStringify$1(value) {
28170
28360
  function matcherKey(matcher) {
28171
28361
  return matcher === void 0 ? "" : stableStringify$1(matcher);
28172
28362
  }
28363
+ function pendingKey({ event, matcher, attributes, handler }) {
28364
+ const key = ownedHookKey({
28365
+ event,
28366
+ matcher,
28367
+ identity: handlerIdentity(handler)
28368
+ });
28369
+ return JSON.stringify([key, attributes]);
28370
+ }
28371
+ /** Groups that share a matcher and attributes are interchangeable homes for a handler. */
28372
+ function groupSlotKey(group) {
28373
+ return JSON.stringify([matcherKey(group.matcher), groupAttributesKey(group)]);
28374
+ }
28375
+ /** Group-level keys whose default value is known, so an explicit default can be ignored. */
28376
+ const GROUP_ATTRIBUTE_DEFAULTS = Object.freeze({
28377
+ __proto__: null,
28378
+ sequential: false
28379
+ });
28380
+ /**
28381
+ * The stable form of a matcher group's keys other than `matcher` and `hooks`.
28382
+ *
28383
+ * A key set to its known default is the same group as one that omits it:
28384
+ * Qwen Code's `sequential` defaults to `false` and rulesync emits it only when
28385
+ * `true`, so an explicit `sequential: false` (or any `undefined` key) is
28386
+ * dropped before comparing. Other `false` values are kept, since their default
28387
+ * is not known here.
28388
+ */
28389
+ function groupAttributesKey(group) {
28390
+ const { matcher: _matcher, hooks: _hooks, ...attributes } = group;
28391
+ return stableStringify$1(Object.fromEntries(Object.entries(attributes).filter(([key, value]) => value !== void 0 && GROUP_ATTRIBUTE_DEFAULTS[key] !== value)));
28392
+ }
28173
28393
  function warnSkip({ logger, event, expected }) {
28174
28394
  logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
28175
28395
  }
@@ -28665,7 +28885,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
28665
28885
  function parseManifest(fileContent) {
28666
28886
  try {
28667
28887
  const parsed = JSON.parse(fileContent);
28668
- if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
28888
+ if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
28669
28889
  return {
28670
28890
  generatedBy: "rulesync",
28671
28891
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -30203,7 +30423,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
30203
30423
  * see the CLI config dir reference:
30204
30424
  * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30205
30425
  */
30206
- function copilotCliSettingsPaths({ global }) {
30426
+ function copilotCliSettingsPaths$1({ global }) {
30207
30427
  return global ? {
30208
30428
  relativeDirPath: COPILOT_DIR,
30209
30429
  relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
@@ -30218,7 +30438,7 @@ function copilotCliSettingsPaths({ global }) {
30218
30438
  * subject, and the permissions feature reports a broken settings file itself.
30219
30439
  */
30220
30440
  async function readDisableAllHooks({ outputRoot, global }) {
30221
- const paths = copilotCliSettingsPaths({ global });
30441
+ const paths = copilotCliSettingsPaths$1({ global });
30222
30442
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
30223
30443
  if (fileContent === null) return;
30224
30444
  try {
@@ -30248,7 +30468,7 @@ var CopilotcliHooksSettings = class CopilotcliHooksSettings extends ToolFile {
30248
30468
  return false;
30249
30469
  }
30250
30470
  static async fromDisableAllHooks({ outputRoot = process.cwd(), global = false, disableAllHooks }) {
30251
- const paths = copilotCliSettingsPaths({ global });
30471
+ const paths = copilotCliSettingsPaths$1({ global });
30252
30472
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30253
30473
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30254
30474
  return new CopilotcliHooksSettings({
@@ -30278,7 +30498,7 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
30278
30498
  this.disableAllHooks = disableAllHooks;
30279
30499
  }
30280
30500
  static getExtraSharedWritePaths({ global = false } = {}) {
30281
- return [copilotCliSettingsPaths({ global })];
30501
+ return [copilotCliSettingsPaths$1({ global })];
30282
30502
  }
30283
30503
  static async getAuxiliaryFiles({ toolHooks } = {}) {
30284
30504
  return toolHooks instanceof CopilotcliHooks && toolHooks.settingsFile ? [toolHooks.settingsFile] : [];
@@ -34722,12 +34942,21 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34722
34942
  validate
34723
34943
  });
34724
34944
  }
34725
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
34945
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
34726
34946
  const paths = QwencodeHooks.getSettablePaths({ global });
34727
34947
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
34728
34948
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
34729
34949
  const config = rulesyncHooks.getJson();
34730
- const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
34950
+ const previouslyOwned = preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0;
34951
+ const merged = mergeGeneratedHookLists({
34952
+ existingContent,
34953
+ generatedHooks: canonicalToQwencodeHooks(config, logger),
34954
+ shape: "matcher-groups",
34955
+ preserveUnowned,
34956
+ previouslyOwned,
34957
+ logger
34958
+ });
34959
+ const patch = { hooks: merged.hooks };
34731
34960
  const disableAllHooks = config.qwencode?.disableAllHooks;
34732
34961
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
34733
34962
  const fileContent = applySharedConfigPatch({
@@ -34742,9 +34971,16 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34742
34971
  relativeDirPath: paths.relativeDirPath,
34743
34972
  relativeFilePath: paths.relativeFilePath,
34744
34973
  fileContent,
34974
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
34745
34975
  validate
34746
34976
  });
34747
34977
  }
34978
+ static supportsPreserveUnowned() {
34979
+ return true;
34980
+ }
34981
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
34982
+ return toolHooks instanceof QwencodeHooks ? toolHooks.getOwnershipLockFiles() : [];
34983
+ }
34748
34984
  toRulesyncHooks() {
34749
34985
  let settings;
34750
34986
  try {
@@ -41144,7 +41380,7 @@ function isCodexStdioServer(config) {
41144
41380
  * @see https://learn.chatgpt.com/docs/extend/mcp
41145
41381
  */
41146
41382
  function isValidRenamedArray(key, value) {
41147
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
41383
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
41148
41384
  }
41149
41385
  /**
41150
41386
  * The keys of Codex's `McpServerOAuthConfig`. Anything else in a server's
@@ -42363,18 +42599,6 @@ function renameEnabledToolsToTools({ server, serverName, logger }) {
42363
42599
  };
42364
42600
  }
42365
42601
  /**
42366
- * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
42367
- * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
42368
- * CLI-written entry can be a bare string. Canonical `enabledTools` is
42369
- * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
42370
- * through would reject the WHOLE imported MCP file rather than one key. Anything
42371
- * that is not a string array is therefore left under `tools` untouched, matching
42372
- * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
42373
- */
42374
- function isStringArray(value) {
42375
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
42376
- }
42377
- /**
42378
42602
  * Resolves and sets the transport type for each MCP server config.
42379
42603
  * GitHub Copilot CLI requires the "type" field for each server. A server it
42380
42604
  * cannot express — one that names no transport, or names one it carries no way
@@ -42416,7 +42640,7 @@ function addTypeField(mcpServers, logger) {
42416
42640
  });
42417
42641
  continue;
42418
42642
  }
42419
- const { httpUrl: _httpUrl, ...rest } = parsed;
42643
+ const { httpUrl: _httpUrl, disabled: _disabled, ...rest } = parsed;
42420
42644
  result[name] = {
42421
42645
  ...renameEnabledToolsToTools({
42422
42646
  server: rest,
@@ -42428,7 +42652,7 @@ function addTypeField(mcpServers, logger) {
42428
42652
  };
42429
42653
  continue;
42430
42654
  }
42431
- const { httpUrl: _localHttpUrl, ...local } = parsed;
42655
+ const { httpUrl: _localHttpUrl, disabled: _localDisabled, ...local } = parsed;
42432
42656
  const [head, ...tail] = resolveLocalMcpCommand(parsed);
42433
42657
  if (head === void 0) {
42434
42658
  warnAndSkipMcpServer({
@@ -42459,7 +42683,7 @@ function removeTypeField(config) {
42459
42683
  const result = {};
42460
42684
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
42461
42685
  const { tools, ...withoutTools } = server;
42462
- const restored = isStringArray(tools) ? {
42686
+ const restored = isStringArray$1(tools) ? {
42463
42687
  ...withoutTools,
42464
42688
  enabledTools: tools
42465
42689
  } : {
@@ -42475,6 +42699,99 @@ function removeTypeField(config) {
42475
42699
  }
42476
42700
  return result;
42477
42701
  }
42702
+ /**
42703
+ * The Copilot CLI settings file carrying `disabledMcpServers`:
42704
+ * `.github/copilot/settings.json` (project) / `~/.copilot/settings.json`
42705
+ * (global). Both scopes document the key ("MCP server names to disable. Listed
42706
+ * servers are configured but not started"); at repository scope it is
42707
+ * union-merged with the user value, so a repository can add names but never
42708
+ * remove them.
42709
+ *
42710
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
42711
+ */
42712
+ function copilotCliSettingsPaths({ global }) {
42713
+ return global ? {
42714
+ relativeDirPath: COPILOT_DIR,
42715
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42716
+ } : {
42717
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
42718
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42719
+ };
42720
+ }
42721
+ async function readCopilotCliSettings({ outputRoot, global }) {
42722
+ const paths = copilotCliSettingsPaths({ global });
42723
+ const existingContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
42724
+ try {
42725
+ return {
42726
+ paths,
42727
+ existingContent,
42728
+ document: parseSharedConfig({
42729
+ format: "json",
42730
+ fileContent: existingContent,
42731
+ filePath: join(paths.relativeDirPath, paths.relativeFilePath),
42732
+ invalidRootPolicy: "error"
42733
+ })
42734
+ };
42735
+ } catch (error) {
42736
+ return {
42737
+ paths,
42738
+ existingContent,
42739
+ document: null,
42740
+ error
42741
+ };
42742
+ }
42743
+ }
42744
+ /**
42745
+ * The names of the servers rulesync actually writes for this target: one
42746
+ * `addTypeField` skips (no transport, WebSocket, missing url or command) is
42747
+ * not managed, so its name neither gains a toggle nor strips a same-named
42748
+ * user-authored entry from the settings file.
42749
+ */
42750
+ function managedServerNames(mcpServers) {
42751
+ return Object.keys(addTypeField(mcpServers) ?? {});
42752
+ }
42753
+ /**
42754
+ * Overlays `disabled: true` on the servers the scope's settings file lists in
42755
+ * `disabledMcpServers`, so import round-trips the toggle into the canonical
42756
+ * config. A settings file that exists but cannot be parsed throws
42757
+ * (fail-closed): importing past it would re-enable every disabled server.
42758
+ */
42759
+ async function overlayDisabledServers({ mcpServers, outputRoot, global }) {
42760
+ const settings = await readCopilotCliSettings({
42761
+ outputRoot,
42762
+ global
42763
+ });
42764
+ if (settings.document === null) throw new Error(`Cannot import Copilot CLI MCP servers: ${formatError(settings.error)}. Fix the file so disabledMcpServers can be read.`, { cause: settings.error });
42765
+ const disabled = settings.document.disabledMcpServers;
42766
+ if (!isStringArray$1(disabled) || disabled.length === 0) return mcpServers;
42767
+ const servers = { ...mcpServers };
42768
+ for (const name of disabled) {
42769
+ if (!Object.hasOwn(servers, name)) continue;
42770
+ const server = servers[name];
42771
+ if (isPlainObject$1(server)) servers[name] = {
42772
+ ...server,
42773
+ disabled: true
42774
+ };
42775
+ }
42776
+ return servers;
42777
+ }
42778
+ /**
42779
+ * Auxiliary writer for `disabledMcpServers` in the Copilot CLI settings file.
42780
+ * Only that key is owned (through the shared-config gateway), so `deniedUrls`,
42781
+ * `disableAllHooks`, `model` and every other setting are preserved. It is a
42782
+ * shared, user-owned file and is never deleted.
42783
+ */
42784
+ var CopilotcliMcpSettings = class extends ToolFile {
42785
+ validate() {
42786
+ return {
42787
+ success: true,
42788
+ error: null
42789
+ };
42790
+ }
42791
+ isDeletable() {
42792
+ return false;
42793
+ }
42794
+ };
42478
42795
  var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42479
42796
  json;
42480
42797
  constructor(params) {
@@ -42517,7 +42834,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42517
42834
  const json = JSON.parse(fileContent);
42518
42835
  const newJson = {
42519
42836
  ...json,
42520
- mcpServers: json.mcpServers ?? {}
42837
+ mcpServers: await overlayDisabledServers({
42838
+ mcpServers: json.mcpServers ?? {},
42839
+ outputRoot,
42840
+ global
42841
+ })
42521
42842
  };
42522
42843
  return new CopilotcliMcp({
42523
42844
  outputRoot,
@@ -42532,7 +42853,17 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42532
42853
  const paths = this.getSettablePaths({ global });
42533
42854
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
42534
42855
  const json = JSON.parse(fileContent);
42535
- const copilotCliMcpServers = addTypeField(rulesyncMcp.getMcpServers(), logger);
42856
+ const settings = await readCopilotCliSettings({
42857
+ outputRoot,
42858
+ global
42859
+ });
42860
+ const existingDisabled = settings.document?.disabledMcpServers;
42861
+ const offSwitchBlocker = settings.document === null ? "cannot be parsed" : existingDisabled !== void 0 && !isStringArray$1(existingDisabled) ? "has a disabledMcpServers value that is not an array of server names" : void 0;
42862
+ const copilotCliMcpServers = addTypeField(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).filter(([name, server]) => {
42863
+ if (server.disabled !== true || offSwitchBlocker === void 0) return true;
42864
+ logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${join(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} ${offSwitchBlocker}, so disabledMcpServers cannot be written to switch it off.`);
42865
+ return false;
42866
+ })), logger);
42536
42867
  const mcpJson = {
42537
42868
  ...json,
42538
42869
  mcpServers: copilotCliMcpServers
@@ -42546,6 +42877,66 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42546
42877
  global
42547
42878
  });
42548
42879
  }
42880
+ /**
42881
+ * `disabledMcpServers` lives in the Copilot CLI settings file the hooks and
42882
+ * permissions features also write. Declared here so the write-order
42883
+ * derivation sees this feature as one of that file's writers — it is not a
42884
+ * settable path, since the servers themselves live in the MCP file.
42885
+ */
42886
+ static getExtraSharedWritePaths({ global = false } = {}) {
42887
+ return [copilotCliSettingsPaths({ global })];
42888
+ }
42889
+ /**
42890
+ * Routes the canonical `disabled: true` to `disabledMcpServers`, the key
42891
+ * Copilot CLI actually consults: an MCP file entry has no `disabled` field.
42892
+ * rulesync owns the toggle for the servers it writes; names it does not
42893
+ * manage keep their existing state. The file is not created or touched when
42894
+ * there is nothing to disable and nothing to retract, nor when the existing
42895
+ * `disabledMcpServers` is not a string array (left as is, with a warning).
42896
+ */
42897
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
42898
+ const servers = rulesyncMcp.forTarget({
42899
+ toolTarget: "copilotcli",
42900
+ global
42901
+ }).getMcpServers();
42902
+ const managedNames = managedServerNames(servers);
42903
+ const disabledNames = managedNames.filter((name) => servers[name]?.disabled === true);
42904
+ const settings = await readCopilotCliSettings({
42905
+ outputRoot,
42906
+ global
42907
+ });
42908
+ if (settings.document === null) {
42909
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: ${formatError(settings.error)}`);
42910
+ return [];
42911
+ }
42912
+ const existing = settings.document.disabledMcpServers;
42913
+ if (existing !== void 0 && !isStringArray$1(existing)) {
42914
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: the existing value is not an array of server names. Fix it in ${join(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} so rulesync can manage the toggle.`);
42915
+ return [];
42916
+ }
42917
+ const existingDisabled = existing ?? [];
42918
+ const managedNameSet = new Set(managedNames);
42919
+ const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
42920
+ if (reEnabled.length > 0) logger?.warn(`Copilot CLI MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
42921
+ const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
42922
+ if (mergedDisabled.length === 0 && existing === void 0) return [];
42923
+ if (isStringArray$1(existing) && existing.length === mergedDisabled.length && existing.toSorted().every((name, index) => name === mergedDisabled[index])) return [];
42924
+ const { paths } = settings;
42925
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
42926
+ return [new CopilotcliMcpSettings({
42927
+ outputRoot,
42928
+ relativeDirPath: paths.relativeDirPath,
42929
+ relativeFilePath: paths.relativeFilePath,
42930
+ fileContent: applySharedConfigPatch({
42931
+ fileKey: sharedConfigFileKey(paths),
42932
+ feature: "mcp",
42933
+ existingContent: settings.existingContent,
42934
+ patch: { disabledMcpServers: mergedDisabled.length > 0 ? mergedDisabled : void 0 },
42935
+ filePath
42936
+ }),
42937
+ global
42938
+ })];
42939
+ }
42549
42940
  toRulesyncMcp() {
42550
42941
  const mcpServers = removeTypeField(this.json);
42551
42942
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
@@ -42977,10 +43368,10 @@ function convertFromCrushFormat(crushServers) {
42977
43368
  if (value === "http" || value === "sse") converted.type = value;
42978
43369
  break;
42979
43370
  case "disabled_tools":
42980
- if (isStringArray$2(value)) converted.disabledTools = value;
43371
+ if (isStringArray$1(value)) converted.disabledTools = value;
42981
43372
  break;
42982
43373
  case "enabled_tools":
42983
- if (isStringArray$2(value)) converted.enabledTools = value;
43374
+ if (isStringArray$1(value)) converted.enabledTools = value;
42984
43375
  break;
42985
43376
  case "oauth_token": break;
42986
43377
  default: {
@@ -43967,7 +44358,7 @@ function convertRowsFromDsh(rows, logger) {
43967
44358
  } else {
43968
44359
  server.type = "stdio";
43969
44360
  if (typeof config.command === "string") server.command = config.command;
43970
- if (isStringArray$2(config.args)) server.args = config.args;
44361
+ if (isStringArray$1(config.args)) server.args = config.args;
43971
44362
  const env = copyStringRecord(config.env);
43972
44363
  if (env !== void 0) server.env = env;
43973
44364
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44398,11 +44789,11 @@ function applyGooseStdioFields(ext, config) {
44398
44789
  if (Array.isArray(command)) {
44399
44790
  if (typeof command[0] === "string") ext.cmd = command[0];
44400
44791
  const rest = command.slice(1).filter((c) => typeof c === "string");
44401
- const args = isStringArray$2(config.args) ? config.args : [];
44792
+ const args = isStringArray$1(config.args) ? config.args : [];
44402
44793
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
44403
44794
  } else if (typeof command === "string") {
44404
44795
  ext.cmd = command;
44405
- if (isStringArray$2(config.args)) ext.args = config.args;
44796
+ if (isStringArray$1(config.args)) ext.args = config.args;
44406
44797
  }
44407
44798
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
44408
44799
  if (typeof config.cwd === "string") ext.cwd = config.cwd;
@@ -44505,7 +44896,7 @@ function convertFromGooseFormat(extensions) {
44505
44896
  else if (type === "streamable_http") server.type = "http";
44506
44897
  else server.type = "stdio";
44507
44898
  if (typeof ext.cmd === "string") server.command = ext.cmd;
44508
- if (isStringArray$2(ext.args)) server.args = ext.args;
44899
+ if (isStringArray$1(ext.args)) server.args = ext.args;
44509
44900
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
44510
44901
  if (typeof ext.cwd === "string") server.cwd = ext.cwd;
44511
44902
  if (typeof ext.uri === "string") server.url = ext.uri;
@@ -44527,11 +44918,11 @@ function buildGoosePluginStdioServer(config) {
44527
44918
  if (Array.isArray(command)) {
44528
44919
  if (typeof command[0] === "string") server.command = command[0];
44529
44920
  const rest = command.slice(1).filter((c) => typeof c === "string");
44530
- const args = isStringArray$2(config.args) ? config.args : [];
44921
+ const args = isStringArray$1(config.args) ? config.args : [];
44531
44922
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
44532
44923
  } else if (typeof command === "string") {
44533
44924
  server.command = command;
44534
- if (isStringArray$2(config.args)) server.args = config.args;
44925
+ if (isStringArray$1(config.args)) server.args = config.args;
44535
44926
  }
44536
44927
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
44537
44928
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44939,7 +45330,7 @@ function copyHermesOauth(source) {
44939
45330
  if (!isRecord$1(source)) return;
44940
45331
  const oauth = {};
44941
45332
  for (const key of HERMES_OAUTH_STRING_KEYS) if (typeof source[key] === "string") oauth[key] = source[key];
44942
- if (typeof oauth.scope !== "string" && isStringArray$2(source.scopes)) oauth.scope = source.scopes.join(" ");
45333
+ if (typeof oauth.scope !== "string" && isStringArray$1(source.scopes)) oauth.scope = source.scopes.join(" ");
44943
45334
  for (const key of ["redirect_port", "timeout"]) if (typeof source[key] === "number") oauth[key] = source[key];
44944
45335
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
44945
45336
  return Object.keys(oauth).length > 0 ? oauth : void 0;
@@ -44966,7 +45357,7 @@ function copyHermesAdvancedFields(source, target) {
44966
45357
  target.auth = source.auth;
44967
45358
  copied = true;
44968
45359
  }
44969
- if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
45360
+ if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
44970
45361
  target.client_cert = source.client_cert;
44971
45362
  copied = true;
44972
45363
  }
@@ -45038,8 +45429,8 @@ function copyHermesAdvancedFields(source, target) {
45038
45429
  */
45039
45430
  function buildHermesToolsBlock(config) {
45040
45431
  const tools = {};
45041
- if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
45042
- if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
45432
+ if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
45433
+ if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
45043
45434
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
45044
45435
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
45045
45436
  return tools;
@@ -45051,8 +45442,8 @@ function buildHermesToolsBlock(config) {
45051
45442
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
45052
45443
  */
45053
45444
  function applyHermesToolsBlock(hermesTools, server) {
45054
- if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
45055
- if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45445
+ if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
45446
+ if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45056
45447
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
45057
45448
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
45058
45449
  }
@@ -45078,11 +45469,11 @@ function convertServerToHermes(config) {
45078
45469
  if (Array.isArray(command)) {
45079
45470
  if (typeof command[0] === "string") out.command = command[0];
45080
45471
  const rest = command.slice(1).filter((c) => typeof c === "string");
45081
- const args = isStringArray$2(config.args) ? config.args : [];
45472
+ const args = isStringArray$1(config.args) ? config.args : [];
45082
45473
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
45083
45474
  } else if (typeof command === "string") {
45084
45475
  out.command = command;
45085
- if (isStringArray$2(config.args)) out.args = config.args;
45476
+ if (isStringArray$1(config.args)) out.args = config.args;
45086
45477
  }
45087
45478
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
45088
45479
  } else if (url !== void 0) {
@@ -45132,7 +45523,7 @@ function convertFromHermesFormat(mcpServers) {
45132
45523
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
45133
45524
  const server = {};
45134
45525
  if (typeof config.command === "string") server.command = config.command;
45135
- if (isStringArray$2(config.args)) server.args = config.args;
45526
+ if (isStringArray$1(config.args)) server.args = config.args;
45136
45527
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
45137
45528
  if (typeof config.url === "string") server.url = config.url;
45138
45529
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -46324,7 +46715,7 @@ function mergeToolLists(...lists) {
46324
46715
  function toKiroMcpServers(servers) {
46325
46716
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46326
46717
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
46327
- const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46718
+ const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46328
46719
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
46329
46720
  return [name, {
46330
46721
  ...rest,
@@ -46348,7 +46739,7 @@ function fromKiroMcpServers(servers) {
46348
46739
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46349
46740
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
46350
46741
  const { autoApprove, ...rest } = server;
46351
- if (!isStringArray$2(autoApprove)) return [name, server];
46742
+ if (!isStringArray$1(autoApprove)) return [name, server];
46352
46743
  return [name, {
46353
46744
  ...rest,
46354
46745
  kiroAutoApprove: autoApprove
@@ -47777,7 +48168,7 @@ function headersRecordToPoolList(headers) {
47777
48168
  return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
47778
48169
  }
47779
48170
  function poolHeadersListToRecord(serverName, headers) {
47780
- if (!isStringArray$2(headers)) return void 0;
48171
+ if (!isStringArray$1(headers)) return void 0;
47781
48172
  const result = {};
47782
48173
  for (const entry of headers) {
47783
48174
  const separator = entry.indexOf(":");
@@ -47803,7 +48194,7 @@ const GLOB_METACHARACTERS = /[*?[]/;
47803
48194
  /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
47804
48195
  const MAX_LISTED_GLOB_PATTERNS = 10;
47805
48196
  function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
47806
- if (!isStringArray$2(value)) {
48197
+ if (!isStringArray$1(value)) {
47807
48198
  warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
47808
48199
  return;
47809
48200
  }
@@ -48013,7 +48404,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
48013
48404
  const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
48014
48405
  return [serverName, {
48015
48406
  ...serverConfig,
48016
- ...isStringArray$2(poolAllow) && { poolAllow }
48407
+ ...isStringArray$1(poolAllow) && { poolAllow }
48017
48408
  }];
48018
48409
  })), logger);
48019
48410
  return new PoolMcp({
@@ -48244,7 +48635,7 @@ function pickServerLists(source) {
48244
48635
  if (!isPlainObject$1(source)) return {};
48245
48636
  return Object.fromEntries(QWENCODE_MCP_SERVER_LIST_KEYS.flatMap((key) => {
48246
48637
  const value = source[key];
48247
- return isStringArray$2(value) ? [[key, value]] : [];
48638
+ return isStringArray$1(value) ? [[key, value]] : [];
48248
48639
  }));
48249
48640
  }
48250
48641
  /**
@@ -48937,7 +49328,7 @@ async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger
48937
49328
  }
48938
49329
  function disabledNamesOf(config) {
48939
49330
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
48940
- return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
49331
+ return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
48941
49332
  }
48942
49333
  /**
48943
49334
  * The value `mcp.mcpConfigPath` needs so Rovo Dev reads the `mcp.json` rulesync
@@ -49353,7 +49744,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
49353
49744
  return [];
49354
49745
  }
49355
49746
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
49356
- const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49747
+ const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49357
49748
  const managedNameSet = new Set(managedNames);
49358
49749
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
49359
49750
  if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
@@ -56512,8 +56903,9 @@ function convertClaudeToRulesyncPermissions(params) {
56512
56903
  //#endregion
56513
56904
  //#region src/features/permissions/cline-permissions.ts
56514
56905
  /**
56515
- * Cline CLI loads command permissions from the `CLINE_COMMAND_PERMISSIONS` environment variable.
56516
- * Cline supports only `allow` and `deny` (no `ask`), and only for shell commands.
56906
+ * The legacy Cline CLI loaded command permissions from the `CLINE_COMMAND_PERMISSIONS`
56907
+ * environment variable. It supported only `allow` and `deny` (no `ask`), and only for
56908
+ * shell commands.
56517
56909
  * Schema:
56518
56910
  * ```json
56519
56911
  * { "allow": ["pattern1", ...], "deny": ["pattern2", ...], "allowRedirects": false }
@@ -56521,7 +56913,14 @@ function convertClaudeToRulesyncPermissions(params) {
56521
56913
  *
56522
56914
  * Rulesync writes the JSON to `.cline/command-permissions.json` so users can do:
56523
56915
  * `export CLINE_COMMAND_PERMISSIONS=$(cat .cline/command-permissions.json)`
56916
+ *
56917
+ * Deprecated: no Cline CLI 3 release reads `CLINE_COMMAND_PERMISSIONS`. Upstream removed
56918
+ * it from the docs in cline/cline@6354d209, and the CLI reference now says the CLI has no
56919
+ * allow or deny list for shell commands and recommends a `PreToolUse` hook instead
56920
+ * (https://docs.cline.bot/cli/cli-reference). The output is still generated for
56921
+ * compatibility, with a deprecation warning on every generation.
56524
56922
  */
56923
+ const CLINE_PERMISSIONS_DEPRECATION_WARNING = "The Cline permissions output (.cline/command-permissions.json) is deprecated. No Cline CLI 3 release reads CLINE_COMMAND_PERMISSIONS, so the generated allow/deny lists are not enforced by current Cline releases. To block shell commands in Cline, use a preToolUse hook (rulesync hooks feature, cline target) that cancels the run_commands calls you do not allow. Note that a hook returning `cancel: true` stops the whole Cline task, not just that one tool call (https://github.com/cline/cline/issues/14697). See https://docs.cline.bot/cli/cli-reference.";
56525
56924
  const ClineCommandPermissionsSchema = z.looseObject({
56526
56925
  allow: z.optional(z.array(z.string())),
56527
56926
  deny: z.optional(z.array(z.string())),
@@ -56639,6 +57038,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56639
57038
  });
56640
57039
  }
56641
57040
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
57041
+ logger?.warn(CLINE_PERMISSIONS_DEPRECATION_WARNING);
56642
57042
  const paths = ClinePermissions.getSettablePaths({ global });
56643
57043
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
56644
57044
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
@@ -56668,7 +57068,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56668
57068
  const mergedDeny = uniq([...existing.deny ?? [], ...dedupedDeny]).toSorted();
56669
57069
  const denySet = new Set(mergedDeny);
56670
57070
  const collisions = dedupedAllow.filter((p) => denySet.has(p));
56671
- if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. Cline documents that deny rules always take precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
57071
+ if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. The legacy Cline CLI gave deny rules precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
56672
57072
  const next = {
56673
57073
  ...existing,
56674
57074
  allow: dedupedAllow,
@@ -59951,9 +60351,9 @@ function managedContinueToolNames(config) {
59951
60351
  */
59952
60352
  function parseContinuePermissionLists(parsed) {
59953
60353
  return {
59954
- exclude: isStringArray$2(parsed.exclude) ? parsed.exclude : [],
59955
- ask: isStringArray$2(parsed.ask) ? parsed.ask : [],
59956
- allow: isStringArray$2(parsed.allow) ? parsed.allow : []
60354
+ exclude: isStringArray$1(parsed.exclude) ? parsed.exclude : [],
60355
+ ask: isStringArray$1(parsed.ask) ? parsed.ask : [],
60356
+ allow: isStringArray$1(parsed.allow) ? parsed.allow : []
59957
60357
  };
59958
60358
  }
59959
60359
  /**
@@ -60036,6 +60436,193 @@ const AUTO_APPROVE_KEYS = {
60036
60436
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60037
60437
  */
60038
60438
  const DEFAULT_PERMISSION_MODE_KEY = "chat.permissions.default";
60439
+ /**
60440
+ * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
60441
+ * at path `a.b` is written to `chat.agent.sandbox.a.b`.
60442
+ */
60443
+ const SANDBOX_KEY_PREFIX = "chat.agent.sandbox.";
60444
+ /**
60445
+ * The `copilot.sandbox` leaves rulesync manages, each a flat VS Code setting
60446
+ * (`SANDBOX_KEY_PREFIX` + the dotted path) with the value shape VS Code
60447
+ * registers for it. The shape is re-checked on import, so a hand-written value
60448
+ * VS Code would not accept is skipped instead of failing the whole import.
60449
+ *
60450
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60451
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60452
+ */
60453
+ const SANDBOX_LEAVES = [
60454
+ {
60455
+ path: ["enabled"],
60456
+ schema: CopilotSandboxEnabledSchema,
60457
+ normalize: (value) => value === true ? "on" : value === false ? "off" : value
60458
+ },
60459
+ {
60460
+ path: ["network", "allowNetwork"],
60461
+ schema: z.boolean()
60462
+ },
60463
+ {
60464
+ path: ["network", "allowLocalNetwork"],
60465
+ schema: z.boolean()
60466
+ },
60467
+ {
60468
+ path: ["allowUnsandboxedCommands"],
60469
+ schema: z.boolean()
60470
+ },
60471
+ {
60472
+ path: ["mcpServers"],
60473
+ schema: z.boolean()
60474
+ },
60475
+ {
60476
+ path: ["lspServers"],
60477
+ schema: z.boolean()
60478
+ },
60479
+ {
60480
+ path: ["credentials", "authenticategit"],
60481
+ schema: z.boolean()
60482
+ },
60483
+ {
60484
+ path: ["credentials", "authenticategh"],
60485
+ schema: z.boolean()
60486
+ },
60487
+ {
60488
+ path: ["fileSystem", "allowDevToolAccess"],
60489
+ schema: z.boolean()
60490
+ },
60491
+ {
60492
+ path: ["fileSystem", "userConfiguredPaths"],
60493
+ schema: CopilotSandboxUserConfiguredPathsSchema
60494
+ }
60495
+ ];
60496
+ const USER_CONFIGURED_PATHS_KEY = `${SANDBOX_KEY_PREFIX}fileSystem.userConfiguredPaths`;
60497
+ /**
60498
+ * `copilot.sandbox` values that loosen the Agent Host sandbox. They are still
60499
+ * written — a sandbox policy is the user's call — but never silently. Each
60500
+ * `widens` names the value that does *not* loosen anything and reports the
60501
+ * rest, so an unexpected value is reported rather than ignored.
60502
+ *
60503
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60504
+ */
60505
+ const COPILOT_TRUST_AFFECTING_SANDBOX_PATHS = [
60506
+ {
60507
+ path: ["enabled"],
60508
+ reason: "turns the sandbox off, so agent terminal commands run unconfined",
60509
+ widens: (value) => value !== "on"
60510
+ },
60511
+ {
60512
+ path: ["network", "allowNetwork"],
60513
+ reason: "lets sandboxed commands reach the external network",
60514
+ widens: isNotFalse
60515
+ },
60516
+ {
60517
+ path: ["network", "allowLocalNetwork"],
60518
+ reason: "lets sandboxed commands reach local network resources",
60519
+ widens: isNotFalse
60520
+ },
60521
+ {
60522
+ path: ["allowUnsandboxedCommands"],
60523
+ reason: "lets the agent ask to run a blocked command outside the sandbox",
60524
+ widens: isNotFalse
60525
+ },
60526
+ {
60527
+ path: ["mcpServers"],
60528
+ reason: "runs locally launched MCP servers outside the sandbox",
60529
+ widens: isNotTrue
60530
+ },
60531
+ {
60532
+ path: ["lspServers"],
60533
+ reason: "runs language servers outside the sandbox",
60534
+ widens: isNotTrue
60535
+ },
60536
+ {
60537
+ path: ["credentials", "authenticategit"],
60538
+ reason: "hands your Git credentials to sandboxed commands",
60539
+ widens: isNotFalse
60540
+ },
60541
+ {
60542
+ path: ["credentials", "authenticategh"],
60543
+ reason: "hands your GitHub CLI authentication to sandboxed commands",
60544
+ widens: isNotFalse
60545
+ },
60546
+ {
60547
+ path: ["fileSystem", "allowDevToolAccess"],
60548
+ reason: "grants sandboxed commands developer-tool directories, which can hold credentials such as registry tokens",
60549
+ widens: isNotFalse
60550
+ },
60551
+ {
60552
+ path: [
60553
+ "fileSystem",
60554
+ "userConfiguredPaths",
60555
+ "readwritePaths"
60556
+ ],
60557
+ reason: "adds paths sandboxed commands may write to, outside the working directory",
60558
+ widens: isNonEmptyList
60559
+ },
60560
+ {
60561
+ path: [
60562
+ "fileSystem",
60563
+ "userConfiguredPaths",
60564
+ "readonlyPaths"
60565
+ ],
60566
+ reason: "adds paths sandboxed commands may read, outside the working directory",
60567
+ widens: isNonEmptyList
60568
+ }
60569
+ ];
60570
+ const COPILOT_TOOL_LABEL = "GitHub Copilot";
60571
+ function sandboxKey(path) {
60572
+ return `${SANDBOX_KEY_PREFIX}${path.join(".")}`;
60573
+ }
60574
+ /**
60575
+ * Patch entries for every `copilot.sandbox` leaf the canonical config states.
60576
+ * Leaves it does not state are left as the file has them, so authoring only
60577
+ * `network.allowNetwork` never disturbs a hand-written `enabled`. The
60578
+ * `userConfiguredPaths` object merges per list for the same reason: authoring
60579
+ * `readwritePaths` keeps a hand-written `deniedPaths`, which would otherwise be
60580
+ * a restriction lost without a word.
60581
+ */
60582
+ function buildSandboxPatch({ sandbox, existingSettings }) {
60583
+ const patch = {};
60584
+ for (const { path } of SANDBOX_LEAVES) {
60585
+ const value = readSandboxPath({
60586
+ sandbox,
60587
+ path
60588
+ });
60589
+ if (value === void 0 || value === UNREADABLE_SANDBOX_PATH) continue;
60590
+ const key = sandboxKey(path);
60591
+ if (key === USER_CONFIGURED_PATHS_KEY && isPlainObject$1(value)) {
60592
+ const existing = existingSettings[key];
60593
+ patch[key] = {
60594
+ ...isPlainObject$1(existing) ? existing : {},
60595
+ ...value
60596
+ };
60597
+ continue;
60598
+ }
60599
+ patch[key] = value;
60600
+ }
60601
+ return patch;
60602
+ }
60603
+ /**
60604
+ * Lift every managed `chat.agent.sandbox.*` key the file holds back into a
60605
+ * nested `copilot.sandbox` object. A value whose shape VS Code would reject is
60606
+ * skipped; the write path leaves it in place.
60607
+ */
60608
+ function readSandboxFromSettings(settings) {
60609
+ const sandbox = {};
60610
+ for (const { path, schema, normalize } of SANDBOX_LEAVES) {
60611
+ const value = settings[sandboxKey(path)];
60612
+ const parsed = schema.safeParse(normalize === void 0 ? value : normalize(value));
60613
+ if (!parsed.success) continue;
60614
+ let cursor = sandbox;
60615
+ for (const segment of path.slice(0, -1)) {
60616
+ const next = cursor[segment];
60617
+ const child = isPlainObject$1(next) ? next : {};
60618
+ cursor[segment] = child;
60619
+ cursor = child;
60620
+ }
60621
+ const leaf = path[path.length - 1];
60622
+ if (leaf !== void 0) cursor[leaf] = parsed.data;
60623
+ }
60624
+ return Object.keys(sandbox).length > 0 ? sandbox : void 0;
60625
+ }
60039
60626
  function isCopilotDefaultPermissionMode(value) {
60040
60627
  return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
60041
60628
  }
@@ -60080,7 +60667,9 @@ function buildAutoApproveValue(rules) {
60080
60667
  * hand-written edits or urls map.
60081
60668
  * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
60082
60669
  * (the starting permission level of new chat sessions); without it the key is
60083
- * left untouched.
60670
+ * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
60671
+ * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
60672
+ * warns about every value it writes that loosens the sandbox.
60084
60673
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
60085
60674
  * platform-dependent path outside rulesync's home-relative global model.
60086
60675
  */
@@ -60128,6 +60717,26 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60128
60717
  }
60129
60718
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
60130
60719
  if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
60720
+ const sandbox = config.copilot?.sandbox;
60721
+ if (sandbox !== void 0) {
60722
+ Object.assign(patch, buildSandboxPatch({
60723
+ sandbox,
60724
+ existingSettings: parseSharedConfig({
60725
+ format: "jsonc",
60726
+ fileContent: existingContent,
60727
+ filePath
60728
+ })
60729
+ }));
60730
+ warnOnTrustAffectingEntries({
60731
+ toolLabel: COPILOT_TOOL_LABEL,
60732
+ entries: collectTrustAffectingSandboxPaths({
60733
+ sandbox,
60734
+ paths: COPILOT_TRUST_AFFECTING_SANDBOX_PATHS
60735
+ }),
60736
+ relativeFilePath: join(paths.relativeDirPath, paths.relativeFilePath),
60737
+ logger
60738
+ });
60739
+ }
60131
60740
  return new CopilotPermissions({
60132
60741
  outputRoot,
60133
60742
  relativeDirPath: paths.relativeDirPath,
@@ -60164,9 +60773,14 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60164
60773
  if (Object.keys(rules).length > 0) permission[category] = rules;
60165
60774
  }
60166
60775
  const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY];
60776
+ const sandbox = readSandboxFromSettings(settings);
60777
+ const copilot = {
60778
+ ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
60779
+ ...sandbox !== void 0 && { sandbox }
60780
+ };
60167
60781
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
60168
60782
  permission,
60169
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { copilot: { defaultPermissionMode } }
60783
+ ...Object.keys(copilot).length > 0 && { copilot }
60170
60784
  }, null, 2) });
60171
60785
  }
60172
60786
  validate() {
@@ -60480,8 +61094,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60480
61094
  logger
60481
61095
  });
60482
61096
  const managedToolNames = managedCrushToolNames(config);
60483
- const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
60484
- const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
61097
+ const preservedAllowed = (isStringArray$1(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
61098
+ const preservedDisabled = (isStringArray$1(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
60485
61099
  const allowedList = uniq$1([...preservedAllowed, ...generated.allowed]);
60486
61100
  const disabledList = uniq$1([...preservedDisabled, ...generated.disabled]);
60487
61101
  const patch = {};
@@ -60507,8 +61121,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60507
61121
  const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
60508
61122
  const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
60509
61123
  const rulesyncConfig = convertCrushListsToRulesync({
60510
- allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
60511
- disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
61124
+ allowed: isStringArray$1(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
61125
+ disabled: isStringArray$1(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
60512
61126
  });
60513
61127
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
60514
61128
  }
@@ -62683,7 +63297,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62683
63297
  function convertGoosePermissionConfigToRulesync(userPermission) {
62684
63298
  const permission = {};
62685
63299
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
62686
- const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
63300
+ const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
62687
63301
  const action = GOOSE_LIST_TO_ACTION[key];
62688
63302
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62689
63303
  permission[category] ??= {};
@@ -63031,7 +63645,7 @@ const ACTION_RANK$3 = {
63031
63645
  * (mirrors the Cursor adapter's preservation of unmanaged types).
63032
63646
  */
63033
63647
  function unmanagedEntries(existingPermission, key) {
63034
- return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63648
+ return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63035
63649
  }
63036
63650
  /**
63037
63651
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -63088,7 +63702,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
63088
63702
  const generated = ranked.get(GROK_ALL_TOOLS_ENTRY);
63089
63703
  for (const action of ["deny", "ask"]) {
63090
63704
  const entries = existingPermission[action];
63091
- if (!isStringArray$2(entries)) continue;
63705
+ if (!isStringArray$1(entries)) continue;
63092
63706
  if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63093
63707
  if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
63094
63708
  logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
@@ -63119,9 +63733,9 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63119
63733
  * described more than once resolves to the strictest action.
63120
63734
  */
63121
63735
  function parseGrokPermissionArrays(permission) {
63122
- const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
63123
- const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
63124
- const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
63736
+ const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
63737
+ const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
63738
+ const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
63125
63739
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
63126
63740
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
63127
63741
  const result = {};
@@ -63329,20 +63943,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
63329
63943
  const permission = clonePermissionBlock(provenance.permission);
63330
63944
  reconcileCommandAllowlist({
63331
63945
  permission,
63332
- commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
63946
+ commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
63333
63947
  });
63334
63948
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
63335
63949
  reconcileNativeDenies({
63336
63950
  permission,
63337
63951
  category: SHELL_PERMISSION_CATEGORY,
63338
- patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
63952
+ patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
63339
63953
  });
63340
63954
  const security = isRecord$1(config.security) ? config.security : {};
63341
63955
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
63342
63956
  reconcileNativeDenies({
63343
63957
  permission,
63344
63958
  category: WEBFETCH_PERMISSION_CATEGORY,
63345
- patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63959
+ patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63346
63960
  });
63347
63961
  removeEmptyCategories(permission);
63348
63962
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -64736,13 +65350,13 @@ function classifyExistingEntry({ emitted, capability, pattern, effect }) {
64736
65350
  * how many entries rulesync does not generate.
64737
65351
  */
64738
65352
  function mergeExistingRule({ rule, emitted, logger }) {
64739
- if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$2(rule.match)) return {
65353
+ if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$1(rule.match)) return {
64740
65354
  rule,
64741
65355
  unrelatedEntries: 0
64742
65356
  };
64743
65357
  const capability = rule.capability;
64744
65358
  const isCatchAll = rule.match === void 0;
64745
- const patterns = isStringArray$2(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
65359
+ const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64746
65360
  if (patterns.length === 0) return {
64747
65361
  rule,
64748
65362
  unrelatedEntries: 0
@@ -64926,7 +65540,7 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
64926
65540
  for (const rule of rules) {
64927
65541
  if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
64928
65542
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
64929
- const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$2(rule.match) ? rule.match : [];
65543
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$1(rule.match) ? rule.match : [];
64930
65544
  const categoryRules = permission[category] ??= {};
64931
65545
  for (const pattern of patterns) {
64932
65546
  const previous = categoryRules[pattern];
@@ -66644,8 +67258,8 @@ function importPoolTool({ toolConfig, bucket }) {
66644
67258
  bucket[CATCH_ALL_PATTERN$3] = "deny";
66645
67259
  return;
66646
67260
  }
66647
- const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
66648
- const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
67261
+ const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
67262
+ const allowed = isStringArray$1(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
66649
67263
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
66650
67264
  for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
66651
67265
  }
@@ -68850,7 +69464,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
68850
69464
  permission[category] ??= {};
68851
69465
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
68852
69466
  }
68853
- if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
69467
+ if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
68854
69468
  permission.read ??= {};
68855
69469
  permission.read[path] ??= "allow";
68856
69470
  }
@@ -69106,7 +69720,7 @@ function stripRefusedOverridePaths(override) {
69106
69720
  */
69107
69721
  function collectTrustAffectingOverrideEntries({ tools, general }) {
69108
69722
  const entries = [];
69109
- const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69723
+ const allowed = isStringArray$1(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69110
69724
  if (allowed.length > 0) entries.push({
69111
69725
  label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
69112
69726
  reason: "auto-approves what it names as the override spells it; a non-shell entry is only checked against a whole-tool deny of its tool"
@@ -69257,7 +69871,7 @@ function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logge
69257
69871
  * hand-written entry went.
69258
69872
  */
69259
69873
  function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
69260
- const dropped = (key, list) => uniq(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69874
+ const dropped = (key, list) => uniq(isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69261
69875
  if (list.includes(entry)) return false;
69262
69876
  const toolName = parseTabnineEntry(entry)?.toolName;
69263
69877
  return toolName === void 0 || !list.includes(toolName);
@@ -69339,7 +69953,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69339
69953
  }
69340
69954
  const overrideList = (key) => {
69341
69955
  const value = overrideTools[key];
69342
- if (value === void 0 || isStringArray$2(value)) return value ?? [];
69956
+ if (value === void 0 || isStringArray$1(value)) return value ?? [];
69343
69957
  warnWithFallback(logger, `Tabnine CLI permissions: ignored tools.${key} of the tabnine override (${quoteValueForWarning(value)}); it must be a list of tool names.`);
69344
69958
  return [];
69345
69959
  };
@@ -69377,7 +69991,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69377
69991
  if (ignoredOverrideKeys.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: ignored ${ignoredOverrideKeys.length} key(s) of the tabnine override (${ignoredOverrideKeys.map(quoteValueForWarning).join(", ")}); only 'tools' and 'general' are written through the permissions feature.`);
69378
69992
  const existingTools = this.existingToolsGroup(existingContent);
69379
69993
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => category === "bash" || category === "*" ? SHELL_TOOL_NAME : toTabnineToolName(category)));
69380
- const preservedEntries = (key) => (isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69994
+ const preservedEntries = (key) => (isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69381
69995
  const toolName = parseTabnineEntry(entry)?.toolName;
69382
69996
  return toolName === void 0 || !managedToolNames.has(toolName);
69383
69997
  });
@@ -69455,7 +70069,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69455
70069
  if (rules !== void 0 && !Object.hasOwn(rules, pattern)) rules[pattern] = action;
69456
70070
  };
69457
70071
  for (const [key, action] of [[EXCLUDE_KEY, "deny"], [ALLOWED_KEY, "allow"]]) {
69458
- const entries = isStringArray$2(tools[key]) ? tools[key] : [];
70072
+ const entries = isStringArray$1(tools[key]) ? tools[key] : [];
69459
70073
  for (const entry of entries) {
69460
70074
  const parsed = parseTabnineEntry(entry);
69461
70075
  if (parsed === void 0) {
@@ -70323,12 +70937,12 @@ const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|
70323
70937
  * The ranges are the `East_Asian_Width=A` property of Unicode 17.0, taken from
70324
70938
  * the table `get-east-asian-width` 1.6.0 carries and merged where two are
70325
70939
  * adjacent. The table is written out here rather than read from the package so
70326
- * that the one lookup adds no dependency of its own, and the drift test in
70327
- * `display-width.test.ts` is what keeps it honest: it loads the package through
70328
- * the prompt renderer that already depends on it, walks every code point, and
70329
- * fails when this table and the property disagree in either direction. When
70330
- * the renderer's copy moves to a newer Unicode, that test is what fails, and
70331
- * the table is regenerated from the new property — its ranges, marks aside,
70940
+ * that the one lookup adds no runtime dependency of its own, and the drift test
70941
+ * in `display-width.test.ts` is what keeps it honest: it loads the package,
70942
+ * pinned as a dev dependency, walks every code point, and fails when this
70943
+ * table and the property disagree in either direction. When that pin moves to
70944
+ * a newer Unicode, that test is what fails, and the table is regenerated from
70945
+ * the new property — its ranges, marks aside,
70332
70946
  * merged where adjacent and written as escapes — with the version named here.
70333
70947
  *
70334
70948
  * Three stretches of the property are left out because they never reach this
@@ -72077,8 +72691,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
72077
72691
  const executionProfiles = isRecord$1(agents["execution_profiles"]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
72078
72692
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles["default"]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
72079
72693
  const config = convertWarpToRulesyncPermissions({
72080
- allow: defaultProfile ? isStringArray$2(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72081
- deny: defaultProfile ? isStringArray$2(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72694
+ allow: defaultProfile ? isStringArray$1(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72695
+ deny: defaultProfile ? isStringArray$1(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72082
72696
  });
72083
72697
  const warpOverride = {};
72084
72698
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -72608,8 +73222,8 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
72608
73222
  const allowlist = defaultProfile[PROFILE_ALLOWLIST_KEY];
72609
73223
  const denylist = defaultProfile[PROFILE_DENYLIST_KEY];
72610
73224
  const config = convertWarpToRulesyncPermissions({
72611
- allow: isStringArray$2(allowlist) ? allowlist : [],
72612
- deny: isStringArray$2(denylist) ? denylist : []
73225
+ allow: isStringArray$1(allowlist) ? allowlist : [],
73226
+ deny: isStringArray$1(denylist) ? denylist : []
72613
73227
  });
72614
73228
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
72615
73229
  const warpcliOverride = {};
@@ -84624,7 +85238,7 @@ var BobSubagent = class extends RooSubagent {
84624
85238
  const mode = super.toRooMode(rulesyncSubagent);
84625
85239
  const frontmatter = rulesyncSubagent.getFrontmatter();
84626
85240
  const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
84627
- if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
85241
+ if (isStringArray$1(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
84628
85242
  return mode;
84629
85243
  }
84630
85244
  };
@@ -90079,7 +90693,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90079
90693
  const mode = super.toRooMode(rulesyncSubagent);
90080
90694
  const frontmatter = rulesyncSubagent.getFrontmatter();
90081
90695
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
90082
- if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90696
+ if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90083
90697
  return mode;
90084
90698
  }
90085
90699
  toRulesyncSubagents() {
@@ -90091,7 +90705,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90091
90705
  targets: ["zoocode"],
90092
90706
  roo: restRooSection
90093
90707
  };
90094
- if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90708
+ if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90095
90709
  return new RulesyncSubagent({
90096
90710
  outputRoot: ".",
90097
90711
  frontmatter: rebuilt,
@@ -94032,7 +94646,8 @@ var NestedAgentsmdRule = class extends ToolRule {
94032
94646
  * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
94033
94647
  * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
94034
94648
  * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
94035
- * budget (32 KiB by default), which the RulesProcessor warns about.
94649
+ * budget (32 KiB by default, or the value set in the project's
94650
+ * `.codex/config.toml`), which the RulesProcessor warns about.
94036
94651
  *
94037
94652
  * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
94038
94653
  */
@@ -94044,6 +94659,29 @@ var CodexcliRule = class extends NestedAgentsmdRule {
94044
94659
  };
94045
94660
  }
94046
94661
  };
94662
+ /**
94663
+ * Read the `project_doc_max_bytes` value set in the project's
94664
+ * `.codex/config.toml`, which Codex loads for trusted projects and which
94665
+ * overrides the 32 KiB default budget for project `AGENTS.md` files.
94666
+ *
94667
+ * Answers `undefined` when the file is missing, is not valid TOML, or does not
94668
+ * set the key to a positive integer, so the caller falls back to the default.
94669
+ *
94670
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
94671
+ */
94672
+ async function readCodexcliProjectDocMaxBytes(outputRoot) {
94673
+ const content = await readFileContentOrNull(join(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME));
94674
+ if (content === null) return;
94675
+ let parsed;
94676
+ try {
94677
+ parsed = smolToml.parse(content);
94678
+ } catch {
94679
+ return;
94680
+ }
94681
+ if (!isPlainObject$1(parsed)) return;
94682
+ const value = parsed.project_doc_max_bytes;
94683
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
94684
+ }
94047
94685
  //#endregion
94048
94686
  //#region src/features/rules/commandcode-rule.ts
94049
94687
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
@@ -99418,7 +100056,10 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
99418
100056
  collisionPolicy: "fold",
99419
100057
  projectInstructionBudget: {
99420
100058
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
99421
- remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (ignore this warning if you already have)."
100059
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
100060
+ readConfiguredBytes: readCodexcliProjectDocMaxBytes,
100061
+ configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
100062
+ defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
99422
100063
  }
99423
100064
  }
99424
100065
  }],
@@ -99963,7 +100604,7 @@ var RulesProcessor = class extends FeatureProcessor {
99963
100604
  factory
99964
100605
  });
99965
100606
  await this.warnForWarpLegacyRootFile(toolRules);
99966
- this.warnForInstructionBudget({
100607
+ await this.warnForInstructionBudget({
99967
100608
  toolRules,
99968
100609
  meta
99969
100610
  });
@@ -99984,14 +100625,20 @@ var RulesProcessor = class extends FeatureProcessor {
99984
100625
  * once, at its shallowest directory. With several output roots the warning
99985
100626
  * names the root it refers to. An output root below the git root is measured
99986
100627
  * without the parent directories' files Codex would also load, so the
99987
- * estimate can come out low there. The budget checked is the tool's default: a value
99988
- * raised by hand in the tool's own config is not read. Global scope is not
100628
+ * estimate can come out low there. The budget checked is the value set in the
100629
+ * output root's tool config when the tool supports one (Codex's
100630
+ * `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
100631
+ * the warning names which one it used. Global scope is not
99989
100632
  * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
99990
100633
  * project budget.
99991
100634
  */
99992
- warnForInstructionBudget({ toolRules, meta }) {
100635
+ async warnForInstructionBudget({ toolRules, meta }) {
99993
100636
  const budget = meta.projectInstructionBudget;
99994
100637
  if (budget === void 0 || this.global) return;
100638
+ const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
100639
+ const limitBytes = configuredBytes ?? budget.bytes;
100640
+ const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
100641
+ const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
99995
100642
  const files = toolRules.map((rule) => ({
99996
100643
  dir: normalizeRelativeDir(rule.getRelativeDirPath()),
99997
100644
  path: toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
@@ -100001,8 +100648,8 @@ var RulesProcessor = class extends FeatureProcessor {
100001
100648
  const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
100002
100649
  const rootFiles = files.filter(({ dir }) => dir === ".");
100003
100650
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100004
- if (rootBytes > budget.bytes) {
100005
- this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${budget.bytes}-byte limit. ${budget.remedy}`);
100651
+ if (rootBytes > limitBytes) {
100652
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${remedy}`);
100006
100653
  return;
100007
100654
  }
100008
100655
  const reportedDirs = [];
@@ -100011,9 +100658,9 @@ var RulesProcessor = class extends FeatureProcessor {
100011
100658
  if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
100012
100659
  const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
100013
100660
  const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
100014
- if (chainBytes > budget.bytes) {
100661
+ if (chainBytes > limitBytes) {
100015
100662
  reportedDirs.push(file.dir);
100016
- this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${budget.bytes}-byte limit (as is every directory below it). ${budget.remedy}`);
100663
+ this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${limitLabel} (as is every directory below it). ${remedy}`);
100017
100664
  }
100018
100665
  }
100019
100666
  }
@@ -103918,4 +104565,4 @@ async function importChecksCore(params) {
103918
104565
  //#endregion
103919
104566
  export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, LETTACODE_DIR as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
103920
104567
 
103921
- //# sourceMappingURL=import-Tya9qZRK.js.map
104568
+ //# sourceMappingURL=import-BsEFYJsg.js.map