rulesync 29.0.0 → 31.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.
@@ -913,6 +913,7 @@ const checksProcessorToolTargetTuple = [
913
913
  "cursor",
914
914
  "factorydroid",
915
915
  "gitlabduo",
916
+ "goose",
916
917
  "hermesagent",
917
918
  "kilo",
918
919
  "qwencode",
@@ -7861,6 +7862,26 @@ const CopilotPermissionsOverrideSchema = z.looseObject({
7861
7862
  permission: z.optional(ToolScopedPermissionSchema),
7862
7863
  defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7863
7864
  });
7865
+ /**
7866
+ * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
7867
+ * before prompting. `always-ask` auto-approves only read-only tools, `write`
7868
+ * also workspace writes, and `yolo` (oh-my-pi's default) every tier.
7869
+ *
7870
+ * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
7871
+ */
7872
+ const OMP_APPROVAL_MODES = [
7873
+ "always-ask",
7874
+ "write",
7875
+ "yolo"
7876
+ ];
7877
+ /**
7878
+ * Tool-scoped override block for oh-my-pi (`omp`). `approvalMode` writes the
7879
+ * `tools.approvalMode` key of `config.yml`, which has no canonical category.
7880
+ */
7881
+ const OmpPermissionsOverrideSchema = z.looseObject({
7882
+ permission: z.optional(ToolScopedPermissionSchema),
7883
+ approvalMode: z.optional(z.enum(OMP_APPROVAL_MODES))
7884
+ });
7864
7885
  const KimiCodePermissionsOverrideSchema = z.looseObject({
7865
7886
  permission: z.optional(ToolScopedPermissionSchema),
7866
7887
  defaultPermissionMode: z.optional(z.enum([
@@ -7868,6 +7889,14 @@ const KimiCodePermissionsOverrideSchema = z.looseObject({
7868
7889
  "yolo",
7869
7890
  "auto"
7870
7891
  ])),
7892
+ /**
7893
+ * Kimi's `[permission] dangerous_command_guard` switch (Kimi Code 0.40.0,
7894
+ * default `true`). `false` turns off the built-in dangerous-command policy
7895
+ * (no confirmation for `rm -rf`, `shutdown` and the like).
7896
+ *
7897
+ * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
7898
+ */
7899
+ dangerousCommandGuard: z.optional(z.boolean()),
7871
7900
  rules: z.optional(z.array(z.looseObject({
7872
7901
  decision: PermissionActionSchema,
7873
7902
  pattern: z.string(),
@@ -8083,7 +8112,10 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8083
8112
  * `sandbox`, `sandboxImage`, `disabled`, `visible`, `eager`, `listDirectory`,
8084
8113
  * `todoWrite`, `workflowsEnabled`, `workflowNameOnly`, `executionSandbox`) and
8085
8114
  * `security` (`folderTrust`, `allowedHttpHookUrls`, `allowPrivateNetworkHooks`,
8086
- * `allowedInsecureVoiceBaseUrls`). Qwen Code strips `tools.workflowsEnabled`,
8115
+ * `allowedInsecureVoiceBaseUrls`) and `skills` (the skill kill-switches
8116
+ * `disabled`, `defaultDisabled`, `enabled`, `disabledLevels`, each a list Qwen
8117
+ * Code unions across scopes) and `general` (`outputStyle`, the output style
8118
+ * that reshapes how responses are written). Qwen Code strips `tools.workflowsEnabled`,
8087
8119
  * `tools.executionSandbox`, `security.allowPrivateNetworkHooks` and
8088
8120
  * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
8089
8121
  * skips those four in project scope and announces a granting value in global
@@ -8100,7 +8132,8 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8100
8132
  * registry controls (`disabled`, `visible`, `listDirectory`, `todoWrite`, and `eager`, which
8101
8133
  * demotes an omitted tool to deferred rather than removing it, replacing rather
8102
8134
  * than merging with the list a higher scope set), the Auto Mode
8103
- * classifier config, and, because these groups are loose objects, any key
8135
+ * classifier config, the skill kill-switches, and, because these groups are
8136
+ * loose objects, any key
8104
8137
  * rulesync does not model. Import flags a scope-dependent key only when it read
8105
8138
  * the project file, since that is the value a `--global` regenerate would
8106
8139
  * promote. It also
@@ -8111,11 +8144,13 @@ const CursorPermissionsOverrideSchema = z.looseObject({
8111
8144
  * matching `settings.json` group and emitted only for Qwen, while the shared
8112
8145
  * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
8113
8146
  * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
8114
- * `tools`/`security`/`autoMode` key can be authored.
8147
+ * `tools`/`security`/`skills`/`general`/`autoMode` key can be authored.
8115
8148
  *
8116
8149
  * @example
8117
8150
  * {
8118
8151
  * "tools": { "approvalMode": "auto-edit" },
8152
+ * "skills": { "disabled": ["pdf"], "disabledLevels": ["bundled"] },
8153
+ * "general": { "outputStyle": "Concise" },
8119
8154
  * "security": { "folderTrust": { "enabled": true } },
8120
8155
  * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
8121
8156
  * }
@@ -8124,6 +8159,8 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
8124
8159
  permission: z.optional(ToolScopedPermissionSchema),
8125
8160
  tools: z.optional(z.looseObject({})),
8126
8161
  security: z.optional(z.looseObject({})),
8162
+ skills: z.optional(z.looseObject({})),
8163
+ general: z.optional(z.looseObject({})),
8127
8164
  autoMode: z.optional(z.looseObject({}))
8128
8165
  });
8129
8166
  /**
@@ -8209,6 +8246,8 @@ const TabninePermissionsOverrideSchema = z.looseObject({
8209
8246
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
8210
8247
  * @example
8211
8248
  * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
8249
+ * @example
8250
+ * { "mode": "deny" }
8212
8251
  */
8213
8252
  const ReasonixPermissionsOverrideSchema = z.looseObject({
8214
8253
  permission: z.optional(ToolScopedPermissionSchema),
@@ -8217,7 +8256,12 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
8217
8256
  rawAllow: z.optional(z.array(z.string())),
8218
8257
  rawAsk: z.optional(z.array(z.string())),
8219
8258
  rawDeny: z.optional(z.array(z.string())),
8220
- allowDynamicBash: z.optional(z.boolean())
8259
+ allowDynamicBash: z.optional(z.boolean()),
8260
+ mode: z.optional(z.enum([
8261
+ "ask",
8262
+ "allow",
8263
+ "deny"
8264
+ ]))
8221
8265
  });
8222
8266
  /**
8223
8267
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -8260,7 +8304,8 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({
8260
8304
  * {
8261
8305
  * "agent_mode_coding_permissions": "always_allow_reading",
8262
8306
  * "agent_mode_execute_readonly_commands": true,
8263
- * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] }
8307
+ * "execution_profile": { "read_files": "always_allow", "mcp_denylist": ["untrusted-server"] },
8308
+ * "auto_approve_bypasses_command_denylist": false
8264
8309
  * }
8265
8310
  */
8266
8311
  /**
@@ -8312,6 +8357,16 @@ const WarpExecutionProfileOverrideSchema = z.looseObject({
8312
8357
  mcp_allowlist: z.optional(z.array(z.string())),
8313
8358
  mcp_denylist: z.optional(z.array(z.string()))
8314
8359
  });
8360
+ /**
8361
+ * Whether auto-approve may run commands that match the command denylist
8362
+ * (Warp defaults to `true`). It is not an execution-profile field: it lives in
8363
+ * the sibling `[agents.warp_agent.other]` table of the same `settings.toml`,
8364
+ * shared by the Warp app and the Warp Agent CLI.
8365
+ *
8366
+ * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8367
+ * @see https://github.com/warpdotdev/warp/blob/main/app/src/settings/ai.rs (`AutoApproveBypassesCommandDenylist`)
8368
+ */
8369
+ const WarpAutoApproveBypassSchema = z.optional(z.boolean());
8315
8370
  const WarpPermissionsOverrideSchema = z.looseObject({
8316
8371
  permission: z.optional(ToolScopedPermissionSchema),
8317
8372
  agent_mode_coding_permissions: z.optional(z.enum([
@@ -8321,7 +8376,8 @@ const WarpPermissionsOverrideSchema = z.looseObject({
8321
8376
  ])),
8322
8377
  agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
8323
8378
  agent_mode_execute_readonly_commands: z.optional(z.boolean()),
8324
- execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
8379
+ execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
8380
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8325
8381
  });
8326
8382
  /**
8327
8383
  * Tool-scoped override block for the standalone Warp Agent CLI. The CLI reads
@@ -8330,16 +8386,21 @@ const WarpPermissionsOverrideSchema = z.looseObject({
8330
8386
  * the app's legacy `[agents.profiles]` keys), so only the nested
8331
8387
  * `execution_profile` block is offered; it is merged into that record, and the
8332
8388
  * shared `permission.bash` block still drives `command_allowlist` /
8333
- * `command_denylist`.
8389
+ * `command_denylist`. `auto_approve_bypasses_command_denylist` is written to
8390
+ * the sibling `[agents.warp_agent.other]` table.
8334
8391
  *
8335
8392
  * @example
8336
- * { "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" } }
8393
+ * {
8394
+ * "execution_profile": { "apply_code_diffs": "always_ask", "run_agents": "never_allow" },
8395
+ * "auto_approve_bypasses_command_denylist": false
8396
+ * }
8337
8397
  *
8338
8398
  * @see https://docs.warp.dev/agents/cli/permissions-and-profiles/
8339
8399
  */
8340
8400
  const WarpcliPermissionsOverrideSchema = z.looseObject({
8341
8401
  permission: z.optional(ToolScopedPermissionSchema),
8342
- execution_profile: z.optional(WarpExecutionProfileOverrideSchema)
8402
+ execution_profile: z.optional(WarpExecutionProfileOverrideSchema),
8403
+ auto_approve_bypasses_command_denylist: WarpAutoApproveBypassSchema
8343
8404
  });
8344
8405
  /**
8345
8406
  * deepagents-cli's approval-mode knobs under `[startup]` in
@@ -9035,7 +9096,7 @@ const PermissionsConfigSchema = z.looseObject({
9035
9096
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
9036
9097
  crush: z.optional(CanonicalPermissionsOverrideSchema),
9037
9098
  goose: z.optional(CanonicalPermissionsOverrideSchema),
9038
- omp: z.optional(CanonicalPermissionsOverrideSchema),
9099
+ omp: z.optional(OmpPermissionsOverrideSchema),
9039
9100
  pool: z.optional(CanonicalPermissionsOverrideSchema),
9040
9101
  qoder: z.optional(CanonicalPermissionsOverrideSchema),
9041
9102
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
@@ -12745,7 +12806,9 @@ const SHARED_CONFIG_OWNERSHIP = {
12745
12806
  ownedKeys: [
12746
12807
  "permissions",
12747
12808
  "tools",
12748
- "security"
12809
+ "security",
12810
+ "skills",
12811
+ "general"
12749
12812
  ]
12750
12813
  }
12751
12814
  }
@@ -13115,7 +13178,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13115
13178
  features: {
13116
13179
  mcp: {
13117
13180
  kind: "replace-owned-keys",
13118
- ownedKeys: ["mcp_servers"]
13181
+ ownedKeys: ["mcp_servers", "disabled_mcp_tools"]
13119
13182
  },
13120
13183
  permissions: {
13121
13184
  kind: "replace-owned-keys",
@@ -13250,6 +13313,14 @@ function serializeSharedConfigFile({ fileKey, document, existingContent, filePat
13250
13313
  });
13251
13314
  }
13252
13315
  /**
13316
+ * A patch value for an owned key that holds a map of managed entries: the map
13317
+ * itself, or `undefined` when it is empty, which retracts the key instead of
13318
+ * leaving an empty object behind (`applySharedConfigPatch` deletes a key whose patch value is `undefined`).
13319
+ */
13320
+ function undefinedWhenEmpty(entries) {
13321
+ return Object.keys(entries).length > 0 ? entries : void 0;
13322
+ }
13323
+ /**
13253
13324
  * Execute a feature's declared write to a gateway-managed shared file: parse
13254
13325
  * the existing content, merge the patch under the feature's declared policy,
13255
13326
  * and serialize it back over the existing content (see
@@ -13736,14 +13807,23 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
13736
13807
  async removeOrphanAiFiles(existingFiles, generatedFiles) {
13737
13808
  const generatedPaths = new Set(generatedFiles.map((f) => caseFoldIdentity(f.getFilePath())));
13738
13809
  const orphanFiles = existingFiles.filter((f) => !generatedPaths.has(caseFoldIdentity(f.getFilePath())));
13810
+ const refusedDirs = /* @__PURE__ */ new Map();
13739
13811
  let removedCount = 0;
13740
13812
  for (const aiFile of orphanFiles) {
13741
13813
  const filePath = aiFile.getFilePath();
13742
- if (await refusesDeleteOutsideRoot({
13743
- logger: this.logger,
13744
- rootPath: aiFile.getOutputRoot(),
13745
- filePath
13746
- })) continue;
13814
+ const rootPath = aiFile.getOutputRoot();
13815
+ const dirPath = dirname(filePath);
13816
+ const cacheKey = JSON.stringify([rootPath, dirPath]);
13817
+ let refused = refusedDirs.get(cacheKey);
13818
+ if (refused === void 0) {
13819
+ refused = await refusesDeleteOutsideRoot({
13820
+ logger: this.logger,
13821
+ rootPath,
13822
+ dirPath
13823
+ });
13824
+ refusedDirs.set(cacheKey, refused);
13825
+ }
13826
+ if (refused) continue;
13747
13827
  const loggedPath = stripControlCharacters(filePath);
13748
13828
  if (this.dryRun) this.logger.info(`[DRY RUN] Would delete: ${loggedPath}`);
13749
13829
  else {
@@ -13864,22 +13944,24 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
13864
13944
  return true;
13865
13945
  }
13866
13946
  /**
13867
- * Whether deleting `filePath` has to be refused because a link on the way to
13868
- * the directory holding it leads out of `rootPath`, warning about the refusal.
13947
+ * Whether deleting the files in `dirPath` has to be refused because a link on
13948
+ * the way to that directory leads out of `rootPath`, warning about the refusal
13949
+ * once for the whole directory.
13869
13950
  *
13870
13951
  * Deletion candidates are enumerated without following links *inside* the
13871
13952
  * scanned directory, but the scanned directory itself is reached through
13872
13953
  * whatever links its path contains: a checked-out repository can carry
13873
13954
  * `.cursor/commands -> ~/notes`, and every file in `~/notes` would then read as
13874
- * an orphan. Only the parent is judged, because removing a file that is itself
13875
- * a link unlinks the link where it is spelled and never touches its target.
13955
+ * an orphan. Only the directory is judged, never the file in it, because
13956
+ * removing a file that is itself a link unlinks the link where it is spelled
13957
+ * and never touches its target.
13876
13958
  */
13877
- async function refusesDeleteOutsideRoot({ logger, rootPath, filePath }) {
13959
+ async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath }) {
13878
13960
  if (!await writablePathEscapesRoot({
13879
13961
  rootPath,
13880
- targetPath: dirname(filePath)
13962
+ targetPath: dirPath
13881
13963
  })) return false;
13882
- logger.warn(`Refusing to delete ${quoteForLog(filePath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13964
+ logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
13883
13965
  return true;
13884
13966
  }
13885
13967
  /**
@@ -14118,12 +14200,12 @@ const AmpCheckFrontmatterSchema = z.looseObject({
14118
14200
  * this format continue to apply.
14119
14201
  * @see https://ampcode.com/docs/review
14120
14202
  */
14121
- var AmpCheck = class AmpCheck extends ToolCheck {
14203
+ var AmpCheck = class extends ToolCheck {
14122
14204
  frontmatter;
14123
14205
  body;
14124
14206
  constructor({ frontmatter, body, fileContent, ...rest }) {
14125
14207
  if (rest.validate !== false) {
14126
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14208
+ const result = new.target.getFrontmatterSchema().safeParse(frontmatter);
14127
14209
  if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
14128
14210
  }
14129
14211
  super({
@@ -14133,6 +14215,26 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14133
14215
  this.frontmatter = frontmatter;
14134
14216
  this.body = body;
14135
14217
  }
14218
+ /**
14219
+ * The tool target this check format belongs to, which is also the name of
14220
+ * its tool-specific frontmatter section. Goose reads the same per-file
14221
+ * format with a few extra keys, so `GooseCheck` reuses this class through
14222
+ * these hooks.
14223
+ */
14224
+ static getCheckToolTarget() {
14225
+ return "amp";
14226
+ }
14227
+ /** The frontmatter schema a check file of this tool is validated against. */
14228
+ static getFrontmatterSchema() {
14229
+ return AmpCheckFrontmatterSchema;
14230
+ }
14231
+ /**
14232
+ * Frontmatter keys only this tool reads. Import moves them into the tool's
14233
+ * own section so they are not passed on to every other checks target.
14234
+ */
14235
+ static getToolOnlyKeys() {
14236
+ return [];
14237
+ }
14136
14238
  static getSettablePaths({ global = false } = {}) {
14137
14239
  return { relativeDirPath: global ? AMP_CHECKS_GLOBAL_DIR : AMP_CHECKS_PROJECT_DIR };
14138
14240
  }
@@ -14144,6 +14246,10 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14144
14246
  }
14145
14247
  toRulesyncCheck() {
14146
14248
  const { name: _name, description, "severity-default": severityDefault, tools, ...restFields } = this.frontmatter;
14249
+ const checkClass = this.constructor;
14250
+ const toolOnlyKeys = new Set(checkClass.getToolOnlyKeys());
14251
+ const sharedFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolOnlyKeys.has(key)));
14252
+ const toolSection = Object.fromEntries(Object.entries(restFields).filter(([key]) => toolOnlyKeys.has(key)));
14147
14253
  return new RulesyncCheck({
14148
14254
  outputRoot: ".",
14149
14255
  frontmatter: {
@@ -14151,7 +14257,8 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14151
14257
  ...description !== void 0 && { description },
14152
14258
  ...severityDefault !== void 0 && { severity: severityDefault },
14153
14259
  ...tools !== void 0 && { tools },
14154
- ...restFields
14260
+ ...sharedFields,
14261
+ ...Object.keys(toolSection).length > 0 && { [checkClass.getCheckToolTarget()]: toolSection }
14155
14262
  },
14156
14263
  body: this.body,
14157
14264
  relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
@@ -14166,22 +14273,24 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14166
14273
  const { targets: _targets, description, severity, tools, ...restFields } = rulesyncFrontmatter;
14167
14274
  const toolTargetKeys = new Set(ALL_TOOL_TARGETS);
14168
14275
  const passthroughFields = Object.fromEntries(Object.entries(restFields).filter(([key]) => !toolTargetKeys.has(key) && key !== "name"));
14169
- const ampSection = this.filterToolSpecificSection(rulesyncFrontmatter.amp ?? {}, ["name"]);
14276
+ const toolTarget = this.getCheckToolTarget();
14277
+ const rawSection = rulesyncFrontmatter[toolTarget];
14278
+ const toolSection = this.filterToolSpecificSection(rawSection !== null && typeof rawSection === "object" && !Array.isArray(rawSection) ? rawSection : {}, ["name"]);
14170
14279
  const rawAmpFrontmatter = {
14171
14280
  name,
14172
14281
  ...description !== void 0 && { description },
14173
14282
  ...severity !== void 0 && { "severity-default": severity },
14174
14283
  ...tools !== void 0 && { tools },
14175
14284
  ...passthroughFields,
14176
- ...ampSection
14285
+ ...toolSection
14177
14286
  };
14178
- const result = AmpCheckFrontmatterSchema.safeParse(rawAmpFrontmatter);
14179
- if (!result.success) throw new Error(`Invalid amp check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14287
+ const result = this.getFrontmatterSchema().safeParse(rawAmpFrontmatter);
14288
+ if (!result.success) throw new Error(`Invalid ${toolTarget} check frontmatter in ${relativeFilePath}: ${formatError(result.error)}`);
14180
14289
  const ampFrontmatter = result.data;
14181
14290
  const body = rulesyncCheck.getBody();
14182
14291
  const fileContent = stringifyFrontmatter(body, ampFrontmatter);
14183
14292
  const paths = this.getSettablePaths({ global });
14184
- return new AmpCheck({
14293
+ return new this({
14185
14294
  outputRoot,
14186
14295
  frontmatter: ampFrontmatter,
14187
14296
  body,
@@ -14196,7 +14305,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14196
14305
  success: true,
14197
14306
  error: null
14198
14307
  };
14199
- const result = AmpCheckFrontmatterSchema.safeParse(this.frontmatter);
14308
+ const result = this.constructor.getFrontmatterSchema().safeParse(this.frontmatter);
14200
14309
  if (result.success) return {
14201
14310
  success: true,
14202
14311
  error: null
@@ -14209,7 +14318,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14209
14318
  static isTargetedByRulesyncCheck(rulesyncCheck) {
14210
14319
  return this.isTargetedByRulesyncCheckDefault({
14211
14320
  rulesyncCheck,
14212
- toolTarget: "amp"
14321
+ toolTarget: this.getCheckToolTarget()
14213
14322
  });
14214
14323
  }
14215
14324
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
@@ -14217,9 +14326,9 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14217
14326
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
14218
14327
  const fileContent = await readFileContent(filePath);
14219
14328
  const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
14220
- const result = AmpCheckFrontmatterSchema.safeParse(frontmatter);
14329
+ const result = this.getFrontmatterSchema().safeParse(frontmatter);
14221
14330
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
14222
- return new AmpCheck({
14331
+ return new this({
14223
14332
  outputRoot,
14224
14333
  relativeDirPath: paths.relativeDirPath,
14225
14334
  relativeFilePath,
@@ -14230,7 +14339,7 @@ var AmpCheck = class AmpCheck extends ToolCheck {
14230
14339
  });
14231
14340
  }
14232
14341
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
14233
- return new AmpCheck({
14342
+ return new this({
14234
14343
  outputRoot,
14235
14344
  relativeDirPath,
14236
14345
  relativeFilePath,
@@ -15330,6 +15439,60 @@ var GitlabduoCheck = class GitlabduoCheck extends ToolCheck {
15330
15439
  }
15331
15440
  };
15332
15441
  //#endregion
15442
+ //#region src/constants/goose-paths.ts
15443
+ const GOOSE_DIR = ".goose";
15444
+ const GOOSE_GLOBAL_DIR = join(".config", "goose");
15445
+ const GOOSE_RULE_FILE_NAME = ".goosehints";
15446
+ const GOOSE_MCP_FILE_NAME = "config.yaml";
15447
+ const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
15448
+ const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
15449
+ const GOOSE_HOOKS_FILE_NAME = "hooks.json";
15450
+ const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
15451
+ const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
15452
+ const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
15453
+ const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
15454
+ const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
15455
+ const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
15456
+ const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
15457
+ const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
15458
+ const GOOSE_CHECKS_PROJECT_DIR = join(".agents", "checks");
15459
+ const GOOSE_CHECKS_GLOBAL_DIR = join(GOOSE_GLOBAL_DIR, "checks");
15460
+ //#endregion
15461
+ //#region src/features/checks/goose-check.ts
15462
+ const GooseCheckFrontmatterSchema = z.extend(AmpCheckFrontmatterSchema, {
15463
+ model: z.optional(z.string()),
15464
+ "turn-limit": z.optional(z.int().check(z.nonnegative()))
15465
+ });
15466
+ /**
15467
+ * Represents a Goose review check (`goose review`), written to the user-wide
15468
+ * `~/.config/goose/checks/` (global scope only).
15469
+ *
15470
+ * Goose discovers repository checks in `.agents/checks/*.md`, the same
15471
+ * cross-tool directory the `amp` checks target already writes (Goose's
15472
+ * `tools` and `severity-default` fields exist for parity with Amp's), so the
15473
+ * project scope is left to `amp` rather than having two targets own the same
15474
+ * files. Its global discovery reads `~/.config/goose/checks/` first, then
15475
+ * `~/.config/agents/checks/`. The file format is Amp's, so this class reuses
15476
+ * `AmpCheck` and adds Goose's `model` and `turn-limit`, which live in the
15477
+ * `goose:` section of a rulesync check; `name` is derived from the file name.
15478
+ *
15479
+ * @see https://github.com/aaif-goose/goose/blob/main/crates/goose/src/checks/mod.rs
15480
+ */
15481
+ var GooseCheck = class extends AmpCheck {
15482
+ static getCheckToolTarget() {
15483
+ return "goose";
15484
+ }
15485
+ static getFrontmatterSchema() {
15486
+ return GooseCheckFrontmatterSchema;
15487
+ }
15488
+ static getToolOnlyKeys() {
15489
+ return ["model", "turn-limit"];
15490
+ }
15491
+ static getSettablePaths({ global = false } = {}) {
15492
+ return { relativeDirPath: global ? GOOSE_CHECKS_GLOBAL_DIR : GOOSE_CHECKS_PROJECT_DIR };
15493
+ }
15494
+ };
15495
+ //#endregion
15333
15496
  //#region src/constants/hermesagent-paths.ts
15334
15497
  /**
15335
15498
  * Hermes Agent configuration-layout conventions.
@@ -16058,6 +16221,14 @@ const toolCheckFactories = /* @__PURE__ */ new Map([
16058
16221
  committedOutput: true
16059
16222
  }
16060
16223
  }],
16224
+ ["goose", {
16225
+ class: GooseCheck,
16226
+ meta: {
16227
+ supportsGlobal: true,
16228
+ supportsProject: false,
16229
+ filePattern: "*.md"
16230
+ }
16231
+ }],
16061
16232
  ["hermesagent", {
16062
16233
  class: HermesagentCheck,
16063
16234
  meta: {
@@ -16103,7 +16274,9 @@ const defaultGetFactory$6 = (target) => {
16103
16274
  return factory;
16104
16275
  };
16105
16276
  const allToolTargetKeys$5 = [...toolCheckFactories.keys()];
16106
- const checksProcessorToolTargets = allToolTargetKeys$5;
16277
+ const checksProcessorToolTargets = allToolTargetKeys$5.filter((target) => {
16278
+ return toolCheckFactories.get(target)?.meta.supportsProject !== false;
16279
+ });
16107
16280
  const checksProcessorToolTargetsGlobal = allToolTargetKeys$5.filter((target) => {
16108
16281
  return toolCheckFactories.get(target)?.meta.supportsGlobal ?? false;
16109
16282
  });
@@ -16274,23 +16447,6 @@ var ChecksProcessor = class extends FeatureProcessor {
16274
16447
  }
16275
16448
  };
16276
16449
  //#endregion
16277
- //#region src/constants/goose-paths.ts
16278
- const GOOSE_DIR = ".goose";
16279
- const GOOSE_GLOBAL_DIR = join(".config", "goose");
16280
- const GOOSE_RULE_FILE_NAME = ".goosehints";
16281
- const GOOSE_MCP_FILE_NAME = "config.yaml";
16282
- const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
16283
- const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
16284
- const GOOSE_HOOKS_FILE_NAME = "hooks.json";
16285
- const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
16286
- const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
16287
- const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
16288
- const GOOSE_GLOBAL_SKILLS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "skills");
16289
- const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
16290
- const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
16291
- const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
16292
- const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
16293
- //#endregion
16294
16450
  //#region src/utils/tool-home.ts
16295
16451
  /**
16296
16452
  * Where the rulesync-side source files of a tool with a home override belong.
@@ -17202,7 +17358,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
17202
17358
  static getProjectRelativeDirPath() {
17203
17359
  throw new Error("Please implement this method in the subclass.");
17204
17360
  }
17205
- /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/antigravity/global_workflows`). */
17361
+ /** Global-scope workflows directory under `~/.gemini/` (e.g. `.gemini/config/workflows`). */
17206
17362
  static getGlobalRelativeDirPath() {
17207
17363
  throw new Error("Please implement this method in the subclass.");
17208
17364
  }
@@ -17365,7 +17521,8 @@ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
17365
17521
  const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
17366
17522
  const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
17367
17523
  const ANTIGRAVITY_IDE_GLOBAL_RULES_SUBDIR = join(ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "rules");
17368
- const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17524
+ const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR, "workflows");
17525
+ const ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
17369
17526
  const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
17370
17527
  const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
17371
17528
  //#endregion
@@ -17395,10 +17552,12 @@ var AntigravityIdeSkill = class extends AntigravitySharedSkill {
17395
17552
  * Emits each command as a skill in `.agents/skills/<name>/SKILL.md` (project
17396
17553
  * scope) and `~/.gemini/config/skills/<name>/SKILL.md` (global scope), the
17397
17554
  * IDE's skills directories. Legacy workflows are still imported from
17398
- * `.agents/workflows/` and `~/.gemini/antigravity/global_workflows/`. All
17399
- * body and frontmatter handling is shared with {@link AntigravitySharedCommand}.
17555
+ * `.agents/workflows/` and `~/.gemini/config/workflows/` (the global workflows
17556
+ * directory of Antigravity 2.0), and, in global scope only, from the pre-2.0
17557
+ * `~/.gemini/antigravity/global_workflows/`. All body and frontmatter handling
17558
+ * is shared with {@link AntigravitySharedCommand}.
17400
17559
  */
17401
- var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17560
+ var AntigravityIdeCommand = class AntigravityIdeCommand extends AntigravitySharedCommand {
17402
17561
  static getProjectRelativeDirPath() {
17403
17562
  return ANTIGRAVITY_IDE_COMMANDS_DIR_PATH;
17404
17563
  }
@@ -17408,6 +17567,37 @@ var AntigravityIdeCommand = class extends AntigravitySharedCommand {
17408
17567
  static getSkillsRelativeDirPath({ global }) {
17409
17568
  return AntigravityIdeSkill.getSettablePaths({ global }).relativeDirPath;
17410
17569
  }
17570
+ /**
17571
+ * Import-only, global scope: the workflows left in the pre-2.0
17572
+ * `~/.gemini/antigravity/global_workflows/` directory, where rulesync also
17573
+ * wrote IDE global commands before they were emitted as skills. The
17574
+ * directory is never swept by `generate --delete`; a workflow with the same
17575
+ * file name in `~/.gemini/config/workflows/` takes precedence.
17576
+ *
17577
+ * @see https://antigravity.google/docs/migration/workflows-to-skills
17578
+ */
17579
+ static async loadAdditionalImportFiles({ outputRoot = process.cwd(), global = false, logger } = {}) {
17580
+ if (!global) return [];
17581
+ const filePaths = await findFilesByGlobs("*.md", { cwd: join(outputRoot, ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH) });
17582
+ return (await Promise.all(filePaths.map(async (filePath) => {
17583
+ const fileContent = await readFileContent(filePath);
17584
+ const { frontmatter, body } = parseFrontmatter(fileContent, filePath);
17585
+ const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter);
17586
+ if (!result.success) {
17587
+ logger?.warn(`Skipping ${filePath}: invalid frontmatter (${formatError(result.error)}).`);
17588
+ return;
17589
+ }
17590
+ return new AntigravityIdeCommand({
17591
+ outputRoot,
17592
+ relativeDirPath: ANTIGRAVITY_IDE_PRE_V2_GLOBAL_WORKFLOWS_DIR_PATH,
17593
+ relativeFilePath: basename(filePath),
17594
+ frontmatter: result.data,
17595
+ body: body.trim(),
17596
+ fileContent,
17597
+ global
17598
+ });
17599
+ }))).filter((command) => command !== void 0);
17600
+ }
17411
17601
  getToolTargetName() {
17412
17602
  return "antigravity-ide";
17413
17603
  }
@@ -17978,27 +18168,71 @@ const CLINE_HOOKS_GLOBAL_DIR_PATH = join("Documents", "Cline", "Hooks");
17978
18168
  const CLINE_HOOKS_MANIFEST_FILE_NAME = "rulesync-hooks.json";
17979
18169
  //#endregion
17980
18170
  //#region src/features/commands/cline-command.ts
18171
+ const CLINE_WORKFLOW_FIELD_TYPES = {
18172
+ name: "string",
18173
+ disabled: "boolean",
18174
+ enabled: "boolean"
18175
+ };
18176
+ function sanitizeClineWorkflowFrontmatter({ cline, relativeFilePath, logger }) {
18177
+ if (!isRecord$1(cline)) return {};
18178
+ return Object.fromEntries(Object.entries(cline).filter(([key, value]) => {
18179
+ const expected = Object.hasOwn(CLINE_WORKFLOW_FIELD_TYPES, key) ? CLINE_WORKFLOW_FIELD_TYPES[key] : void 0;
18180
+ if (expected === void 0 || typeof value === expected) return true;
18181
+ warnWithFallback(logger, `Cline commands: dropping cline.${key} from ${relativeFilePath}: Cline rejects the whole workflow unless it is a ${expected}.`);
18182
+ return false;
18183
+ }));
18184
+ }
18185
+ /**
18186
+ * Cline workflows (commands). A workflow may carry YAML frontmatter that Cline
18187
+ * reads: `name` (the slash-command name) and a `disabled` / `enabled` switch
18188
+ * (`parseWorkflowConfigFromMarkdown` in Cline's
18189
+ * `user-instruction-config-loader.ts`). The `cline:` section of a
18190
+ * `.rulesync/commands/*.md` file is written as that frontmatter, and every key
18191
+ * of an imported workflow's frontmatter is lifted back into `cline:`, so
18192
+ * hand-written workflow frontmatter survives an import → generate round trip.
18193
+ * Cline's own UI toggle is kept in Cline's state, not in the file. A workflow
18194
+ * without a `cline:` section is written as a bare body, as before.
18195
+ *
18196
+ * @see https://github.com/cline/cline/blob/main/sdk/packages/core/src/extensions/config/user-instruction-config-loader.ts
18197
+ */
17981
18198
  var ClineCommand = class ClineCommand extends ToolCommand {
17982
18199
  static getSettablePaths({ global } = {}) {
17983
18200
  if (global) return { relativeDirPath: CLINE_COMMANDS_GLOBAL_DIR_PATH };
17984
18201
  return { relativeDirPath: CLINE_COMMANDS_DIR_PATH };
17985
18202
  }
18203
+ /** The workflow's own frontmatter, or an empty object for a bare body. */
18204
+ getClineFrontmatter() {
18205
+ const { frontmatter } = parseFrontmatter(this.getFileContent(), this.getFilePath());
18206
+ return isRecord$1(frontmatter) ? frontmatter : {};
18207
+ }
17986
18208
  toRulesyncCommand() {
18209
+ const cline = this.getClineFrontmatter();
18210
+ const rulesyncFrontmatter = {
18211
+ targets: ["*"],
18212
+ ...Object.keys(cline).length > 0 && { cline }
18213
+ };
18214
+ const body = this.getBody();
17987
18215
  return new RulesyncCommand({
17988
18216
  outputRoot: process.cwd(),
17989
- frontmatter: { targets: ["*"] },
17990
- body: this.getFileContent(),
18217
+ frontmatter: rulesyncFrontmatter,
18218
+ body,
17991
18219
  relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
17992
18220
  relativeFilePath: this.relativeFilePath,
17993
- fileContent: this.getFileContent(),
18221
+ fileContent: stringifyFrontmatter(body, rulesyncFrontmatter),
17994
18222
  validate: true
17995
18223
  });
17996
18224
  }
17997
- static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
18225
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false, logger }) {
17998
18226
  const paths = this.getSettablePaths({ global });
18227
+ const cline = sanitizeClineWorkflowFrontmatter({
18228
+ cline: rulesyncCommand.getFrontmatter().cline,
18229
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18230
+ logger
18231
+ });
18232
+ const body = rulesyncCommand.getBody();
17999
18233
  return new ClineCommand({
18000
18234
  outputRoot,
18001
- fileContent: rulesyncCommand.getBody(),
18235
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body, cline) : body,
18002
18236
  relativeDirPath: paths.relativeDirPath,
18003
18237
  relativeFilePath: rulesyncCommand.getRelativeFilePath(),
18004
18238
  validate
@@ -18011,7 +18245,7 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18011
18245
  };
18012
18246
  }
18013
18247
  getBody() {
18014
- return this.getFileContent();
18248
+ return parseFrontmatter(this.getFileContent(), this.getFilePath()).body.trim();
18015
18249
  }
18016
18250
  static isTargetedByRulesyncCommand(rulesyncCommand) {
18017
18251
  return this.isTargetedByRulesyncCommandDefault({
@@ -18022,12 +18256,13 @@ var ClineCommand = class ClineCommand extends ToolCommand {
18022
18256
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
18023
18257
  const paths = this.getSettablePaths({ global });
18024
18258
  const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
18025
- const { body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
18259
+ const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
18260
+ const cline = isRecord$1(frontmatter) ? frontmatter : {};
18026
18261
  return new ClineCommand({
18027
18262
  outputRoot,
18028
18263
  relativeDirPath: paths.relativeDirPath,
18029
18264
  relativeFilePath,
18030
- fileContent: content.trim(),
18265
+ fileContent: Object.keys(cline).length > 0 ? stringifyFrontmatter(body.trim(), cline) : body.trim(),
18031
18266
  validate
18032
18267
  });
18033
18268
  }
@@ -25060,7 +25295,7 @@ function findCommandPaths({ command, scan }) {
25060
25295
  } else {
25061
25296
  if (path !== void 0) toAnchor.add(path);
25062
25297
  state.owner = text.slice(text.lastIndexOf("/") + 1);
25063
- state.expecting = afterCommandWord(state.owner);
25298
+ state.expecting = afterCommandWord$1(state.owner);
25064
25299
  state.prefix = void 0;
25065
25300
  state.shellCode = false;
25066
25301
  }
@@ -25126,7 +25361,7 @@ function readOption({ state, text }) {
25126
25361
  if (SHELL_NAME_PATTERN.test(state.owner) && SHELL_CODE_OPTION.test(text)) state.shellCode = true;
25127
25362
  }
25128
25363
  /** What follows a command word whose basename is `name`. */
25129
- function afterCommandWord(name) {
25364
+ function afterCommandWord$1(name) {
25130
25365
  if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
25131
25366
  if (PACKAGE_RUNNERS.has(name)) return "wrapped";
25132
25367
  return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
@@ -33469,6 +33704,19 @@ function isQwencodeLegacyMillisecondTimeout(timeout) {
33469
33704
  return typeof timeout === "number" && Number.isFinite(timeout) && timeout >= QWENCODE_LEGACY_MILLISECOND_TIMEOUT_THRESHOLD;
33470
33705
  }
33471
33706
  /**
33707
+ * Read a `timeout` the way Qwen Code does. Settings files are not type-checked
33708
+ * and Qwen Code coerces a numeric string such as `"60000"` with `Number()`, so
33709
+ * import accepts one too; a value that is not a finite number is dropped, since
33710
+ * Qwen Code falls back to the hook type's default for it.
33711
+ * https://github.com/QwenLM/qwen-code/blob/main/packages/core/src/hooks/hook-timeout.ts
33712
+ */
33713
+ function readQwencodeTimeout(timeout) {
33714
+ if (timeout === void 0) return void 0;
33715
+ if (typeof timeout === "string" && timeout.trim() === "") return void 0;
33716
+ const value = Number(timeout);
33717
+ return Number.isFinite(value) ? value : void 0;
33718
+ }
33719
+ /**
33472
33720
  * Canonical `timeout` is seconds. A command hook value below the threshold is
33473
33721
  * already read as seconds; a larger one can only be expressed in milliseconds.
33474
33722
  */
@@ -33579,7 +33827,7 @@ const QwencodeHookEntrySchema = z.looseObject({
33579
33827
  type: z.optional(z.string()),
33580
33828
  command: z.optional(z.string()),
33581
33829
  url: z.optional(z.string()),
33582
- timeout: z.optional(z.number()),
33830
+ timeout: z.optional(z.union([z.number(), z.string()])),
33583
33831
  name: z.optional(z.string()),
33584
33832
  description: z.optional(z.string()),
33585
33833
  statusMessage: z.optional(z.string()),
@@ -33612,12 +33860,13 @@ function qwencodeHookEntryToCanonical({ hook, sequential, matcher }) {
33612
33860
  const isCommand = hookType === "command";
33613
33861
  const isPrompt = hookType === "prompt";
33614
33862
  const shell = h.shell === "bash" || h.shell === "powershell" ? h.shell : void 0;
33863
+ const timeout = readQwencodeTimeout(h.timeout);
33615
33864
  return {
33616
33865
  type: hookType,
33617
33866
  ...compact({
33618
33867
  command: h.command,
33619
33868
  url: h.url,
33620
- timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(h.timeout) : h.timeout,
33869
+ timeout: isCommand ? qwencodeCommandTimeoutToCanonicalTimeout(timeout) : timeout,
33621
33870
  name: h.name,
33622
33871
  description: h.description,
33623
33872
  statusMessage: h.statusMessage,
@@ -38119,7 +38368,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
38119
38368
  fileKey: sharedConfigFileKey(basePaths),
38120
38369
  feature: "mcp",
38121
38370
  existingContent: fileContent ?? "",
38122
- patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
38371
+ patch: { [AMP_MCP_SERVERS_KEY]: undefinedWhenEmpty(filterMcpServers(rulesyncMcp.getMcpServers())) },
38123
38372
  filePath: join(jsonDir, relativeFilePath),
38124
38373
  logger
38125
38374
  }),
@@ -38417,6 +38666,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38417
38666
  const paths = this.getSettablePaths({ global });
38418
38667
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
38419
38668
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
38669
+ const mcpServers = rulesyncMcp.getMcpServers();
38420
38670
  return new AugmentcodeMcp({
38421
38671
  outputRoot,
38422
38672
  relativeDirPath: paths.relativeDirPath,
@@ -38425,7 +38675,7 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
38425
38675
  fileKey: sharedConfigFileKey(paths),
38426
38676
  feature: "mcp",
38427
38677
  existingContent,
38428
- patch: { mcpServers: rulesyncMcp.getMcpServers() },
38678
+ patch: { mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : void 0 },
38429
38679
  filePath,
38430
38680
  logger
38431
38681
  }),
@@ -38981,6 +39231,229 @@ var ClaudecodeMcp = class ClaudecodeMcp extends ToolMcp {
38981
39231
  }
38982
39232
  };
38983
39233
  //#endregion
39234
+ //#region src/features/mcp/claudecode-plugin-mcp.ts
39235
+ /**
39236
+ * The placeholder Claude Code substitutes with the installed plugin's own
39237
+ * directory in an MCP server's `command`, `args` and `env`.
39238
+ * @see https://code.claude.com/docs/en/plugins-reference
39239
+ */
39240
+ const PLUGIN_ROOT_PLACEHOLDER = "${CLAUDE_PLUGIN_ROOT}";
39241
+ const PLUGIN_ROOT_PREFIX = /^(?:\$\{CLAUDE_PLUGIN_ROOT\}|"\$CLAUDE_PLUGIN_ROOT"|\$CLAUDE_PLUGIN_ROOT)\//;
39242
+ /**
39243
+ * Whether a server `command` names a file inside the plugin rather than an
39244
+ * executable found on `PATH`. The command is spawned without a shell, so a
39245
+ * name with a slash is a path, and one without (`npx`, `node`, `uvx`) is a
39246
+ * `PATH` lookup. An absolute path, a home path, one that leaves the plugin
39247
+ * (`../`), and one that already starts with a variable are left as written.
39248
+ */
39249
+ function isPluginRelativeCommand(command) {
39250
+ return /[\\/]/.test(command) && !command.startsWith("$") && !command.startsWith("%") && !command.startsWith("~") && !/^\.\.[\\/]/.test(command) && !posix.isAbsolute(command) && !win32.isAbsolute(command);
39251
+ }
39252
+ function anchorToPluginRoot(path) {
39253
+ return `${PLUGIN_ROOT_PLACEHOLDER}/${path.replace(/^\.[\\/]/, "")}`;
39254
+ }
39255
+ /** The runner name of a command word: its basename without a `.exe` suffix. */
39256
+ function runnerName(command) {
39257
+ return (command.split(/[\\/]/).pop() ?? command).replace(/\.exe$/i, "");
39258
+ }
39259
+ /** What follows a command word, as plugin hook commands read it. */
39260
+ function afterCommandWord(name) {
39261
+ if (Object.hasOwn(RUN_SUBCOMMANDS, name)) return "subcommand";
39262
+ if (PACKAGE_RUNNERS.has(name)) return "command";
39263
+ return SCRIPT_RUNNER_PATTERN.test(name) ? "script" : "none";
39264
+ }
39265
+ /**
39266
+ * The positions in `args` that name a file the server runs, read with the
39267
+ * rules plugin hook commands follow (`hook-command-paths.ts`): the script of a
39268
+ * script runner (`node ./dist/server.js`), the command after a run subcommand
39269
+ * or package runner (`uv run ./server.py`, `npx tsx ./x.ts`), and the value of
39270
+ * an option that loads a file (`node -r ./register.js`). Only a position whose
39271
+ * value `isPath` accepts is returned. Everything else is data the server reads
39272
+ * relative to the consumer's project (`server-filesystem ./docs`), and the
39273
+ * scan stops at an option that runs inline code or standard input
39274
+ * (`node -e`, `python3 -m`, `sh -c`, `bash -s`).
39275
+ */
39276
+ function fileArgumentIndices({ command, args, isPath }) {
39277
+ const indices = /* @__PURE__ */ new Set();
39278
+ let owner = runnerName(command);
39279
+ let expecting = afterCommandWord(owner);
39280
+ for (let index = 0; index < args.length && expecting !== "none"; index++) {
39281
+ const arg = args[index];
39282
+ if (typeof arg !== "string") break;
39283
+ if (arg.startsWith("-") && arg !== "-") {
39284
+ if (optionsFor({
39285
+ table: CODE_OPTIONS,
39286
+ name: owner
39287
+ })?.has(arg) || SHELL_NAME_PATTERN.test(owner) && SHELL_CODE_OPTION.test(arg)) break;
39288
+ if (optionsFor({
39289
+ table: VALUE_OPTIONS,
39290
+ name: owner
39291
+ })?.has(arg)) {
39292
+ const value = args[index + 1];
39293
+ if (typeof value === "string" && isPath(value)) indices.add(index + 1);
39294
+ index++;
39295
+ }
39296
+ continue;
39297
+ }
39298
+ if (expecting === "subcommand") {
39299
+ if (RUN_SUBCOMMANDS[owner]?.has(arg)) {
39300
+ expecting = "command";
39301
+ continue;
39302
+ }
39303
+ if (!SCRIPT_RUNNER_PATTERN.test(owner)) break;
39304
+ expecting = "script";
39305
+ }
39306
+ if (isPath(arg)) indices.add(index);
39307
+ if (expecting === "script") break;
39308
+ owner = runnerName(arg);
39309
+ expecting = afterCommandWord(owner);
39310
+ }
39311
+ return indices;
39312
+ }
39313
+ function isDotRelative(value) {
39314
+ return /^\.[\\/]/.test(value);
39315
+ }
39316
+ /** Turn a plugin-root path back into its `./` form, or return it unchanged. */
39317
+ function relativizeFromPluginRoot(value) {
39318
+ return PLUGIN_ROOT_PREFIX.test(value) ? value.replace(PLUGIN_ROOT_PREFIX, "./") : value;
39319
+ }
39320
+ /** The command and arguments of a stdio server, with inline array arguments split out. */
39321
+ function splitCommand(server) {
39322
+ if (Array.isArray(server.command)) {
39323
+ const [first, ...rest] = server.command;
39324
+ return {
39325
+ command: typeof first === "string" ? first : void 0,
39326
+ args: [...rest, ...Array.isArray(server.args) ? server.args : []]
39327
+ };
39328
+ }
39329
+ return {
39330
+ command: typeof server.command === "string" ? server.command : void 0,
39331
+ args: Array.isArray(server.args) ? server.args : []
39332
+ };
39333
+ }
39334
+ /**
39335
+ * Rewrite a server's command and the argument positions that name a file it
39336
+ * runs (see `fileArgumentIndices`), leaving every other argument as written.
39337
+ * Generate and import share this, so a position generate never anchors is
39338
+ * never relativized on import either, and a hand-written
39339
+ * `${CLAUDE_PLUGIN_ROOT}/…` data argument keeps its meaning.
39340
+ */
39341
+ function rewriteServer({ server, rewriteCommand, isPath, rewritePath }) {
39342
+ const { command, args } = splitCommand(server);
39343
+ if (command === void 0) return server;
39344
+ const indices = fileArgumentIndices({
39345
+ command,
39346
+ args,
39347
+ isPath
39348
+ });
39349
+ const rewriteArgs = (values, offset) => values.map((value, index) => indices.has(index + offset) && typeof value === "string" ? rewritePath(value) : value);
39350
+ const next = { ...server };
39351
+ if (Array.isArray(server.command)) {
39352
+ const inlineArgs = server.command.slice(1);
39353
+ next.command = [rewriteCommand(command), ...rewriteArgs(inlineArgs, 0)];
39354
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, inlineArgs.length);
39355
+ } else {
39356
+ next.command = rewriteCommand(command);
39357
+ if (Array.isArray(server.args)) next.args = rewriteArgs(server.args, 0);
39358
+ }
39359
+ return next;
39360
+ }
39361
+ /** Anchor a server's plugin-relative command and the files its runner executes. */
39362
+ function anchorServer(server) {
39363
+ return rewriteServer({
39364
+ server,
39365
+ rewriteCommand: (command) => isPluginRelativeCommand(command) ? anchorToPluginRoot(command) : command,
39366
+ isPath: isDotRelative,
39367
+ rewritePath: anchorToPluginRoot
39368
+ });
39369
+ }
39370
+ /** Turn the plugin-root forms generate writes back into `./`. */
39371
+ function relativizeServer(server) {
39372
+ return rewriteServer({
39373
+ server,
39374
+ rewriteCommand: relativizeFromPluginRoot,
39375
+ isPath: (value) => PLUGIN_ROOT_PREFIX.test(value),
39376
+ rewritePath: relativizeFromPluginRoot
39377
+ });
39378
+ }
39379
+ /** Apply a per-server transform to every server entry that is an object. */
39380
+ function mapServers({ mcpServers, transform }) {
39381
+ if (mcpServers === null || typeof mcpServers !== "object" || Array.isArray(mcpServers)) return mcpServers;
39382
+ return Object.fromEntries(Object.entries(mcpServers).map(([name, server]) => server === null || typeof server !== "object" || Array.isArray(server) ? [name, server] : [name, transform(server)]));
39383
+ }
39384
+ /**
39385
+ * MCP servers bundled in a Claude Code plugin (`.mcp.json` at the plugin
39386
+ * root).
39387
+ *
39388
+ * Claude Code loads a plugin's `.mcp.json` with the consumer's project as the
39389
+ * working directory, so a relative `command` such as `./servers/db-server`
39390
+ * would be looked up in the consumer's repository, where the bundled binary
39391
+ * does not exist. Generate therefore rewrites a relative `command`, and the
39392
+ * `./` script a script runner executes (`node ./dist/server.js`,
39393
+ * `uv run ./server.py`), to `${CLAUDE_PLUGIN_ROOT}/…`, which Claude Code
39394
+ * substitutes with the installed plugin's directory. Other `./` arguments are
39395
+ * data the server reads relative to the consumer's project
39396
+ * (`server-filesystem ./docs`), so they are left as written, the same rule
39397
+ * plugin hook commands follow. Bare executables (`npx`, `node`), absolute paths
39398
+ * and values that already start with a variable pass through. `env` values and
39399
+ * the remote transports' `url` / `headers` are left as written, because
39400
+ * rulesync cannot tell a path from an opaque string there. Import turns the
39401
+ * plugin-root forms in `command` and `args` back into `./`, including the
39402
+ * shell-variable spellings a hand-written file may have copied from a hook
39403
+ * command (which Claude Code does not substitute in MCP config).
39404
+ *
39405
+ * @see https://code.claude.com/docs/en/plugins-reference
39406
+ */
39407
+ var ClaudecodePluginMcp = class ClaudecodePluginMcp extends ClaudecodeMcp {
39408
+ static async fromRulesyncMcp(params) {
39409
+ const base = await super.fromRulesyncMcp(params);
39410
+ const json = base.getJson();
39411
+ const mcpServers = mapServers({
39412
+ mcpServers: json.mcpServers,
39413
+ transform: anchorServer
39414
+ });
39415
+ return new ClaudecodePluginMcp({
39416
+ outputRoot: params.outputRoot ?? process.cwd(),
39417
+ relativeDirPath: base.getRelativeDirPath(),
39418
+ relativeFilePath: base.getRelativeFilePath(),
39419
+ fileContent: JSON.stringify({
39420
+ ...json,
39421
+ mcpServers
39422
+ }, null, 2),
39423
+ validate: params.validate ?? true,
39424
+ global: params.global ?? false
39425
+ });
39426
+ }
39427
+ static async fromFile(params) {
39428
+ const base = await super.fromFile(params);
39429
+ return new ClaudecodePluginMcp({
39430
+ outputRoot: params.outputRoot ?? process.cwd(),
39431
+ relativeDirPath: base.getRelativeDirPath(),
39432
+ relativeFilePath: base.getRelativeFilePath(),
39433
+ fileContent: base.getFileContent(),
39434
+ validate: params.validate ?? true,
39435
+ global: params.global ?? false
39436
+ });
39437
+ }
39438
+ toRulesyncMcp() {
39439
+ const mcpServers = mapServers({
39440
+ mcpServers: this.getJson().mcpServers ?? {},
39441
+ transform: relativizeServer
39442
+ });
39443
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
39444
+ }
39445
+ static forDeletion(params) {
39446
+ return new ClaudecodePluginMcp({
39447
+ outputRoot: params.outputRoot ?? process.cwd(),
39448
+ relativeDirPath: params.relativeDirPath,
39449
+ relativeFilePath: params.relativeFilePath,
39450
+ fileContent: "{}",
39451
+ validate: false,
39452
+ global: params.global ?? false
39453
+ });
39454
+ }
39455
+ };
39456
+ //#endregion
38984
39457
  //#region src/features/mcp/cline-mcp.ts
38985
39458
  const CLINE_GLOBAL_ONLY_MESSAGE = "Cline MCP is global-only; use --global to sync ~/.cline/data/settings/cline_mcp_settings.json";
38986
39459
  function parseClineSettings(fileContent, relativeDirPath, relativeFilePath) {
@@ -40169,6 +40642,35 @@ function convertFromCodexFormat(codexMcp) {
40169
40642
  }
40170
40643
  return result;
40171
40644
  }
40645
+ /**
40646
+ * Keys Codex refuses on each transport. Its `McpServerConfig` deserializer
40647
+ * calls `throw_if_set` for these, and the error fails the whole `config.toml`,
40648
+ * not just the one server, so they are dropped with a warning instead of
40649
+ * written. Checked against `codex-rs/config/src/mcp_types.rs` at rust-v0.162.1.
40650
+ */
40651
+ const CODEX_STDIO_REJECTED_KEYS = [
40652
+ "url",
40653
+ "bearer_token_env_var",
40654
+ "bearer_token",
40655
+ "http_headers_helper",
40656
+ "http_headers",
40657
+ "env_http_headers",
40658
+ "oauth",
40659
+ "oauth_resource",
40660
+ "auth"
40661
+ ];
40662
+ const CODEX_HTTP_REJECTED_KEYS = [
40663
+ "args",
40664
+ "env",
40665
+ "env_vars",
40666
+ "cwd",
40667
+ "bearer_token"
40668
+ ];
40669
+ function dropTransportRejectedKeys({ converted, isStdio, serverName }) {
40670
+ const dropped = (isStdio ? CODEX_STDIO_REJECTED_KEYS : CODEX_HTTP_REJECTED_KEYS).filter((key) => converted[key] !== void 0);
40671
+ for (const key of dropped) delete converted[key];
40672
+ if (dropped.length > 0) warnWithFallback(void 0, `[CodexCliMcp] Dropping ${dropped.map((key) => `'${key}'`).join(", ")} from ${isStdio ? "stdio" : "url-based"} MCP server "${serverName}": Codex rejects ${dropped.length === 1 ? "this key" : "these keys"} on a ${isStdio ? "stdio" : "url-based"} server and would fail to load the whole config.toml.`);
40673
+ }
40172
40674
  function convertToCodexFormat(mcpServers) {
40173
40675
  const result = {};
40174
40676
  const originalNames = /* @__PURE__ */ new Map();
@@ -40211,6 +40713,11 @@ function convertToCodexFormat(mcpServers) {
40211
40713
  converted,
40212
40714
  serverName: name
40213
40715
  });
40716
+ dropTransportRejectedKeys({
40717
+ converted,
40718
+ isStdio,
40719
+ serverName: name
40720
+ });
40214
40721
  const previousName = originalNames.get(codexName);
40215
40722
  if (previousName !== void 0) warnWithFallback(void 0, `Codex MCP server name collision: "${previousName}" and "${name}" both normalize to "${codexName}"; "${name}" (processed last) overwrites "${previousName}".`);
40216
40723
  originalNames.set(codexName, name);
@@ -41690,7 +42197,7 @@ var CrushMcp = class CrushMcp extends ToolMcp {
41690
42197
  fileKey: sharedConfigFileKey(this.getSettablePaths({ global })),
41691
42198
  feature: "mcp",
41692
42199
  existingContent,
41693
- patch: { ["mcp"]: converted },
42200
+ patch: { ["mcp"]: undefinedWhenEmpty(converted) },
41694
42201
  filePath: location.filePath,
41695
42202
  logger
41696
42203
  }),
@@ -43270,13 +43777,23 @@ var GooseMcp = class GooseMcp extends ToolMcp {
43270
43777
  //#region src/features/mcp/grokcli-mcp.ts
43271
43778
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
43272
43779
  /**
43780
+ * Grok keeps per-server MCP tool deny lists in a top-level
43781
+ * `[disabled_mcp_tools]` table (`map<server, string[]>` of unqualified tool
43782
+ * names), not inside `[mcp_servers.<name>]`. Grok reads it from the user
43783
+ * `~/.grok/config.toml` only: a project `.grok/config.toml` contributes just
43784
+ * `[mcp_servers]`, `[plugins]`, `[permission]` and `[mcp] max_output_bytes`.
43785
+ * https://github.com/xai-org/grok-build/blob/2bdd1d6a/crates/codegen/xai-grok-pager/docs/user-guide/26-config-reference.md
43786
+ */
43787
+ const GROK_DISABLED_MCP_TOOLS_KEY = "disabled_mcp_tools";
43788
+ /**
43273
43789
  * Grok Build stores MCP servers in `config.toml` under a `[mcp_servers.<name>]`
43274
43790
  * table. Verified against `grok mcp add` (grok 0.2.54): a stdio server emits
43275
43791
  * `command`, `args`, `enabled = true`, and an `[mcp_servers.<name>.env]` table;
43276
43792
  * a remote server emits `url` and `enabled`. Unlike Codex CLI, Grok uses a
43277
- * literal `env` table (not the `env_vars` passthrough list) and has no
43278
- * per-server tool allow/deny lists, so the only field rename is
43279
- * `disabled` (rulesync) ↔ `enabled = false` (grok).
43793
+ * literal `env` table (not the `env_vars` passthrough list), so the only field
43794
+ * rename is `disabled` (rulesync) ↔ `enabled = false` (grok). `disabledTools`
43795
+ * never lands in the server table: it is lifted into the top-level
43796
+ * `[disabled_mcp_tools]` table by `buildDisabledMcpTools`.
43280
43797
  */
43281
43798
  function convertToGrokFormat(mcpServers) {
43282
43799
  const result = {};
@@ -43285,7 +43802,7 @@ function convertToGrokFormat(mcpServers) {
43285
43802
  if (!isRecord$1(config)) continue;
43286
43803
  const converted = {};
43287
43804
  for (const [key, value] of Object.entries(config)) {
43288
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
43805
+ if (PROTOTYPE_POLLUTION_KEYS.has(key) || key === "disabledTools") continue;
43289
43806
  if (key === "disabled") {
43290
43807
  if (value === true) converted["enabled"] = false;
43291
43808
  } else converted[key] = value;
@@ -43309,6 +43826,38 @@ function convertFromGrokFormat(grokMcp) {
43309
43826
  }
43310
43827
  return result;
43311
43828
  }
43829
+ function toToolNameList(value) {
43830
+ if (!Array.isArray(value)) return [];
43831
+ return value.filter((tool) => typeof tool === "string");
43832
+ }
43833
+ /**
43834
+ * Recompute the global `[disabled_mcp_tools]` table: every server Rulesync
43835
+ * declares gets its `disabledTools` (or loses a stale entry when it has none —
43836
+ * Grok itself removes an entry whose list empties), while entries for other
43837
+ * servers — ones toggled in Grok's `/mcps` UI, plugin or compat servers, and
43838
+ * the `__managed_gateway_connectors` sentinel — are carried over untouched.
43839
+ * Returns `undefined` when nothing is left, so the key is retracted.
43840
+ */
43841
+ function buildDisabledMcpTools({ mcpServers, existingContent, filePath }) {
43842
+ const existing = parseSharedConfig({
43843
+ format: "toml",
43844
+ fileContent: existingContent,
43845
+ filePath
43846
+ })[GROK_DISABLED_MCP_TOOLS_KEY];
43847
+ const result = isRecord$1(existing) ? { ...existing } : {};
43848
+ for (const [name, config] of Object.entries(mcpServers)) {
43849
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
43850
+ const tools = toToolNameList(config.disabledTools);
43851
+ if (tools.length > 0) result[name] = tools;
43852
+ else delete result[name];
43853
+ }
43854
+ return Object.keys(result).length > 0 ? result : void 0;
43855
+ }
43856
+ function warnProjectDisabledTools(mcpServers, logger) {
43857
+ const serverNames = Object.entries(mcpServers).filter(([, config]) => isRecord$1(config) && config.disabledTools !== void 0).map(([name]) => name);
43858
+ if (serverNames.length === 0) return;
43859
+ 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.`);
43860
+ }
43312
43861
  var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43313
43862
  toml;
43314
43863
  constructor({ ...rest }) {
@@ -43345,16 +43894,19 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43345
43894
  relativeDirPath: paths.relativeDirPath,
43346
43895
  relativeFilePath: paths.relativeFilePath,
43347
43896
  fileContent,
43348
- validate
43897
+ validate,
43898
+ global
43349
43899
  });
43350
43900
  }
43351
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
43901
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
43352
43902
  const paths = this.getSettablePaths({ global });
43353
43903
  const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
43354
43904
  const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
43355
- const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
43905
+ const strippedMcpServers = rulesyncMcp.getMcpServers();
43906
+ const converted = convertToGrokFormat(strippedMcpServers);
43356
43907
  const filteredMcpServers = this.removeEmptyEntries(converted);
43357
- for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
43908
+ if (!global) warnProjectDisabledTools(strippedMcpServers, logger);
43909
+ for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(logger, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
43358
43910
  return new GrokcliMcp({
43359
43911
  outputRoot,
43360
43912
  relativeDirPath: paths.relativeDirPath,
@@ -43363,14 +43915,31 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
43363
43915
  fileKey: sharedConfigFileKey(paths),
43364
43916
  feature: "mcp",
43365
43917
  existingContent: configTomlFileContent,
43366
- patch: { mcp_servers: filteredMcpServers },
43918
+ patch: {
43919
+ mcp_servers: Object.keys(filteredMcpServers).length > 0 ? filteredMcpServers : void 0,
43920
+ ...global && { [GROK_DISABLED_MCP_TOOLS_KEY]: buildDisabledMcpTools({
43921
+ mcpServers: strippedMcpServers,
43922
+ existingContent: configTomlFileContent,
43923
+ filePath: configTomlFilePath
43924
+ }) }
43925
+ },
43367
43926
  filePath: configTomlFilePath
43368
43927
  }),
43369
- validate
43928
+ validate,
43929
+ global
43370
43930
  });
43371
43931
  }
43372
43932
  toRulesyncMcp() {
43373
43933
  const converted = convertFromGrokFormat(this.toml.mcp_servers ?? {});
43934
+ const disabledMcpTools = this.toml[GROK_DISABLED_MCP_TOOLS_KEY];
43935
+ if (this.global && isRecord$1(disabledMcpTools)) for (const [name, config] of Object.entries(converted)) {
43936
+ if (!Object.hasOwn(disabledMcpTools, name)) continue;
43937
+ const tools = toToolNameList(disabledMcpTools[name]);
43938
+ if (tools.length > 0) converted[name] = {
43939
+ ...config,
43940
+ disabledTools: tools
43941
+ };
43942
+ }
43374
43943
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
43375
43944
  }
43376
43945
  validate() {
@@ -46434,7 +47003,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
46434
47003
  fileKey: sharedConfigFileKey(paths),
46435
47004
  feature: "mcp",
46436
47005
  existingContent,
46437
- patch: { [POOL_MCP_SERVERS_KEY]: converted },
47006
+ patch: { [POOL_MCP_SERVERS_KEY]: undefinedWhenEmpty(converted) },
46438
47007
  filePath
46439
47008
  }),
46440
47009
  validate,
@@ -46564,7 +47133,7 @@ var QoderMcp = class QoderMcp extends ToolMcp {
46564
47133
  fileKey: sharedConfigFileKey(paths),
46565
47134
  feature: "mcp",
46566
47135
  existingContent,
46567
- patch: { mcpServers },
47136
+ patch: { mcpServers: undefinedWhenEmpty(mcpServers) },
46568
47137
  filePath
46569
47138
  }) : JSON.stringify({
46570
47139
  ...parseQoderMcpConfig({
@@ -46943,18 +47512,28 @@ function warnAboutRetiredFields({ name, serverRecord, logger }) {
46943
47512
  * `" Serial "` counts too and is written as authored.
46944
47513
  */
46945
47514
  const REASONIX_CONCURRENCY_VALUES = /* @__PURE__ */ new Set(["serial", "parallel"]);
46946
- /**
46947
- * The type Reasonix decodes each fixed-type scalar passthrough field into
46948
- * (`PluginEntry` in `internal/config/plugin_entry.go`, and
46949
- * `internal/contract/config/plugin_entry.go` on the CLI v2 line).
46950
- */
46951
47515
  const REASONIX_TYPED_PLUGIN_FIELDS = {
47516
+ startup_timeout_seconds: "integer",
47517
+ call_timeout_seconds: "integer",
47518
+ tool_timeout_seconds: "integer table",
46952
47519
  concurrency: "string",
46953
47520
  auto_start: "boolean",
46954
47521
  load: "string",
46955
47522
  oauth_allow_missing_pkce_metadata: "boolean"
46956
47523
  };
46957
47524
  /**
47525
+ * Whether `value` is written as a TOML integer. smol-toml writes a JS number
47526
+ * outside the safe-integer range, or with a fraction, as a TOML float.
47527
+ */
47528
+ function isTomlInteger(value) {
47529
+ return typeof value === "number" && Number.isSafeInteger(value);
47530
+ }
47531
+ function hasExpectedType(value, expected) {
47532
+ if (expected === "integer") return isTomlInteger(value);
47533
+ if (expected === "integer table") return isRecord$1(value) && Object.values(value).every(isTomlInteger);
47534
+ return typeof value === expected;
47535
+ }
47536
+ /**
46958
47537
  * Why a passthrough value cannot be written, or `undefined` when it can (and
46959
47538
  * for every field without a fixed type). A wrong type is the case that matters:
46960
47539
  * a TOML type mismatch makes Reasonix refuse the whole config file. An unknown
@@ -46966,7 +47545,7 @@ const REASONIX_TYPED_PLUGIN_FIELDS = {
46966
47545
  */
46967
47546
  function invalidPassthroughFieldReason(field, value) {
46968
47547
  const expected = REASONIX_TYPED_PLUGIN_FIELDS[field];
46969
- if (expected !== void 0 && typeof value !== expected) return `Reasonix expects a ${expected} and fails to load a config file holding any other type.`;
47548
+ if (expected !== void 0 && !hasExpectedType(value, expected)) return `Reasonix expects ${expected === "integer" ? "an" : "a"} ${expected === "integer table" ? "table of integers" : expected} and fails to load a config file holding any other type.`;
46970
47549
  if (field === "concurrency" && typeof value === "string" && !REASONIX_CONCURRENCY_VALUES.has(value.trim().toLowerCase())) return `Reasonix accepts only "serial" or "parallel" and ignores ${JSON.stringify(value)}, falling back to the server-name default.`;
46971
47550
  }
46972
47551
  /**
@@ -47988,7 +48567,7 @@ var TabnineMcp = class TabnineMcp extends ToolMcp {
47988
48567
  fileKey: sharedConfigFileKey(paths),
47989
48568
  feature: "mcp",
47990
48569
  existingContent,
47991
- patch: { mcpServers: convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger) },
48570
+ patch: { mcpServers: undefinedWhenEmpty(convertToTabnineFormat(rulesyncMcp.getMcpServers(), logger)) },
47992
48571
  filePath
47993
48572
  }),
47994
48573
  validate,
@@ -49309,6 +49888,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49309
49888
  const paths = this.getSettablePaths({ global });
49310
49889
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
49311
49890
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
49891
+ const contextServers = convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger);
49312
49892
  return new ZedMcp({
49313
49893
  outputRoot,
49314
49894
  relativeDirPath: paths.relativeDirPath,
@@ -49317,7 +49897,7 @@ var ZedMcp = class ZedMcp extends ToolMcp {
49317
49897
  fileKey: sharedConfigFileKey(paths),
49318
49898
  feature: "mcp",
49319
49899
  existingContent,
49320
- patch: { context_servers: convertServersToZedFormat(rulesyncMcp.getMcpServers(), logger) },
49900
+ patch: { context_servers: Object.keys(contextServers).length > 0 ? contextServers : void 0 },
49321
49901
  filePath
49322
49902
  }),
49323
49903
  validate
@@ -49450,7 +50030,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49450
50030
  }
49451
50031
  }],
49452
50032
  ["claudecode-plugin", {
49453
- class: ClaudecodeMcp,
50033
+ class: ClaudecodePluginMcp,
49454
50034
  meta: {
49455
50035
  supportsProject: true,
49456
50036
  supportsGlobal: false,
@@ -49626,7 +50206,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
49626
50206
  supportsProject: true,
49627
50207
  supportsGlobal: true,
49628
50208
  supportsEnabledTools: false,
49629
- supportsDisabledTools: false
50209
+ supportsDisabledTools: true
49630
50210
  }
49631
50211
  }],
49632
50212
  ["hermesagent", {
@@ -50792,6 +51372,8 @@ function parsedGlobsIntersect(left, right, budget) {
50792
51372
  * @see https://antigravity.google/docs/permissions?tab=cli
50793
51373
  */
50794
51374
  const REGEX_PREFIX = "regex:";
51375
+ const TRAILING_ANCHOR = /(?<!\\)\$$/;
51376
+ const PLAIN_TARGET_GLOB_CHARACTERS = /[*?[{\\]/;
50795
51377
  const REGEX_METACHARACTERS$1 = /[\\^$.|?*+()[\]{}]/;
50796
51378
  const WHITESPACE_CODES = [
50797
51379
  9,
@@ -50932,6 +51514,7 @@ function replacePosixClasses(source) {
50932
51514
  function isInvalidRegex(source) {
50933
51515
  const unquoted = source.replace(RE2_QUOTED, (match) => match === "\\\\" ? match : "q");
50934
51516
  if (LOOKAROUND.test(unquoted) || hasRefusedEscape(unquoted)) return true;
51517
+ if (hasClassEscapeRangeEnd(unquoted)) return true;
50935
51518
  const withoutPosixClasses = replacePosixClasses(unquoted);
50936
51519
  if (withoutPosixClasses === void 0 || hasRefusedRepeat(withoutPosixClasses)) return true;
50937
51520
  const asJavaScript = withoutPosixClasses.replace(/\(\?P</g, "(?<").replace(/\(\?[imsU]*-?[imsU]*\)/g, "").replace(/\(\?[imsU]*-?[imsU]*:/g, "(?:").replace(/\\A/g, "^").replace(/\\z/g, "$");
@@ -50956,6 +51539,74 @@ function hasRefusedEscape(source) {
50956
51539
  }
50957
51540
  return false;
50958
51541
  }
51542
+ const CLASS_ESCAPE_LETTERS = /[dDsSwWpP]/;
51543
+ /** The length of the escape at `index`, braces (`\x{41}`, `\p{Greek}`) included. */
51544
+ function escapeLength(source, index) {
51545
+ const escaped = source[index + 1] ?? "";
51546
+ if (/[xpP]/.test(escaped) && source[index + 2] === "{") {
51547
+ const close = source.indexOf("}", index + 3);
51548
+ return close === -1 ? source.length - index : close - index + 1;
51549
+ }
51550
+ if (escaped === "p" || escaped === "P") return 3;
51551
+ return escaped === "x" ? 4 : 2;
51552
+ }
51553
+ /** Step past `^` and a leading `]` (a member) after the `[` that ends at `index`. */
51554
+ function openClass(source, start) {
51555
+ const index = source[start] === "^" ? start + 1 : start;
51556
+ return source[index] === "]" ? {
51557
+ index: index + 1,
51558
+ afterCharacter: true
51559
+ } : {
51560
+ index,
51561
+ afterCharacter: false
51562
+ };
51563
+ }
51564
+ /**
51565
+ * Whether a bracket range ends in a class escape (`[a-\d]`, `[--\s]`). Go
51566
+ * reads the end of a range as one character and refuses such a regex, while
51567
+ * JavaScript without the `u` flag reads it as three members. A class escape
51568
+ * that starts the range (`[\d-z]`) is a member followed by `-`, which both
51569
+ * accept. The `u` flag cannot be used instead: it also refuses RE2 spellings
51570
+ * such as `\x{41}` and `\-` outside a class.
51571
+ */
51572
+ function hasClassEscapeRangeEnd(source) {
51573
+ let inClass = false;
51574
+ let afterCharacter = false;
51575
+ let index = 0;
51576
+ while (index < source.length) {
51577
+ const character = source[index] ?? "";
51578
+ if (character === "\\") {
51579
+ afterCharacter = inClass && !CLASS_ESCAPE_LETTERS.test(source[index + 1] ?? "");
51580
+ index += escapeLength(source, index);
51581
+ continue;
51582
+ }
51583
+ if (!inClass) {
51584
+ inClass = character === "[";
51585
+ ({index, afterCharacter} = inClass ? openClass(source, index + 1) : {
51586
+ index: index + 1,
51587
+ afterCharacter: false
51588
+ });
51589
+ continue;
51590
+ }
51591
+ const posixEnd = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
51592
+ if (character === "]" || posixEnd !== -1) {
51593
+ inClass = character !== "]";
51594
+ afterCharacter = false;
51595
+ index = posixEnd === -1 ? index + 1 : posixEnd + 2;
51596
+ continue;
51597
+ }
51598
+ if (character === "-" && afterCharacter && source[index + 1] !== "]") {
51599
+ const end = index + 1;
51600
+ if (source[end] === "\\" && CLASS_ESCAPE_LETTERS.test(source[end + 1] ?? "")) return true;
51601
+ afterCharacter = false;
51602
+ index = end + (source[end] === "\\" ? escapeLength(source, end) : 1);
51603
+ continue;
51604
+ }
51605
+ afterCharacter = true;
51606
+ index += 1;
51607
+ }
51608
+ return false;
51609
+ }
50959
51610
  /**
50960
51611
  * Whether a repeat count is over Go's limit. Braces inside a bracket class
50961
51612
  * (`[a{1001}]`) and in `\x{2003}` or `\p{Greek}` are not repeats, so those
@@ -51103,7 +51754,7 @@ function toAntigravityCommandLiteralPrefix(pattern) {
51103
51754
  * the rule a regex.
51104
51755
  */
51105
51756
  function regexWordToGlob(word) {
51106
- const body = word.replace(/^\^/, "").replace(/(?<!\\)\$$/, "");
51757
+ const body = word.replace(/^\^/, "").replace(TRAILING_ANCHOR, "");
51107
51758
  const characters = Array.from(body);
51108
51759
  if (characters.length === 0) return;
51109
51760
  let glob = "";
@@ -51144,12 +51795,61 @@ function regexToGlob(source) {
51144
51795
  function fromAntigravityCommandTarget(target) {
51145
51796
  if (target.startsWith(REGEX_PREFIX)) return regexToGlob(target.slice(6)) ?? target;
51146
51797
  if (target === "*") return target;
51147
- if (/[*?[{\\]/.test(target)) {
51798
+ if (PLAIN_TARGET_GLOB_CHARACTERS.test(target)) {
51148
51799
  const words = splitWords(target);
51149
51800
  return `${REGEX_PREFIX}${words.map((word) => `^${[...word].map(escapeRegexCharacter).join("")}$`).join(" ")}`;
51150
51801
  }
51151
51802
  return `${target} *`;
51152
51803
  }
51804
+ /**
51805
+ * Whether a regex word means the same anchored or not: each end is either
51806
+ * anchored (`^`, an unescaped `$`) or open-ended (`.*`).
51807
+ */
51808
+ function isAnchoredWord(word) {
51809
+ const startAnchored = word.startsWith("^") || word.startsWith(".*");
51810
+ const endAnchored = TRAILING_ANCHOR.test(word) || word.endsWith(".*");
51811
+ return startAnchored && endAnchored;
51812
+ }
51813
+ /**
51814
+ * Explain what importing an Antigravity `command` target may lose, or return
51815
+ * `undefined` when the import is faithful.
51816
+ *
51817
+ * - A `regex:` target with a word that is not anchored is imported as a glob
51818
+ * that assumes anchoring. If Antigravity matches such a word unanchored,
51819
+ * the imported rule is narrower (`regex:rm` also matches `rmdir`).
51820
+ * - A plain target holding characters that are special in a glob is most
51821
+ * likely the verbatim output of an older rulesync (`command(rm -rf *)`).
51822
+ * Antigravity matches it literally, so it imports as a regex that matches a
51823
+ * literal `*` word and is inert in the other tools it is exported to.
51824
+ */
51825
+ function describeAntigravityCommandImportLoss(target) {
51826
+ if (target.startsWith(REGEX_PREFIX)) {
51827
+ const source = target.slice(6);
51828
+ if (splitWords(source).some((word) => !isAnchoredWord(word)) && regexToGlob(source) !== void 0) return {
51829
+ 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",
51830
+ denyConsequence: "The imported deny may block less than it does in Antigravity."
51831
+ };
51832
+ return;
51833
+ }
51834
+ if (target !== "*" && PLAIN_TARGET_GLOB_CHARACTERS.test(target)) return {
51835
+ 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",
51836
+ denyConsequence: "This deny is NOT enforced by the other tools it is generated for."
51837
+ };
51838
+ }
51839
+ /**
51840
+ * Import one Antigravity `command` target as a canonical bash pattern, warning
51841
+ * when the result may differ from what Antigravity enforces. Shared by the
51842
+ * Antigravity CLI and IDE importers.
51843
+ */
51844
+ function importAntigravityCommandTarget({ target, entry, decision, logger, toolLabel }) {
51845
+ const pattern = fromAntigravityCommandTarget(target);
51846
+ const loss = describeAntigravityCommandImportLoss(target);
51847
+ if (loss !== void 0) {
51848
+ const consequence = decision === "deny" ? ` ${loss.denyConsequence}` : "";
51849
+ logger.warn(`${toolLabel} permissions: importing ${entry} as "${pattern}": ${loss.reason}.${consequence}`);
51850
+ }
51851
+ return pattern;
51852
+ }
51153
51853
  //#endregion
51154
51854
  //#region src/features/permissions/antigravity-targets.ts
51155
51855
  /**
@@ -51180,6 +51880,7 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
51180
51880
  "**/*"
51181
51881
  ]);
51182
51882
  const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
51883
+ const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
51183
51884
  const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
51184
51885
  const DENY_HINTS = {
51185
51886
  read_file: "deny a directory or file path instead",
@@ -51262,6 +51963,10 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
51262
51963
  return;
51263
51964
  }
51264
51965
  if (result.note !== void 0 && decision !== "deny") logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}.`);
51966
+ if (PATH_ACTIONS.has(action) && HOME_OR_VARIABLE_PATH.test(result.target)) {
51967
+ const consequence = decision === "deny" ? "this deny may block nothing" : `${toolLabel} may fall back to its default`;
51968
+ 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.`);
51969
+ }
51265
51970
  return result.target === "*" ? action : `${action}(${result.target})`;
51266
51971
  }
51267
51972
  //#endregion
@@ -51828,9 +52533,9 @@ function convertRulesyncToAntigravityCliPermissions({ config, logger }) {
51828
52533
  }
51829
52534
  }
51830
52535
  return {
51831
- allow,
51832
- ask,
51833
- deny
52536
+ allow: [...new Set(allow)],
52537
+ ask: [...new Set(ask)],
52538
+ deny: [...new Set(deny)]
51834
52539
  };
51835
52540
  }
51836
52541
  /**
@@ -51842,7 +52547,13 @@ function convertAntigravityCliToRulesyncPermissions(params) {
51842
52547
  for (const entry of entries) {
51843
52548
  const { toolName, pattern: target } = parsePermissionEntry$1(entry);
51844
52549
  const canonical = toCanonicalToolName$9(toolName);
51845
- const pattern = toolName === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52550
+ const pattern = toolName === "command" ? importAntigravityCommandTarget({
52551
+ target,
52552
+ entry,
52553
+ decision: action,
52554
+ logger: moduleLogger$4,
52555
+ toolLabel: "Antigravity CLI"
52556
+ }) : target;
51846
52557
  if (!permission[canonical]) permission[canonical] = {};
51847
52558
  permission[canonical][pattern] = action;
51848
52559
  }
@@ -52046,9 +52757,9 @@ function convertRulesyncToAntigravityIdePermissions({ config, logger }) {
52046
52757
  }
52047
52758
  }
52048
52759
  return {
52049
- allow,
52050
- ask,
52051
- deny
52760
+ allow: [...new Set(allow)],
52761
+ ask: [...new Set(ask)],
52762
+ deny: [...new Set(deny)]
52052
52763
  };
52053
52764
  }
52054
52765
  /**
@@ -52060,7 +52771,13 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
52060
52771
  for (const entry of entries) {
52061
52772
  const { action: ideAction, pattern: target } = parsePermissionEntry(entry);
52062
52773
  const canonical = toCanonicalCategory$2(ideAction);
52063
- const pattern = ideAction === "command" && target !== "*" ? fromAntigravityCommandTarget(target) : target;
52774
+ const pattern = ideAction === "command" ? importAntigravityCommandTarget({
52775
+ target,
52776
+ entry,
52777
+ decision: action,
52778
+ logger: fallbackLogger,
52779
+ toolLabel: "Antigravity IDE"
52780
+ }) : target;
52064
52781
  if (!permission[canonical]) permission[canonical] = {};
52065
52782
  permission[canonical][pattern] = action;
52066
52783
  }
@@ -54042,7 +54759,6 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54042
54759
  managedSourcesBehavior: "managed settings",
54043
54760
  modelPricing: "managed settings",
54044
54761
  parentSettingsBehavior: "managed settings",
54045
- permissionExplainerEnabled: "~/.claude.json",
54046
54762
  pluginSuggestionMarketplaces: "managed settings",
54047
54763
  pluginTrustMessage: "managed settings",
54048
54764
  prStatusFooterEnabled: "~/.claude.json",
@@ -54051,10 +54767,24 @@ const CLAUDECODE_UNHONORED_KEY_SOURCES = {
54051
54767
  sshHostAllowlist: "managed settings",
54052
54768
  strictKnownMarketplaces: "managed settings",
54053
54769
  strictPluginOnlyCustomization: "managed settings",
54054
- teammateDefaultModel: "~/.claude.json",
54055
54770
  wslInheritsWindowsSettings: "managed settings"
54056
54771
  };
54057
54772
  /**
54773
+ * Top-level settings keys Claude Code has removed, with the version that
54774
+ * removed each. Current versions ignore them wherever they are set, so they
54775
+ * are dropped in **both** scopes, and the warning names the version so the
54776
+ * author knows to delete the key rather than move it to another file.
54777
+ *
54778
+ * Derived from the "Removed in" notes of the settings reference.
54779
+ *
54780
+ * @see https://code.claude.com/docs/en/settings-reference
54781
+ */
54782
+ const CLAUDECODE_REMOVED_KEYS = {
54783
+ permissionExplainerEnabled: "v2.1.257",
54784
+ taskOutputMaxChars: "v2.1.277",
54785
+ teammateDefaultModel: "v2.1.234"
54786
+ };
54787
+ /**
54058
54788
  * Top-level settings keys whose value Claude Code **executes**. The generic
54059
54789
  * passthrough refuses them outright rather than warning: `.rulesync/*` files
54060
54790
  * are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
@@ -54334,6 +55064,15 @@ const CLAUDECODE_SETTINGS_KEY_ALIASES = {
54334
55064
  allowedMarketplaces: "strictKnownMarketplaces"
54335
55065
  };
54336
55066
  /**
55067
+ * The warning for a key that neither scope can honor — one Claude Code has
55068
+ * removed, or one read only from a file rulesync does not generate — or
55069
+ * `undefined` when the key may be written in at least one scope.
55070
+ */
55071
+ function neverHonoredKeyWarning({ canonicalKey, shown, relativeFilePath }) {
55072
+ 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.`;
55073
+ 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.`;
55074
+ }
55075
+ /**
54337
55076
  * Copy of the authored top-level passthrough with the keys the target file
54338
55077
  * cannot honor removed, warning once per dropped key. Like
54339
55078
  * `stripSandboxPaths`, only the override copy is filtered — a value
@@ -54350,8 +55089,13 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
54350
55089
  logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
54351
55090
  continue;
54352
55091
  }
54353
- if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
54354
- logger?.warn(`Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
55092
+ const neverHonored = neverHonoredKeyWarning({
55093
+ canonicalKey,
55094
+ shown,
55095
+ relativeFilePath
55096
+ });
55097
+ if (neverHonored !== void 0) {
55098
+ logger?.warn(neverHonored);
54355
55099
  continue;
54356
55100
  }
54357
55101
  if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
@@ -62042,8 +62786,10 @@ function mergeKimiCodeToolsSection({ existingContent, patch }) {
62042
62786
  * table. The gateway replaces the owned `permission` key wholesale and rulesync
62043
62787
  * only ever authors `rules`, so a hand-written sibling such as
62044
62788
  * `dangerous_command_guard = false` (Kimi Code 0.40.0) would otherwise be
62045
- * deleted on every generate. Rulesync does not model these keys; they pass
62046
- * through verbatim and `rules` always comes from the patch.
62789
+ * deleted on every generate. Unmodeled keys pass through verbatim; `rules`
62790
+ * always comes from the patch, and so does `dangerous_command_guard` when the
62791
+ * `kimi-code.dangerousCommandGuard` override sets it. Leaving the override out
62792
+ * keeps whatever the file already has.
62047
62793
  *
62048
62794
  * @see https://moonshotai.github.io/kimi-code/en/configuration/config-files.html#permission
62049
62795
  */
@@ -62286,14 +63032,18 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62286
63032
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions, logger }) {
62287
63033
  const config = rulesyncPermissions.getJson();
62288
63034
  const defaultPermissionMode = config["kimi-code"]?.defaultPermissionMode;
63035
+ const dangerousCommandGuard = config["kimi-code"]?.dangerousCommandGuard;
62289
63036
  const tools = buildKimiCodeToolsSection(config["kimi-code"]?.tools);
62290
63037
  const document = {
62291
63038
  ...defaultPermissionMode && { default_permission_mode: defaultPermissionMode },
62292
63039
  ...tools && { tools },
62293
- permission: { rules: canonicalToKimiCodeRules({
62294
- config,
62295
- logger
62296
- }) }
63040
+ permission: {
63041
+ ...typeof dangerousCommandGuard === "boolean" && { dangerous_command_guard: dangerousCommandGuard },
63042
+ rules: canonicalToKimiCodeRules({
63043
+ config,
63044
+ logger
63045
+ })
63046
+ }
62297
63047
  };
62298
63048
  let fileContent;
62299
63049
  try {
@@ -62315,11 +63065,14 @@ var KimiCodePermissions = class KimiCodePermissions extends ToolPermissions {
62315
63065
  format: "toml",
62316
63066
  fileContent: this.getFileContent()
62317
63067
  });
62318
- const { permission, nativeRules } = preserveKimiCodeRules((isRecord$1(config.permission) ? config.permission : {}).rules);
63068
+ const permissionConfig = isRecord$1(config.permission) ? config.permission : {};
63069
+ const { permission, nativeRules } = preserveKimiCodeRules(permissionConfig.rules);
62319
63070
  const defaultPermissionMode = config.default_permission_mode;
63071
+ const dangerousCommandGuard = permissionConfig.dangerous_command_guard;
62320
63072
  const tools = buildKimiCodeToolsSection(config.tools);
62321
63073
  const toolOverride = {
62322
63074
  ...defaultPermissionMode === "manual" || defaultPermissionMode === "yolo" || defaultPermissionMode === "auto" ? { defaultPermissionMode } : {},
63075
+ ...typeof dangerousCommandGuard === "boolean" && { dangerousCommandGuard },
62323
63076
  ...nativeRules.length > 0 && { rules: nativeRules },
62324
63077
  ...tools && { tools }
62325
63078
  };
@@ -63201,6 +63954,7 @@ var MimocodePermissions = class extends OpencodePermissions {
63201
63954
  //#endregion
63202
63955
  //#region src/features/permissions/omp-permissions.ts
63203
63956
  const CATCH_ALL_PATTERN$4 = "*";
63957
+ const APPROVAL_MODE_KEY = "approvalMode";
63204
63958
  const ACTION_TO_OMP_APPROVAL = {
63205
63959
  allow: "allow",
63206
63960
  ask: "prompt",
@@ -63256,10 +64010,14 @@ const MANAGED_OMP_TOOLS = /* @__PURE__ */ new Set([
63256
64010
  * commands, and its deny / ask also restrict every other tool's policy (a
63257
64011
  * catch-all one at its own strictness, a pattern-specific one as `prompt`).
63258
64012
  *
64013
+ * The `omp.approvalMode` override writes `tools.approvalMode` (`always-ask`,
64014
+ * `write` or `yolo`); without it the key is left as it is in the file, and on
64015
+ * import a recognized value is read back into the override.
64016
+ *
63259
64017
  * `config.yml` carries every other oh-my-pi setting, so only `tools.approval`
63260
- * (the keys rulesync manages) and `bash.patterns` are rewritten; sibling keys
63261
- * such as `tools.approvalMode` and `bash.enabled` are kept, and the file is
63262
- * never deleted.
64018
+ * (the keys rulesync manages), `bash.patterns` and an authored
64019
+ * `tools.approvalMode` are rewritten; sibling keys such as `bash.enabled` are
64020
+ * kept, and the file is never deleted.
63263
64021
  *
63264
64022
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/settings.md
63265
64023
  * @see https://github.com/can1357/oh-my-pi/blob/main/docs/approval-mode.md
@@ -63302,16 +64060,19 @@ var OmpPermissions = class OmpPermissions extends ToolPermissions {
63302
64060
  fileContent: existingContent,
63303
64061
  filePath
63304
64062
  });
64063
+ const config = rulesyncPermissions.getJson();
63305
64064
  const { approval, patterns, floor } = convertRulesyncToOmp({
63306
- config: rulesyncPermissions.getJson(),
64065
+ config,
63307
64066
  logger
63308
64067
  });
64068
+ const approvalMode = config.omp?.approvalMode;
63309
64069
  const existingTools = isRecord$1(existing.tools) ? existing.tools : {};
63310
64070
  const nextApproval = {
63311
64071
  ...isRecord$1(existingTools.approval) ? Object.fromEntries(Object.entries(existingTools.approval).filter(([tool]) => !MANAGED_OMP_TOOLS.has(tool) && !Object.hasOwn(approval, tool)).map(([tool, value]) => [tool, raisedApproval(value, floor)])) : {},
63312
64072
  ...approval
63313
64073
  };
63314
64074
  const toolsSiblings = withoutKey(existingTools, "approval");
64075
+ if (approvalMode !== void 0) toolsSiblings[APPROVAL_MODE_KEY] = approvalMode;
63315
64076
  const nextTools = Object.keys(nextApproval).length > 0 ? {
63316
64077
  ...toolsSiblings,
63317
64078
  approval: nextApproval
@@ -63567,7 +64328,14 @@ function convertOmpToRulesync(config) {
63567
64328
  permission[category] = { [CATCH_ALL_PATTERN$4]: stricter(permission[category]?.[CATCH_ALL_PATTERN$4], action) };
63568
64329
  }
63569
64330
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
63570
- return { permission };
64331
+ const approvalMode = tools[APPROVAL_MODE_KEY];
64332
+ return {
64333
+ permission,
64334
+ ...isOmpApprovalMode(approvalMode) && { omp: { approvalMode } }
64335
+ };
64336
+ }
64337
+ function isOmpApprovalMode(value) {
64338
+ return OMP_APPROVAL_MODES.some((mode) => mode === value);
63571
64339
  }
63572
64340
  //#endregion
63573
64341
  //#region src/features/permissions/pi-permissions.ts
@@ -64607,6 +65375,13 @@ const QWEN_OVERRIDE_SECURITY_KEYS = [
64607
65375
  "allowPrivateNetworkHooks",
64608
65376
  "allowedInsecureVoiceBaseUrls"
64609
65377
  ];
65378
+ const QWEN_OVERRIDE_SKILLS_KEYS = [
65379
+ "disabled",
65380
+ "defaultDisabled",
65381
+ "enabled",
65382
+ "disabledLevels"
65383
+ ];
65384
+ const QWEN_OVERRIDE_GENERAL_KEYS = ["outputStyle"];
64610
65385
  /**
64611
65386
  * What each rule means for the two directions, so a key's behavior is declared
64612
65387
  * once beside its rule rather than spelled out at every branch.
@@ -64663,77 +65438,125 @@ const QWEN_SCOPE_RULES = {
64663
65438
  globalNote: "This is not a key rulesync models, so it cannot say what Qwen Code does with it — and some settings in these groups are potent (`tools.discoveryCommand` and `tools.callCommand` are spawned as commands when Qwen Code starts). Check what it now says for every project on this machine."
64664
65439
  }
64665
65440
  };
64666
- const QWEN_OVERRIDE_GROUPS = [{
64667
- groupName: "tools",
64668
- overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
64669
- scopedKeys: {
64670
- workflowsEnabled: { rule: "workspace-stripped" },
64671
- executionSandbox: {
64672
- rule: "workspace-stripped",
64673
- grants: () => true,
64674
- globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot choose its own sandbox; in the global scope it sets the filesystem and network policy every tool command runs under for every project on this machine (a System value still replaces it, and Qwen Code needs a restart to pick it up)."
64675
- },
64676
- workflowNameOnly: {
64677
- rule: "global-machine-wide",
64678
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code lets a workspace only turn this key on: \`true\` restricts the model to named workflows in this repository, while any other value cannot lift a restriction set in user or system settings.`,
64679
- globalNote: "Qwen Code honors this key in the global scope for every project on this machine, and a repository can only add the restriction, never lift it, so this decides whether the model may run inline or path-based workflow scripts anywhere a workspace does not turn the restriction on itself."
64680
- },
64681
- approvalMode: {
64682
- rule: "global-machine-wide",
64683
- projectNote: autonomyProjectNote
64684
- },
64685
- autoAccept: {
64686
- rule: "global-machine-wide",
64687
- projectNote: autonomyProjectNote
64688
- },
64689
- sandbox: {
64690
- rule: "global-machine-wide",
64691
- projectNote: autonomyProjectNote
64692
- },
64693
- sandboxImage: {
64694
- rule: "global-machine-wide",
64695
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so a sandboxed run in this repository executes inside the named image — check that it is one you trust to run your code.`,
64696
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope every sandboxed run on this machine executes inside the named image — check that it is one you trust to run your code."
64697
- },
64698
- disabled: {
64699
- rule: "global-machine-wide",
64700
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so a name that is gone is a tool the model can call again in this repository unless another scope still disables it.`,
64701
- globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope this decides which tools stay out of the registry for every project on this machine — dropping a name hands that tool back to the model unless another scope still disables it."
64702
- },
64703
- visible: {
64704
- rule: "global-machine-wide",
64705
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides which deferred tools are visible at startup in this repository.`,
64706
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides which deferred tools are visible at startup for every project on this machine."
64707
- },
64708
- eager: {
64709
- rule: "global-machine-wide",
64710
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so in this repository every non-exempt eager-by-default tool it does not name is demoted to deferred — still callable, but only after \`tool_search\` loads it, and out of reach for the session if \`tool_search\` is not registered. A name added back is a tool whose schema is in the initial request again.`,
64711
- globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope it decides which eager-by-default tools keep their schemas in the initial request for every project on this machine — the ones it does not name are demoted to deferred, reachable only through `tool_search`, and out of reach entirely for a session where `tool_search` is not registered, while a name it adds back is a tool declared up front again."
64712
- },
64713
- listDirectory: {
64714
- rule: "global-machine-wide",
64715
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
64716
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
64717
- },
64718
- todoWrite: {
65441
+ const QWEN_SCOPED_TOOLS_KEYS = {
65442
+ workflowsEnabled: { rule: "workspace-stripped" },
65443
+ executionSandbox: {
65444
+ rule: "workspace-stripped",
65445
+ grants: () => true,
65446
+ 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)."
65447
+ },
65448
+ workflowNameOnly: {
65449
+ rule: "global-machine-wide",
65450
+ 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.`,
65451
+ 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."
65452
+ },
65453
+ approvalMode: {
65454
+ rule: "global-machine-wide",
65455
+ projectNote: autonomyProjectNote
65456
+ },
65457
+ autoAccept: {
65458
+ rule: "global-machine-wide",
65459
+ projectNote: autonomyProjectNote
65460
+ },
65461
+ sandbox: {
65462
+ rule: "global-machine-wide",
65463
+ projectNote: autonomyProjectNote
65464
+ },
65465
+ sandboxImage: {
65466
+ rule: "global-machine-wide",
65467
+ 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.`,
65468
+ 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."
65469
+ },
65470
+ disabled: {
65471
+ rule: "global-machine-wide",
65472
+ 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.`,
65473
+ 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."
65474
+ },
65475
+ visible: {
65476
+ rule: "global-machine-wide",
65477
+ 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.`,
65478
+ 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."
65479
+ },
65480
+ eager: {
65481
+ rule: "global-machine-wide",
65482
+ 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.`,
65483
+ 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."
65484
+ },
65485
+ listDirectory: {
65486
+ rule: "global-machine-wide",
65487
+ 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.`,
65488
+ 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."
65489
+ },
65490
+ todoWrite: {
65491
+ rule: "global-machine-wide",
65492
+ 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.`,
65493
+ 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."
65494
+ }
65495
+ };
65496
+ /**
65497
+ * The notes for a skills list that hides something. Qwen Code unions the list
65498
+ * across scopes, but the override replaces the one in the file it writes, so
65499
+ * the consequence worth naming is what dropping an entry hands back.
65500
+ */
65501
+ function skillsHidingListNotes(hides) {
65502
+ return {
65503
+ 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.`,
65504
+ 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.`
65505
+ };
65506
+ }
65507
+ const QWEN_SCOPED_SKILLS_KEYS = {
65508
+ disabled: {
65509
+ rule: "global-machine-wide",
65510
+ ...skillsHidingListNotes("skills are hidden")
65511
+ },
65512
+ defaultDisabled: {
65513
+ rule: "global-machine-wide",
65514
+ ...skillsHidingListNotes("skills start disabled until `skills.enabled` opts into them")
65515
+ },
65516
+ disabledLevels: {
65517
+ rule: "global-machine-wide",
65518
+ ...skillsHidingListNotes("skill discovery levels (`project`, `user`, `extension`, `bundled`) are skipped")
65519
+ },
65520
+ enabled: {
65521
+ rule: "global-machine-wide",
65522
+ 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.`,
65523
+ 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)."
65524
+ }
65525
+ };
65526
+ const QWEN_OVERRIDE_GROUPS = [
65527
+ {
65528
+ groupName: "tools",
65529
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
65530
+ scopedKeys: QWEN_SCOPED_TOOLS_KEYS
65531
+ },
65532
+ {
65533
+ groupName: "security",
65534
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
65535
+ scopedKeys: {
65536
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
65537
+ allowedInsecureVoiceBaseUrls: {
65538
+ rule: "workspace-stripped",
65539
+ grants: isNonEmptyArray
65540
+ },
65541
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
65542
+ folderTrust: { rule: "user-scope-trust-check" }
65543
+ }
65544
+ },
65545
+ {
65546
+ groupName: "skills",
65547
+ overrideKeys: QWEN_OVERRIDE_SKILLS_KEYS,
65548
+ scopedKeys: QWEN_SCOPED_SKILLS_KEYS
65549
+ },
65550
+ {
65551
+ groupName: "general",
65552
+ overrideKeys: QWEN_OVERRIDE_GENERAL_KEYS,
65553
+ scopedKeys: { outputStyle: {
64719
65554
  rule: "global-machine-wide",
64720
- projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`todo_write\` tool is registered in this repository.`,
64721
- globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `todo_write` tool is registered for every project on this machine."
64722
- }
64723
- }
64724
- }, {
64725
- groupName: "security",
64726
- overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
64727
- scopedKeys: {
64728
- allowPrivateNetworkHooks: { rule: "workspace-stripped" },
64729
- allowedInsecureVoiceBaseUrls: {
64730
- rule: "workspace-stripped",
64731
- grants: isNonEmptyArray
64732
- },
64733
- allowedHttpHookUrls: { rule: "workspace-non-overriding" },
64734
- folderTrust: { rule: "user-scope-trust-check" }
65555
+ 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.`,
65556
+ 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`)."
65557
+ } }
64735
65558
  }
64736
- }];
65559
+ ];
64737
65560
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
64738
65561
  const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
64739
65562
  rule: "global-machine-wide",
@@ -64864,7 +65687,7 @@ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGrou
64864
65687
  return scoped;
64865
65688
  }
64866
65689
  /**
64867
- * Build the `tools`/`security` patch groups contributed by the `qwencode`
65690
+ * Build the `tools`/`security`/`skills`/`general` patch groups contributed by the `qwencode`
64868
65691
  * override. Each group is shallow-merged over what `settings.json` already has,
64869
65692
  * after the scope gate has handled the keys Qwen Code honors in one scope only.
64870
65693
  */
@@ -65033,7 +65856,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
65033
65856
  });
65034
65857
  const overrideGroups = {
65035
65858
  tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
65036
- security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
65859
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS),
65860
+ skills: pickQwenOverrideKeys(settings.skills, QWEN_OVERRIDE_SKILLS_KEYS),
65861
+ general: pickQwenOverrideKeys(settings.general, QWEN_OVERRIDE_GENERAL_KEYS)
65037
65862
  };
65038
65863
  if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
65039
65864
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
@@ -65336,6 +66161,37 @@ function applyAllowDynamicBash({ permissions, authored }) {
65336
66161
  reason: "lets an Allow fallback, Auto included, run the nested and indirect Bash that otherwise needs a human or an exact-literal grant — command and process substitution, a dynamic command name, 'eval', 'source', 'sh -c' and their kind"
65337
66162
  }];
65338
66163
  }
66164
+ /** The writer fallbacks `[permissions] mode` accepts. */
66165
+ const REASONIX_PERMISSION_MODES = /* @__PURE__ */ new Set([
66166
+ "ask",
66167
+ "allow",
66168
+ "deny"
66169
+ ]);
66170
+ /**
66171
+ * Writes the override's `mode` into `[permissions]`. Reasonix honors the key
66172
+ * only in the user config and ignores a project value as user-scope only, so a
66173
+ * project-scope generate skips it with a warning and leaves whatever the file
66174
+ * had. A global `allow` is reported when it is newly written: it lets every
66175
+ * writer call that no rule matches run without a prompt.
66176
+ *
66177
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
66178
+ */
66179
+ function applyPermissionsMode({ permissions, authored, global, relativeFilePath, logger }) {
66180
+ if (authored === void 0) return [];
66181
+ if (!global) {
66182
+ 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.`);
66183
+ return [];
66184
+ }
66185
+ const previous = permissions.mode;
66186
+ permissions.mode = authored;
66187
+ const normalize = (value) => typeof value === "string" ? value.trim().toLowerCase() : void 0;
66188
+ const mode = normalize(authored);
66189
+ if (mode === "ask" || mode === "deny" || normalize(previous) === mode) return [];
66190
+ return [{
66191
+ label: "permissions.mode",
66192
+ reason: "lets every writer call that no rule matches run without a prompt"
66193
+ }];
66194
+ }
65339
66195
  function parseReasonixConfig(fileContent) {
65340
66196
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
65341
66197
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -65386,7 +66242,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65386
66242
  relativeDirPath: paths.relativeDirPath,
65387
66243
  relativeFilePath: paths.relativeFilePath,
65388
66244
  fileContent,
65389
- validate
66245
+ validate,
66246
+ global
65390
66247
  });
65391
66248
  }
65392
66249
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
@@ -65441,6 +66298,13 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65441
66298
  permissions: mergedPermissions,
65442
66299
  authored: override?.allowDynamicBash
65443
66300
  });
66301
+ trustAffecting.push(...applyPermissionsMode({
66302
+ permissions: mergedPermissions,
66303
+ authored: override?.mode,
66304
+ global,
66305
+ relativeFilePath: relativeFilePathForLog,
66306
+ logger
66307
+ }));
65444
66308
  const patch = { permissions: mergedPermissions };
65445
66309
  if (override?.sandbox !== void 0) {
65446
66310
  const mergedSandbox = {
@@ -65482,7 +66346,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65482
66346
  patch,
65483
66347
  filePath
65484
66348
  }),
65485
- validate
66349
+ validate,
66350
+ global
65486
66351
  });
65487
66352
  }
65488
66353
  toRulesyncPermissions() {
@@ -65507,6 +66372,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
65507
66372
  const reasonixOverride = {};
65508
66373
  const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
65509
66374
  if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
66375
+ if (this.global && typeof permissions.mode === "string" && REASONIX_PERMISSION_MODES.has(permissions.mode)) reasonixOverride.mode = permissions.mode;
65510
66376
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
65511
66377
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
65512
66378
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -67072,10 +67938,15 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67072
67938
  outputRoot,
67073
67939
  logger
67074
67940
  });
67941
+ const inheritedProvider = global ? void 0 : await readGlobalTaktProvider({
67942
+ projectFilePath: filePath,
67943
+ logger
67944
+ });
67075
67945
  const rulesyncJson = rulesyncPermissions.getJson();
67076
67946
  const provider = resolveActiveProvider({
67077
67947
  config,
67078
- runtime: runtime.active ? runtime.config : void 0
67948
+ runtime: runtime.active ? runtime.config : void 0,
67949
+ inheritedProvider
67079
67950
  });
67080
67951
  const mode = deriveTaktPermissionMode(rulesyncJson);
67081
67952
  const override = isPlainObject$1(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
@@ -67185,10 +68056,12 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
67185
68056
  * `provider_profiles` (unless that key is the pre-0.68.0 `claude` fallback),
67186
68057
  * else the `claude-sdk` default (Takt 0.68.0+).
67187
68058
  */
67188
- function resolveActiveProvider({ config, runtime }) {
68059
+ function resolveActiveProvider({ config, runtime, inheritedProvider }) {
67189
68060
  const fromRuntime = runtime === void 0 ? void 0 : resolveRuntimeProvider(runtime);
67190
68061
  if (fromRuntime !== void 0) return fromRuntime;
67191
- if (typeof config[TAKT_PROVIDER_KEY] === "string" && config[TAKT_PROVIDER_KEY].trim() !== "") return config[TAKT_PROVIDER_KEY];
68062
+ const local = readProviderName(config);
68063
+ if (local !== void 0) return local;
68064
+ if (inheritedProvider !== void 0) return inheritedProvider;
67192
68065
  const profiles = config[TAKT_PROVIDER_PROFILES_KEY];
67193
68066
  if (isPlainObject$1(profiles)) {
67194
68067
  const keys = Object.keys(profiles);
@@ -67196,6 +68069,43 @@ function resolveActiveProvider({ config, runtime }) {
67196
68069
  }
67197
68070
  return TAKT_DEFAULT_PROVIDER;
67198
68071
  }
68072
+ /**
68073
+ * The top-level `provider:` of a Takt `config.yaml`, when it names one: either
68074
+ * the plain name or the block form `provider: { type: <name>, ... }` Takt also
68075
+ * accepts (`ConfigProviderReference`).
68076
+ */
68077
+ function readProviderName(config) {
68078
+ const raw = config[TAKT_PROVIDER_KEY];
68079
+ const provider = isPlainObject$1(raw) ? raw.type : raw;
68080
+ return typeof provider === "string" && provider.trim() !== "" ? provider : void 0;
68081
+ }
68082
+ /**
68083
+ * The top-level `provider:` of the global `~/.takt/config.yaml`, read for a
68084
+ * project-scope generate. An unresolvable home directory, a missing or
68085
+ * unparsable file, or one that names no provider all mean "nothing inherited".
68086
+ */
68087
+ async function readGlobalTaktProvider({ projectFilePath, logger }) {
68088
+ let globalPath;
68089
+ try {
68090
+ globalPath = join(getHomeDirectory(), TAKT_DIR, TAKT_CONFIG_FILE_NAME);
68091
+ } catch {
68092
+ return;
68093
+ }
68094
+ if (globalPath === projectFilePath) return;
68095
+ try {
68096
+ const content = await readFileContentOrNull(globalPath);
68097
+ if (content === null) return;
68098
+ return readProviderName(parseSharedConfig({
68099
+ format: "yaml",
68100
+ fileContent: content,
68101
+ filePath: globalPath,
68102
+ invalidRootPolicy: "error"
68103
+ }));
68104
+ } catch (error) {
68105
+ logger?.warn(`Takt permissions: could not read the provider from ${globalPath} (${formatError(error)}); falling back to the project config alone.`);
68106
+ return;
68107
+ }
68108
+ }
67199
68109
  /** A plain object carrying at least one entry (Takt's "assignment" test). */
67200
68110
  function hasEntries(value) {
67201
68111
  return isPlainObject$1(value) && Object.keys(value).length > 0;
@@ -69226,6 +70136,9 @@ const WARP_OVERRIDE_KEYS = [
69226
70136
  "agent_mode_execute_readonly_commands"
69227
70137
  ];
69228
70138
  const WARP_EXECUTION_PROFILE_OVERRIDE_KEY = "execution_profile";
70139
+ const WARP_AUTO_APPROVE_BYPASS_KEY = "auto_approve_bypasses_command_denylist";
70140
+ const WARP_AGENT_KEY = "warp_agent";
70141
+ const WARP_AGENT_OTHER_KEY = "other";
69229
70142
  const WARP_EXECUTION_PROFILE_KEYS = [
69230
70143
  "read_files",
69231
70144
  "apply_code_diffs",
@@ -69374,7 +70287,7 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69374
70287
  const override = config.warp;
69375
70288
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69376
70289
  if (isRecord$1(override)) {
69377
- const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, ...legacyOverride } = override;
70290
+ const { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: _executionProfile, [WARP_AUTO_APPROVE_BYPASS_KEY]: _autoApproveBypass, ...legacyOverride } = override;
69378
70291
  Object.assign(profiles, legacyOverride);
69379
70292
  }
69380
70293
  const mergedAllow = uniq(allow.toSorted());
@@ -69384,9 +70297,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69384
70297
  if (mergedDeny.length > 0) profiles[DENYLIST_KEY] = mergedDeny;
69385
70298
  else delete profiles[DENYLIST_KEY];
69386
70299
  agents.profiles = profiles;
70300
+ applyAutoApproveBypassOverride({
70301
+ agents,
70302
+ override
70303
+ });
69387
70304
  warnAboutDenylistReplacement({
69388
70305
  toolLabel: "Warp",
70306
+ overrideKey: "warp",
69389
70307
  denyCount: mergedDeny.length,
70308
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
69390
70309
  logger
69391
70310
  });
69392
70311
  mergeIntoDefaultExecutionProfile({
@@ -69425,6 +70344,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69425
70344
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
69426
70345
  const executionProfileOverride = defaultProfile ? liftExecutionProfileOverride(defaultProfile) : void 0;
69427
70346
  if (executionProfileOverride) warpOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70347
+ const autoApproveBypass = readAutoApproveBypass(agents);
70348
+ if (autoApproveBypass !== void 0) warpOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69428
70349
  const result = { ...config };
69429
70350
  if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
69430
70351
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -69448,12 +70369,40 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
69448
70369
  };
69449
70370
  /**
69450
70371
  * Writing `command_denylist` at all replaces Warp's built-in default denylist,
69451
- * so any non-empty list rulesync writes deserves a heads-up.
70372
+ * so any non-empty list rulesync writes deserves a heads-up. Unless
70373
+ * `auto_approve_bypasses_command_denylist` is `false`, auto-approve also runs
70374
+ * commands that match the list, so the warning says so.
69452
70375
  * https://docs.warp.dev/agents/cli/permissions-and-profiles/
69453
70376
  */
69454
- function warnAboutDenylistReplacement({ toolLabel, denyCount, logger }) {
70377
+ function warnAboutDenylistReplacement({ toolLabel, overrideKey, denyCount, autoApproveBypassesDenylist, logger }) {
69455
70378
  if (denyCount === 0 || !logger) return;
69456
- logger.warn(`${toolLabel}'s command_denylist replaces its built-in default denylist, which covers rm, curl, wget, eval, ssh, shells, and other risky command patterns. The ${denyCount} deny rule(s) from .rulesync/permissions.jsonc are now the whole denylist — add equivalents for the built-in patterns you want to keep.`);
70379
+ const bypassNote = autoApproveBypassesDenylist === false ? "" : ` Auto-approve also runs commands that match the denylist unless ${overrideKey}.${WARP_AUTO_APPROVE_BYPASS_KEY} is set to false.`;
70380
+ 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}`);
70381
+ }
70382
+ /**
70383
+ * Write the override's `auto_approve_bypasses_command_denylist` into
70384
+ * `[agents.warp_agent.other]`, keeping every sibling key of `agents.warp_agent`
70385
+ * and of its `other` table. Nothing is written when the override omits it.
70386
+ */
70387
+ function applyAutoApproveBypassOverride({ agents, override }) {
70388
+ if (!isRecord$1(override)) return;
70389
+ const value = override[WARP_AUTO_APPROVE_BYPASS_KEY];
70390
+ if (typeof value !== "boolean") return;
70391
+ const warpAgent = isRecord$1(agents[WARP_AGENT_KEY]) ? { ...agents[WARP_AGENT_KEY] } : {};
70392
+ const other = isRecord$1(warpAgent[WARP_AGENT_OTHER_KEY]) ? { ...warpAgent[WARP_AGENT_OTHER_KEY] } : {};
70393
+ other[WARP_AUTO_APPROVE_BYPASS_KEY] = value;
70394
+ warpAgent[WARP_AGENT_OTHER_KEY] = other;
70395
+ agents[WARP_AGENT_KEY] = warpAgent;
70396
+ }
70397
+ /**
70398
+ * Read `[agents.warp_agent.other] auto_approve_bypasses_command_denylist`, or
70399
+ * `undefined` when it is unset (Warp then defaults to `true`).
70400
+ */
70401
+ function readAutoApproveBypass(agents) {
70402
+ const warpAgent = agents[WARP_AGENT_KEY];
70403
+ const other = isRecord$1(warpAgent) ? warpAgent[WARP_AGENT_OTHER_KEY] : void 0;
70404
+ const value = isRecord$1(other) ? other[WARP_AUTO_APPROVE_BYPASS_KEY] : void 0;
70405
+ return typeof value === "boolean" ? value : void 0;
69457
70406
  }
69458
70407
  /**
69459
70408
  * On migrated installs the legacy `[agents.profiles]` keys are inert: runtime
@@ -69807,6 +70756,9 @@ const WARPCLI_GLOBAL_ONLY_MESSAGE = "Warp Agent CLI permissions are global-only;
69807
70756
  * (`read_files`, `apply_code_diffs`, `run_agents`, ...), merged first so the
69808
70757
  * rulesync-owned command lists always win.
69809
70758
  *
70759
+ * The `warpcli.auto_approve_bypasses_command_denylist` override is written to
70760
+ * the sibling `[agents.warp_agent.other]` table.
70761
+ *
69810
70762
  * Unlike the `warp` target, the CLI never read the app's legacy
69811
70763
  * `[agents.profiles]` keys, so none are written, and there is no settings
69812
70764
  * migration to wait for: the CLI's settings file "is created the first time you
@@ -69873,11 +70825,6 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69873
70825
  });
69874
70826
  const mergedAllow = uniq(allow.toSorted());
69875
70827
  const mergedDeny = uniq(deny.toSorted());
69876
- warnAboutDenylistReplacement({
69877
- toolLabel: "Warp Agent CLI",
69878
- denyCount: mergedDeny.length,
69879
- logger
69880
- });
69881
70828
  const override = config.warpcli;
69882
70829
  const executionProfileOverride = isRecord$1(override) && isRecord$1(override["execution_profile"]) ? override[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] : void 0;
69883
70830
  const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
@@ -69887,7 +70834,18 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69887
70834
  mergedDeny,
69888
70835
  executionProfileOverride
69889
70836
  });
70837
+ applyAutoApproveBypassOverride({
70838
+ agents,
70839
+ override
70840
+ });
69890
70841
  settings.agents = agents;
70842
+ warnAboutDenylistReplacement({
70843
+ toolLabel: "Warp Agent CLI",
70844
+ overrideKey: "warpcli",
70845
+ denyCount: mergedDeny.length,
70846
+ autoApproveBypassesDenylist: readAutoApproveBypass(agents),
70847
+ logger
70848
+ });
69891
70849
  return new WarpcliPermissions({
69892
70850
  outputRoot,
69893
70851
  relativeDirPath: paths.relativeDirPath,
@@ -69914,8 +70872,12 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
69914
70872
  deny: isStringArray$2(denylist) ? denylist : []
69915
70873
  });
69916
70874
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
70875
+ const warpcliOverride = {};
70876
+ if (executionProfileOverride) warpcliOverride[WARP_EXECUTION_PROFILE_OVERRIDE_KEY] = executionProfileOverride;
70877
+ const autoApproveBypass = readAutoApproveBypass(agents);
70878
+ if (autoApproveBypass !== void 0) warpcliOverride[WARP_AUTO_APPROVE_BYPASS_KEY] = autoApproveBypass;
69917
70879
  const result = { ...config };
69918
- if (executionProfileOverride) result.warpcli = { [WARP_EXECUTION_PROFILE_OVERRIDE_KEY]: executionProfileOverride };
70880
+ if (Object.keys(warpcliOverride).length > 0) result.warpcli = warpcliOverride;
69919
70881
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
69920
70882
  }
69921
70883
  validate() {
@@ -81989,9 +82951,8 @@ var CodebuddySubagent = class CodebuddySubagent extends ToolSubagent {
81989
82951
  * Keys of the rulesync `codewhale` section copied into the profile. Codewhale
81990
82952
  * parses agent profiles with `deny_unknown_fields`, so an unknown key would
81991
82953
  * make the whole profile fail to load; anything else in the section is dropped
81992
- * with a warning. `[tools]` and `[permissions]` are left out on purpose:
81993
- * Codewhale only accepts values there that narrow the defaults, and rejects a
81994
- * profile that tries to widen them.
82954
+ * with a warning. The `[tools]` and `[permissions]` tables are handled
82955
+ * separately (see `CODEWHALE_NARROWING_TABLES`).
81995
82956
  */
81996
82957
  const CODEWHALE_SECTION_KEYS = [
81997
82958
  "role_hint",
@@ -82012,6 +82973,48 @@ const CODEWHALE_SECTION_KEY_ALIASES = {
82012
82973
  reasoning_effort: ["thinking", "reasoning"]
82013
82974
  };
82014
82975
  const CODEWHALE_SECTION_ALIAS_TO_KEY = new Map(Object.entries(CODEWHALE_SECTION_KEY_ALIASES).flatMap(([key, aliases]) => (aliases ?? []).map((alias) => [alias, key])));
82976
+ /**
82977
+ * The `[tools]` and `[permissions]` tables of an agent profile, with the only
82978
+ * values Codewhale accepts in each key. Codewhale rejects a profile that tries
82979
+ * to widen access through them (`reject_permission_expansion` in
82980
+ * `fleet/profile.rs`), and both tables deny unknown fields, so any other key
82981
+ * or value is dropped with a warning.
82982
+ */
82983
+ const CODEWHALE_NARROWING_TABLES = {
82984
+ tools: { posture: (value) => typeof value === "string" && [
82985
+ "read-only",
82986
+ "readonly",
82987
+ "read_only"
82988
+ ].includes(value) },
82989
+ permissions: {
82990
+ allow_shell: (value) => value === false,
82991
+ trust: (value) => value === false,
82992
+ approval_required: (value) => value === true
82993
+ }
82994
+ };
82995
+ /**
82996
+ * Keeps the narrowing-only entries of a `[tools]` / `[permissions]` table and
82997
+ * reports every other entry by its dotted path. A non-table value is dropped
82998
+ * as a whole.
82999
+ */
83000
+ function filterNarrowingTable(tableName, value) {
83001
+ const allowed = CODEWHALE_NARROWING_TABLES[tableName] ?? {};
83002
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {
83003
+ kept: void 0,
83004
+ dropped: [tableName]
83005
+ };
83006
+ const kept = {};
83007
+ const dropped = [];
83008
+ for (const [key, entry] of Object.entries(value)) {
83009
+ const normalized = typeof entry === "string" ? entry.trim() : entry;
83010
+ if (Object.hasOwn(allowed, key) && allowed[key]?.(normalized)) kept[key] = normalized;
83011
+ else dropped.push(`${tableName}.${key}`);
83012
+ }
83013
+ return {
83014
+ kept: Object.keys(kept).length > 0 ? kept : void 0,
83015
+ dropped
83016
+ };
83017
+ }
82015
83018
  const CodewhaleSubagentTomlSchema = z.looseObject({
82016
83019
  id: z.optional(z.string()),
82017
83020
  name: z.optional(z.string()),
@@ -82038,7 +83041,8 @@ function stringifyCodewhaleSubagentToml(fields, instructions) {
82038
83041
  *
82039
83042
  * Generation writes `id` (the file stem), `display_name` (the rulesync name),
82040
83043
  * `description`, and the body as `[instructions] text`, plus the allowlisted
82041
- * keys of the rulesync `codewhale` section.
83044
+ * keys of the rulesync `codewhale` section and its narrowing-only `[tools]` /
83045
+ * `[permissions]` entries.
82042
83046
  *
82043
83047
  * @see https://github.com/Hmbown/Codewhale/blob/main/docs/SUBAGENTS.md
82044
83048
  * @see https://github.com/Hmbown/Codewhale/blob/main/crates/tui/src/fleet/profile.rs
@@ -82073,6 +83077,10 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82073
83077
  const spelling = [key, ...CODEWHALE_SECTION_KEY_ALIASES[key] ?? []].find((candidate) => record[candidate] !== void 0);
82074
83078
  if (spelling !== void 0) codewhaleSection[key] = record[spelling];
82075
83079
  }
83080
+ for (const tableName of Object.keys(CODEWHALE_NARROWING_TABLES)) if (record[tableName] !== void 0) {
83081
+ const { kept } = filterNarrowingTable(tableName, record[tableName]);
83082
+ if (kept !== void 0) codewhaleSection[tableName] = kept;
83083
+ }
82076
83084
  return new RulesyncSubagent({
82077
83085
  outputRoot: ".",
82078
83086
  frontmatter: {
@@ -82095,6 +83103,12 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82095
83103
  const droppedKeys = [];
82096
83104
  const sectionSpellings = /* @__PURE__ */ new Map();
82097
83105
  for (const [rawKey, value] of Object.entries(rawSection)) {
83106
+ if (Object.hasOwn(CODEWHALE_NARROWING_TABLES, rawKey)) {
83107
+ const { kept, dropped } = filterNarrowingTable(rawKey, value);
83108
+ if (kept !== void 0) sectionFields[rawKey] = kept;
83109
+ droppedKeys.push(...dropped);
83110
+ continue;
83111
+ }
82098
83112
  const key = CODEWHALE_SECTION_ALIAS_TO_KEY.get(rawKey) ?? rawKey;
82099
83113
  if (CODEWHALE_SECTION_KEYS.includes(key) && typeof value === "string") {
82100
83114
  if (key === rawKey || sectionFields[key] === void 0) {
@@ -82104,7 +83118,7 @@ var CodewhaleSubagent = class CodewhaleSubagent extends ToolSubagent {
82104
83118
  } else droppedKeys.push(rawKey);
82105
83119
  } else droppedKeys.push(rawKey);
82106
83120
  }
82107
- if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias).`);
83121
+ if (droppedKeys.length > 0) logger?.warn(`Dropping unsupported or duplicate codewhale subagent keys in ${rulesyncSubagent.getRelativeFilePath()}: ${droppedKeys.join(", ")}. Codewhale rejects agent profiles with unknown fields, non-string values there, [tools] / [permissions] values that would widen access, or two spellings of one key; supported keys are ${CODEWHALE_SECTION_KEYS.join(", ")}, each a string (the canonical spelling wins over an alias), plus the narrowing-only tools.posture = "read-only" and permissions.allow_shell = false, permissions.trust = false, permissions.approval_required = true.`);
82108
83122
  const body = stringifyCodewhaleSubagentToml({
82109
83123
  id: toProfileId(relativeFilePath),
82110
83124
  display_name: frontmatter.name,
@@ -88601,6 +89615,99 @@ const AntigravityRuleFrontmatterSchema = z.looseObject({
88601
89615
  function toGlobalRuleFileName(relativeFilePath) {
88602
89616
  return relativeFilePath.split(/[\\/]/).join("-");
88603
89617
  }
89618
+ /** The project rules directory both Antigravity targets share, as a POSIX path. */
89619
+ const PROJECT_RULES_DIR = `${ANTIGRAVITY_IDE_AGENTS_DIR}/rules`;
89620
+ /**
89621
+ * The subproject directory a rule is scoped to, or `undefined` when it goes to
89622
+ * the project-root `.agents/rules/`. Antigravity reads a `.agents/rules/`
89623
+ * directory in any subdirectory and loads it for files under that directory,
89624
+ * so a non-root rule carrying `agentsmd.subprojectPath` is written there. Project
89625
+ * scope only: there is no workspace tree to nest under in global scope.
89626
+ *
89627
+ * @see https://antigravity.google/docs/rules
89628
+ */
89629
+ function resolveAntigravitySubprojectPath({ rulesyncRule, global }) {
89630
+ const { root, agentsmd } = rulesyncRule.getFrontmatter();
89631
+ const subprojectPath = !global && root !== true ? agentsmd?.subprojectPath : void 0;
89632
+ return subprojectPath && ![
89633
+ "",
89634
+ ".",
89635
+ "./"
89636
+ ].includes(subprojectPath) ? subprojectPath : void 0;
89637
+ }
89638
+ /**
89639
+ * File name of a rule in a subproject's `.agents/rules/`. Antigravity reads
89640
+ * only the direct children of a rules directory, so a nested rulesync rule
89641
+ * (`frontend/style.md`) is flattened (`frontend-style.md`), and the name keeps
89642
+ * its case so the IDE and the CLI write the same file and an imported
89643
+ * `codeStyle.md` is written back to the same place.
89644
+ */
89645
+ function toNestedRuleFileName(relativeFilePath) {
89646
+ return toGlobalRuleFileName(relativeFilePath);
89647
+ }
89648
+ /** The `.agents/rules/` directory of a subproject. */
89649
+ function toAntigravityNestedRulesDirPath(subprojectPath) {
89650
+ return join(subprojectPath, ANTIGRAVITY_IDE_AGENTS_DIR, "rules");
89651
+ }
89652
+ /**
89653
+ * Split a nested rule directory such as `packages/api/.agents/rules/sub` into
89654
+ * the subproject (`packages/api`) and the folder inside its rules directory
89655
+ * (`sub`). Returns `undefined` for the project-root `.agents/rules/`, for a
89656
+ * path with no `.agents/rules` segment, and for one under a hidden directory —
89657
+ * another tool's output or a worktree, not a subproject.
89658
+ */
89659
+ function extractAntigravitySubprojectPath(relativeDirPath) {
89660
+ const segments = toPosixPath(relativeDirPath).split("/");
89661
+ const index = segments.findIndex((segment, i) => segment === ".agents" && segments[i + 1] === "rules");
89662
+ if (index <= 0) return;
89663
+ const subprojectSegments = segments.slice(0, index);
89664
+ if (subprojectSegments.some((segment) => segment === "" || segment.startsWith("."))) return;
89665
+ return {
89666
+ subprojectPath: subprojectSegments.join("/"),
89667
+ innerDirPath: segments.slice(index + 2).join("/")
89668
+ };
89669
+ }
89670
+ /**
89671
+ * The `.agents/rules/` folders of project subdirectories. Only their direct
89672
+ * `.md` children are read, because Antigravity ignores files nested in
89673
+ * subfolders of a rules directory. The shared nested
89674
+ * scan ignores every hidden directory, which would skip `.agents` itself, so
89675
+ * only a `.agents/rules/` below another hidden directory is ignored here
89676
+ * (`extractAntigravitySubprojectPath` rejects one again on import). Import-only,
89677
+ * like every other nested scan: these files sit outside the rulesync-owned root
89678
+ * `.agents/rules/`, so `--delete` never sweeps them.
89679
+ */
89680
+ function getAntigravityNestedFilePatterns() {
89681
+ return {
89682
+ include: [`**/${PROJECT_RULES_DIR}/*.md`],
89683
+ ignore: [
89684
+ `${PROJECT_RULES_DIR}/**`,
89685
+ `**/.*/**/${PROJECT_RULES_DIR}/**`,
89686
+ ...NESTED_SCAN_EXCLUDED_DIRS_ANY_DEPTH.map((dir) => `**/${dir}/**`),
89687
+ ...NESTED_SCAN_EXCLUDED_ROOT_DIRS.map((dir) => `${dir}/**`)
89688
+ ],
89689
+ ownedDirPath: PROJECT_RULES_DIR
89690
+ };
89691
+ }
89692
+ /**
89693
+ * Resolve where a non-root project rule file read from disk lives. A file
89694
+ * discovered by `getNestedFilePatterns` arrives as its own directory plus its
89695
+ * basename; it is re-split into the subproject's rules directory and the path
89696
+ * inside it, matching how the root `.agents/rules/` is read.
89697
+ */
89698
+ function resolveAntigravityNonRootFile({ relativeDirPath, relativeFilePath, defaultDirPath }) {
89699
+ const nested = relativeDirPath === void 0 ? void 0 : extractAntigravitySubprojectPath(relativeDirPath);
89700
+ if (nested === void 0) return {
89701
+ relativeDirPath: defaultDirPath,
89702
+ relativeFilePath
89703
+ };
89704
+ return {
89705
+ relativeDirPath: toAntigravityNestedRulesDirPath(nested.subprojectPath),
89706
+ relativeFilePath: join(nested.innerDirPath, relativeFilePath)
89707
+ };
89708
+ }
89709
+ /** The targets a nested rule is imported with: the two that read the shared tree. */
89710
+ const ANTIGRAVITY_NESTED_RULE_TARGETS = ["antigravity-ide", "antigravity-cli"];
88604
89711
  /**
88605
89712
  * Converts a comma-separated globs string or array to an array of globs.
88606
89713
  * @param globs - Comma-separated globs string (e.g., "*.ts,*.js") or array of globs
@@ -88788,12 +89895,17 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88788
89895
  });
88789
89896
  }
88790
89897
  if (!paths.nonRoot) throw new Error(`nonRoot path is not set for ${relativeFilePath}`);
88791
- const relativePath = join(paths.nonRoot.relativeDirPath, relativeFilePath);
89898
+ const nonRoot = resolveAntigravityNonRootFile({
89899
+ relativeDirPath,
89900
+ relativeFilePath,
89901
+ defaultDirPath: paths.nonRoot.relativeDirPath
89902
+ });
89903
+ const relativePath = join(nonRoot.relativeDirPath, nonRoot.relativeFilePath);
88792
89904
  const fileContent = await readFileContent(join(outputRoot, relativePath));
88793
89905
  const rule = new AntigravityCliRule({
88794
89906
  outputRoot,
88795
- relativeDirPath: paths.nonRoot.relativeDirPath,
88796
- relativeFilePath,
89907
+ relativeDirPath: nonRoot.relativeDirPath,
89908
+ relativeFilePath: nonRoot.relativeFilePath,
88797
89909
  fileContent,
88798
89910
  validate,
88799
89911
  root: false,
@@ -88825,11 +89937,21 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88825
89937
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
88826
89938
  const frontmatter = strategy.generateFrontmatter(normalizeStoredAntigravity(storedAntigravity), rulesyncFrontmatter);
88827
89939
  const fileContent = stringifyFrontmatter(rulesyncRule.getBody(), frontmatter);
88828
- if (!global) return new AntigravityCliRule({
88829
- ...params,
88830
- fileContent,
88831
- global
88832
- });
89940
+ if (!global) {
89941
+ const subprojectPath = resolveAntigravitySubprojectPath({
89942
+ rulesyncRule,
89943
+ global
89944
+ });
89945
+ return new AntigravityCliRule({
89946
+ ...params,
89947
+ ...subprojectPath !== void 0 && {
89948
+ relativeDirPath: toAntigravityNestedRulesDirPath(subprojectPath),
89949
+ relativeFilePath: toNestedRuleFileName(params.relativeFilePath)
89950
+ },
89951
+ fileContent,
89952
+ global
89953
+ });
89954
+ }
88833
89955
  return new AntigravityCliRule({
88834
89956
  ...params,
88835
89957
  relativeFilePath: toGlobalRuleFileName(params.relativeFilePath),
@@ -88845,22 +89967,32 @@ var AntigravityCliRule = class AntigravityCliRule extends ToolRule {
88845
89967
  globs: [],
88846
89968
  antigravity: frontmatter
88847
89969
  };
89970
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
88848
89971
  return new RulesyncRule({
88849
89972
  outputRoot: process.cwd(),
88850
89973
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
88851
89974
  relativeFilePath: this.getRelativeFilePath(),
88852
89975
  frontmatter: {
88853
89976
  root: false,
88854
- targets: ["*"],
89977
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
88855
89978
  ...rulesyncData,
88856
89979
  antigravity: {
88857
89980
  ...rulesyncData.antigravity,
88858
89981
  globs: frontmatter.globs ? parseGlobsString$1(frontmatter.globs) : void 0
88859
- }
89982
+ },
89983
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
88860
89984
  },
88861
89985
  body
88862
89986
  });
88863
89987
  }
89988
+ /**
89989
+ * The `.agents/rules/` folders of project subdirectories, imported back as
89990
+ * directory-scoped rules.
89991
+ * @see https://antigravity.google/docs/rules
89992
+ */
89993
+ static getNestedFilePatterns() {
89994
+ return getAntigravityNestedFilePatterns();
89995
+ }
88864
89996
  validate() {
88865
89997
  if (this.root) return {
88866
89998
  success: true,
@@ -88990,8 +90122,15 @@ var AntigravityIdeRule = class extends ToolRule {
88990
90122
  global
88991
90123
  });
88992
90124
  }
88993
- const nonRootDirPath = paths.nonRoot.relativeDirPath;
88994
- const filePath = join(outputRoot, nonRootDirPath, relativeFilePath);
90125
+ const nonRoot = this.supportsSubprojectRules() ? resolveAntigravityNonRootFile({
90126
+ relativeDirPath,
90127
+ relativeFilePath,
90128
+ defaultDirPath: paths.nonRoot.relativeDirPath
90129
+ }) : {
90130
+ relativeDirPath: paths.nonRoot.relativeDirPath,
90131
+ relativeFilePath
90132
+ };
90133
+ const filePath = join(outputRoot, nonRoot.relativeDirPath, nonRoot.relativeFilePath);
88995
90134
  const { frontmatter, body } = parseFrontmatter(await readFileContent(filePath), filePath);
88996
90135
  let parsedFrontmatter;
88997
90136
  if (validate) {
@@ -89001,8 +90140,8 @@ var AntigravityIdeRule = class extends ToolRule {
89001
90140
  } else parsedFrontmatter = frontmatter;
89002
90141
  return new this({
89003
90142
  outputRoot,
89004
- relativeDirPath: nonRootDirPath,
89005
- relativeFilePath,
90143
+ relativeDirPath: nonRoot.relativeDirPath,
90144
+ relativeFilePath: nonRoot.relativeFilePath,
89006
90145
  body,
89007
90146
  frontmatter: parsedFrontmatter,
89008
90147
  validate,
@@ -89010,6 +90149,25 @@ var AntigravityIdeRule = class extends ToolRule {
89010
90149
  global
89011
90150
  });
89012
90151
  }
90152
+ /**
90153
+ * Whether a rule scoped to a subproject goes to that subproject's own
90154
+ * `.agents/rules/`. A subclass that writes somewhere other than a project
90155
+ * tree (the plugin bundle) turns it off.
90156
+ */
90157
+ static supportsSubprojectRules() {
90158
+ return true;
90159
+ }
90160
+ /**
90161
+ * The `.agents/rules/` folders of project subdirectories, imported back as
90162
+ * directory-scoped rules.
90163
+ * @see https://antigravity.google/docs/rules
90164
+ */
90165
+ static getNestedFilePatterns() {
90166
+ return this.supportsSubprojectRules() ? getAntigravityNestedFilePatterns() : {
90167
+ include: [],
90168
+ ignore: []
90169
+ };
90170
+ }
89013
90171
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
89014
90172
  const paths = this.getSettablePaths({ global });
89015
90173
  if (rulesyncRule.getFrontmatter().root) {
@@ -89032,10 +90190,14 @@ var AntigravityIdeRule = class extends ToolRule {
89032
90190
  const strategy = STRATEGIES$1.find((s) => s.canHandle(storedTrigger));
89033
90191
  if (!strategy) throw new Error(`No strategy found for trigger: ${storedTrigger}`);
89034
90192
  const frontmatter = strategy.generateFrontmatter(normalized, rulesyncFrontmatter);
89035
- const relativeFilePath = global ? toGlobalRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
90193
+ const subprojectPath = this.supportsSubprojectRules() ? resolveAntigravitySubprojectPath({
90194
+ rulesyncRule,
90195
+ global
90196
+ }) : void 0;
90197
+ const relativeFilePath = global || subprojectPath !== void 0 ? toNestedRuleFileName(rulesyncRule.getRelativeFilePath()) : toKebabCaseFilename(rulesyncRule.getRelativeFilePath());
89036
90198
  return new this({
89037
90199
  outputRoot,
89038
- relativeDirPath: paths.nonRoot.relativeDirPath,
90200
+ relativeDirPath: subprojectPath === void 0 ? paths.nonRoot.relativeDirPath : toAntigravityNestedRulesDirPath(subprojectPath),
89039
90201
  relativeFilePath,
89040
90202
  frontmatter,
89041
90203
  body: rulesyncRule.getBody(),
@@ -89056,15 +90218,17 @@ var AntigravityIdeRule = class extends ToolRule {
89056
90218
  ...rulesyncData.antigravity,
89057
90219
  globs: this.frontmatter.globs ? parseGlobsString$1(this.frontmatter.globs) : void 0
89058
90220
  };
90221
+ const subprojectPath = this.global ? void 0 : extractAntigravitySubprojectPath(this.getRelativeDirPath())?.subprojectPath;
89059
90222
  return new RulesyncRule({
89060
90223
  outputRoot: process.cwd(),
89061
90224
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
89062
90225
  relativeFilePath: this.getRelativeFilePath(),
89063
90226
  frontmatter: {
89064
90227
  root: false,
89065
- targets: ["*"],
90228
+ targets: subprojectPath === void 0 ? ["*"] : [...ANTIGRAVITY_NESTED_RULE_TARGETS],
89066
90229
  ...rulesyncData,
89067
- antigravity: antigravityForRulesync
90230
+ antigravity: antigravityForRulesync,
90231
+ ...subprojectPath !== void 0 && { agentsmd: { subprojectPath } }
89068
90232
  },
89069
90233
  body: this.body
89070
90234
  });
@@ -89118,6 +90282,9 @@ var AntigravityIdeRule = class extends ToolRule {
89118
90282
  //#endregion
89119
90283
  //#region src/features/rules/antigravity-plugin-rule.ts
89120
90284
  var AntigravityPluginRule = class extends AntigravityIdeRule {
90285
+ static supportsSubprojectRules() {
90286
+ return false;
90287
+ }
89121
90288
  static isTargetedByRulesyncRule(rulesyncRule) {
89122
90289
  const targets = rulesyncRule.getFrontmatter().targets;
89123
90290
  return targets.includes("*") || targets.includes("antigravity-plugin");
@@ -95467,6 +96634,12 @@ var VibeRule = class extends NestedAgentsmdRule {
95467
96634
  * file, so every non-root rule without `agentsmd.subprojectPath` folds into
95468
96635
  * the root `./AGENTS.md`.
95469
96636
  *
96637
+ * A legacy root `WARP.md` is read on import when the root `AGENTS.md` is
96638
+ * absent, so a repository that has not moved to `AGENTS.md` yet imports its
96639
+ * rules. It is never written or deleted. When both exist, Warp reads
96640
+ * `WARP.md`, so import keeps reading the `AGENTS.md` rulesync manages and warns
96641
+ * that Warp ignores it, as generate already does.
96642
+ *
95470
96643
  * In global mode, Warp reads a third rule source from `~/.agents/AGENTS.md`
95471
96644
  * (the cross-tool agent config directory), indexed like project rules and also
95472
96645
  * used from remote hosts in SSH sessions. The same root-fold policy applies.
@@ -95481,6 +96654,31 @@ var WarpRule = class extends NestedAgentsmdRule {
95481
96654
  toolTarget: "warp"
95482
96655
  };
95483
96656
  }
96657
+ static getSettablePaths(options = {}) {
96658
+ const paths = super.getSettablePaths(options);
96659
+ if (options.global) return paths;
96660
+ return {
96661
+ ...paths,
96662
+ importOnlyRoots: [{
96663
+ relativeDirPath: ".",
96664
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96665
+ onlyWhenRootAbsent: true,
96666
+ outranksRoot: true
96667
+ }]
96668
+ };
96669
+ }
96670
+ static async fromFile(params) {
96671
+ const { outputRoot = process.cwd(), relativeDirPath, relativeFilePath, validate = true } = params;
96672
+ if (!params.global && relativeFilePath === "WARP.md" && (relativeDirPath === void 0 || relativeDirPath === ".")) return new this({
96673
+ outputRoot,
96674
+ relativeDirPath: ".",
96675
+ relativeFilePath: WARP_LEGACY_RULE_FILE_NAME,
96676
+ fileContent: await readFileContent(join(outputRoot, WARP_LEGACY_RULE_FILE_NAME)),
96677
+ validate,
96678
+ root: true
96679
+ });
96680
+ return super.fromFile(params);
96681
+ }
95484
96682
  };
95485
96683
  //#endregion
95486
96684
  //#region src/features/rules/zcode-rule.ts
@@ -96783,7 +97981,7 @@ var RulesProcessor = class extends FeatureProcessor {
96783
97981
  if (!importOnlyRoots || importOnlyRoots.length === 0) return;
96784
97982
  const existingPaths = [];
96785
97983
  for (const importOnlyRoot of importOnlyRoots) {
96786
- if (importOnlyRoot.onlyWhenRootAbsent !== true) continue;
97984
+ if (importOnlyRoot.onlyWhenRootAbsent !== true || importOnlyRoot.outranksRoot === true) continue;
96787
97985
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
96788
97986
  cwd: this.outputRoot,
96789
97987
  type: "file"
@@ -97484,13 +98682,14 @@ As this project's AI coding tool, you must follow the additional conventions bel
97484
98682
  const rootFilePath = primaryRootFilePaths[0];
97485
98683
  const scannedPaths = [];
97486
98684
  const skippedPaths = [];
98685
+ const skippedOutrankingPaths = [];
97487
98686
  for (const importOnlyRoot of importOnlyRoots) {
97488
98687
  const matchedPaths = await findFilesByGlobs(rootRelativeGlob(importOnlyRoot.relativeDirPath, importOnlyRoot.relativeFilePath ?? `*.${factory.meta.extension}`), {
97489
98688
  cwd: this.outputRoot,
97490
98689
  type: "file"
97491
98690
  });
97492
98691
  if (importOnlyRoot.onlyWhenRootAbsent === true && rootFilePath !== void 0) {
97493
- skippedPaths.push(...matchedPaths);
98692
+ (importOnlyRoot.outranksRoot === true ? skippedOutrankingPaths : skippedPaths).push(...matchedPaths);
97494
98693
  continue;
97495
98694
  }
97496
98695
  scannedPaths.push(...matchedPaths);
@@ -97501,6 +98700,13 @@ As this project's AI coding tool, you must follow the additional conventions bel
97501
98700
  const remainingCount = skippedNames.length - listedNames.length;
97502
98701
  this.logger.warn(`Not importing ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} for ${this.toolTarget}: ${stripControlCharacters(relative(this.outputRoot, rootFilePath))} exists, and the tool reads that file exclusively. Delete them once you have checked that content is in the root file, or move it into ${RULESYNC_RULES_RELATIVE_DIR_PATH} if it is not.`);
97503
98702
  }
98703
+ if (skippedOutrankingPaths.length > 0 && rootFilePath !== void 0) {
98704
+ const rootName = stripControlCharacters(relative(this.outputRoot, rootFilePath));
98705
+ const skippedNames = skippedOutrankingPaths.map((filePath) => stripControlCharacters(relative(this.outputRoot, filePath)));
98706
+ const listedNames = skippedNames.slice(0, MAX_LISTED_SKIPPED_IMPORT_ONLY_PATHS);
98707
+ const remainingCount = skippedNames.length - listedNames.length;
98708
+ 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.`);
98709
+ }
97504
98710
  return await buildImportRulesFromPaths(scannedPaths);
97505
98711
  })();
97506
98712
  this.logger.debug(`Found ${importOnlyToolRules.length} import-only tool rule files`);
@@ -100321,4 +101527,4 @@ async function importChecksCore(params) {
100321
101527
  //#endregion
100322
101528
  export { RulesyncHooks as $, removeFile as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_RELATIVE_DIR_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, DEPRECATED_FEATURE_REPLACEMENTS as Bn, getFileSize as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_MCP_SCHEMA_URL as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_USER_CONFIG_DIR_NAME as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, quoteForLog as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, truncateText as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as In, directoryExists as It, RulesyncRule as J, stripHiddenCharacters as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, stripControlCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, parseCommaSeparatedList as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectoryStrict as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, ALL_FEATURES as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_MCP_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, hasDeceptiveHiddenCharacters as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, formatError as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, hasEnclosingMarkOutsideKeycap as Wn, isSymlink as Wt, RulesyncPermissions as X, readFileContentOrNull as Xt, RulesyncRuleFrontmatterSchema as Y, readFileContent as Yt, RulesyncMcp as Z, removeDirectory as Zt, LETTACODE_DIR as _, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, toPosixPath as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, ALL_TOOL_TARGETS as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ToolTargetSchema as dn, ConfigResolver as dt, removeFileStrict as en, getRulesyncSourceCandidates as et, displayWidthOf as f, CURATED_RULES_FEATURE_SUBDIR as fn, mergeInputRootConfigs as ft, HooksProcessor as g, RULESYNC_CHECKS_RELATIVE_DIR_PATH as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as hn, ConfigFileSchema as ht, inspectInputRoots as i, splitPathSegments as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_RULES_RELATIVE_DIR_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_PERMISSIONS_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, ALL_TOOL_TARGETS_WITH_WILDCARD as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, MAX_FILE_SIZE as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, resolvePath as nn, parseJsonc as nt, convertFromTool as o, writeFileBuffer as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, FEATURE_SOURCE_TREE_ENTRIES as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, stripControlCharactersKeepingLineFeeds as qn, listSubdirectoryNames as qt, generate as r, runWithDirectoryRollback as rn, RulesyncCommand as rt, isPackagingToolTarget as s, writeFileContent as sn, stringifyFrontmatter as st, importFromTool as t, removeTempDirectory as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, PACKAGING_TOOL_TARGETS as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_CONFIG_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CONFIG_SCHEMA_URL as yn, findControlCharacter as yt, QWENCODE_DIR as z, ALL_FEATURES_WITH_WILDCARD as zn, findFilesByGlobs as zt };
100323
101529
 
100324
- //# sourceMappingURL=import--SCvWeDn.js.map
101530
+ //# sourceMappingURL=import-ORYbMDJK.js.map