rulesync 9.7.0 → 10.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.
@@ -114,28 +114,35 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$249 } = node_path.posix;
117
+ const { join: join$251 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
+ const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
128
+ const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
129
+ const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
127
130
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
131
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
129
132
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
130
133
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
131
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$249(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
134
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_RELATIVE_DIR_PATH, "skills");
135
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$251(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
133
136
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
134
137
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
135
138
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
136
139
  const RULESYNC_PERMISSIONS_FILE_NAME = "permissions.json";
140
+ const RULESYNC_MCP_JSONC_FILE_NAME = "mcp.jsonc";
141
+ const RULESYNC_HOOKS_JSONC_FILE_NAME = "hooks.jsonc";
142
+ const RULESYNC_PERMISSIONS_JSONC_FILE_NAME = "permissions.jsonc";
137
143
  const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
138
144
  const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
145
+ const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
139
146
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
140
147
  //#endregion
141
148
  //#region src/types/tool-target-tuples.ts
@@ -347,7 +354,8 @@ const hooksProcessorToolTargetTuple = [
347
354
  "junie",
348
355
  "vibe",
349
356
  "qwencode",
350
- "reasonix"
357
+ "reasonix",
358
+ "grokcli"
351
359
  ];
352
360
  const permissionsProcessorToolTargetTuple = [
353
361
  "amp",
@@ -6295,10 +6303,13 @@ const CLAUDE_HOOK_EVENTS = [
6295
6303
  * Hook events supported by Devin Local (native `.devin/` hooks).
6296
6304
  *
6297
6305
  * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
6298
- * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6299
- * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
6306
+ * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6307
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
6308
+ * `PostCompaction` (fires after context compaction; added in the Devin CLI
6309
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
6300
6310
  * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
6301
- * a `matcher` (regex against `tool_name`); the session/turn events do not.
6311
+ * a `matcher` (regex against `tool_name`); the session/turn/compaction events
6312
+ * do not.
6302
6313
  *
6303
6314
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
6304
6315
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -6313,7 +6324,8 @@ const DEVIN_HOOK_EVENTS = [
6313
6324
  "postToolUse",
6314
6325
  "beforeSubmitPrompt",
6315
6326
  "stop",
6316
- "permissionRequest"
6327
+ "permissionRequest",
6328
+ "postCompact"
6317
6329
  ];
6318
6330
  /** Hook events supported by OpenCode. */
6319
6331
  const OPENCODE_HOOK_EVENTS = [
@@ -6507,7 +6519,7 @@ const ANTIGRAVITY_HOOK_EVENTS = [
6507
6519
  * Hook events supported by AugmentCode (Auggie CLI).
6508
6520
  * Auggie mirrors Claude Code's lifecycle hooks but exposes a smaller set:
6509
6521
  * PreToolUse / PostToolUse (tool events, matcher-aware) plus the
6510
- * SessionStart / SessionEnd / Stop session events (no matcher).
6522
+ * SessionStart / SessionEnd / Stop / Notification session events (no matcher).
6511
6523
  * @see https://docs.augmentcode.com/cli/hooks
6512
6524
  */
6513
6525
  const AUGMENTCODE_HOOK_EVENTS = [
@@ -6515,7 +6527,8 @@ const AUGMENTCODE_HOOK_EVENTS = [
6515
6527
  "postToolUse",
6516
6528
  "sessionStart",
6517
6529
  "sessionEnd",
6518
- "stop"
6530
+ "stop",
6531
+ "notification"
6519
6532
  ];
6520
6533
  /**
6521
6534
  * Hook events supported by Mistral Vibe (mistral-vibe).
@@ -6567,6 +6580,10 @@ const JUNIE_HOOK_EVENTS = [
6567
6580
  * The Qwen-specific events
6568
6581
  * `TodoCreated`, `TodoCompleted`, and `StopFailure` map to the canonical
6569
6582
  * `todoCreated`, `todoCompleted`, and `stopFailure` events respectively.
6583
+ * Qwen's `HookEventName` enum (`packages/core/src/hooks/types.ts`) also documents
6584
+ * `PostToolBatch`, `UserPromptExpansion`, `PermissionDenied`, and
6585
+ * `InstructionsLoaded`, which map to the canonical `postToolBatch`,
6586
+ * `userPromptExpansion`, `permissionDenied`, and `instructionsLoaded` events.
6570
6587
  * @see https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
6571
6588
  */
6572
6589
  const QWENCODE_HOOK_EVENTS = [
@@ -6575,7 +6592,9 @@ const QWENCODE_HOOK_EVENTS = [
6575
6592
  "preToolUse",
6576
6593
  "postToolUse",
6577
6594
  "postToolUseFailure",
6595
+ "postToolBatch",
6578
6596
  "beforeSubmitPrompt",
6597
+ "userPromptExpansion",
6579
6598
  "stop",
6580
6599
  "stopFailure",
6581
6600
  "subagentStart",
@@ -6583,7 +6602,9 @@ const QWENCODE_HOOK_EVENTS = [
6583
6602
  "preCompact",
6584
6603
  "postCompact",
6585
6604
  "permissionRequest",
6605
+ "permissionDenied",
6586
6606
  "notification",
6607
+ "instructionsLoaded",
6587
6608
  "todoCreated",
6588
6609
  "todoCompleted"
6589
6610
  ];
@@ -6593,11 +6614,11 @@ const QWENCODE_HOOK_EVENTS = [
6593
6614
  * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
6594
6615
  * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
6595
6616
  * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
6596
- * `SubagentStop`, `Notification`, `PreCompact`). The eight events with a clean
6597
- * canonical equivalent are mapped: `PreToolUse`, `PostToolUse`,
6617
+ * `SubagentStop`, `Notification`, `PreCompact`). All ten have a clean canonical
6618
+ * equivalent and are mapped: `PreToolUse`, `PostToolUse`,
6598
6619
  * `UserPromptSubmit` ← `beforeSubmitPrompt`, `Stop`, `SessionStart`,
6599
- * `SessionEnd`, `SubagentStop`, and `PostLLMCall` ← `postModelInvocation`.
6600
- * (`Notification` and `PreCompact` have no canonical event and are left out.)
6620
+ * `SessionEnd`, `SubagentStop`, `PostLLMCall` ← `postModelInvocation`,
6621
+ * `Notification` ← `notification`, and `PreCompact` `preCompact`.
6601
6622
  * `match` (Reasonix's matcher field name) is honored only on
6602
6623
  * `PreToolUse`/`PostToolUse`, matching the canonical `matcher` field's
6603
6624
  * tool-event scoping used by other adapters.
@@ -6611,7 +6632,39 @@ const REASONIX_HOOK_EVENTS = [
6611
6632
  "sessionStart",
6612
6633
  "sessionEnd",
6613
6634
  "subagentStop",
6614
- "postModelInvocation"
6635
+ "postModelInvocation",
6636
+ "notification",
6637
+ "preCompact"
6638
+ ];
6639
+ /**
6640
+ * Hook events supported by Grok CLI (xAI Grok Build).
6641
+ *
6642
+ * Grok Build documents a Claude-Code-compatible hooks surface with fourteen
6643
+ * PascalCase events, all of which map 1:1 onto an existing canonical arm:
6644
+ * `SessionStart`, `SessionEnd`, `UserPromptSubmit` ← `beforeSubmitPrompt`,
6645
+ * `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied`,
6646
+ * `Stop`, `StopFailure`, `Notification`, `SubagentStart`, `SubagentStop`,
6647
+ * `PreCompact`, `PostCompact`. A `matcher` (a regex tested against the tool
6648
+ * name) is meaningful only on the tool-name events (`PreToolUse`, `PostToolUse`,
6649
+ * `PostToolUseFailure`, `PermissionDenied`), matching Claude Code's semantics;
6650
+ * the remaining lifecycle events are matcher-less.
6651
+ * @see https://docs.x.ai/build/features/hooks
6652
+ */
6653
+ const GROKCLI_HOOK_EVENTS = [
6654
+ "sessionStart",
6655
+ "sessionEnd",
6656
+ "beforeSubmitPrompt",
6657
+ "preToolUse",
6658
+ "postToolUse",
6659
+ "postToolUseFailure",
6660
+ "permissionDenied",
6661
+ "stop",
6662
+ "stopFailure",
6663
+ "notification",
6664
+ "subagentStart",
6665
+ "subagentStop",
6666
+ "preCompact",
6667
+ "postCompact"
6615
6668
  ];
6616
6669
  /**
6617
6670
  * Hook events supported by Hermes Agent's native Shell Hooks system.
@@ -6685,6 +6738,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
6685
6738
  junie: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6686
6739
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6687
6740
  reasonix: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6741
+ grokcli: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6688
6742
  qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
6689
6743
  hooks: zod_mini.z.optional(hooksRecordSchema),
6690
6744
  disableAllHooks: zod_mini.z.optional(zod_mini.z.boolean())
@@ -6743,7 +6797,8 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6743
6797
  postToolUse: "PostToolUse",
6744
6798
  beforeSubmitPrompt: "UserPromptSubmit",
6745
6799
  stop: "Stop",
6746
- permissionRequest: "PermissionRequest"
6800
+ permissionRequest: "PermissionRequest",
6801
+ postCompact: "PostCompaction"
6747
6802
  };
6748
6803
  /**
6749
6804
  * Map Devin Local PascalCase event names to canonical camelCase.
@@ -6758,7 +6813,8 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6758
6813
  postToolUse: "PostToolUse",
6759
6814
  sessionStart: "SessionStart",
6760
6815
  sessionEnd: "SessionEnd",
6761
- stop: "Stop"
6816
+ stop: "Stop",
6817
+ notification: "Notification"
6762
6818
  };
6763
6819
  /**
6764
6820
  * Map AugmentCode PascalCase event names to canonical camelCase.
@@ -7028,7 +7084,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
7028
7084
  preToolUse: "PreToolUse",
7029
7085
  postToolUse: "PostToolUse",
7030
7086
  postToolUseFailure: "PostToolUseFailure",
7087
+ postToolBatch: "PostToolBatch",
7031
7088
  beforeSubmitPrompt: "UserPromptSubmit",
7089
+ userPromptExpansion: "UserPromptExpansion",
7032
7090
  stop: "Stop",
7033
7091
  subagentStart: "SubagentStart",
7034
7092
  subagentStop: "SubagentStop",
@@ -7036,7 +7094,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
7036
7094
  preCompact: "PreCompact",
7037
7095
  postCompact: "PostCompact",
7038
7096
  permissionRequest: "PermissionRequest",
7097
+ permissionDenied: "PermissionDenied",
7039
7098
  notification: "Notification",
7099
+ instructionsLoaded: "InstructionsLoaded",
7040
7100
  todoCreated: "TodoCreated",
7041
7101
  todoCompleted: "TodoCompleted"
7042
7102
  };
@@ -7058,12 +7118,40 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
7058
7118
  sessionStart: "SessionStart",
7059
7119
  sessionEnd: "SessionEnd",
7060
7120
  subagentStop: "SubagentStop",
7061
- postModelInvocation: "PostLLMCall"
7121
+ postModelInvocation: "PostLLMCall",
7122
+ notification: "Notification",
7123
+ preCompact: "PreCompact"
7062
7124
  };
7063
7125
  /**
7064
7126
  * Map Reasonix PascalCase event names to canonical camelCase.
7065
7127
  */
7066
7128
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
7129
+ /**
7130
+ * Map canonical camelCase event names to Grok CLI PascalCase.
7131
+ * Grok Build reuses the same Claude-style PascalCase event names, so each
7132
+ * canonical arm maps to its PascalCase equivalent.
7133
+ * @see https://docs.x.ai/build/features/hooks
7134
+ */
7135
+ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
7136
+ sessionStart: "SessionStart",
7137
+ sessionEnd: "SessionEnd",
7138
+ beforeSubmitPrompt: "UserPromptSubmit",
7139
+ preToolUse: "PreToolUse",
7140
+ postToolUse: "PostToolUse",
7141
+ postToolUseFailure: "PostToolUseFailure",
7142
+ permissionDenied: "PermissionDenied",
7143
+ stop: "Stop",
7144
+ stopFailure: "StopFailure",
7145
+ notification: "Notification",
7146
+ subagentStart: "SubagentStart",
7147
+ subagentStop: "SubagentStop",
7148
+ preCompact: "PreCompact",
7149
+ postCompact: "PostCompact"
7150
+ };
7151
+ /**
7152
+ * Map Grok CLI PascalCase event names to canonical camelCase.
7153
+ */
7154
+ const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
7067
7155
  //#endregion
7068
7156
  //#region src/features/hooks/tool-hooks-converter.ts
7069
7157
  function isToolMatcherEntry(x) {
@@ -7262,12 +7350,55 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
7262
7350
  return canonical;
7263
7351
  }
7264
7352
  //#endregion
7353
+ //#region src/utils/jsonc.ts
7354
+ /**
7355
+ * Rebuild the parsed value from its own enumerable entries, dropping
7356
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
7357
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
7358
+ * literal `__proto__` key would replace the containing object's prototype
7359
+ * instead of becoming an own property; rebuilding gives every object a clean
7360
+ * `Object.prototype` again and severs that path.
7361
+ */
7362
+ function deepSanitize(value) {
7363
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
7364
+ if (value !== null && typeof value === "object") {
7365
+ const sanitized = {};
7366
+ for (const [key, entry] of Object.entries(value)) {
7367
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
7368
+ sanitized[key] = deepSanitize(entry);
7369
+ }
7370
+ return sanitized;
7371
+ }
7372
+ return value;
7373
+ }
7374
+ /**
7375
+ * Parse a JSONC (JSON with Comments) document strictly.
7376
+ *
7377
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
7378
+ * errors and returns a best-effort value), this throws on any parse error so
7379
+ * a malformed source file fails loudly instead of generating half-empty tool
7380
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
7381
+ * `JSON.parse` on files that may contain comments or trailing commas.
7382
+ */
7383
+ function parseJsonc$6(content) {
7384
+ const errors = [];
7385
+ const result = (0, jsonc_parser.parse)(content, errors, {
7386
+ allowTrailingComma: true,
7387
+ disallowComments: false
7388
+ });
7389
+ if (errors.length > 0) {
7390
+ const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
7391
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
7392
+ }
7393
+ return deepSanitize(result);
7394
+ }
7395
+ //#endregion
7265
7396
  //#region src/features/hooks/rulesync-hooks.ts
7266
7397
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7267
7398
  json;
7268
7399
  constructor(params) {
7269
7400
  super({ ...params });
7270
- this.json = JSON.parse(this.fileContent);
7401
+ this.json = parseJsonc$6(this.fileContent);
7271
7402
  if (params.validate) {
7272
7403
  const result = this.validate();
7273
7404
  if (!result.success) throw result.error;
@@ -7276,7 +7407,11 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7276
7407
  static getSettablePaths() {
7277
7408
  return {
7278
7409
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7279
- relativeFilePath: "hooks.json"
7410
+ relativeFilePath: RULESYNC_HOOKS_FILE_NAME,
7411
+ jsonc: {
7412
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
7413
+ relativeFilePath: RULESYNC_HOOKS_JSONC_FILE_NAME
7414
+ }
7280
7415
  };
7281
7416
  }
7282
7417
  validate() {
@@ -7292,16 +7427,23 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
7292
7427
  }
7293
7428
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
7294
7429
  const paths = RulesyncHooks.getSettablePaths();
7295
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7296
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} found.`);
7297
- const fileContent = await readFileContent(filePath);
7298
- return new RulesyncHooks({
7299
- outputRoot,
7430
+ const candidates = [paths.jsonc, {
7300
7431
  relativeDirPath: paths.relativeDirPath,
7301
- relativeFilePath: paths.relativeFilePath,
7302
- fileContent,
7303
- validate
7304
- });
7432
+ relativeFilePath: paths.relativeFilePath
7433
+ }];
7434
+ for (const candidate of candidates) {
7435
+ const filePath = (0, node_path.join)(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
7436
+ if (!await fileExists(filePath)) continue;
7437
+ const fileContent = await readFileContent(filePath);
7438
+ return new RulesyncHooks({
7439
+ outputRoot,
7440
+ relativeDirPath: candidate.relativeDirPath,
7441
+ relativeFilePath: candidate.relativeFilePath,
7442
+ fileContent,
7443
+ validate
7444
+ });
7445
+ }
7446
+ throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} or ${RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH} found.`);
7305
7447
  }
7306
7448
  getJson() {
7307
7449
  return this.json;
@@ -7588,7 +7730,8 @@ const AUGMENTCODE_CONVERTER_CONFIG = {
7588
7730
  noMatcherEvents: /* @__PURE__ */ new Set([
7589
7731
  "sessionStart",
7590
7732
  "sessionEnd",
7591
- "stop"
7733
+ "stop",
7734
+ "notification"
7592
7735
  ])
7593
7736
  };
7594
7737
  /**
@@ -8687,7 +8830,8 @@ const DEVIN_CONVERTER_CONFIG = {
8687
8830
  "sessionStart",
8688
8831
  "sessionEnd",
8689
8832
  "stop",
8690
- "beforeSubmitPrompt"
8833
+ "beforeSubmitPrompt",
8834
+ "postCompact"
8691
8835
  ])
8692
8836
  };
8693
8837
  /**
@@ -8996,6 +9140,167 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8996
9140
  }
8997
9141
  };
8998
9142
  //#endregion
9143
+ //#region src/constants/grokcli-paths.ts
9144
+ /**
9145
+ * Grok Build CLI (xAI) configuration-layout conventions.
9146
+ *
9147
+ * Single source of truth for where Grok Build expects its files. Grok Build
9148
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9149
+ * with project/global scopes resolved by the directory the CLI runs in
9150
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9151
+ *
9152
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9153
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9154
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9155
+ * @see https://docs.x.ai/build/overview
9156
+ */
9157
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9158
+ const GROKCLI_DIR = ".grok";
9159
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9160
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9161
+ /**
9162
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9163
+ * permission mode, and other settings all live here; permissions reuse the same
9164
+ * file name as MCP since Grok consolidates everything into one config.
9165
+ */
9166
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9167
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9168
+ const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
9169
+ /**
9170
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9171
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9172
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9173
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9174
+ * @see https://docs.x.ai/build/features/hooks
9175
+ */
9176
+ const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
9177
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9178
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9179
+ /**
9180
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9181
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9182
+ * (global), each a Markdown file with YAML frontmatter (verified via
9183
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9184
+ */
9185
+ const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
9186
+ /**
9187
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9188
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9189
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9190
+ * MCP/skills/subagents adapters).
9191
+ */
9192
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9193
+ //#endregion
9194
+ //#region src/features/hooks/grokcli-hooks.ts
9195
+ const GROKCLI_CONVERTER_CONFIG = {
9196
+ supportedEvents: GROKCLI_HOOK_EVENTS,
9197
+ canonicalToToolEventNames: CANONICAL_TO_GROKCLI_EVENT_NAMES,
9198
+ toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
9199
+ projectDirVar: "",
9200
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
9201
+ noMatcherEvents: /* @__PURE__ */ new Set([
9202
+ "sessionStart",
9203
+ "sessionEnd",
9204
+ "beforeSubmitPrompt",
9205
+ "stop",
9206
+ "stopFailure",
9207
+ "notification",
9208
+ "subagentStart",
9209
+ "subagentStop",
9210
+ "preCompact",
9211
+ "postCompact"
9212
+ ])
9213
+ };
9214
+ /**
9215
+ * Hooks generator for Grok CLI (xAI Grok Build).
9216
+ *
9217
+ * Grok Build adopts a Claude-Code-compatible lifecycle hooks model: each event
9218
+ * maps to an array of `{ matcher?, hooks: [{ type, command, timeout? }] }`
9219
+ * matcher groups under a top-level `hooks` key. rulesync writes all its hooks
9220
+ * into a single standalone `rulesync.json` file discovered from
9221
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global). The file
9222
+ * is dedicated to hooks and owned wholesale by rulesync, so it may be deleted
9223
+ * as an orphan.
9224
+ *
9225
+ * @see https://docs.x.ai/build/features/hooks
9226
+ */
9227
+ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
9228
+ constructor(params) {
9229
+ super({
9230
+ ...params,
9231
+ fileContent: params.fileContent ?? "{}"
9232
+ });
9233
+ }
9234
+ static getSettablePaths(_options = {}) {
9235
+ return {
9236
+ relativeDirPath: GROKCLI_HOOKS_DIR_PATH,
9237
+ relativeFilePath: GROKCLI_HOOKS_FILE_NAME
9238
+ };
9239
+ }
9240
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9241
+ const paths = GrokcliHooks.getSettablePaths({ global });
9242
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9243
+ return new GrokcliHooks({
9244
+ outputRoot,
9245
+ relativeDirPath: paths.relativeDirPath,
9246
+ relativeFilePath: paths.relativeFilePath,
9247
+ fileContent,
9248
+ validate
9249
+ });
9250
+ }
9251
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9252
+ const paths = GrokcliHooks.getSettablePaths({ global });
9253
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9254
+ const config = rulesyncHooks.getJson();
9255
+ const grokHooks = canonicalToToolHooks({
9256
+ config,
9257
+ toolOverrideHooks: config.grokcli?.hooks,
9258
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
9259
+ logger
9260
+ });
9261
+ await readOrInitializeFileContent(filePath, JSON.stringify({ hooks: {} }, null, 2));
9262
+ const fileContent = JSON.stringify({ hooks: grokHooks }, null, 2);
9263
+ return new GrokcliHooks({
9264
+ outputRoot,
9265
+ relativeDirPath: paths.relativeDirPath,
9266
+ relativeFilePath: paths.relativeFilePath,
9267
+ fileContent,
9268
+ validate
9269
+ });
9270
+ }
9271
+ toRulesyncHooks() {
9272
+ let parsed;
9273
+ try {
9274
+ parsed = JSON.parse(this.getFileContent());
9275
+ } catch (error) {
9276
+ throw new Error(`Failed to parse Grok hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9277
+ }
9278
+ const hooks = toolHooksToCanonical({
9279
+ hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
9280
+ converterConfig: GROKCLI_CONVERTER_CONFIG
9281
+ });
9282
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9283
+ version: 1,
9284
+ hooks
9285
+ }, null, 2) });
9286
+ }
9287
+ validate() {
9288
+ return {
9289
+ success: true,
9290
+ error: null
9291
+ };
9292
+ }
9293
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9294
+ return new GrokcliHooks({
9295
+ outputRoot,
9296
+ relativeDirPath,
9297
+ relativeFilePath,
9298
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9299
+ validate: false
9300
+ });
9301
+ }
9302
+ };
9303
+ //#endregion
8999
9304
  //#region src/features/hooks/hermesagent-hooks.ts
9000
9305
  /**
9001
9306
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -10202,9 +10507,9 @@ function reasonixHooksToCanonical(hooks) {
10202
10507
  * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
10203
10508
  * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
10204
10509
  * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
10205
- * The eight upstream events with a clean canonical equivalent are mapped:
10510
+ * All ten upstream events have a clean canonical equivalent and are mapped:
10206
10511
  * PreToolUse/PostToolUse/UserPromptSubmit/Stop plus SessionStart/SessionEnd/
10207
- * SubagentStop/PostLLMCall (see REASONIX_HOOK_EVENTS).
10512
+ * SubagentStop/PostLLMCall/Notification/PreCompact (see REASONIX_HOOK_EVENTS).
10208
10513
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
10209
10514
  */
10210
10515
  var ReasonixHooks = class ReasonixHooks extends ToolHooks {
@@ -10792,6 +11097,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
10792
11097
  supportedEvents: REASONIX_HOOK_EVENTS,
10793
11098
  supportedHookTypes: ["command"],
10794
11099
  supportsMatcher: true
11100
+ }],
11101
+ ["grokcli", {
11102
+ class: GrokcliHooks,
11103
+ meta: {
11104
+ supportsProject: true,
11105
+ supportsGlobal: true,
11106
+ supportsImport: true
11107
+ },
11108
+ supportedEvents: GROKCLI_HOOK_EVENTS,
11109
+ supportedHookTypes: ["command"],
11110
+ supportsMatcher: true
10795
11111
  }]
10796
11112
  ]);
10797
11113
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -12135,15 +12451,51 @@ const RulesyncMcpServerSchema = zod_mini.z.extend(McpServerSchema, {
12135
12451
  exposed: zod_mini.z.optional(zod_mini.z.boolean())
12136
12452
  });
12137
12453
  const RulesyncMcpConfigSchema = zod_mini.z.object({ mcpServers: zod_mini.z.record(zod_mini.z.string(), RulesyncMcpServerSchema) });
12454
+ /**
12455
+ * Tool-scoped MCP block: servers that apply only to one tool. A named entry
12456
+ * replaces/adds the same-named shared server wholesale for that tool; `null`
12457
+ * removes the shared server for that tool. Mirrors `{toolname}.hooks` in
12458
+ * `.rulesync/hooks.json` and `{toolname}.permission` in
12459
+ * `.rulesync/permissions.json`.
12460
+ */
12461
+ const toolScopedMcpSchema = zod_mini.z.looseObject({ mcpServers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.nullable(RulesyncMcpServerSchema))) });
12138
12462
  const RulesyncMcpFileSchema = zod_mini.z.looseObject({
12139
12463
  $schema: zod_mini.z.optional(zod_mini.z.string()),
12140
- ...RulesyncMcpConfigSchema.shape
12464
+ ...RulesyncMcpConfigSchema.shape,
12465
+ amp: zod_mini.z.optional(toolScopedMcpSchema),
12466
+ "antigravity-cli": zod_mini.z.optional(toolScopedMcpSchema),
12467
+ "antigravity-ide": zod_mini.z.optional(toolScopedMcpSchema),
12468
+ augmentcode: zod_mini.z.optional(toolScopedMcpSchema),
12469
+ claudecode: zod_mini.z.optional(toolScopedMcpSchema),
12470
+ cline: zod_mini.z.optional(toolScopedMcpSchema),
12471
+ codexcli: zod_mini.z.optional(toolScopedMcpSchema),
12472
+ copilot: zod_mini.z.optional(toolScopedMcpSchema),
12473
+ copilotcli: zod_mini.z.optional(toolScopedMcpSchema),
12474
+ cursor: zod_mini.z.optional(toolScopedMcpSchema),
12475
+ deepagents: zod_mini.z.optional(toolScopedMcpSchema),
12476
+ devin: zod_mini.z.optional(toolScopedMcpSchema),
12477
+ factorydroid: zod_mini.z.optional(toolScopedMcpSchema),
12478
+ goose: zod_mini.z.optional(toolScopedMcpSchema),
12479
+ grokcli: zod_mini.z.optional(toolScopedMcpSchema),
12480
+ hermesagent: zod_mini.z.optional(toolScopedMcpSchema),
12481
+ junie: zod_mini.z.optional(toolScopedMcpSchema),
12482
+ kilo: zod_mini.z.optional(toolScopedMcpSchema),
12483
+ kiro: zod_mini.z.optional(toolScopedMcpSchema),
12484
+ opencode: zod_mini.z.optional(toolScopedMcpSchema),
12485
+ qwencode: zod_mini.z.optional(toolScopedMcpSchema),
12486
+ reasonix: zod_mini.z.optional(toolScopedMcpSchema),
12487
+ roo: zod_mini.z.optional(toolScopedMcpSchema),
12488
+ rovodev: zod_mini.z.optional(toolScopedMcpSchema),
12489
+ takt: zod_mini.z.optional(toolScopedMcpSchema),
12490
+ vibe: zod_mini.z.optional(toolScopedMcpSchema),
12491
+ warp: zod_mini.z.optional(toolScopedMcpSchema),
12492
+ zed: zod_mini.z.optional(toolScopedMcpSchema)
12141
12493
  });
12142
12494
  var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12143
12495
  json;
12144
12496
  constructor(params) {
12145
12497
  super(params);
12146
- this.json = JSON.parse(this.fileContent);
12498
+ this.json = parseJsonc$6(this.fileContent);
12147
12499
  if (params.validate) {
12148
12500
  const result = this.validate();
12149
12501
  if (!result.success) throw result.error;
@@ -12153,7 +12505,11 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12153
12505
  return {
12154
12506
  recommended: {
12155
12507
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12156
- relativeFilePath: "mcp.json"
12508
+ relativeFilePath: RULESYNC_MCP_FILE_NAME
12509
+ },
12510
+ jsonc: {
12511
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
12512
+ relativeFilePath: RULESYNC_MCP_JSONC_FILE_NAME
12157
12513
  },
12158
12514
  legacy: {
12159
12515
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
@@ -12175,7 +12531,18 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12175
12531
  static async fromFile({ outputRoot = process.cwd(), validate = true, logger }) {
12176
12532
  const paths = this.getSettablePaths();
12177
12533
  const recommendedPath = (0, node_path.join)(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
12534
+ const jsoncPath = (0, node_path.join)(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
12178
12535
  const legacyPath = (0, node_path.join)(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
12536
+ if (await fileExists(jsoncPath)) {
12537
+ const fileContent = await readFileContent(jsoncPath);
12538
+ return new RulesyncMcp({
12539
+ outputRoot,
12540
+ relativeDirPath: paths.jsonc.relativeDirPath,
12541
+ relativeFilePath: paths.jsonc.relativeFilePath,
12542
+ fileContent,
12543
+ validate
12544
+ });
12545
+ }
12179
12546
  if (await fileExists(recommendedPath)) {
12180
12547
  const fileContent = await readFileContent(recommendedPath);
12181
12548
  return new RulesyncMcp({
@@ -12235,7 +12602,141 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
12235
12602
  getJson() {
12236
12603
  return this.json;
12237
12604
  }
12238
- };
12605
+ /**
12606
+ * Build the effective MCP config for one tool target:
12607
+ *
12608
+ * 1. Filter shared servers by the DEPRECATED per-server `targets` field
12609
+ * (missing/`["*"]` means every tool). A deprecation warning points at
12610
+ * the tool-scoped blocks that replace it.
12611
+ * 2. Overlay the tool-scoped `{toolname}.mcpServers` block(s): a named
12612
+ * entry replaces/adds the shared server wholesale for this tool; `null`
12613
+ * removes it.
12614
+ *
12615
+ * Targets that share one output file resolve identically so the shared
12616
+ * file's content never depends on which of them generates last — see
12617
+ * `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
12618
+ * and the Antigravity pair).
12619
+ *
12620
+ * Returns the same instance when neither mechanism is used.
12621
+ */
12622
+ forTarget({ toolTarget, logger }) {
12623
+ const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
12624
+ const json = this.json;
12625
+ const sharedServers = this.json.mcpServers ?? {};
12626
+ const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
12627
+ if (serverNamesWithTargets.length > 0) this.warnTargetsDeprecationOnce({
12628
+ serverNamesWithTargets,
12629
+ logger
12630
+ });
12631
+ for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
12632
+ if (!isRecord(json[ignoredKey])) continue;
12633
+ this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
12634
+ }
12635
+ const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
12636
+ if (serverNamesWithTargets.length === 0 && toolBlockKeys.length === 0) return this;
12637
+ const effectiveServers = Object.fromEntries(Object.entries(sharedServers).filter(([, serverConfig]) => {
12638
+ const targets = serverConfig.targets;
12639
+ if (targets === void 0) return true;
12640
+ return targets.some((target) => target === "*" || acceptedTargetNames.has(target));
12641
+ }));
12642
+ for (const blockKey of blockKeys) {
12643
+ const toolBlock = json[blockKey];
12644
+ const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
12645
+ for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
12646
+ if (isPrototypePollutionKey(serverName)) continue;
12647
+ if (serverConfig === null) delete effectiveServers[serverName];
12648
+ else effectiveServers[serverName] = serverConfig;
12649
+ }
12650
+ }
12651
+ const rest = Object.fromEntries(Object.entries(json).filter(([key]) => !MCP_TOOL_BLOCK_KEYS.has(key)));
12652
+ return new RulesyncMcp({
12653
+ outputRoot: this.outputRoot,
12654
+ relativeDirPath: this.relativeDirPath,
12655
+ relativeFilePath: this.relativeFilePath,
12656
+ fileContent: JSON.stringify({
12657
+ ...rest,
12658
+ mcpServers: effectiveServers
12659
+ }, null, 2)
12660
+ });
12661
+ }
12662
+ /**
12663
+ * The deprecation warning would otherwise repeat once per generated tool
12664
+ * target (a full `--targets "*"` run creates one RulesyncMcp per target),
12665
+ * so it is deduplicated per source file path.
12666
+ */
12667
+ warnTargetsDeprecationOnce({ serverNamesWithTargets, logger }) {
12668
+ this.warnOncePerFile("targets-deprecation", `The per-server "targets" field in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is deprecated (servers: ${serverNamesWithTargets.join(", ")}). Author tool-scoped "{toolname}.mcpServers" blocks instead.`, logger);
12669
+ }
12670
+ warnOncePerFile(kind, message, logger) {
12671
+ const dedupeKey = `${this.getFilePath()}#${kind}`;
12672
+ if (warnedOnceKeys.has(dedupeKey)) return;
12673
+ warnedOnceKeys.add(dedupeKey);
12674
+ logger?.warn(message);
12675
+ }
12676
+ };
12677
+ /**
12678
+ * All keys that may hold a tool-scoped `{toolname}.mcpServers` block. Derived
12679
+ * from the MCP processor's target tuple so a newly added MCP-capable tool is
12680
+ * covered automatically.
12681
+ */
12682
+ const MCP_TOOL_BLOCK_KEYS = new Set(mcpProcessorToolTargetTuple);
12683
+ /**
12684
+ * Targets whose `{toolname}.mcpServers` block key is ALWAYS another target's
12685
+ * key (at every scope): `claudecode-legacy` is a deprecated alias of
12686
+ * `claudecode`, and the Kiro IDE/CLI targets share the `kiro` block because
12687
+ * all three write the same `.kiro/settings/mcp.json` at both scopes. Blocks
12688
+ * authored under these source names are never read (a warning is emitted).
12689
+ */
12690
+ const MCP_BLOCK_KEY_ALIASES = {
12691
+ "claudecode-legacy": "claudecode",
12692
+ "kiro-cli": "kiro",
12693
+ "kiro-ide": "kiro"
12694
+ };
12695
+ const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
12696
+ /**
12697
+ * Resolve which tool-scoped blocks a target reads and which deprecated
12698
+ * `targets` names match it. Targets that share one output file must resolve
12699
+ * identically, otherwise the shared file's content would depend on which
12700
+ * target happened to generate last:
12701
+ *
12702
+ * - `claudecode-legacy` aliases `claudecode`; the Kiro trio shares `kiro`
12703
+ * (same output file at both scopes).
12704
+ * - `antigravity-ide` / `antigravity-cli` share their output file at BOTH
12705
+ * scopes — `.agents/mcp_config.json` (project) and
12706
+ * `~/.gemini/config/mcp_config.json` (global; both global subdirs are
12707
+ * `config`) — so both targets always apply both blocks in a fixed order
12708
+ * (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
12709
+ * per server on conflict).
12710
+ */
12711
+ function resolveMcpTarget({ toolTarget }) {
12712
+ if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
12713
+ blockKeys: ["claudecode"],
12714
+ acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
12715
+ };
12716
+ if (toolTarget === "kiro" || toolTarget === "kiro-cli" || toolTarget === "kiro-ide") return {
12717
+ blockKeys: ["kiro"],
12718
+ acceptedTargetNames: /* @__PURE__ */ new Set([
12719
+ "kiro",
12720
+ "kiro-cli",
12721
+ "kiro-ide"
12722
+ ])
12723
+ };
12724
+ if (toolTarget === "antigravity-ide" || toolTarget === "antigravity-cli") return {
12725
+ blockKeys: ["antigravity-ide", "antigravity-cli"],
12726
+ acceptedTargetNames: /* @__PURE__ */ new Set(["antigravity-ide", "antigravity-cli"])
12727
+ };
12728
+ return {
12729
+ blockKeys: [toolTarget],
12730
+ acceptedTargetNames: /* @__PURE__ */ new Set([toolTarget])
12731
+ };
12732
+ }
12733
+ /**
12734
+ * Deduplication set for once-per-source-file warnings, keyed by
12735
+ * `<absolute file path>#<warning kind>`. Never cleared: rulesync CLI runs
12736
+ * are one-shot processes, and in a long-lived embedding (the rulesync MCP
12737
+ * server) repeating the same warning per generate would only add noise.
12738
+ */
12739
+ const warnedOnceKeys = /* @__PURE__ */ new Set();
12239
12740
  //#endregion
12240
12741
  //#region src/features/mcp/tool-mcp.ts
12241
12742
  var ToolMcp = class extends ToolFile {
@@ -13047,7 +13548,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
13047
13548
  ...rest,
13048
13549
  validate: false
13049
13550
  });
13050
- this.toml = smol_toml.parse(this.fileContent);
13551
+ let toml;
13552
+ try {
13553
+ toml = smol_toml.parse(this.fileContent);
13554
+ } catch (error) {
13555
+ throw new Error(`Failed to parse Codex CLI config at ${this.getFilePath()}: ${formatError(error)}`, { cause: error });
13556
+ }
13557
+ this.toml = toml;
13051
13558
  if (rest.validate) {
13052
13559
  const result = this.validate();
13053
13560
  if (!result.success) throw result.error;
@@ -13083,7 +13590,12 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
13083
13590
  const paths = this.getSettablePaths({ global });
13084
13591
  const configTomlFilePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13085
13592
  const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13086
- const configToml = smol_toml.parse(configTomlFileContent || smol_toml.stringify({}));
13593
+ let configToml;
13594
+ try {
13595
+ configToml = smol_toml.parse(configTomlFileContent || smol_toml.stringify({}));
13596
+ } catch (error) {
13597
+ throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
13598
+ }
13087
13599
  const strippedMcpServers = rulesyncMcp.getMcpServers();
13088
13600
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
13089
13601
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
@@ -14057,47 +14569,6 @@ var GooseMcp = class GooseMcp extends ToolMcp {
14057
14569
  }
14058
14570
  };
14059
14571
  //#endregion
14060
- //#region src/constants/grokcli-paths.ts
14061
- /**
14062
- * Grok Build CLI (xAI) configuration-layout conventions.
14063
- *
14064
- * Single source of truth for where Grok Build expects its files. Grok Build
14065
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
14066
- * with project/global scopes resolved by the directory the CLI runs in
14067
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
14068
- *
14069
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
14070
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
14071
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
14072
- * @see https://docs.x.ai/build/overview
14073
- */
14074
- /** Root directory for Grok Build configuration, relative to the scope root. */
14075
- const GROKCLI_DIR = ".grok";
14076
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
14077
- const GROKCLI_MCP_FILE_NAME = "config.toml";
14078
- /**
14079
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
14080
- * permission mode, and other settings all live here; permissions reuse the same
14081
- * file name as MCP since Grok consolidates everything into one config.
14082
- */
14083
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
14084
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
14085
- const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
14086
- /**
14087
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
14088
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
14089
- * (global), each a Markdown file with YAML frontmatter (verified via
14090
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
14091
- */
14092
- const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
14093
- /**
14094
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
14095
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
14096
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
14097
- * MCP/skills/subagents adapters).
14098
- */
14099
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
14100
- //#endregion
14101
14572
  //#region src/features/mcp/grokcli-mcp.ts
14102
14573
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
14103
14574
  /**
@@ -14257,6 +14728,54 @@ function resolveHermesTimeout(config) {
14257
14728
  if (typeof config.networkTimeout === "number") return config.networkTimeout;
14258
14729
  }
14259
14730
  /**
14731
+ * Copies the advanced Hermes-recognized per-server fields that have no canonical
14732
+ * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
14733
+ * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
14734
+ * `connect_timeout` (seconds), and `supports_parallel_tool_calls` — verbatim
14735
+ * from `source` to `target`. Field names are identical on both sides (the
14736
+ * canonical `McpServerSchema` is a `looseObject`), so this serves export and
14737
+ * import alike. See the Hermes mcp-config-reference.
14738
+ */
14739
+ function copyHermesAdvancedFields(source, target) {
14740
+ if (typeof source.auth === "string") target.auth = source.auth;
14741
+ if (typeof source.client_cert === "string" || isStringArray(source.client_cert)) target.client_cert = source.client_cert;
14742
+ if (typeof source.client_key === "string") target.client_key = source.client_key;
14743
+ if (typeof source.connect_timeout === "number") target.connect_timeout = source.connect_timeout;
14744
+ if (typeof source.supports_parallel_tool_calls === "boolean") target.supports_parallel_tool_calls = source.supports_parallel_tool_calls;
14745
+ }
14746
+ /**
14747
+ * Builds Hermes's per-server `tools` block from a canonical server config. The
14748
+ * canonical `enabledTools`/`disabledTools` arrays become `include`/`exclude`,
14749
+ * and the boolean `promptsEnabled`/`resourcesEnabled` toggles become Hermes's
14750
+ * `prompts`/`resources` capability flags. Returns an empty object when the
14751
+ * server has no tool scoping (the caller omits the block in that case).
14752
+ *
14753
+ * Note: `promptsEnabled`/`resourcesEnabled` are canonical top-level keys rather
14754
+ * than a nested canonical `tools` object, because canonical `McpServerSchema.tools`
14755
+ * is reserved as a `string[]` (used by other tools) — reusing it for an object
14756
+ * would fail validation on the next `generate`.
14757
+ */
14758
+ function buildHermesToolsBlock(config) {
14759
+ const tools = {};
14760
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14761
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14762
+ if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
14763
+ if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
14764
+ return tools;
14765
+ }
14766
+ /**
14767
+ * Applies a Hermes per-server `tools` block back onto a canonical server config
14768
+ * (inverse of {@link buildHermesToolsBlock}): `include`/`exclude` become
14769
+ * `enabledTools`/`disabledTools`, and `prompts`/`resources` become the boolean
14770
+ * `promptsEnabled`/`resourcesEnabled` top-level toggles.
14771
+ */
14772
+ function applyHermesToolsBlock(hermesTools, server) {
14773
+ if (isStringArray(hermesTools.include)) server.enabledTools = hermesTools.include;
14774
+ if (isStringArray(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
14775
+ if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
14776
+ if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
14777
+ }
14778
+ /**
14260
14779
  * Converts a single rulesync canonical MCP server into a Hermes `mcp_servers:` entry.
14261
14780
  *
14262
14781
  * Hermes is close to the MCP spec but not identical: `command` must be a single
@@ -14290,9 +14809,8 @@ function convertServerToHermes(config) {
14290
14809
  if (config.disabled === true) out.enabled = false;
14291
14810
  const timeout = resolveHermesTimeout(config);
14292
14811
  if (timeout !== void 0) out.timeout = timeout;
14293
- const tools = {};
14294
- if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14295
- if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14812
+ copyHermesAdvancedFields(config, out);
14813
+ const tools = buildHermesToolsBlock(config);
14296
14814
  if (Object.keys(tools).length > 0) out.tools = tools;
14297
14815
  return out;
14298
14816
  }
@@ -14335,10 +14853,8 @@ function convertFromHermesFormat(mcpServers) {
14335
14853
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
14336
14854
  if (config.enabled === false) server.disabled = true;
14337
14855
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
14338
- if (isRecord(config.tools)) {
14339
- if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
14340
- if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
14341
- }
14856
+ copyHermesAdvancedFields(config, server);
14857
+ if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
14342
14858
  result[name] = server;
14343
14859
  }
14344
14860
  return result;
@@ -16470,10 +16986,14 @@ var McpProcessor = class extends FeatureProcessor {
16470
16986
  if (!rulesyncMcp) throw new Error(`No ${RULESYNC_MCP_RELATIVE_FILE_PATH} found.`);
16471
16987
  const factory = this.getFactory(this.toolTarget);
16472
16988
  return await Promise.all([rulesyncMcp].map(async (mcp) => {
16989
+ const targetedRulesyncMcp = mcp.forTarget({
16990
+ toolTarget: this.toolTarget,
16991
+ logger: this.logger
16992
+ });
16473
16993
  const fieldsToStrip = [];
16474
16994
  if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
16475
16995
  if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
16476
- const filteredRulesyncMcp = mcp.stripMcpServerFields(fieldsToStrip);
16996
+ const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
16477
16997
  return await factory.class.fromRulesyncMcp({
16478
16998
  outputRoot: this.outputRoot,
16479
16999
  rulesyncMcp: filteredRulesyncMcp,
@@ -16523,6 +17043,25 @@ const PermissionActionSchema = zod_mini.z.enum([
16523
17043
  */
16524
17044
  const PermissionRulesSchema = zod_mini.z.record(zod_mini.z.string(), PermissionActionSchema);
16525
17045
  /**
17046
+ * Tool-scoped canonical `permission` block: the same shape as the shared
17047
+ * top-level `permission` record, but applied only to the tool whose override
17048
+ * key it appears under. During generation the categories placed here are
17049
+ * merged over the shared `permission` block per category (the tool-scoped
17050
+ * category replaces the shared one wholesale), mirroring how
17051
+ * `.rulesync/hooks.json` merges `{toolname}.hooks` per event.
17052
+ *
17053
+ * @example
17054
+ * { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
17055
+ */
17056
+ const ToolScopedPermissionSchema = zod_mini.z.record(zod_mini.z.string(), PermissionRulesSchema);
17057
+ /**
17058
+ * Generic tool-scoped override block for tools that have no tool-specific
17059
+ * override keys of their own; it carries only the canonical tool-scoped
17060
+ * `permission` block. Kept `looseObject` so future tool-specific keys can be
17061
+ * added without a schema break.
17062
+ */
17063
+ const CanonicalPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(ToolScopedPermissionSchema) });
17064
+ /**
16526
17065
  * OpenCode-specific permission value. Unlike the shared canonical block, which
16527
17066
  * only accepts a pattern-to-action map, OpenCode also allows a bare action
16528
17067
  * string that applies to the whole category (e.g. `"external_directory": "deny"`).
@@ -16552,7 +17091,7 @@ const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: z
16552
17091
  * @example
16553
17092
  * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
16554
17093
  */
16555
- const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
17094
+ const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(ToolScopedPermissionSchema) });
16556
17095
  /**
16557
17096
  * Tool-scoped override block for Cline. Cline's `command-permissions.json`
16558
17097
  * carries a single global `allowRedirects` boolean (gates shell redirection
@@ -16564,7 +17103,10 @@ const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
16564
17103
  * @example
16565
17104
  * { "allowRedirects": true }
16566
17105
  */
16567
- const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean()) });
17106
+ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({
17107
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17108
+ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean())
17109
+ });
16568
17110
  /**
16569
17111
  * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
16570
17112
  * object is a free-form record with tool-specific keys that have no canonical
@@ -16594,7 +17136,10 @@ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
16594
17136
  * @example
16595
17137
  * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16596
17138
  */
16597
- const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})) });
17139
+ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
17140
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17141
+ permissions: zod_mini.z.optional(zod_mini.z.looseObject({}))
17142
+ });
16598
17143
  /**
16599
17144
  * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16600
17145
  * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
@@ -16626,6 +17171,7 @@ const VibePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_m
16626
17171
  * { "approvalMode": "auto-review" }
16627
17172
  */
16628
17173
  const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
17174
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16629
17175
  approvalMode: zod_mini.z.optional(zod_mini.z.string()),
16630
17176
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
16631
17177
  });
@@ -16633,18 +17179,28 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
16633
17179
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16634
17180
  * autonomy/sandbox controls with no canonical permission category — under
16635
17181
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16636
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16637
- * placed here are merged into the matching `settings.json` group and emitted
16638
- * only for Qwen, while the shared `permission` block continues to drive the
16639
- * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16640
- * passthrough) so any current or future `tools`/`security` key can be authored.
17182
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). It also
17183
+ * exposes `permissions.autoMode` (the Auto Mode classifier config:
17184
+ * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` see
17185
+ * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
17186
+ * likewise has no canonical category. Fields placed here are merged into the
17187
+ * matching `settings.json` group and emitted only for Qwen, while the shared
17188
+ * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
17189
+ * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
17190
+ * `tools`/`security`/`autoMode` key can be authored.
16641
17191
  *
16642
17192
  * @example
16643
- * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
17193
+ * {
17194
+ * "tools": { "approvalMode": "auto-edit" },
17195
+ * "security": { "folderTrust": { "enabled": true } },
17196
+ * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
17197
+ * }
16644
17198
  */
16645
17199
  const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
17200
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16646
17201
  tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
16647
- security: zod_mini.z.optional(zod_mini.z.looseObject({}))
17202
+ security: zod_mini.z.optional(zod_mini.z.looseObject({})),
17203
+ autoMode: zod_mini.z.optional(zod_mini.z.looseObject({}))
16648
17204
  });
16649
17205
  /**
16650
17206
  * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
@@ -16663,6 +17219,7 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
16663
17219
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16664
17220
  */
16665
17221
  const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
17222
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16666
17223
  sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
16667
17224
  agent: zod_mini.z.optional(zod_mini.z.looseObject({}))
16668
17225
  });
@@ -16671,16 +17228,21 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
16671
17228
  * exposes security controls with no canonical per-command allow/ask/deny slot —
16672
17229
  * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16673
17230
  * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16674
- * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
17231
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
17232
+ * `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
16675
17233
  * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16676
- * `interactionMode`). Fields placed here are merged into `settings.json` and
17234
+ * `subagentAutonomyLevel`, `interactionMode`). Fields placed here are merged
17235
+ * into `settings.json` and
16677
17236
  * emitted only for Factory Droid, while the shared `permission` block continues
16678
17237
  * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16679
17238
  *
16680
17239
  * @example
16681
17240
  * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16682
17241
  */
16683
- const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) });
17242
+ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({
17243
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17244
+ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17245
+ });
16684
17246
  /**
16685
17247
  * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16686
17248
  * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
@@ -16697,6 +17259,7 @@ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBl
16697
17259
  * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16698
17260
  */
16699
17261
  const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
17262
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16700
17263
  agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.string()),
16701
17264
  agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16702
17265
  agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean())
@@ -16714,6 +17277,7 @@ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
16714
17277
  * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16715
17278
  */
16716
17279
  const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
17280
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16717
17281
  allowReadonlyCommands: zod_mini.z.optional(zod_mini.z.boolean()),
16718
17282
  defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
16719
17283
  });
@@ -16741,6 +17305,7 @@ const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
16741
17305
  * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16742
17306
  */
16743
17307
  const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
17308
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16744
17309
  step_permission_overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16745
17310
  provider_options: zod_mini.z.optional(zod_mini.z.looseObject({}))
16746
17311
  });
@@ -16768,6 +17333,7 @@ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
16768
17333
  * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16769
17334
  */
16770
17335
  const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
17336
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16771
17337
  permissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
16772
17338
  tool: zod_mini.z.string(),
16773
17339
  action: zod_mini.z.string()
@@ -16796,6 +17362,7 @@ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
16796
17362
  * { "toolPermission": "strict", "enableTerminalSandbox": true }
16797
17363
  */
16798
17364
  const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
17365
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
16799
17366
  toolPermission: zod_mini.z.optional(zod_mini.z.string()),
16800
17367
  enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
16801
17368
  });
@@ -16821,10 +17388,13 @@ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
16821
17388
  * "permission": { "type": "webhook-policy", "webhookUrl": "https://api.example.com/validate" } },
16822
17389
  * { "toolName": "view", "eventType": "tool-response", "permission": { "type": "allow" } } ] }
16823
17390
  */
16824
- const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({ toolPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
16825
- toolName: zod_mini.z.string(),
16826
- permission: zod_mini.z.looseObject({ type: zod_mini.z.string() })
16827
- }))) });
17391
+ const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({
17392
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17393
+ toolPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
17394
+ toolName: zod_mini.z.string(),
17395
+ permission: zod_mini.z.looseObject({ type: zod_mini.z.string() })
17396
+ })))
17397
+ });
16828
17398
  /**
16829
17399
  * Tool-scoped override block for Kiro. Kiro's agent config (`.kiro/agents/<name>.json`)
16830
17400
  * exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny
@@ -16850,21 +17420,59 @@ const AugmentcodePermissionsOverrideSchema = zod_mini.z.looseObject({ toolPermis
16850
17420
  * "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] },
16851
17421
  * "web_fetch": { "trusted": [".*github\\.com.*"] } } }
16852
17422
  */
16853
- const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zod_mini.z.optional(zod_mini.z.looseObject({
16854
- shell: zod_mini.z.optional(zod_mini.z.looseObject({
16855
- autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean()),
16856
- denyByDefault: zod_mini.z.optional(zod_mini.z.boolean())
16857
- })),
16858
- aws: zod_mini.z.optional(zod_mini.z.looseObject({
16859
- allowedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16860
- deniedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16861
- autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean())
16862
- })),
16863
- web_fetch: zod_mini.z.optional(zod_mini.z.looseObject({
16864
- trusted: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16865
- blocked: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17423
+ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({
17424
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17425
+ toolsSettings: zod_mini.z.optional(zod_mini.z.looseObject({
17426
+ shell: zod_mini.z.optional(zod_mini.z.looseObject({
17427
+ autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean()),
17428
+ denyByDefault: zod_mini.z.optional(zod_mini.z.boolean())
17429
+ })),
17430
+ aws: zod_mini.z.optional(zod_mini.z.looseObject({
17431
+ allowedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17432
+ deniedServices: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17433
+ autoAllowReadonly: zod_mini.z.optional(zod_mini.z.boolean())
17434
+ })),
17435
+ web_fetch: zod_mini.z.optional(zod_mini.z.looseObject({
17436
+ trusted: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
17437
+ blocked: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
17438
+ }))
16866
17439
  }))
16867
- })) });
17440
+ });
17441
+ /**
17442
+ * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17443
+ * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
17444
+ * Codex still accepts, so it is included so existing configs round-trip. The
17445
+ * granular table form (`{ granular = { … } }`) is modeled separately in the
17446
+ * override union.
17447
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17448
+ */
17449
+ const CodexApprovalPolicySchema = zod_mini.z.enum([
17450
+ "untrusted",
17451
+ "on-request",
17452
+ "on-failure",
17453
+ "never"
17454
+ ]);
17455
+ /**
17456
+ * Codex CLI's classic sandbox mode. Serialized as a kebab-case string in
17457
+ * `.codex/config.toml`.
17458
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17459
+ */
17460
+ const CodexSandboxModeSchema = zod_mini.z.enum([
17461
+ "read-only",
17462
+ "workspace-write",
17463
+ "danger-full-access"
17464
+ ]);
17465
+ /**
17466
+ * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
17467
+ * value Codex still accepts for backward compatibility, so it is included so
17468
+ * existing configs round-trip through the rulesync model.
17469
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17470
+ */
17471
+ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17472
+ "user",
17473
+ "auto_review",
17474
+ "guardian_subagent"
17475
+ ]);
16868
17476
  /**
16869
17477
  * Codex CLI-scoped permission override.
16870
17478
  *
@@ -16874,15 +17482,16 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zo
16874
17482
  * override whose fields are written verbatim as top-level `.codex/config.toml`
16875
17483
  * keys (the override wins per key; existing sibling keys the user set directly
16876
17484
  * are preserved):
16877
- * - `approval_policy` — `untrusted` | `on-request` | `never`, or a
16878
- * `{ granular = { … } }` table (kept verbatim; the granular schema has
16879
- * required fields that are brittle to model as typed keys).
17485
+ * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
17486
+ * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
17487
+ * schema has required fields that are brittle to model as typed keys).
16880
17488
  * - `sandbox_mode` — `read-only` | `workspace-write` | `danger-full-access`,
16881
17489
  * with the sibling `sandbox_workspace_write` table (`network_access`,
16882
17490
  * `writable_roots`, …).
16883
17491
  * - `apps` — per-app tool gating (`apps.<id>.tools.<tool>.approval_mode` /
16884
17492
  * `.enabled`, `apps.<id>.default_tools_approval_mode`).
16885
- * - `approvals_reviewer` — the reviewer-approval surface.
17493
+ * - `approvals_reviewer` — the reviewer-approval surface (`user` | `auto_review`
17494
+ * | `guardian_subagent`), or a table for the richer reviewer config.
16886
17495
  *
16887
17496
  * Two surfaces are deliberately NOT authorable here so the override can never
16888
17497
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -16900,11 +17509,12 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zo
16900
17509
  * "sandbox_workspace_write": { "network_access": true } }
16901
17510
  */
16902
17511
  const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
16903
- approval_policy: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
16904
- sandbox_mode: zod_mini.z.optional(zod_mini.z.string()),
17512
+ permission: zod_mini.z.optional(ToolScopedPermissionSchema),
17513
+ approval_policy: zod_mini.z.optional(zod_mini.z.union([CodexApprovalPolicySchema, zod_mini.z.looseObject({})])),
17514
+ sandbox_mode: zod_mini.z.optional(CodexSandboxModeSchema),
16905
17515
  sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
16906
17516
  apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
16907
- approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})]))
17517
+ approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})]))
16908
17518
  });
16909
17519
  /**
16910
17520
  * Permissions configuration.
@@ -16918,6 +17528,15 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
16918
17528
  * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
16919
17529
  * block and ignores them.
16920
17530
  *
17531
+ * Additionally, every permissions-capable tool accepts a canonical tool-scoped
17532
+ * `permission` block under its override key (`{toolname}.permission`, same
17533
+ * shape as the shared `permission` record). Its categories apply only to that
17534
+ * tool, merged over the shared block per category — see
17535
+ * `RulesyncPermissions.forTarget`. `kiro-cli`/`kiro-ide` alias to the `kiro`
17536
+ * key and `hermesagent` to `hermes` (matching the shared output file each
17537
+ * writes). OpenCode/Kilo/Vibe keep their existing tool-native `permission`
17538
+ * override semantics (handled by their translators, not the central merge).
17539
+ *
16921
17540
  * @example
16922
17541
  * {
16923
17542
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
@@ -16943,7 +17562,13 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
16943
17562
  "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema),
16944
17563
  augmentcode: zod_mini.z.optional(AugmentcodePermissionsOverrideSchema),
16945
17564
  kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
16946
- codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema)
17565
+ codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
17566
+ "antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17567
+ devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17568
+ goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17569
+ grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17570
+ rovodev: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
17571
+ zed: zod_mini.z.optional(CanonicalPermissionsOverrideSchema)
16947
17572
  });
16948
17573
  /**
16949
17574
  * Full permissions file schema including optional $schema field.
@@ -16958,7 +17583,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16958
17583
  json;
16959
17584
  constructor(params) {
16960
17585
  super({ ...params });
16961
- this.json = JSON.parse(this.fileContent);
17586
+ this.json = parseJsonc$6(this.fileContent);
16962
17587
  if (params.validate) {
16963
17588
  const result = this.validate();
16964
17589
  if (!result.success) throw result.error;
@@ -16967,7 +17592,11 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16967
17592
  static getSettablePaths() {
16968
17593
  return {
16969
17594
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
16970
- relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME
17595
+ relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
17596
+ jsonc: {
17597
+ relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
17598
+ relativeFilePath: RULESYNC_PERMISSIONS_JSONC_FILE_NAME
17599
+ }
16971
17600
  };
16972
17601
  }
16973
17602
  validate() {
@@ -16983,20 +17612,85 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
16983
17612
  }
16984
17613
  static async fromFile({ outputRoot = process.cwd(), validate = true }) {
16985
17614
  const paths = RulesyncPermissions.getSettablePaths();
16986
- const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16987
- if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
16988
- const fileContent = await readFileContent(filePath);
16989
- return new RulesyncPermissions({
16990
- outputRoot,
17615
+ const candidates = [paths.jsonc, {
16991
17616
  relativeDirPath: paths.relativeDirPath,
16992
- relativeFilePath: paths.relativeFilePath,
16993
- fileContent,
16994
- validate
16995
- });
17617
+ relativeFilePath: paths.relativeFilePath
17618
+ }];
17619
+ for (const candidate of candidates) {
17620
+ const filePath = (0, node_path.join)(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
17621
+ if (!await fileExists(filePath)) continue;
17622
+ const fileContent = await readFileContent(filePath);
17623
+ return new RulesyncPermissions({
17624
+ outputRoot,
17625
+ relativeDirPath: candidate.relativeDirPath,
17626
+ relativeFilePath: candidate.relativeFilePath,
17627
+ fileContent,
17628
+ validate
17629
+ });
17630
+ }
17631
+ throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} or ${RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH} found.`);
16996
17632
  }
16997
17633
  getJson() {
16998
17634
  return this.json;
16999
17635
  }
17636
+ /**
17637
+ * Build the effective permissions config for one tool target by merging the
17638
+ * tool-scoped `{toolname}.permission` block over the shared `permission`
17639
+ * block, per category (the tool-scoped category replaces the shared one
17640
+ * wholesale — mirroring how `{toolname}.hooks` merges per event in
17641
+ * `.rulesync/hooks.json`). The consumed `permission` key is stripped from
17642
+ * the override block so verbatim-passthrough translators (e.g. the Hermes
17643
+ * deep merge or the Codex CLI top-level key whitelist) never see it.
17644
+ *
17645
+ * Returns the same instance when the target has no tool-scoped canonical
17646
+ * `permission` block, or when the target's translator natively consumes its
17647
+ * own `permission` override shape (OpenCode / Kilo / Vibe).
17648
+ */
17649
+ forTarget({ toolTarget, logger }) {
17650
+ if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
17651
+ const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
17652
+ const json = this.json;
17653
+ if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
17654
+ const overrideBlock = json[overrideKey];
17655
+ if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
17656
+ const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
17657
+ const merged = {
17658
+ ...json,
17659
+ permission: {
17660
+ ...this.json.permission,
17661
+ ...toolScopedPermission
17662
+ }
17663
+ };
17664
+ if (Object.keys(restOverride).length > 0) merged[overrideKey] = restOverride;
17665
+ else delete merged[overrideKey];
17666
+ return new RulesyncPermissions({
17667
+ outputRoot: this.outputRoot,
17668
+ relativeDirPath: this.relativeDirPath,
17669
+ relativeFilePath: this.relativeFilePath,
17670
+ fileContent: JSON.stringify(merged, null, 2)
17671
+ });
17672
+ }
17673
+ };
17674
+ /**
17675
+ * Targets whose tool-scoped `permission` override uses tool-native semantics
17676
+ * consumed directly by their translator (bare action strings / tool-only
17677
+ * categories for OpenCode and Kilo, `sensitive_patterns` objects for Vibe).
17678
+ * The central canonical merge must not touch those blocks.
17679
+ */
17680
+ const NATIVE_PERMISSION_OVERRIDE_TARGETS = /* @__PURE__ */ new Set([
17681
+ "opencode",
17682
+ "kilo",
17683
+ "vibe"
17684
+ ]);
17685
+ /**
17686
+ * Targets that read their tool-scoped override from a differently named key:
17687
+ * Kiro IDE/CLI share the `kiro` block (they write the same agent config) and
17688
+ * Hermes Agent's established override key is `hermes`.
17689
+ */
17690
+ const PERMISSION_OVERRIDE_KEY_ALIASES = {
17691
+ "kiro-cli": "kiro",
17692
+ "kiro-ide": "kiro",
17693
+ hermesagent: "hermes"
17000
17694
  };
17001
17695
  //#endregion
17002
17696
  //#region src/features/permissions/tool-permissions.ts
@@ -18836,10 +19530,15 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18836
19530
  existingDomainsHadUnknown,
18837
19531
  logger
18838
19532
  });
18839
- permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
19533
+ const profile = mergeWithExistingProfile({
18840
19534
  newProfile,
18841
19535
  existingProfile
18842
19536
  });
19537
+ permissionsTable[RULESYNC_PROFILE_NAME] = preserveUnmanagedProfileKeys({
19538
+ rawExistingProfile: permissionsTable[RULESYNC_PROFILE_NAME],
19539
+ profile,
19540
+ logger
19541
+ });
18843
19542
  const overridePatch = computeCodexcliOverridePatch({
18844
19543
  existing,
18845
19544
  override: rulesyncPermissions.getJson().codexcli,
@@ -19035,6 +19734,43 @@ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingD
19035
19734
  if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
19036
19735
  if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19037
19736
  }
19737
+ const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
19738
+ "description",
19739
+ "extends",
19740
+ "filesystem",
19741
+ "network"
19742
+ ]);
19743
+ const MANAGED_NETWORK_KEYS = /* @__PURE__ */ new Set([
19744
+ "enabled",
19745
+ "mode",
19746
+ "domains",
19747
+ "unix_sockets"
19748
+ ]);
19749
+ /**
19750
+ * Re-attach the keys of the existing rulesync profile that rulesync does not
19751
+ * manage. The managed keys always come from `profile` (the freshly computed
19752
+ * merge result); unmanaged siblings — at the profile level and inside the
19753
+ * `network` table — are preserved verbatim from the existing config so a
19754
+ * regeneration never deletes user-authored Codex settings.
19755
+ */
19756
+ function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
19757
+ const rawProfileTable = toMutableTable(rawExistingProfile);
19758
+ const profileExtras = Object.fromEntries(Object.entries(rawProfileTable).filter(([key]) => !MANAGED_PROFILE_KEYS.has(key)));
19759
+ const rawNetworkTable = toMutableTable(rawProfileTable.network);
19760
+ const networkExtras = Object.fromEntries(Object.entries(rawNetworkTable).filter(([key]) => !MANAGED_NETWORK_KEYS.has(key)));
19761
+ const preservedKeyNames = [...Object.keys(profileExtras), ...Object.keys(networkExtras).map((key) => `network.${key}`)];
19762
+ if (preservedKeyNames.length > 0) logger?.warn(`Preserving unmanaged keys in the "${RULESYNC_PROFILE_NAME}" permissions profile: ${preservedKeyNames.join(", ")}. Review them manually; rulesync carries them forward verbatim.`);
19763
+ const result = {
19764
+ ...profileExtras,
19765
+ ...profile
19766
+ };
19767
+ const mergedNetwork = {
19768
+ ...networkExtras,
19769
+ ...profile.network
19770
+ };
19771
+ if (Object.keys(mergedNetwork).length > 0) result.network = mergedNetwork;
19772
+ return result;
19773
+ }
19038
19774
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
19039
19775
  if (!existingProfile) return newProfile;
19040
19776
  const mergedNetwork = { ...newProfile.network };
@@ -19765,9 +20501,11 @@ const FACTORYDROID_OVERRIDE_KEYS = [
19765
20501
  "networkPolicy",
19766
20502
  "sandbox",
19767
20503
  "mcpPolicy",
20504
+ "mcpAutonomyOverrides",
19768
20505
  "enableDroidShield",
19769
20506
  "sessionDefaultSettings",
19770
20507
  "maxAutonomyLevel",
20508
+ "subagentAutonomyLevel",
19771
20509
  "interactionMode"
19772
20510
  ];
19773
20511
  /**
@@ -20125,7 +20863,6 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
20125
20863
  }
20126
20864
  //#endregion
20127
20865
  //#region src/features/permissions/grokcli-permissions.ts
20128
- const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
20129
20866
  const GROKCLI_UI_KEY = "ui";
20130
20867
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
20131
20868
  const GROKCLI_PERMISSION_KEY = "permission";
@@ -20227,13 +20964,19 @@ function parseGrokEntry(entry) {
20227
20964
  * compatibility with older Grok versions: `always-approve` when the config is
20228
20965
  * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
20229
20966
  * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
20230
- * arrays).
20231
- *
20232
- * This surface is **global only** — the adapter syncs the user-level
20233
- * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
20234
- * file; project scope is not modeled here.) The shared config is merged in
20967
+ * arrays). It is a user-level UI setting, so it is only written in global scope
20968
+ * (see below).
20969
+ *
20970
+ * Both scopes are supported: the adapter syncs the project-level
20971
+ * `./.grok/config.toml` (default) and the user-level `~/.grok/config.toml`
20972
+ * (with `--global`). Grok documents that "Project configs are limited to MCP
20973
+ * servers, plugins, and permission rules, not full user configs"
20974
+ * (https://docs.x.ai/build/settings), so the project config carries only the
20975
+ * fine-grained `[permission]` rules; the coarse `[ui] permission_mode` UI toggle
20976
+ * is written in global scope only. The shared config is merged in
20235
20977
  * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
20236
- * tools it models and the `[ui] permission_mode` value, while every other key
20978
+ * tools it models and (in global scope) the `[ui] permission_mode` value, while
20979
+ * every other key
20237
20980
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
20238
20981
  * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
20239
20982
  * file is never deleted.
@@ -20255,7 +20998,6 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20255
20998
  };
20256
20999
  }
20257
21000
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
20258
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
20259
21001
  const paths = GrokcliPermissions.getSettablePaths({ global });
20260
21002
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
20261
21003
  return new GrokcliPermissions({
@@ -20264,11 +21006,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20264
21006
  relativeFilePath: paths.relativeFilePath,
20265
21007
  fileContent,
20266
21008
  validate,
20267
- global: true
21009
+ global
20268
21010
  });
20269
21011
  }
20270
21012
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
20271
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
20272
21013
  const paths = GrokcliPermissions.getSettablePaths({ global });
20273
21014
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
20274
21015
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -20287,11 +21028,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20287
21028
  deny: buckets.deny,
20288
21029
  ask: buckets.ask
20289
21030
  };
20290
- const mode = deriveGrokPermissionMode(config);
20291
- const ui = {
21031
+ const uiPatch = global ? { [GROKCLI_UI_KEY]: {
20292
21032
  ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
20293
- [GROKCLI_PERMISSION_MODE_KEY]: mode
20294
- };
21033
+ [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
21034
+ } } : {};
20295
21035
  return new GrokcliPermissions({
20296
21036
  outputRoot,
20297
21037
  relativeDirPath: paths.relativeDirPath,
@@ -20302,12 +21042,12 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20302
21042
  existingContent,
20303
21043
  patch: {
20304
21044
  [GROKCLI_PERMISSION_KEY]: permission,
20305
- [GROKCLI_UI_KEY]: ui
21045
+ ...uiPatch
20306
21046
  },
20307
21047
  filePath
20308
21048
  }),
20309
21049
  validate: true,
20310
- global: true
21050
+ global
20311
21051
  });
20312
21052
  }
20313
21053
  toRulesyncPermissions() {
@@ -20328,14 +21068,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
20328
21068
  error: null
20329
21069
  };
20330
21070
  }
20331
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
21071
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
20332
21072
  return new GrokcliPermissions({
20333
21073
  outputRoot,
20334
21074
  relativeDirPath,
20335
21075
  relativeFilePath,
20336
21076
  fileContent: "",
20337
21077
  validate: false,
20338
- global: true
21078
+ global
20339
21079
  });
20340
21080
  }
20341
21081
  };
@@ -21250,6 +21990,22 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
21250
21990
  "notebookedit",
21251
21991
  "agent"
21252
21992
  ]);
21993
+ /**
21994
+ * Translate between rulesync's canonical permission category names and
21995
+ * OpenCode's native permission keys. OpenCode has no `agent` key — subagent
21996
+ * launches are gated by the `task` key (see the documented key list at
21997
+ * https://opencode.ai/docs/permissions/). Without this translation a canonical
21998
+ * `agent: deny` would be written verbatim into `opencode.json` and silently
21999
+ * ignored by OpenCode. Unknown names pass through unchanged.
22000
+ */
22001
+ const CANONICAL_TO_OPENCODE_PERMISSION_KEYS = { agent: "task" };
22002
+ const OPENCODE_TO_CANONICAL_PERMISSION_KEYS = Object.fromEntries(Object.entries(CANONICAL_TO_OPENCODE_PERMISSION_KEYS).map(([canonical, opencode]) => [opencode, canonical]));
22003
+ function toOpencodePermissionKey(canonical) {
22004
+ return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? canonical;
22005
+ }
22006
+ function toCanonicalPermissionKey(opencodeKey) {
22007
+ return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
22008
+ }
21253
22009
  function isSharedPermissionCategory(category) {
21254
22010
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
21255
22011
  }
@@ -21316,6 +22072,8 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21316
22072
  }
21317
22073
  const rulesyncJson = rulesyncPermissions.getJson();
21318
22074
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
22075
+ const sharedPermission = {};
22076
+ for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
21319
22077
  return new OpencodePermissions({
21320
22078
  outputRoot,
21321
22079
  relativeDirPath: basePaths.relativeDirPath,
@@ -21325,7 +22083,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21325
22083
  feature: "permissions",
21326
22084
  existingContent: fileContent ?? "",
21327
22085
  patch: { permission: {
21328
- ...rulesyncJson.permission,
22086
+ ...sharedPermission,
21329
22087
  ...overridePermission
21330
22088
  } },
21331
22089
  filePath: (0, node_path.join)(jsonDir, relativeFilePath)
@@ -21341,8 +22099,11 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
21341
22099
  }
21342
22100
  const shared = {};
21343
22101
  const overrideOnly = {};
21344
- for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
21345
- else overrideOnly[category] = value;
22102
+ for (const [category, value] of Object.entries(rawPermission)) {
22103
+ const canonicalCategory = toCanonicalPermissionKey(category);
22104
+ if (isSharedPermissionCategory(canonicalCategory)) shared[canonicalCategory] = typeof value === "string" ? { "*": value } : value;
22105
+ else overrideOnly[category] = value;
22106
+ }
21346
22107
  const json = Object.keys(overrideOnly).length > 0 ? {
21347
22108
  permission: shared,
21348
22109
  opencode: { permission: overrideOnly }
@@ -21467,6 +22228,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
21467
22228
  "disabled"
21468
22229
  ];
21469
22230
  const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
22231
+ const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
21470
22232
  function asPlainRecord(value) {
21471
22233
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
21472
22234
  }
@@ -21538,8 +22300,9 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21538
22300
  else delete mergedPermissions.ask;
21539
22301
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21540
22302
  else delete mergedPermissions.deny;
21541
- const patch = { permissions: mergedPermissions };
21542
22303
  const override = config.qwencode;
22304
+ if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
22305
+ const patch = { permissions: mergedPermissions };
21543
22306
  if (override?.tools !== void 0) patch.tools = {
21544
22307
  ...asPlainRecord(settings.tools),
21545
22308
  ...asPlainRecord(override.tools)
@@ -21581,9 +22344,11 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21581
22344
  });
21582
22345
  const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
21583
22346
  const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
22347
+ const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
21584
22348
  const qwencodeOverride = {};
21585
22349
  if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
21586
22350
  if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
22351
+ if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
21587
22352
  const result = { ...config };
21588
22353
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
21589
22354
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -23092,7 +23857,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
23092
23857
  ["grokcli", {
23093
23858
  class: GrokcliPermissions,
23094
23859
  meta: {
23095
- supportsProject: false,
23860
+ supportsProject: true,
23096
23861
  supportsGlobal: true,
23097
23862
  supportsImport: true
23098
23863
  }
@@ -23267,16 +24032,20 @@ var PermissionsProcessor = class extends FeatureProcessor {
23267
24032
  if (!rulesyncPermissions) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
23268
24033
  const factory = toolPermissionsFactories.get(this.toolTarget);
23269
24034
  if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
24035
+ const effectivePermissions = rulesyncPermissions.forTarget({
24036
+ toolTarget: this.toolTarget,
24037
+ logger: this.logger
24038
+ });
23270
24039
  const toolPermissions = await factory.class.fromRulesyncPermissions({
23271
24040
  outputRoot: this.outputRoot,
23272
- rulesyncPermissions,
24041
+ rulesyncPermissions: effectivePermissions,
23273
24042
  logger: this.logger,
23274
24043
  global: this.global
23275
24044
  });
23276
24045
  if (this.toolTarget !== "codexcli") return [toolPermissions];
23277
24046
  return [toolPermissions, createCodexcliBashRulesFile({
23278
24047
  outputRoot: this.outputRoot,
23279
- config: rulesyncPermissions.getJson()
24048
+ config: effectivePermissions.getJson()
23280
24049
  })];
23281
24050
  }
23282
24051
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -39372,6 +40141,16 @@ async function generatePermissionsCore(params) {
39372
40141
  //#endregion
39373
40142
  //#region src/lib/import.ts
39374
40143
  /**
40144
+ * Import always writes the `.json` variant of a fixed-path source file, but a
40145
+ * sibling `.jsonc` variant takes precedence at read time — so an import into a
40146
+ * project that authors the `.jsonc` variant would be silently shadowed. Warn
40147
+ * so the user merges (or removes) one of the two by hand.
40148
+ */
40149
+ async function warnIfShadowedByJsonc(params) {
40150
+ const { outputRoot, jsonRelativePath, jsoncRelativePath, logger } = params;
40151
+ if (await fileExists((0, node_path.join)(outputRoot, jsoncRelativePath))) logger.warn(`${jsoncRelativePath} exists and takes precedence over the imported ${jsonRelativePath}. Merge the imported content into ${jsoncRelativePath} (or remove it) so the import takes effect.`);
40152
+ }
40153
+ /**
39375
40154
  * Import configuration files from AI tools.
39376
40155
  */
39377
40156
  async function importFromTool(params) {
@@ -39483,6 +40262,12 @@ async function importMcpCore(params) {
39483
40262
  }
39484
40263
  const rulesyncFiles = await mcpProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39485
40264
  const { count: writtenCount } = await mcpProcessor.writeAiFiles(rulesyncFiles);
40265
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40266
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40267
+ jsonRelativePath: RULESYNC_MCP_RELATIVE_FILE_PATH,
40268
+ jsoncRelativePath: RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH,
40269
+ logger
40270
+ });
39486
40271
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} MCP files`);
39487
40272
  return writtenCount;
39488
40273
  }
@@ -39582,6 +40367,12 @@ async function importHooksCore(params) {
39582
40367
  }
39583
40368
  const rulesyncFiles = await hooksProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39584
40369
  const { count: writtenCount } = await hooksProcessor.writeAiFiles(rulesyncFiles);
40370
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40371
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40372
+ jsonRelativePath: RULESYNC_HOOKS_RELATIVE_FILE_PATH,
40373
+ jsoncRelativePath: RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH,
40374
+ logger
40375
+ });
39585
40376
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} hooks file(s)`);
39586
40377
  return writtenCount;
39587
40378
  }
@@ -39611,6 +40402,12 @@ async function importPermissionsCore(params) {
39611
40402
  }
39612
40403
  const rulesyncFiles = await permissionsProcessor.convertToolFilesToRulesyncFiles(toolFiles);
39613
40404
  const { count: writtenCount } = await permissionsProcessor.writeAiFiles(rulesyncFiles);
40405
+ if (writtenCount > 0) await warnIfShadowedByJsonc({
40406
+ outputRoot: config.getOutputRoots()[0] ?? ".",
40407
+ jsonRelativePath: RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
40408
+ jsoncRelativePath: RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH,
40409
+ logger
40410
+ });
39614
40411
  if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} permissions file(s)`);
39615
40412
  return writtenCount;
39616
40413
  }
@@ -39675,6 +40472,12 @@ Object.defineProperty(exports, "CLIError", {
39675
40472
  return CLIError;
39676
40473
  }
39677
40474
  });
40475
+ Object.defineProperty(exports, "CODEXCLI_DIR", {
40476
+ enumerable: true,
40477
+ get: function() {
40478
+ return CODEXCLI_DIR;
40479
+ }
40480
+ });
39678
40481
  Object.defineProperty(exports, "CommandsProcessor", {
39679
40482
  enumerable: true,
39680
40483
  get: function() {
@@ -39771,6 +40574,12 @@ Object.defineProperty(exports, "RULESYNC_HOOKS_FILE_NAME", {
39771
40574
  return RULESYNC_HOOKS_FILE_NAME;
39772
40575
  }
39773
40576
  });
40577
+ Object.defineProperty(exports, "RULESYNC_HOOKS_JSONC_FILE_NAME", {
40578
+ enumerable: true,
40579
+ get: function() {
40580
+ return RULESYNC_HOOKS_JSONC_FILE_NAME;
40581
+ }
40582
+ });
39774
40583
  Object.defineProperty(exports, "RULESYNC_HOOKS_RELATIVE_FILE_PATH", {
39775
40584
  enumerable: true,
39776
40585
  get: function() {
@@ -39795,6 +40604,12 @@ Object.defineProperty(exports, "RULESYNC_MCP_FILE_NAME", {
39795
40604
  return RULESYNC_MCP_FILE_NAME;
39796
40605
  }
39797
40606
  });
40607
+ Object.defineProperty(exports, "RULESYNC_MCP_JSONC_FILE_NAME", {
40608
+ enumerable: true,
40609
+ get: function() {
40610
+ return RULESYNC_MCP_JSONC_FILE_NAME;
40611
+ }
40612
+ });
39798
40613
  Object.defineProperty(exports, "RULESYNC_MCP_RELATIVE_FILE_PATH", {
39799
40614
  enumerable: true,
39800
40615
  get: function() {
@@ -39819,12 +40634,24 @@ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_FILE_NAME", {
39819
40634
  return RULESYNC_PERMISSIONS_FILE_NAME;
39820
40635
  }
39821
40636
  });
40637
+ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_JSONC_FILE_NAME", {
40638
+ enumerable: true,
40639
+ get: function() {
40640
+ return RULESYNC_PERMISSIONS_JSONC_FILE_NAME;
40641
+ }
40642
+ });
39822
40643
  Object.defineProperty(exports, "RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH", {
39823
40644
  enumerable: true,
39824
40645
  get: function() {
39825
40646
  return RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH;
39826
40647
  }
39827
40648
  });
40649
+ Object.defineProperty(exports, "RULESYNC_PERMISSIONS_SCHEMA_URL", {
40650
+ enumerable: true,
40651
+ get: function() {
40652
+ return RULESYNC_PERMISSIONS_SCHEMA_URL;
40653
+ }
40654
+ });
39828
40655
  Object.defineProperty(exports, "RULESYNC_RELATIVE_DIR_PATH", {
39829
40656
  enumerable: true,
39830
40657
  get: function() {