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.
@@ -453,36 +453,36 @@ const isFeatureValueEnabled = (value) => {
453
453
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
454
454
  //#endregion
455
455
  //#region src/constants/rulesync-paths.ts
456
- const { join: join$413 } = node_path.posix;
456
+ const { join: join$416 } = node_path.posix;
457
457
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
458
458
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
459
459
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
460
460
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
461
461
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
462
462
  const RULES_FEATURE_SUBDIR = "rules";
463
- const CURATED_RULES_FEATURE_SUBDIR = join$413(RULES_FEATURE_SUBDIR, ".curated");
463
+ const CURATED_RULES_FEATURE_SUBDIR = join$416(RULES_FEATURE_SUBDIR, ".curated");
464
464
  const COMMANDS_FEATURE_SUBDIR = "commands";
465
465
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
466
466
  const CHECKS_FEATURE_SUBDIR = "checks";
467
467
  const SKILLS_FEATURE_SUBDIR = "skills";
468
- const CURATED_SKILLS_FEATURE_SUBDIR = join$413(SKILLS_FEATURE_SUBDIR, ".curated");
469
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
470
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
471
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
472
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
473
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
474
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
475
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
476
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
477
- join$413(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
478
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
479
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
468
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$416(SKILLS_FEATURE_SUBDIR, ".curated");
469
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
470
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
471
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
472
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
473
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
474
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
475
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
476
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
477
+ join$416(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
478
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
479
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
480
480
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
481
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
481
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
482
482
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
483
483
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
484
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
485
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$413(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
484
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
485
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$416(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
486
486
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
487
487
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
488
488
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -7883,6 +7883,26 @@ const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
7883
7883
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7884
7884
  defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7885
7885
  });
7886
+ /**
7887
+ * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
7888
+ * before prompting. `always-ask` auto-approves only read-only tools, `write`
7889
+ * also workspace writes, and `yolo` (oh-my-pi's default) every tier.
7890
+ *
7891
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
7892
+ */
7893
+ const OMP_APPROVAL_MODES = [
7894
+ "always-ask",
7895
+ "write",
7896
+ "yolo"
7897
+ ];
7898
+ /**
7899
+ * Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
7900
+ * `tools.approvalMode` key of `config.yml`, which has no canonical category.
7901
+ */
7902
+ const OmpPermissionsOverrideSchema = zod_mini.z.looseObject({
7903
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7904
+ approvalMode: zod_mini.z.optional(zod_mini.z.enum(OMP_APPROVAL_MODES))
7905
+ });
7886
7906
  const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
7887
7907
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7888
7908
  defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum([
@@ -7890,6 +7910,14 @@ const KimiCodePermissionsOverrideSchema = zod_mini.z.looseObject({
7890
7910
  "yolo",
7891
7911
  "auto"
7892
7912
  ])),
7913
+ /**
7914
+ * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
7915
+ * default `true`). `false` turns off the built-in dangerous-command policy
7916
+ * (no confirmation for `rm -rf`, `shutdown` and the like).
7917
+ *
7918
+ * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
7919
+ */
7920
+ dangerousCommandGuard: zod_mini.z.optional(zod_mini.z.boolean()),
7893
7921
  rules: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
7894
7922
  decision: PermissionActionSchema,
7895
7923
  pattern: zod_mini.z.string(),
@@ -8105,7 +8133,10 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8105
8133
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8106
8134
  * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8107
8135
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8108
- * `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
8136
+ * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8137
+ * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
8138
+ * Code unions across scopes) and `general` (`outputStyle`, the output style
8139
+ * that reshapes how responses are written). Qwen Code strips `tools.workflowsEnabled`,
8109
8140
  * `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
8110
8141
  * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
8111
8142
  * skips those four in project scope and announces a granting value in global
@@ -8122,7 +8153,8 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8122
8153
  * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8123
8154
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8124
8155
  * than merging with the list a higher scope set), the Auto Mode
8125
- * classifier config, and, because these groups are loose objects, any key
8156
+ * classifier config, the skill kill-switches, and, because these groups are
8157
+ * loose objects, any key
8126
8158
  * rulesync does not model. Import flags a scope-dependent key only when it read
8127
8159
  * the project file, since that is the value a `--global` regenerate would
8128
8160
  * promote. It also
@@ -8133,11 +8165,13 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
8133
8165
  * matching `settings.json` group and emitted only for Qwen, while the shared
8134
8166
  * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
8135
8167
  * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
8136
- * `tools`/`security`/`autoMode` key can be authored.
8168
+ * `tools`/`security`/`skills`/`general`/`autoMode` key can be authored.
8137
8169
  *
8138
8170
  * @example
8139
8171
  * {
8140
8172
  * "tools": { "approvalMode": "auto-edit" },
8173
+ * "skills": { "disabled": ["pdf"], "disabledLevels": ["bundled"] },
8174
+ * "general": { "outputStyle": "Concise" },
8141
8175
  * "security": { "folderTrust": { "enabled": true } },
8142
8176
  * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
8143
8177
  * }
@@ -8146,6 +8180,8 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
8146
8180
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8147
8181
  tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
8148
8182
  security: zod_mini.z.optional(zod_mini.z.looseObject({})),
8183
+ skills: zod_mini.z.optional(zod_mini.z.looseObject({})),
8184
+ general: zod_mini.z.optional(zod_mini.z.looseObject({})),
8149
8185
  autoMode: zod_mini.z.optional(zod_mini.z.looseObject({}))
8150
8186
  });
8151
8187
  /**
@@ -8231,6 +8267,8 @@ const TabninePermissionsOverrideSchema = zod_mini.z.looseObject({
8231
8267
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
8232
8268
  * @example
8233
8269
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8270
+ * @example
8271
+ * { "mode": "deny" }
8234
8272
  */
8235
8273
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8236
8274
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
@@ -8239,7 +8277,12 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
8239
8277
  rawAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8240
8278
  rawAsk: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8241
8279
  rawDeny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8242
- allowDynamicBash: zod_mini.z.optional(zod_mini.z.boolean())
8280
+ allowDynamicBash: zod_mini.z.optional(zod_mini.z.boolean()),
8281
+ mode: zod_mini.z.optional(zod_mini.z.enum([
8282
+ "ask",
8283
+ "allow",
8284
+ "deny"
8285
+ ]))
8243
8286
  });
8244
8287
  /**
8245
8288
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -8282,7 +8325,8 @@ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
8282
8325
  * {
8283
8326
  * "agent_mode_coding_permissions": "always_allow_reading",
8284
8327
  * "agent_mode_execute_readonly_commands": true,
8285
- * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
8328
+ * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] },
8329
+ * "auto_approve_bypasses_command_denylist": false
8286
8330
  * }
8287
8331
  */
8288
8332
  /**
@@ -8334,6 +8378,16 @@ const WarpExecutionProfileOverrideSchema = zod_mini.z.looseObject({
8334
8378
  mcp_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8335
8379
  mcp_denylist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8336
8380
  });
8381
+ /**
8382
+ * Whether auto-approve may run commands that match the command denylist
8383
+ * (Warp defaults to `true`). It is not an execution-profile field: it lives in
8384
+ * the sibling `[agents.warp_agent.other]` table of the same `settings.toml`,
8385
+ * shared by the Warp app and the Warp Agent CLI.
8386
+ *
8387
+ * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8388
+ * @see https://github.com/warpdotdev/warp/blob/main/app/src/settings/ai.rs (`AutoApproveBypassesCommandDenylist`)
8389
+ */
8390
+ const WarpAutoApproveBypassSchema = zod_mini.z.optional(zod_mini.z.boolean());
8337
8391
  const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
8338
8392
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8339
8393
  agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.enum([
@@ -8343,7 +8397,8 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
8343
8397
  ])),
8344
8398
  agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
8345
8399
  agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean()),
8346
- execution_profile: zod_mini.z.optional(WarpExecutionProfileOverrideSchema)
8400
+ execution_profile: zod_mini.z.optional(WarpExecutionProfileOverrideSchema),
8401
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8347
8402
  });
8348
8403
  /**
8349
8404
  * Tool-scoped override block for the standalone Warp Agent CLI. The CLI reads
@@ -8352,16 +8407,21 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
8352
8407
  * the app's legacy `[agents.profiles]` keys), so only the nested
8353
8408
  * `execution_profile` block is offered; it is merged into that record, and the
8354
8409
  * shared `permission.bash` block still drives `command_allowlist` /
8355
- * `command_denylist`.
8410
+ * `command_denylist`. `auto_approve_bypasses_command_denylist` is written to
8411
+ * the sibling `[agents.warp_agent.other]` table.
8356
8412
  *
8357
8413
  * @example
8358
- * { "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" } }
8414
+ * {
8415
+ * "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" },
8416
+ * "auto_approve_bypasses_command_denylist": false
8417
+ * }
8359
8418
  *
8360
8419
  * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8361
8420
  */
8362
8421
  const WarpcliPermissionsOverrideSchema = zod_mini.z.looseObject({
8363
8422
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
8364
- execution_profile: zod_mini.z.optional(WarpExecutionProfileOverrideSchema)
8423
+ execution_profile: zod_mini.z.optional(WarpExecutionProfileOverrideSchema),
8424
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8365
8425
  });
8366
8426
  /**
8367
8427
  * deepagents-cli's approval-mode knobs under `[startup]` in
@@ -9057,7 +9117,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
9057
9117
  copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9058
9118
  crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9059
9119
  goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9060
- omp: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9120
+ omp: zod_mini.z.optional(OmpPermissionsOverrideSchema),
9061
9121
  pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9062
9122
  qoder: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
9063
9123
  grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
@@ -12767,7 +12827,9 @@ const SHARED_CONFIG_OWNERSHIP = {
12767
12827
  ownedKeys: [
12768
12828
  "permissions",
12769
12829
  "tools",
12770
- "security"
12830
+ "security",
12831
+ "skills",
12832
+ "general"
12771
12833
  ]
12772
12834
  }
12773
12835
  }
@@ -13137,7 +13199,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13137
13199
  features: {
13138
13200
  mcp: {
13139
13201
  kind: "replace-owned-keys",
13140
- ownedKeys: ["mcp_servers"]
13202
+ ownedKeys: ["mcp_servers", "disabled_mcp_tools"]
13141
13203
  },
13142
13204
  permissions: {
13143
13205
  kind: "replace-owned-keys",
@@ -13272,6 +13334,14 @@ function serializeSharedConfigFile({ fileKey, document, existingContent, filePat
13272
13334
  });
13273
13335
  }
13274
13336
  /**
13337
+ * A patch value for an owned key that holds a map of managed entries: the map
13338
+ * itself, or `undefined` when it is empty, which retracts the key instead of
13339
+ * leaving an empty object behind (`applySharedConfigPatch` deletes a key whose patch value is `undefined`).
13340
+ */
13341
+ function undefinedWhenEmpty(entries) {
13342
+ return Object.keys(entries).length > 0 ? entries : void 0;
13343
+ }
13344
+ /**
13275
13345
  * Execute a feature's declared write to a gateway-managed shared file: parse
13276
13346
  * the existing content, merge the patch under the feature's declared policy,
13277
13347
  * and serialize it back over the existing content (see
@@ -13758,14 +13828,23 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
13758
13828
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
13759
13829
  const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
13760
13830
  const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
13831
+ const refusedDirs = /* @__PURE__ */ new Map();
13761
13832
  let removedCount = 0;
13762
13833
  for (const aiFile of orphanFiles) {
13763
13834
  const filePath = aiFile.getFilePath();
13764
- if (await refusesDeleteOutsideRoot({
13765
- logger: this.logger,
13766
- rootPath: aiFile.getOutputRoot(),
13767
- filePath
13768
- })) continue;
13835
+ const rootPath = aiFile.getOutputRoot();
13836
+ const dirPath = (0, node_path.dirname)(filePath);
13837
+ const cacheKey = JSON.stringify([rootPath, dirPath]);
13838
+ let refused = refusedDirs.get(cacheKey);
13839
+ if (refused === void 0) {
13840
+ refused = await refusesDeleteOutsideRoot({
13841
+ logger: this.logger,
13842
+ rootPath,
13843
+ dirPath
13844
+ });
13845
+ refusedDirs.set(cacheKey, refused);
13846
+ }
13847
+ if (refused) continue;
13769
13848
  const loggedPath = stripControlCharacters(filePath);
13770
13849
  if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
13771
13850
  else {
@@ -13886,22 +13965,24 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
13886
13965
  return true;
13887
13966
  }
13888
13967
  /**
13889
- * Whether deleting `filePath` has to be refused because a link on the way to
13890
- * the directory holding it leads out of `rootPath`, warning about the refusal.
13968
+ * Whether deleting the files in `dirPath` has to be refused because a link on
13969
+ * the way to that directory leads out of `rootPath`, warning about the refusal
13970
+ * once for the whole directory.
13891
13971
  *
13892
13972
  * Deletion candidates are enumerated without following links *inside* the
13893
13973
  * scanned directory, but the scanned directory itself is reached through
13894
13974
  * whatever links its path contains: a checked-out repository can carry
13895
13975
  * `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
13896
- * an orphan. Only the parent is judged, because removing a file that is itself
13897
- * a link unlinks the link where it is spelled and never touches its target.
13976
+ * an orphan. Only the directory is judged, never the file in it, because
13977
+ * removing a file that is itself a link unlinks the link where it is spelled
13978
+ * and never touches its target.
13898
13979
  */
13899
- async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
13980
+ async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath }) {
13900
13981
  if (!await writablePathEscapesRoot({
13901
13982
  rootPath,
13902
- targetPath: (0, node_path.dirname)(filePath)
13983
+ targetPath: dirPath
13903
13984
  })) return false;
13904
- logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13985
+ logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13905
13986
  return true;
13906
13987
  }
13907
13988
  /**
@@ -17224,7 +17305,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
17224
17305
  static getProjectRelativeDirPath() {
17225
17306
  throw new Error("Please implement this method in the subclass.");
17226
17307
  }
17227
- /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/antigravity/global_workflows`). */
17308
+ /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/config/workflows`). */
17228
17309
  static getGlobalRelativeDirPath() {
17229
17310
  throw new Error("Please implement this method in the subclass.");
17230
17311
  }
@@ -17387,7 +17468,8 @@ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
17387
17468
  const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
17388
17469
  const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
17389
17470
  const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = (0, node_path.join)(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
17390
- const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17471
+ const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "workflows");
17472
+ const ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17391
17473
  const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
17392
17474
  const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
17393
17475
  //#endregion
@@ -17417,10 +17499,12 @@ var AntigravityIdeSkill = class extends AntigravitySharedSkill {
17417
17499
  * Emits each command as a skill in `.agents/skills/<name>/SKILL.md` (project
17418
17500
  * scope) and `~/.gemini/config/skills/<name>/SKILL.md` (global scope), the
17419
17501
  * IDE's skills directories. Legacy workflows are still imported from
17420
- * `.agents/workflows/` and `~/.gemini/antigravity/global_workflows/`. All
17421
- * body and frontmatter handling is shared with {@link AntigravitySharedCommand}.
17502
+ * `.agents/workflows/` and `~/.gemini/config/workflows/` (the global workflows
17503
+ * directory of Antigravity 2.0), and, in global scope only, from the pre-2.0
17504
+ * `~/.gemini/antigravity/global_workflows/`. All body and frontmatter handling
17505
+ * is shared with {@link AntigravitySharedCommand}.
17422
17506
  */
17423
- var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17507
+ var AntigravityIdeCommand = class AntigravityIdeCommand extends AntigravitySharedCommand {
17424
17508
  static getProjectRelativeDirPath() {
17425
17509
  return ANTIGRAVITY_IDE_COMMANDS_DIR_PATH;
17426
17510
  }
@@ -17430,6 +17514,37 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17430
17514
  static getSkillsRelativeDirPath({ global }) {
17431
17515
  return AntigravityIdeSkill.getSettablePaths({ global }).relativeDirPath;
17432
17516
  }
17517
+ /**
17518
+ * Import-only, global scope: the workflows left in the pre-2.0
17519
+ * `~/.gemini/antigravity/global_workflows/` directory, where rulesync also
17520
+ * wrote IDE global commands before they were emitted as skills. The
17521
+ * directory is never swept by `generate --delete`; a workflow with the same
17522
+ * file name in `~/.gemini/config/workflows/` takes precedence.
17523
+ *
17524
+ * @see https://antigravity.google/docs/migration/workflows-to-skills
17525
+ */
17526
+ static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
17527
+ if (!global) return [];
17528
+ const filePaths = await findFilesByGlobs("*.md", { cwd: (0, node_path.join)(outputRoot, ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH) });
17529
+ return (await Promise.all(filePaths.map(async (filePath) => {
17530
+ const fileContent = await readFileContent(filePath);
17531
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
17532
+ const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter);
17533
+ if (!result.success) {
17534
+ logger?.warn(`Skipping ${filePath}: invalid frontmatter (${formatError(result.error)}).`);
17535
+ return;
17536
+ }
17537
+ return new AntigravityIdeCommand({
17538
+ outputRoot,
17539
+ relativeDirPath: ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH,
17540
+ relativeFilePath: (0, node_path.basename)(filePath),
17541
+ frontmatter: result.data,
17542
+ body: body.trim(),
17543
+ fileContent,
17544
+ global
17545
+ });
17546
+ }))).filter((command) => command !== void 0);
17547
+ }
17433
17548
  getToolTargetName() {
17434
17549
  return "antigravity-ide";
17435
17550
  }
@@ -25082,7 +25197,7 @@ function findCommandPaths({ command, scan }) {
25082
25197
  } else {
25083
25198
  if (path !== void 0) toAnchor.add(path);
25084
25199
  state.owner = text.slice(text.lastIndexOf("/") + 1);
25085
- state.expecting = afterCommandWord(state.owner);
25200
+ state.expecting = afterCommandWord$1(state.owner);
25086
25201
  state.prefix = void 0;
25087
25202
  state.shellCode = false;
25088
25203
  }
@@ -25148,7 +25263,7 @@ function readOption({ state, text }) {
25148
25263
  if (SHELL_NAME_PATTERN.test(state.owner) && SHELL_CODE_OPTION.test(text)) state.shellCode = true;
25149
25264
  }
25150
25265
  /** What follows a command word whose basename is `name`. */
25151
- function afterCommandWord(name) {
25266
+ function afterCommandWord$1(name) {
25152
25267
  if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
25153
25268
  if (PACKAGE_RUNNERS.has(name)) return "wrapped";
25154
25269
  return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
@@ -33491,6 +33606,19 @@ function isQwencodeLegacyMillisecondTimeout(timeout) {
33491
33606
  return typeof timeout === "number" && Number.isFinite(timeout) && timeout >= QWENCODE_LEGACY_MILLISECOND_TIMEOUT_THRESHOLD;
33492
33607
  }
33493
33608
  /**
33609
+ * Read a `timeout` the way Qwen Code does. Settings files are not type-checked
33610
+ * and Qwen Code coerces a numeric string such as `"60000"` with `Number()`, so
33611
+ * import accepts one too; a value that is not a finite number is dropped, since
33612
+ * Qwen Code falls back to the hook type's default for it.
33613
+ * https://github.com/QwenLM/qwen-code/blob/main/packages/core/src/hooks/hook-timeout.ts
33614
+ */
33615
+ function readQwencodeTimeout(timeout) {
33616
+ if (timeout === void 0) return void 0;
33617
+ if (typeof timeout === "string" && timeout.trim() === "") return void 0;
33618
+ const value = Number(timeout);
33619
+ return Number.isFinite(value) ? value : void 0;
33620
+ }
33621
+ /**
33494
33622
  * Canonical `timeout` is seconds. A command hook value below the threshold is
33495
33623
  * already read as seconds; a larger one can only be expressed in milliseconds.
33496
33624
  */
@@ -33601,7 +33729,7 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
33601
33729
  type: zod_mini.z.optional(zod_mini.z.string()),
33602
33730
  command: zod_mini.z.optional(zod_mini.z.string()),
33603
33731
  url: zod_mini.z.optional(zod_mini.z.string()),
33604
- timeout: zod_mini.z.optional(zod_mini.z.number()),
33732
+ timeout: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.number(), zod_mini.z.string()])),
33605
33733
  name: zod_mini.z.optional(zod_mini.z.string()),
33606
33734
  description: zod_mini.z.optional(zod_mini.z.string()),
33607
33735
  statusMessage: zod_mini.z.optional(zod_mini.z.string()),
@@ -33634,12 +33762,13 @@ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
33634
33762
  const isCommand = hookType === "command";
33635
33763
  const isPrompt = hookType === "prompt";
33636
33764
  const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
33765
+ const timeout = readQwencodeTimeout(h.timeout);
33637
33766
  return {
33638
33767
  type: hookType,
33639
33768
  ...compact({
33640
33769
  command: h.command,
33641
33770
  url: h.url,
33642
- timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(h.timeout) : h.timeout,
33771
+ timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(timeout) : timeout,
33643
33772
  name: h.name,
33644
33773
  description: h.description,
33645
33774
  statusMessage: h.statusMessage,
@@ -38141,7 +38270,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
38141
38270
  fileKey: sharedConfigFileKey(basePaths),
38142
38271
  feature: "mcp",
38143
38272
  existingContent: fileContent ?? "",
38144
- patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
38273
+ patch: { [AMP_MCP_SERVERS_KEY]: undefinedWhenEmpty(filterMcpServers(rulesyncMcp.getMcpServers())) },
38145
38274
  filePath: (0, node_path.join)(jsonDir, relativeFilePath),
38146
38275
  logger
38147
38276
  }),
@@ -38439,6 +38568,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38439
38568
  const paths = this.getSettablePaths({ global });
38440
38569
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
38441
38570
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
38571
+ const mcpServers = rulesyncMcp.getMcpServers();
38442
38572
  return new AugmentcodeMcp({
38443
38573
  outputRoot,
38444
38574
  relativeDirPath: paths.relativeDirPath,
@@ -38447,7 +38577,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38447
38577
  fileKey: sharedConfigFileKey(paths),
38448
38578
  feature: "mcp",
38449
38579
  existingContent,
38450
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
38580
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
38451
38581
  filePath,
38452
38582
  logger
38453
38583
  }),
@@ -39003,6 +39133,229 @@ var ClaudecodeMcp = class ClaudecodeMcp extends ToolMcp {
39003
39133
  }
39004
39134
  };
39005
39135
  //#endregion
39136
+ //#region src/features/mcp/claudecode-plugin-mcp.ts
39137
+ /**
39138
+ * The placeholder Claude Code substitutes with the installed plugin's own
39139
+ * directory in an MCP server's `command`, `args` and `env`.
39140
+ * @see https://code.claude.com/docs/en/plugins-reference
39141
+ */
39142
+ const PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
39143
+ const PLUGIN_ROOT_PREFIX = /^(?:\$\{CLAUDE_PLUGIN_ROOT\}|"\$CLAUDE_PLUGIN_ROOT"|\$CLAUDE_PLUGIN_ROOT)\//;
39144
+ /**
39145
+ * Whether a server `command` names a file inside the plugin rather than an
39146
+ * executable found on `PATH`. The command is spawned without a shell, so a
39147
+ * name with a slash is a path, and one without (`npx`, `node`, `uvx`) is a
39148
+ * `PATH` lookup. An absolute path, a home path, one that leaves the plugin
39149
+ * (`../`), and one that already starts with a variable are left as written.
39150
+ */
39151
+ function isPluginRelativeCommand(command) {
39152
+ return /[\\/]/.test(command) && !command.startsWith("$") && !command.startsWith("%") && !command.startsWith("~") && !/^\.\.[\\/]/.test(command) && !node_path.posix.isAbsolute(command) && !node_path.win32.isAbsolute(command);
39153
+ }
39154
+ function anchorToPluginRoot(path) {
39155
+ return `${PLUGIN_ROOT_PLACEHOLDER}/${path.replace(/^\.[\\/]/, "")}`;
39156
+ }
39157
+ /** The runner name of a command word: its basename without a `.exe` suffix. */
39158
+ function runnerName(command) {
39159
+ return (command.split(/[\\/]/).pop() ?? command).replace(/\.exe$/i, "");
39160
+ }
39161
+ /** What follows a command word, as plugin hook commands read it. */
39162
+ function afterCommandWord(name) {
39163
+ if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
39164
+ if (PACKAGE_RUNNERS.has(name)) return "command";
39165
+ return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
39166
+ }
39167
+ /**
39168
+ * The positions in `args` that name a file the server runs, read with the
39169
+ * rules plugin hook commands follow (`hook-command-paths.ts`): the script of a
39170
+ * script runner (`node ./dist/server.js`), the command after a run subcommand
39171
+ * or package runner (`uv run ./server.py`, `npx tsx ./x.ts`), and the value of
39172
+ * an option that loads a file (`node -r ./register.js`). Only a position whose
39173
+ * value `isPath` accepts is returned. Everything else is data the server reads
39174
+ * relative to the consumer's project (`server-filesystem ./docs`), and the
39175
+ * scan stops at an option that runs inline code or standard input
39176
+ * (`node -e`, `python3 -m`, `sh -c`, `bash -s`).
39177
+ */
39178
+ function fileArgumentIndices({ command, args, isPath }) {
39179
+ const indices = /* @__PURE__ */ new Set();
39180
+ let owner = runnerName(command);
39181
+ let expecting = afterCommandWord(owner);
39182
+ for (let index = 0; index < args.length && expecting !== "none"; index++) {
39183
+ const arg = args[index];
39184
+ if (typeof arg !== "string") break;
39185
+ if (arg.startsWith("-") && arg !== "-") {
39186
+ if (optionsFor({
39187
+ table: CODE_OPTIONS,
39188
+ name: owner
39189
+ })?.has(arg) || SHELL_NAME_PATTERN.test(owner) && SHELL_CODE_OPTION.test(arg)) break;
39190
+ if (optionsFor({
39191
+ table: VALUE_OPTIONS,
39192
+ name: owner
39193
+ })?.has(arg)) {
39194
+ const value = args[index + 1];
39195
+ if (typeof value === "string" && isPath(value)) indices.add(index + 1);
39196
+ index++;
39197
+ }
39198
+ continue;
39199
+ }
39200
+ if (expecting === "subcommand") {
39201
+ if (RUN_SUBCOMMANDS[owner]?.has(arg)) {
39202
+ expecting = "command";
39203
+ continue;
39204
+ }
39205
+ if (!SCRIPT_RUNNER_PATTERN.test(owner)) break;
39206
+ expecting = "script";
39207
+ }
39208
+ if (isPath(arg)) indices.add(index);
39209
+ if (expecting === "script") break;
39210
+ owner = runnerName(arg);
39211
+ expecting = afterCommandWord(owner);
39212
+ }
39213
+ return indices;
39214
+ }
39215
+ function isDotRelative(value) {
39216
+ return /^\.[\\/]/.test(value);
39217
+ }
39218
+ /** Turn a plugin-root path back into its `./` form, or return it unchanged. */
39219
+ function relativizeFromPluginRoot(value) {
39220
+ return PLUGIN_ROOT_PREFIX.test(value) ? value.replace(PLUGIN_ROOT_PREFIX, "./") : value;
39221
+ }
39222
+ /** The command and arguments of a stdio server, with inline array arguments split out. */
39223
+ function splitCommand(server) {
39224
+ if (Array.isArray(server.command)) {
39225
+ const [first, ...rest] = server.command;
39226
+ return {
39227
+ command: typeof first === "string" ? first : void 0,
39228
+ args: [...rest, ...Array.isArray(server.args) ? server.args : []]
39229
+ };
39230
+ }
39231
+ return {
39232
+ command: typeof server.command === "string" ? server.command : void 0,
39233
+ args: Array.isArray(server.args) ? server.args : []
39234
+ };
39235
+ }
39236
+ /**
39237
+ * Rewrite a server's command and the argument positions that name a file it
39238
+ * runs (see `fileArgumentIndices`), leaving every other argument as written.
39239
+ * Generate and import share this, so a position generate never anchors is
39240
+ * never relativized on import either, and a hand-written
39241
+ * `${CLAUDE_PLUGIN_ROOT}/…` data argument keeps its meaning.
39242
+ */
39243
+ function rewriteServer({ server, rewriteCommand, isPath, rewritePath }) {
39244
+ const { command, args } = splitCommand(server);
39245
+ if (command === void 0) return server;
39246
+ const indices = fileArgumentIndices({
39247
+ command,
39248
+ args,
39249
+ isPath
39250
+ });
39251
+ const rewriteArgs = (values, offset) => values.map((value, index) => indices.has(index + offset) && typeof value === "string" ? rewritePath(value) : value);
39252
+ const next = { ...server };
39253
+ if (Array.isArray(server.command)) {
39254
+ const inlineArgs = server.command.slice(1);
39255
+ next.command = [rewriteCommand(command), ...rewriteArgs(inlineArgs, 0)];
39256
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, inlineArgs.length);
39257
+ } else {
39258
+ next.command = rewriteCommand(command);
39259
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, 0);
39260
+ }
39261
+ return next;
39262
+ }
39263
+ /** Anchor a server's plugin-relative command and the files its runner executes. */
39264
+ function anchorServer(server) {
39265
+ return rewriteServer({
39266
+ server,
39267
+ rewriteCommand: (command) => isPluginRelativeCommand(command) ? anchorToPluginRoot(command) : command,
39268
+ isPath: isDotRelative,
39269
+ rewritePath: anchorToPluginRoot
39270
+ });
39271
+ }
39272
+ /** Turn the plugin-root forms generate writes back into `./`. */
39273
+ function relativizeServer(server) {
39274
+ return rewriteServer({
39275
+ server,
39276
+ rewriteCommand: relativizeFromPluginRoot,
39277
+ isPath: (value) => PLUGIN_ROOT_PREFIX.test(value),
39278
+ rewritePath: relativizeFromPluginRoot
39279
+ });
39280
+ }
39281
+ /** Apply a per-server transform to every server entry that is an object. */
39282
+ function mapServers({ mcpServers, transform }) {
39283
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return mcpServers;
39284
+ return Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => server === null || typeof server !== "object" || Array.isArray(server) ? [name, server] : [name, transform(server)]));
39285
+ }
39286
+ /**
39287
+ * MCP servers bundled in a Claude Code plugin (`.mcp.json` at the plugin
39288
+ * root).
39289
+ *
39290
+ * Claude Code loads a plugin's `.mcp.json` with the consumer's project as the
39291
+ * working directory, so a relative `command` such as `./servers/db-server`
39292
+ * would be looked up in the consumer's repository, where the bundled binary
39293
+ * does not exist. Generate therefore rewrites a relative `command`, and the
39294
+ * `./` script a script runner executes (`node ./dist/server.js`,
39295
+ * `uv run ./server.py`), to `${CLAUDE_PLUGIN_ROOT}/…`, which Claude Code
39296
+ * substitutes with the installed plugin's directory. Other `./` arguments are
39297
+ * data the server reads relative to the consumer's project
39298
+ * (`server-filesystem ./docs`), so they are left as written, the same rule
39299
+ * plugin hook commands follow. Bare executables (`npx`, `node`), absolute paths
39300
+ * and values that already start with a variable pass through. `env` values and
39301
+ * the remote transports' `url` / `headers` are left as written, because
39302
+ * rulesync cannot tell a path from an opaque string there. Import turns the
39303
+ * plugin-root forms in `command` and `args` back into `./`, including the
39304
+ * shell-variable spellings a hand-written file may have copied from a hook
39305
+ * command (which Claude Code does not substitute in MCP config).
39306
+ *
39307
+ * @see https://code.claude.com/docs/en/plugins-reference
39308
+ */
39309
+ var ClaudecodePluginMcp = class ClaudecodePluginMcp extends ClaudecodeMcp {
39310
+ static async fromRulesyncMcp(params) {
39311
+ const base = await super.fromRulesyncMcp(params);
39312
+ const json = base.getJson();
39313
+ const mcpServers = mapServers({
39314
+ mcpServers: json.mcpServers,
39315
+ transform: anchorServer
39316
+ });
39317
+ return new ClaudecodePluginMcp({
39318
+ outputRoot: params.outputRoot ?? process.cwd(),
39319
+ relativeDirPath: base.getRelativeDirPath(),
39320
+ relativeFilePath: base.getRelativeFilePath(),
39321
+ fileContent: JSON.stringify({
39322
+ ...json,
39323
+ mcpServers
39324
+ }, null, 2),
39325
+ validate: params.validate ?? true,
39326
+ global: params.global ?? false
39327
+ });
39328
+ }
39329
+ static async fromFile(params) {
39330
+ const base = await super.fromFile(params);
39331
+ return new ClaudecodePluginMcp({
39332
+ outputRoot: params.outputRoot ?? process.cwd(),
39333
+ relativeDirPath: base.getRelativeDirPath(),
39334
+ relativeFilePath: base.getRelativeFilePath(),
39335
+ fileContent: base.getFileContent(),
39336
+ validate: params.validate ?? true,
39337
+ global: params.global ?? false
39338
+ });
39339
+ }
39340
+ toRulesyncMcp() {
39341
+ const mcpServers = mapServers({
39342
+ mcpServers: this.getJson().mcpServers ?? {},
39343
+ transform: relativizeServer
39344
+ });
39345
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
39346
+ }
39347
+ static forDeletion(params) {
39348
+ return new ClaudecodePluginMcp({
39349
+ outputRoot: params.outputRoot ?? process.cwd(),
39350
+ relativeDirPath: params.relativeDirPath,
39351
+ relativeFilePath: params.relativeFilePath,
39352
+ fileContent: "{}",
39353
+ validate: false,
39354
+ global: params.global ?? false
39355
+ });
39356
+ }
39357
+ };
39358
+ //#endregion
39006
39359
  //#region src/features/mcp/cline-mcp.ts
39007
39360
  const CLINE_GLOBAL_ONLY_MESSAGE = "Cline MCP is global-only; use --global to sync ~/.cline/data/settings/cline_mcp_settings.json";
39008
39361
  function parseClineSettings(fileContent, relativeDirPath, relativeFilePath) {
@@ -41712,7 +42065,7 @@ var CrushMcp = class CrushMcp extends ToolMcp {
41712
42065
  fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
41713
42066
  feature: "mcp",
41714
42067
  existingContent,
41715
- patch: { ["mcp"]: converted },
42068
+ patch: { ["mcp"]: undefinedWhenEmpty(converted) },
41716
42069
  filePath: location.filePath,
41717
42070
  logger
41718
42071
  }),
@@ -43292,13 +43645,23 @@ var GooseMcp = class GooseMcp extends ToolMcp {
43292
43645
  //#region src/features/mcp/grokcli-mcp.ts
43293
43646
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
43294
43647
  /**
43648
+ * Grok keeps per-server MCP tool deny lists in a top-level
43649
+ * `[disabled_mcp_tools]` table (`map<server, string[]>` of unqualified tool
43650
+ * names), not inside `[mcp_servers.<name>]`. Grok reads it from the user
43651
+ * `~/.grok/config.toml` only: a project `.grok/config.toml` contributes just
43652
+ * `[mcp_servers]`, `[plugins]`, `[permission]` and `[mcp] max_output_bytes`.
43653
+ * https://github.com/xai-org/grok-build/blob/2bdd1d6a/crates/codegen/xai-grok-pager/docs/user-guide/26-config-reference.md
43654
+ */
43655
+ const GROK_DISABLED_MCP_TOOLS_KEY = "disabled_mcp_tools";
43656
+ /**
43295
43657
  * Grok Build stores MCP servers in `config.toml` under a `[mcp_servers.<name>]`
43296
43658
  * table. Verified against `grok mcp add` (grok 0.2.54): a stdio server emits
43297
43659
  * `command`, `args`, `enabled = true`, and an `[mcp_servers.<name>.env]` table;
43298
43660
  * a remote server emits `url` and `enabled`. Unlike Codex CLI, Grok uses a
43299
- * literal `env` table (not the `env_vars` passthrough list) and has no
43300
- * per-server tool allow/deny lists, so the only field rename is
43301
- * `disabled` (rulesync) ↔ `enabled = false` (grok).
43661
+ * literal `env` table (not the `env_vars` passthrough list), so the only field
43662
+ * rename is `disabled` (rulesync) ↔ `enabled = false` (grok). `disabledTools`
43663
+ * never lands in the server table: it is lifted into the top-level
43664
+ * `[disabled_mcp_tools]` table by `buildDisabledMcpTools`.
43302
43665
  */
43303
43666
  function convertToGrokFormat(mcpServers) {
43304
43667
  const result = {};
@@ -43307,7 +43670,7 @@ function convertToGrokFormat(mcpServers) {
43307
43670
  if (!isRecord$1(config)) continue;
43308
43671
  const converted = {};
43309
43672
  for (const [key, value] of Object.entries(config)) {
43310
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
43673
+ if (PROTOTYPE_POLLUTION_KEYS.has(key) || key === "disabledTools") continue;
43311
43674
  if (key === "disabled") {
43312
43675
  if (value === true) converted["enabled"] = false;
43313
43676
  } else converted[key] = value;
@@ -43331,6 +43694,38 @@ function convertFromGrokFormat(grokMcp) {
43331
43694
  }
43332
43695
  return result;
43333
43696
  }
43697
+ function toToolNameList(value) {
43698
+ if (!Array.isArray(value)) return [];
43699
+ return value.filter((tool) => typeof tool === "string");
43700
+ }
43701
+ /**
43702
+ * Recompute the global `[disabled_mcp_tools]` table: every server Rulesync
43703
+ * declares gets its `disabledTools` (or loses a stale entry when it has none —
43704
+ * Grok itself removes an entry whose list empties), while entries for other
43705
+ * servers — ones toggled in Grok's `/mcps` UI, plugin or compat servers, and
43706
+ * the `__managed_gateway_connectors` sentinel — are carried over untouched.
43707
+ * Returns `undefined` when nothing is left, so the key is retracted.
43708
+ */
43709
+ function buildDisabledMcpTools({ mcpServers, existingContent, filePath }) {
43710
+ const existing = parseSharedConfig({
43711
+ format: "toml",
43712
+ fileContent: existingContent,
43713
+ filePath
43714
+ })[GROK_DISABLED_MCP_TOOLS_KEY];
43715
+ const result = isRecord$1(existing) ? { ...existing } : {};
43716
+ for (const [name, config] of Object.entries(mcpServers)) {
43717
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
43718
+ const tools = toToolNameList(config.disabledTools);
43719
+ if (tools.length > 0) result[name] = tools;
43720
+ else delete result[name];
43721
+ }
43722
+ return Object.keys(result).length > 0 ? result : void 0;
43723
+ }
43724
+ function warnProjectDisabledTools(mcpServers, logger) {
43725
+ const serverNames = Object.entries(mcpServers).filter(([, config]) => isRecord$1(config) && config.disabledTools !== void 0).map(([name]) => name);
43726
+ if (serverNames.length === 0) return;
43727
+ 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.`);
43728
+ }
43334
43729
  var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43335
43730
  toml;
43336
43731
  constructor({ ...rest }) {
@@ -43367,16 +43762,19 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43367
43762
  relativeDirPath: paths.relativeDirPath,
43368
43763
  relativeFilePath: paths.relativeFilePath,
43369
43764
  fileContent,
43370
- validate
43765
+ validate,
43766
+ global
43371
43767
  });
43372
43768
  }
43373
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
43769
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
43374
43770
  const paths = this.getSettablePaths({ global });
43375
43771
  const configTomlFilePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43376
43772
  const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
43377
- const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
43773
+ const strippedMcpServers = rulesyncMcp.getMcpServers();
43774
+ const converted = convertToGrokFormat(strippedMcpServers);
43378
43775
  const filteredMcpServers = this.removeEmptyEntries(converted);
43379
- 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`);
43776
+ if (!global) warnProjectDisabledTools(strippedMcpServers, logger);
43777
+ 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`);
43380
43778
  return new GrokcliMcp({
43381
43779
  outputRoot,
43382
43780
  relativeDirPath: paths.relativeDirPath,
@@ -43385,14 +43783,31 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43385
43783
  fileKey: sharedConfigFileKey(paths),
43386
43784
  feature: "mcp",
43387
43785
  existingContent: configTomlFileContent,
43388
- patch: { mcp_servers: filteredMcpServers },
43786
+ patch: {
43787
+ mcp_servers: Object.keys(filteredMcpServers).length > 0 ? filteredMcpServers : void 0,
43788
+ ...global && { [GROK_DISABLED_MCP_TOOLS_KEY]: buildDisabledMcpTools({
43789
+ mcpServers: strippedMcpServers,
43790
+ existingContent: configTomlFileContent,
43791
+ filePath: configTomlFilePath
43792
+ }) }
43793
+ },
43389
43794
  filePath: configTomlFilePath
43390
43795
  }),
43391
- validate
43796
+ validate,
43797
+ global
43392
43798
  });
43393
43799
  }
43394
43800
  toRulesyncMcp() {
43395
43801
  const converted = convertFromGrokFormat(this.toml.mcp_servers ?? {});
43802
+ const disabledMcpTools = this.toml[GROK_DISABLED_MCP_TOOLS_KEY];
43803
+ if (this.global && isRecord$1(disabledMcpTools)) for (const [name, config] of Object.entries(converted)) {
43804
+ if (!Object.hasOwn(disabledMcpTools, name)) continue;
43805
+ const tools = toToolNameList(disabledMcpTools[name]);
43806
+ if (tools.length > 0) converted[name] = {
43807
+ ...config,
43808
+ disabledTools: tools
43809
+ };
43810
+ }
43396
43811
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
43397
43812
  }
43398
43813
  validate() {
@@ -46456,7 +46871,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
46456
46871
  fileKey: sharedConfigFileKey(paths),
46457
46872
  feature: "mcp",
46458
46873
  existingContent,
46459
- patch: { [POOL_MCP_SERVERS_KEY]: converted },
46874
+ patch: { [POOL_MCP_SERVERS_KEY]: undefinedWhenEmpty(converted) },
46460
46875
  filePath
46461
46876
  }),
46462
46877
  validate,
@@ -46586,7 +47001,7 @@ var QoderMcp = class QoderMcp extends ToolMcp {
46586
47001
  fileKey: sharedConfigFileKey(paths),
46587
47002
  feature: "mcp",
46588
47003
  existingContent,
46589
- patch: { mcpServers },
47004
+ patch: { mcpServers: undefinedWhenEmpty(mcpServers) },
46590
47005
  filePath
46591
47006
  }) : JSON.stringify({
46592
47007
  ...parseQoderMcpConfig({
@@ -46965,18 +47380,28 @@ function warnAboutRetiredFields({ name, serverRecord, logger }) {
46965
47380
  * `" Serial "` counts too and is written as authored.
46966
47381
  */
46967
47382
  const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
46968
- /**
46969
- * The type Reasonix decodes each fixed-type scalar passthrough field into
46970
- * (`PluginEntry` in `internal/config/plugin_entry.go`, and
46971
- * `internal/contract/config/plugin_entry.go` on the CLI v2 line).
46972
- */
46973
47383
  const REASONIX_TYPED_PLUGIN_FIELDS = {
47384
+ startup_timeout_seconds: "integer",
47385
+ call_timeout_seconds: "integer",
47386
+ tool_timeout_seconds: "integer table",
46974
47387
  concurrency: "string",
46975
47388
  auto_start: "boolean",
46976
47389
  load: "string",
46977
47390
  oauth_allow_missing_pkce_metadata: "boolean"
46978
47391
  };
46979
47392
  /**
47393
+ * Whether `value` is written as a TOML integer. smol-toml writes a JS number
47394
+ * outside the safe-integer range, or with a fraction, as a TOML float.
47395
+ */
47396
+ function isTomlInteger(value) {
47397
+ return typeof value === "number" && Number.isSafeInteger(value);
47398
+ }
47399
+ function hasExpectedType(value, expected) {
47400
+ if (expected === "integer") return isTomlInteger(value);
47401
+ if (expected === "integer table") return isRecord$1(value) && Object.values(value).every(isTomlInteger);
47402
+ return typeof value === expected;
47403
+ }
47404
+ /**
46980
47405
  * Why a passthrough value cannot be written, or `undefined` when it can (and
46981
47406
  * for every field without a fixed type). A wrong type is the case that matters:
46982
47407
  * a TOML type mismatch makes Reasonix refuse the whole config file. An unknown
@@ -46988,7 +47413,7 @@ const REASONIX_TYPED_PLUGIN_FIELDS = {
46988
47413
  */
46989
47414
  function invalidPassthroughFieldReason(field, value) {
46990
47415
  const expected = REASONIX_TYPED_PLUGIN_FIELDS[field];
46991
- if (expected !== void 0 && typeof value !== expected) return `Reasonix expects a ${expected} and fails to load a config file holding any other type.`;
47416
+ 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.`;
46992
47417
  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.`;
46993
47418
  }
46994
47419
  /**
@@ -48010,7 +48435,7 @@ var TabnineMcp = class TabnineMcp extends ToolMcp {
48010
48435
  fileKey: sharedConfigFileKey(paths),
48011
48436
  feature: "mcp",
48012
48437
  existingContent,
48013
- patch: { mcpServers: convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger) },
48438
+ patch: { mcpServers: undefinedWhenEmpty(convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger)) },
48014
48439
  filePath
48015
48440
  }),
48016
48441
  validate,
@@ -49331,6 +49756,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49331
49756
  const paths = this.getSettablePaths({ global });
49332
49757
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
49333
49758
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
49759
+ const contextServers = convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger);
49334
49760
  return new ZedMcp({
49335
49761
  outputRoot,
49336
49762
  relativeDirPath: paths.relativeDirPath,
@@ -49339,7 +49765,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49339
49765
  fileKey: sharedConfigFileKey(paths),
49340
49766
  feature: "mcp",
49341
49767
  existingContent,
49342
- patch: { context_servers: convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger) },
49768
+ patch: { context_servers: Object.keys(contextServers).length > 0 ? contextServers : void 0 },
49343
49769
  filePath
49344
49770
  }),
49345
49771
  validate
@@ -49472,7 +49898,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49472
49898
  }
49473
49899
  }],
49474
49900
  ["claudecode-plugin", {
49475
- class: ClaudecodeMcp,
49901
+ class: ClaudecodePluginMcp,
49476
49902
  meta: {
49477
49903
  supportsProject: true,
49478
49904
  supportsGlobal: false,
@@ -49648,7 +50074,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49648
50074
  supportsProject: true,
49649
50075
  supportsGlobal: true,
49650
50076
  supportsEnabledTools: false,
49651
- supportsDisabledTools: false
50077
+ supportsDisabledTools: true
49652
50078
  }
49653
50079
  }],
49654
50080
  ["hermesagent", {
@@ -50814,6 +51240,8 @@ function parsedGlobsIntersect(left, right, budget) {
50814
51240
  * @see https://antigravity.google/docs/permissions?tab=cli
50815
51241
  */
50816
51242
  const REGEX_PREFIX = "regex:";
51243
+ const TRAILING_ANCHOR = /(?<!\\)\$$/;
51244
+ const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
50817
51245
  const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
50818
51246
  const WHITESPACE_CODES = [
50819
51247
  9,
@@ -50954,6 +51382,7 @@ function replacePosixClasses(source) {
50954
51382
  function isInvalidRegex(source) {
50955
51383
  const unquoted = source.replace(RE2_QUOTED, (match) => match === "\\\\" ? match : "q");
50956
51384
  if (LOOKAROUND.test(unquoted) || hasRefusedEscape(unquoted)) return true;
51385
+ if (hasClassEscapeRangeEnd(unquoted)) return true;
50957
51386
  const withoutPosixClasses = replacePosixClasses(unquoted);
50958
51387
  if (withoutPosixClasses === void 0 || hasRefusedRepeat(withoutPosixClasses)) return true;
50959
51388
  const asJavaScript = withoutPosixClasses.replace(/\(\?P</g, "(?<").replace(/\(\?[imsU]*-?[imsU]*\)/g, "").replace(/\(\?[imsU]*-?[imsU]*:/g, "(?:").replace(/\\A/g, "^").replace(/\\z/g, "$");
@@ -50978,6 +51407,74 @@ function hasRefusedEscape(source) {
50978
51407
  }
50979
51408
  return false;
50980
51409
  }
51410
+ const CLASS_ESCAPE_LETTERS = /[dDsSwWpP]/;
51411
+ /** The length of the escape at `index`, braces (`\x{41}`, `\p{Greek}`) included. */
51412
+ function escapeLength(source, index) {
51413
+ const escaped = source[index + 1] ?? "";
51414
+ if (/[xpP]/.test(escaped) && source[index + 2] === "{") {
51415
+ const close = source.indexOf("}", index + 3);
51416
+ return close === -1 ? source.length - index : close - index + 1;
51417
+ }
51418
+ if (escaped === "p" || escaped === "P") return 3;
51419
+ return escaped === "x" ? 4 : 2;
51420
+ }
51421
+ /** Step past `^` and a leading `]` (a member) after the `[` that ends at `index`. */
51422
+ function openClass(source, start) {
51423
+ const index = source[start] === "^" ? start + 1 : start;
51424
+ return source[index] === "]" ? {
51425
+ index: index + 1,
51426
+ afterCharacter: true
51427
+ } : {
51428
+ index,
51429
+ afterCharacter: false
51430
+ };
51431
+ }
51432
+ /**
51433
+ * Whether a bracket range ends in a class escape (`[a-\d]`, `[--\s]`). Go
51434
+ * reads the end of a range as one character and refuses such a regex, while
51435
+ * JavaScript without the `u` flag reads it as three members. A class escape
51436
+ * that starts the range (`[\d-z]`) is a member followed by `-`, which both
51437
+ * accept. The `u` flag cannot be used instead: it also refuses RE2 spellings
51438
+ * such as `\x{41}` and `\-` outside a class.
51439
+ */
51440
+ function hasClassEscapeRangeEnd(source) {
51441
+ let inClass = false;
51442
+ let afterCharacter = false;
51443
+ let index = 0;
51444
+ while (index < source.length) {
51445
+ const character = source[index] ?? "";
51446
+ if (character === "\\") {
51447
+ afterCharacter = inClass && !CLASS_ESCAPE_LETTERS.test(source[index + 1] ?? "");
51448
+ index += escapeLength(source, index);
51449
+ continue;
51450
+ }
51451
+ if (!inClass) {
51452
+ inClass = character === "[";
51453
+ ({index, afterCharacter} = inClass ? openClass(source, index + 1) : {
51454
+ index: index + 1,
51455
+ afterCharacter: false
51456
+ });
51457
+ continue;
51458
+ }
51459
+ const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
51460
+ if (character === "]" || posixEnd !== -1) {
51461
+ inClass = character !== "]";
51462
+ afterCharacter = false;
51463
+ index = posixEnd === -1 ? index + 1 : posixEnd + 2;
51464
+ continue;
51465
+ }
51466
+ if (character === "-" && afterCharacter && source[index + 1] !== "]") {
51467
+ const end = index + 1;
51468
+ if (source[end] === "\\" && CLASS_ESCAPE_LETTERS.test(source[end + 1] ?? "")) return true;
51469
+ afterCharacter = false;
51470
+ index = end + (source[end] === "\\" ? escapeLength(source, end) : 1);
51471
+ continue;
51472
+ }
51473
+ afterCharacter = true;
51474
+ index += 1;
51475
+ }
51476
+ return false;
51477
+ }
50981
51478
  /**
50982
51479
  * Whether a repeat count is over Go's limit. Braces inside a bracket class
50983
51480
  * (`[a{1001}]`) and in `\x{2003}` or `\p{Greek}` are not repeats, so those
@@ -51125,7 +51622,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
51125
51622
  * the rule a regex.
51126
51623
  */
51127
51624
  function regexWordToGlob(word) {
51128
- const body = word.replace(/^\^/, "").replace(/(?<!\\)\$$/, "");
51625
+ const body = word.replace(/^\^/, "").replace(TRAILING_ANCHOR, "");
51129
51626
  const characters = Array.from(body);
51130
51627
  if (characters.length === 0) return;
51131
51628
  let glob = "";
@@ -51166,12 +51663,61 @@ function regexToGlob(source) {
51166
51663
  function fromAntigravityCommandTarget(target) {
51167
51664
  if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
51168
51665
  if (target === "*") return target;
51169
- if (/[*?[{\\]/.test(target)) {
51666
+ if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
51170
51667
  const words = splitWords(target);
51171
51668
  return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
51172
51669
  }
51173
51670
  return `${target} *`;
51174
51671
  }
51672
+ /**
51673
+ * Whether a regex word means the same anchored or not: each end is either
51674
+ * anchored (`^`, an unescaped `$`) or open-ended (`.*`).
51675
+ */
51676
+ function isAnchoredWord(word) {
51677
+ const startAnchored = word.startsWith("^") || word.startsWith(".*");
51678
+ const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
51679
+ return startAnchored && endAnchored;
51680
+ }
51681
+ /**
51682
+ * Explain what importing an Antigravity `command` target may lose, or return
51683
+ * `undefined` when the import is faithful.
51684
+ *
51685
+ * - A `regex:` target with a word that is not anchored is imported as a glob
51686
+ * that assumes anchoring. If Antigravity matches such a word unanchored,
51687
+ * the imported rule is narrower (`regex:rm` also matches `rmdir`).
51688
+ * - A plain target holding characters that are special in a glob is most
51689
+ * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
51690
+ * Antigravity matches it literally, so it imports as a regex that matches a
51691
+ * literal `*` word and is inert in the other tools it is exported to.
51692
+ */
51693
+ function describeAntigravityCommandImportLoss(target) {
51694
+ if (target.startsWith(REGEX_PREFIX)) {
51695
+ const source = target.slice(6);
51696
+ if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
51697
+ 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",
51698
+ denyConsequence: "The imported deny may block less than it does in Antigravity."
51699
+ };
51700
+ return;
51701
+ }
51702
+ if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
51703
+ 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",
51704
+ denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
51705
+ };
51706
+ }
51707
+ /**
51708
+ * Import one Antigravity `command` target as a canonical bash pattern, warning
51709
+ * when the result may differ from what Antigravity enforces. Shared by the
51710
+ * Antigravity CLI and IDE importers.
51711
+ */
51712
+ function importAntigravityCommandTarget({ target, entry, decision, logger, toolLabel }) {
51713
+ const pattern = fromAntigravityCommandTarget(target);
51714
+ const loss = describeAntigravityCommandImportLoss(target);
51715
+ if (loss !== void 0) {
51716
+ const consequence = decision === "deny" ? ` ${loss.denyConsequence}` : "";
51717
+ logger.warn(`${toolLabel} permissions: importing ${entry} as "${pattern}": ${loss.reason}.${consequence}`);
51718
+ }
51719
+ return pattern;
51720
+ }
51175
51721
  //#endregion
51176
51722
  //#region src/features/permissions/antigravity-targets.ts
51177
51723
  /**
@@ -51202,6 +51748,7 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
51202
51748
  "**/*"
51203
51749
  ]);
51204
51750
  const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
51751
+ const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
51205
51752
  const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
51206
51753
  const DENY_HINTS = {
51207
51754
  read_file: "deny a directory or file path instead",
@@ -51284,6 +51831,10 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
51284
51831
  return;
51285
51832
  }
51286
51833
  if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
51834
+ if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
51835
+ const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
51836
+ 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.`);
51837
+ }
51287
51838
  return result.target === "*" ? action : `${action}(${result.target})`;
51288
51839
  }
51289
51840
  //#endregion
@@ -51850,9 +52401,9 @@ function convertRulesyncToAntigravityCliPermissions({ config, logger }) {
51850
52401
  }
51851
52402
  }
51852
52403
  return {
51853
- allow,
51854
- ask,
51855
- deny
52404
+ allow: [...new Set(allow)],
52405
+ ask: [...new Set(ask)],
52406
+ deny: [...new Set(deny)]
51856
52407
  };
51857
52408
  }
51858
52409
  /**
@@ -51864,7 +52415,13 @@ function convertAntigravityCliToRulesyncPermissions(params) {
51864
52415
  for (const entry of entries) {
51865
52416
  const { toolName, pattern: target } = parsePermissionEntry$1(entry);
51866
52417
  const canonical = toCanonicalToolName$9(toolName);
51867
- const pattern = toolName === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52418
+ const pattern = toolName === "command" ? importAntigravityCommandTarget({
52419
+ target,
52420
+ entry,
52421
+ decision: action,
52422
+ logger: moduleLogger$4,
52423
+ toolLabel: "Antigravity CLI"
52424
+ }) : target;
51868
52425
  if (!permission[canonical]) permission[canonical] = {};
51869
52426
  permission[canonical][pattern] = action;
51870
52427
  }
@@ -52068,9 +52625,9 @@ function convertRulesyncToAntigravityIdePermissions({ config, logger }) {
52068
52625
  }
52069
52626
  }
52070
52627
  return {
52071
- allow,
52072
- ask,
52073
- deny
52628
+ allow: [...new Set(allow)],
52629
+ ask: [...new Set(ask)],
52630
+ deny: [...new Set(deny)]
52074
52631
  };
52075
52632
  }
52076
52633
  /**
@@ -52082,7 +52639,13 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
52082
52639
  for (const entry of entries) {
52083
52640
  const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
52084
52641
  const canonical = toCanonicalCategory$2(ideAction);
52085
- const pattern = ideAction === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52642
+ const pattern = ideAction === "command" ? importAntigravityCommandTarget({
52643
+ target,
52644
+ entry,
52645
+ decision: action,
52646
+ logger: fallbackLogger,
52647
+ toolLabel: "Antigravity IDE"
52648
+ }) : target;
52086
52649
  if (!permission[canonical]) permission[canonical] = {};
52087
52650
  permission[canonical][pattern] = action;
52088
52651
  }
@@ -54064,7 +54627,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54064
54627
  managedSourcesBehavior: "managed settings",
54065
54628
  modelPricing: "managed settings",
54066
54629
  parentSettingsBehavior: "managed settings",
54067
- permissionExplainerEnabled: "~/.claude.json",
54068
54630
  pluginSuggestionMarketplaces: "managed settings",
54069
54631
  pluginTrustMessage: "managed settings",
54070
54632
  prStatusFooterEnabled: "~/.claude.json",
@@ -54073,10 +54635,24 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54073
54635
  sshHostAllowlist: "managed settings",
54074
54636
  strictKnownMarketplaces: "managed settings",
54075
54637
  strictPluginOnlyCustomization: "managed settings",
54076
- teammateDefaultModel: "~/.claude.json",
54077
54638
  wslInheritsWindowsSettings: "managed settings"
54078
54639
  };
54079
54640
  /**
54641
+ * Top-level settings keys Claude Code has removed, with the version that
54642
+ * removed each. Current versions ignore them wherever they are set, so they
54643
+ * are dropped in **both** scopes, and the warning names the version so the
54644
+ * author knows to delete the key rather than move it to another file.
54645
+ *
54646
+ * Derived from the "Removed in" notes of the settings reference.
54647
+ *
54648
+ * @see https://code.claude.com/docs/en/settings-reference
54649
+ */
54650
+ const CLAUDECODE_REMOVED_KEYS = {
54651
+ permissionExplainerEnabled: "v2.1.257",
54652
+ taskOutputMaxChars: "v2.1.277",
54653
+ teammateDefaultModel: "v2.1.234"
54654
+ };
54655
+ /**
54080
54656
  * Top-level settings keys whose value Claude Code **executes**. The generic
54081
54657
  * passthrough refuses them outright rather than warning: `.rulesync/*` files
54082
54658
  * are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
@@ -54356,6 +54932,15 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
54356
54932
  allowedMarketplaces: "strictKnownMarketplaces"
54357
54933
  };
54358
54934
  /**
54935
+ * The warning for a key that neither scope can honor — one Claude Code has
54936
+ * removed, or one read only from a file rulesync does not generate — or
54937
+ * `undefined` when the key may be written in at least one scope.
54938
+ */
54939
+ function neverHonoredKeyWarning({ canonicalKey, shown, relativeFilePath }) {
54940
+ 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.`;
54941
+ 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.`;
54942
+ }
54943
+ /**
54359
54944
  * Copy of the authored top-level passthrough with the keys the target file
54360
54945
  * cannot honor removed, warning once per dropped key. Like
54361
54946
  * `stripSandboxPaths`, only the override copy is filtered — a value
@@ -54372,8 +54957,13 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
54372
54957
  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.`);
54373
54958
  continue;
54374
54959
  }
54375
- if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
54376
- 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.`);
54960
+ const neverHonored = neverHonoredKeyWarning({
54961
+ canonicalKey,
54962
+ shown,
54963
+ relativeFilePath
54964
+ });
54965
+ if (neverHonored !== void 0) {
54966
+ logger?.warn(neverHonored);
54377
54967
  continue;
54378
54968
  }
54379
54969
  if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
@@ -62064,8 +62654,10 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
62064
62654
  * table. The gateway replaces the owned `permission` key wholesale and rulesync
62065
62655
  * only ever authors `rules`, so a hand-written sibling such as
62066
62656
  * `dangerous_command_guard = false` (Kimi Code 0.40.0) would otherwise be
62067
- * deleted on every generate. Rulesync does not model these keys; they pass
62068
- * through verbatim and `rules` always comes from the patch.
62657
+ * deleted on every generate. Unmodeled keys pass through verbatim; `rules`
62658
+ * always comes from the patch, and so does `dangerous_command_guard` when the
62659
+ * `kimi-code.dangerousCommandGuard` override sets it. Leaving the override out
62660
+ * keeps whatever the file already has.
62069
62661
  *
62070
62662
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
62071
62663
  */
@@ -62308,14 +62900,18 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62308
62900
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, logger }) {
62309
62901
  const config = rulesyncPermissions.getJson();
62310
62902
  const defaultPermissionMode = config["kimi-code"]?.defaultPermissionMode;
62903
+ const dangerousCommandGuard = config["kimi-code"]?.dangerousCommandGuard;
62311
62904
  const tools = buildKimiCodeToolsSection(config["kimi-code"]?.tools);
62312
62905
  const document = {
62313
62906
  ...defaultPermissionMode && { default_permission_mode: defaultPermissionMode },
62314
62907
  ...tools && { tools },
62315
- permission: { rules: canonicalToKimiCodeRules({
62316
- config,
62317
- logger
62318
- }) }
62908
+ permission: {
62909
+ ...typeof dangerousCommandGuard === "boolean" && { dangerous_command_guard: dangerousCommandGuard },
62910
+ rules: canonicalToKimiCodeRules({
62911
+ config,
62912
+ logger
62913
+ })
62914
+ }
62319
62915
  };
62320
62916
  let fileContent;
62321
62917
  try {
@@ -62337,11 +62933,14 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62337
62933
  format: "toml",
62338
62934
  fileContent: this.getFileContent()
62339
62935
  });
62340
- const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
62936
+ const permissionConfig = isRecord$1(config.permission) ? config.permission : {};
62937
+ const { permission, nativeRules } = preserveKimiCodeRules(permissionConfig.rules);
62341
62938
  const defaultPermissionMode = config.default_permission_mode;
62939
+ const dangerousCommandGuard = permissionConfig.dangerous_command_guard;
62342
62940
  const tools = buildKimiCodeToolsSection(config.tools);
62343
62941
  const toolOverride = {
62344
62942
  ...defaultPermissionMode === "manual" || defaultPermissionMode === "yolo" || defaultPermissionMode === "auto" ? { defaultPermissionMode } : {},
62943
+ ...typeof dangerousCommandGuard === "boolean" && { dangerousCommandGuard },
62345
62944
  ...nativeRules.length > 0 && { rules: nativeRules },
62346
62945
  ...tools && { tools }
62347
62946
  };
@@ -63223,6 +63822,7 @@ var MimocodePermissions = class extends OpencodePermissions {
63223
63822
  //#endregion
63224
63823
  //#region src/features/permissions/omp-permissions.ts
63225
63824
  const CATCH_ALL_PATTERN$4 = "*";
63825
+ const APPROVAL_MODE_KEY = "approvalMode";
63226
63826
  const ACTION_TO_OMP_APPROVAL = {
63227
63827
  allow: "allow",
63228
63828
  ask: "prompt",
@@ -63278,10 +63878,14 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
63278
63878
  * commands, and its deny / ask also restrict every other tool's policy (a
63279
63879
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
63280
63880
  *
63881
+ * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
63882
+ * `write` or `yolo`); without it the key is left as it is in the file, and on
63883
+ * import a recognized value is read back into the override.
63884
+ *
63281
63885
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
63282
- * (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
63283
- * such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
63284
- * never deleted.
63886
+ * (the keys rulesync manages), `bash.patterns` and an authored
63887
+ * `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
63888
+ * kept, and the file is never deleted.
63285
63889
  *
63286
63890
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
63287
63891
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
@@ -63324,16 +63928,19 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
63324
63928
  fileContent: existingContent,
63325
63929
  filePath
63326
63930
  });
63931
+ const config = rulesyncPermissions.getJson();
63327
63932
  const { approval, patterns, floor } = convertRulesyncToOmp({
63328
- config: rulesyncPermissions.getJson(),
63933
+ config,
63329
63934
  logger
63330
63935
  });
63936
+ const approvalMode = config.omp?.approvalMode;
63331
63937
  const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
63332
63938
  const nextApproval = {
63333
63939
  ...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)])) : {},
63334
63940
  ...approval
63335
63941
  };
63336
63942
  const toolsSiblings = withoutKey(existingTools, "approval");
63943
+ if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
63337
63944
  const nextTools = Object.keys(nextApproval).length > 0 ? {
63338
63945
  ...toolsSiblings,
63339
63946
  approval: nextApproval
@@ -63589,7 +64196,14 @@ function convertOmpToRulesync(config) {
63589
64196
  permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
63590
64197
  }
63591
64198
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
63592
- return { permission };
64199
+ const approvalMode = tools[APPROVAL_MODE_KEY];
64200
+ return {
64201
+ permission,
64202
+ ...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
64203
+ };
64204
+ }
64205
+ function isOmpApprovalMode(value) {
64206
+ return OMP_APPROVAL_MODES.some((mode) => mode === value);
63593
64207
  }
63594
64208
  //#endregion
63595
64209
  //#region src/features/permissions/pi-permissions.ts
@@ -64629,6 +65243,13 @@ const QWEN_OVERRIDE_SECURITY_KEYS = [
64629
65243
  "allowPrivateNetworkHooks",
64630
65244
  "allowedInsecureVoiceBaseUrls"
64631
65245
  ];
65246
+ const QWEN_OVERRIDE_SKILLS_KEYS = [
65247
+ "disabled",
65248
+ "defaultDisabled",
65249
+ "enabled",
65250
+ "disabledLevels"
65251
+ ];
65252
+ const QWEN_OVERRIDE_GENERAL_KEYS = ["outputStyle"];
64632
65253
  /**
64633
65254
  * What each rule means for the two directions, so a key's behavior is declared
64634
65255
  * once beside its rule rather than spelled out at every branch.
@@ -64685,77 +65306,125 @@ const QWEN_SCOPE_RULES = {
64685
65306
  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."
64686
65307
  }
64687
65308
  };
64688
- const QWEN_OVERRIDE_GROUPS = [{
64689
- groupName: "tools",
64690
- overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
64691
- scopedKeys: {
64692
- workflowsEnabled: { rule: "workspace-stripped" },
64693
- executionSandbox: {
64694
- rule: "workspace-stripped",
64695
- grants: () => true,
64696
- 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)."
64697
- },
64698
- workflowNameOnly: {
64699
- rule: "global-machine-wide",
64700
- 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.`,
64701
- 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."
64702
- },
64703
- approvalMode: {
64704
- rule: "global-machine-wide",
64705
- projectNote: autonomyProjectNote
64706
- },
64707
- autoAccept: {
64708
- rule: "global-machine-wide",
64709
- projectNote: autonomyProjectNote
64710
- },
64711
- sandbox: {
64712
- rule: "global-machine-wide",
64713
- projectNote: autonomyProjectNote
64714
- },
64715
- sandboxImage: {
64716
- rule: "global-machine-wide",
64717
- 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.`,
64718
- 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."
64719
- },
64720
- disabled: {
64721
- rule: "global-machine-wide",
64722
- 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.`,
64723
- 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."
64724
- },
64725
- visible: {
64726
- rule: "global-machine-wide",
64727
- 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.`,
64728
- 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."
64729
- },
64730
- eager: {
64731
- rule: "global-machine-wide",
64732
- 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.`,
64733
- 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."
64734
- },
64735
- listDirectory: {
64736
- rule: "global-machine-wide",
64737
- 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.`,
64738
- 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."
64739
- },
64740
- todoWrite: {
65309
+ const QWEN_SCOPED_TOOLS_KEYS = {
65310
+ workflowsEnabled: { rule: "workspace-stripped" },
65311
+ executionSandbox: {
65312
+ rule: "workspace-stripped",
65313
+ grants: () => true,
65314
+ 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)."
65315
+ },
65316
+ workflowNameOnly: {
65317
+ rule: "global-machine-wide",
65318
+ 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.`,
65319
+ 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."
65320
+ },
65321
+ approvalMode: {
65322
+ rule: "global-machine-wide",
65323
+ projectNote: autonomyProjectNote
65324
+ },
65325
+ autoAccept: {
65326
+ rule: "global-machine-wide",
65327
+ projectNote: autonomyProjectNote
65328
+ },
65329
+ sandbox: {
65330
+ rule: "global-machine-wide",
65331
+ projectNote: autonomyProjectNote
65332
+ },
65333
+ sandboxImage: {
65334
+ rule: "global-machine-wide",
65335
+ 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.`,
65336
+ 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."
65337
+ },
65338
+ disabled: {
65339
+ rule: "global-machine-wide",
65340
+ 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.`,
65341
+ 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."
65342
+ },
65343
+ visible: {
65344
+ rule: "global-machine-wide",
65345
+ 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.`,
65346
+ 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."
65347
+ },
65348
+ eager: {
65349
+ rule: "global-machine-wide",
65350
+ 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.`,
65351
+ 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."
65352
+ },
65353
+ listDirectory: {
65354
+ rule: "global-machine-wide",
65355
+ 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.`,
65356
+ 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."
65357
+ },
65358
+ todoWrite: {
65359
+ rule: "global-machine-wide",
65360
+ 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.`,
65361
+ 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."
65362
+ }
65363
+ };
65364
+ /**
65365
+ * The notes for a skills list that hides something. Qwen Code unions the list
65366
+ * across scopes, but the override replaces the one in the file it writes, so
65367
+ * the consequence worth naming is what dropping an entry hands back.
65368
+ */
65369
+ function skillsHidingListNotes(hides) {
65370
+ return {
65371
+ 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.`,
65372
+ 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.`
65373
+ };
65374
+ }
65375
+ const QWEN_SCOPED_SKILLS_KEYS = {
65376
+ disabled: {
65377
+ rule: "global-machine-wide",
65378
+ ...skillsHidingListNotes("skills are hidden")
65379
+ },
65380
+ defaultDisabled: {
65381
+ rule: "global-machine-wide",
65382
+ ...skillsHidingListNotes("skills start disabled until `skills.enabled` opts into them")
65383
+ },
65384
+ disabledLevels: {
65385
+ rule: "global-machine-wide",
65386
+ ...skillsHidingListNotes("skill discovery levels (`project`, `user`, `extension`, `bundled`) are skipped")
65387
+ },
65388
+ enabled: {
65389
+ rule: "global-machine-wide",
65390
+ 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.`,
65391
+ 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)."
65392
+ }
65393
+ };
65394
+ const QWEN_OVERRIDE_GROUPS = [
65395
+ {
65396
+ groupName: "tools",
65397
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
65398
+ scopedKeys: QWEN_SCOPED_TOOLS_KEYS
65399
+ },
65400
+ {
65401
+ groupName: "security",
65402
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
65403
+ scopedKeys: {
65404
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
65405
+ allowedInsecureVoiceBaseUrls: {
65406
+ rule: "workspace-stripped",
65407
+ grants: isNonEmptyArray
65408
+ },
65409
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
65410
+ folderTrust: { rule: "user-scope-trust-check" }
65411
+ }
65412
+ },
65413
+ {
65414
+ groupName: "skills",
65415
+ overrideKeys: QWEN_OVERRIDE_SKILLS_KEYS,
65416
+ scopedKeys: QWEN_SCOPED_SKILLS_KEYS
65417
+ },
65418
+ {
65419
+ groupName: "general",
65420
+ overrideKeys: QWEN_OVERRIDE_GENERAL_KEYS,
65421
+ scopedKeys: { outputStyle: {
64741
65422
  rule: "global-machine-wide",
64742
- 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.`,
64743
- 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."
64744
- }
64745
- }
64746
- }, {
64747
- groupName: "security",
64748
- overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
64749
- scopedKeys: {
64750
- allowPrivateNetworkHooks: { rule: "workspace-stripped" },
64751
- allowedInsecureVoiceBaseUrls: {
64752
- rule: "workspace-stripped",
64753
- grants: isNonEmptyArray
64754
- },
64755
- allowedHttpHookUrls: { rule: "workspace-non-overriding" },
64756
- folderTrust: { rule: "user-scope-trust-check" }
65423
+ 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.`,
65424
+ 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`)."
65425
+ } }
64757
65426
  }
64758
- }];
65427
+ ];
64759
65428
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
64760
65429
  const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
64761
65430
  rule: "global-machine-wide",
@@ -64886,7 +65555,7 @@ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGrou
64886
65555
  return scoped;
64887
65556
  }
64888
65557
  /**
64889
- * Build the `tools`/`security` patch groups contributed by the `qwencode`
65558
+ * Build the `tools`/`security`/`skills`/`general` patch groups contributed by the `qwencode`
64890
65559
  * override. Each group is shallow-merged over what `settings.json` already has,
64891
65560
  * after the scope gate has handled the keys Qwen Code honors in one scope only.
64892
65561
  */
@@ -65055,7 +65724,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
65055
65724
  });
65056
65725
  const overrideGroups = {
65057
65726
  tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
65058
- security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
65727
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS),
65728
+ skills: pickQwenOverrideKeys(settings.skills, QWEN_OVERRIDE_SKILLS_KEYS),
65729
+ general: pickQwenOverrideKeys(settings.general, QWEN_OVERRIDE_GENERAL_KEYS)
65059
65730
  };
65060
65731
  if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
65061
65732
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
@@ -65358,6 +66029,37 @@ function applyAllowDynamicBash({ permissions, authored }) {
65358
66029
  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"
65359
66030
  }];
65360
66031
  }
66032
+ /** The writer fallbacks `[permissions] mode` accepts. */
66033
+ const REASONIX_PERMISSION_MODES = /* @__PURE__ */ new Set([
66034
+ "ask",
66035
+ "allow",
66036
+ "deny"
66037
+ ]);
66038
+ /**
66039
+ * Writes the override's `mode` into `[permissions]`. Reasonix honors the key
66040
+ * only in the user config and ignores a project value as user-scope only, so a
66041
+ * project-scope generate skips it with a warning and leaves whatever the file
66042
+ * had. A global `allow` is reported when it is newly written: it lets every
66043
+ * writer call that no rule matches run without a prompt.
66044
+ *
66045
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
66046
+ */
66047
+ function applyPermissionsMode({ permissions, authored, global, relativeFilePath, logger }) {
66048
+ if (authored === void 0) return [];
66049
+ if (!global) {
66050
+ 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.`);
66051
+ return [];
66052
+ }
66053
+ const previous = permissions.mode;
66054
+ permissions.mode = authored;
66055
+ const normalize = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
66056
+ const mode = normalize(authored);
66057
+ if (mode === "ask" || mode === "deny" || normalize(previous) === mode) return [];
66058
+ return [{
66059
+ label: "permissions.mode",
66060
+ reason: "lets every writer call that no rule matches run without a prompt"
66061
+ }];
66062
+ }
65361
66063
  function parseReasonixConfig(fileContent) {
65362
66064
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
65363
66065
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -65408,7 +66110,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65408
66110
  relativeDirPath: paths.relativeDirPath,
65409
66111
  relativeFilePath: paths.relativeFilePath,
65410
66112
  fileContent,
65411
- validate
66113
+ validate,
66114
+ global
65412
66115
  });
65413
66116
  }
65414
66117
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
@@ -65463,6 +66166,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65463
66166
  permissions: mergedPermissions,
65464
66167
  authored: override?.allowDynamicBash
65465
66168
  });
66169
+ trustAffecting.push(...applyPermissionsMode({
66170
+ permissions: mergedPermissions,
66171
+ authored: override?.mode,
66172
+ global,
66173
+ relativeFilePath: relativeFilePathForLog,
66174
+ logger
66175
+ }));
65466
66176
  const patch = { permissions: mergedPermissions };
65467
66177
  if (override?.sandbox !== void 0) {
65468
66178
  const mergedSandbox = {
@@ -65504,7 +66214,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65504
66214
  patch,
65505
66215
  filePath
65506
66216
  }),
65507
- validate
66217
+ validate,
66218
+ global
65508
66219
  });
65509
66220
  }
65510
66221
  toRulesyncPermissions() {
@@ -65529,6 +66240,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65529
66240
  const reasonixOverride = {};
65530
66241
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
65531
66242
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
66243
+ if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
65532
66244
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
65533
66245
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
65534
66246
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -69248,6 +69960,9 @@ const WARP_OVERRIDE_KEYS = [
69248
69960
  "agent_mode_execute_readonly_commands"
69249
69961
  ];
69250
69962
  const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
69963
+ const WARP_AUTO_APPROVE_BYPASS_KEY = "auto_approve_bypasses_command_denylist";
69964
+ const WARP_AGENT_KEY = "warp_agent";
69965
+ const WARP_AGENT_OTHER_KEY = "other";
69251
69966
  const WARP_EXECUTION_PROFILE_KEYS = [
69252
69967
  "read_files",
69253
69968
  "apply_code_diffs",
@@ -69396,7 +70111,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69396
70111
  const override = config.warp;
69397
70112
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69398
70113
  if (isRecord$1(override)) {
69399
- const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
70114
+ const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, [WARP_AUTO_APPROVE_BYPASS_KEY]: _autoApproveBypass, ...legacyOverride } = override;
69400
70115
  Object.assign(profiles, legacyOverride);
69401
70116
  }
69402
70117
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
@@ -69406,9 +70121,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69406
70121
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
69407
70122
  else delete profiles[DENYLIST_KEY];
69408
70123
  agents.profiles = profiles;
70124
+ applyAutoApproveBypassOverride({
70125
+ agents,
70126
+ override
70127
+ });
69409
70128
  warnAboutDenylistReplacement({
69410
70129
  toolLabel: "Warp",
70130
+ overrideKey: "warp",
69411
70131
  denyCount: mergedDeny.length,
70132
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
69412
70133
  logger
69413
70134
  });
69414
70135
  mergeIntoDefaultExecutionProfile({
@@ -69447,6 +70168,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69447
70168
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
69448
70169
  const executionProfileOverride = defaultProfile ? liftExecutionProfileOverride(defaultProfile) : void 0;
69449
70170
  if (executionProfileOverride) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70171
+ const autoApproveBypass = readAutoApproveBypass(agents);
70172
+ if (autoApproveBypass !== void 0) warpOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69450
70173
  const result = { ...config };
69451
70174
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
69452
70175
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -69470,12 +70193,40 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69470
70193
  };
69471
70194
  /**
69472
70195
  * Writing `command_denylist` at all replaces Warp's built-in default denylist,
69473
- * so any non-empty list rulesync writes deserves a heads-up.
70196
+ * so any non-empty list rulesync writes deserves a heads-up. Unless
70197
+ * `auto_approve_bypasses_command_denylist` is `false`, auto-approve also runs
70198
+ * commands that match the list, so the warning says so.
69474
70199
  * https://docs.warp.dev/agents/cli/permissions-and-profiles/
69475
70200
  */
69476
- function warnAboutDenylistReplacement({ toolLabel, denyCount, logger }) {
70201
+ function warnAboutDenylistReplacement({ toolLabel, overrideKey, denyCount, autoApproveBypassesDenylist, logger }) {
69477
70202
  if (denyCount === 0 || !logger) return;
69478
- 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.`);
70203
+ const bypassNote = autoApproveBypassesDenylist === false ? "" : ` Auto-approve also runs commands that match the denylist unless ${overrideKey}.${WARP_AUTO_APPROVE_BYPASS_KEY} is set to false.`;
70204
+ 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}`);
70205
+ }
70206
+ /**
70207
+ * Write the override's `auto_approve_bypasses_command_denylist` into
70208
+ * `[agents.warp_agent.other]`, keeping every sibling key of `agents.warp_agent`
70209
+ * and of its `other` table. Nothing is written when the override omits it.
70210
+ */
70211
+ function applyAutoApproveBypassOverride({ agents, override }) {
70212
+ if (!isRecord$1(override)) return;
70213
+ const value = override[WARP_AUTO_APPROVE_BYPASS_KEY];
70214
+ if (typeof value !== "boolean") return;
70215
+ const warpAgent = isRecord$1(agents[WARP_AGENT_KEY]) ? { ...agents[WARP_AGENT_KEY] } : {};
70216
+ const other = isRecord$1(warpAgent[WARP_AGENT_OTHER_KEY]) ? { ...warpAgent[WARP_AGENT_OTHER_KEY] } : {};
70217
+ other[WARP_AUTO_APPROVE_BYPASS_KEY] = value;
70218
+ warpAgent[WARP_AGENT_OTHER_KEY] = other;
70219
+ agents[WARP_AGENT_KEY] = warpAgent;
70220
+ }
70221
+ /**
70222
+ * Read `[agents.warp_agent.other] auto_approve_bypasses_command_denylist`, or
70223
+ * `undefined` when it is unset (Warp then defaults to `true`).
70224
+ */
70225
+ function readAutoApproveBypass(agents) {
70226
+ const warpAgent = agents[WARP_AGENT_KEY];
70227
+ const other = isRecord$1(warpAgent) ? warpAgent[WARP_AGENT_OTHER_KEY] : void 0;
70228
+ const value = isRecord$1(other) ? other[WARP_AUTO_APPROVE_BYPASS_KEY] : void 0;
70229
+ return typeof value === "boolean" ? value : void 0;
69479
70230
  }
69480
70231
  /**
69481
70232
  * On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
@@ -69829,6 +70580,9 @@ const WARPCLI_GLOBAL_ONLY_MESSAGE = "Warp Agent CLI permissions are global-only;
69829
70580
  * (`read_files`, `apply_code_diffs`, `run_agents`, ...), merged first so the
69830
70581
  * rulesync-owned command lists always win.
69831
70582
  *
70583
+ * The `warpcli.auto_approve_bypasses_command_denylist` override is written to
70584
+ * the sibling `[agents.warp_agent.other]` table.
70585
+ *
69832
70586
  * Unlike the `warp` target, the CLI never read the app's legacy
69833
70587
  * `[agents.profiles]` keys, so none are written, and there is no settings
69834
70588
  * migration to wait for: the CLI's settings file "is created the first time you
@@ -69895,11 +70649,6 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69895
70649
  });
69896
70650
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
69897
70651
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
69898
- warnAboutDenylistReplacement({
69899
- toolLabel: "Warp Agent CLI",
69900
- denyCount: mergedDeny.length,
69901
- logger
69902
- });
69903
70652
  const override = config.warpcli;
69904
70653
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69905
70654
  const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
@@ -69909,7 +70658,18 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69909
70658
  mergedDeny,
69910
70659
  executionProfileOverride
69911
70660
  });
70661
+ applyAutoApproveBypassOverride({
70662
+ agents,
70663
+ override
70664
+ });
69912
70665
  settings.agents = agents;
70666
+ warnAboutDenylistReplacement({
70667
+ toolLabel: "Warp Agent CLI",
70668
+ overrideKey: "warpcli",
70669
+ denyCount: mergedDeny.length,
70670
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
70671
+ logger
70672
+ });
69913
70673
  return new WarpcliPermissions({
69914
70674
  outputRoot,
69915
70675
  relativeDirPath: paths.relativeDirPath,
@@ -69936,8 +70696,12 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69936
70696
  deny: isStringArray$2(denylist) ? denylist : []
69937
70697
  });
69938
70698
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
70699
+ const warpcliOverride = {};
70700
+ if (executionProfileOverride) warpcliOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70701
+ const autoApproveBypass = readAutoApproveBypass(agents);
70702
+ if (autoApproveBypass !== void 0) warpcliOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69939
70703
  const result = { ...config };
69940
- if (executionProfileOverride) result.warpcli = { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: executionProfileOverride };
70704
+ if (Object.keys(warpcliOverride).length > 0) result.warpcli = warpcliOverride;
69941
70705
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
69942
70706
  }
69943
70707
  validate() {
@@ -82011,9 +82775,8 @@ var CodebuddySubagent = class CodebuddySubagent extends ToolSubagent {
82011
82775
  * Keys of the rulesync `codewhale` section copied into the profile. Codewhale
82012
82776
  * parses agent profiles with `deny_unknown_fields`, so an unknown key would
82013
82777
  * make the whole profile fail to load; anything else in the section is dropped
82014
- * with a warning. `[tools]` and `[permissions]` are left out on purpose:
82015
- * Codewhale only accepts values there that narrow the defaults, and rejects a
82016
- * profile that tries to widen them.
82778
+ * with a warning. The `[tools]` and `[permissions]` tables are handled
82779
+ * separately (see `CODEWHALE_NARROWING_TABLES`).
82017
82780
  */
82018
82781
  const CODEWHALE_SECTION_KEYS = [
82019
82782
  "role_hint",
@@ -82034,6 +82797,48 @@ const CODEWHALE_SECTION_KEY_ALIASES = {
82034
82797
  reasoning_effort: ["thinking", "reasoning"]
82035
82798
  };
82036
82799
  const CODEWHALE_SECTION_ALIAS_TO_KEY = new Map(Object.entries(CODEWHALE_SECTION_KEY_ALIASES).flatMap(([key, aliases]) => (aliases ?? []).map((alias) => [alias, key])));
82800
+ /**
82801
+ * The `[tools]` and `[permissions]` tables of an agent profile, with the only
82802
+ * values Codewhale accepts in each key. Codewhale rejects a profile that tries
82803
+ * to widen access through them (`reject_permission_expansion` in
82804
+ * `fleet/profile.rs`), and both tables deny unknown fields, so any other key
82805
+ * or value is dropped with a warning.
82806
+ */
82807
+ const CODEWHALE_NARROWING_TABLES = {
82808
+ tools: { posture: (value) => typeof value === "string" && [
82809
+ "read-only",
82810
+ "readonly",
82811
+ "read_only"
82812
+ ].includes(value) },
82813
+ permissions: {
82814
+ allow_shell: (value) => value === false,
82815
+ trust: (value) => value === false,
82816
+ approval_required: (value) => value === true
82817
+ }
82818
+ };
82819
+ /**
82820
+ * Keeps the narrowing-only entries of a `[tools]` / `[permissions]` table and
82821
+ * reports every other entry by its dotted path. A non-table value is dropped
82822
+ * as a whole.
82823
+ */
82824
+ function filterNarrowingTable(tableName, value) {
82825
+ const allowed = CODEWHALE_NARROWING_TABLES[tableName] ?? {};
82826
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {
82827
+ kept: void 0,
82828
+ dropped: [tableName]
82829
+ };
82830
+ const kept = {};
82831
+ const dropped = [];
82832
+ for (const [key, entry] of Object.entries(value)) {
82833
+ const normalized = typeof entry === "string" ? entry.trim() : entry;
82834
+ if (Object.hasOwn(allowed, key) && allowed[key]?.(normalized)) kept[key] = normalized;
82835
+ else dropped.push(`${tableName}.${key}`);
82836
+ }
82837
+ return {
82838
+ kept: Object.keys(kept).length > 0 ? kept : void 0,
82839
+ dropped
82840
+ };
82841
+ }
82037
82842
  const CodewhaleSubagentTomlSchema = zod_mini.z.looseObject({
82038
82843
  id: zod_mini.z.optional(zod_mini.z.string()),
82039
82844
  name: zod_mini.z.optional(zod_mini.z.string()),
@@ -82060,7 +82865,8 @@ function stringifyCodewhaleSubagentToml(fields, instructions) {
82060
82865
  *
82061
82866
  * Generation writes `id` (the file stem), `display_name` (the rulesync name),
82062
82867
  * `description`, and the body as `[instructions] text`, plus the allowlisted
82063
- * keys of the rulesync `codewhale` section.
82868
+ * keys of the rulesync `codewhale` section and its narrowing-only `[tools]` /
82869
+ * `[permissions]` entries.
82064
82870
  *
82065
82871
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/SUBAGENTS.md
82066
82872
  * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/fleet/profile.rs
@@ -82095,6 +82901,10 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82095
82901
  const spelling = [key, ...CODEWHALE_SECTION_KEY_ALIASES[key] ?? []].find((candidate) => record[candidate] !== void 0);
82096
82902
  if (spelling !== void 0) codewhaleSection[key] = record[spelling];
82097
82903
  }
82904
+ for (const tableName of Object.keys(CODEWHALE_NARROWING_TABLES)) if (record[tableName] !== void 0) {
82905
+ const { kept } = filterNarrowingTable(tableName, record[tableName]);
82906
+ if (kept !== void 0) codewhaleSection[tableName] = kept;
82907
+ }
82098
82908
  return new RulesyncSubagent({
82099
82909
  outputRoot: ".",
82100
82910
  frontmatter: {
@@ -82117,6 +82927,12 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82117
82927
  const droppedKeys = [];
82118
82928
  const sectionSpellings = /* @__PURE__ */ new Map();
82119
82929
  for (const [rawKey, value] of Object.entries(rawSection)) {
82930
+ if (Object.hasOwn(CODEWHALE_NARROWING_TABLES, rawKey)) {
82931
+ const { kept, dropped } = filterNarrowingTable(rawKey, value);
82932
+ if (kept !== void 0) sectionFields[rawKey] = kept;
82933
+ droppedKeys.push(...dropped);
82934
+ continue;
82935
+ }
82120
82936
  const key = CODEWHALE_SECTION_ALIAS_TO_KEY.get(rawKey) ?? rawKey;
82121
82937
  if (CODEWHALE_SECTION_KEYS.includes(key) && typeof value === "string") {
82122
82938
  if (key === rawKey || sectionFields[key] === void 0) {
@@ -82126,7 +82942,7 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82126
82942
  } else droppedKeys.push(rawKey);
82127
82943
  } else droppedKeys.push(rawKey);
82128
82944
  }
82129
- 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).`);
82945
+ 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.`);
82130
82946
  const body = stringifyCodewhaleSubagentToml({
82131
82947
  id: toProfileId(relativeFilePath),
82132
82948
  display_name: frontmatter.name,
@@ -88623,6 +89439,99 @@ const AntigravityRuleFrontmatterSchema = zod_mini.z.looseObject({
88623
89439
  function toGlobalRuleFileName(relativeFilePath) {
88624
89440
  return relativeFilePath.split(/[\\/]/).join("-");
88625
89441
  }
89442
+ /** The project rules directory both Antigravity targets share, as a POSIX path. */
89443
+ const PROJECT_RULES_DIR = `${ANTIGRAVITY_IDE_AGENTS_DIR}/rules`;
89444
+ /**
89445
+ * The subproject directory a rule is scoped to, or `undefined` when it goes to
89446
+ * the project-root `.agents/rules/`. Antigravity reads a `.agents/rules/`
89447
+ * directory in any subdirectory and loads it for files under that directory,
89448
+ * so a non-root rule carrying `agentsmd.subprojectPath` is written there. Project
89449
+ * scope only: there is no workspace tree to nest under in global scope.
89450
+ *
89451
+ * @see https://antigravity.google/docs/rules
89452
+ */
89453
+ function resolveAntigravitySubprojectPath({ rulesyncRule, global }) {
89454
+ const { root, agentsmd } = rulesyncRule.getFrontmatter();
89455
+ const subprojectPath = !global && root !== true ? agentsmd?.subprojectPath : void 0;
89456
+ return subprojectPath && ![
89457
+ "",
89458
+ ".",
89459
+ "./"
89460
+ ].includes(subprojectPath) ? subprojectPath : void 0;
89461
+ }
89462
+ /**
89463
+ * File name of a rule in a subproject's `.agents/rules/`. Antigravity reads
89464
+ * only the direct children of a rules directory, so a nested rulesync rule
89465
+ * (`frontend/style.md`) is flattened (`frontend-style.md`), and the name keeps
89466
+ * its case so the IDE and the CLI write the same file and an imported
89467
+ * `codeStyle.md` is written back to the same place.
89468
+ */
89469
+ function toNestedRuleFileName(relativeFilePath) {
89470
+ return toGlobalRuleFileName(relativeFilePath);
89471
+ }
89472
+ /** The `.agents/rules/` directory of a subproject. */
89473
+ function toAntigravityNestedRulesDirPath(subprojectPath) {
89474
+ return (0, node_path.join)(subprojectPath, ANTIGRAVITY_IDE_AGENTS_DIR, "rules");
89475
+ }
89476
+ /**
89477
+ * Split a nested rule directory such as `packages/api/.agents/rules/sub` into
89478
+ * the subproject (`packages/api`) and the folder inside its rules directory
89479
+ * (`sub`). Returns `undefined` for the project-root `.agents/rules/`, for a
89480
+ * path with no `.agents/rules` segment, and for one under a hidden directory —
89481
+ * another tool's output or a worktree, not a subproject.
89482
+ */
89483
+ function extractAntigravitySubprojectPath(relativeDirPath) {
89484
+ const segments = toPosixPath(relativeDirPath).split("/");
89485
+ const index = segments.findIndex((segment, i) => segment === ".agents" && segments[i + 1] === "rules");
89486
+ if (index <= 0) return;
89487
+ const subprojectSegments = segments.slice(0, index);
89488
+ if (subprojectSegments.some((segment) => segment === "" || segment.startsWith("."))) return;
89489
+ return {
89490
+ subprojectPath: subprojectSegments.join("/"),
89491
+ innerDirPath: segments.slice(index + 2).join("/")
89492
+ };
89493
+ }
89494
+ /**
89495
+ * The `.agents/rules/` folders of project subdirectories. Only their direct
89496
+ * `.md` children are read, because Antigravity ignores files nested in
89497
+ * subfolders of a rules directory. The shared nested
89498
+ * scan ignores every hidden directory, which would skip `.agents` itself, so
89499
+ * only a `.agents/rules/` below another hidden directory is ignored here
89500
+ * (`extractAntigravitySubprojectPath` rejects one again on import). Import-only,
89501
+ * like every other nested scan: these files sit outside the rulesync-owned root
89502
+ * `.agents/rules/`, so `--delete` never sweeps them.
89503
+ */
89504
+ function getAntigravityNestedFilePatterns() {
89505
+ return {
89506
+ include: [`**/${PROJECT_RULES_DIR}/*.md`],
89507
+ ignore: [
89508
+ `${PROJECT_RULES_DIR}/**`,
89509
+ `**/.*/**/${PROJECT_RULES_DIR}/**`,
89510
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
89511
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
89512
+ ],
89513
+ ownedDirPath: PROJECT_RULES_DIR
89514
+ };
89515
+ }
89516
+ /**
89517
+ * Resolve where a non-root project rule file read from disk lives. A file
89518
+ * discovered by `getNestedFilePatterns` arrives as its own directory plus its
89519
+ * basename; it is re-split into the subproject's rules directory and the path
89520
+ * inside it, matching how the root `.agents/rules/` is read.
89521
+ */
89522
+ function resolveAntigravityNonRootFile({ relativeDirPath, relativeFilePath, defaultDirPath }) {
89523
+ const nested = relativeDirPath === void 0 ? void 0 : extractAntigravitySubprojectPath(relativeDirPath);
89524
+ if (nested === void 0) return {
89525
+ relativeDirPath: defaultDirPath,
89526
+ relativeFilePath
89527
+ };
89528
+ return {
89529
+ relativeDirPath: toAntigravityNestedRulesDirPath(nested.subprojectPath),
89530
+ relativeFilePath: (0, node_path.join)(nested.innerDirPath, relativeFilePath)
89531
+ };
89532
+ }
89533
+ /** The targets a nested rule is imported with: the two that read the shared tree. */
89534
+ const ANTIGRAVITY_NESTED_RULE_TARGETS = ["antigravity-ide", "antigravity-cli"];
88626
89535
  /**
88627
89536
  * Converts a comma-separated globs string or array to an array of globs.
88628
89537
  * @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
@@ -88810,12 +89719,17 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88810
89719
  });
88811
89720
  }
88812
89721
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
88813
- const relativePath = (0, node_path.join)(paths.nonRoot.relativeDirPath, relativeFilePath);
89722
+ const nonRoot = resolveAntigravityNonRootFile({
89723
+ relativeDirPath,
89724
+ relativeFilePath,
89725
+ defaultDirPath: paths.nonRoot.relativeDirPath
89726
+ });
89727
+ const relativePath = (0, node_path.join)(nonRoot.relativeDirPath, nonRoot.relativeFilePath);
88814
89728
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativePath));
88815
89729
  const rule = new AntigravityCliRule({
88816
89730
  outputRoot,
88817
- relativeDirPath: paths.nonRoot.relativeDirPath,
88818
- relativeFilePath,
89731
+ relativeDirPath: nonRoot.relativeDirPath,
89732
+ relativeFilePath: nonRoot.relativeFilePath,
88819
89733
  fileContent,
88820
89734
  validate,
88821
89735
  root: false,
@@ -88847,11 +89761,21 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88847
89761
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
88848
89762
  const frontmatter = strategy.generateFrontmatter(normalizeStoredAntigravity(storedAntigravity), rulesyncFrontmatter);
88849
89763
  const fileContent = stringifyFrontmatter(rulesyncRule.getBody(), frontmatter);
88850
- if (!global) return new AntigravityCliRule({
88851
- ...params,
88852
- fileContent,
88853
- global
88854
- });
89764
+ if (!global) {
89765
+ const subprojectPath = resolveAntigravitySubprojectPath({
89766
+ rulesyncRule,
89767
+ global
89768
+ });
89769
+ return new AntigravityCliRule({
89770
+ ...params,
89771
+ ...subprojectPath !== void 0 && {
89772
+ relativeDirPath: toAntigravityNestedRulesDirPath(subprojectPath),
89773
+ relativeFilePath: toNestedRuleFileName(params.relativeFilePath)
89774
+ },
89775
+ fileContent,
89776
+ global
89777
+ });
89778
+ }
88855
89779
  return new AntigravityCliRule({
88856
89780
  ...params,
88857
89781
  relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
@@ -88867,22 +89791,32 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88867
89791
  globs: [],
88868
89792
  antigravity: frontmatter
88869
89793
  };
89794
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
88870
89795
  return new RulesyncRule({
88871
89796
  outputRoot: process.cwd(),
88872
89797
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
88873
89798
  relativeFilePath: this.getRelativeFilePath(),
88874
89799
  frontmatter: {
88875
89800
  root: false,
88876
- targets: ["*"],
89801
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
88877
89802
  ...rulesyncData,
88878
89803
  antigravity: {
88879
89804
  ...rulesyncData.antigravity,
88880
89805
  globs: frontmatter.globs ? parseGlobsString$1(frontmatter.globs) : void 0
88881
- }
89806
+ },
89807
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
88882
89808
  },
88883
89809
  body
88884
89810
  });
88885
89811
  }
89812
+ /**
89813
+ * The `.agents/rules/` folders of project subdirectories, imported back as
89814
+ * directory-scoped rules.
89815
+ * @see https://antigravity.google/docs/rules
89816
+ */
89817
+ static getNestedFilePatterns() {
89818
+ return getAntigravityNestedFilePatterns();
89819
+ }
88886
89820
  validate() {
88887
89821
  if (this.root) return {
88888
89822
  success: true,
@@ -89012,8 +89946,15 @@ var AntigravityIdeRule = class extends ToolRule {
89012
89946
  global
89013
89947
  });
89014
89948
  }
89015
- const nonRootDirPath = paths.nonRoot.relativeDirPath;
89016
- const filePath = (0, node_path.join)(outputRoot, nonRootDirPath, relativeFilePath);
89949
+ const nonRoot = this.supportsSubprojectRules() ? resolveAntigravityNonRootFile({
89950
+ relativeDirPath,
89951
+ relativeFilePath,
89952
+ defaultDirPath: paths.nonRoot.relativeDirPath
89953
+ }) : {
89954
+ relativeDirPath: paths.nonRoot.relativeDirPath,
89955
+ relativeFilePath
89956
+ };
89957
+ const filePath = (0, node_path.join)(outputRoot, nonRoot.relativeDirPath, nonRoot.relativeFilePath);
89017
89958
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
89018
89959
  let parsedFrontmatter;
89019
89960
  if (validate) {
@@ -89023,8 +89964,8 @@ var AntigravityIdeRule = class extends ToolRule {
89023
89964
  } else parsedFrontmatter = frontmatter;
89024
89965
  return new this({
89025
89966
  outputRoot,
89026
- relativeDirPath: nonRootDirPath,
89027
- relativeFilePath,
89967
+ relativeDirPath: nonRoot.relativeDirPath,
89968
+ relativeFilePath: nonRoot.relativeFilePath,
89028
89969
  body,
89029
89970
  frontmatter: parsedFrontmatter,
89030
89971
  validate,
@@ -89032,6 +89973,25 @@ var AntigravityIdeRule = class extends ToolRule {
89032
89973
  global
89033
89974
  });
89034
89975
  }
89976
+ /**
89977
+ * Whether a rule scoped to a subproject goes to that subproject's own
89978
+ * `.agents/rules/`. A subclass that writes somewhere other than a project
89979
+ * tree (the plugin bundle) turns it off.
89980
+ */
89981
+ static supportsSubprojectRules() {
89982
+ return true;
89983
+ }
89984
+ /**
89985
+ * The `.agents/rules/` folders of project subdirectories, imported back as
89986
+ * directory-scoped rules.
89987
+ * @see https://antigravity.google/docs/rules
89988
+ */
89989
+ static getNestedFilePatterns() {
89990
+ return this.supportsSubprojectRules() ? getAntigravityNestedFilePatterns() : {
89991
+ include: [],
89992
+ ignore: []
89993
+ };
89994
+ }
89035
89995
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
89036
89996
  const paths = this.getSettablePaths({ global });
89037
89997
  if (rulesyncRule.getFrontmatter().root) {
@@ -89054,10 +90014,14 @@ var AntigravityIdeRule = class extends ToolRule {
89054
90014
  const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
89055
90015
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
89056
90016
  const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
89057
- const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
90017
+ const subprojectPath = this.supportsSubprojectRules() ? resolveAntigravitySubprojectPath({
90018
+ rulesyncRule,
90019
+ global
90020
+ }) : void 0;
90021
+ const relativeFilePath = global || subprojectPath !== void 0 ? toNestedRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
89058
90022
  return new this({
89059
90023
  outputRoot,
89060
- relativeDirPath: paths.nonRoot.relativeDirPath,
90024
+ relativeDirPath: subprojectPath === void 0 ? paths.nonRoot.relativeDirPath : toAntigravityNestedRulesDirPath(subprojectPath),
89061
90025
  relativeFilePath,
89062
90026
  frontmatter,
89063
90027
  body: rulesyncRule.getBody(),
@@ -89078,15 +90042,17 @@ var AntigravityIdeRule = class extends ToolRule {
89078
90042
  ...rulesyncData.antigravity,
89079
90043
  globs: this.frontmatter.globs ? parseGlobsString$1(this.frontmatter.globs) : void 0
89080
90044
  };
90045
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
89081
90046
  return new RulesyncRule({
89082
90047
  outputRoot: process.cwd(),
89083
90048
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
89084
90049
  relativeFilePath: this.getRelativeFilePath(),
89085
90050
  frontmatter: {
89086
90051
  root: false,
89087
- targets: ["*"],
90052
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
89088
90053
  ...rulesyncData,
89089
- antigravity: antigravityForRulesync
90054
+ antigravity: antigravityForRulesync,
90055
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
89090
90056
  },
89091
90057
  body: this.body
89092
90058
  });
@@ -89140,6 +90106,9 @@ var AntigravityIdeRule = class extends ToolRule {
89140
90106
  //#endregion
89141
90107
  //#region src/features/rules/antigravity-plugin-rule.ts
89142
90108
  var AntigravityPluginRule = class extends AntigravityIdeRule {
90109
+ static supportsSubprojectRules() {
90110
+ return false;
90111
+ }
89143
90112
  static isTargetedByRulesyncRule(rulesyncRule) {
89144
90113
  const targets = rulesyncRule.getFrontmatter().targets;
89145
90114
  return targets.includes("*") || targets.includes("antigravity-plugin");
@@ -95489,6 +96458,12 @@ var VibeRule = class extends NestedAgentsmdRule {
95489
96458
  * file, so every non-root rule without `agentsmd.subprojectPath` folds into
95490
96459
  * the root `./AGENTS.md`.
95491
96460
  *
96461
+ * A legacy root `WARP.md` is read on import when the root `AGENTS.md` is
96462
+ * absent, so a repository that has not moved to `AGENTS.md` yet imports its
96463
+ * rules. It is never written or deleted. When both exist, Warp reads
96464
+ * `WARP.md`, so import keeps reading the `AGENTS.md` rulesync manages and warns
96465
+ * that Warp ignores it, as generate already does.
96466
+ *
95492
96467
  * In global mode, Warp reads a third rule source from `~/.agents/AGENTS.md`
95493
96468
  * (the cross-tool agent config directory), indexed like project rules and also
95494
96469
  * used from remote hosts in SSH sessions. The same root-fold policy applies.
@@ -95503,6 +96478,31 @@ var WarpRule = class extends NestedAgentsmdRule {
95503
96478
  toolTarget: "warp"
95504
96479
  };
95505
96480
  }
96481
+ static getSettablePaths(options = {}) {
96482
+ const paths = super.getSettablePaths(options);
96483
+ if (options.global) return paths;
96484
+ return {
96485
+ ...paths,
96486
+ importOnlyRoots: [{
96487
+ relativeDirPath: ".",
96488
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96489
+ onlyWhenRootAbsent: true,
96490
+ outranksRoot: true
96491
+ }]
96492
+ };
96493
+ }
96494
+ static async fromFile(params) {
96495
+ const { outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true } = params;
96496
+ if (!params.global && relativeFilePath === "WARP.md" && (relativeDirPath === void 0 || relativeDirPath === ".")) return new this({
96497
+ outputRoot,
96498
+ relativeDirPath: ".",
96499
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96500
+ fileContent: await readFileContent((0, node_path.join)(outputRoot, WARP_LEGACY_RULE_FILE_NAME)),
96501
+ validate,
96502
+ root: true
96503
+ });
96504
+ return super.fromFile(params);
96505
+ }
95506
96506
  };
95507
96507
  //#endregion
95508
96508
  //#region src/features/rules/zcode-rule.ts
@@ -96805,7 +97805,7 @@ var RulesProcessor = class extends FeatureProcessor {
96805
97805
  if (!importOnlyRoots || importOnlyRoots.length === 0) return;
96806
97806
  const existingPaths = [];
96807
97807
  for (const importOnlyRoot of importOnlyRoots) {
96808
- if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
97808
+ if (importOnlyRoot.onlyWhenRootAbsent !== true || importOnlyRoot.outranksRoot === true) continue;
96809
97809
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
96810
97810
  cwd: this.outputRoot,
96811
97811
  type: "file"
@@ -97506,13 +98506,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
97506
98506
  const rootFilePath = primaryRootFilePaths[0];
97507
98507
  const scannedPaths = [];
97508
98508
  const skippedPaths = [];
98509
+ const skippedOutrankingPaths = [];
97509
98510
  for (const importOnlyRoot of importOnlyRoots) {
97510
98511
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
97511
98512
  cwd: this.outputRoot,
97512
98513
  type: "file"
97513
98514
  });
97514
98515
  if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
97515
- skippedPaths.push(...matchedPaths);
98516
+ (importOnlyRoot.outranksRoot === true ? skippedOutrankingPaths : skippedPaths).push(...matchedPaths);
97516
98517
  continue;
97517
98518
  }
97518
98519
  scannedPaths.push(...matchedPaths);
@@ -97523,6 +98524,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
97523
98524
  const remainingCount = skippedNames.length - listedNames.length;
97524
98525
  this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters((0, node_path.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.`);
97525
98526
  }
98527
+ if (skippedOutrankingPaths.length > 0 && rootFilePath !== void 0) {
98528
+ const rootName = stripControlCharacters((0, node_path.relative)(this.outputRoot, rootFilePath));
98529
+ const skippedNames = skippedOutrankingPaths.map((filePath) => stripControlCharacters((0, node_path.relative)(this.outputRoot, filePath)));
98530
+ const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
98531
+ const remainingCount = skippedNames.length - listedNames.length;
98532
+ 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.`);
98533
+ }
97526
98534
  return await buildImportRulesFromPaths(scannedPaths);
97527
98535
  })();
97528
98536
  this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);