rulesync 9.6.3 → 9.8.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.
@@ -8,6 +8,7 @@ import { intersection, kebabCase, uniq } from "es-toolkit";
8
8
  import { globbySync } from "globby";
9
9
  import { isDeepStrictEqual } from "node:util";
10
10
  import * as smolToml from "smol-toml";
11
+ import { parse as parse$1, stringify } from "smol-toml";
11
12
  import matter from "gray-matter";
12
13
  import { YAMLException, dump, load } from "js-yaml";
13
14
  import { omit } from "es-toolkit/object";
@@ -321,7 +322,8 @@ const hooksProcessorToolTargetTuple = [
321
322
  "junie",
322
323
  "vibe",
323
324
  "qwencode",
324
- "reasonix"
325
+ "reasonix",
326
+ "grokcli"
325
327
  ];
326
328
  const permissionsProcessorToolTargetTuple = [
327
329
  "amp",
@@ -2677,6 +2679,13 @@ const CODEXCLI_MCP_FILE_NAME = "config.toml";
2677
2679
  const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
2678
2680
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
2679
2681
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = join("agents", "openai.yaml");
2682
+ const CODEXCLI_OVERRIDE_KEYS = [
2683
+ "approval_policy",
2684
+ "sandbox_mode",
2685
+ "sandbox_workspace_write",
2686
+ "apps",
2687
+ "approvals_reviewer"
2688
+ ];
2680
2689
  //#endregion
2681
2690
  //#region src/features/commands/codexcli-command.ts
2682
2691
  const CodexcliCommandFrontmatterSchema = z.looseObject({
@@ -4055,14 +4064,22 @@ function sanitizeSharedConfigValue(value) {
4055
4064
  * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
4056
4065
  * path when one is given.
4057
4066
  */
4058
- function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
4067
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty", jsoncParseErrors = "tolerate" }) {
4059
4068
  if (fileContent.trim() === "") return {};
4060
4069
  const at = filePath === void 0 ? "" : ` at ${filePath}`;
4061
4070
  let parsed;
4062
4071
  try {
4063
4072
  if (format === "yaml") parsed = loadYaml(fileContent);
4073
+ else if (format === "toml") parsed = parse$1(fileContent);
4064
4074
  else if (format === "json") parsed = JSON.parse(fileContent);
4065
- else parsed = parse(fileContent);
4075
+ else if (jsoncParseErrors === "error") {
4076
+ const errors = [];
4077
+ parsed = parse(fileContent, errors, { allowTrailingComma: true });
4078
+ if (errors.length > 0) {
4079
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
4080
+ throw new Error(details);
4081
+ }
4082
+ } else parsed = parse(fileContent);
4066
4083
  } catch (error) {
4067
4084
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4068
4085
  }
@@ -4076,13 +4093,15 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
4076
4093
  /**
4077
4094
  * Serialize a shared config document. YAML output always ends with exactly one
4078
4095
  * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4079
- * writers have always emitted (no trailing newline).
4096
+ * writers have always emitted (no trailing newline); TOML output matches the
4097
+ * `smol-toml` `stringify` shape the TOML writers have always emitted.
4080
4098
  */
4081
4099
  function stringifySharedConfig({ format, document }) {
4082
4100
  if (format === "yaml") return dump(document, {
4083
4101
  noRefs: true,
4084
4102
  sortKeys: false
4085
4103
  }).trimEnd() + "\n";
4104
+ if (format === "toml") return stringify(document);
4086
4105
  return JSON.stringify(document, null, 2);
4087
4106
  }
4088
4107
  /**
@@ -4121,6 +4140,25 @@ function mergeSharedConfigDeep({ base, patch }) {
4121
4140
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4122
4141
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4123
4142
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4143
+ const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
4144
+ const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
4145
+ const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
4146
+ const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
4147
+ const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
4148
+ /**
4149
+ * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
4150
+ * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
4151
+ * feature classes don't pull the processor registry through this module and
4152
+ * create an import cycle); the ownership lock-step test keeps the two aligned.
4153
+ * Lets a tool whose file lives at a scope-dependent path (`.zed/settings.json`
4154
+ * vs `.config/zed/settings.json`) resolve its declaration from the settable
4155
+ * paths it already holds.
4156
+ */
4157
+ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
4158
+ const dir = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
4159
+ const file = relativeFilePath.replace(/\\/g, "/");
4160
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
4161
+ };
4124
4162
  /**
4125
4163
  * Who owns what in each gateway-managed shared config file, and which policy
4126
4164
  * resolves conflicts. Keys are `dir/file` tokens matching
@@ -4177,6 +4215,298 @@ const SHARED_CONFIG_OWNERSHIP = {
4177
4215
  },
4178
4216
  permissions: { kind: "deep-merge" }
4179
4217
  }
4218
+ },
4219
+ ".zed/settings.json": {
4220
+ format: "json",
4221
+ features: {
4222
+ ignore: {
4223
+ kind: "replace-owned-keys",
4224
+ ownedKeys: ["private_files"]
4225
+ },
4226
+ mcp: {
4227
+ kind: "replace-owned-keys",
4228
+ ownedKeys: ["context_servers"]
4229
+ },
4230
+ permissions: {
4231
+ kind: "replace-owned-keys",
4232
+ ownedKeys: ["agent"]
4233
+ }
4234
+ }
4235
+ },
4236
+ ".config/zed/settings.json": {
4237
+ format: "json",
4238
+ features: {
4239
+ mcp: {
4240
+ kind: "replace-owned-keys",
4241
+ ownedKeys: ["context_servers"]
4242
+ },
4243
+ permissions: {
4244
+ kind: "replace-owned-keys",
4245
+ ownedKeys: ["agent"]
4246
+ }
4247
+ }
4248
+ },
4249
+ ".qwen/settings.json": {
4250
+ format: "json",
4251
+ features: {
4252
+ mcp: {
4253
+ kind: "replace-owned-keys",
4254
+ ownedKeys: ["mcpServers"]
4255
+ },
4256
+ hooks: {
4257
+ kind: "replace-owned-keys",
4258
+ ownedKeys: ["hooks", "disableAllHooks"]
4259
+ },
4260
+ permissions: {
4261
+ kind: "replace-owned-keys",
4262
+ ownedKeys: [
4263
+ "permissions",
4264
+ "tools",
4265
+ "security"
4266
+ ]
4267
+ }
4268
+ }
4269
+ },
4270
+ ".augment/settings.json": {
4271
+ format: "json",
4272
+ features: {
4273
+ mcp: {
4274
+ kind: "replace-owned-keys",
4275
+ ownedKeys: ["mcpServers"]
4276
+ },
4277
+ hooks: {
4278
+ kind: "replace-owned-keys",
4279
+ ownedKeys: ["hooks"]
4280
+ },
4281
+ permissions: {
4282
+ kind: "replace-owned-keys",
4283
+ ownedKeys: ["toolPermissions"]
4284
+ }
4285
+ }
4286
+ },
4287
+ ".devin/config.json": {
4288
+ format: "json",
4289
+ features: {
4290
+ mcp: {
4291
+ kind: "replace-owned-keys",
4292
+ ownedKeys: ["mcpServers"]
4293
+ },
4294
+ permissions: {
4295
+ kind: "replace-owned-keys",
4296
+ ownedKeys: ["permissions"]
4297
+ }
4298
+ }
4299
+ },
4300
+ ".config/devin/config.json": {
4301
+ format: "json",
4302
+ features: {
4303
+ mcp: {
4304
+ kind: "replace-owned-keys",
4305
+ ownedKeys: ["mcpServers"]
4306
+ },
4307
+ hooks: {
4308
+ kind: "replace-owned-keys",
4309
+ ownedKeys: ["hooks"]
4310
+ },
4311
+ permissions: {
4312
+ kind: "replace-owned-keys",
4313
+ ownedKeys: ["permissions"]
4314
+ }
4315
+ }
4316
+ },
4317
+ ".kiro/agents/default.json": {
4318
+ format: "json",
4319
+ features: {
4320
+ hooks: {
4321
+ kind: "replace-owned-keys",
4322
+ ownedKeys: ["hooks"]
4323
+ },
4324
+ permissions: {
4325
+ kind: "replace-owned-keys",
4326
+ ownedKeys: ["allowedTools", "toolsSettings"]
4327
+ }
4328
+ }
4329
+ },
4330
+ ".amp/settings.json": {
4331
+ format: "jsonc",
4332
+ invalidRootPolicy: "error",
4333
+ jsoncParseErrors: "error",
4334
+ features: {
4335
+ mcp: {
4336
+ kind: "replace-owned-keys",
4337
+ ownedKeys: ["amp.mcpServers"]
4338
+ },
4339
+ permissions: {
4340
+ kind: "replace-owned-keys",
4341
+ ownedKeys: [
4342
+ "amp.tools.disable",
4343
+ "amp.permissions",
4344
+ "amp.guardedFiles.allowlist",
4345
+ "amp.dangerouslyAllowAll",
4346
+ "amp.mcpPermissions"
4347
+ ]
4348
+ }
4349
+ }
4350
+ },
4351
+ ".config/amp/settings.json": {
4352
+ format: "jsonc",
4353
+ invalidRootPolicy: "error",
4354
+ jsoncParseErrors: "error",
4355
+ features: {
4356
+ mcp: {
4357
+ kind: "replace-owned-keys",
4358
+ ownedKeys: ["amp.mcpServers"]
4359
+ },
4360
+ permissions: {
4361
+ kind: "replace-owned-keys",
4362
+ ownedKeys: [
4363
+ "amp.tools.disable",
4364
+ "amp.permissions",
4365
+ "amp.guardedFiles.allowlist",
4366
+ "amp.dangerouslyAllowAll",
4367
+ "amp.mcpPermissions"
4368
+ ]
4369
+ }
4370
+ }
4371
+ },
4372
+ "opencode.json": {
4373
+ format: "jsonc",
4374
+ features: {
4375
+ mcp: {
4376
+ kind: "replace-owned-keys",
4377
+ ownedKeys: ["mcp", "tools"]
4378
+ },
4379
+ permissions: {
4380
+ kind: "replace-owned-keys",
4381
+ ownedKeys: ["permission"]
4382
+ },
4383
+ rules: {
4384
+ kind: "replace-owned-keys",
4385
+ ownedKeys: ["instructions"]
4386
+ }
4387
+ }
4388
+ },
4389
+ ".config/opencode/opencode.json": {
4390
+ format: "jsonc",
4391
+ features: {
4392
+ mcp: {
4393
+ kind: "replace-owned-keys",
4394
+ ownedKeys: ["mcp", "tools"]
4395
+ },
4396
+ permissions: {
4397
+ kind: "replace-owned-keys",
4398
+ ownedKeys: ["permission"]
4399
+ }
4400
+ }
4401
+ },
4402
+ "kilo.json": {
4403
+ format: "jsonc",
4404
+ features: {
4405
+ mcp: {
4406
+ kind: "replace-owned-keys",
4407
+ ownedKeys: ["mcp", "tools"]
4408
+ },
4409
+ rules: {
4410
+ kind: "replace-owned-keys",
4411
+ ownedKeys: ["instructions"]
4412
+ }
4413
+ }
4414
+ },
4415
+ ".config/kilo/kilo.json": {
4416
+ format: "jsonc",
4417
+ features: { mcp: {
4418
+ kind: "replace-owned-keys",
4419
+ ownedKeys: ["mcp", "tools"]
4420
+ } }
4421
+ },
4422
+ [CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
4423
+ format: "toml",
4424
+ features: {
4425
+ hooks: {
4426
+ kind: "replace-owned-keys",
4427
+ ownedKeys: ["features"]
4428
+ },
4429
+ mcp: {
4430
+ kind: "replace-owned-keys",
4431
+ ownedKeys: ["mcp_servers"]
4432
+ },
4433
+ permissions: {
4434
+ kind: "replace-owned-keys",
4435
+ ownedKeys: [
4436
+ "permissions",
4437
+ "default_permissions",
4438
+ ...CODEXCLI_OVERRIDE_KEYS
4439
+ ]
4440
+ }
4441
+ }
4442
+ },
4443
+ [GROKCLI_CONFIG_SHARED_FILE_KEY]: {
4444
+ format: "toml",
4445
+ features: {
4446
+ mcp: {
4447
+ kind: "replace-owned-keys",
4448
+ ownedKeys: ["mcp_servers"]
4449
+ },
4450
+ permissions: {
4451
+ kind: "replace-owned-keys",
4452
+ ownedKeys: ["permission", "ui"]
4453
+ }
4454
+ }
4455
+ },
4456
+ [VIBE_CONFIG_SHARED_FILE_KEY]: {
4457
+ format: "toml",
4458
+ features: {
4459
+ hooks: {
4460
+ kind: "replace-owned-keys",
4461
+ ownedKeys: ["enable_experimental_hooks"]
4462
+ },
4463
+ mcp: {
4464
+ kind: "replace-owned-keys",
4465
+ ownedKeys: ["mcp_servers"]
4466
+ },
4467
+ permissions: {
4468
+ kind: "replace-owned-keys",
4469
+ ownedKeys: [
4470
+ "tools",
4471
+ "enabled_tools",
4472
+ "disabled_tools"
4473
+ ]
4474
+ }
4475
+ }
4476
+ },
4477
+ [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
4478
+ format: "toml",
4479
+ features: {
4480
+ mcp: {
4481
+ kind: "replace-owned-keys",
4482
+ ownedKeys: ["plugins"]
4483
+ },
4484
+ permissions: {
4485
+ kind: "replace-owned-keys",
4486
+ ownedKeys: [
4487
+ "permissions",
4488
+ "sandbox",
4489
+ "agent"
4490
+ ]
4491
+ }
4492
+ }
4493
+ },
4494
+ [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
4495
+ format: "toml",
4496
+ features: {
4497
+ mcp: {
4498
+ kind: "replace-owned-keys",
4499
+ ownedKeys: ["plugins"]
4500
+ },
4501
+ permissions: {
4502
+ kind: "replace-owned-keys",
4503
+ ownedKeys: [
4504
+ "permissions",
4505
+ "sandbox",
4506
+ "agent"
4507
+ ]
4508
+ }
4509
+ }
4180
4510
  }
4181
4511
  };
4182
4512
  /**
@@ -4197,17 +4527,20 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
4197
4527
  format: declaration.format,
4198
4528
  fileContent: existingContent,
4199
4529
  filePath,
4200
- ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4530
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy },
4531
+ ...declaration.jsoncParseErrors !== void 0 && { jsoncParseErrors: declaration.jsoncParseErrors }
4201
4532
  });
4202
4533
  if (policy.kind === "replace-owned-keys") {
4203
4534
  const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4204
4535
  if (unowned.length > 0) throw new Error(`Feature '${feature}' tried to write undeclared keys [${unowned.join(", ")}] to '${fileKey}'; extend its ownedKeys declaration if that ownership is intended.`);
4536
+ const document = mergeSharedConfigShallow({
4537
+ base,
4538
+ patch
4539
+ });
4540
+ for (const [key, value] of Object.entries(patch)) if (value === void 0) delete document[key];
4205
4541
  return stringifySharedConfig({
4206
4542
  format: declaration.format,
4207
- document: mergeSharedConfigShallow({
4208
- base,
4209
- patch
4210
- })
4543
+ document
4211
4544
  });
4212
4545
  }
4213
4546
  const merged = mergeSharedConfigDeep({
@@ -5937,10 +6270,13 @@ const CLAUDE_HOOK_EVENTS = [
5937
6270
  * Hook events supported by Devin Local (native `.devin/` hooks).
5938
6271
  *
5939
6272
  * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
5940
- * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
5941
- * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
6273
+ * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6274
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
6275
+ * `PostCompaction` (fires after context compaction; added in the Devin CLI
6276
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
5942
6277
  * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
5943
- * a `matcher` (regex against `tool_name`); the session/turn events do not.
6278
+ * a `matcher` (regex against `tool_name`); the session/turn/compaction events
6279
+ * do not.
5944
6280
  *
5945
6281
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
5946
6282
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -5955,7 +6291,8 @@ const DEVIN_HOOK_EVENTS = [
5955
6291
  "postToolUse",
5956
6292
  "beforeSubmitPrompt",
5957
6293
  "stop",
5958
- "permissionRequest"
6294
+ "permissionRequest",
6295
+ "postCompact"
5959
6296
  ];
5960
6297
  /** Hook events supported by OpenCode. */
5961
6298
  const OPENCODE_HOOK_EVENTS = [
@@ -6149,7 +6486,7 @@ const ANTIGRAVITY_HOOK_EVENTS = [
6149
6486
  * Hook events supported by AugmentCode (Auggie CLI).
6150
6487
  * Auggie mirrors Claude Code's lifecycle hooks but exposes a smaller set:
6151
6488
  * PreToolUse / PostToolUse (tool events, matcher-aware) plus the
6152
- * SessionStart / SessionEnd / Stop session events (no matcher).
6489
+ * SessionStart / SessionEnd / Stop / Notification session events (no matcher).
6153
6490
  * @see https://docs.augmentcode.com/cli/hooks
6154
6491
  */
6155
6492
  const AUGMENTCODE_HOOK_EVENTS = [
@@ -6157,7 +6494,8 @@ const AUGMENTCODE_HOOK_EVENTS = [
6157
6494
  "postToolUse",
6158
6495
  "sessionStart",
6159
6496
  "sessionEnd",
6160
- "stop"
6497
+ "stop",
6498
+ "notification"
6161
6499
  ];
6162
6500
  /**
6163
6501
  * Hook events supported by Mistral Vibe (mistral-vibe).
@@ -6209,6 +6547,10 @@ const JUNIE_HOOK_EVENTS = [
6209
6547
  * The Qwen-specific events
6210
6548
  * `TodoCreated`, `TodoCompleted`, and `StopFailure` map to the canonical
6211
6549
  * `todoCreated`, `todoCompleted`, and `stopFailure` events respectively.
6550
+ * Qwen's `HookEventName` enum (`packages/core/src/hooks/types.ts`) also documents
6551
+ * `PostToolBatch`, `UserPromptExpansion`, `PermissionDenied`, and
6552
+ * `InstructionsLoaded`, which map to the canonical `postToolBatch`,
6553
+ * `userPromptExpansion`, `permissionDenied`, and `instructionsLoaded` events.
6212
6554
  * @see https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
6213
6555
  */
6214
6556
  const QWENCODE_HOOK_EVENTS = [
@@ -6217,7 +6559,9 @@ const QWENCODE_HOOK_EVENTS = [
6217
6559
  "preToolUse",
6218
6560
  "postToolUse",
6219
6561
  "postToolUseFailure",
6562
+ "postToolBatch",
6220
6563
  "beforeSubmitPrompt",
6564
+ "userPromptExpansion",
6221
6565
  "stop",
6222
6566
  "stopFailure",
6223
6567
  "subagentStart",
@@ -6225,7 +6569,9 @@ const QWENCODE_HOOK_EVENTS = [
6225
6569
  "preCompact",
6226
6570
  "postCompact",
6227
6571
  "permissionRequest",
6572
+ "permissionDenied",
6228
6573
  "notification",
6574
+ "instructionsLoaded",
6229
6575
  "todoCreated",
6230
6576
  "todoCompleted"
6231
6577
  ];
@@ -6235,11 +6581,11 @@ const QWENCODE_HOOK_EVENTS = [
6235
6581
  * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
6236
6582
  * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
6237
6583
  * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
6238
- * `SubagentStop`, `Notification`, `PreCompact`). The eight events with a clean
6239
- * canonical equivalent are mapped: `PreToolUse`, `PostToolUse`,
6584
+ * `SubagentStop`, `Notification`, `PreCompact`). All ten have a clean canonical
6585
+ * equivalent and are mapped: `PreToolUse`, `PostToolUse`,
6240
6586
  * `UserPromptSubmit` ← `beforeSubmitPrompt`, `Stop`, `SessionStart`,
6241
- * `SessionEnd`, `SubagentStop`, and `PostLLMCall` ← `postModelInvocation`.
6242
- * (`Notification` and `PreCompact` have no canonical event and are left out.)
6587
+ * `SessionEnd`, `SubagentStop`, `PostLLMCall` ← `postModelInvocation`,
6588
+ * `Notification` ← `notification`, and `PreCompact` `preCompact`.
6243
6589
  * `match` (Reasonix's matcher field name) is honored only on
6244
6590
  * `PreToolUse`/`PostToolUse`, matching the canonical `matcher` field's
6245
6591
  * tool-event scoping used by other adapters.
@@ -6253,7 +6599,39 @@ const REASONIX_HOOK_EVENTS = [
6253
6599
  "sessionStart",
6254
6600
  "sessionEnd",
6255
6601
  "subagentStop",
6256
- "postModelInvocation"
6602
+ "postModelInvocation",
6603
+ "notification",
6604
+ "preCompact"
6605
+ ];
6606
+ /**
6607
+ * Hook events supported by Grok CLI (xAI Grok Build).
6608
+ *
6609
+ * Grok Build documents a Claude-Code-compatible hooks surface with fourteen
6610
+ * PascalCase events, all of which map 1:1 onto an existing canonical arm:
6611
+ * `SessionStart`, `SessionEnd`, `UserPromptSubmit` ← `beforeSubmitPrompt`,
6612
+ * `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied`,
6613
+ * `Stop`, `StopFailure`, `Notification`, `SubagentStart`, `SubagentStop`,
6614
+ * `PreCompact`, `PostCompact`. A `matcher` (a regex tested against the tool
6615
+ * name) is meaningful only on the tool-name events (`PreToolUse`, `PostToolUse`,
6616
+ * `PostToolUseFailure`, `PermissionDenied`), matching Claude Code's semantics;
6617
+ * the remaining lifecycle events are matcher-less.
6618
+ * @see https://docs.x.ai/build/features/hooks
6619
+ */
6620
+ const GROKCLI_HOOK_EVENTS = [
6621
+ "sessionStart",
6622
+ "sessionEnd",
6623
+ "beforeSubmitPrompt",
6624
+ "preToolUse",
6625
+ "postToolUse",
6626
+ "postToolUseFailure",
6627
+ "permissionDenied",
6628
+ "stop",
6629
+ "stopFailure",
6630
+ "notification",
6631
+ "subagentStart",
6632
+ "subagentStop",
6633
+ "preCompact",
6634
+ "postCompact"
6257
6635
  ];
6258
6636
  /**
6259
6637
  * Hook events supported by Hermes Agent's native Shell Hooks system.
@@ -6327,6 +6705,7 @@ const HooksConfigSchema = z.looseObject({
6327
6705
  junie: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6328
6706
  vibe: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6329
6707
  reasonix: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6708
+ grokcli: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
6330
6709
  qwencode: z.optional(z.looseObject({
6331
6710
  hooks: z.optional(hooksRecordSchema),
6332
6711
  disableAllHooks: z.optional(z.boolean())
@@ -6385,7 +6764,8 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6385
6764
  postToolUse: "PostToolUse",
6386
6765
  beforeSubmitPrompt: "UserPromptSubmit",
6387
6766
  stop: "Stop",
6388
- permissionRequest: "PermissionRequest"
6767
+ permissionRequest: "PermissionRequest",
6768
+ postCompact: "PostCompaction"
6389
6769
  };
6390
6770
  /**
6391
6771
  * Map Devin Local PascalCase event names to canonical camelCase.
@@ -6400,7 +6780,8 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6400
6780
  postToolUse: "PostToolUse",
6401
6781
  sessionStart: "SessionStart",
6402
6782
  sessionEnd: "SessionEnd",
6403
- stop: "Stop"
6783
+ stop: "Stop",
6784
+ notification: "Notification"
6404
6785
  };
6405
6786
  /**
6406
6787
  * Map AugmentCode PascalCase event names to canonical camelCase.
@@ -6670,7 +7051,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
6670
7051
  preToolUse: "PreToolUse",
6671
7052
  postToolUse: "PostToolUse",
6672
7053
  postToolUseFailure: "PostToolUseFailure",
7054
+ postToolBatch: "PostToolBatch",
6673
7055
  beforeSubmitPrompt: "UserPromptSubmit",
7056
+ userPromptExpansion: "UserPromptExpansion",
6674
7057
  stop: "Stop",
6675
7058
  subagentStart: "SubagentStart",
6676
7059
  subagentStop: "SubagentStop",
@@ -6678,7 +7061,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
6678
7061
  preCompact: "PreCompact",
6679
7062
  postCompact: "PostCompact",
6680
7063
  permissionRequest: "PermissionRequest",
7064
+ permissionDenied: "PermissionDenied",
6681
7065
  notification: "Notification",
7066
+ instructionsLoaded: "InstructionsLoaded",
6682
7067
  todoCreated: "TodoCreated",
6683
7068
  todoCompleted: "TodoCompleted"
6684
7069
  };
@@ -6700,12 +7085,40 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6700
7085
  sessionStart: "SessionStart",
6701
7086
  sessionEnd: "SessionEnd",
6702
7087
  subagentStop: "SubagentStop",
6703
- postModelInvocation: "PostLLMCall"
7088
+ postModelInvocation: "PostLLMCall",
7089
+ notification: "Notification",
7090
+ preCompact: "PreCompact"
6704
7091
  };
6705
7092
  /**
6706
7093
  * Map Reasonix PascalCase event names to canonical camelCase.
6707
7094
  */
6708
7095
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
7096
+ /**
7097
+ * Map canonical camelCase event names to Grok CLI PascalCase.
7098
+ * Grok Build reuses the same Claude-style PascalCase event names, so each
7099
+ * canonical arm maps to its PascalCase equivalent.
7100
+ * @see https://docs.x.ai/build/features/hooks
7101
+ */
7102
+ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
7103
+ sessionStart: "SessionStart",
7104
+ sessionEnd: "SessionEnd",
7105
+ beforeSubmitPrompt: "UserPromptSubmit",
7106
+ preToolUse: "PreToolUse",
7107
+ postToolUse: "PostToolUse",
7108
+ postToolUseFailure: "PostToolUseFailure",
7109
+ permissionDenied: "PermissionDenied",
7110
+ stop: "Stop",
7111
+ stopFailure: "StopFailure",
7112
+ notification: "Notification",
7113
+ subagentStart: "SubagentStart",
7114
+ subagentStop: "SubagentStop",
7115
+ preCompact: "PreCompact",
7116
+ postCompact: "PostCompact"
7117
+ };
7118
+ /**
7119
+ * Map Grok CLI PascalCase event names to canonical camelCase.
7120
+ */
7121
+ const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
6709
7122
  //#endregion
6710
7123
  //#region src/features/hooks/tool-hooks-converter.ts
6711
7124
  function isToolMatcherEntry(x) {
@@ -7230,7 +7643,8 @@ const AUGMENTCODE_CONVERTER_CONFIG = {
7230
7643
  noMatcherEvents: /* @__PURE__ */ new Set([
7231
7644
  "sessionStart",
7232
7645
  "sessionEnd",
7233
- "stop"
7646
+ "stop",
7647
+ "notification"
7234
7648
  ])
7235
7649
  };
7236
7650
  /**
@@ -7280,12 +7694,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7280
7694
  const paths = AugmentcodeHooks.getSettablePaths({ global });
7281
7695
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7282
7696
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7283
- let settings;
7284
- try {
7285
- settings = JSON.parse(existingContent);
7286
- } catch (error) {
7287
- throw new Error(`Failed to parse existing AugmentCode settings at ${filePath}: ${formatError(error)}`, { cause: error });
7288
- }
7289
7697
  const config = rulesyncHooks.getJson();
7290
7698
  const augmentHooks = canonicalToToolHooks({
7291
7699
  config,
@@ -7293,11 +7701,13 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7293
7701
  converterConfig: AUGMENTCODE_CONVERTER_CONFIG,
7294
7702
  logger
7295
7703
  });
7296
- const merged = {
7297
- ...settings,
7298
- hooks: augmentHooks
7299
- };
7300
- const fileContent = JSON.stringify(merged, null, 2);
7704
+ const fileContent = applySharedConfigPatch({
7705
+ fileKey: sharedConfigFileKey(paths),
7706
+ feature: "hooks",
7707
+ existingContent,
7708
+ patch: { hooks: augmentHooks },
7709
+ filePath
7710
+ });
7301
7711
  return new AugmentcodeHooks({
7302
7712
  outputRoot,
7303
7713
  relativeDirPath: paths.relativeDirPath,
@@ -7464,15 +7874,28 @@ const CODEXCLI_CONVERTER_CONFIG = {
7464
7874
  */
7465
7875
  async function buildCodexConfigTomlContent({ outputRoot }) {
7466
7876
  const configPath = join(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
7467
- const existingContent = await readFileContentOrNull(configPath) ?? smolToml.stringify({});
7877
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
7468
7878
  let configToml;
7469
7879
  try {
7470
- configToml = smolToml.parse(existingContent);
7880
+ configToml = smolToml.parse(existingContent || smolToml.stringify({}));
7471
7881
  } catch (error) {
7472
7882
  throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
7473
7883
  }
7474
- if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
7475
- return smolToml.stringify(configToml);
7884
+ let features;
7885
+ if (typeof configToml.features === "object" && configToml.features !== null) {
7886
+ features = { ...configToml.features };
7887
+ delete features.codex_hooks;
7888
+ }
7889
+ return applySharedConfigPatch({
7890
+ fileKey: sharedConfigFileKey({
7891
+ relativeDirPath: CODEXCLI_DIR,
7892
+ relativeFilePath: CODEXCLI_MCP_FILE_NAME
7893
+ }),
7894
+ feature: "hooks",
7895
+ existingContent,
7896
+ patch: { features },
7897
+ filePath: configPath
7898
+ });
7476
7899
  }
7477
7900
  /**
7478
7901
  * Represents the `.codex/config.toml` file as a generated ToolFile,
@@ -8320,7 +8743,8 @@ const DEVIN_CONVERTER_CONFIG = {
8320
8743
  "sessionStart",
8321
8744
  "sessionEnd",
8322
8745
  "stop",
8323
- "beforeSubmitPrompt"
8746
+ "beforeSubmitPrompt",
8747
+ "postCompact"
8324
8748
  ])
8325
8749
  };
8326
8750
  /**
@@ -8388,17 +8812,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
8388
8812
  let fileContent;
8389
8813
  if (global) {
8390
8814
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8391
- let parsedSettings;
8392
- try {
8393
- parsedSettings = JSON.parse(existingContent);
8394
- } catch (error) {
8395
- throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
8396
- }
8397
- const merged = {
8398
- ...isRecord(parsedSettings) ? parsedSettings : {},
8399
- hooks: devinHooks
8400
- };
8401
- fileContent = JSON.stringify(merged, null, 2);
8815
+ fileContent = applySharedConfigPatch({
8816
+ fileKey: sharedConfigFileKey(paths),
8817
+ feature: "hooks",
8818
+ existingContent,
8819
+ patch: { hooks: devinHooks },
8820
+ filePath
8821
+ });
8402
8822
  } else {
8403
8823
  await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8404
8824
  fileContent = JSON.stringify(devinHooks, null, 2);
@@ -8633,6 +9053,167 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8633
9053
  }
8634
9054
  };
8635
9055
  //#endregion
9056
+ //#region src/constants/grokcli-paths.ts
9057
+ /**
9058
+ * Grok Build CLI (xAI) configuration-layout conventions.
9059
+ *
9060
+ * Single source of truth for where Grok Build expects its files. Grok Build
9061
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9062
+ * with project/global scopes resolved by the directory the CLI runs in
9063
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9064
+ *
9065
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9066
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9067
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9068
+ * @see https://docs.x.ai/build/overview
9069
+ */
9070
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9071
+ const GROKCLI_DIR = ".grok";
9072
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9073
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9074
+ /**
9075
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9076
+ * permission mode, and other settings all live here; permissions reuse the same
9077
+ * file name as MCP since Grok consolidates everything into one config.
9078
+ */
9079
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9080
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9081
+ const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
9082
+ /**
9083
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9084
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9085
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9086
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9087
+ * @see https://docs.x.ai/build/features/hooks
9088
+ */
9089
+ const GROKCLI_HOOKS_DIR_PATH = join(GROKCLI_DIR, "hooks");
9090
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9091
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9092
+ /**
9093
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9094
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9095
+ * (global), each a Markdown file with YAML frontmatter (verified via
9096
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9097
+ */
9098
+ const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
9099
+ /**
9100
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9101
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9102
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9103
+ * MCP/skills/subagents adapters).
9104
+ */
9105
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9106
+ //#endregion
9107
+ //#region src/features/hooks/grokcli-hooks.ts
9108
+ const GROKCLI_CONVERTER_CONFIG = {
9109
+ supportedEvents: GROKCLI_HOOK_EVENTS,
9110
+ canonicalToToolEventNames: CANONICAL_TO_GROKCLI_EVENT_NAMES,
9111
+ toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
9112
+ projectDirVar: "",
9113
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
9114
+ noMatcherEvents: /* @__PURE__ */ new Set([
9115
+ "sessionStart",
9116
+ "sessionEnd",
9117
+ "beforeSubmitPrompt",
9118
+ "stop",
9119
+ "stopFailure",
9120
+ "notification",
9121
+ "subagentStart",
9122
+ "subagentStop",
9123
+ "preCompact",
9124
+ "postCompact"
9125
+ ])
9126
+ };
9127
+ /**
9128
+ * Hooks generator for Grok CLI (xAI Grok Build).
9129
+ *
9130
+ * Grok Build adopts a Claude-Code-compatible lifecycle hooks model: each event
9131
+ * maps to an array of `{ matcher?, hooks: [{ type, command, timeout? }] }`
9132
+ * matcher groups under a top-level `hooks` key. rulesync writes all its hooks
9133
+ * into a single standalone `rulesync.json` file discovered from
9134
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global). The file
9135
+ * is dedicated to hooks and owned wholesale by rulesync, so it may be deleted
9136
+ * as an orphan.
9137
+ *
9138
+ * @see https://docs.x.ai/build/features/hooks
9139
+ */
9140
+ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
9141
+ constructor(params) {
9142
+ super({
9143
+ ...params,
9144
+ fileContent: params.fileContent ?? "{}"
9145
+ });
9146
+ }
9147
+ static getSettablePaths(_options = {}) {
9148
+ return {
9149
+ relativeDirPath: GROKCLI_HOOKS_DIR_PATH,
9150
+ relativeFilePath: GROKCLI_HOOKS_FILE_NAME
9151
+ };
9152
+ }
9153
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9154
+ const paths = GrokcliHooks.getSettablePaths({ global });
9155
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9156
+ return new GrokcliHooks({
9157
+ outputRoot,
9158
+ relativeDirPath: paths.relativeDirPath,
9159
+ relativeFilePath: paths.relativeFilePath,
9160
+ fileContent,
9161
+ validate
9162
+ });
9163
+ }
9164
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9165
+ const paths = GrokcliHooks.getSettablePaths({ global });
9166
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9167
+ const config = rulesyncHooks.getJson();
9168
+ const grokHooks = canonicalToToolHooks({
9169
+ config,
9170
+ toolOverrideHooks: config.grokcli?.hooks,
9171
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
9172
+ logger
9173
+ });
9174
+ await readOrInitializeFileContent(filePath, JSON.stringify({ hooks: {} }, null, 2));
9175
+ const fileContent = JSON.stringify({ hooks: grokHooks }, null, 2);
9176
+ return new GrokcliHooks({
9177
+ outputRoot,
9178
+ relativeDirPath: paths.relativeDirPath,
9179
+ relativeFilePath: paths.relativeFilePath,
9180
+ fileContent,
9181
+ validate
9182
+ });
9183
+ }
9184
+ toRulesyncHooks() {
9185
+ let parsed;
9186
+ try {
9187
+ parsed = JSON.parse(this.getFileContent());
9188
+ } catch (error) {
9189
+ throw new Error(`Failed to parse Grok hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9190
+ }
9191
+ const hooks = toolHooksToCanonical({
9192
+ hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
9193
+ converterConfig: GROKCLI_CONVERTER_CONFIG
9194
+ });
9195
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9196
+ version: 1,
9197
+ hooks
9198
+ }, null, 2) });
9199
+ }
9200
+ validate() {
9201
+ return {
9202
+ success: true,
9203
+ error: null
9204
+ };
9205
+ }
9206
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9207
+ return new GrokcliHooks({
9208
+ outputRoot,
9209
+ relativeDirPath,
9210
+ relativeFilePath,
9211
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9212
+ validate: false
9213
+ });
9214
+ }
9215
+ };
9216
+ //#endregion
8636
9217
  //#region src/features/hooks/hermesagent-hooks.ts
8637
9218
  /**
8638
9219
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -9210,18 +9791,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
9210
9791
  const paths = KiroHooks.getSettablePaths({ global });
9211
9792
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9212
9793
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9213
- let agentConfig;
9214
- try {
9215
- agentConfig = JSON.parse(existingContent);
9216
- } catch (error) {
9217
- throw new Error(`Failed to parse existing Kiro agent config at ${filePath}: ${formatError(error)}`, { cause: error });
9218
- }
9219
9794
  const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson(), this.getOverrideKey());
9220
- const merged = {
9221
- ...agentConfig,
9222
- hooks: kiroHooks
9223
- };
9224
- const fileContent = JSON.stringify(merged, null, 2);
9795
+ const fileContent = applySharedConfigPatch({
9796
+ fileKey: sharedConfigFileKey(paths),
9797
+ feature: "hooks",
9798
+ existingContent,
9799
+ patch: { hooks: kiroHooks },
9800
+ filePath
9801
+ });
9225
9802
  return new KiroHooks({
9226
9803
  outputRoot,
9227
9804
  relativeDirPath: paths.relativeDirPath,
@@ -9716,21 +10293,17 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
9716
10293
  const paths = QwencodeHooks.getSettablePaths({ global });
9717
10294
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9718
10295
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9719
- let settings;
9720
- try {
9721
- settings = JSON.parse(existingContent);
9722
- } catch (error) {
9723
- throw new Error(`Failed to parse existing Qwen Code settings at ${filePath}: ${formatError(error)}`, { cause: error });
9724
- }
9725
10296
  const config = rulesyncHooks.getJson();
9726
- const qwencodeHooks = canonicalToQwencodeHooks(config);
9727
- const merged = {
9728
- ...settings,
9729
- hooks: qwencodeHooks
9730
- };
10297
+ const patch = { hooks: canonicalToQwencodeHooks(config) };
9731
10298
  const disableAllHooks = config.qwencode?.disableAllHooks;
9732
- if (typeof disableAllHooks === "boolean") merged.disableAllHooks = disableAllHooks;
9733
- const fileContent = JSON.stringify(merged, null, 2);
10299
+ if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
10300
+ const fileContent = applySharedConfigPatch({
10301
+ fileKey: sharedConfigFileKey(paths),
10302
+ feature: "hooks",
10303
+ existingContent,
10304
+ patch,
10305
+ filePath
10306
+ });
9734
10307
  return new QwencodeHooks({
9735
10308
  outputRoot,
9736
10309
  relativeDirPath: paths.relativeDirPath,
@@ -9847,9 +10420,9 @@ function reasonixHooksToCanonical(hooks) {
9847
10420
  * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9848
10421
  * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9849
10422
  * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9850
- * The eight upstream events with a clean canonical equivalent are mapped:
10423
+ * All ten upstream events have a clean canonical equivalent and are mapped:
9851
10424
  * PreToolUse/PostToolUse/UserPromptSubmit/Stop plus SessionStart/SessionEnd/
9852
- * SubagentStop/PostLLMCall (see REASONIX_HOOK_EVENTS).
10425
+ * SubagentStop/PostLLMCall/Notification/PreCompact (see REASONIX_HOOK_EVENTS).
9853
10426
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9854
10427
  */
9855
10428
  var ReasonixHooks = class ReasonixHooks extends ToolHooks {
@@ -9989,10 +10562,6 @@ function canonicalToVibeHooks(config, toolOverride) {
9989
10562
  }
9990
10563
  return { hooks };
9991
10564
  }
9992
- /**
9993
- * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
9994
- * into a canonical event → definition[] record.
9995
- */
9996
10565
  /** Convert one raw `[[hooks]]` entry to a canonical definition, or null to skip. */
9997
10566
  function vibeEntryToCanonicalDef(raw) {
9998
10567
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -10012,6 +10581,10 @@ function vibeEntryToCanonicalDef(raw) {
10012
10581
  def
10013
10582
  };
10014
10583
  }
10584
+ /**
10585
+ * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
10586
+ * into a canonical event → definition[] record.
10587
+ */
10015
10588
  function vibeHooksToCanonical(parsed) {
10016
10589
  const canonical = {};
10017
10590
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return canonical;
@@ -10040,15 +10613,22 @@ function parseVibeToml(fileContent) {
10040
10613
  */
10041
10614
  async function buildVibeConfigTomlContent({ outputRoot }) {
10042
10615
  const configPath = join(outputRoot, VIBE_DIR, VIBE_CONFIG_FILE_NAME);
10043
- const existingContent = await readFileContentOrNull(configPath) ?? smolToml.stringify({});
10044
- let config;
10616
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
10045
10617
  try {
10046
- config = parseVibeToml(existingContent);
10618
+ parseVibeToml(existingContent);
10047
10619
  } catch (error) {
10048
10620
  throw new Error(`Failed to parse existing Vibe config at ${configPath}: ${formatError(error)}`, { cause: error });
10049
10621
  }
10050
- config.enable_experimental_hooks = true;
10051
- return smolToml.stringify(config);
10622
+ return applySharedConfigPatch({
10623
+ fileKey: sharedConfigFileKey({
10624
+ relativeDirPath: VIBE_DIR,
10625
+ relativeFilePath: VIBE_CONFIG_FILE_NAME
10626
+ }),
10627
+ feature: "hooks",
10628
+ existingContent,
10629
+ patch: { enable_experimental_hooks: true },
10630
+ filePath: configPath
10631
+ });
10052
10632
  }
10053
10633
  /**
10054
10634
  * Represents the `.vibe/config.toml` file as a generated ToolFile so it goes
@@ -10430,6 +11010,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
10430
11010
  supportedEvents: REASONIX_HOOK_EVENTS,
10431
11011
  supportedHookTypes: ["command"],
10432
11012
  supportsMatcher: true
11013
+ }],
11014
+ ["grokcli", {
11015
+ class: GrokcliHooks,
11016
+ meta: {
11017
+ supportsProject: true,
11018
+ supportsGlobal: true,
11019
+ supportsImport: true
11020
+ },
11021
+ supportedEvents: GROKCLI_HOOK_EVENTS,
11022
+ supportedHookTypes: ["command"],
11023
+ supportsMatcher: true
10433
11024
  }]
10434
11025
  ]);
10435
11026
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -11548,17 +12139,18 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
11548
12139
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
11549
12140
  const filePath = join(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
11550
12141
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
11551
- const existingJsonValue = JSON.parse(existingFileContent);
11552
- const mergedPatterns = uniq([...existingJsonValue.private_files ?? [], ...patterns].toSorted());
11553
- const jsonValue = {
11554
- ...existingJsonValue,
11555
- private_files: mergedPatterns
11556
- };
12142
+ const mergedPatterns = uniq([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
11557
12143
  return new ZedIgnore({
11558
12144
  outputRoot,
11559
12145
  relativeDirPath: this.getSettablePaths().relativeDirPath,
11560
12146
  relativeFilePath: this.getSettablePaths().relativeFilePath,
11561
- fileContent: JSON.stringify(jsonValue, null, 2),
12147
+ fileContent: applySharedConfigPatch({
12148
+ fileKey: sharedConfigFileKey(this.getSettablePaths()),
12149
+ feature: "ignore",
12150
+ existingContent: existingFileContent,
12151
+ patch: { private_files: mergedPatterns },
12152
+ filePath
12153
+ }),
11562
12154
  validate: true
11563
12155
  });
11564
12156
  }
@@ -12015,17 +12607,17 @@ var AmpMcp = class AmpMcp extends ToolMcp {
12015
12607
  const basePaths = this.getSettablePaths({ global });
12016
12608
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
12017
12609
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
12018
- const json = fileContent ? parseAmpSettingsJsonc(fileContent) : { [AMP_MCP_SERVERS_KEY]: {} };
12019
- const filteredMcpServers = filterMcpServers(rulesyncMcp.getMcpServers());
12020
- const newJson = {
12021
- ...json,
12022
- [AMP_MCP_SERVERS_KEY]: filteredMcpServers
12023
- };
12024
12610
  return new AmpMcp({
12025
12611
  outputRoot,
12026
12612
  relativeDirPath: basePaths.relativeDirPath,
12027
12613
  relativeFilePath,
12028
- fileContent: JSON.stringify(newJson, null, 2),
12614
+ fileContent: applySharedConfigPatch({
12615
+ fileKey: sharedConfigFileKey(basePaths),
12616
+ feature: "mcp",
12617
+ existingContent: fileContent ?? "",
12618
+ patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
12619
+ filePath: join(jsonDir, relativeFilePath)
12620
+ }),
12029
12621
  validate,
12030
12622
  global
12031
12623
  });
@@ -12312,15 +12904,19 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12312
12904
  }
12313
12905
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12314
12906
  const paths = this.getSettablePaths({ global });
12315
- const merged = {
12316
- ...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
12317
- mcpServers: rulesyncMcp.getMcpServers()
12318
- };
12907
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
12908
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
12319
12909
  return new AugmentcodeMcp({
12320
12910
  outputRoot,
12321
12911
  relativeDirPath: paths.relativeDirPath,
12322
12912
  relativeFilePath: paths.relativeFilePath,
12323
- fileContent: JSON.stringify(merged, null, 2),
12913
+ fileContent: applySharedConfigPatch({
12914
+ fileKey: sharedConfigFileKey(paths),
12915
+ feature: "mcp",
12916
+ existingContent,
12917
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
12918
+ filePath
12919
+ }),
12324
12920
  validate,
12325
12921
  global
12326
12922
  });
@@ -12680,7 +13276,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12680
13276
  ...rest,
12681
13277
  validate: false
12682
13278
  });
12683
- this.toml = smolToml.parse(this.fileContent);
13279
+ let toml;
13280
+ try {
13281
+ toml = smolToml.parse(this.fileContent);
13282
+ } catch (error) {
13283
+ throw new Error(`Failed to parse Codex CLI config at ${this.getFilePath()}: ${formatError(error)}`, { cause: error });
13284
+ }
13285
+ this.toml = toml;
12684
13286
  if (rest.validate) {
12685
13287
  const result = this.validate();
12686
13288
  if (!result.success) throw result.error;
@@ -12714,8 +13316,14 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12714
13316
  }
12715
13317
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12716
13318
  const paths = this.getSettablePaths({ global });
12717
- const configTomlFileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smolToml.stringify({}));
12718
- const configToml = smolToml.parse(configTomlFileContent);
13319
+ const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13320
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13321
+ let configToml;
13322
+ try {
13323
+ configToml = smolToml.parse(configTomlFileContent || smolToml.stringify({}));
13324
+ } catch (error) {
13325
+ throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
13326
+ }
12719
13327
  const strippedMcpServers = rulesyncMcp.getMcpServers();
12720
13328
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
12721
13329
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
@@ -12728,7 +13336,7 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12728
13336
  const filteredMcpServers = this.removeEmptyEntries(converted);
12729
13337
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
12730
13338
  const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
12731
- configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
13339
+ const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
12732
13340
  const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12733
13341
  const serverRecord = serverConfig;
12734
13342
  if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
@@ -12741,7 +13349,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12741
13349
  outputRoot,
12742
13350
  relativeDirPath: paths.relativeDirPath,
12743
13351
  relativeFilePath: paths.relativeFilePath,
12744
- fileContent: smolToml.stringify(configToml),
13352
+ fileContent: applySharedConfigPatch({
13353
+ fileKey: sharedConfigFileKey(paths),
13354
+ feature: "mcp",
13355
+ existingContent: configTomlFileContent,
13356
+ patch: { mcp_servers: mergedMcpServers },
13357
+ filePath: configTomlFilePath
13358
+ }),
12745
13359
  validate
12746
13360
  });
12747
13361
  }
@@ -13308,16 +13922,19 @@ var DevinMcp = class DevinMcp extends ToolMcp {
13308
13922
  }
13309
13923
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13310
13924
  const paths = this.getSettablePaths({ global });
13311
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
13312
- const devinConfig = {
13313
- ...this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath),
13314
- mcpServers: rulesyncMcp.getMcpServers()
13315
- };
13925
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13926
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
13316
13927
  return new DevinMcp({
13317
13928
  outputRoot,
13318
13929
  relativeDirPath: paths.relativeDirPath,
13319
13930
  relativeFilePath: paths.relativeFilePath,
13320
- fileContent: JSON.stringify(devinConfig, null, 2),
13931
+ fileContent: applySharedConfigPatch({
13932
+ fileKey: sharedConfigFileKey(paths),
13933
+ feature: "mcp",
13934
+ existingContent,
13935
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
13936
+ filePath
13937
+ }),
13321
13938
  validate,
13322
13939
  global
13323
13940
  });
@@ -13680,47 +14297,6 @@ var GooseMcp = class GooseMcp extends ToolMcp {
13680
14297
  }
13681
14298
  };
13682
14299
  //#endregion
13683
- //#region src/constants/grokcli-paths.ts
13684
- /**
13685
- * Grok Build CLI (xAI) configuration-layout conventions.
13686
- *
13687
- * Single source of truth for where Grok Build expects its files. Grok Build
13688
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
13689
- * with project/global scopes resolved by the directory the CLI runs in
13690
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
13691
- *
13692
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
13693
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
13694
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
13695
- * @see https://docs.x.ai/build/overview
13696
- */
13697
- /** Root directory for Grok Build configuration, relative to the scope root. */
13698
- const GROKCLI_DIR = ".grok";
13699
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
13700
- const GROKCLI_MCP_FILE_NAME = "config.toml";
13701
- /**
13702
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
13703
- * permission mode, and other settings all live here; permissions reuse the same
13704
- * file name as MCP since Grok consolidates everything into one config.
13705
- */
13706
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
13707
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
13708
- const GROKCLI_SKILLS_DIR_PATH = join(GROKCLI_DIR, "skills");
13709
- /**
13710
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
13711
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
13712
- * (global), each a Markdown file with YAML frontmatter (verified via
13713
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
13714
- */
13715
- const GROKCLI_AGENTS_DIR_PATH = join(GROKCLI_DIR, "agents");
13716
- /**
13717
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
13718
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
13719
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
13720
- * MCP/skills/subagents adapters).
13721
- */
13722
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
13723
- //#endregion
13724
14300
  //#region src/features/mcp/grokcli-mcp.ts
13725
14301
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
13726
14302
  /**
@@ -13804,17 +14380,22 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
13804
14380
  }
13805
14381
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13806
14382
  const paths = this.getSettablePaths({ global });
13807
- const configTomlFileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smolToml.stringify({}));
13808
- const configToml = smolToml.parse(configTomlFileContent);
14383
+ const configTomlFilePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
14384
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13809
14385
  const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
13810
14386
  const filteredMcpServers = this.removeEmptyEntries(converted);
13811
14387
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the grok CLI config`);
13812
- configToml["mcp_servers"] = filteredMcpServers;
13813
14388
  return new GrokcliMcp({
13814
14389
  outputRoot,
13815
14390
  relativeDirPath: paths.relativeDirPath,
13816
14391
  relativeFilePath: paths.relativeFilePath,
13817
- fileContent: smolToml.stringify(configToml),
14392
+ fileContent: applySharedConfigPatch({
14393
+ fileKey: sharedConfigFileKey(paths),
14394
+ feature: "mcp",
14395
+ existingContent: configTomlFileContent,
14396
+ patch: { mcp_servers: filteredMcpServers },
14397
+ filePath: configTomlFilePath
14398
+ }),
13818
14399
  validate
13819
14400
  });
13820
14401
  }
@@ -13875,6 +14456,54 @@ function resolveHermesTimeout(config) {
13875
14456
  if (typeof config.networkTimeout === "number") return config.networkTimeout;
13876
14457
  }
13877
14458
  /**
14459
+ * Copies the advanced Hermes-recognized per-server fields that have no canonical
14460
+ * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
14461
+ * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
14462
+ * `connect_timeout` (seconds), and `supports_parallel_tool_calls` — verbatim
14463
+ * from `source` to `target`. Field names are identical on both sides (the
14464
+ * canonical `McpServerSchema` is a `looseObject`), so this serves export and
14465
+ * import alike. See the Hermes mcp-config-reference.
14466
+ */
14467
+ function copyHermesAdvancedFields(source, target) {
14468
+ if (typeof source.auth === "string") target.auth = source.auth;
14469
+ if (typeof source.client_cert === "string" || isStringArray(source.client_cert)) target.client_cert = source.client_cert;
14470
+ if (typeof source.client_key === "string") target.client_key = source.client_key;
14471
+ if (typeof source.connect_timeout === "number") target.connect_timeout = source.connect_timeout;
14472
+ if (typeof source.supports_parallel_tool_calls === "boolean") target.supports_parallel_tool_calls = source.supports_parallel_tool_calls;
14473
+ }
14474
+ /**
14475
+ * Builds Hermes's per-server `tools` block from a canonical server config. The
14476
+ * canonical `enabledTools`/`disabledTools` arrays become `include`/`exclude`,
14477
+ * and the boolean `promptsEnabled`/`resourcesEnabled` toggles become Hermes's
14478
+ * `prompts`/`resources` capability flags. Returns an empty object when the
14479
+ * server has no tool scoping (the caller omits the block in that case).
14480
+ *
14481
+ * Note: `promptsEnabled`/`resourcesEnabled` are canonical top-level keys rather
14482
+ * than a nested canonical `tools` object, because canonical `McpServerSchema.tools`
14483
+ * is reserved as a `string[]` (used by other tools) — reusing it for an object
14484
+ * would fail validation on the next `generate`.
14485
+ */
14486
+ function buildHermesToolsBlock(config) {
14487
+ const tools = {};
14488
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14489
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14490
+ if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
14491
+ if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
14492
+ return tools;
14493
+ }
14494
+ /**
14495
+ * Applies a Hermes per-server `tools` block back onto a canonical server config
14496
+ * (inverse of {@link buildHermesToolsBlock}): `include`/`exclude` become
14497
+ * `enabledTools`/`disabledTools`, and `prompts`/`resources` become the boolean
14498
+ * `promptsEnabled`/`resourcesEnabled` top-level toggles.
14499
+ */
14500
+ function applyHermesToolsBlock(hermesTools, server) {
14501
+ if (isStringArray(hermesTools.include)) server.enabledTools = hermesTools.include;
14502
+ if (isStringArray(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
14503
+ if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
14504
+ if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
14505
+ }
14506
+ /**
13878
14507
  * Converts a single rulesync canonical MCP server into a Hermes `mcp_servers:` entry.
13879
14508
  *
13880
14509
  * Hermes is close to the MCP spec but not identical: `command` must be a single
@@ -13908,9 +14537,8 @@ function convertServerToHermes(config) {
13908
14537
  if (config.disabled === true) out.enabled = false;
13909
14538
  const timeout = resolveHermesTimeout(config);
13910
14539
  if (timeout !== void 0) out.timeout = timeout;
13911
- const tools = {};
13912
- if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
13913
- if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14540
+ copyHermesAdvancedFields(config, out);
14541
+ const tools = buildHermesToolsBlock(config);
13914
14542
  if (Object.keys(tools).length > 0) out.tools = tools;
13915
14543
  return out;
13916
14544
  }
@@ -13953,10 +14581,8 @@ function convertFromHermesFormat(mcpServers) {
13953
14581
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
13954
14582
  if (config.enabled === false) server.disabled = true;
13955
14583
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
13956
- if (isRecord(config.tools)) {
13957
- if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
13958
- if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
13959
- }
14584
+ copyHermesAdvancedFields(config, server);
14585
+ if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
13960
14586
  result[name] = server;
13961
14587
  }
13962
14588
  return result;
@@ -14347,20 +14973,21 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14347
14973
  fileContent = await readFileContentOrNull(jsonPath);
14348
14974
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
14349
14975
  }
14350
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14351
- const json = parse(fileContent);
14352
14976
  const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(rulesyncMcp.getMcpServers());
14353
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14354
- const newJson = {
14355
- ...jsonWithoutTools,
14356
- mcp: convertedMcp,
14357
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14358
- };
14359
14977
  return new KiloMcp({
14360
14978
  outputRoot,
14361
14979
  relativeDirPath: basePaths.relativeDirPath,
14362
14980
  relativeFilePath,
14363
- fileContent: JSON.stringify(newJson, null, 2),
14981
+ fileContent: applySharedConfigPatch({
14982
+ fileKey: sharedConfigFileKey(basePaths),
14983
+ feature: "mcp",
14984
+ existingContent: fileContent ?? "",
14985
+ patch: {
14986
+ mcp: convertedMcp,
14987
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
14988
+ },
14989
+ filePath: join(jsonDir, relativeFilePath)
14990
+ }),
14364
14991
  validate
14365
14992
  });
14366
14993
  }
@@ -14389,15 +15016,17 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14389
15016
  const json = fileContent ? parse(fileContent) : {};
14390
15017
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14391
15018
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14392
- const newJson = {
14393
- ...json,
14394
- instructions: mergedInstructions
14395
- };
14396
15019
  return new KiloMcp({
14397
15020
  outputRoot,
14398
15021
  relativeDirPath: basePaths.relativeDirPath,
14399
15022
  relativeFilePath,
14400
- fileContent: JSON.stringify(newJson, null, 2),
15023
+ fileContent: applySharedConfigPatch({
15024
+ fileKey: sharedConfigFileKey(basePaths),
15025
+ feature: "rules",
15026
+ existingContent: fileContent ?? "",
15027
+ patch: { instructions: mergedInstructions },
15028
+ filePath: join(jsonDir, relativeFilePath)
15029
+ }),
14401
15030
  validate
14402
15031
  });
14403
15032
  }
@@ -14669,23 +15298,24 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14669
15298
  fileContent = await readFileContentOrNull(jsonPath);
14670
15299
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
14671
15300
  }
14672
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14673
- const json = parse(fileContent);
14674
15301
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
14675
15302
  mcpServers: rulesyncMcp.getMcpServers(),
14676
15303
  replacement: "{env:$1}"
14677
15304
  }));
14678
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14679
- const newJson = {
14680
- ...jsonWithoutTools,
14681
- mcp: convertedMcp,
14682
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14683
- };
14684
15305
  return new OpencodeMcp({
14685
15306
  outputRoot,
14686
15307
  relativeDirPath: basePaths.relativeDirPath,
14687
15308
  relativeFilePath,
14688
- fileContent: JSON.stringify(newJson, null, 2),
15309
+ fileContent: applySharedConfigPatch({
15310
+ fileKey: sharedConfigFileKey(basePaths),
15311
+ feature: "mcp",
15312
+ existingContent: fileContent ?? "",
15313
+ patch: {
15314
+ mcp: convertedMcp,
15315
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
15316
+ },
15317
+ filePath: join(jsonDir, relativeFilePath)
15318
+ }),
14689
15319
  validate
14690
15320
  });
14691
15321
  }
@@ -14720,15 +15350,17 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14720
15350
  const json = fileContent ? parse(fileContent) : {};
14721
15351
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14722
15352
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14723
- const newJson = {
14724
- ...json,
14725
- instructions: mergedInstructions
14726
- };
14727
15353
  return new OpencodeMcp({
14728
15354
  outputRoot,
14729
15355
  relativeDirPath: basePaths.relativeDirPath,
14730
15356
  relativeFilePath,
14731
- fileContent: JSON.stringify(newJson, null, 2),
15357
+ fileContent: applySharedConfigPatch({
15358
+ fileKey: sharedConfigFileKey(basePaths),
15359
+ feature: "rules",
15360
+ existingContent: fileContent ?? "",
15361
+ patch: { instructions: mergedInstructions },
15362
+ filePath: join(jsonDir, relativeFilePath)
15363
+ }),
14732
15364
  validate
14733
15365
  });
14734
15366
  }
@@ -14838,16 +15470,19 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
14838
15470
  }
14839
15471
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14840
15472
  const paths = this.getSettablePaths({ global });
14841
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
14842
- const newJson = {
14843
- ...JSON.parse(fileContent),
14844
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers())
14845
- };
15473
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15474
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
14846
15475
  return new QwencodeMcp({
14847
15476
  outputRoot,
14848
15477
  relativeDirPath: paths.relativeDirPath,
14849
15478
  relativeFilePath: paths.relativeFilePath,
14850
- fileContent: JSON.stringify(newJson, null, 2),
15479
+ fileContent: applySharedConfigPatch({
15480
+ fileKey: sharedConfigFileKey(paths),
15481
+ feature: "mcp",
15482
+ existingContent,
15483
+ patch: { mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()) },
15484
+ filePath
15485
+ }),
14851
15486
  validate
14852
15487
  });
14853
15488
  }
@@ -14932,13 +15567,20 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
14932
15567
  }
14933
15568
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14934
15569
  const paths = this.getSettablePaths({ global });
14935
- const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14936
- config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
15570
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15571
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15572
+ const plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
14937
15573
  return new ReasonixMcp({
14938
15574
  outputRoot,
14939
15575
  relativeDirPath: paths.relativeDirPath,
14940
15576
  relativeFilePath: paths.relativeFilePath,
14941
- fileContent: smolToml.stringify(config),
15577
+ fileContent: applySharedConfigPatch({
15578
+ fileKey: sharedConfigFileKey(paths),
15579
+ feature: "mcp",
15580
+ existingContent,
15581
+ patch: { plugins },
15582
+ filePath
15583
+ }),
14942
15584
  validate,
14943
15585
  global
14944
15586
  });
@@ -15413,13 +16055,20 @@ var VibeMcp = class VibeMcp extends ToolMcp {
15413
16055
  }
15414
16056
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15415
16057
  const paths = this.getSettablePaths({ global });
15416
- const config = parseVibeConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
15417
- config.mcp_servers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
16058
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16059
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
16060
+ const mcpServers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
15418
16061
  return new VibeMcp({
15419
16062
  outputRoot,
15420
16063
  relativeDirPath: paths.relativeDirPath,
15421
16064
  relativeFilePath: paths.relativeFilePath,
15422
- fileContent: smolToml.stringify(config),
16065
+ fileContent: applySharedConfigPatch({
16066
+ fileKey: sharedConfigFileKey(paths),
16067
+ feature: "mcp",
16068
+ existingContent,
16069
+ patch: { mcp_servers: mcpServers },
16070
+ filePath
16071
+ }),
15423
16072
  validate,
15424
16073
  global
15425
16074
  });
@@ -15650,16 +16299,19 @@ var ZedMcp = class ZedMcp extends ToolMcp {
15650
16299
  }
15651
16300
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15652
16301
  const paths = this.getSettablePaths({ global });
15653
- const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "{}");
15654
- const newJson = {
15655
- ...JSON.parse(fileContent),
15656
- context_servers: rulesyncMcp.getMcpServers()
15657
- };
16302
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16303
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
15658
16304
  return new ZedMcp({
15659
16305
  outputRoot,
15660
16306
  relativeDirPath: paths.relativeDirPath,
15661
16307
  relativeFilePath: paths.relativeFilePath,
15662
- fileContent: JSON.stringify(newJson, null, 2),
16308
+ fileContent: applySharedConfigPatch({
16309
+ fileKey: sharedConfigFileKey(paths),
16310
+ feature: "mcp",
16311
+ existingContent,
16312
+ patch: { context_servers: rulesyncMcp.getMcpServers() },
16313
+ filePath
16314
+ }),
15663
16315
  validate
15664
16316
  });
15665
16317
  }
@@ -16225,18 +16877,27 @@ const CursorPermissionsOverrideSchema = z.looseObject({
16225
16877
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16226
16878
  * autonomy/sandbox controls with no canonical permission category — under
16227
16879
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16228
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16229
- * placed here are merged into the matching `settings.json` group and emitted
16230
- * only for Qwen, while the shared `permission` block continues to drive the
16231
- * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16232
- * passthrough) so any current or future `tools`/`security` key can be authored.
16880
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). It also
16881
+ * exposes `permissions.autoMode` (the Auto Mode classifier config:
16882
+ * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` see
16883
+ * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
16884
+ * likewise has no canonical category. Fields placed here are merged into the
16885
+ * matching `settings.json` group and emitted only for Qwen, while the shared
16886
+ * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
16887
+ * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
16888
+ * `tools`/`security`/`autoMode` key can be authored.
16233
16889
  *
16234
16890
  * @example
16235
- * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
16891
+ * {
16892
+ * "tools": { "approvalMode": "auto-edit" },
16893
+ * "security": { "folderTrust": { "enabled": true } },
16894
+ * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
16895
+ * }
16236
16896
  */
16237
16897
  const QwencodePermissionsOverrideSchema = z.looseObject({
16238
16898
  tools: z.optional(z.looseObject({})),
16239
- security: z.optional(z.looseObject({}))
16899
+ security: z.optional(z.looseObject({})),
16900
+ autoMode: z.optional(z.looseObject({}))
16240
16901
  });
16241
16902
  /**
16242
16903
  * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
@@ -16263,9 +16924,11 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
16263
16924
  * exposes security controls with no canonical per-command allow/ask/deny slot —
16264
16925
  * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16265
16926
  * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16266
- * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
16927
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
16928
+ * `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
16267
16929
  * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16268
- * `interactionMode`). Fields placed here are merged into `settings.json` and
16930
+ * `subagentAutonomyLevel`, `interactionMode`). Fields placed here are merged
16931
+ * into `settings.json` and
16269
16932
  * emitted only for Factory Droid, while the shared `permission` block continues
16270
16933
  * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16271
16934
  *
@@ -16458,6 +17121,41 @@ const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(
16458
17121
  }))
16459
17122
  })) });
16460
17123
  /**
17124
+ * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17125
+ * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
17126
+ * Codex still accepts, so it is included so existing configs round-trip. The
17127
+ * granular table form (`{ granular = { … } }`) is modeled separately in the
17128
+ * override union.
17129
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17130
+ */
17131
+ const CodexApprovalPolicySchema = z.enum([
17132
+ "untrusted",
17133
+ "on-request",
17134
+ "on-failure",
17135
+ "never"
17136
+ ]);
17137
+ /**
17138
+ * Codex CLI's classic sandbox mode. Serialized as a kebab-case string in
17139
+ * `.codex/config.toml`.
17140
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17141
+ */
17142
+ const CodexSandboxModeSchema = z.enum([
17143
+ "read-only",
17144
+ "workspace-write",
17145
+ "danger-full-access"
17146
+ ]);
17147
+ /**
17148
+ * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
17149
+ * value Codex still accepts for backward compatibility, so it is included so
17150
+ * existing configs round-trip through the rulesync model.
17151
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17152
+ */
17153
+ const CodexApprovalsReviewerSchema = z.enum([
17154
+ "user",
17155
+ "auto_review",
17156
+ "guardian_subagent"
17157
+ ]);
17158
+ /**
16461
17159
  * Codex CLI-scoped permission override.
16462
17160
  *
16463
17161
  * Codex CLI's permission surface is richer than the canonical allow/ask/deny
@@ -16466,15 +17164,16 @@ const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(
16466
17164
  * override whose fields are written verbatim as top-level `.codex/config.toml`
16467
17165
  * keys (the override wins per key; existing sibling keys the user set directly
16468
17166
  * are preserved):
16469
- * - `approval_policy` — `untrusted` | `on-request` | `never`, or a
16470
- * `{ granular = { … } }` table (kept verbatim; the granular schema has
16471
- * required fields that are brittle to model as typed keys).
17167
+ * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
17168
+ * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
17169
+ * schema has required fields that are brittle to model as typed keys).
16472
17170
  * - `sandbox_mode` — `read-only` | `workspace-write` | `danger-full-access`,
16473
17171
  * with the sibling `sandbox_workspace_write` table (`network_access`,
16474
17172
  * `writable_roots`, …).
16475
17173
  * - `apps` — per-app tool gating (`apps.<id>.tools.<tool>.approval_mode` /
16476
17174
  * `.enabled`, `apps.<id>.default_tools_approval_mode`).
16477
- * - `approvals_reviewer` — the reviewer-approval surface.
17175
+ * - `approvals_reviewer` — the reviewer-approval surface (`user` | `auto_review`
17176
+ * | `guardian_subagent`), or a table for the richer reviewer config.
16478
17177
  *
16479
17178
  * Two surfaces are deliberately NOT authorable here so the override can never
16480
17179
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -16492,11 +17191,11 @@ const KiroPermissionsOverrideSchema = z.looseObject({ toolsSettings: z.optional(
16492
17191
  * "sandbox_workspace_write": { "network_access": true } }
16493
17192
  */
16494
17193
  const CodexcliPermissionsOverrideSchema = z.looseObject({
16495
- approval_policy: z.optional(z.union([z.string(), z.looseObject({})])),
16496
- sandbox_mode: z.optional(z.string()),
17194
+ approval_policy: z.optional(z.union([CodexApprovalPolicySchema, z.looseObject({})])),
17195
+ sandbox_mode: z.optional(CodexSandboxModeSchema),
16497
17196
  sandbox_workspace_write: z.optional(z.looseObject({})),
16498
17197
  apps: z.optional(z.looseObject({})),
16499
- approvals_reviewer: z.optional(z.union([z.string(), z.looseObject({})]))
17198
+ approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})]))
16500
17199
  });
16501
17200
  /**
16502
17201
  * Permissions configuration.
@@ -16828,25 +17527,29 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16828
17527
  const override = config.amp;
16829
17528
  const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16830
17529
  const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16831
- const newJson = {
16832
- ...json,
16833
- [AMP_TOOLS_DISABLE_KEY]: disable
16834
- };
16835
17530
  const mergedPermissions = mergeAmpPermissions([
16836
17531
  ...permissions,
16837
17532
  ...authoredExtras,
16838
17533
  ...preservedDelegates
16839
17534
  ]);
16840
- if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16841
- else delete newJson[AMP_PERMISSIONS_KEY];
16842
- if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16843
- if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16844
- if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
17535
+ const patch = {
17536
+ [AMP_TOOLS_DISABLE_KEY]: disable,
17537
+ [AMP_PERMISSIONS_KEY]: mergedPermissions.length > 0 ? mergedPermissions : void 0
17538
+ };
17539
+ if (override?.guardedFiles?.allowlist !== void 0) patch[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
17540
+ if (override?.dangerouslyAllowAll !== void 0) patch[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
17541
+ if (override?.mcpPermissions !== void 0) patch[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16845
17542
  return new AmpPermissions({
16846
17543
  outputRoot,
16847
17544
  relativeDirPath: basePaths.relativeDirPath,
16848
17545
  relativeFilePath,
16849
- fileContent: JSON.stringify(newJson, null, 2),
17546
+ fileContent: applySharedConfigPatch({
17547
+ fileKey: sharedConfigFileKey(basePaths),
17548
+ feature: "permissions",
17549
+ existingContent: fileContent ?? "",
17550
+ patch,
17551
+ filePath: join(jsonDir, relativeFilePath)
17552
+ }),
16850
17553
  validate: true
16851
17554
  });
16852
17555
  }
@@ -17788,11 +18491,13 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
17788
18491
  ...preservedBasicEntries,
17789
18492
  ...authoredBasics
17790
18493
  ]);
17791
- const merged = {
17792
- ...settings,
17793
- toolPermissions: [...specialEntries, ...sortedBasic]
17794
- };
17795
- const fileContent = JSON.stringify(merged, null, 2);
18494
+ const fileContent = applySharedConfigPatch({
18495
+ fileKey: sharedConfigFileKey(paths),
18496
+ feature: "permissions",
18497
+ existingContent,
18498
+ patch: { toolPermissions: [...specialEntries, ...sortedBasic] },
18499
+ filePath
18500
+ });
17796
18501
  return new AugmentcodePermissions({
17797
18502
  outputRoot,
17798
18503
  relativeDirPath: paths.relativeDirPath,
@@ -18384,13 +19089,6 @@ const WORKSPACE_WIDE_WRITE_PATTERNS = /* @__PURE__ */ new Set([
18384
19089
  ]);
18385
19090
  const CODEX_MINIMAL_KEY = ":minimal";
18386
19091
  const GLOBAL_WILDCARD_DOMAIN = "*";
18387
- const CODEXCLI_OVERRIDE_KEYS = [
18388
- "approval_policy",
18389
- "sandbox_mode",
18390
- "sandbox_workspace_write",
18391
- "apps",
18392
- "approvals_reviewer"
18393
- ];
18394
19092
  var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18395
19093
  static getSettablePaths(_options = {}) {
18396
19094
  return {
@@ -18414,26 +19112,27 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18414
19112
  }
18415
19113
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
18416
19114
  const paths = this.getSettablePaths({ global });
18417
- const existingContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
18418
- const parsed = toMutableTable(smolToml.parse(existingContent));
19115
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19116
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
19117
+ const existing = toMutableTable(smolToml.parse(existingContent || smolToml.stringify({})));
18419
19118
  const newProfile = convertRulesyncToCodexProfile({
18420
19119
  config: rulesyncPermissions.getJson(),
18421
19120
  logger
18422
19121
  });
18423
- const permissionsTable = toMutableTable(parsed.permissions);
19122
+ const permissionsTable = toMutableTable(existing.permissions);
18424
19123
  const { profile: existingProfile, domainsHadUnknown: existingDomainsHadUnknown } = toCodexProfile(permissionsTable[RULESYNC_PROFILE_NAME]);
18425
- if (existingProfile?.extends !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
18426
- if (existingProfile?.network?.unix_sockets !== void 0) logger?.warn(`Preserving existing "network.unix_sockets" from config. Review these entries manually as they may grant broad system access.`);
18427
- 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.`);
18428
- if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19124
+ warnAboutPreservedProfileState({
19125
+ existingProfile,
19126
+ newProfile,
19127
+ existingDomainsHadUnknown,
19128
+ logger
19129
+ });
18429
19130
  permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
18430
19131
  newProfile,
18431
19132
  existingProfile
18432
19133
  });
18433
- parsed.permissions = permissionsTable;
18434
- parsed.default_permissions = RULESYNC_PROFILE_NAME;
18435
- applyCodexcliOverride({
18436
- parsed,
19134
+ const overridePatch = computeCodexcliOverridePatch({
19135
+ existing,
18437
19136
  override: rulesyncPermissions.getJson().codexcli,
18438
19137
  logger
18439
19138
  });
@@ -18441,7 +19140,17 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18441
19140
  outputRoot,
18442
19141
  relativeDirPath: paths.relativeDirPath,
18443
19142
  relativeFilePath: paths.relativeFilePath,
18444
- fileContent: smolToml.stringify(parsed),
19143
+ fileContent: applySharedConfigPatch({
19144
+ fileKey: sharedConfigFileKey(paths),
19145
+ feature: "permissions",
19146
+ existingContent,
19147
+ patch: {
19148
+ permissions: permissionsTable,
19149
+ default_permissions: RULESYNC_PROFILE_NAME,
19150
+ ...overridePatch
19151
+ },
19152
+ filePath
19153
+ }),
18445
19154
  validate
18446
19155
  });
18447
19156
  }
@@ -18611,6 +19320,12 @@ function toCodexProfile(value) {
18611
19320
  domainsHadUnknown
18612
19321
  };
18613
19322
  }
19323
+ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingDomainsHadUnknown, logger }) {
19324
+ if (existingProfile?.extends !== void 0 && existingProfile.extends !== newProfile.extends) logger?.warn(`Existing "extends" value "${existingProfile.extends}" will be replaced by Rulesync-managed "${newProfile.extends ?? "(none)"}".`);
19325
+ if (existingProfile?.network?.unix_sockets !== void 0) logger?.warn(`Preserving existing "network.unix_sockets" from config. Review these entries manually as they may grant broad system access.`);
19326
+ 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.`);
19327
+ if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19328
+ }
18614
19329
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
18615
19330
  if (!existingProfile) return newProfile;
18616
19331
  const mergedNetwork = { ...newProfile.network };
@@ -18661,8 +19376,9 @@ function toMutableTable(value) {
18661
19376
  function isPlainObject(value) {
18662
19377
  return value !== null && typeof value === "object" && !Array.isArray(value);
18663
19378
  }
18664
- function applyCodexcliOverride({ parsed, override, logger }) {
18665
- if (!override) return;
19379
+ function computeCodexcliOverridePatch({ existing, override, logger }) {
19380
+ const patch = {};
19381
+ if (!override) return patch;
18666
19382
  const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
18667
19383
  for (const [key, value] of Object.entries(override)) {
18668
19384
  if (!allowed.has(key)) {
@@ -18670,12 +19386,13 @@ function applyCodexcliOverride({ parsed, override, logger }) {
18670
19386
  continue;
18671
19387
  }
18672
19388
  if (value === void 0) continue;
18673
- const existing = parsed[key];
18674
- parsed[key] = isPlainObject(existing) && isPlainObject(value) ? {
18675
- ...existing,
19389
+ const existingValue = existing[key];
19390
+ patch[key] = isPlainObject(existingValue) && isPlainObject(value) ? {
19391
+ ...existingValue,
18676
19392
  ...value
18677
19393
  } : value;
18678
19394
  }
19395
+ return patch;
18679
19396
  }
18680
19397
  function extractCodexcliOverride(table) {
18681
19398
  const override = {};
@@ -19236,15 +19953,17 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
19236
19953
  else delete mergedPermissions.ask;
19237
19954
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19238
19955
  else delete mergedPermissions.deny;
19239
- const merged = {
19240
- ...settings,
19241
- permissions: mergedPermissions
19242
- };
19243
19956
  return new DevinPermissions({
19244
19957
  outputRoot,
19245
19958
  relativeDirPath: paths.relativeDirPath,
19246
19959
  relativeFilePath: paths.relativeFilePath,
19247
- fileContent: JSON.stringify(merged, null, 2),
19960
+ fileContent: applySharedConfigPatch({
19961
+ fileKey: sharedConfigFileKey(paths),
19962
+ feature: "permissions",
19963
+ existingContent,
19964
+ patch: { permissions: mergedPermissions },
19965
+ filePath
19966
+ }),
19248
19967
  validate
19249
19968
  });
19250
19969
  }
@@ -19337,9 +20056,11 @@ const FACTORYDROID_OVERRIDE_KEYS = [
19337
20056
  "networkPolicy",
19338
20057
  "sandbox",
19339
20058
  "mcpPolicy",
20059
+ "mcpAutonomyOverrides",
19340
20060
  "enableDroidShield",
19341
20061
  "sessionDefaultSettings",
19342
20062
  "maxAutonomyLevel",
20063
+ "subagentAutonomyLevel",
19343
20064
  "interactionMode"
19344
20065
  ];
19345
20066
  /**
@@ -19697,7 +20418,6 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
19697
20418
  }
19698
20419
  //#endregion
19699
20420
  //#region src/features/permissions/grokcli-permissions.ts
19700
- const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
19701
20421
  const GROKCLI_UI_KEY = "ui";
19702
20422
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
19703
20423
  const GROKCLI_PERMISSION_KEY = "permission";
@@ -19799,13 +20519,19 @@ function parseGrokEntry(entry) {
19799
20519
  * compatibility with older Grok versions: `always-approve` when the config is
19800
20520
  * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
19801
20521
  * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
19802
- * arrays).
19803
- *
19804
- * This surface is **global only** — the adapter syncs the user-level
19805
- * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
19806
- * file; project scope is not modeled here.) The shared config is merged in
20522
+ * arrays). It is a user-level UI setting, so it is only written in global scope
20523
+ * (see below).
20524
+ *
20525
+ * Both scopes are supported: the adapter syncs the project-level
20526
+ * `./.grok/config.toml` (default) and the user-level `~/.grok/config.toml`
20527
+ * (with `--global`). Grok documents that "Project configs are limited to MCP
20528
+ * servers, plugins, and permission rules, not full user configs"
20529
+ * (https://docs.x.ai/build/settings), so the project config carries only the
20530
+ * fine-grained `[permission]` rules; the coarse `[ui] permission_mode` UI toggle
20531
+ * is written in global scope only. The shared config is merged in
19807
20532
  * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
19808
- * tools it models and the `[ui] permission_mode` value, while every other key
20533
+ * tools it models and (in global scope) the `[ui] permission_mode` value, while
20534
+ * every other key
19809
20535
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
19810
20536
  * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
19811
20537
  * file is never deleted.
@@ -19827,7 +20553,6 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19827
20553
  };
19828
20554
  }
19829
20555
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19830
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
19831
20556
  const paths = GrokcliPermissions.getSettablePaths({ global });
19832
20557
  const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
19833
20558
  return new GrokcliPermissions({
@@ -19836,11 +20561,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19836
20561
  relativeFilePath: paths.relativeFilePath,
19837
20562
  fileContent,
19838
20563
  validate,
19839
- global: true
20564
+ global
19840
20565
  });
19841
20566
  }
19842
20567
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
19843
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
19844
20568
  const paths = GrokcliPermissions.getSettablePaths({ global });
19845
20569
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19846
20570
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -19853,25 +20577,32 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19853
20577
  const config = rulesyncPermissions.getJson();
19854
20578
  const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19855
20579
  const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19856
- parsed[GROKCLI_PERMISSION_KEY] = {
20580
+ const permission = {
19857
20581
  ...existingPermission,
19858
20582
  allow: buckets.allow,
19859
20583
  deny: buckets.deny,
19860
20584
  ask: buckets.ask
19861
20585
  };
19862
- const mode = deriveGrokPermissionMode(config);
19863
- const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
19864
- parsed[GROKCLI_UI_KEY] = {
19865
- ...existingUi,
19866
- [GROKCLI_PERMISSION_MODE_KEY]: mode
19867
- };
20586
+ const uiPatch = global ? { [GROKCLI_UI_KEY]: {
20587
+ ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
20588
+ [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
20589
+ } } : {};
19868
20590
  return new GrokcliPermissions({
19869
20591
  outputRoot,
19870
20592
  relativeDirPath: paths.relativeDirPath,
19871
20593
  relativeFilePath: paths.relativeFilePath,
19872
- fileContent: smolToml.stringify(parsed),
20594
+ fileContent: applySharedConfigPatch({
20595
+ fileKey: sharedConfigFileKey(paths),
20596
+ feature: "permissions",
20597
+ existingContent,
20598
+ patch: {
20599
+ [GROKCLI_PERMISSION_KEY]: permission,
20600
+ ...uiPatch
20601
+ },
20602
+ filePath
20603
+ }),
19873
20604
  validate: true,
19874
- global: true
20605
+ global
19875
20606
  });
19876
20607
  }
19877
20608
  toRulesyncPermissions() {
@@ -19892,14 +20623,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19892
20623
  error: null
19893
20624
  };
19894
20625
  }
19895
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
20626
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
19896
20627
  return new GrokcliPermissions({
19897
20628
  outputRoot,
19898
20629
  relativeDirPath,
19899
20630
  relativeFilePath,
19900
20631
  fileContent: "",
19901
20632
  validate: false,
19902
- global: true
20633
+ global
19903
20634
  });
19904
20635
  }
19905
20636
  };
@@ -20572,7 +21303,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
20572
21303
  outputRoot,
20573
21304
  relativeDirPath: paths.relativeDirPath,
20574
21305
  relativeFilePath: paths.relativeFilePath,
20575
- fileContent: JSON.stringify(next, null, 2),
21306
+ fileContent: applySharedConfigPatch({
21307
+ fileKey: sharedConfigFileKey(paths),
21308
+ feature: "permissions",
21309
+ existingContent,
21310
+ patch: {
21311
+ allowedTools: next.allowedTools,
21312
+ toolsSettings: next.toolsSettings
21313
+ },
21314
+ filePath
21315
+ }),
20576
21316
  validate
20577
21317
  });
20578
21318
  }
@@ -20805,6 +21545,22 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
20805
21545
  "notebookedit",
20806
21546
  "agent"
20807
21547
  ]);
21548
+ /**
21549
+ * Translate between rulesync's canonical permission category names and
21550
+ * OpenCode's native permission keys. OpenCode has no `agent` key — subagent
21551
+ * launches are gated by the `task` key (see the documented key list at
21552
+ * https://opencode.ai/docs/permissions/). Without this translation a canonical
21553
+ * `agent: deny` would be written verbatim into `opencode.json` and silently
21554
+ * ignored by OpenCode. Unknown names pass through unchanged.
21555
+ */
21556
+ const CANONICAL_TO_OPENCODE_PERMISSION_KEYS = { agent: "task" };
21557
+ const OPENCODE_TO_CANONICAL_PERMISSION_KEYS = Object.fromEntries(Object.entries(CANONICAL_TO_OPENCODE_PERMISSION_KEYS).map(([canonical, opencode]) => [opencode, canonical]));
21558
+ function toOpencodePermissionKey(canonical) {
21559
+ return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? canonical;
21560
+ }
21561
+ function toCanonicalPermissionKey(opencodeKey) {
21562
+ return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
21563
+ }
20808
21564
  function isSharedPermissionCategory(category) {
20809
21565
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
20810
21566
  }
@@ -20869,21 +21625,24 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
20869
21625
  fileContent = await readFileContentOrNull(jsonPath);
20870
21626
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
20871
21627
  }
20872
- const parsed = parse(fileContent ?? "{}");
20873
21628
  const rulesyncJson = rulesyncPermissions.getJson();
20874
21629
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
20875
- const nextJson = {
20876
- ...parsed,
20877
- permission: {
20878
- ...rulesyncJson.permission,
20879
- ...overridePermission
20880
- }
20881
- };
21630
+ const sharedPermission = {};
21631
+ for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
20882
21632
  return new OpencodePermissions({
20883
21633
  outputRoot,
20884
21634
  relativeDirPath: basePaths.relativeDirPath,
20885
21635
  relativeFilePath,
20886
- fileContent: JSON.stringify(nextJson, null, 2),
21636
+ fileContent: applySharedConfigPatch({
21637
+ fileKey: sharedConfigFileKey(basePaths),
21638
+ feature: "permissions",
21639
+ existingContent: fileContent ?? "",
21640
+ patch: { permission: {
21641
+ ...sharedPermission,
21642
+ ...overridePermission
21643
+ } },
21644
+ filePath: join(jsonDir, relativeFilePath)
21645
+ }),
20887
21646
  validate: true
20888
21647
  });
20889
21648
  }
@@ -20895,8 +21654,11 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
20895
21654
  }
20896
21655
  const shared = {};
20897
21656
  const overrideOnly = {};
20898
- for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
20899
- else overrideOnly[category] = value;
21657
+ for (const [category, value] of Object.entries(rawPermission)) {
21658
+ const canonicalCategory = toCanonicalPermissionKey(category);
21659
+ if (isSharedPermissionCategory(canonicalCategory)) shared[canonicalCategory] = typeof value === "string" ? { "*": value } : value;
21660
+ else overrideOnly[category] = value;
21661
+ }
20900
21662
  const json = Object.keys(overrideOnly).length > 0 ? {
20901
21663
  permission: shared,
20902
21664
  opencode: { permission: overrideOnly }
@@ -21021,6 +21783,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
21021
21783
  "disabled"
21022
21784
  ];
21023
21785
  const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
21786
+ const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
21024
21787
  function asPlainRecord(value) {
21025
21788
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
21026
21789
  }
@@ -21092,20 +21855,24 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21092
21855
  else delete mergedPermissions.ask;
21093
21856
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21094
21857
  else delete mergedPermissions.deny;
21095
- const merged = {
21096
- ...settings,
21097
- permissions: mergedPermissions
21098
- };
21099
21858
  const override = config.qwencode;
21100
- if (override?.tools !== void 0) merged.tools = {
21859
+ if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
21860
+ const patch = { permissions: mergedPermissions };
21861
+ if (override?.tools !== void 0) patch.tools = {
21101
21862
  ...asPlainRecord(settings.tools),
21102
21863
  ...asPlainRecord(override.tools)
21103
21864
  };
21104
- if (override?.security !== void 0) merged.security = {
21865
+ if (override?.security !== void 0) patch.security = {
21105
21866
  ...asPlainRecord(settings.security),
21106
21867
  ...asPlainRecord(override.security)
21107
21868
  };
21108
- const fileContent = JSON.stringify(merged, null, 2);
21869
+ const fileContent = applySharedConfigPatch({
21870
+ fileKey: sharedConfigFileKey(paths),
21871
+ feature: "permissions",
21872
+ existingContent,
21873
+ patch,
21874
+ filePath
21875
+ });
21109
21876
  return new QwencodePermissions({
21110
21877
  outputRoot,
21111
21878
  relativeDirPath: paths.relativeDirPath,
@@ -21132,9 +21899,11 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21132
21899
  });
21133
21900
  const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
21134
21901
  const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
21902
+ const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
21135
21903
  const qwencodeOverride = {};
21136
21904
  if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
21137
21905
  if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
21906
+ if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
21138
21907
  const result = { ...config };
21139
21908
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
21140
21909
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -21340,7 +22109,9 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21340
22109
  }
21341
22110
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21342
22111
  const paths = this.getSettablePaths({ global });
21343
- const parsed = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
22112
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
22113
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
22114
+ const parsed = parseReasonixConfig(existingContent);
21344
22115
  const config = rulesyncPermissions.getJson();
21345
22116
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
21346
22117
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
@@ -21365,25 +22136,27 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21365
22136
  else delete mergedPermissions.ask;
21366
22137
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21367
22138
  else delete mergedPermissions.deny;
21368
- const merged = {
21369
- ...parsed,
21370
- permissions: mergedPermissions
21371
- };
22139
+ const patch = { permissions: mergedPermissions };
21372
22140
  const override = config.reasonix;
21373
- if (override?.sandbox !== void 0) merged.sandbox = {
22141
+ if (override?.sandbox !== void 0) patch.sandbox = {
21374
22142
  ...asReasonixRecord(parsed.sandbox),
21375
22143
  ...asReasonixRecord(override.sandbox)
21376
22144
  };
21377
- if (override?.agent !== void 0) merged.agent = {
22145
+ if (override?.agent !== void 0) patch.agent = {
21378
22146
  ...asReasonixRecord(parsed.agent),
21379
22147
  ...asReasonixRecord(override.agent)
21380
22148
  };
21381
- const fileContent = smolToml.stringify(merged);
21382
22149
  return new ReasonixPermissions({
21383
22150
  outputRoot,
21384
22151
  relativeDirPath: paths.relativeDirPath,
21385
22152
  relativeFilePath: paths.relativeFilePath,
21386
- fileContent,
22153
+ fileContent: applySharedConfigPatch({
22154
+ fileKey: sharedConfigFileKey(paths),
22155
+ feature: "permissions",
22156
+ existingContent,
22157
+ patch,
22158
+ filePath
22159
+ }),
21387
22160
  validate
21388
22161
  });
21389
22162
  }
@@ -21954,7 +22727,9 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
21954
22727
  }
21955
22728
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21956
22729
  const paths = this.getSettablePaths({ global });
21957
- const config = parseVibeConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
22730
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
22731
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
22732
+ const config = parseVibeConfig(existingContent);
21958
22733
  const permission = rulesyncPermissions.getJson().permission;
21959
22734
  const tools = toVibeToolsRecord(config.tools);
21960
22735
  const enabledTools = new Set(toStringArray(config.enabled_tools));
@@ -21999,16 +22774,21 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
21999
22774
  tools[vibeToolName] = nextTool;
22000
22775
  }
22001
22776
  applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
22002
- config.tools = tools;
22003
- if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
22004
- else delete config.enabled_tools;
22005
- if (disabledTools.size > 0) config.disabled_tools = [...disabledTools].toSorted();
22006
- else delete config.disabled_tools;
22007
22777
  return new VibePermissions({
22008
22778
  outputRoot,
22009
22779
  relativeDirPath: paths.relativeDirPath,
22010
22780
  relativeFilePath: paths.relativeFilePath,
22011
- fileContent: smolToml.stringify(config),
22781
+ fileContent: applySharedConfigPatch({
22782
+ fileKey: sharedConfigFileKey(paths),
22783
+ feature: "permissions",
22784
+ existingContent,
22785
+ patch: {
22786
+ tools,
22787
+ enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
22788
+ disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
22789
+ },
22790
+ filePath
22791
+ }),
22012
22792
  validate,
22013
22793
  global
22014
22794
  });
@@ -22471,24 +23251,26 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
22471
23251
  }
22472
23252
  const managedToolNames = new Set(Object.keys(managedTools));
22473
23253
  const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
22474
- const mergedSettings = {
22475
- ...settings,
22476
- agent: {
22477
- ...agent,
22478
- tool_permissions: {
22479
- ...toolPermissions,
22480
- tools: {
22481
- ...preservedTools,
22482
- ...managedTools
22483
- }
22484
- }
22485
- }
22486
- };
22487
23254
  return new ZedPermissions({
22488
23255
  outputRoot,
22489
23256
  relativeDirPath: paths.relativeDirPath,
22490
23257
  relativeFilePath: paths.relativeFilePath,
22491
- fileContent: JSON.stringify(mergedSettings, null, 2),
23258
+ fileContent: applySharedConfigPatch({
23259
+ fileKey: sharedConfigFileKey(paths),
23260
+ feature: "permissions",
23261
+ existingContent,
23262
+ patch: { agent: {
23263
+ ...agent,
23264
+ tool_permissions: {
23265
+ ...toolPermissions,
23266
+ tools: {
23267
+ ...preservedTools,
23268
+ ...managedTools
23269
+ }
23270
+ }
23271
+ } },
23272
+ filePath
23273
+ }),
22492
23274
  validate: true
22493
23275
  });
22494
23276
  }
@@ -22630,7 +23412,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
22630
23412
  ["grokcli", {
22631
23413
  class: GrokcliPermissions,
22632
23414
  meta: {
22633
- supportsProject: false,
23415
+ supportsProject: true,
22634
23416
  supportsGlobal: true,
22635
23417
  supportsImport: true
22636
23418
  }
@@ -38178,8 +38960,13 @@ const collectFactoryPaths = (factory) => [...settablePathsForScope({
38178
38960
  * Derive, from the processor registry, every on-disk file that two or more
38179
38961
  * features read-modify-write. Source of truth the generation step graph's
38180
38962
  * `writesSharedFile` declarations must match.
38963
+ *
38964
+ * `minWriters: 1` widens the result to single-feature files as well — used to
38965
+ * validate `SHARED_CONFIG_OWNERSHIP` declarations for files that route through
38966
+ * the gateway without being cross-feature shared (e.g. a global-scope twin
38967
+ * only one feature writes).
38181
38968
  */
38182
- const deriveSharedFileWriters = () => {
38969
+ const deriveSharedFileWriters = ({ minWriters = 2 } = {}) => {
38183
38970
  const byKey = /* @__PURE__ */ new Map();
38184
38971
  const pathByKey = /* @__PURE__ */ new Map();
38185
38972
  for (const entry of PROCESSOR_REGISTRY) {
@@ -38205,7 +38992,7 @@ const deriveSharedFileWriters = () => {
38205
38992
  }
38206
38993
  const writers = [];
38207
38994
  for (const [key, features] of byKey) {
38208
- if (features.size < 2) continue;
38995
+ if (features.size < minWriters) continue;
38209
38996
  const path = pathByKey.get(key);
38210
38997
  const toolsByFeature = /* @__PURE__ */ new Map();
38211
38998
  for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
@@ -39150,4 +39937,4 @@ async function importPermissionsCore(params) {
39150
39937
  //#endregion
39151
39938
  export { removeTempDirectory as $, RulesyncCommand as A, checkPathTraversal as B, RulesyncHooks as C, RULESYNC_RULES_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, formatError as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Et, findControlCharacter as F, findFilesByGlobs as G, directoryExists as H, ConsoleLogger as I, isSymlink as J, getFileSize as K, JsonLogger as L, stringifyFrontmatter as M, loadYaml as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES as Ot, ConfigResolver as P, removeFile as Q, CLIError as R, HooksProcessor as S, RULESYNC_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Tt, ensureDir as U, createTempDirectory as V, fileExists as W, readFileContent as X, listDirectoryFiles as Y, removeDirectory as Z, RulesyncPermissions as _, RULESYNC_MCP_RELATIVE_FILE_PATH as _t, convertFromTool as a, MAX_FILE_SIZE as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_FILE_NAME as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dt, toPosixPath as et, SkillsProcessor as f, RULESYNC_HOOKS_FILE_NAME as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_FILE_NAME as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ToolTargetSchema as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, ALL_FEATURES_WITH_WILDCARD as kt, SubagentsProcessor as l, RULESYNC_CONFIG_RELATIVE_FILE_PATH as lt, RulesyncSkill as m, RULESYNC_IGNORE_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS as nt, RulesProcessor as o, RULESYNC_AIIGNORE_FILE_NAME as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_RELATIVE_FILE_PATH as pt, getHomeDirectory as q, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rt, RulesyncRule as s, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as st, importFromTool as t, writeFileContent as tt, RulesyncSubagent as u, RULESYNC_CONFIG_SCHEMA_URL as ut, McpProcessor as v, RULESYNC_MCP_SCHEMA_URL as vt, CommandsProcessor as w, RULESYNC_SKILLS_RELATIVE_DIR_PATH as wt, RulesyncIgnore as x, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as xt, RulesyncMcp as y, RULESYNC_OVERVIEW_FILE_NAME as yt, ErrorCodes as z };
39152
39939
 
39153
- //# sourceMappingURL=import-A1yGuzwv.js.map
39940
+ //# sourceMappingURL=import-Dioh9ca8.js.map