rulesync 29.0.0 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7861,6 +7861,26 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
7861
7861
  permission: z.optional(ToolScopedPermissionSchema),
7862
7862
  defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7863
7863
  });
7864
+ /**
7865
+ * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
7866
+ * before prompting. `always-ask` auto-approves only read-only tools, `write`
7867
+ * also workspace writes, and `yolo` (oh-my-pi's default) every tier.
7868
+ *
7869
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
7870
+ */
7871
+ const OMP_APPROVAL_MODES = [
7872
+ "always-ask",
7873
+ "write",
7874
+ "yolo"
7875
+ ];
7876
+ /**
7877
+ * Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
7878
+ * `tools.approvalMode` key of `config.yml`, which has no canonical category.
7879
+ */
7880
+ const OmpPermissionsOverrideSchema = z.looseObject({
7881
+ permission: z.optional(ToolScopedPermissionSchema),
7882
+ approvalMode: z.optional(z.enum(OMP_APPROVAL_MODES))
7883
+ });
7864
7884
  const KimiCodePermissionsOverrideSchema = z.looseObject({
7865
7885
  permission: z.optional(ToolScopedPermissionSchema),
7866
7886
  defaultPermissionMode: z.optional(z.enum([
@@ -7868,6 +7888,14 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
7868
7888
  "yolo",
7869
7889
  "auto"
7870
7890
  ])),
7891
+ /**
7892
+ * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
7893
+ * default `true`). `false` turns off the built-in dangerous-command policy
7894
+ * (no confirmation for `rm -rf`, `shutdown` and the like).
7895
+ *
7896
+ * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
7897
+ */
7898
+ dangerousCommandGuard: z.optional(z.boolean()),
7871
7899
  rules: z.optional(z.array(z.looseObject({
7872
7900
  decision: PermissionActionSchema,
7873
7901
  pattern: z.string(),
@@ -8083,7 +8111,10 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8083
8111
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8084
8112
  * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8085
8113
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8086
- * `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
8114
+ * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8115
+ * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
8116
+ * Code unions across scopes) and `general` (`outputStyle`, the output style
8117
+ * that reshapes how responses are written). Qwen Code strips `tools.workflowsEnabled`,
8087
8118
  * `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
8088
8119
  * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
8089
8120
  * skips those four in project scope and announces a granting value in global
@@ -8100,7 +8131,8 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8100
8131
  * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8101
8132
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8102
8133
  * than merging with the list a higher scope set), the Auto Mode
8103
- * classifier config, and, because these groups are loose objects, any key
8134
+ * classifier config, the skill kill-switches, and, because these groups are
8135
+ * loose objects, any key
8104
8136
  * rulesync does not model. Import flags a scope-dependent key only when it read
8105
8137
  * the project file, since that is the value a `--global` regenerate would
8106
8138
  * promote. It also
@@ -8111,11 +8143,13 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8111
8143
  * matching `settings.json` group and emitted only for Qwen, while the shared
8112
8144
  * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
8113
8145
  * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
8114
- * `tools`/`security`/`autoMode` key can be authored.
8146
+ * `tools`/`security`/`skills`/`general`/`autoMode` key can be authored.
8115
8147
  *
8116
8148
  * @example
8117
8149
  * {
8118
8150
  * "tools": { "approvalMode": "auto-edit" },
8151
+ * "skills": { "disabled": ["pdf"], "disabledLevels": ["bundled"] },
8152
+ * "general": { "outputStyle": "Concise" },
8119
8153
  * "security": { "folderTrust": { "enabled": true } },
8120
8154
  * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
8121
8155
  * }
@@ -8124,6 +8158,8 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
8124
8158
  permission: z.optional(ToolScopedPermissionSchema),
8125
8159
  tools: z.optional(z.looseObject({})),
8126
8160
  security: z.optional(z.looseObject({})),
8161
+ skills: z.optional(z.looseObject({})),
8162
+ general: z.optional(z.looseObject({})),
8127
8163
  autoMode: z.optional(z.looseObject({}))
8128
8164
  });
8129
8165
  /**
@@ -8209,6 +8245,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
8209
8245
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
8210
8246
  * @example
8211
8247
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8248
+ * @example
8249
+ * { "mode": "deny" }
8212
8250
  */
8213
8251
  const ReasonixPermissionsOverrideSchema = z.looseObject({
8214
8252
  permission: z.optional(ToolScopedPermissionSchema),
@@ -8217,7 +8255,12 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
8217
8255
  rawAllow: z.optional(z.array(z.string())),
8218
8256
  rawAsk: z.optional(z.array(z.string())),
8219
8257
  rawDeny: z.optional(z.array(z.string())),
8220
- allowDynamicBash: z.optional(z.boolean())
8258
+ allowDynamicBash: z.optional(z.boolean()),
8259
+ mode: z.optional(z.enum([
8260
+ "ask",
8261
+ "allow",
8262
+ "deny"
8263
+ ]))
8221
8264
  });
8222
8265
  /**
8223
8266
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -8260,7 +8303,8 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({
8260
8303
  * {
8261
8304
  * "agent_mode_coding_permissions": "always_allow_reading",
8262
8305
  * "agent_mode_execute_readonly_commands": true,
8263
- * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
8306
+ * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] },
8307
+ * "auto_approve_bypasses_command_denylist": false
8264
8308
  * }
8265
8309
  */
8266
8310
  /**
@@ -8312,6 +8356,16 @@ const WarpExecutionProfileOverrideSchema = z.looseObject({
8312
8356
  mcp_allowlist: z.optional(z.array(z.string())),
8313
8357
  mcp_denylist: z.optional(z.array(z.string()))
8314
8358
  });
8359
+ /**
8360
+ * Whether auto-approve may run commands that match the command denylist
8361
+ * (Warp defaults to `true`). It is not an execution-profile field: it lives in
8362
+ * the sibling `[agents.warp_agent.other]` table of the same `settings.toml`,
8363
+ * shared by the Warp app and the Warp Agent CLI.
8364
+ *
8365
+ * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8366
+ * @see https://github.com/warpdotdev/warp/blob/main/app/src/settings/ai.rs (`AutoApproveBypassesCommandDenylist`)
8367
+ */
8368
+ const WarpAutoApproveBypassSchema = z.optional(z.boolean());
8315
8369
  const WarpPermissionsOverrideSchema = z.looseObject({
8316
8370
  permission: z.optional(ToolScopedPermissionSchema),
8317
8371
  agent_mode_coding_permissions: z.optional(z.enum([
@@ -8321,7 +8375,8 @@ const WarpPermissionsOverrideSchema = z.looseObject({
8321
8375
  ])),
8322
8376
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
8323
8377
  agent_mode_execute_readonly_commands: z.optional(z.boolean()),
8324
- execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
8378
+ execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
8379
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8325
8380
  });
8326
8381
  /**
8327
8382
  * Tool-scoped override block for the standalone Warp Agent CLI. The CLI reads
@@ -8330,16 +8385,21 @@ const WarpPermissionsOverrideSchema = z.looseObject({
8330
8385
  * the app's legacy `[agents.profiles]` keys), so only the nested
8331
8386
  * `execution_profile` block is offered; it is merged into that record, and the
8332
8387
  * shared `permission.bash` block still drives `command_allowlist` /
8333
- * `command_denylist`.
8388
+ * `command_denylist`. `auto_approve_bypasses_command_denylist` is written to
8389
+ * the sibling `[agents.warp_agent.other]` table.
8334
8390
  *
8335
8391
  * @example
8336
- * { "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" } }
8392
+ * {
8393
+ * "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" },
8394
+ * "auto_approve_bypasses_command_denylist": false
8395
+ * }
8337
8396
  *
8338
8397
  * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8339
8398
  */
8340
8399
  const WarpcliPermissionsOverrideSchema = z.looseObject({
8341
8400
  permission: z.optional(ToolScopedPermissionSchema),
8342
- execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
8401
+ execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
8402
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8343
8403
  });
8344
8404
  /**
8345
8405
  * deepagents-cli's approval-mode knobs under `[startup]` in
@@ -9035,7 +9095,7 @@ const PermissionsConfigSchema = z.looseObject({
9035
9095
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
9036
9096
  crush: z.optional(CanonicalPermissionsOverrideSchema),
9037
9097
  goose: z.optional(CanonicalPermissionsOverrideSchema),
9038
- omp: z.optional(CanonicalPermissionsOverrideSchema),
9098
+ omp: z.optional(OmpPermissionsOverrideSchema),
9039
9099
  pool: z.optional(CanonicalPermissionsOverrideSchema),
9040
9100
  qoder: z.optional(CanonicalPermissionsOverrideSchema),
9041
9101
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
@@ -12745,7 +12805,9 @@ const SHARED_CONFIG_OWNERSHIP = {
12745
12805
  ownedKeys: [
12746
12806
  "permissions",
12747
12807
  "tools",
12748
- "security"
12808
+ "security",
12809
+ "skills",
12810
+ "general"
12749
12811
  ]
12750
12812
  }
12751
12813
  }
@@ -13115,7 +13177,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13115
13177
  features: {
13116
13178
  mcp: {
13117
13179
  kind: "replace-owned-keys",
13118
- ownedKeys: ["mcp_servers"]
13180
+ ownedKeys: ["mcp_servers", "disabled_mcp_tools"]
13119
13181
  },
13120
13182
  permissions: {
13121
13183
  kind: "replace-owned-keys",
@@ -13250,6 +13312,14 @@ function serializeSharedConfigFile({ fileKey, document, existingContent, filePat
13250
13312
  });
13251
13313
  }
13252
13314
  /**
13315
+ * A patch value for an owned key that holds a map of managed entries: the map
13316
+ * itself, or `undefined` when it is empty, which retracts the key instead of
13317
+ * leaving an empty object behind (`applySharedConfigPatch` deletes a key whose patch value is `undefined`).
13318
+ */
13319
+ function undefinedWhenEmpty(entries) {
13320
+ return Object.keys(entries).length > 0 ? entries : void 0;
13321
+ }
13322
+ /**
13253
13323
  * Execute a feature's declared write to a gateway-managed shared file: parse
13254
13324
  * the existing content, merge the patch under the feature's declared policy,
13255
13325
  * and serialize it back over the existing content (see
@@ -13736,14 +13806,23 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
13736
13806
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
13737
13807
  const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
13738
13808
  const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
13809
+ const refusedDirs = /* @__PURE__ */ new Map();
13739
13810
  let removedCount = 0;
13740
13811
  for (const aiFile of orphanFiles) {
13741
13812
  const filePath = aiFile.getFilePath();
13742
- if (await refusesDeleteOutsideRoot({
13743
- logger: this.logger,
13744
- rootPath: aiFile.getOutputRoot(),
13745
- filePath
13746
- })) continue;
13813
+ const rootPath = aiFile.getOutputRoot();
13814
+ const dirPath = dirname(filePath);
13815
+ const cacheKey = JSON.stringify([rootPath, dirPath]);
13816
+ let refused = refusedDirs.get(cacheKey);
13817
+ if (refused === void 0) {
13818
+ refused = await refusesDeleteOutsideRoot({
13819
+ logger: this.logger,
13820
+ rootPath,
13821
+ dirPath
13822
+ });
13823
+ refusedDirs.set(cacheKey, refused);
13824
+ }
13825
+ if (refused) continue;
13747
13826
  const loggedPath = stripControlCharacters(filePath);
13748
13827
  if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
13749
13828
  else {
@@ -13864,22 +13943,24 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
13864
13943
  return true;
13865
13944
  }
13866
13945
  /**
13867
- * Whether deleting `filePath` has to be refused because a link on the way to
13868
- * the directory holding it leads out of `rootPath`, warning about the refusal.
13946
+ * Whether deleting the files in `dirPath` has to be refused because a link on
13947
+ * the way to that directory leads out of `rootPath`, warning about the refusal
13948
+ * once for the whole directory.
13869
13949
  *
13870
13950
  * Deletion candidates are enumerated without following links *inside* the
13871
13951
  * scanned directory, but the scanned directory itself is reached through
13872
13952
  * whatever links its path contains: a checked-out repository can carry
13873
13953
  * `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
13874
- * an orphan. Only the parent is judged, because removing a file that is itself
13875
- * a link unlinks the link where it is spelled and never touches its target.
13954
+ * an orphan. Only the directory is judged, never the file in it, because
13955
+ * removing a file that is itself a link unlinks the link where it is spelled
13956
+ * and never touches its target.
13876
13957
  */
13877
- async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
13958
+ async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath }) {
13878
13959
  if (!await writablePathEscapesRoot({
13879
13960
  rootPath,
13880
- targetPath: dirname(filePath)
13961
+ targetPath: dirPath
13881
13962
  })) return false;
13882
- logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13963
+ logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13883
13964
  return true;
13884
13965
  }
13885
13966
  /**
@@ -17202,7 +17283,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
17202
17283
  static getProjectRelativeDirPath() {
17203
17284
  throw new Error("Please implement this method in the subclass.");
17204
17285
  }
17205
- /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/antigravity/global_workflows`). */
17286
+ /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/config/workflows`). */
17206
17287
  static getGlobalRelativeDirPath() {
17207
17288
  throw new Error("Please implement this method in the subclass.");
17208
17289
  }
@@ -17365,7 +17446,8 @@ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
17365
17446
  const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
17366
17447
  const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
17367
17448
  const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = join(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
17368
- const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17449
+ const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "workflows");
17450
+ const ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17369
17451
  const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
17370
17452
  const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
17371
17453
  //#endregion
@@ -17395,10 +17477,12 @@ var AntigravityIdeSkill = class extends AntigravitySharedSkill {
17395
17477
  * Emits each command as a skill in `.agents/skills/<name>/SKILL.md` (project
17396
17478
  * scope) and `~/.gemini/config/skills/<name>/SKILL.md` (global scope), the
17397
17479
  * IDE's skills directories. Legacy workflows are still imported from
17398
- * `.agents/workflows/` and `~/.gemini/antigravity/global_workflows/`. All
17399
- * body and frontmatter handling is shared with {@link AntigravitySharedCommand}.
17480
+ * `.agents/workflows/` and `~/.gemini/config/workflows/` (the global workflows
17481
+ * directory of Antigravity 2.0), and, in global scope only, from the pre-2.0
17482
+ * `~/.gemini/antigravity/global_workflows/`. All body and frontmatter handling
17483
+ * is shared with {@link AntigravitySharedCommand}.
17400
17484
  */
17401
- var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17485
+ var AntigravityIdeCommand = class AntigravityIdeCommand extends AntigravitySharedCommand {
17402
17486
  static getProjectRelativeDirPath() {
17403
17487
  return ANTIGRAVITY_IDE_COMMANDS_DIR_PATH;
17404
17488
  }
@@ -17408,6 +17492,37 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17408
17492
  static getSkillsRelativeDirPath({ global }) {
17409
17493
  return AntigravityIdeSkill.getSettablePaths({ global }).relativeDirPath;
17410
17494
  }
17495
+ /**
17496
+ * Import-only, global scope: the workflows left in the pre-2.0
17497
+ * `~/.gemini/antigravity/global_workflows/` directory, where rulesync also
17498
+ * wrote IDE global commands before they were emitted as skills. The
17499
+ * directory is never swept by `generate --delete`; a workflow with the same
17500
+ * file name in `~/.gemini/config/workflows/` takes precedence.
17501
+ *
17502
+ * @see https://antigravity.google/docs/migration/workflows-to-skills
17503
+ */
17504
+ static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
17505
+ if (!global) return [];
17506
+ const filePaths = await findFilesByGlobs("*.md", { cwd: join(outputRoot, ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH) });
17507
+ return (await Promise.all(filePaths.map(async (filePath) => {
17508
+ const fileContent = await readFileContent(filePath);
17509
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
17510
+ const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter);
17511
+ if (!result.success) {
17512
+ logger?.warn(`Skipping ${filePath}: invalid frontmatter (${formatError(result.error)}).`);
17513
+ return;
17514
+ }
17515
+ return new AntigravityIdeCommand({
17516
+ outputRoot,
17517
+ relativeDirPath: ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH,
17518
+ relativeFilePath: basename(filePath),
17519
+ frontmatter: result.data,
17520
+ body: body.trim(),
17521
+ fileContent,
17522
+ global
17523
+ });
17524
+ }))).filter((command) => command !== void 0);
17525
+ }
17411
17526
  getToolTargetName() {
17412
17527
  return "antigravity-ide";
17413
17528
  }
@@ -25060,7 +25175,7 @@ function findCommandPaths({ command, scan }) {
25060
25175
  } else {
25061
25176
  if (path !== void 0) toAnchor.add(path);
25062
25177
  state.owner = text.slice(text.lastIndexOf("/") + 1);
25063
- state.expecting = afterCommandWord(state.owner);
25178
+ state.expecting = afterCommandWord$1(state.owner);
25064
25179
  state.prefix = void 0;
25065
25180
  state.shellCode = false;
25066
25181
  }
@@ -25126,7 +25241,7 @@ function readOption({ state, text }) {
25126
25241
  if (SHELL_NAME_PATTERN.test(state.owner) && SHELL_CODE_OPTION.test(text)) state.shellCode = true;
25127
25242
  }
25128
25243
  /** What follows a command word whose basename is `name`. */
25129
- function afterCommandWord(name) {
25244
+ function afterCommandWord$1(name) {
25130
25245
  if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
25131
25246
  if (PACKAGE_RUNNERS.has(name)) return "wrapped";
25132
25247
  return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
@@ -33469,6 +33584,19 @@ function isQwencodeLegacyMillisecondTimeout(timeout) {
33469
33584
  return typeof timeout === "number" && Number.isFinite(timeout) && timeout >= QWENCODE_LEGACY_MILLISECOND_TIMEOUT_THRESHOLD;
33470
33585
  }
33471
33586
  /**
33587
+ * Read a `timeout` the way Qwen Code does. Settings files are not type-checked
33588
+ * and Qwen Code coerces a numeric string such as `"60000"` with `Number()`, so
33589
+ * import accepts one too; a value that is not a finite number is dropped, since
33590
+ * Qwen Code falls back to the hook type's default for it.
33591
+ * https://github.com/QwenLM/qwen-code/blob/main/packages/core/src/hooks/hook-timeout.ts
33592
+ */
33593
+ function readQwencodeTimeout(timeout) {
33594
+ if (timeout === void 0) return void 0;
33595
+ if (typeof timeout === "string" && timeout.trim() === "") return void 0;
33596
+ const value = Number(timeout);
33597
+ return Number.isFinite(value) ? value : void 0;
33598
+ }
33599
+ /**
33472
33600
  * Canonical `timeout` is seconds. A command hook value below the threshold is
33473
33601
  * already read as seconds; a larger one can only be expressed in milliseconds.
33474
33602
  */
@@ -33579,7 +33707,7 @@ const QwencodeHookEntrySchema = z.looseObject({
33579
33707
  type: z.optional(z.string()),
33580
33708
  command: z.optional(z.string()),
33581
33709
  url: z.optional(z.string()),
33582
- timeout: z.optional(z.number()),
33710
+ timeout: z.optional(z.union([z.number(), z.string()])),
33583
33711
  name: z.optional(z.string()),
33584
33712
  description: z.optional(z.string()),
33585
33713
  statusMessage: z.optional(z.string()),
@@ -33612,12 +33740,13 @@ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
33612
33740
  const isCommand = hookType === "command";
33613
33741
  const isPrompt = hookType === "prompt";
33614
33742
  const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
33743
+ const timeout = readQwencodeTimeout(h.timeout);
33615
33744
  return {
33616
33745
  type: hookType,
33617
33746
  ...compact({
33618
33747
  command: h.command,
33619
33748
  url: h.url,
33620
- timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(h.timeout) : h.timeout,
33749
+ timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(timeout) : timeout,
33621
33750
  name: h.name,
33622
33751
  description: h.description,
33623
33752
  statusMessage: h.statusMessage,
@@ -38119,7 +38248,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
38119
38248
  fileKey: sharedConfigFileKey(basePaths),
38120
38249
  feature: "mcp",
38121
38250
  existingContent: fileContent ?? "",
38122
- patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
38251
+ patch: { [AMP_MCP_SERVERS_KEY]: undefinedWhenEmpty(filterMcpServers(rulesyncMcp.getMcpServers())) },
38123
38252
  filePath: join(jsonDir, relativeFilePath),
38124
38253
  logger
38125
38254
  }),
@@ -38417,6 +38546,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38417
38546
  const paths = this.getSettablePaths({ global });
38418
38547
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
38419
38548
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
38549
+ const mcpServers = rulesyncMcp.getMcpServers();
38420
38550
  return new AugmentcodeMcp({
38421
38551
  outputRoot,
38422
38552
  relativeDirPath: paths.relativeDirPath,
@@ -38425,7 +38555,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38425
38555
  fileKey: sharedConfigFileKey(paths),
38426
38556
  feature: "mcp",
38427
38557
  existingContent,
38428
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
38558
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
38429
38559
  filePath,
38430
38560
  logger
38431
38561
  }),
@@ -38981,6 +39111,229 @@ var ClaudecodeMcp = class ClaudecodeMcp extends ToolMcp {
38981
39111
  }
38982
39112
  };
38983
39113
  //#endregion
39114
+ //#region src/features/mcp/claudecode-plugin-mcp.ts
39115
+ /**
39116
+ * The placeholder Claude Code substitutes with the installed plugin's own
39117
+ * directory in an MCP server's `command`, `args` and `env`.
39118
+ * @see https://code.claude.com/docs/en/plugins-reference
39119
+ */
39120
+ const PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
39121
+ const PLUGIN_ROOT_PREFIX = /^(?:\$\{CLAUDE_PLUGIN_ROOT\}|"\$CLAUDE_PLUGIN_ROOT"|\$CLAUDE_PLUGIN_ROOT)\//;
39122
+ /**
39123
+ * Whether a server `command` names a file inside the plugin rather than an
39124
+ * executable found on `PATH`. The command is spawned without a shell, so a
39125
+ * name with a slash is a path, and one without (`npx`, `node`, `uvx`) is a
39126
+ * `PATH` lookup. An absolute path, a home path, one that leaves the plugin
39127
+ * (`../`), and one that already starts with a variable are left as written.
39128
+ */
39129
+ function isPluginRelativeCommand(command) {
39130
+ return /[\\/]/.test(command) && !command.startsWith("$") && !command.startsWith("%") && !command.startsWith("~") && !/^\.\.[\\/]/.test(command) && !posix.isAbsolute(command) && !win32.isAbsolute(command);
39131
+ }
39132
+ function anchorToPluginRoot(path) {
39133
+ return `${PLUGIN_ROOT_PLACEHOLDER}/${path.replace(/^\.[\\/]/, "")}`;
39134
+ }
39135
+ /** The runner name of a command word: its basename without a `.exe` suffix. */
39136
+ function runnerName(command) {
39137
+ return (command.split(/[\\/]/).pop() ?? command).replace(/\.exe$/i, "");
39138
+ }
39139
+ /** What follows a command word, as plugin hook commands read it. */
39140
+ function afterCommandWord(name) {
39141
+ if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
39142
+ if (PACKAGE_RUNNERS.has(name)) return "command";
39143
+ return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
39144
+ }
39145
+ /**
39146
+ * The positions in `args` that name a file the server runs, read with the
39147
+ * rules plugin hook commands follow (`hook-command-paths.ts`): the script of a
39148
+ * script runner (`node ./dist/server.js`), the command after a run subcommand
39149
+ * or package runner (`uv run ./server.py`, `npx tsx ./x.ts`), and the value of
39150
+ * an option that loads a file (`node -r ./register.js`). Only a position whose
39151
+ * value `isPath` accepts is returned. Everything else is data the server reads
39152
+ * relative to the consumer's project (`server-filesystem ./docs`), and the
39153
+ * scan stops at an option that runs inline code or standard input
39154
+ * (`node -e`, `python3 -m`, `sh -c`, `bash -s`).
39155
+ */
39156
+ function fileArgumentIndices({ command, args, isPath }) {
39157
+ const indices = /* @__PURE__ */ new Set();
39158
+ let owner = runnerName(command);
39159
+ let expecting = afterCommandWord(owner);
39160
+ for (let index = 0; index < args.length && expecting !== "none"; index++) {
39161
+ const arg = args[index];
39162
+ if (typeof arg !== "string") break;
39163
+ if (arg.startsWith("-") && arg !== "-") {
39164
+ if (optionsFor({
39165
+ table: CODE_OPTIONS,
39166
+ name: owner
39167
+ })?.has(arg) || SHELL_NAME_PATTERN.test(owner) && SHELL_CODE_OPTION.test(arg)) break;
39168
+ if (optionsFor({
39169
+ table: VALUE_OPTIONS,
39170
+ name: owner
39171
+ })?.has(arg)) {
39172
+ const value = args[index + 1];
39173
+ if (typeof value === "string" && isPath(value)) indices.add(index + 1);
39174
+ index++;
39175
+ }
39176
+ continue;
39177
+ }
39178
+ if (expecting === "subcommand") {
39179
+ if (RUN_SUBCOMMANDS[owner]?.has(arg)) {
39180
+ expecting = "command";
39181
+ continue;
39182
+ }
39183
+ if (!SCRIPT_RUNNER_PATTERN.test(owner)) break;
39184
+ expecting = "script";
39185
+ }
39186
+ if (isPath(arg)) indices.add(index);
39187
+ if (expecting === "script") break;
39188
+ owner = runnerName(arg);
39189
+ expecting = afterCommandWord(owner);
39190
+ }
39191
+ return indices;
39192
+ }
39193
+ function isDotRelative(value) {
39194
+ return /^\.[\\/]/.test(value);
39195
+ }
39196
+ /** Turn a plugin-root path back into its `./` form, or return it unchanged. */
39197
+ function relativizeFromPluginRoot(value) {
39198
+ return PLUGIN_ROOT_PREFIX.test(value) ? value.replace(PLUGIN_ROOT_PREFIX, "./") : value;
39199
+ }
39200
+ /** The command and arguments of a stdio server, with inline array arguments split out. */
39201
+ function splitCommand(server) {
39202
+ if (Array.isArray(server.command)) {
39203
+ const [first, ...rest] = server.command;
39204
+ return {
39205
+ command: typeof first === "string" ? first : void 0,
39206
+ args: [...rest, ...Array.isArray(server.args) ? server.args : []]
39207
+ };
39208
+ }
39209
+ return {
39210
+ command: typeof server.command === "string" ? server.command : void 0,
39211
+ args: Array.isArray(server.args) ? server.args : []
39212
+ };
39213
+ }
39214
+ /**
39215
+ * Rewrite a server's command and the argument positions that name a file it
39216
+ * runs (see `fileArgumentIndices`), leaving every other argument as written.
39217
+ * Generate and import share this, so a position generate never anchors is
39218
+ * never relativized on import either, and a hand-written
39219
+ * `${CLAUDE_PLUGIN_ROOT}/…` data argument keeps its meaning.
39220
+ */
39221
+ function rewriteServer({ server, rewriteCommand, isPath, rewritePath }) {
39222
+ const { command, args } = splitCommand(server);
39223
+ if (command === void 0) return server;
39224
+ const indices = fileArgumentIndices({
39225
+ command,
39226
+ args,
39227
+ isPath
39228
+ });
39229
+ const rewriteArgs = (values, offset) => values.map((value, index) => indices.has(index + offset) && typeof value === "string" ? rewritePath(value) : value);
39230
+ const next = { ...server };
39231
+ if (Array.isArray(server.command)) {
39232
+ const inlineArgs = server.command.slice(1);
39233
+ next.command = [rewriteCommand(command), ...rewriteArgs(inlineArgs, 0)];
39234
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, inlineArgs.length);
39235
+ } else {
39236
+ next.command = rewriteCommand(command);
39237
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, 0);
39238
+ }
39239
+ return next;
39240
+ }
39241
+ /** Anchor a server's plugin-relative command and the files its runner executes. */
39242
+ function anchorServer(server) {
39243
+ return rewriteServer({
39244
+ server,
39245
+ rewriteCommand: (command) => isPluginRelativeCommand(command) ? anchorToPluginRoot(command) : command,
39246
+ isPath: isDotRelative,
39247
+ rewritePath: anchorToPluginRoot
39248
+ });
39249
+ }
39250
+ /** Turn the plugin-root forms generate writes back into `./`. */
39251
+ function relativizeServer(server) {
39252
+ return rewriteServer({
39253
+ server,
39254
+ rewriteCommand: relativizeFromPluginRoot,
39255
+ isPath: (value) => PLUGIN_ROOT_PREFIX.test(value),
39256
+ rewritePath: relativizeFromPluginRoot
39257
+ });
39258
+ }
39259
+ /** Apply a per-server transform to every server entry that is an object. */
39260
+ function mapServers({ mcpServers, transform }) {
39261
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return mcpServers;
39262
+ return Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => server === null || typeof server !== "object" || Array.isArray(server) ? [name, server] : [name, transform(server)]));
39263
+ }
39264
+ /**
39265
+ * MCP servers bundled in a Claude Code plugin (`.mcp.json` at the plugin
39266
+ * root).
39267
+ *
39268
+ * Claude Code loads a plugin's `.mcp.json` with the consumer's project as the
39269
+ * working directory, so a relative `command` such as `./servers/db-server`
39270
+ * would be looked up in the consumer's repository, where the bundled binary
39271
+ * does not exist. Generate therefore rewrites a relative `command`, and the
39272
+ * `./` script a script runner executes (`node ./dist/server.js`,
39273
+ * `uv run ./server.py`), to `${CLAUDE_PLUGIN_ROOT}/…`, which Claude Code
39274
+ * substitutes with the installed plugin's directory. Other `./` arguments are
39275
+ * data the server reads relative to the consumer's project
39276
+ * (`server-filesystem ./docs`), so they are left as written, the same rule
39277
+ * plugin hook commands follow. Bare executables (`npx`, `node`), absolute paths
39278
+ * and values that already start with a variable pass through. `env` values and
39279
+ * the remote transports' `url` / `headers` are left as written, because
39280
+ * rulesync cannot tell a path from an opaque string there. Import turns the
39281
+ * plugin-root forms in `command` and `args` back into `./`, including the
39282
+ * shell-variable spellings a hand-written file may have copied from a hook
39283
+ * command (which Claude Code does not substitute in MCP config).
39284
+ *
39285
+ * @see https://code.claude.com/docs/en/plugins-reference
39286
+ */
39287
+ var ClaudecodePluginMcp = class ClaudecodePluginMcp extends ClaudecodeMcp {
39288
+ static async fromRulesyncMcp(params) {
39289
+ const base = await super.fromRulesyncMcp(params);
39290
+ const json = base.getJson();
39291
+ const mcpServers = mapServers({
39292
+ mcpServers: json.mcpServers,
39293
+ transform: anchorServer
39294
+ });
39295
+ return new ClaudecodePluginMcp({
39296
+ outputRoot: params.outputRoot ?? process.cwd(),
39297
+ relativeDirPath: base.getRelativeDirPath(),
39298
+ relativeFilePath: base.getRelativeFilePath(),
39299
+ fileContent: JSON.stringify({
39300
+ ...json,
39301
+ mcpServers
39302
+ }, null, 2),
39303
+ validate: params.validate ?? true,
39304
+ global: params.global ?? false
39305
+ });
39306
+ }
39307
+ static async fromFile(params) {
39308
+ const base = await super.fromFile(params);
39309
+ return new ClaudecodePluginMcp({
39310
+ outputRoot: params.outputRoot ?? process.cwd(),
39311
+ relativeDirPath: base.getRelativeDirPath(),
39312
+ relativeFilePath: base.getRelativeFilePath(),
39313
+ fileContent: base.getFileContent(),
39314
+ validate: params.validate ?? true,
39315
+ global: params.global ?? false
39316
+ });
39317
+ }
39318
+ toRulesyncMcp() {
39319
+ const mcpServers = mapServers({
39320
+ mcpServers: this.getJson().mcpServers ?? {},
39321
+ transform: relativizeServer
39322
+ });
39323
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
39324
+ }
39325
+ static forDeletion(params) {
39326
+ return new ClaudecodePluginMcp({
39327
+ outputRoot: params.outputRoot ?? process.cwd(),
39328
+ relativeDirPath: params.relativeDirPath,
39329
+ relativeFilePath: params.relativeFilePath,
39330
+ fileContent: "{}",
39331
+ validate: false,
39332
+ global: params.global ?? false
39333
+ });
39334
+ }
39335
+ };
39336
+ //#endregion
38984
39337
  //#region src/features/mcp/cline-mcp.ts
38985
39338
  const CLINE_GLOBAL_ONLY_MESSAGE = "Cline MCP is global-only; use --global to sync ~/.cline/data/settings/cline_mcp_settings.json";
38986
39339
  function parseClineSettings(fileContent, relativeDirPath, relativeFilePath) {
@@ -41690,7 +42043,7 @@ var CrushMcp = class CrushMcp extends ToolMcp {
41690
42043
  fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
41691
42044
  feature: "mcp",
41692
42045
  existingContent,
41693
- patch: { ["mcp"]: converted },
42046
+ patch: { ["mcp"]: undefinedWhenEmpty(converted) },
41694
42047
  filePath: location.filePath,
41695
42048
  logger
41696
42049
  }),
@@ -43270,13 +43623,23 @@ var GooseMcp = class GooseMcp extends ToolMcp {
43270
43623
  //#region src/features/mcp/grokcli-mcp.ts
43271
43624
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
43272
43625
  /**
43626
+ * Grok keeps per-server MCP tool deny lists in a top-level
43627
+ * `[disabled_mcp_tools]` table (`map<server, string[]>` of unqualified tool
43628
+ * names), not inside `[mcp_servers.<name>]`. Grok reads it from the user
43629
+ * `~/.grok/config.toml` only: a project `.grok/config.toml` contributes just
43630
+ * `[mcp_servers]`, `[plugins]`, `[permission]` and `[mcp] max_output_bytes`.
43631
+ * https://github.com/xai-org/grok-build/blob/2bdd1d6a/crates/codegen/xai-grok-pager/docs/user-guide/26-config-reference.md
43632
+ */
43633
+ const GROK_DISABLED_MCP_TOOLS_KEY = "disabled_mcp_tools";
43634
+ /**
43273
43635
  * Grok Build stores MCP servers in `config.toml` under a `[mcp_servers.<name>]`
43274
43636
  * table. Verified against `grok mcp add` (grok 0.2.54): a stdio server emits
43275
43637
  * `command`, `args`, `enabled = true`, and an `[mcp_servers.<name>.env]` table;
43276
43638
  * a remote server emits `url` and `enabled`. Unlike Codex CLI, Grok uses a
43277
- * literal `env` table (not the `env_vars` passthrough list) and has no
43278
- * per-server tool allow/deny lists, so the only field rename is
43279
- * `disabled` (rulesync) ↔ `enabled = false` (grok).
43639
+ * literal `env` table (not the `env_vars` passthrough list), so the only field
43640
+ * rename is `disabled` (rulesync) ↔ `enabled = false` (grok). `disabledTools`
43641
+ * never lands in the server table: it is lifted into the top-level
43642
+ * `[disabled_mcp_tools]` table by `buildDisabledMcpTools`.
43280
43643
  */
43281
43644
  function convertToGrokFormat(mcpServers) {
43282
43645
  const result = {};
@@ -43285,7 +43648,7 @@ function convertToGrokFormat(mcpServers) {
43285
43648
  if (!isRecord$1(config)) continue;
43286
43649
  const converted = {};
43287
43650
  for (const [key, value] of Object.entries(config)) {
43288
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
43651
+ if (PROTOTYPE_POLLUTION_KEYS.has(key) || key === "disabledTools") continue;
43289
43652
  if (key === "disabled") {
43290
43653
  if (value === true) converted["enabled"] = false;
43291
43654
  } else converted[key] = value;
@@ -43309,6 +43672,38 @@ function convertFromGrokFormat(grokMcp) {
43309
43672
  }
43310
43673
  return result;
43311
43674
  }
43675
+ function toToolNameList(value) {
43676
+ if (!Array.isArray(value)) return [];
43677
+ return value.filter((tool) => typeof tool === "string");
43678
+ }
43679
+ /**
43680
+ * Recompute the global `[disabled_mcp_tools]` table: every server Rulesync
43681
+ * declares gets its `disabledTools` (or loses a stale entry when it has none —
43682
+ * Grok itself removes an entry whose list empties), while entries for other
43683
+ * servers — ones toggled in Grok's `/mcps` UI, plugin or compat servers, and
43684
+ * the `__managed_gateway_connectors` sentinel — are carried over untouched.
43685
+ * Returns `undefined` when nothing is left, so the key is retracted.
43686
+ */
43687
+ function buildDisabledMcpTools({ mcpServers, existingContent, filePath }) {
43688
+ const existing = parseSharedConfig({
43689
+ format: "toml",
43690
+ fileContent: existingContent,
43691
+ filePath
43692
+ })[GROK_DISABLED_MCP_TOOLS_KEY];
43693
+ const result = isRecord$1(existing) ? { ...existing } : {};
43694
+ for (const [name, config] of Object.entries(mcpServers)) {
43695
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
43696
+ const tools = toToolNameList(config.disabledTools);
43697
+ if (tools.length > 0) result[name] = tools;
43698
+ else delete result[name];
43699
+ }
43700
+ return Object.keys(result).length > 0 ? result : void 0;
43701
+ }
43702
+ function warnProjectDisabledTools(mcpServers, logger) {
43703
+ const serverNames = Object.entries(mcpServers).filter(([, config]) => isRecord$1(config) && config.disabledTools !== void 0).map(([name]) => name);
43704
+ if (serverNames.length === 0) return;
43705
+ warnWithFallback(logger, `grokcli reads per-server \`disabledTools\` only from the user ~/.grok/config.toml ([disabled_mcp_tools]); dropping it from ${serverNames.join(", ")} in project mode. Generate with --global to apply it.`);
43706
+ }
43312
43707
  var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43313
43708
  toml;
43314
43709
  constructor({ ...rest }) {
@@ -43345,16 +43740,19 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43345
43740
  relativeDirPath: paths.relativeDirPath,
43346
43741
  relativeFilePath: paths.relativeFilePath,
43347
43742
  fileContent,
43348
- validate
43743
+ validate,
43744
+ global
43349
43745
  });
43350
43746
  }
43351
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
43747
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
43352
43748
  const paths = this.getSettablePaths({ global });
43353
43749
  const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43354
43750
  const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
43355
- const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
43751
+ const strippedMcpServers = rulesyncMcp.getMcpServers();
43752
+ const converted = convertToGrokFormat(strippedMcpServers);
43356
43753
  const filteredMcpServers = this.removeEmptyEntries(converted);
43357
- for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
43754
+ if (!global) warnProjectDisabledTools(strippedMcpServers, logger);
43755
+ for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(logger, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
43358
43756
  return new GrokcliMcp({
43359
43757
  outputRoot,
43360
43758
  relativeDirPath: paths.relativeDirPath,
@@ -43363,14 +43761,31 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43363
43761
  fileKey: sharedConfigFileKey(paths),
43364
43762
  feature: "mcp",
43365
43763
  existingContent: configTomlFileContent,
43366
- patch: { mcp_servers: filteredMcpServers },
43764
+ patch: {
43765
+ mcp_servers: Object.keys(filteredMcpServers).length > 0 ? filteredMcpServers : void 0,
43766
+ ...global && { [GROK_DISABLED_MCP_TOOLS_KEY]: buildDisabledMcpTools({
43767
+ mcpServers: strippedMcpServers,
43768
+ existingContent: configTomlFileContent,
43769
+ filePath: configTomlFilePath
43770
+ }) }
43771
+ },
43367
43772
  filePath: configTomlFilePath
43368
43773
  }),
43369
- validate
43774
+ validate,
43775
+ global
43370
43776
  });
43371
43777
  }
43372
43778
  toRulesyncMcp() {
43373
43779
  const converted = convertFromGrokFormat(this.toml.mcp_servers ?? {});
43780
+ const disabledMcpTools = this.toml[GROK_DISABLED_MCP_TOOLS_KEY];
43781
+ if (this.global && isRecord$1(disabledMcpTools)) for (const [name, config] of Object.entries(converted)) {
43782
+ if (!Object.hasOwn(disabledMcpTools, name)) continue;
43783
+ const tools = toToolNameList(disabledMcpTools[name]);
43784
+ if (tools.length > 0) converted[name] = {
43785
+ ...config,
43786
+ disabledTools: tools
43787
+ };
43788
+ }
43374
43789
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
43375
43790
  }
43376
43791
  validate() {
@@ -46434,7 +46849,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
46434
46849
  fileKey: sharedConfigFileKey(paths),
46435
46850
  feature: "mcp",
46436
46851
  existingContent,
46437
- patch: { [POOL_MCP_SERVERS_KEY]: converted },
46852
+ patch: { [POOL_MCP_SERVERS_KEY]: undefinedWhenEmpty(converted) },
46438
46853
  filePath
46439
46854
  }),
46440
46855
  validate,
@@ -46564,7 +46979,7 @@ var QoderMcp = class QoderMcp extends ToolMcp {
46564
46979
  fileKey: sharedConfigFileKey(paths),
46565
46980
  feature: "mcp",
46566
46981
  existingContent,
46567
- patch: { mcpServers },
46982
+ patch: { mcpServers: undefinedWhenEmpty(mcpServers) },
46568
46983
  filePath
46569
46984
  }) : JSON.stringify({
46570
46985
  ...parseQoderMcpConfig({
@@ -46943,18 +47358,28 @@ function warnAboutRetiredFields({ name, serverRecord, logger }) {
46943
47358
  * `" Serial "` counts too and is written as authored.
46944
47359
  */
46945
47360
  const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
46946
- /**
46947
- * The type Reasonix decodes each fixed-type scalar passthrough field into
46948
- * (`PluginEntry` in `internal/config/plugin_entry.go`, and
46949
- * `internal/contract/config/plugin_entry.go` on the CLI v2 line).
46950
- */
46951
47361
  const REASONIX_TYPED_PLUGIN_FIELDS = {
47362
+ startup_timeout_seconds: "integer",
47363
+ call_timeout_seconds: "integer",
47364
+ tool_timeout_seconds: "integer table",
46952
47365
  concurrency: "string",
46953
47366
  auto_start: "boolean",
46954
47367
  load: "string",
46955
47368
  oauth_allow_missing_pkce_metadata: "boolean"
46956
47369
  };
46957
47370
  /**
47371
+ * Whether `value` is written as a TOML integer. smol-toml writes a JS number
47372
+ * outside the safe-integer range, or with a fraction, as a TOML float.
47373
+ */
47374
+ function isTomlInteger(value) {
47375
+ return typeof value === "number" && Number.isSafeInteger(value);
47376
+ }
47377
+ function hasExpectedType(value, expected) {
47378
+ if (expected === "integer") return isTomlInteger(value);
47379
+ if (expected === "integer table") return isRecord$1(value) && Object.values(value).every(isTomlInteger);
47380
+ return typeof value === expected;
47381
+ }
47382
+ /**
46958
47383
  * Why a passthrough value cannot be written, or `undefined` when it can (and
46959
47384
  * for every field without a fixed type). A wrong type is the case that matters:
46960
47385
  * a TOML type mismatch makes Reasonix refuse the whole config file. An unknown
@@ -46966,7 +47391,7 @@ const REASONIX_TYPED_PLUGIN_FIELDS = {
46966
47391
  */
46967
47392
  function invalidPassthroughFieldReason(field, value) {
46968
47393
  const expected = REASONIX_TYPED_PLUGIN_FIELDS[field];
46969
- if (expected !== void 0 && typeof value !== expected) return `Reasonix expects a ${expected} and fails to load a config file holding any other type.`;
47394
+ if (expected !== void 0 && !hasExpectedType(value, expected)) return `Reasonix expects ${expected === "integer" ? "an" : "a"} ${expected === "integer table" ? "table of integers" : expected} and fails to load a config file holding any other type.`;
46970
47395
  if (field === "concurrency" && typeof value === "string" && !REASONIX_CONCURRENCY_VALUES.has(value.trim().toLowerCase())) return `Reasonix accepts only "serial" or "parallel" and ignores ${JSON.stringify(value)}, falling back to the server-name default.`;
46971
47396
  }
46972
47397
  /**
@@ -47988,7 +48413,7 @@ var TabnineMcp = class TabnineMcp extends ToolMcp {
47988
48413
  fileKey: sharedConfigFileKey(paths),
47989
48414
  feature: "mcp",
47990
48415
  existingContent,
47991
- patch: { mcpServers: convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger) },
48416
+ patch: { mcpServers: undefinedWhenEmpty(convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger)) },
47992
48417
  filePath
47993
48418
  }),
47994
48419
  validate,
@@ -49309,6 +49734,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49309
49734
  const paths = this.getSettablePaths({ global });
49310
49735
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
49311
49736
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
49737
+ const contextServers = convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger);
49312
49738
  return new ZedMcp({
49313
49739
  outputRoot,
49314
49740
  relativeDirPath: paths.relativeDirPath,
@@ -49317,7 +49743,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49317
49743
  fileKey: sharedConfigFileKey(paths),
49318
49744
  feature: "mcp",
49319
49745
  existingContent,
49320
- patch: { context_servers: convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger) },
49746
+ patch: { context_servers: Object.keys(contextServers).length > 0 ? contextServers : void 0 },
49321
49747
  filePath
49322
49748
  }),
49323
49749
  validate
@@ -49450,7 +49876,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49450
49876
  }
49451
49877
  }],
49452
49878
  ["claudecode-plugin", {
49453
- class: ClaudecodeMcp,
49879
+ class: ClaudecodePluginMcp,
49454
49880
  meta: {
49455
49881
  supportsProject: true,
49456
49882
  supportsGlobal: false,
@@ -49626,7 +50052,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49626
50052
  supportsProject: true,
49627
50053
  supportsGlobal: true,
49628
50054
  supportsEnabledTools: false,
49629
- supportsDisabledTools: false
50055
+ supportsDisabledTools: true
49630
50056
  }
49631
50057
  }],
49632
50058
  ["hermesagent", {
@@ -50792,6 +51218,8 @@ function parsedGlobsIntersect(left, right, budget) {
50792
51218
  * @see https://antigravity.google/docs/permissions?tab=cli
50793
51219
  */
50794
51220
  const REGEX_PREFIX = "regex:";
51221
+ const TRAILING_ANCHOR = /(?<!\\)\$$/;
51222
+ const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
50795
51223
  const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
50796
51224
  const WHITESPACE_CODES = [
50797
51225
  9,
@@ -50932,6 +51360,7 @@ function replacePosixClasses(source) {
50932
51360
  function isInvalidRegex(source) {
50933
51361
  const unquoted = source.replace(RE2_QUOTED, (match) => match === "\\\\" ? match : "q");
50934
51362
  if (LOOKAROUND.test(unquoted) || hasRefusedEscape(unquoted)) return true;
51363
+ if (hasClassEscapeRangeEnd(unquoted)) return true;
50935
51364
  const withoutPosixClasses = replacePosixClasses(unquoted);
50936
51365
  if (withoutPosixClasses === void 0 || hasRefusedRepeat(withoutPosixClasses)) return true;
50937
51366
  const asJavaScript = withoutPosixClasses.replace(/\(\?P</g, "(?<").replace(/\(\?[imsU]*-?[imsU]*\)/g, "").replace(/\(\?[imsU]*-?[imsU]*:/g, "(?:").replace(/\\A/g, "^").replace(/\\z/g, "$");
@@ -50956,6 +51385,74 @@ function hasRefusedEscape(source) {
50956
51385
  }
50957
51386
  return false;
50958
51387
  }
51388
+ const CLASS_ESCAPE_LETTERS = /[dDsSwWpP]/;
51389
+ /** The length of the escape at `index`, braces (`\x{41}`, `\p{Greek}`) included. */
51390
+ function escapeLength(source, index) {
51391
+ const escaped = source[index + 1] ?? "";
51392
+ if (/[xpP]/.test(escaped) && source[index + 2] === "{") {
51393
+ const close = source.indexOf("}", index + 3);
51394
+ return close === -1 ? source.length - index : close - index + 1;
51395
+ }
51396
+ if (escaped === "p" || escaped === "P") return 3;
51397
+ return escaped === "x" ? 4 : 2;
51398
+ }
51399
+ /** Step past `^` and a leading `]` (a member) after the `[` that ends at `index`. */
51400
+ function openClass(source, start) {
51401
+ const index = source[start] === "^" ? start + 1 : start;
51402
+ return source[index] === "]" ? {
51403
+ index: index + 1,
51404
+ afterCharacter: true
51405
+ } : {
51406
+ index,
51407
+ afterCharacter: false
51408
+ };
51409
+ }
51410
+ /**
51411
+ * Whether a bracket range ends in a class escape (`[a-\d]`, `[--\s]`). Go
51412
+ * reads the end of a range as one character and refuses such a regex, while
51413
+ * JavaScript without the `u` flag reads it as three members. A class escape
51414
+ * that starts the range (`[\d-z]`) is a member followed by `-`, which both
51415
+ * accept. The `u` flag cannot be used instead: it also refuses RE2 spellings
51416
+ * such as `\x{41}` and `\-` outside a class.
51417
+ */
51418
+ function hasClassEscapeRangeEnd(source) {
51419
+ let inClass = false;
51420
+ let afterCharacter = false;
51421
+ let index = 0;
51422
+ while (index < source.length) {
51423
+ const character = source[index] ?? "";
51424
+ if (character === "\\") {
51425
+ afterCharacter = inClass && !CLASS_ESCAPE_LETTERS.test(source[index + 1] ?? "");
51426
+ index += escapeLength(source, index);
51427
+ continue;
51428
+ }
51429
+ if (!inClass) {
51430
+ inClass = character === "[";
51431
+ ({index, afterCharacter} = inClass ? openClass(source, index + 1) : {
51432
+ index: index + 1,
51433
+ afterCharacter: false
51434
+ });
51435
+ continue;
51436
+ }
51437
+ const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
51438
+ if (character === "]" || posixEnd !== -1) {
51439
+ inClass = character !== "]";
51440
+ afterCharacter = false;
51441
+ index = posixEnd === -1 ? index + 1 : posixEnd + 2;
51442
+ continue;
51443
+ }
51444
+ if (character === "-" && afterCharacter && source[index + 1] !== "]") {
51445
+ const end = index + 1;
51446
+ if (source[end] === "\\" && CLASS_ESCAPE_LETTERS.test(source[end + 1] ?? "")) return true;
51447
+ afterCharacter = false;
51448
+ index = end + (source[end] === "\\" ? escapeLength(source, end) : 1);
51449
+ continue;
51450
+ }
51451
+ afterCharacter = true;
51452
+ index += 1;
51453
+ }
51454
+ return false;
51455
+ }
50959
51456
  /**
50960
51457
  * Whether a repeat count is over Go's limit. Braces inside a bracket class
50961
51458
  * (`[a{1001}]`) and in `\x{2003}` or `\p{Greek}` are not repeats, so those
@@ -51103,7 +51600,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
51103
51600
  * the rule a regex.
51104
51601
  */
51105
51602
  function regexWordToGlob(word) {
51106
- const body = word.replace(/^\^/, "").replace(/(?<!\\)\$$/, "");
51603
+ const body = word.replace(/^\^/, "").replace(TRAILING_ANCHOR, "");
51107
51604
  const characters = Array.from(body);
51108
51605
  if (characters.length === 0) return;
51109
51606
  let glob = "";
@@ -51144,12 +51641,61 @@ function regexToGlob(source) {
51144
51641
  function fromAntigravityCommandTarget(target) {
51145
51642
  if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
51146
51643
  if (target === "*") return target;
51147
- if (/[*?[{\\]/.test(target)) {
51644
+ if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
51148
51645
  const words = splitWords(target);
51149
51646
  return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
51150
51647
  }
51151
51648
  return `${target} *`;
51152
51649
  }
51650
+ /**
51651
+ * Whether a regex word means the same anchored or not: each end is either
51652
+ * anchored (`^`, an unescaped `$`) or open-ended (`.*`).
51653
+ */
51654
+ function isAnchoredWord(word) {
51655
+ const startAnchored = word.startsWith("^") || word.startsWith(".*");
51656
+ const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
51657
+ return startAnchored && endAnchored;
51658
+ }
51659
+ /**
51660
+ * Explain what importing an Antigravity `command` target may lose, or return
51661
+ * `undefined` when the import is faithful.
51662
+ *
51663
+ * - A `regex:` target with a word that is not anchored is imported as a glob
51664
+ * that assumes anchoring. If Antigravity matches such a word unanchored,
51665
+ * the imported rule is narrower (`regex:rm` also matches `rmdir`).
51666
+ * - A plain target holding characters that are special in a glob is most
51667
+ * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
51668
+ * Antigravity matches it literally, so it imports as a regex that matches a
51669
+ * literal `*` word and is inert in the other tools it is exported to.
51670
+ */
51671
+ function describeAntigravityCommandImportLoss(target) {
51672
+ if (target.startsWith(REGEX_PREFIX)) {
51673
+ const source = target.slice(6);
51674
+ if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
51675
+ reason: "it has a regex word without `^...$` anchors and is imported as a glob that assumes Antigravity anchors every word, so the imported rule may be narrower than the original",
51676
+ denyConsequence: "The imported deny may block less than it does in Antigravity."
51677
+ };
51678
+ return;
51679
+ }
51680
+ if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
51681
+ reason: "it holds characters that are special in a glob (`*`, `?`, `[`, `{` or `\\`), which Antigravity matches literally (likely output of an older rulesync), so it is imported as a literal-match regex that no other tool enforces",
51682
+ denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
51683
+ };
51684
+ }
51685
+ /**
51686
+ * Import one Antigravity `command` target as a canonical bash pattern, warning
51687
+ * when the result may differ from what Antigravity enforces. Shared by the
51688
+ * Antigravity CLI and IDE importers.
51689
+ */
51690
+ function importAntigravityCommandTarget({ target, entry, decision, logger, toolLabel }) {
51691
+ const pattern = fromAntigravityCommandTarget(target);
51692
+ const loss = describeAntigravityCommandImportLoss(target);
51693
+ if (loss !== void 0) {
51694
+ const consequence = decision === "deny" ? ` ${loss.denyConsequence}` : "";
51695
+ logger.warn(`${toolLabel} permissions: importing ${entry} as "${pattern}": ${loss.reason}.${consequence}`);
51696
+ }
51697
+ return pattern;
51698
+ }
51153
51699
  //#endregion
51154
51700
  //#region src/features/permissions/antigravity-targets.ts
51155
51701
  /**
@@ -51180,6 +51726,7 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
51180
51726
  "**/*"
51181
51727
  ]);
51182
51728
  const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
51729
+ const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
51183
51730
  const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
51184
51731
  const DENY_HINTS = {
51185
51732
  read_file: "deny a directory or file path instead",
@@ -51262,6 +51809,10 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
51262
51809
  return;
51263
51810
  }
51264
51811
  if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
51812
+ if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
51813
+ const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
51814
+ logger.warn(`${toolLabel} permissions: ${rule}: \`~\` and environment variables may not be expanded in Antigravity path targets, so ${action}(${result.target}) may match nothing and ${consequence}; use an absolute path instead.`);
51815
+ }
51265
51816
  return result.target === "*" ? action : `${action}(${result.target})`;
51266
51817
  }
51267
51818
  //#endregion
@@ -51828,9 +52379,9 @@ function convertRulesyncToAntigravityCliPermissions({ config, logger }) {
51828
52379
  }
51829
52380
  }
51830
52381
  return {
51831
- allow,
51832
- ask,
51833
- deny
52382
+ allow: [...new Set(allow)],
52383
+ ask: [...new Set(ask)],
52384
+ deny: [...new Set(deny)]
51834
52385
  };
51835
52386
  }
51836
52387
  /**
@@ -51842,7 +52393,13 @@ function convertAntigravityCliToRulesyncPermissions(params) {
51842
52393
  for (const entry of entries) {
51843
52394
  const { toolName, pattern: target } = parsePermissionEntry$1(entry);
51844
52395
  const canonical = toCanonicalToolName$9(toolName);
51845
- const pattern = toolName === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52396
+ const pattern = toolName === "command" ? importAntigravityCommandTarget({
52397
+ target,
52398
+ entry,
52399
+ decision: action,
52400
+ logger: moduleLogger$4,
52401
+ toolLabel: "Antigravity CLI"
52402
+ }) : target;
51846
52403
  if (!permission[canonical]) permission[canonical] = {};
51847
52404
  permission[canonical][pattern] = action;
51848
52405
  }
@@ -52046,9 +52603,9 @@ function convertRulesyncToAntigravityIdePermissions({ config, logger }) {
52046
52603
  }
52047
52604
  }
52048
52605
  return {
52049
- allow,
52050
- ask,
52051
- deny
52606
+ allow: [...new Set(allow)],
52607
+ ask: [...new Set(ask)],
52608
+ deny: [...new Set(deny)]
52052
52609
  };
52053
52610
  }
52054
52611
  /**
@@ -52060,7 +52617,13 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
52060
52617
  for (const entry of entries) {
52061
52618
  const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
52062
52619
  const canonical = toCanonicalCategory$2(ideAction);
52063
- const pattern = ideAction === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52620
+ const pattern = ideAction === "command" ? importAntigravityCommandTarget({
52621
+ target,
52622
+ entry,
52623
+ decision: action,
52624
+ logger: fallbackLogger,
52625
+ toolLabel: "Antigravity IDE"
52626
+ }) : target;
52064
52627
  if (!permission[canonical]) permission[canonical] = {};
52065
52628
  permission[canonical][pattern] = action;
52066
52629
  }
@@ -54042,7 +54605,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54042
54605
  managedSourcesBehavior: "managed settings",
54043
54606
  modelPricing: "managed settings",
54044
54607
  parentSettingsBehavior: "managed settings",
54045
- permissionExplainerEnabled: "~/.claude.json",
54046
54608
  pluginSuggestionMarketplaces: "managed settings",
54047
54609
  pluginTrustMessage: "managed settings",
54048
54610
  prStatusFooterEnabled: "~/.claude.json",
@@ -54051,10 +54613,24 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54051
54613
  sshHostAllowlist: "managed settings",
54052
54614
  strictKnownMarketplaces: "managed settings",
54053
54615
  strictPluginOnlyCustomization: "managed settings",
54054
- teammateDefaultModel: "~/.claude.json",
54055
54616
  wslInheritsWindowsSettings: "managed settings"
54056
54617
  };
54057
54618
  /**
54619
+ * Top-level settings keys Claude Code has removed, with the version that
54620
+ * removed each. Current versions ignore them wherever they are set, so they
54621
+ * are dropped in **both** scopes, and the warning names the version so the
54622
+ * author knows to delete the key rather than move it to another file.
54623
+ *
54624
+ * Derived from the "Removed in" notes of the settings reference.
54625
+ *
54626
+ * @see https://code.claude.com/docs/en/settings-reference
54627
+ */
54628
+ const CLAUDECODE_REMOVED_KEYS = {
54629
+ permissionExplainerEnabled: "v2.1.257",
54630
+ taskOutputMaxChars: "v2.1.277",
54631
+ teammateDefaultModel: "v2.1.234"
54632
+ };
54633
+ /**
54058
54634
  * Top-level settings keys whose value Claude Code **executes**. The generic
54059
54635
  * passthrough refuses them outright rather than warning: `.rulesync/*` files
54060
54636
  * are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
@@ -54334,6 +54910,15 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
54334
54910
  allowedMarketplaces: "strictKnownMarketplaces"
54335
54911
  };
54336
54912
  /**
54913
+ * The warning for a key that neither scope can honor — one Claude Code has
54914
+ * removed, or one read only from a file rulesync does not generate — or
54915
+ * `undefined` when the key may be written in at least one scope.
54916
+ */
54917
+ function neverHonoredKeyWarning({ canonicalKey, shown, relativeFilePath }) {
54918
+ if (Object.hasOwn(CLAUDECODE_REMOVED_KEYS, canonicalKey)) return `Claude Code permissions: '${shown}' was removed in Claude Code ${CLAUDECODE_REMOVED_KEYS[canonicalKey]} and has no effect on current versions, so it is not written to ${relativeFilePath}. Delete it from .rulesync/permissions.jsonc, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`;
54919
+ if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) return `Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`;
54920
+ }
54921
+ /**
54337
54922
  * Copy of the authored top-level passthrough with the keys the target file
54338
54923
  * cannot honor removed, warning once per dropped key. Like
54339
54924
  * `stripSandboxPaths`, only the override copy is filtered — a value
@@ -54350,8 +54935,13 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
54350
54935
  logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
54351
54936
  continue;
54352
54937
  }
54353
- if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
54354
- logger?.warn(`Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
54938
+ const neverHonored = neverHonoredKeyWarning({
54939
+ canonicalKey,
54940
+ shown,
54941
+ relativeFilePath
54942
+ });
54943
+ if (neverHonored !== void 0) {
54944
+ logger?.warn(neverHonored);
54355
54945
  continue;
54356
54946
  }
54357
54947
  if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
@@ -62042,8 +62632,10 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
62042
62632
  * table. The gateway replaces the owned `permission` key wholesale and rulesync
62043
62633
  * only ever authors `rules`, so a hand-written sibling such as
62044
62634
  * `dangerous_command_guard = false` (Kimi Code 0.40.0) would otherwise be
62045
- * deleted on every generate. Rulesync does not model these keys; they pass
62046
- * through verbatim and `rules` always comes from the patch.
62635
+ * deleted on every generate. Unmodeled keys pass through verbatim; `rules`
62636
+ * always comes from the patch, and so does `dangerous_command_guard` when the
62637
+ * `kimi-code.dangerousCommandGuard` override sets it. Leaving the override out
62638
+ * keeps whatever the file already has.
62047
62639
  *
62048
62640
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
62049
62641
  */
@@ -62286,14 +62878,18 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62286
62878
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, logger }) {
62287
62879
  const config = rulesyncPermissions.getJson();
62288
62880
  const defaultPermissionMode = config["kimi-code"]?.defaultPermissionMode;
62881
+ const dangerousCommandGuard = config["kimi-code"]?.dangerousCommandGuard;
62289
62882
  const tools = buildKimiCodeToolsSection(config["kimi-code"]?.tools);
62290
62883
  const document = {
62291
62884
  ...defaultPermissionMode && { default_permission_mode: defaultPermissionMode },
62292
62885
  ...tools && { tools },
62293
- permission: { rules: canonicalToKimiCodeRules({
62294
- config,
62295
- logger
62296
- }) }
62886
+ permission: {
62887
+ ...typeof dangerousCommandGuard === "boolean" && { dangerous_command_guard: dangerousCommandGuard },
62888
+ rules: canonicalToKimiCodeRules({
62889
+ config,
62890
+ logger
62891
+ })
62892
+ }
62297
62893
  };
62298
62894
  let fileContent;
62299
62895
  try {
@@ -62315,11 +62911,14 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62315
62911
  format: "toml",
62316
62912
  fileContent: this.getFileContent()
62317
62913
  });
62318
- const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
62914
+ const permissionConfig = isRecord$1(config.permission) ? config.permission : {};
62915
+ const { permission, nativeRules } = preserveKimiCodeRules(permissionConfig.rules);
62319
62916
  const defaultPermissionMode = config.default_permission_mode;
62917
+ const dangerousCommandGuard = permissionConfig.dangerous_command_guard;
62320
62918
  const tools = buildKimiCodeToolsSection(config.tools);
62321
62919
  const toolOverride = {
62322
62920
  ...defaultPermissionMode === "manual" || defaultPermissionMode === "yolo" || defaultPermissionMode === "auto" ? { defaultPermissionMode } : {},
62921
+ ...typeof dangerousCommandGuard === "boolean" && { dangerousCommandGuard },
62323
62922
  ...nativeRules.length > 0 && { rules: nativeRules },
62324
62923
  ...tools && { tools }
62325
62924
  };
@@ -63201,6 +63800,7 @@ var MimocodePermissions = class extends OpencodePermissions {
63201
63800
  //#endregion
63202
63801
  //#region src/features/permissions/omp-permissions.ts
63203
63802
  const CATCH_ALL_PATTERN$4 = "*";
63803
+ const APPROVAL_MODE_KEY = "approvalMode";
63204
63804
  const ACTION_TO_OMP_APPROVAL = {
63205
63805
  allow: "allow",
63206
63806
  ask: "prompt",
@@ -63256,10 +63856,14 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
63256
63856
  * commands, and its deny / ask also restrict every other tool's policy (a
63257
63857
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
63258
63858
  *
63859
+ * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
63860
+ * `write` or `yolo`); without it the key is left as it is in the file, and on
63861
+ * import a recognized value is read back into the override.
63862
+ *
63259
63863
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
63260
- * (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
63261
- * such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
63262
- * never deleted.
63864
+ * (the keys rulesync manages), `bash.patterns` and an authored
63865
+ * `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
63866
+ * kept, and the file is never deleted.
63263
63867
  *
63264
63868
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
63265
63869
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
@@ -63302,16 +63906,19 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
63302
63906
  fileContent: existingContent,
63303
63907
  filePath
63304
63908
  });
63909
+ const config = rulesyncPermissions.getJson();
63305
63910
  const { approval, patterns, floor } = convertRulesyncToOmp({
63306
- config: rulesyncPermissions.getJson(),
63911
+ config,
63307
63912
  logger
63308
63913
  });
63914
+ const approvalMode = config.omp?.approvalMode;
63309
63915
  const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
63310
63916
  const nextApproval = {
63311
63917
  ...isRecord$1(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
63312
63918
  ...approval
63313
63919
  };
63314
63920
  const toolsSiblings = withoutKey(existingTools, "approval");
63921
+ if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
63315
63922
  const nextTools = Object.keys(nextApproval).length > 0 ? {
63316
63923
  ...toolsSiblings,
63317
63924
  approval: nextApproval
@@ -63567,7 +64174,14 @@ function convertOmpToRulesync(config) {
63567
64174
  permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
63568
64175
  }
63569
64176
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
63570
- return { permission };
64177
+ const approvalMode = tools[APPROVAL_MODE_KEY];
64178
+ return {
64179
+ permission,
64180
+ ...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
64181
+ };
64182
+ }
64183
+ function isOmpApprovalMode(value) {
64184
+ return OMP_APPROVAL_MODES.some((mode) => mode === value);
63571
64185
  }
63572
64186
  //#endregion
63573
64187
  //#region src/features/permissions/pi-permissions.ts
@@ -64607,6 +65221,13 @@ const QWEN_OVERRIDE_SECURITY_KEYS = [
64607
65221
  "allowPrivateNetworkHooks",
64608
65222
  "allowedInsecureVoiceBaseUrls"
64609
65223
  ];
65224
+ const QWEN_OVERRIDE_SKILLS_KEYS = [
65225
+ "disabled",
65226
+ "defaultDisabled",
65227
+ "enabled",
65228
+ "disabledLevels"
65229
+ ];
65230
+ const QWEN_OVERRIDE_GENERAL_KEYS = ["outputStyle"];
64610
65231
  /**
64611
65232
  * What each rule means for the two directions, so a key's behavior is declared
64612
65233
  * once beside its rule rather than spelled out at every branch.
@@ -64663,77 +65284,125 @@ const QWEN_SCOPE_RULES = {
64663
65284
  globalNote: "This is not a key rulesync models, so it cannot say what Qwen Code does with it — and some settings in these groups are potent (`tools.discoveryCommand` and `tools.callCommand` are spawned as commands when Qwen Code starts). Check what it now says for every project on this machine."
64664
65285
  }
64665
65286
  };
64666
- const QWEN_OVERRIDE_GROUPS = [{
64667
- groupName: "tools",
64668
- overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
64669
- scopedKeys: {
64670
- workflowsEnabled: { rule: "workspace-stripped" },
64671
- executionSandbox: {
64672
- rule: "workspace-stripped",
64673
- grants: () => true,
64674
- globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
64675
- },
64676
- workflowNameOnly: {
64677
- rule: "global-machine-wide",
64678
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
64679
- globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
64680
- },
64681
- approvalMode: {
64682
- rule: "global-machine-wide",
64683
- projectNote: autonomyProjectNote
64684
- },
64685
- autoAccept: {
64686
- rule: "global-machine-wide",
64687
- projectNote: autonomyProjectNote
64688
- },
64689
- sandbox: {
64690
- rule: "global-machine-wide",
64691
- projectNote: autonomyProjectNote
64692
- },
64693
- sandboxImage: {
64694
- rule: "global-machine-wide",
64695
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so a sandboxed run in this repository executes inside the named image — check that it is one you trust to run your code.`,
64696
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope every sandboxed run on this machine executes inside the named image — check that it is one you trust to run your code."
64697
- },
64698
- disabled: {
64699
- rule: "global-machine-wide",
64700
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so a name that is gone is a tool the model can call again in this repository unless another scope still disables it.`,
64701
- globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope this decides which tools stay out of the registry for every project on this machine — dropping a name hands that tool back to the model unless another scope still disables it."
64702
- },
64703
- visible: {
64704
- rule: "global-machine-wide",
64705
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides which deferred tools are visible at startup in this repository.`,
64706
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides which deferred tools are visible at startup for every project on this machine."
64707
- },
64708
- eager: {
64709
- rule: "global-machine-wide",
64710
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so in this repository every non-exempt eager-by-default tool it does not name is demoted to deferred — still callable, but only after \`tool_search\` loads it, and out of reach for the session if \`tool_search\` is not registered. A name added back is a tool whose schema is in the initial request again.`,
64711
- globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope it decides which eager-by-default tools keep their schemas in the initial request for every project on this machine — the ones it does not name are demoted to deferred, reachable only through `tool_search`, and out of reach entirely for a session where `tool_search` is not registered, while a name it adds back is a tool declared up front again."
64712
- },
64713
- listDirectory: {
64714
- rule: "global-machine-wide",
64715
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
64716
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
64717
- },
64718
- todoWrite: {
65287
+ const QWEN_SCOPED_TOOLS_KEYS = {
65288
+ workflowsEnabled: { rule: "workspace-stripped" },
65289
+ executionSandbox: {
65290
+ rule: "workspace-stripped",
65291
+ grants: () => true,
65292
+ globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
65293
+ },
65294
+ workflowNameOnly: {
65295
+ rule: "global-machine-wide",
65296
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
65297
+ globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
65298
+ },
65299
+ approvalMode: {
65300
+ rule: "global-machine-wide",
65301
+ projectNote: autonomyProjectNote
65302
+ },
65303
+ autoAccept: {
65304
+ rule: "global-machine-wide",
65305
+ projectNote: autonomyProjectNote
65306
+ },
65307
+ sandbox: {
65308
+ rule: "global-machine-wide",
65309
+ projectNote: autonomyProjectNote
65310
+ },
65311
+ sandboxImage: {
65312
+ rule: "global-machine-wide",
65313
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so a sandboxed run in this repository executes inside the named image — check that it is one you trust to run your code.`,
65314
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope every sandboxed run on this machine executes inside the named image — check that it is one you trust to run your code."
65315
+ },
65316
+ disabled: {
65317
+ rule: "global-machine-wide",
65318
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so a name that is gone is a tool the model can call again in this repository unless another scope still disables it.`,
65319
+ globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope this decides which tools stay out of the registry for every project on this machine — dropping a name hands that tool back to the model unless another scope still disables it."
65320
+ },
65321
+ visible: {
65322
+ rule: "global-machine-wide",
65323
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides which deferred tools are visible at startup in this repository.`,
65324
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides which deferred tools are visible at startup for every project on this machine."
65325
+ },
65326
+ eager: {
65327
+ rule: "global-machine-wide",
65328
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so in this repository every non-exempt eager-by-default tool it does not name is demoted to deferred — still callable, but only after \`tool_search\` loads it, and out of reach for the session if \`tool_search\` is not registered. A name added back is a tool whose schema is in the initial request again.`,
65329
+ globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope it decides which eager-by-default tools keep their schemas in the initial request for every project on this machine — the ones it does not name are demoted to deferred, reachable only through `tool_search`, and out of reach entirely for a session where `tool_search` is not registered, while a name it adds back is a tool declared up front again."
65330
+ },
65331
+ listDirectory: {
65332
+ rule: "global-machine-wide",
65333
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
65334
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
65335
+ },
65336
+ todoWrite: {
65337
+ rule: "global-machine-wide",
65338
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
65339
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
65340
+ }
65341
+ };
65342
+ /**
65343
+ * The notes for a skills list that hides something. Qwen Code unions the list
65344
+ * across scopes, but the override replaces the one in the file it writes, so
65345
+ * the consequence worth naming is what dropping an entry hands back.
65346
+ */
65347
+ function skillsHidingListNotes(hides) {
65348
+ return {
65349
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so it decides which ${hides} in this repository — an entry that is gone is one the model can load again unless another scope still names it.`,
65350
+ globalNote: `Qwen Code honors this key wherever it is written and unions it across scopes, but the override replaces the list in this file rather than adding to it, so in the global scope this decides which ${hides} for every project on this machine — an entry that is gone is one the model can load again unless another scope still names it.`
65351
+ };
65352
+ }
65353
+ const QWEN_SCOPED_SKILLS_KEYS = {
65354
+ disabled: {
65355
+ rule: "global-machine-wide",
65356
+ ...skillsHidingListNotes("skills are hidden")
65357
+ },
65358
+ defaultDisabled: {
65359
+ rule: "global-machine-wide",
65360
+ ...skillsHidingListNotes("skills start disabled until `skills.enabled` opts into them")
65361
+ },
65362
+ disabledLevels: {
65363
+ rule: "global-machine-wide",
65364
+ ...skillsHidingListNotes("skill discovery levels (`project`, `user`, `extension`, `bundled`) are skipped")
65365
+ },
65366
+ enabled: {
65367
+ rule: "global-machine-wide",
65368
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so in this repository it opts the model into the skills it names that \`skills.defaultDisabled\` would otherwise keep off in any scope (it cannot override \`skills.disabled\` or a skipped \`skills.disabledLevels\` level). Check it if the override arrived with a repository you cloned.`,
65369
+ globalNote: "Qwen Code honors this key wherever it is written and unions it across scopes, so in the global scope it opts the model into the skills it names that `skills.defaultDisabled` would otherwise keep off, for every project on this machine (it cannot override `skills.disabled` or a skipped `skills.disabledLevels` level)."
65370
+ }
65371
+ };
65372
+ const QWEN_OVERRIDE_GROUPS = [
65373
+ {
65374
+ groupName: "tools",
65375
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
65376
+ scopedKeys: QWEN_SCOPED_TOOLS_KEYS
65377
+ },
65378
+ {
65379
+ groupName: "security",
65380
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
65381
+ scopedKeys: {
65382
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
65383
+ allowedInsecureVoiceBaseUrls: {
65384
+ rule: "workspace-stripped",
65385
+ grants: isNonEmptyArray
65386
+ },
65387
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
65388
+ folderTrust: { rule: "user-scope-trust-check" }
65389
+ }
65390
+ },
65391
+ {
65392
+ groupName: "skills",
65393
+ overrideKeys: QWEN_OVERRIDE_SKILLS_KEYS,
65394
+ scopedKeys: QWEN_SCOPED_SKILLS_KEYS
65395
+ },
65396
+ {
65397
+ groupName: "general",
65398
+ overrideKeys: QWEN_OVERRIDE_GENERAL_KEYS,
65399
+ scopedKeys: { outputStyle: {
64719
65400
  rule: "global-machine-wide",
64720
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
64721
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
64722
- }
64723
- }
64724
- }, {
64725
- groupName: "security",
64726
- overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
64727
- scopedKeys: {
64728
- allowPrivateNetworkHooks: { rule: "workspace-stripped" },
64729
- allowedInsecureVoiceBaseUrls: {
64730
- rule: "workspace-stripped",
64731
- grants: isNonEmptyArray
64732
- },
64733
- allowedHttpHookUrls: { rule: "workspace-non-overriding" },
64734
- folderTrust: { rule: "user-scope-trust-check" }
65401
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it picks the output style for this repository. A custom style adds the instructions in its style file to the system prompt and can drop the built-in coding guidance (\`keep-coding-instructions\`) — a project style file is read only when the workspace is trusted. Check it if the override arrived with a repository you cloned.`,
65402
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope it picks the output style for every project on this machine that does not choose its own. A custom style adds the instructions in its style file to the system prompt and can drop the built-in coding guidance (`keep-coding-instructions`)."
65403
+ } }
64735
65404
  }
64736
- }];
65405
+ ];
64737
65406
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
64738
65407
  const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
64739
65408
  rule: "global-machine-wide",
@@ -64864,7 +65533,7 @@ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGrou
64864
65533
  return scoped;
64865
65534
  }
64866
65535
  /**
64867
- * Build the `tools`/`security` patch groups contributed by the `qwencode`
65536
+ * Build the `tools`/`security`/`skills`/`general` patch groups contributed by the `qwencode`
64868
65537
  * override. Each group is shallow-merged over what `settings.json` already has,
64869
65538
  * after the scope gate has handled the keys Qwen Code honors in one scope only.
64870
65539
  */
@@ -65033,7 +65702,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
65033
65702
  });
65034
65703
  const overrideGroups = {
65035
65704
  tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
65036
- security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
65705
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS),
65706
+ skills: pickQwenOverrideKeys(settings.skills, QWEN_OVERRIDE_SKILLS_KEYS),
65707
+ general: pickQwenOverrideKeys(settings.general, QWEN_OVERRIDE_GENERAL_KEYS)
65037
65708
  };
65038
65709
  if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
65039
65710
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
@@ -65336,6 +66007,37 @@ function applyAllowDynamicBash({ permissions, authored }) {
65336
66007
  reason: "lets an Allow fallback, Auto included, run the nested and indirect Bash that otherwise needs a human or an exact-literal grant — command and process substitution, a dynamic command name, 'eval', 'source', 'sh -c' and their kind"
65337
66008
  }];
65338
66009
  }
66010
+ /** The writer fallbacks `[permissions] mode` accepts. */
66011
+ const REASONIX_PERMISSION_MODES = /* @__PURE__ */ new Set([
66012
+ "ask",
66013
+ "allow",
66014
+ "deny"
66015
+ ]);
66016
+ /**
66017
+ * Writes the override's `mode` into `[permissions]`. Reasonix honors the key
66018
+ * only in the user config and ignores a project value as user-scope only, so a
66019
+ * project-scope generate skips it with a warning and leaves whatever the file
66020
+ * had. A global `allow` is reported when it is newly written: it lets every
66021
+ * writer call that no rule matches run without a prompt.
66022
+ *
66023
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
66024
+ */
66025
+ function applyPermissionsMode({ permissions, authored, global, relativeFilePath, logger }) {
66026
+ if (authored === void 0) return [];
66027
+ if (!global) {
66028
+ logger?.warn(`Reasonix permissions: skipping reasonix.mode = ${quoteValueForWarning(authored)} for the project-scoped ${relativeFilePath}; Reasonix honors [permissions] mode only in the user config and ignores a project value, so generate it with --global instead.`);
66029
+ return [];
66030
+ }
66031
+ const previous = permissions.mode;
66032
+ permissions.mode = authored;
66033
+ const normalize = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
66034
+ const mode = normalize(authored);
66035
+ if (mode === "ask" || mode === "deny" || normalize(previous) === mode) return [];
66036
+ return [{
66037
+ label: "permissions.mode",
66038
+ reason: "lets every writer call that no rule matches run without a prompt"
66039
+ }];
66040
+ }
65339
66041
  function parseReasonixConfig(fileContent) {
65340
66042
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
65341
66043
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -65386,7 +66088,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65386
66088
  relativeDirPath: paths.relativeDirPath,
65387
66089
  relativeFilePath: paths.relativeFilePath,
65388
66090
  fileContent,
65389
- validate
66091
+ validate,
66092
+ global
65390
66093
  });
65391
66094
  }
65392
66095
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
@@ -65441,6 +66144,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65441
66144
  permissions: mergedPermissions,
65442
66145
  authored: override?.allowDynamicBash
65443
66146
  });
66147
+ trustAffecting.push(...applyPermissionsMode({
66148
+ permissions: mergedPermissions,
66149
+ authored: override?.mode,
66150
+ global,
66151
+ relativeFilePath: relativeFilePathForLog,
66152
+ logger
66153
+ }));
65444
66154
  const patch = { permissions: mergedPermissions };
65445
66155
  if (override?.sandbox !== void 0) {
65446
66156
  const mergedSandbox = {
@@ -65482,7 +66192,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65482
66192
  patch,
65483
66193
  filePath
65484
66194
  }),
65485
- validate
66195
+ validate,
66196
+ global
65486
66197
  });
65487
66198
  }
65488
66199
  toRulesyncPermissions() {
@@ -65507,6 +66218,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65507
66218
  const reasonixOverride = {};
65508
66219
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
65509
66220
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
66221
+ if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
65510
66222
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
65511
66223
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
65512
66224
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -69226,6 +69938,9 @@ const WARP_OVERRIDE_KEYS = [
69226
69938
  "agent_mode_execute_readonly_commands"
69227
69939
  ];
69228
69940
  const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
69941
+ const WARP_AUTO_APPROVE_BYPASS_KEY = "auto_approve_bypasses_command_denylist";
69942
+ const WARP_AGENT_KEY = "warp_agent";
69943
+ const WARP_AGENT_OTHER_KEY = "other";
69229
69944
  const WARP_EXECUTION_PROFILE_KEYS = [
69230
69945
  "read_files",
69231
69946
  "apply_code_diffs",
@@ -69374,7 +70089,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69374
70089
  const override = config.warp;
69375
70090
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69376
70091
  if (isRecord$1(override)) {
69377
- const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
70092
+ const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, [WARP_AUTO_APPROVE_BYPASS_KEY]: _autoApproveBypass, ...legacyOverride } = override;
69378
70093
  Object.assign(profiles, legacyOverride);
69379
70094
  }
69380
70095
  const mergedAllow = uniq(allow.toSorted());
@@ -69384,9 +70099,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69384
70099
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
69385
70100
  else delete profiles[DENYLIST_KEY];
69386
70101
  agents.profiles = profiles;
70102
+ applyAutoApproveBypassOverride({
70103
+ agents,
70104
+ override
70105
+ });
69387
70106
  warnAboutDenylistReplacement({
69388
70107
  toolLabel: "Warp",
70108
+ overrideKey: "warp",
69389
70109
  denyCount: mergedDeny.length,
70110
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
69390
70111
  logger
69391
70112
  });
69392
70113
  mergeIntoDefaultExecutionProfile({
@@ -69425,6 +70146,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69425
70146
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
69426
70147
  const executionProfileOverride = defaultProfile ? liftExecutionProfileOverride(defaultProfile) : void 0;
69427
70148
  if (executionProfileOverride) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70149
+ const autoApproveBypass = readAutoApproveBypass(agents);
70150
+ if (autoApproveBypass !== void 0) warpOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69428
70151
  const result = { ...config };
69429
70152
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
69430
70153
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -69448,12 +70171,40 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69448
70171
  };
69449
70172
  /**
69450
70173
  * Writing `command_denylist` at all replaces Warp's built-in default denylist,
69451
- * so any non-empty list rulesync writes deserves a heads-up.
70174
+ * so any non-empty list rulesync writes deserves a heads-up. Unless
70175
+ * `auto_approve_bypasses_command_denylist` is `false`, auto-approve also runs
70176
+ * commands that match the list, so the warning says so.
69452
70177
  * https://docs.warp.dev/agents/cli/permissions-and-profiles/
69453
70178
  */
69454
- function warnAboutDenylistReplacement({ toolLabel, denyCount, logger }) {
70179
+ function warnAboutDenylistReplacement({ toolLabel, overrideKey, denyCount, autoApproveBypassesDenylist, logger }) {
69455
70180
  if (denyCount === 0 || !logger) return;
69456
- logger.warn(`${toolLabel}'s command_denylist replaces its built-in default denylist, which covers rm, curl, wget, eval, ssh, shells, and other risky command patterns. The ${denyCount} deny rule(s) from .rulesync/permissions.jsonc are now the whole denylist — add equivalents for the built-in patterns you want to keep.`);
70181
+ const bypassNote = autoApproveBypassesDenylist === false ? "" : ` Auto-approve also runs commands that match the denylist unless ${overrideKey}.${WARP_AUTO_APPROVE_BYPASS_KEY} is set to false.`;
70182
+ logger.warn(`${toolLabel}'s command_denylist replaces its built-in default denylist, which covers rm, curl, wget, eval, ssh, shells, and other risky command patterns. The ${denyCount} deny rule(s) from .rulesync/permissions.jsonc are now the whole denylist — add equivalents for the built-in patterns you want to keep.${bypassNote}`);
70183
+ }
70184
+ /**
70185
+ * Write the override's `auto_approve_bypasses_command_denylist` into
70186
+ * `[agents.warp_agent.other]`, keeping every sibling key of `agents.warp_agent`
70187
+ * and of its `other` table. Nothing is written when the override omits it.
70188
+ */
70189
+ function applyAutoApproveBypassOverride({ agents, override }) {
70190
+ if (!isRecord$1(override)) return;
70191
+ const value = override[WARP_AUTO_APPROVE_BYPASS_KEY];
70192
+ if (typeof value !== "boolean") return;
70193
+ const warpAgent = isRecord$1(agents[WARP_AGENT_KEY]) ? { ...agents[WARP_AGENT_KEY] } : {};
70194
+ const other = isRecord$1(warpAgent[WARP_AGENT_OTHER_KEY]) ? { ...warpAgent[WARP_AGENT_OTHER_KEY] } : {};
70195
+ other[WARP_AUTO_APPROVE_BYPASS_KEY] = value;
70196
+ warpAgent[WARP_AGENT_OTHER_KEY] = other;
70197
+ agents[WARP_AGENT_KEY] = warpAgent;
70198
+ }
70199
+ /**
70200
+ * Read `[agents.warp_agent.other] auto_approve_bypasses_command_denylist`, or
70201
+ * `undefined` when it is unset (Warp then defaults to `true`).
70202
+ */
70203
+ function readAutoApproveBypass(agents) {
70204
+ const warpAgent = agents[WARP_AGENT_KEY];
70205
+ const other = isRecord$1(warpAgent) ? warpAgent[WARP_AGENT_OTHER_KEY] : void 0;
70206
+ const value = isRecord$1(other) ? other[WARP_AUTO_APPROVE_BYPASS_KEY] : void 0;
70207
+ return typeof value === "boolean" ? value : void 0;
69457
70208
  }
69458
70209
  /**
69459
70210
  * On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
@@ -69807,6 +70558,9 @@ const WARPCLI_GLOBAL_ONLY_MESSAGE = "Warp Agent CLI permissions are global-only;
69807
70558
  * (`read_files`, `apply_code_diffs`, `run_agents`, ...), merged first so the
69808
70559
  * rulesync-owned command lists always win.
69809
70560
  *
70561
+ * The `warpcli.auto_approve_bypasses_command_denylist` override is written to
70562
+ * the sibling `[agents.warp_agent.other]` table.
70563
+ *
69810
70564
  * Unlike the `warp` target, the CLI never read the app's legacy
69811
70565
  * `[agents.profiles]` keys, so none are written, and there is no settings
69812
70566
  * migration to wait for: the CLI's settings file "is created the first time you
@@ -69873,11 +70627,6 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69873
70627
  });
69874
70628
  const mergedAllow = uniq(allow.toSorted());
69875
70629
  const mergedDeny = uniq(deny.toSorted());
69876
- warnAboutDenylistReplacement({
69877
- toolLabel: "Warp Agent CLI",
69878
- denyCount: mergedDeny.length,
69879
- logger
69880
- });
69881
70630
  const override = config.warpcli;
69882
70631
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69883
70632
  const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
@@ -69887,7 +70636,18 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69887
70636
  mergedDeny,
69888
70637
  executionProfileOverride
69889
70638
  });
70639
+ applyAutoApproveBypassOverride({
70640
+ agents,
70641
+ override
70642
+ });
69890
70643
  settings.agents = agents;
70644
+ warnAboutDenylistReplacement({
70645
+ toolLabel: "Warp Agent CLI",
70646
+ overrideKey: "warpcli",
70647
+ denyCount: mergedDeny.length,
70648
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
70649
+ logger
70650
+ });
69891
70651
  return new WarpcliPermissions({
69892
70652
  outputRoot,
69893
70653
  relativeDirPath: paths.relativeDirPath,
@@ -69914,8 +70674,12 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69914
70674
  deny: isStringArray$2(denylist) ? denylist : []
69915
70675
  });
69916
70676
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
70677
+ const warpcliOverride = {};
70678
+ if (executionProfileOverride) warpcliOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70679
+ const autoApproveBypass = readAutoApproveBypass(agents);
70680
+ if (autoApproveBypass !== void 0) warpcliOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69917
70681
  const result = { ...config };
69918
- if (executionProfileOverride) result.warpcli = { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: executionProfileOverride };
70682
+ if (Object.keys(warpcliOverride).length > 0) result.warpcli = warpcliOverride;
69919
70683
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
69920
70684
  }
69921
70685
  validate() {
@@ -81989,9 +82753,8 @@ var CodebuddySubagent = class CodebuddySubagent extends ToolSubagent {
81989
82753
  * Keys of the rulesync `codewhale` section copied into the profile. Codewhale
81990
82754
  * parses agent profiles with `deny_unknown_fields`, so an unknown key would
81991
82755
  * make the whole profile fail to load; anything else in the section is dropped
81992
- * with a warning. `[tools]` and `[permissions]` are left out on purpose:
81993
- * Codewhale only accepts values there that narrow the defaults, and rejects a
81994
- * profile that tries to widen them.
82756
+ * with a warning. The `[tools]` and `[permissions]` tables are handled
82757
+ * separately (see `CODEWHALE_NARROWING_TABLES`).
81995
82758
  */
81996
82759
  const CODEWHALE_SECTION_KEYS = [
81997
82760
  "role_hint",
@@ -82012,6 +82775,48 @@ const CODEWHALE_SECTION_KEY_ALIASES = {
82012
82775
  reasoning_effort: ["thinking", "reasoning"]
82013
82776
  };
82014
82777
  const CODEWHALE_SECTION_ALIAS_TO_KEY = new Map(Object.entries(CODEWHALE_SECTION_KEY_ALIASES).flatMap(([key, aliases]) => (aliases ?? []).map((alias) => [alias, key])));
82778
+ /**
82779
+ * The `[tools]` and `[permissions]` tables of an agent profile, with the only
82780
+ * values Codewhale accepts in each key. Codewhale rejects a profile that tries
82781
+ * to widen access through them (`reject_permission_expansion` in
82782
+ * `fleet/profile.rs`), and both tables deny unknown fields, so any other key
82783
+ * or value is dropped with a warning.
82784
+ */
82785
+ const CODEWHALE_NARROWING_TABLES = {
82786
+ tools: { posture: (value) => typeof value === "string" && [
82787
+ "read-only",
82788
+ "readonly",
82789
+ "read_only"
82790
+ ].includes(value) },
82791
+ permissions: {
82792
+ allow_shell: (value) => value === false,
82793
+ trust: (value) => value === false,
82794
+ approval_required: (value) => value === true
82795
+ }
82796
+ };
82797
+ /**
82798
+ * Keeps the narrowing-only entries of a `[tools]` / `[permissions]` table and
82799
+ * reports every other entry by its dotted path. A non-table value is dropped
82800
+ * as a whole.
82801
+ */
82802
+ function filterNarrowingTable(tableName, value) {
82803
+ const allowed = CODEWHALE_NARROWING_TABLES[tableName] ?? {};
82804
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {
82805
+ kept: void 0,
82806
+ dropped: [tableName]
82807
+ };
82808
+ const kept = {};
82809
+ const dropped = [];
82810
+ for (const [key, entry] of Object.entries(value)) {
82811
+ const normalized = typeof entry === "string" ? entry.trim() : entry;
82812
+ if (Object.hasOwn(allowed, key) && allowed[key]?.(normalized)) kept[key] = normalized;
82813
+ else dropped.push(`${tableName}.${key}`);
82814
+ }
82815
+ return {
82816
+ kept: Object.keys(kept).length > 0 ? kept : void 0,
82817
+ dropped
82818
+ };
82819
+ }
82015
82820
  const CodewhaleSubagentTomlSchema = z.looseObject({
82016
82821
  id: z.optional(z.string()),
82017
82822
  name: z.optional(z.string()),
@@ -82038,7 +82843,8 @@ function stringifyCodewhaleSubagentToml(fields, instructions) {
82038
82843
  *
82039
82844
  * Generation writes `id` (the file stem), `display_name` (the rulesync name),
82040
82845
  * `description`, and the body as `[instructions] text`, plus the allowlisted
82041
- * keys of the rulesync `codewhale` section.
82846
+ * keys of the rulesync `codewhale` section and its narrowing-only `[tools]` /
82847
+ * `[permissions]` entries.
82042
82848
  *
82043
82849
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/SUBAGENTS.md
82044
82850
  * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/fleet/profile.rs
@@ -82073,6 +82879,10 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82073
82879
  const spelling = [key, ...CODEWHALE_SECTION_KEY_ALIASES[key] ?? []].find((candidate) => record[candidate] !== void 0);
82074
82880
  if (spelling !== void 0) codewhaleSection[key] = record[spelling];
82075
82881
  }
82882
+ for (const tableName of Object.keys(CODEWHALE_NARROWING_TABLES)) if (record[tableName] !== void 0) {
82883
+ const { kept } = filterNarrowingTable(tableName, record[tableName]);
82884
+ if (kept !== void 0) codewhaleSection[tableName] = kept;
82885
+ }
82076
82886
  return new RulesyncSubagent({
82077
82887
  outputRoot: ".",
82078
82888
  frontmatter: {
@@ -82095,6 +82905,12 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82095
82905
  const droppedKeys = [];
82096
82906
  const sectionSpellings = /* @__PURE__ */ new Map();
82097
82907
  for (const [rawKey, value] of Object.entries(rawSection)) {
82908
+ if (Object.hasOwn(CODEWHALE_NARROWING_TABLES, rawKey)) {
82909
+ const { kept, dropped } = filterNarrowingTable(rawKey, value);
82910
+ if (kept !== void 0) sectionFields[rawKey] = kept;
82911
+ droppedKeys.push(...dropped);
82912
+ continue;
82913
+ }
82098
82914
  const key = CODEWHALE_SECTION_ALIAS_TO_KEY.get(rawKey) ?? rawKey;
82099
82915
  if (CODEWHALE_SECTION_KEYS.includes(key) && typeof value === "string") {
82100
82916
  if (key === rawKey || sectionFields[key] === void 0) {
@@ -82104,7 +82920,7 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82104
82920
  } else droppedKeys.push(rawKey);
82105
82921
  } else droppedKeys.push(rawKey);
82106
82922
  }
82107
- if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias).`);
82923
+ if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, [tools] / [permissions] values that would widen access, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias), plus the narrowing-only tools.posture = "read-only" and permissions.allow_shell = false, permissions.trust = false, permissions.approval_required = true.`);
82108
82924
  const body = stringifyCodewhaleSubagentToml({
82109
82925
  id: toProfileId(relativeFilePath),
82110
82926
  display_name: frontmatter.name,
@@ -88601,6 +89417,99 @@ const AntigravityRuleFrontmatterSchema = z.looseObject({
88601
89417
  function toGlobalRuleFileName(relativeFilePath) {
88602
89418
  return relativeFilePath.split(/[\\/]/).join("-");
88603
89419
  }
89420
+ /** The project rules directory both Antigravity targets share, as a POSIX path. */
89421
+ const PROJECT_RULES_DIR = `${ANTIGRAVITY_IDE_AGENTS_DIR}/rules`;
89422
+ /**
89423
+ * The subproject directory a rule is scoped to, or `undefined` when it goes to
89424
+ * the project-root `.agents/rules/`. Antigravity reads a `.agents/rules/`
89425
+ * directory in any subdirectory and loads it for files under that directory,
89426
+ * so a non-root rule carrying `agentsmd.subprojectPath` is written there. Project
89427
+ * scope only: there is no workspace tree to nest under in global scope.
89428
+ *
89429
+ * @see https://antigravity.google/docs/rules
89430
+ */
89431
+ function resolveAntigravitySubprojectPath({ rulesyncRule, global }) {
89432
+ const { root, agentsmd } = rulesyncRule.getFrontmatter();
89433
+ const subprojectPath = !global && root !== true ? agentsmd?.subprojectPath : void 0;
89434
+ return subprojectPath && ![
89435
+ "",
89436
+ ".",
89437
+ "./"
89438
+ ].includes(subprojectPath) ? subprojectPath : void 0;
89439
+ }
89440
+ /**
89441
+ * File name of a rule in a subproject's `.agents/rules/`. Antigravity reads
89442
+ * only the direct children of a rules directory, so a nested rulesync rule
89443
+ * (`frontend/style.md`) is flattened (`frontend-style.md`), and the name keeps
89444
+ * its case so the IDE and the CLI write the same file and an imported
89445
+ * `codeStyle.md` is written back to the same place.
89446
+ */
89447
+ function toNestedRuleFileName(relativeFilePath) {
89448
+ return toGlobalRuleFileName(relativeFilePath);
89449
+ }
89450
+ /** The `.agents/rules/` directory of a subproject. */
89451
+ function toAntigravityNestedRulesDirPath(subprojectPath) {
89452
+ return join(subprojectPath, ANTIGRAVITY_IDE_AGENTS_DIR, "rules");
89453
+ }
89454
+ /**
89455
+ * Split a nested rule directory such as `packages/api/.agents/rules/sub` into
89456
+ * the subproject (`packages/api`) and the folder inside its rules directory
89457
+ * (`sub`). Returns `undefined` for the project-root `.agents/rules/`, for a
89458
+ * path with no `.agents/rules` segment, and for one under a hidden directory —
89459
+ * another tool's output or a worktree, not a subproject.
89460
+ */
89461
+ function extractAntigravitySubprojectPath(relativeDirPath) {
89462
+ const segments = toPosixPath(relativeDirPath).split("/");
89463
+ const index = segments.findIndex((segment, i) => segment === ".agents" && segments[i + 1] === "rules");
89464
+ if (index <= 0) return;
89465
+ const subprojectSegments = segments.slice(0, index);
89466
+ if (subprojectSegments.some((segment) => segment === "" || segment.startsWith("."))) return;
89467
+ return {
89468
+ subprojectPath: subprojectSegments.join("/"),
89469
+ innerDirPath: segments.slice(index + 2).join("/")
89470
+ };
89471
+ }
89472
+ /**
89473
+ * The `.agents/rules/` folders of project subdirectories. Only their direct
89474
+ * `.md` children are read, because Antigravity ignores files nested in
89475
+ * subfolders of a rules directory. The shared nested
89476
+ * scan ignores every hidden directory, which would skip `.agents` itself, so
89477
+ * only a `.agents/rules/` below another hidden directory is ignored here
89478
+ * (`extractAntigravitySubprojectPath` rejects one again on import). Import-only,
89479
+ * like every other nested scan: these files sit outside the rulesync-owned root
89480
+ * `.agents/rules/`, so `--delete` never sweeps them.
89481
+ */
89482
+ function getAntigravityNestedFilePatterns() {
89483
+ return {
89484
+ include: [`**/${PROJECT_RULES_DIR}/*.md`],
89485
+ ignore: [
89486
+ `${PROJECT_RULES_DIR}/**`,
89487
+ `**/.*/**/${PROJECT_RULES_DIR}/**`,
89488
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
89489
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
89490
+ ],
89491
+ ownedDirPath: PROJECT_RULES_DIR
89492
+ };
89493
+ }
89494
+ /**
89495
+ * Resolve where a non-root project rule file read from disk lives. A file
89496
+ * discovered by `getNestedFilePatterns` arrives as its own directory plus its
89497
+ * basename; it is re-split into the subproject's rules directory and the path
89498
+ * inside it, matching how the root `.agents/rules/` is read.
89499
+ */
89500
+ function resolveAntigravityNonRootFile({ relativeDirPath, relativeFilePath, defaultDirPath }) {
89501
+ const nested = relativeDirPath === void 0 ? void 0 : extractAntigravitySubprojectPath(relativeDirPath);
89502
+ if (nested === void 0) return {
89503
+ relativeDirPath: defaultDirPath,
89504
+ relativeFilePath
89505
+ };
89506
+ return {
89507
+ relativeDirPath: toAntigravityNestedRulesDirPath(nested.subprojectPath),
89508
+ relativeFilePath: join(nested.innerDirPath, relativeFilePath)
89509
+ };
89510
+ }
89511
+ /** The targets a nested rule is imported with: the two that read the shared tree. */
89512
+ const ANTIGRAVITY_NESTED_RULE_TARGETS = ["antigravity-ide", "antigravity-cli"];
88604
89513
  /**
88605
89514
  * Converts a comma-separated globs string or array to an array of globs.
88606
89515
  * @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
@@ -88788,12 +89697,17 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88788
89697
  });
88789
89698
  }
88790
89699
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
88791
- const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
89700
+ const nonRoot = resolveAntigravityNonRootFile({
89701
+ relativeDirPath,
89702
+ relativeFilePath,
89703
+ defaultDirPath: paths.nonRoot.relativeDirPath
89704
+ });
89705
+ const relativePath = join(nonRoot.relativeDirPath, nonRoot.relativeFilePath);
88792
89706
  const fileContent = await readFileContent(join(outputRoot, relativePath));
88793
89707
  const rule = new AntigravityCliRule({
88794
89708
  outputRoot,
88795
- relativeDirPath: paths.nonRoot.relativeDirPath,
88796
- relativeFilePath,
89709
+ relativeDirPath: nonRoot.relativeDirPath,
89710
+ relativeFilePath: nonRoot.relativeFilePath,
88797
89711
  fileContent,
88798
89712
  validate,
88799
89713
  root: false,
@@ -88825,11 +89739,21 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88825
89739
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
88826
89740
  const frontmatter = strategy.generateFrontmatter(normalizeStoredAntigravity(storedAntigravity), rulesyncFrontmatter);
88827
89741
  const fileContent = stringifyFrontmatter(rulesyncRule.getBody(), frontmatter);
88828
- if (!global) return new AntigravityCliRule({
88829
- ...params,
88830
- fileContent,
88831
- global
88832
- });
89742
+ if (!global) {
89743
+ const subprojectPath = resolveAntigravitySubprojectPath({
89744
+ rulesyncRule,
89745
+ global
89746
+ });
89747
+ return new AntigravityCliRule({
89748
+ ...params,
89749
+ ...subprojectPath !== void 0 && {
89750
+ relativeDirPath: toAntigravityNestedRulesDirPath(subprojectPath),
89751
+ relativeFilePath: toNestedRuleFileName(params.relativeFilePath)
89752
+ },
89753
+ fileContent,
89754
+ global
89755
+ });
89756
+ }
88833
89757
  return new AntigravityCliRule({
88834
89758
  ...params,
88835
89759
  relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
@@ -88845,22 +89769,32 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88845
89769
  globs: [],
88846
89770
  antigravity: frontmatter
88847
89771
  };
89772
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
88848
89773
  return new RulesyncRule({
88849
89774
  outputRoot: process.cwd(),
88850
89775
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
88851
89776
  relativeFilePath: this.getRelativeFilePath(),
88852
89777
  frontmatter: {
88853
89778
  root: false,
88854
- targets: ["*"],
89779
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
88855
89780
  ...rulesyncData,
88856
89781
  antigravity: {
88857
89782
  ...rulesyncData.antigravity,
88858
89783
  globs: frontmatter.globs ? parseGlobsString$1(frontmatter.globs) : void 0
88859
- }
89784
+ },
89785
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
88860
89786
  },
88861
89787
  body
88862
89788
  });
88863
89789
  }
89790
+ /**
89791
+ * The `.agents/rules/` folders of project subdirectories, imported back as
89792
+ * directory-scoped rules.
89793
+ * @see https://antigravity.google/docs/rules
89794
+ */
89795
+ static getNestedFilePatterns() {
89796
+ return getAntigravityNestedFilePatterns();
89797
+ }
88864
89798
  validate() {
88865
89799
  if (this.root) return {
88866
89800
  success: true,
@@ -88990,8 +89924,15 @@ var AntigravityIdeRule = class extends ToolRule {
88990
89924
  global
88991
89925
  });
88992
89926
  }
88993
- const nonRootDirPath = paths.nonRoot.relativeDirPath;
88994
- const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
89927
+ const nonRoot = this.supportsSubprojectRules() ? resolveAntigravityNonRootFile({
89928
+ relativeDirPath,
89929
+ relativeFilePath,
89930
+ defaultDirPath: paths.nonRoot.relativeDirPath
89931
+ }) : {
89932
+ relativeDirPath: paths.nonRoot.relativeDirPath,
89933
+ relativeFilePath
89934
+ };
89935
+ const filePath = join(outputRoot, nonRoot.relativeDirPath, nonRoot.relativeFilePath);
88995
89936
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
88996
89937
  let parsedFrontmatter;
88997
89938
  if (validate) {
@@ -89001,8 +89942,8 @@ var AntigravityIdeRule = class extends ToolRule {
89001
89942
  } else parsedFrontmatter = frontmatter;
89002
89943
  return new this({
89003
89944
  outputRoot,
89004
- relativeDirPath: nonRootDirPath,
89005
- relativeFilePath,
89945
+ relativeDirPath: nonRoot.relativeDirPath,
89946
+ relativeFilePath: nonRoot.relativeFilePath,
89006
89947
  body,
89007
89948
  frontmatter: parsedFrontmatter,
89008
89949
  validate,
@@ -89010,6 +89951,25 @@ var AntigravityIdeRule = class extends ToolRule {
89010
89951
  global
89011
89952
  });
89012
89953
  }
89954
+ /**
89955
+ * Whether a rule scoped to a subproject goes to that subproject's own
89956
+ * `.agents/rules/`. A subclass that writes somewhere other than a project
89957
+ * tree (the plugin bundle) turns it off.
89958
+ */
89959
+ static supportsSubprojectRules() {
89960
+ return true;
89961
+ }
89962
+ /**
89963
+ * The `.agents/rules/` folders of project subdirectories, imported back as
89964
+ * directory-scoped rules.
89965
+ * @see https://antigravity.google/docs/rules
89966
+ */
89967
+ static getNestedFilePatterns() {
89968
+ return this.supportsSubprojectRules() ? getAntigravityNestedFilePatterns() : {
89969
+ include: [],
89970
+ ignore: []
89971
+ };
89972
+ }
89013
89973
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
89014
89974
  const paths = this.getSettablePaths({ global });
89015
89975
  if (rulesyncRule.getFrontmatter().root) {
@@ -89032,10 +89992,14 @@ var AntigravityIdeRule = class extends ToolRule {
89032
89992
  const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
89033
89993
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
89034
89994
  const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
89035
- const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
89995
+ const subprojectPath = this.supportsSubprojectRules() ? resolveAntigravitySubprojectPath({
89996
+ rulesyncRule,
89997
+ global
89998
+ }) : void 0;
89999
+ const relativeFilePath = global || subprojectPath !== void 0 ? toNestedRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
89036
90000
  return new this({
89037
90001
  outputRoot,
89038
- relativeDirPath: paths.nonRoot.relativeDirPath,
90002
+ relativeDirPath: subprojectPath === void 0 ? paths.nonRoot.relativeDirPath : toAntigravityNestedRulesDirPath(subprojectPath),
89039
90003
  relativeFilePath,
89040
90004
  frontmatter,
89041
90005
  body: rulesyncRule.getBody(),
@@ -89056,15 +90020,17 @@ var AntigravityIdeRule = class extends ToolRule {
89056
90020
  ...rulesyncData.antigravity,
89057
90021
  globs: this.frontmatter.globs ? parseGlobsString$1(this.frontmatter.globs) : void 0
89058
90022
  };
90023
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
89059
90024
  return new RulesyncRule({
89060
90025
  outputRoot: process.cwd(),
89061
90026
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
89062
90027
  relativeFilePath: this.getRelativeFilePath(),
89063
90028
  frontmatter: {
89064
90029
  root: false,
89065
- targets: ["*"],
90030
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
89066
90031
  ...rulesyncData,
89067
- antigravity: antigravityForRulesync
90032
+ antigravity: antigravityForRulesync,
90033
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
89068
90034
  },
89069
90035
  body: this.body
89070
90036
  });
@@ -89118,6 +90084,9 @@ var AntigravityIdeRule = class extends ToolRule {
89118
90084
  //#endregion
89119
90085
  //#region src/features/rules/antigravity-plugin-rule.ts
89120
90086
  var AntigravityPluginRule = class extends AntigravityIdeRule {
90087
+ static supportsSubprojectRules() {
90088
+ return false;
90089
+ }
89121
90090
  static isTargetedByRulesyncRule(rulesyncRule) {
89122
90091
  const targets = rulesyncRule.getFrontmatter().targets;
89123
90092
  return targets.includes("*") || targets.includes("antigravity-plugin");
@@ -95467,6 +96436,12 @@ var VibeRule = class extends NestedAgentsmdRule {
95467
96436
  * file, so every non-root rule without `agentsmd.subprojectPath` folds into
95468
96437
  * the root `./AGENTS.md`.
95469
96438
  *
96439
+ * A legacy root `WARP.md` is read on import when the root `AGENTS.md` is
96440
+ * absent, so a repository that has not moved to `AGENTS.md` yet imports its
96441
+ * rules. It is never written or deleted. When both exist, Warp reads
96442
+ * `WARP.md`, so import keeps reading the `AGENTS.md` rulesync manages and warns
96443
+ * that Warp ignores it, as generate already does.
96444
+ *
95470
96445
  * In global mode, Warp reads a third rule source from `~/.agents/AGENTS.md`
95471
96446
  * (the cross-tool agent config directory), indexed like project rules and also
95472
96447
  * used from remote hosts in SSH sessions. The same root-fold policy applies.
@@ -95481,6 +96456,31 @@ var WarpRule = class extends NestedAgentsmdRule {
95481
96456
  toolTarget: "warp"
95482
96457
  };
95483
96458
  }
96459
+ static getSettablePaths(options = {}) {
96460
+ const paths = super.getSettablePaths(options);
96461
+ if (options.global) return paths;
96462
+ return {
96463
+ ...paths,
96464
+ importOnlyRoots: [{
96465
+ relativeDirPath: ".",
96466
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96467
+ onlyWhenRootAbsent: true,
96468
+ outranksRoot: true
96469
+ }]
96470
+ };
96471
+ }
96472
+ static async fromFile(params) {
96473
+ const { outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true } = params;
96474
+ if (!params.global && relativeFilePath === "WARP.md" && (relativeDirPath === void 0 || relativeDirPath === ".")) return new this({
96475
+ outputRoot,
96476
+ relativeDirPath: ".",
96477
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96478
+ fileContent: await readFileContent(join(outputRoot, WARP_LEGACY_RULE_FILE_NAME)),
96479
+ validate,
96480
+ root: true
96481
+ });
96482
+ return super.fromFile(params);
96483
+ }
95484
96484
  };
95485
96485
  //#endregion
95486
96486
  //#region src/features/rules/zcode-rule.ts
@@ -96783,7 +97783,7 @@ var RulesProcessor = class extends FeatureProcessor {
96783
97783
  if (!importOnlyRoots || importOnlyRoots.length === 0) return;
96784
97784
  const existingPaths = [];
96785
97785
  for (const importOnlyRoot of importOnlyRoots) {
96786
- if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
97786
+ if (importOnlyRoot.onlyWhenRootAbsent !== true || importOnlyRoot.outranksRoot === true) continue;
96787
97787
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
96788
97788
  cwd: this.outputRoot,
96789
97789
  type: "file"
@@ -97484,13 +98484,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
97484
98484
  const rootFilePath = primaryRootFilePaths[0];
97485
98485
  const scannedPaths = [];
97486
98486
  const skippedPaths = [];
98487
+ const skippedOutrankingPaths = [];
97487
98488
  for (const importOnlyRoot of importOnlyRoots) {
97488
98489
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
97489
98490
  cwd: this.outputRoot,
97490
98491
  type: "file"
97491
98492
  });
97492
98493
  if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
97493
- skippedPaths.push(...matchedPaths);
98494
+ (importOnlyRoot.outranksRoot === true ? skippedOutrankingPaths : skippedPaths).push(...matchedPaths);
97494
98495
  continue;
97495
98496
  }
97496
98497
  scannedPaths.push(...matchedPaths);
@@ -97501,6 +98502,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
97501
98502
  const remainingCount = skippedNames.length - listedNames.length;
97502
98503
  this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters(relative(this.outputRoot, rootFilePath))} exists, and the tool reads that file exclusively. Delete them once you have checked that content is in the root file, or move it into ${RULESYNC_RULES_RELATIVE_DIR_PATH} if it is not.`);
97503
98504
  }
98505
+ if (skippedOutrankingPaths.length > 0 && rootFilePath !== void 0) {
98506
+ const rootName = stripControlCharacters(relative(this.outputRoot, rootFilePath));
98507
+ const skippedNames = skippedOutrankingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
98508
+ const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
98509
+ const remainingCount = skippedNames.length - listedNames.length;
98510
+ this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${rootName} is imported instead, but ${this.toolTarget} reads ${listedNames.length === 1 ? "that file" : "those files"} in place of ${rootName} when both exist. Move any content only it has into ${RULESYNC_RULES_RELATIVE_DIR_PATH} and delete it, so the generated ${rootName} takes effect.`);
98511
+ }
97504
98512
  return await buildImportRulesFromPaths(scannedPaths);
97505
98513
  })();
97506
98514
  this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);
@@ -100321,4 +101329,4 @@ async function importChecksCore(params) {
100321
101329
  //#endregion
100322
101330
  export { RulesyncHooks as $, removeFile as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
100323
101331
 
100324
- //# sourceMappingURL=import--SCvWeDn.js.map
101332
+ //# sourceMappingURL=import-DBd2G6Eo.js.map