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.
@@ -114,22 +114,22 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$249 } = node_path.posix;
117
+ const { join: join$250 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
127
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
128
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
129
129
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
130
130
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
131
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$249(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$249(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
131
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$250(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$250(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
133
133
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
134
134
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
135
135
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
@@ -347,7 +347,8 @@ const hooksProcessorToolTargetTuple = [
347
347
  "junie",
348
348
  "vibe",
349
349
  "qwencode",
350
- "reasonix"
350
+ "reasonix",
351
+ "grokcli"
351
352
  ];
352
353
  const permissionsProcessorToolTargetTuple = [
353
354
  "amp",
@@ -2703,6 +2704,13 @@ const CODEXCLI_MCP_FILE_NAME = "config.toml";
2703
2704
  const CODEXCLI_RULE_FILE_NAME = "AGENTS.md";
2704
2705
  const CODEXCLI_BASH_RULES_FILE_NAME = "rulesync.rules";
2705
2706
  const CODEXCLI_OPENAI_YAML_RELATIVE_PATH = (0, node_path.join)("agents", "openai.yaml");
2707
+ const CODEXCLI_OVERRIDE_KEYS = [
2708
+ "approval_policy",
2709
+ "sandbox_mode",
2710
+ "sandbox_workspace_write",
2711
+ "apps",
2712
+ "approvals_reviewer"
2713
+ ];
2706
2714
  //#endregion
2707
2715
  //#region src/features/commands/codexcli-command.ts
2708
2716
  const CodexcliCommandFrontmatterSchema = zod_mini.z.looseObject({
@@ -4082,14 +4090,22 @@ function sanitizeSharedConfigValue(value) {
4082
4090
  * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
4083
4091
  * path when one is given.
4084
4092
  */
4085
- function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
4093
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty", jsoncParseErrors = "tolerate" }) {
4086
4094
  if (fileContent.trim() === "") return {};
4087
4095
  const at = filePath === void 0 ? "" : ` at ${filePath}`;
4088
4096
  let parsed;
4089
4097
  try {
4090
4098
  if (format === "yaml") parsed = loadYaml(fileContent);
4099
+ else if (format === "toml") parsed = (0, smol_toml.parse)(fileContent);
4091
4100
  else if (format === "json") parsed = JSON.parse(fileContent);
4092
- else parsed = (0, jsonc_parser.parse)(fileContent);
4101
+ else if (jsoncParseErrors === "error") {
4102
+ const errors = [];
4103
+ parsed = (0, jsonc_parser.parse)(fileContent, errors, { allowTrailingComma: true });
4104
+ if (errors.length > 0) {
4105
+ const details = errors.map((error) => `${(0, jsonc_parser.printParseErrorCode)(error.error)} at offset ${error.offset}`).join(", ");
4106
+ throw new Error(details);
4107
+ }
4108
+ } else parsed = (0, jsonc_parser.parse)(fileContent);
4093
4109
  } catch (error) {
4094
4110
  throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4095
4111
  }
@@ -4103,13 +4119,15 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
4103
4119
  /**
4104
4120
  * Serialize a shared config document. YAML output always ends with exactly one
4105
4121
  * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4106
- * writers have always emitted (no trailing newline).
4122
+ * writers have always emitted (no trailing newline); TOML output matches the
4123
+ * `smol-toml` `stringify` shape the TOML writers have always emitted.
4107
4124
  */
4108
4125
  function stringifySharedConfig({ format, document }) {
4109
4126
  if (format === "yaml") return (0, js_yaml.dump)(document, {
4110
4127
  noRefs: true,
4111
4128
  sortKeys: false
4112
4129
  }).trimEnd() + "\n";
4130
+ if (format === "toml") return (0, smol_toml.stringify)(document);
4113
4131
  return JSON.stringify(document, null, 2);
4114
4132
  }
4115
4133
  /**
@@ -4148,6 +4166,25 @@ function mergeSharedConfigDeep({ base, patch }) {
4148
4166
  const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4149
4167
  const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4150
4168
  const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4169
+ const CODEXCLI_CONFIG_SHARED_FILE_KEY = ".codex/config.toml";
4170
+ const GROKCLI_CONFIG_SHARED_FILE_KEY = ".grok/config.toml";
4171
+ const VIBE_CONFIG_SHARED_FILE_KEY = ".vibe/config.toml";
4172
+ const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
4173
+ const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
4174
+ /**
4175
+ * Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
4176
+ * Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
4177
+ * feature classes don't pull the processor registry through this module and
4178
+ * create an import cycle); the ownership lock-step test keeps the two aligned.
4179
+ * Lets a tool whose file lives at a scope-dependent path (`.zed/settings.json`
4180
+ * vs `.config/zed/settings.json`) resolve its declaration from the settable
4181
+ * paths it already holds.
4182
+ */
4183
+ const sharedConfigFileKey = ({ relativeDirPath, relativeFilePath }) => {
4184
+ const dir = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
4185
+ const file = relativeFilePath.replace(/\\/g, "/");
4186
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
4187
+ };
4151
4188
  /**
4152
4189
  * Who owns what in each gateway-managed shared config file, and which policy
4153
4190
  * resolves conflicts. Keys are `dir/file` tokens matching
@@ -4204,6 +4241,298 @@ const SHARED_CONFIG_OWNERSHIP = {
4204
4241
  },
4205
4242
  permissions: { kind: "deep-merge" }
4206
4243
  }
4244
+ },
4245
+ ".zed/settings.json": {
4246
+ format: "json",
4247
+ features: {
4248
+ ignore: {
4249
+ kind: "replace-owned-keys",
4250
+ ownedKeys: ["private_files"]
4251
+ },
4252
+ mcp: {
4253
+ kind: "replace-owned-keys",
4254
+ ownedKeys: ["context_servers"]
4255
+ },
4256
+ permissions: {
4257
+ kind: "replace-owned-keys",
4258
+ ownedKeys: ["agent"]
4259
+ }
4260
+ }
4261
+ },
4262
+ ".config/zed/settings.json": {
4263
+ format: "json",
4264
+ features: {
4265
+ mcp: {
4266
+ kind: "replace-owned-keys",
4267
+ ownedKeys: ["context_servers"]
4268
+ },
4269
+ permissions: {
4270
+ kind: "replace-owned-keys",
4271
+ ownedKeys: ["agent"]
4272
+ }
4273
+ }
4274
+ },
4275
+ ".qwen/settings.json": {
4276
+ format: "json",
4277
+ features: {
4278
+ mcp: {
4279
+ kind: "replace-owned-keys",
4280
+ ownedKeys: ["mcpServers"]
4281
+ },
4282
+ hooks: {
4283
+ kind: "replace-owned-keys",
4284
+ ownedKeys: ["hooks", "disableAllHooks"]
4285
+ },
4286
+ permissions: {
4287
+ kind: "replace-owned-keys",
4288
+ ownedKeys: [
4289
+ "permissions",
4290
+ "tools",
4291
+ "security"
4292
+ ]
4293
+ }
4294
+ }
4295
+ },
4296
+ ".augment/settings.json": {
4297
+ format: "json",
4298
+ features: {
4299
+ mcp: {
4300
+ kind: "replace-owned-keys",
4301
+ ownedKeys: ["mcpServers"]
4302
+ },
4303
+ hooks: {
4304
+ kind: "replace-owned-keys",
4305
+ ownedKeys: ["hooks"]
4306
+ },
4307
+ permissions: {
4308
+ kind: "replace-owned-keys",
4309
+ ownedKeys: ["toolPermissions"]
4310
+ }
4311
+ }
4312
+ },
4313
+ ".devin/config.json": {
4314
+ format: "json",
4315
+ features: {
4316
+ mcp: {
4317
+ kind: "replace-owned-keys",
4318
+ ownedKeys: ["mcpServers"]
4319
+ },
4320
+ permissions: {
4321
+ kind: "replace-owned-keys",
4322
+ ownedKeys: ["permissions"]
4323
+ }
4324
+ }
4325
+ },
4326
+ ".config/devin/config.json": {
4327
+ format: "json",
4328
+ features: {
4329
+ mcp: {
4330
+ kind: "replace-owned-keys",
4331
+ ownedKeys: ["mcpServers"]
4332
+ },
4333
+ hooks: {
4334
+ kind: "replace-owned-keys",
4335
+ ownedKeys: ["hooks"]
4336
+ },
4337
+ permissions: {
4338
+ kind: "replace-owned-keys",
4339
+ ownedKeys: ["permissions"]
4340
+ }
4341
+ }
4342
+ },
4343
+ ".kiro/agents/default.json": {
4344
+ format: "json",
4345
+ features: {
4346
+ hooks: {
4347
+ kind: "replace-owned-keys",
4348
+ ownedKeys: ["hooks"]
4349
+ },
4350
+ permissions: {
4351
+ kind: "replace-owned-keys",
4352
+ ownedKeys: ["allowedTools", "toolsSettings"]
4353
+ }
4354
+ }
4355
+ },
4356
+ ".amp/settings.json": {
4357
+ format: "jsonc",
4358
+ invalidRootPolicy: "error",
4359
+ jsoncParseErrors: "error",
4360
+ features: {
4361
+ mcp: {
4362
+ kind: "replace-owned-keys",
4363
+ ownedKeys: ["amp.mcpServers"]
4364
+ },
4365
+ permissions: {
4366
+ kind: "replace-owned-keys",
4367
+ ownedKeys: [
4368
+ "amp.tools.disable",
4369
+ "amp.permissions",
4370
+ "amp.guardedFiles.allowlist",
4371
+ "amp.dangerouslyAllowAll",
4372
+ "amp.mcpPermissions"
4373
+ ]
4374
+ }
4375
+ }
4376
+ },
4377
+ ".config/amp/settings.json": {
4378
+ format: "jsonc",
4379
+ invalidRootPolicy: "error",
4380
+ jsoncParseErrors: "error",
4381
+ features: {
4382
+ mcp: {
4383
+ kind: "replace-owned-keys",
4384
+ ownedKeys: ["amp.mcpServers"]
4385
+ },
4386
+ permissions: {
4387
+ kind: "replace-owned-keys",
4388
+ ownedKeys: [
4389
+ "amp.tools.disable",
4390
+ "amp.permissions",
4391
+ "amp.guardedFiles.allowlist",
4392
+ "amp.dangerouslyAllowAll",
4393
+ "amp.mcpPermissions"
4394
+ ]
4395
+ }
4396
+ }
4397
+ },
4398
+ "opencode.json": {
4399
+ format: "jsonc",
4400
+ features: {
4401
+ mcp: {
4402
+ kind: "replace-owned-keys",
4403
+ ownedKeys: ["mcp", "tools"]
4404
+ },
4405
+ permissions: {
4406
+ kind: "replace-owned-keys",
4407
+ ownedKeys: ["permission"]
4408
+ },
4409
+ rules: {
4410
+ kind: "replace-owned-keys",
4411
+ ownedKeys: ["instructions"]
4412
+ }
4413
+ }
4414
+ },
4415
+ ".config/opencode/opencode.json": {
4416
+ format: "jsonc",
4417
+ features: {
4418
+ mcp: {
4419
+ kind: "replace-owned-keys",
4420
+ ownedKeys: ["mcp", "tools"]
4421
+ },
4422
+ permissions: {
4423
+ kind: "replace-owned-keys",
4424
+ ownedKeys: ["permission"]
4425
+ }
4426
+ }
4427
+ },
4428
+ "kilo.json": {
4429
+ format: "jsonc",
4430
+ features: {
4431
+ mcp: {
4432
+ kind: "replace-owned-keys",
4433
+ ownedKeys: ["mcp", "tools"]
4434
+ },
4435
+ rules: {
4436
+ kind: "replace-owned-keys",
4437
+ ownedKeys: ["instructions"]
4438
+ }
4439
+ }
4440
+ },
4441
+ ".config/kilo/kilo.json": {
4442
+ format: "jsonc",
4443
+ features: { mcp: {
4444
+ kind: "replace-owned-keys",
4445
+ ownedKeys: ["mcp", "tools"]
4446
+ } }
4447
+ },
4448
+ [CODEXCLI_CONFIG_SHARED_FILE_KEY]: {
4449
+ format: "toml",
4450
+ features: {
4451
+ hooks: {
4452
+ kind: "replace-owned-keys",
4453
+ ownedKeys: ["features"]
4454
+ },
4455
+ mcp: {
4456
+ kind: "replace-owned-keys",
4457
+ ownedKeys: ["mcp_servers"]
4458
+ },
4459
+ permissions: {
4460
+ kind: "replace-owned-keys",
4461
+ ownedKeys: [
4462
+ "permissions",
4463
+ "default_permissions",
4464
+ ...CODEXCLI_OVERRIDE_KEYS
4465
+ ]
4466
+ }
4467
+ }
4468
+ },
4469
+ [GROKCLI_CONFIG_SHARED_FILE_KEY]: {
4470
+ format: "toml",
4471
+ features: {
4472
+ mcp: {
4473
+ kind: "replace-owned-keys",
4474
+ ownedKeys: ["mcp_servers"]
4475
+ },
4476
+ permissions: {
4477
+ kind: "replace-owned-keys",
4478
+ ownedKeys: ["permission", "ui"]
4479
+ }
4480
+ }
4481
+ },
4482
+ [VIBE_CONFIG_SHARED_FILE_KEY]: {
4483
+ format: "toml",
4484
+ features: {
4485
+ hooks: {
4486
+ kind: "replace-owned-keys",
4487
+ ownedKeys: ["enable_experimental_hooks"]
4488
+ },
4489
+ mcp: {
4490
+ kind: "replace-owned-keys",
4491
+ ownedKeys: ["mcp_servers"]
4492
+ },
4493
+ permissions: {
4494
+ kind: "replace-owned-keys",
4495
+ ownedKeys: [
4496
+ "tools",
4497
+ "enabled_tools",
4498
+ "disabled_tools"
4499
+ ]
4500
+ }
4501
+ }
4502
+ },
4503
+ [REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY]: {
4504
+ format: "toml",
4505
+ features: {
4506
+ mcp: {
4507
+ kind: "replace-owned-keys",
4508
+ ownedKeys: ["plugins"]
4509
+ },
4510
+ permissions: {
4511
+ kind: "replace-owned-keys",
4512
+ ownedKeys: [
4513
+ "permissions",
4514
+ "sandbox",
4515
+ "agent"
4516
+ ]
4517
+ }
4518
+ }
4519
+ },
4520
+ [REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY]: {
4521
+ format: "toml",
4522
+ features: {
4523
+ mcp: {
4524
+ kind: "replace-owned-keys",
4525
+ ownedKeys: ["plugins"]
4526
+ },
4527
+ permissions: {
4528
+ kind: "replace-owned-keys",
4529
+ ownedKeys: [
4530
+ "permissions",
4531
+ "sandbox",
4532
+ "agent"
4533
+ ]
4534
+ }
4535
+ }
4207
4536
  }
4208
4537
  };
4209
4538
  /**
@@ -4224,17 +4553,20 @@ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, file
4224
4553
  format: declaration.format,
4225
4554
  fileContent: existingContent,
4226
4555
  filePath,
4227
- ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4556
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy },
4557
+ ...declaration.jsoncParseErrors !== void 0 && { jsoncParseErrors: declaration.jsoncParseErrors }
4228
4558
  });
4229
4559
  if (policy.kind === "replace-owned-keys") {
4230
4560
  const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4231
4561
  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.`);
4562
+ const document = mergeSharedConfigShallow({
4563
+ base,
4564
+ patch
4565
+ });
4566
+ for (const [key, value] of Object.entries(patch)) if (value === void 0) delete document[key];
4232
4567
  return stringifySharedConfig({
4233
4568
  format: declaration.format,
4234
- document: mergeSharedConfigShallow({
4235
- base,
4236
- patch
4237
- })
4569
+ document
4238
4570
  });
4239
4571
  }
4240
4572
  const merged = mergeSharedConfigDeep({
@@ -5964,10 +6296,13 @@ const CLAUDE_HOOK_EVENTS = [
5964
6296
  * Hook events supported by Devin Local (native `.devin/` hooks).
5965
6297
  *
5966
6298
  * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
5967
- * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
5968
- * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
6299
+ * eight events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
6300
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, `SessionEnd`, and
6301
+ * `PostCompaction` (fires after context compaction; added in the Devin CLI
6302
+ * stable changelog — https://docs.devin.ai/cli/changelog/stable). The
5969
6303
  * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
5970
- * a `matcher` (regex against `tool_name`); the session/turn events do not.
6304
+ * a `matcher` (regex against `tool_name`); the session/turn/compaction events
6305
+ * do not.
5971
6306
  *
5972
6307
  * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
5973
6308
  * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
@@ -5982,7 +6317,8 @@ const DEVIN_HOOK_EVENTS = [
5982
6317
  "postToolUse",
5983
6318
  "beforeSubmitPrompt",
5984
6319
  "stop",
5985
- "permissionRequest"
6320
+ "permissionRequest",
6321
+ "postCompact"
5986
6322
  ];
5987
6323
  /** Hook events supported by OpenCode. */
5988
6324
  const OPENCODE_HOOK_EVENTS = [
@@ -6176,7 +6512,7 @@ const ANTIGRAVITY_HOOK_EVENTS = [
6176
6512
  * Hook events supported by AugmentCode (Auggie CLI).
6177
6513
  * Auggie mirrors Claude Code's lifecycle hooks but exposes a smaller set:
6178
6514
  * PreToolUse / PostToolUse (tool events, matcher-aware) plus the
6179
- * SessionStart / SessionEnd / Stop session events (no matcher).
6515
+ * SessionStart / SessionEnd / Stop / Notification session events (no matcher).
6180
6516
  * @see https://docs.augmentcode.com/cli/hooks
6181
6517
  */
6182
6518
  const AUGMENTCODE_HOOK_EVENTS = [
@@ -6184,7 +6520,8 @@ const AUGMENTCODE_HOOK_EVENTS = [
6184
6520
  "postToolUse",
6185
6521
  "sessionStart",
6186
6522
  "sessionEnd",
6187
- "stop"
6523
+ "stop",
6524
+ "notification"
6188
6525
  ];
6189
6526
  /**
6190
6527
  * Hook events supported by Mistral Vibe (mistral-vibe).
@@ -6236,6 +6573,10 @@ const JUNIE_HOOK_EVENTS = [
6236
6573
  * The Qwen-specific events
6237
6574
  * `TodoCreated`, `TodoCompleted`, and `StopFailure` map to the canonical
6238
6575
  * `todoCreated`, `todoCompleted`, and `stopFailure` events respectively.
6576
+ * Qwen's `HookEventName` enum (`packages/core/src/hooks/types.ts`) also documents
6577
+ * `PostToolBatch`, `UserPromptExpansion`, `PermissionDenied`, and
6578
+ * `InstructionsLoaded`, which map to the canonical `postToolBatch`,
6579
+ * `userPromptExpansion`, `permissionDenied`, and `instructionsLoaded` events.
6239
6580
  * @see https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
6240
6581
  */
6241
6582
  const QWENCODE_HOOK_EVENTS = [
@@ -6244,7 +6585,9 @@ const QWENCODE_HOOK_EVENTS = [
6244
6585
  "preToolUse",
6245
6586
  "postToolUse",
6246
6587
  "postToolUseFailure",
6588
+ "postToolBatch",
6247
6589
  "beforeSubmitPrompt",
6590
+ "userPromptExpansion",
6248
6591
  "stop",
6249
6592
  "stopFailure",
6250
6593
  "subagentStart",
@@ -6252,7 +6595,9 @@ const QWENCODE_HOOK_EVENTS = [
6252
6595
  "preCompact",
6253
6596
  "postCompact",
6254
6597
  "permissionRequest",
6598
+ "permissionDenied",
6255
6599
  "notification",
6600
+ "instructionsLoaded",
6256
6601
  "todoCreated",
6257
6602
  "todoCompleted"
6258
6603
  ];
@@ -6262,11 +6607,11 @@ const QWENCODE_HOOK_EVENTS = [
6262
6607
  * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
6263
6608
  * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
6264
6609
  * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
6265
- * `SubagentStop`, `Notification`, `PreCompact`). The eight events with a clean
6266
- * canonical equivalent are mapped: `PreToolUse`, `PostToolUse`,
6610
+ * `SubagentStop`, `Notification`, `PreCompact`). All ten have a clean canonical
6611
+ * equivalent and are mapped: `PreToolUse`, `PostToolUse`,
6267
6612
  * `UserPromptSubmit` ← `beforeSubmitPrompt`, `Stop`, `SessionStart`,
6268
- * `SessionEnd`, `SubagentStop`, and `PostLLMCall` ← `postModelInvocation`.
6269
- * (`Notification` and `PreCompact` have no canonical event and are left out.)
6613
+ * `SessionEnd`, `SubagentStop`, `PostLLMCall` ← `postModelInvocation`,
6614
+ * `Notification` ← `notification`, and `PreCompact` `preCompact`.
6270
6615
  * `match` (Reasonix's matcher field name) is honored only on
6271
6616
  * `PreToolUse`/`PostToolUse`, matching the canonical `matcher` field's
6272
6617
  * tool-event scoping used by other adapters.
@@ -6280,7 +6625,39 @@ const REASONIX_HOOK_EVENTS = [
6280
6625
  "sessionStart",
6281
6626
  "sessionEnd",
6282
6627
  "subagentStop",
6283
- "postModelInvocation"
6628
+ "postModelInvocation",
6629
+ "notification",
6630
+ "preCompact"
6631
+ ];
6632
+ /**
6633
+ * Hook events supported by Grok CLI (xAI Grok Build).
6634
+ *
6635
+ * Grok Build documents a Claude-Code-compatible hooks surface with fourteen
6636
+ * PascalCase events, all of which map 1:1 onto an existing canonical arm:
6637
+ * `SessionStart`, `SessionEnd`, `UserPromptSubmit` ← `beforeSubmitPrompt`,
6638
+ * `PreToolUse`, `PostToolUse`, `PostToolUseFailure`, `PermissionDenied`,
6639
+ * `Stop`, `StopFailure`, `Notification`, `SubagentStart`, `SubagentStop`,
6640
+ * `PreCompact`, `PostCompact`. A `matcher` (a regex tested against the tool
6641
+ * name) is meaningful only on the tool-name events (`PreToolUse`, `PostToolUse`,
6642
+ * `PostToolUseFailure`, `PermissionDenied`), matching Claude Code's semantics;
6643
+ * the remaining lifecycle events are matcher-less.
6644
+ * @see https://docs.x.ai/build/features/hooks
6645
+ */
6646
+ const GROKCLI_HOOK_EVENTS = [
6647
+ "sessionStart",
6648
+ "sessionEnd",
6649
+ "beforeSubmitPrompt",
6650
+ "preToolUse",
6651
+ "postToolUse",
6652
+ "postToolUseFailure",
6653
+ "permissionDenied",
6654
+ "stop",
6655
+ "stopFailure",
6656
+ "notification",
6657
+ "subagentStart",
6658
+ "subagentStop",
6659
+ "preCompact",
6660
+ "postCompact"
6284
6661
  ];
6285
6662
  /**
6286
6663
  * Hook events supported by Hermes Agent's native Shell Hooks system.
@@ -6354,6 +6731,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
6354
6731
  junie: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6355
6732
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6356
6733
  reasonix: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6734
+ grokcli: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
6357
6735
  qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
6358
6736
  hooks: zod_mini.z.optional(hooksRecordSchema),
6359
6737
  disableAllHooks: zod_mini.z.optional(zod_mini.z.boolean())
@@ -6412,7 +6790,8 @@ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6412
6790
  postToolUse: "PostToolUse",
6413
6791
  beforeSubmitPrompt: "UserPromptSubmit",
6414
6792
  stop: "Stop",
6415
- permissionRequest: "PermissionRequest"
6793
+ permissionRequest: "PermissionRequest",
6794
+ postCompact: "PostCompaction"
6416
6795
  };
6417
6796
  /**
6418
6797
  * Map Devin Local PascalCase event names to canonical camelCase.
@@ -6427,7 +6806,8 @@ const CANONICAL_TO_AUGMENTCODE_EVENT_NAMES = {
6427
6806
  postToolUse: "PostToolUse",
6428
6807
  sessionStart: "SessionStart",
6429
6808
  sessionEnd: "SessionEnd",
6430
- stop: "Stop"
6809
+ stop: "Stop",
6810
+ notification: "Notification"
6431
6811
  };
6432
6812
  /**
6433
6813
  * Map AugmentCode PascalCase event names to canonical camelCase.
@@ -6697,7 +7077,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
6697
7077
  preToolUse: "PreToolUse",
6698
7078
  postToolUse: "PostToolUse",
6699
7079
  postToolUseFailure: "PostToolUseFailure",
7080
+ postToolBatch: "PostToolBatch",
6700
7081
  beforeSubmitPrompt: "UserPromptSubmit",
7082
+ userPromptExpansion: "UserPromptExpansion",
6701
7083
  stop: "Stop",
6702
7084
  subagentStart: "SubagentStart",
6703
7085
  subagentStop: "SubagentStop",
@@ -6705,7 +7087,9 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
6705
7087
  preCompact: "PreCompact",
6706
7088
  postCompact: "PostCompact",
6707
7089
  permissionRequest: "PermissionRequest",
7090
+ permissionDenied: "PermissionDenied",
6708
7091
  notification: "Notification",
7092
+ instructionsLoaded: "InstructionsLoaded",
6709
7093
  todoCreated: "TodoCreated",
6710
7094
  todoCompleted: "TodoCompleted"
6711
7095
  };
@@ -6727,12 +7111,40 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6727
7111
  sessionStart: "SessionStart",
6728
7112
  sessionEnd: "SessionEnd",
6729
7113
  subagentStop: "SubagentStop",
6730
- postModelInvocation: "PostLLMCall"
7114
+ postModelInvocation: "PostLLMCall",
7115
+ notification: "Notification",
7116
+ preCompact: "PreCompact"
6731
7117
  };
6732
7118
  /**
6733
7119
  * Map Reasonix PascalCase event names to canonical camelCase.
6734
7120
  */
6735
7121
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
7122
+ /**
7123
+ * Map canonical camelCase event names to Grok CLI PascalCase.
7124
+ * Grok Build reuses the same Claude-style PascalCase event names, so each
7125
+ * canonical arm maps to its PascalCase equivalent.
7126
+ * @see https://docs.x.ai/build/features/hooks
7127
+ */
7128
+ const CANONICAL_TO_GROKCLI_EVENT_NAMES = {
7129
+ sessionStart: "SessionStart",
7130
+ sessionEnd: "SessionEnd",
7131
+ beforeSubmitPrompt: "UserPromptSubmit",
7132
+ preToolUse: "PreToolUse",
7133
+ postToolUse: "PostToolUse",
7134
+ postToolUseFailure: "PostToolUseFailure",
7135
+ permissionDenied: "PermissionDenied",
7136
+ stop: "Stop",
7137
+ stopFailure: "StopFailure",
7138
+ notification: "Notification",
7139
+ subagentStart: "SubagentStart",
7140
+ subagentStop: "SubagentStop",
7141
+ preCompact: "PreCompact",
7142
+ postCompact: "PostCompact"
7143
+ };
7144
+ /**
7145
+ * Map Grok CLI PascalCase event names to canonical camelCase.
7146
+ */
7147
+ const GROKCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_GROKCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
6736
7148
  //#endregion
6737
7149
  //#region src/features/hooks/tool-hooks-converter.ts
6738
7150
  function isToolMatcherEntry(x) {
@@ -7257,7 +7669,8 @@ const AUGMENTCODE_CONVERTER_CONFIG = {
7257
7669
  noMatcherEvents: /* @__PURE__ */ new Set([
7258
7670
  "sessionStart",
7259
7671
  "sessionEnd",
7260
- "stop"
7672
+ "stop",
7673
+ "notification"
7261
7674
  ])
7262
7675
  };
7263
7676
  /**
@@ -7307,12 +7720,6 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7307
7720
  const paths = AugmentcodeHooks.getSettablePaths({ global });
7308
7721
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7309
7722
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7310
- let settings;
7311
- try {
7312
- settings = JSON.parse(existingContent);
7313
- } catch (error) {
7314
- throw new Error(`Failed to parse existing AugmentCode settings at ${filePath}: ${formatError(error)}`, { cause: error });
7315
- }
7316
7723
  const config = rulesyncHooks.getJson();
7317
7724
  const augmentHooks = canonicalToToolHooks({
7318
7725
  config,
@@ -7320,11 +7727,13 @@ var AugmentcodeHooks = class AugmentcodeHooks extends ToolHooks {
7320
7727
  converterConfig: AUGMENTCODE_CONVERTER_CONFIG,
7321
7728
  logger
7322
7729
  });
7323
- const merged = {
7324
- ...settings,
7325
- hooks: augmentHooks
7326
- };
7327
- const fileContent = JSON.stringify(merged, null, 2);
7730
+ const fileContent = applySharedConfigPatch({
7731
+ fileKey: sharedConfigFileKey(paths),
7732
+ feature: "hooks",
7733
+ existingContent,
7734
+ patch: { hooks: augmentHooks },
7735
+ filePath
7736
+ });
7328
7737
  return new AugmentcodeHooks({
7329
7738
  outputRoot,
7330
7739
  relativeDirPath: paths.relativeDirPath,
@@ -7491,15 +7900,28 @@ const CODEXCLI_CONVERTER_CONFIG = {
7491
7900
  */
7492
7901
  async function buildCodexConfigTomlContent({ outputRoot }) {
7493
7902
  const configPath = (0, node_path.join)(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
7494
- const existingContent = await readFileContentOrNull(configPath) ?? smol_toml.stringify({});
7903
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
7495
7904
  let configToml;
7496
7905
  try {
7497
- configToml = smol_toml.parse(existingContent);
7906
+ configToml = smol_toml.parse(existingContent || smol_toml.stringify({}));
7498
7907
  } catch (error) {
7499
7908
  throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
7500
7909
  }
7501
- if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
7502
- return smol_toml.stringify(configToml);
7910
+ let features;
7911
+ if (typeof configToml.features === "object" && configToml.features !== null) {
7912
+ features = { ...configToml.features };
7913
+ delete features.codex_hooks;
7914
+ }
7915
+ return applySharedConfigPatch({
7916
+ fileKey: sharedConfigFileKey({
7917
+ relativeDirPath: CODEXCLI_DIR,
7918
+ relativeFilePath: CODEXCLI_MCP_FILE_NAME
7919
+ }),
7920
+ feature: "hooks",
7921
+ existingContent,
7922
+ patch: { features },
7923
+ filePath: configPath
7924
+ });
7503
7925
  }
7504
7926
  /**
7505
7927
  * Represents the `.codex/config.toml` file as a generated ToolFile,
@@ -8347,7 +8769,8 @@ const DEVIN_CONVERTER_CONFIG = {
8347
8769
  "sessionStart",
8348
8770
  "sessionEnd",
8349
8771
  "stop",
8350
- "beforeSubmitPrompt"
8772
+ "beforeSubmitPrompt",
8773
+ "postCompact"
8351
8774
  ])
8352
8775
  };
8353
8776
  /**
@@ -8415,17 +8838,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
8415
8838
  let fileContent;
8416
8839
  if (global) {
8417
8840
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8418
- let parsedSettings;
8419
- try {
8420
- parsedSettings = JSON.parse(existingContent);
8421
- } catch (error) {
8422
- throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
8423
- }
8424
- const merged = {
8425
- ...isRecord(parsedSettings) ? parsedSettings : {},
8426
- hooks: devinHooks
8427
- };
8428
- fileContent = JSON.stringify(merged, null, 2);
8841
+ fileContent = applySharedConfigPatch({
8842
+ fileKey: sharedConfigFileKey(paths),
8843
+ feature: "hooks",
8844
+ existingContent,
8845
+ patch: { hooks: devinHooks },
8846
+ filePath
8847
+ });
8429
8848
  } else {
8430
8849
  await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8431
8850
  fileContent = JSON.stringify(devinHooks, null, 2);
@@ -8660,6 +9079,167 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8660
9079
  }
8661
9080
  };
8662
9081
  //#endregion
9082
+ //#region src/constants/grokcli-paths.ts
9083
+ /**
9084
+ * Grok Build CLI (xAI) configuration-layout conventions.
9085
+ *
9086
+ * Single source of truth for where Grok Build expects its files. Grok Build
9087
+ * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
9088
+ * with project/global scopes resolved by the directory the CLI runs in
9089
+ * (`./.grok/config.toml` vs `~/.grok/config.toml`).
9090
+ *
9091
+ * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
9092
+ * `-s project` writes `./.grok/config.toml`, `-s user` writes
9093
+ * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
9094
+ * @see https://docs.x.ai/build/overview
9095
+ */
9096
+ /** Root directory for Grok Build configuration, relative to the scope root. */
9097
+ const GROKCLI_DIR = ".grok";
9098
+ /** MCP servers and other settings live in `config.toml` under `.grok/`. */
9099
+ const GROKCLI_MCP_FILE_NAME = "config.toml";
9100
+ /**
9101
+ * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
9102
+ * permission mode, and other settings all live here; permissions reuse the same
9103
+ * file name as MCP since Grok consolidates everything into one config.
9104
+ */
9105
+ const GROKCLI_CONFIG_FILE_NAME = "config.toml";
9106
+ /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
9107
+ const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
9108
+ /**
9109
+ * Hooks directory under `.grok/`. Grok Build discovers hook config files from
9110
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global), each a
9111
+ * standalone JSON file using the Claude-Code-compatible nested `{ hooks: { … } }`
9112
+ * shape. rulesync writes all its hooks into a single `rulesync.json`.
9113
+ * @see https://docs.x.ai/build/features/hooks
9114
+ */
9115
+ const GROKCLI_HOOKS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "hooks");
9116
+ /** rulesync-managed Grok hooks file under `.grok/hooks/`. */
9117
+ const GROKCLI_HOOKS_FILE_NAME = "rulesync.json";
9118
+ /**
9119
+ * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
9120
+ * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
9121
+ * (global), each a Markdown file with YAML frontmatter (verified via
9122
+ * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
9123
+ */
9124
+ const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
9125
+ /**
9126
+ * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
9127
+ * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
9128
+ * `grok inspect`, consistent with the `.grok/` global discovery used by the
9129
+ * MCP/skills/subagents adapters).
9130
+ */
9131
+ const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
9132
+ //#endregion
9133
+ //#region src/features/hooks/grokcli-hooks.ts
9134
+ const GROKCLI_CONVERTER_CONFIG = {
9135
+ supportedEvents: GROKCLI_HOOK_EVENTS,
9136
+ canonicalToToolEventNames: CANONICAL_TO_GROKCLI_EVENT_NAMES,
9137
+ toolToCanonicalEventNames: GROKCLI_TO_CANONICAL_EVENT_NAMES,
9138
+ projectDirVar: "",
9139
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
9140
+ noMatcherEvents: /* @__PURE__ */ new Set([
9141
+ "sessionStart",
9142
+ "sessionEnd",
9143
+ "beforeSubmitPrompt",
9144
+ "stop",
9145
+ "stopFailure",
9146
+ "notification",
9147
+ "subagentStart",
9148
+ "subagentStop",
9149
+ "preCompact",
9150
+ "postCompact"
9151
+ ])
9152
+ };
9153
+ /**
9154
+ * Hooks generator for Grok CLI (xAI Grok Build).
9155
+ *
9156
+ * Grok Build adopts a Claude-Code-compatible lifecycle hooks model: each event
9157
+ * maps to an array of `{ matcher?, hooks: [{ type, command, timeout? }] }`
9158
+ * matcher groups under a top-level `hooks` key. rulesync writes all its hooks
9159
+ * into a single standalone `rulesync.json` file discovered from
9160
+ * `.grok/hooks/*.json` (project) and `~/.grok/hooks/*.json` (global). The file
9161
+ * is dedicated to hooks and owned wholesale by rulesync, so it may be deleted
9162
+ * as an orphan.
9163
+ *
9164
+ * @see https://docs.x.ai/build/features/hooks
9165
+ */
9166
+ var GrokcliHooks = class GrokcliHooks extends ToolHooks {
9167
+ constructor(params) {
9168
+ super({
9169
+ ...params,
9170
+ fileContent: params.fileContent ?? "{}"
9171
+ });
9172
+ }
9173
+ static getSettablePaths(_options = {}) {
9174
+ return {
9175
+ relativeDirPath: GROKCLI_HOOKS_DIR_PATH,
9176
+ relativeFilePath: GROKCLI_HOOKS_FILE_NAME
9177
+ };
9178
+ }
9179
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9180
+ const paths = GrokcliHooks.getSettablePaths({ global });
9181
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9182
+ return new GrokcliHooks({
9183
+ outputRoot,
9184
+ relativeDirPath: paths.relativeDirPath,
9185
+ relativeFilePath: paths.relativeFilePath,
9186
+ fileContent,
9187
+ validate
9188
+ });
9189
+ }
9190
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9191
+ const paths = GrokcliHooks.getSettablePaths({ global });
9192
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9193
+ const config = rulesyncHooks.getJson();
9194
+ const grokHooks = canonicalToToolHooks({
9195
+ config,
9196
+ toolOverrideHooks: config.grokcli?.hooks,
9197
+ converterConfig: GROKCLI_CONVERTER_CONFIG,
9198
+ logger
9199
+ });
9200
+ await readOrInitializeFileContent(filePath, JSON.stringify({ hooks: {} }, null, 2));
9201
+ const fileContent = JSON.stringify({ hooks: grokHooks }, null, 2);
9202
+ return new GrokcliHooks({
9203
+ outputRoot,
9204
+ relativeDirPath: paths.relativeDirPath,
9205
+ relativeFilePath: paths.relativeFilePath,
9206
+ fileContent,
9207
+ validate
9208
+ });
9209
+ }
9210
+ toRulesyncHooks() {
9211
+ let parsed;
9212
+ try {
9213
+ parsed = JSON.parse(this.getFileContent());
9214
+ } catch (error) {
9215
+ throw new Error(`Failed to parse Grok hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9216
+ }
9217
+ const hooks = toolHooksToCanonical({
9218
+ hooks: isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {},
9219
+ converterConfig: GROKCLI_CONVERTER_CONFIG
9220
+ });
9221
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9222
+ version: 1,
9223
+ hooks
9224
+ }, null, 2) });
9225
+ }
9226
+ validate() {
9227
+ return {
9228
+ success: true,
9229
+ error: null
9230
+ };
9231
+ }
9232
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9233
+ return new GrokcliHooks({
9234
+ outputRoot,
9235
+ relativeDirPath,
9236
+ relativeFilePath,
9237
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9238
+ validate: false
9239
+ });
9240
+ }
9241
+ };
9242
+ //#endregion
8663
9243
  //#region src/features/hooks/hermesagent-hooks.ts
8664
9244
  /**
8665
9245
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -9237,18 +9817,14 @@ var KiroHooks = class KiroHooks extends ToolHooks {
9237
9817
  const paths = KiroHooks.getSettablePaths({ global });
9238
9818
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9239
9819
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9240
- let agentConfig;
9241
- try {
9242
- agentConfig = JSON.parse(existingContent);
9243
- } catch (error) {
9244
- throw new Error(`Failed to parse existing Kiro agent config at ${filePath}: ${formatError(error)}`, { cause: error });
9245
- }
9246
9820
  const kiroHooks = canonicalToKiroHooks(rulesyncHooks.getJson(), this.getOverrideKey());
9247
- const merged = {
9248
- ...agentConfig,
9249
- hooks: kiroHooks
9250
- };
9251
- const fileContent = JSON.stringify(merged, null, 2);
9821
+ const fileContent = applySharedConfigPatch({
9822
+ fileKey: sharedConfigFileKey(paths),
9823
+ feature: "hooks",
9824
+ existingContent,
9825
+ patch: { hooks: kiroHooks },
9826
+ filePath
9827
+ });
9252
9828
  return new KiroHooks({
9253
9829
  outputRoot,
9254
9830
  relativeDirPath: paths.relativeDirPath,
@@ -9743,21 +10319,17 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
9743
10319
  const paths = QwencodeHooks.getSettablePaths({ global });
9744
10320
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9745
10321
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9746
- let settings;
9747
- try {
9748
- settings = JSON.parse(existingContent);
9749
- } catch (error) {
9750
- throw new Error(`Failed to parse existing Qwen Code settings at ${filePath}: ${formatError(error)}`, { cause: error });
9751
- }
9752
10322
  const config = rulesyncHooks.getJson();
9753
- const qwencodeHooks = canonicalToQwencodeHooks(config);
9754
- const merged = {
9755
- ...settings,
9756
- hooks: qwencodeHooks
9757
- };
10323
+ const patch = { hooks: canonicalToQwencodeHooks(config) };
9758
10324
  const disableAllHooks = config.qwencode?.disableAllHooks;
9759
- if (typeof disableAllHooks === "boolean") merged.disableAllHooks = disableAllHooks;
9760
- const fileContent = JSON.stringify(merged, null, 2);
10325
+ if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
10326
+ const fileContent = applySharedConfigPatch({
10327
+ fileKey: sharedConfigFileKey(paths),
10328
+ feature: "hooks",
10329
+ existingContent,
10330
+ patch,
10331
+ filePath
10332
+ });
9761
10333
  return new QwencodeHooks({
9762
10334
  outputRoot,
9763
10335
  relativeDirPath: paths.relativeDirPath,
@@ -9874,9 +10446,9 @@ function reasonixHooksToCanonical(hooks) {
9874
10446
  * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9875
10447
  * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9876
10448
  * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9877
- * The eight upstream events with a clean canonical equivalent are mapped:
10449
+ * All ten upstream events have a clean canonical equivalent and are mapped:
9878
10450
  * PreToolUse/PostToolUse/UserPromptSubmit/Stop plus SessionStart/SessionEnd/
9879
- * SubagentStop/PostLLMCall (see REASONIX_HOOK_EVENTS).
10451
+ * SubagentStop/PostLLMCall/Notification/PreCompact (see REASONIX_HOOK_EVENTS).
9880
10452
  * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9881
10453
  */
9882
10454
  var ReasonixHooks = class ReasonixHooks extends ToolHooks {
@@ -10016,10 +10588,6 @@ function canonicalToVibeHooks(config, toolOverride) {
10016
10588
  }
10017
10589
  return { hooks };
10018
10590
  }
10019
- /**
10020
- * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
10021
- * into a canonical event → definition[] record.
10022
- */
10023
10591
  /** Convert one raw `[[hooks]]` entry to a canonical definition, or null to skip. */
10024
10592
  function vibeEntryToCanonicalDef(raw) {
10025
10593
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
@@ -10039,6 +10607,10 @@ function vibeEntryToCanonicalDef(raw) {
10039
10607
  def
10040
10608
  };
10041
10609
  }
10610
+ /**
10611
+ * Reverse {@link canonicalToVibeHooks}: parse the flat `[[hooks]]` array back
10612
+ * into a canonical event → definition[] record.
10613
+ */
10042
10614
  function vibeHooksToCanonical(parsed) {
10043
10615
  const canonical = {};
10044
10616
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return canonical;
@@ -10067,15 +10639,22 @@ function parseVibeToml(fileContent) {
10067
10639
  */
10068
10640
  async function buildVibeConfigTomlContent({ outputRoot }) {
10069
10641
  const configPath = (0, node_path.join)(outputRoot, VIBE_DIR, VIBE_CONFIG_FILE_NAME);
10070
- const existingContent = await readFileContentOrNull(configPath) ?? smol_toml.stringify({});
10071
- let config;
10642
+ const existingContent = await readFileContentOrNull(configPath) ?? "";
10072
10643
  try {
10073
- config = parseVibeToml(existingContent);
10644
+ parseVibeToml(existingContent);
10074
10645
  } catch (error) {
10075
10646
  throw new Error(`Failed to parse existing Vibe config at ${configPath}: ${formatError(error)}`, { cause: error });
10076
10647
  }
10077
- config.enable_experimental_hooks = true;
10078
- return smol_toml.stringify(config);
10648
+ return applySharedConfigPatch({
10649
+ fileKey: sharedConfigFileKey({
10650
+ relativeDirPath: VIBE_DIR,
10651
+ relativeFilePath: VIBE_CONFIG_FILE_NAME
10652
+ }),
10653
+ feature: "hooks",
10654
+ existingContent,
10655
+ patch: { enable_experimental_hooks: true },
10656
+ filePath: configPath
10657
+ });
10079
10658
  }
10080
10659
  /**
10081
10660
  * Represents the `.vibe/config.toml` file as a generated ToolFile so it goes
@@ -10457,6 +11036,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
10457
11036
  supportedEvents: REASONIX_HOOK_EVENTS,
10458
11037
  supportedHookTypes: ["command"],
10459
11038
  supportsMatcher: true
11039
+ }],
11040
+ ["grokcli", {
11041
+ class: GrokcliHooks,
11042
+ meta: {
11043
+ supportsProject: true,
11044
+ supportsGlobal: true,
11045
+ supportsImport: true
11046
+ },
11047
+ supportedEvents: GROKCLI_HOOK_EVENTS,
11048
+ supportedHookTypes: ["command"],
11049
+ supportsMatcher: true
10460
11050
  }]
10461
11051
  ]);
10462
11052
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -11575,17 +12165,18 @@ var ZedIgnore = class ZedIgnore extends ToolIgnore {
11575
12165
  const patterns = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
11576
12166
  const filePath = (0, node_path.join)(outputRoot, this.getSettablePaths().relativeDirPath, this.getSettablePaths().relativeFilePath);
11577
12167
  const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
11578
- const existingJsonValue = JSON.parse(existingFileContent);
11579
- const mergedPatterns = (0, es_toolkit.uniq)([...existingJsonValue.private_files ?? [], ...patterns].toSorted());
11580
- const jsonValue = {
11581
- ...existingJsonValue,
11582
- private_files: mergedPatterns
11583
- };
12168
+ const mergedPatterns = (0, es_toolkit.uniq)([...JSON.parse(existingFileContent).private_files ?? [], ...patterns].toSorted());
11584
12169
  return new ZedIgnore({
11585
12170
  outputRoot,
11586
12171
  relativeDirPath: this.getSettablePaths().relativeDirPath,
11587
12172
  relativeFilePath: this.getSettablePaths().relativeFilePath,
11588
- fileContent: JSON.stringify(jsonValue, null, 2),
12173
+ fileContent: applySharedConfigPatch({
12174
+ fileKey: sharedConfigFileKey(this.getSettablePaths()),
12175
+ feature: "ignore",
12176
+ existingContent: existingFileContent,
12177
+ patch: { private_files: mergedPatterns },
12178
+ filePath
12179
+ }),
11589
12180
  validate: true
11590
12181
  });
11591
12182
  }
@@ -12042,17 +12633,17 @@ var AmpMcp = class AmpMcp extends ToolMcp {
12042
12633
  const basePaths = this.getSettablePaths({ global });
12043
12634
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
12044
12635
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
12045
- const json = fileContent ? parseAmpSettingsJsonc(fileContent) : { [AMP_MCP_SERVERS_KEY]: {} };
12046
- const filteredMcpServers = filterMcpServers(rulesyncMcp.getMcpServers());
12047
- const newJson = {
12048
- ...json,
12049
- [AMP_MCP_SERVERS_KEY]: filteredMcpServers
12050
- };
12051
12636
  return new AmpMcp({
12052
12637
  outputRoot,
12053
12638
  relativeDirPath: basePaths.relativeDirPath,
12054
12639
  relativeFilePath,
12055
- fileContent: JSON.stringify(newJson, null, 2),
12640
+ fileContent: applySharedConfigPatch({
12641
+ fileKey: sharedConfigFileKey(basePaths),
12642
+ feature: "mcp",
12643
+ existingContent: fileContent ?? "",
12644
+ patch: { [AMP_MCP_SERVERS_KEY]: filterMcpServers(rulesyncMcp.getMcpServers()) },
12645
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
12646
+ }),
12056
12647
  validate,
12057
12648
  global
12058
12649
  });
@@ -12339,15 +12930,19 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12339
12930
  }
12340
12931
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12341
12932
  const paths = this.getSettablePaths({ global });
12342
- const merged = {
12343
- ...parseAugmentcodeSettings(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
12344
- mcpServers: rulesyncMcp.getMcpServers()
12345
- };
12933
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
12934
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
12346
12935
  return new AugmentcodeMcp({
12347
12936
  outputRoot,
12348
12937
  relativeDirPath: paths.relativeDirPath,
12349
12938
  relativeFilePath: paths.relativeFilePath,
12350
- fileContent: JSON.stringify(merged, null, 2),
12939
+ fileContent: applySharedConfigPatch({
12940
+ fileKey: sharedConfigFileKey(paths),
12941
+ feature: "mcp",
12942
+ existingContent,
12943
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
12944
+ filePath
12945
+ }),
12351
12946
  validate,
12352
12947
  global
12353
12948
  });
@@ -12707,7 +13302,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12707
13302
  ...rest,
12708
13303
  validate: false
12709
13304
  });
12710
- this.toml = smol_toml.parse(this.fileContent);
13305
+ let toml;
13306
+ try {
13307
+ toml = smol_toml.parse(this.fileContent);
13308
+ } catch (error) {
13309
+ throw new Error(`Failed to parse Codex CLI config at ${this.getFilePath()}: ${formatError(error)}`, { cause: error });
13310
+ }
13311
+ this.toml = toml;
12711
13312
  if (rest.validate) {
12712
13313
  const result = this.validate();
12713
13314
  if (!result.success) throw result.error;
@@ -12741,8 +13342,14 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12741
13342
  }
12742
13343
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12743
13344
  const paths = this.getSettablePaths({ global });
12744
- const configTomlFileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smol_toml.stringify({}));
12745
- const configToml = smol_toml.parse(configTomlFileContent);
13345
+ const configTomlFilePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13346
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13347
+ let configToml;
13348
+ try {
13349
+ configToml = smol_toml.parse(configTomlFileContent || smol_toml.stringify({}));
13350
+ } catch (error) {
13351
+ throw new Error(`Failed to parse existing Codex CLI config at ${configTomlFilePath}: ${formatError(error)}`, { cause: error });
13352
+ }
12746
13353
  const strippedMcpServers = rulesyncMcp.getMcpServers();
12747
13354
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
12748
13355
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
@@ -12755,7 +13362,7 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12755
13362
  const filteredMcpServers = this.removeEmptyEntries(converted);
12756
13363
  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`);
12757
13364
  const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
12758
- configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
13365
+ const mergedMcpServers = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
12759
13366
  const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12760
13367
  const serverRecord = serverConfig;
12761
13368
  if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
@@ -12768,7 +13375,13 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
12768
13375
  outputRoot,
12769
13376
  relativeDirPath: paths.relativeDirPath,
12770
13377
  relativeFilePath: paths.relativeFilePath,
12771
- fileContent: smol_toml.stringify(configToml),
13378
+ fileContent: applySharedConfigPatch({
13379
+ fileKey: sharedConfigFileKey(paths),
13380
+ feature: "mcp",
13381
+ existingContent: configTomlFileContent,
13382
+ patch: { mcp_servers: mergedMcpServers },
13383
+ filePath: configTomlFilePath
13384
+ }),
12772
13385
  validate
12773
13386
  });
12774
13387
  }
@@ -13335,16 +13948,19 @@ var DevinMcp = class DevinMcp extends ToolMcp {
13335
13948
  }
13336
13949
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13337
13950
  const paths = this.getSettablePaths({ global });
13338
- const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
13339
- const devinConfig = {
13340
- ...this.parseJsonOrThrow(fileContent, paths.relativeDirPath, paths.relativeFilePath),
13341
- mcpServers: rulesyncMcp.getMcpServers()
13342
- };
13951
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
13952
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
13343
13953
  return new DevinMcp({
13344
13954
  outputRoot,
13345
13955
  relativeDirPath: paths.relativeDirPath,
13346
13956
  relativeFilePath: paths.relativeFilePath,
13347
- fileContent: JSON.stringify(devinConfig, null, 2),
13957
+ fileContent: applySharedConfigPatch({
13958
+ fileKey: sharedConfigFileKey(paths),
13959
+ feature: "mcp",
13960
+ existingContent,
13961
+ patch: { mcpServers: rulesyncMcp.getMcpServers() },
13962
+ filePath
13963
+ }),
13348
13964
  validate,
13349
13965
  global
13350
13966
  });
@@ -13707,47 +14323,6 @@ var GooseMcp = class GooseMcp extends ToolMcp {
13707
14323
  }
13708
14324
  };
13709
14325
  //#endregion
13710
- //#region src/constants/grokcli-paths.ts
13711
- /**
13712
- * Grok Build CLI (xAI) configuration-layout conventions.
13713
- *
13714
- * Single source of truth for where Grok Build expects its files. Grok Build
13715
- * stores MCP servers (and other settings) in a `config.toml` under `.grok/`,
13716
- * with project/global scopes resolved by the directory the CLI runs in
13717
- * (`./.grok/config.toml` vs `~/.grok/config.toml`).
13718
- *
13719
- * Verified against `grok` 0.2.54 (`grok mcp add --help`, `grok mcp add`):
13720
- * `-s project` writes `./.grok/config.toml`, `-s user` writes
13721
- * `~/.grok/config.toml`, both as a TOML `[mcp_servers.<name>]` table.
13722
- * @see https://docs.x.ai/build/overview
13723
- */
13724
- /** Root directory for Grok Build configuration, relative to the scope root. */
13725
- const GROKCLI_DIR = ".grok";
13726
- /** MCP servers and other settings live in `config.toml` under `.grok/`. */
13727
- const GROKCLI_MCP_FILE_NAME = "config.toml";
13728
- /**
13729
- * Shared Grok CLI config file (`config.toml`). MCP servers, the `[ui]`
13730
- * permission mode, and other settings all live here; permissions reuse the same
13731
- * file name as MCP since Grok consolidates everything into one config.
13732
- */
13733
- const GROKCLI_CONFIG_FILE_NAME = "config.toml";
13734
- /** Skills directory under `.grok/` (project: `./.grok/skills`, global: `~/.grok/skills`). */
13735
- const GROKCLI_SKILLS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "skills");
13736
- /**
13737
- * Subagents (agent profiles) directory under `.grok/`. Grok Build discovers
13738
- * agent definitions from `.grok/agents/*.md` (project) and `~/.grok/agents/*.md`
13739
- * (global), each a Markdown file with YAML frontmatter (verified via
13740
- * `grok inspect`; format matches the bundled `~/.grok/bundled/agents/*.md`).
13741
- */
13742
- const GROKCLI_AGENTS_DIR_PATH = (0, node_path.join)(GROKCLI_DIR, "agents");
13743
- /**
13744
- * Instruction file. Grok reads the AGENTS.md instruction-file family natively,
13745
- * including the user-level `~/.grok/AGENTS.md` for global rules (verified via
13746
- * `grok inspect`, consistent with the `.grok/` global discovery used by the
13747
- * MCP/skills/subagents adapters).
13748
- */
13749
- const GROKCLI_RULE_FILE_NAME = "AGENTS.md";
13750
- //#endregion
13751
14326
  //#region src/features/mcp/grokcli-mcp.ts
13752
14327
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH = 32;
13753
14328
  /**
@@ -13831,17 +14406,22 @@ var GrokcliMcp = class GrokcliMcp extends ToolMcp {
13831
14406
  }
13832
14407
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13833
14408
  const paths = this.getSettablePaths({ global });
13834
- const configTomlFileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), smol_toml.stringify({}));
13835
- const configToml = smol_toml.parse(configTomlFileContent);
14409
+ const configTomlFilePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
14410
+ const configTomlFileContent = await readFileContentOrNull(configTomlFilePath) ?? "";
13836
14411
  const converted = convertToGrokFormat(rulesyncMcp.getMcpServers());
13837
14412
  const filteredMcpServers = this.removeEmptyEntries(converted);
13838
14413
  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`);
13839
- configToml["mcp_servers"] = filteredMcpServers;
13840
14414
  return new GrokcliMcp({
13841
14415
  outputRoot,
13842
14416
  relativeDirPath: paths.relativeDirPath,
13843
14417
  relativeFilePath: paths.relativeFilePath,
13844
- fileContent: smol_toml.stringify(configToml),
14418
+ fileContent: applySharedConfigPatch({
14419
+ fileKey: sharedConfigFileKey(paths),
14420
+ feature: "mcp",
14421
+ existingContent: configTomlFileContent,
14422
+ patch: { mcp_servers: filteredMcpServers },
14423
+ filePath: configTomlFilePath
14424
+ }),
13845
14425
  validate
13846
14426
  });
13847
14427
  }
@@ -13902,6 +14482,54 @@ function resolveHermesTimeout(config) {
13902
14482
  if (typeof config.networkTimeout === "number") return config.networkTimeout;
13903
14483
  }
13904
14484
  /**
14485
+ * Copies the advanced Hermes-recognized per-server fields that have no canonical
14486
+ * alias — `auth` (`oauth` for OAuth 2.1/PKCE), mTLS `client_cert` (string PEM
14487
+ * path, or `[cert, key]`/`[cert, key, password]` list) and `client_key`,
14488
+ * `connect_timeout` (seconds), and `supports_parallel_tool_calls` — verbatim
14489
+ * from `source` to `target`. Field names are identical on both sides (the
14490
+ * canonical `McpServerSchema` is a `looseObject`), so this serves export and
14491
+ * import alike. See the Hermes mcp-config-reference.
14492
+ */
14493
+ function copyHermesAdvancedFields(source, target) {
14494
+ if (typeof source.auth === "string") target.auth = source.auth;
14495
+ if (typeof source.client_cert === "string" || isStringArray(source.client_cert)) target.client_cert = source.client_cert;
14496
+ if (typeof source.client_key === "string") target.client_key = source.client_key;
14497
+ if (typeof source.connect_timeout === "number") target.connect_timeout = source.connect_timeout;
14498
+ if (typeof source.supports_parallel_tool_calls === "boolean") target.supports_parallel_tool_calls = source.supports_parallel_tool_calls;
14499
+ }
14500
+ /**
14501
+ * Builds Hermes's per-server `tools` block from a canonical server config. The
14502
+ * canonical `enabledTools`/`disabledTools` arrays become `include`/`exclude`,
14503
+ * and the boolean `promptsEnabled`/`resourcesEnabled` toggles become Hermes's
14504
+ * `prompts`/`resources` capability flags. Returns an empty object when the
14505
+ * server has no tool scoping (the caller omits the block in that case).
14506
+ *
14507
+ * Note: `promptsEnabled`/`resourcesEnabled` are canonical top-level keys rather
14508
+ * than a nested canonical `tools` object, because canonical `McpServerSchema.tools`
14509
+ * is reserved as a `string[]` (used by other tools) — reusing it for an object
14510
+ * would fail validation on the next `generate`.
14511
+ */
14512
+ function buildHermesToolsBlock(config) {
14513
+ const tools = {};
14514
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
14515
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14516
+ if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
14517
+ if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
14518
+ return tools;
14519
+ }
14520
+ /**
14521
+ * Applies a Hermes per-server `tools` block back onto a canonical server config
14522
+ * (inverse of {@link buildHermesToolsBlock}): `include`/`exclude` become
14523
+ * `enabledTools`/`disabledTools`, and `prompts`/`resources` become the boolean
14524
+ * `promptsEnabled`/`resourcesEnabled` top-level toggles.
14525
+ */
14526
+ function applyHermesToolsBlock(hermesTools, server) {
14527
+ if (isStringArray(hermesTools.include)) server.enabledTools = hermesTools.include;
14528
+ if (isStringArray(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
14529
+ if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
14530
+ if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
14531
+ }
14532
+ /**
13905
14533
  * Converts a single rulesync canonical MCP server into a Hermes `mcp_servers:` entry.
13906
14534
  *
13907
14535
  * Hermes is close to the MCP spec but not identical: `command` must be a single
@@ -13935,9 +14563,8 @@ function convertServerToHermes(config) {
13935
14563
  if (config.disabled === true) out.enabled = false;
13936
14564
  const timeout = resolveHermesTimeout(config);
13937
14565
  if (timeout !== void 0) out.timeout = timeout;
13938
- const tools = {};
13939
- if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
13940
- if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
14566
+ copyHermesAdvancedFields(config, out);
14567
+ const tools = buildHermesToolsBlock(config);
13941
14568
  if (Object.keys(tools).length > 0) out.tools = tools;
13942
14569
  return out;
13943
14570
  }
@@ -13980,10 +14607,8 @@ function convertFromHermesFormat(mcpServers) {
13980
14607
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
13981
14608
  if (config.enabled === false) server.disabled = true;
13982
14609
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
13983
- if (isRecord(config.tools)) {
13984
- if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
13985
- if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
13986
- }
14610
+ copyHermesAdvancedFields(config, server);
14611
+ if (isRecord(config.tools)) applyHermesToolsBlock(config.tools, server);
13987
14612
  result[name] = server;
13988
14613
  }
13989
14614
  return result;
@@ -14374,20 +14999,21 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14374
14999
  fileContent = await readFileContentOrNull(jsonPath);
14375
15000
  if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
14376
15001
  }
14377
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14378
- const json = (0, jsonc_parser.parse)(fileContent);
14379
15002
  const { mcp: convertedMcp, tools: mcpTools } = convertToKiloFormat(rulesyncMcp.getMcpServers());
14380
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14381
- const newJson = {
14382
- ...jsonWithoutTools,
14383
- mcp: convertedMcp,
14384
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14385
- };
14386
15003
  return new KiloMcp({
14387
15004
  outputRoot,
14388
15005
  relativeDirPath: basePaths.relativeDirPath,
14389
15006
  relativeFilePath,
14390
- fileContent: JSON.stringify(newJson, null, 2),
15007
+ fileContent: applySharedConfigPatch({
15008
+ fileKey: sharedConfigFileKey(basePaths),
15009
+ feature: "mcp",
15010
+ existingContent: fileContent ?? "",
15011
+ patch: {
15012
+ mcp: convertedMcp,
15013
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
15014
+ },
15015
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
15016
+ }),
14391
15017
  validate
14392
15018
  });
14393
15019
  }
@@ -14416,15 +15042,17 @@ var KiloMcp = class KiloMcp extends ToolMcp {
14416
15042
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
14417
15043
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14418
15044
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14419
- const newJson = {
14420
- ...json,
14421
- instructions: mergedInstructions
14422
- };
14423
15045
  return new KiloMcp({
14424
15046
  outputRoot,
14425
15047
  relativeDirPath: basePaths.relativeDirPath,
14426
15048
  relativeFilePath,
14427
- fileContent: JSON.stringify(newJson, null, 2),
15049
+ fileContent: applySharedConfigPatch({
15050
+ fileKey: sharedConfigFileKey(basePaths),
15051
+ feature: "rules",
15052
+ existingContent: fileContent ?? "",
15053
+ patch: { instructions: mergedInstructions },
15054
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
15055
+ }),
14428
15056
  validate
14429
15057
  });
14430
15058
  }
@@ -14696,23 +15324,24 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14696
15324
  fileContent = await readFileContentOrNull(jsonPath);
14697
15325
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
14698
15326
  }
14699
- if (!fileContent) fileContent = JSON.stringify({ mcp: {} }, null, 2);
14700
- const json = (0, jsonc_parser.parse)(fileContent);
14701
15327
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(convertEnvVarRefsToToolFormat({
14702
15328
  mcpServers: rulesyncMcp.getMcpServers(),
14703
15329
  replacement: "{env:$1}"
14704
15330
  }));
14705
- const { tools: _existingTools, ...jsonWithoutTools } = json;
14706
- const newJson = {
14707
- ...jsonWithoutTools,
14708
- mcp: convertedMcp,
14709
- ...Object.keys(mcpTools).length > 0 && { tools: mcpTools }
14710
- };
14711
15331
  return new OpencodeMcp({
14712
15332
  outputRoot,
14713
15333
  relativeDirPath: basePaths.relativeDirPath,
14714
15334
  relativeFilePath,
14715
- fileContent: JSON.stringify(newJson, null, 2),
15335
+ fileContent: applySharedConfigPatch({
15336
+ fileKey: sharedConfigFileKey(basePaths),
15337
+ feature: "mcp",
15338
+ existingContent: fileContent ?? "",
15339
+ patch: {
15340
+ mcp: convertedMcp,
15341
+ tools: Object.keys(mcpTools).length > 0 ? mcpTools : void 0
15342
+ },
15343
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
15344
+ }),
14716
15345
  validate
14717
15346
  });
14718
15347
  }
@@ -14747,15 +15376,17 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
14747
15376
  const json = fileContent ? (0, jsonc_parser.parse)(fileContent) : {};
14748
15377
  const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
14749
15378
  const mergedInstructions = Array.from(/* @__PURE__ */ new Set([...existingInstructions, ...instructions])).toSorted();
14750
- const newJson = {
14751
- ...json,
14752
- instructions: mergedInstructions
14753
- };
14754
15379
  return new OpencodeMcp({
14755
15380
  outputRoot,
14756
15381
  relativeDirPath: basePaths.relativeDirPath,
14757
15382
  relativeFilePath,
14758
- fileContent: JSON.stringify(newJson, null, 2),
15383
+ fileContent: applySharedConfigPatch({
15384
+ fileKey: sharedConfigFileKey(basePaths),
15385
+ feature: "rules",
15386
+ existingContent: fileContent ?? "",
15387
+ patch: { instructions: mergedInstructions },
15388
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
15389
+ }),
14759
15390
  validate
14760
15391
  });
14761
15392
  }
@@ -14865,16 +15496,19 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
14865
15496
  }
14866
15497
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14867
15498
  const paths = this.getSettablePaths({ global });
14868
- const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ mcpServers: {} }, null, 2));
14869
- const newJson = {
14870
- ...JSON.parse(fileContent),
14871
- mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers())
14872
- };
15499
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15500
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({ mcpServers: {} }, null, 2));
14873
15501
  return new QwencodeMcp({
14874
15502
  outputRoot,
14875
15503
  relativeDirPath: paths.relativeDirPath,
14876
15504
  relativeFilePath: paths.relativeFilePath,
14877
- fileContent: JSON.stringify(newJson, null, 2),
15505
+ fileContent: applySharedConfigPatch({
15506
+ fileKey: sharedConfigFileKey(paths),
15507
+ feature: "mcp",
15508
+ existingContent,
15509
+ patch: { mcpServers: convertToQwencodeFormat(rulesyncMcp.getMcpServers()) },
15510
+ filePath
15511
+ }),
14878
15512
  validate
14879
15513
  });
14880
15514
  }
@@ -14959,13 +15593,20 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
14959
15593
  }
14960
15594
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14961
15595
  const paths = this.getSettablePaths({ global });
14962
- const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
14963
- config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
15596
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15597
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15598
+ const plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
14964
15599
  return new ReasonixMcp({
14965
15600
  outputRoot,
14966
15601
  relativeDirPath: paths.relativeDirPath,
14967
15602
  relativeFilePath: paths.relativeFilePath,
14968
- fileContent: smol_toml.stringify(config),
15603
+ fileContent: applySharedConfigPatch({
15604
+ fileKey: sharedConfigFileKey(paths),
15605
+ feature: "mcp",
15606
+ existingContent,
15607
+ patch: { plugins },
15608
+ filePath
15609
+ }),
14969
15610
  validate,
14970
15611
  global
14971
15612
  });
@@ -15440,13 +16081,20 @@ var VibeMcp = class VibeMcp extends ToolMcp {
15440
16081
  }
15441
16082
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15442
16083
  const paths = this.getSettablePaths({ global });
15443
- const config = parseVibeConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
15444
- config.mcp_servers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
16084
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16085
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
16086
+ const mcpServers = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToVibe(name, server));
15445
16087
  return new VibeMcp({
15446
16088
  outputRoot,
15447
16089
  relativeDirPath: paths.relativeDirPath,
15448
16090
  relativeFilePath: paths.relativeFilePath,
15449
- fileContent: smol_toml.stringify(config),
16091
+ fileContent: applySharedConfigPatch({
16092
+ fileKey: sharedConfigFileKey(paths),
16093
+ feature: "mcp",
16094
+ existingContent,
16095
+ patch: { mcp_servers: mcpServers },
16096
+ filePath
16097
+ }),
15450
16098
  validate,
15451
16099
  global
15452
16100
  });
@@ -15677,16 +16325,19 @@ var ZedMcp = class ZedMcp extends ToolMcp {
15677
16325
  }
15678
16326
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15679
16327
  const paths = this.getSettablePaths({ global });
15680
- const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "{}");
15681
- const newJson = {
15682
- ...JSON.parse(fileContent),
15683
- context_servers: rulesyncMcp.getMcpServers()
15684
- };
16328
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
16329
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
15685
16330
  return new ZedMcp({
15686
16331
  outputRoot,
15687
16332
  relativeDirPath: paths.relativeDirPath,
15688
16333
  relativeFilePath: paths.relativeFilePath,
15689
- fileContent: JSON.stringify(newJson, null, 2),
16334
+ fileContent: applySharedConfigPatch({
16335
+ fileKey: sharedConfigFileKey(paths),
16336
+ feature: "mcp",
16337
+ existingContent,
16338
+ patch: { context_servers: rulesyncMcp.getMcpServers() },
16339
+ filePath
16340
+ }),
15690
16341
  validate
15691
16342
  });
15692
16343
  }
@@ -16252,18 +16903,27 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
16252
16903
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16253
16904
  * autonomy/sandbox controls with no canonical permission category — under
16254
16905
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16255
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16256
- * placed here are merged into the matching `settings.json` group and emitted
16257
- * only for Qwen, while the shared `permission` block continues to drive the
16258
- * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16259
- * passthrough) so any current or future `tools`/`security` key can be authored.
16906
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). It also
16907
+ * exposes `permissions.autoMode` (the Auto Mode classifier config:
16908
+ * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` see
16909
+ * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
16910
+ * likewise has no canonical category. Fields placed here are merged into the
16911
+ * matching `settings.json` group and emitted only for Qwen, while the shared
16912
+ * `permission` block continues to drive the `permissions.allow`/`ask`/`deny`
16913
+ * arrays. Kept `looseObject` (verbatim passthrough) so any current or future
16914
+ * `tools`/`security`/`autoMode` key can be authored.
16260
16915
  *
16261
16916
  * @example
16262
- * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
16917
+ * {
16918
+ * "tools": { "approvalMode": "auto-edit" },
16919
+ * "security": { "folderTrust": { "enabled": true } },
16920
+ * "autoMode": { "hints": { "allow": ["Running tests"] }, "classifyAllShell": true }
16921
+ * }
16263
16922
  */
16264
16923
  const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
16265
16924
  tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
16266
- security: zod_mini.z.optional(zod_mini.z.looseObject({}))
16925
+ security: zod_mini.z.optional(zod_mini.z.looseObject({})),
16926
+ autoMode: zod_mini.z.optional(zod_mini.z.looseObject({}))
16267
16927
  });
16268
16928
  /**
16269
16929
  * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
@@ -16290,9 +16950,11 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
16290
16950
  * exposes security controls with no canonical per-command allow/ask/deny slot —
16291
16951
  * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16292
16952
  * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16293
- * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
16953
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`,
16954
+ * `mcpAutonomyOverrides` (per-MCP-tool autonomy levels), `enableDroidShield`,
16294
16955
  * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16295
- * `interactionMode`). Fields placed here are merged into `settings.json` and
16956
+ * `subagentAutonomyLevel`, `interactionMode`). Fields placed here are merged
16957
+ * into `settings.json` and
16296
16958
  * emitted only for Factory Droid, while the shared `permission` block continues
16297
16959
  * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16298
16960
  *
@@ -16485,6 +17147,41 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zo
16485
17147
  }))
16486
17148
  })) });
16487
17149
  /**
17150
+ * Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
17151
+ * `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
17152
+ * Codex still accepts, so it is included so existing configs round-trip. The
17153
+ * granular table form (`{ granular = { … } }`) is modeled separately in the
17154
+ * override union.
17155
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17156
+ */
17157
+ const CodexApprovalPolicySchema = zod_mini.z.enum([
17158
+ "untrusted",
17159
+ "on-request",
17160
+ "on-failure",
17161
+ "never"
17162
+ ]);
17163
+ /**
17164
+ * Codex CLI's classic sandbox mode. Serialized as a kebab-case string in
17165
+ * `.codex/config.toml`.
17166
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17167
+ */
17168
+ const CodexSandboxModeSchema = zod_mini.z.enum([
17169
+ "read-only",
17170
+ "workspace-write",
17171
+ "danger-full-access"
17172
+ ]);
17173
+ /**
17174
+ * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
17175
+ * value Codex still accepts for backward compatibility, so it is included so
17176
+ * existing configs round-trip through the rulesync model.
17177
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
17178
+ */
17179
+ const CodexApprovalsReviewerSchema = zod_mini.z.enum([
17180
+ "user",
17181
+ "auto_review",
17182
+ "guardian_subagent"
17183
+ ]);
17184
+ /**
16488
17185
  * Codex CLI-scoped permission override.
16489
17186
  *
16490
17187
  * Codex CLI's permission surface is richer than the canonical allow/ask/deny
@@ -16493,15 +17190,16 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zo
16493
17190
  * override whose fields are written verbatim as top-level `.codex/config.toml`
16494
17191
  * keys (the override wins per key; existing sibling keys the user set directly
16495
17192
  * are preserved):
16496
- * - `approval_policy` — `untrusted` | `on-request` | `never`, or a
16497
- * `{ granular = { … } }` table (kept verbatim; the granular schema has
16498
- * required fields that are brittle to model as typed keys).
17193
+ * - `approval_policy` — `untrusted` | `on-request` (legacy alias `on-failure`) |
17194
+ * `never`, or a `{ granular = { … } }` table (kept verbatim; the granular
17195
+ * schema has required fields that are brittle to model as typed keys).
16499
17196
  * - `sandbox_mode` — `read-only` | `workspace-write` | `danger-full-access`,
16500
17197
  * with the sibling `sandbox_workspace_write` table (`network_access`,
16501
17198
  * `writable_roots`, …).
16502
17199
  * - `apps` — per-app tool gating (`apps.<id>.tools.<tool>.approval_mode` /
16503
17200
  * `.enabled`, `apps.<id>.default_tools_approval_mode`).
16504
- * - `approvals_reviewer` — the reviewer-approval surface.
17201
+ * - `approvals_reviewer` — the reviewer-approval surface (`user` | `auto_review`
17202
+ * | `guardian_subagent`), or a table for the richer reviewer config.
16505
17203
  *
16506
17204
  * Two surfaces are deliberately NOT authorable here so the override can never
16507
17205
  * clobber a feature-owned key: `mcp_servers.*` per-MCP gating is owned by the
@@ -16519,11 +17217,11 @@ const KiroPermissionsOverrideSchema = zod_mini.z.looseObject({ toolsSettings: zo
16519
17217
  * "sandbox_workspace_write": { "network_access": true } }
16520
17218
  */
16521
17219
  const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
16522
- approval_policy: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
16523
- sandbox_mode: zod_mini.z.optional(zod_mini.z.string()),
17220
+ approval_policy: zod_mini.z.optional(zod_mini.z.union([CodexApprovalPolicySchema, zod_mini.z.looseObject({})])),
17221
+ sandbox_mode: zod_mini.z.optional(CodexSandboxModeSchema),
16524
17222
  sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
16525
17223
  apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
16526
- approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})]))
17224
+ approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})]))
16527
17225
  });
16528
17226
  /**
16529
17227
  * Permissions configuration.
@@ -16855,25 +17553,29 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16855
17553
  const override = config.amp;
16856
17554
  const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16857
17555
  const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16858
- const newJson = {
16859
- ...json,
16860
- [AMP_TOOLS_DISABLE_KEY]: disable
16861
- };
16862
17556
  const mergedPermissions = mergeAmpPermissions([
16863
17557
  ...permissions,
16864
17558
  ...authoredExtras,
16865
17559
  ...preservedDelegates
16866
17560
  ]);
16867
- if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16868
- else delete newJson[AMP_PERMISSIONS_KEY];
16869
- if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16870
- if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16871
- if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
17561
+ const patch = {
17562
+ [AMP_TOOLS_DISABLE_KEY]: disable,
17563
+ [AMP_PERMISSIONS_KEY]: mergedPermissions.length > 0 ? mergedPermissions : void 0
17564
+ };
17565
+ if (override?.guardedFiles?.allowlist !== void 0) patch[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
17566
+ if (override?.dangerouslyAllowAll !== void 0) patch[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
17567
+ if (override?.mcpPermissions !== void 0) patch[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16872
17568
  return new AmpPermissions({
16873
17569
  outputRoot,
16874
17570
  relativeDirPath: basePaths.relativeDirPath,
16875
17571
  relativeFilePath,
16876
- fileContent: JSON.stringify(newJson, null, 2),
17572
+ fileContent: applySharedConfigPatch({
17573
+ fileKey: sharedConfigFileKey(basePaths),
17574
+ feature: "permissions",
17575
+ existingContent: fileContent ?? "",
17576
+ patch,
17577
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
17578
+ }),
16877
17579
  validate: true
16878
17580
  });
16879
17581
  }
@@ -17815,11 +18517,13 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
17815
18517
  ...preservedBasicEntries,
17816
18518
  ...authoredBasics
17817
18519
  ]);
17818
- const merged = {
17819
- ...settings,
17820
- toolPermissions: [...specialEntries, ...sortedBasic]
17821
- };
17822
- const fileContent = JSON.stringify(merged, null, 2);
18520
+ const fileContent = applySharedConfigPatch({
18521
+ fileKey: sharedConfigFileKey(paths),
18522
+ feature: "permissions",
18523
+ existingContent,
18524
+ patch: { toolPermissions: [...specialEntries, ...sortedBasic] },
18525
+ filePath
18526
+ });
17823
18527
  return new AugmentcodePermissions({
17824
18528
  outputRoot,
17825
18529
  relativeDirPath: paths.relativeDirPath,
@@ -18411,13 +19115,6 @@ const WORKSPACE_WIDE_WRITE_PATTERNS = /* @__PURE__ */ new Set([
18411
19115
  ]);
18412
19116
  const CODEX_MINIMAL_KEY = ":minimal";
18413
19117
  const GLOBAL_WILDCARD_DOMAIN = "*";
18414
- const CODEXCLI_OVERRIDE_KEYS = [
18415
- "approval_policy",
18416
- "sandbox_mode",
18417
- "sandbox_workspace_write",
18418
- "apps",
18419
- "approvals_reviewer"
18420
- ];
18421
19118
  var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18422
19119
  static getSettablePaths(_options = {}) {
18423
19120
  return {
@@ -18441,26 +19138,27 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18441
19138
  }
18442
19139
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
18443
19140
  const paths = this.getSettablePaths({ global });
18444
- const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
18445
- const parsed = toMutableTable(smol_toml.parse(existingContent));
19141
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19142
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
19143
+ const existing = toMutableTable(smol_toml.parse(existingContent || smol_toml.stringify({})));
18446
19144
  const newProfile = convertRulesyncToCodexProfile({
18447
19145
  config: rulesyncPermissions.getJson(),
18448
19146
  logger
18449
19147
  });
18450
- const permissionsTable = toMutableTable(parsed.permissions);
19148
+ const permissionsTable = toMutableTable(existing.permissions);
18451
19149
  const { profile: existingProfile, domainsHadUnknown: existingDomainsHadUnknown } = toCodexProfile(permissionsTable[RULESYNC_PROFILE_NAME]);
18452
- 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)"}".`);
18453
- 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.`);
18454
- 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.`);
18455
- if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19150
+ warnAboutPreservedProfileState({
19151
+ existingProfile,
19152
+ newProfile,
19153
+ existingDomainsHadUnknown,
19154
+ logger
19155
+ });
18456
19156
  permissionsTable[RULESYNC_PROFILE_NAME] = mergeWithExistingProfile({
18457
19157
  newProfile,
18458
19158
  existingProfile
18459
19159
  });
18460
- parsed.permissions = permissionsTable;
18461
- parsed.default_permissions = RULESYNC_PROFILE_NAME;
18462
- applyCodexcliOverride({
18463
- parsed,
19160
+ const overridePatch = computeCodexcliOverridePatch({
19161
+ existing,
18464
19162
  override: rulesyncPermissions.getJson().codexcli,
18465
19163
  logger
18466
19164
  });
@@ -18468,7 +19166,17 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
18468
19166
  outputRoot,
18469
19167
  relativeDirPath: paths.relativeDirPath,
18470
19168
  relativeFilePath: paths.relativeFilePath,
18471
- fileContent: smol_toml.stringify(parsed),
19169
+ fileContent: applySharedConfigPatch({
19170
+ fileKey: sharedConfigFileKey(paths),
19171
+ feature: "permissions",
19172
+ existingContent,
19173
+ patch: {
19174
+ permissions: permissionsTable,
19175
+ default_permissions: RULESYNC_PROFILE_NAME,
19176
+ ...overridePatch
19177
+ },
19178
+ filePath
19179
+ }),
18472
19180
  validate
18473
19181
  });
18474
19182
  }
@@ -18638,6 +19346,12 @@ function toCodexProfile(value) {
18638
19346
  domainsHadUnknown
18639
19347
  };
18640
19348
  }
19349
+ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingDomainsHadUnknown, logger }) {
19350
+ 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)"}".`);
19351
+ 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.`);
19352
+ 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.`);
19353
+ if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
19354
+ }
18641
19355
  function mergeWithExistingProfile({ newProfile, existingProfile }) {
18642
19356
  if (!existingProfile) return newProfile;
18643
19357
  const mergedNetwork = { ...newProfile.network };
@@ -18688,8 +19402,9 @@ function toMutableTable(value) {
18688
19402
  function isPlainObject(value) {
18689
19403
  return value !== null && typeof value === "object" && !Array.isArray(value);
18690
19404
  }
18691
- function applyCodexcliOverride({ parsed, override, logger }) {
18692
- if (!override) return;
19405
+ function computeCodexcliOverridePatch({ existing, override, logger }) {
19406
+ const patch = {};
19407
+ if (!override) return patch;
18693
19408
  const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
18694
19409
  for (const [key, value] of Object.entries(override)) {
18695
19410
  if (!allowed.has(key)) {
@@ -18697,12 +19412,13 @@ function applyCodexcliOverride({ parsed, override, logger }) {
18697
19412
  continue;
18698
19413
  }
18699
19414
  if (value === void 0) continue;
18700
- const existing = parsed[key];
18701
- parsed[key] = isPlainObject(existing) && isPlainObject(value) ? {
18702
- ...existing,
19415
+ const existingValue = existing[key];
19416
+ patch[key] = isPlainObject(existingValue) && isPlainObject(value) ? {
19417
+ ...existingValue,
18703
19418
  ...value
18704
19419
  } : value;
18705
19420
  }
19421
+ return patch;
18706
19422
  }
18707
19423
  function extractCodexcliOverride(table) {
18708
19424
  const override = {};
@@ -19263,15 +19979,17 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
19263
19979
  else delete mergedPermissions.ask;
19264
19980
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19265
19981
  else delete mergedPermissions.deny;
19266
- const merged = {
19267
- ...settings,
19268
- permissions: mergedPermissions
19269
- };
19270
19982
  return new DevinPermissions({
19271
19983
  outputRoot,
19272
19984
  relativeDirPath: paths.relativeDirPath,
19273
19985
  relativeFilePath: paths.relativeFilePath,
19274
- fileContent: JSON.stringify(merged, null, 2),
19986
+ fileContent: applySharedConfigPatch({
19987
+ fileKey: sharedConfigFileKey(paths),
19988
+ feature: "permissions",
19989
+ existingContent,
19990
+ patch: { permissions: mergedPermissions },
19991
+ filePath
19992
+ }),
19275
19993
  validate
19276
19994
  });
19277
19995
  }
@@ -19364,9 +20082,11 @@ const FACTORYDROID_OVERRIDE_KEYS = [
19364
20082
  "networkPolicy",
19365
20083
  "sandbox",
19366
20084
  "mcpPolicy",
20085
+ "mcpAutonomyOverrides",
19367
20086
  "enableDroidShield",
19368
20087
  "sessionDefaultSettings",
19369
20088
  "maxAutonomyLevel",
20089
+ "subagentAutonomyLevel",
19370
20090
  "interactionMode"
19371
20091
  ];
19372
20092
  /**
@@ -19724,7 +20444,6 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
19724
20444
  }
19725
20445
  //#endregion
19726
20446
  //#region src/features/permissions/grokcli-permissions.ts
19727
- const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
19728
20447
  const GROKCLI_UI_KEY = "ui";
19729
20448
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
19730
20449
  const GROKCLI_PERMISSION_KEY = "permission";
@@ -19826,13 +20545,19 @@ function parseGrokEntry(entry) {
19826
20545
  * compatibility with older Grok versions: `always-approve` when the config is
19827
20546
  * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
19828
20547
  * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
19829
- * arrays).
19830
- *
19831
- * This surface is **global only** — the adapter syncs the user-level
19832
- * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
19833
- * file; project scope is not modeled here.) The shared config is merged in
20548
+ * arrays). It is a user-level UI setting, so it is only written in global scope
20549
+ * (see below).
20550
+ *
20551
+ * Both scopes are supported: the adapter syncs the project-level
20552
+ * `./.grok/config.toml` (default) and the user-level `~/.grok/config.toml`
20553
+ * (with `--global`). Grok documents that "Project configs are limited to MCP
20554
+ * servers, plugins, and permission rules, not full user configs"
20555
+ * (https://docs.x.ai/build/settings), so the project config carries only the
20556
+ * fine-grained `[permission]` rules; the coarse `[ui] permission_mode` UI toggle
20557
+ * is written in global scope only. The shared config is merged in
19834
20558
  * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
19835
- * tools it models and the `[ui] permission_mode` value, while every other key
20559
+ * tools it models and (in global scope) the `[ui] permission_mode` value, while
20560
+ * every other key
19836
20561
  * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
19837
20562
  * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
19838
20563
  * file is never deleted.
@@ -19854,7 +20579,6 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19854
20579
  };
19855
20580
  }
19856
20581
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19857
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
19858
20582
  const paths = GrokcliPermissions.getSettablePaths({ global });
19859
20583
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
19860
20584
  return new GrokcliPermissions({
@@ -19863,11 +20587,10 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19863
20587
  relativeFilePath: paths.relativeFilePath,
19864
20588
  fileContent,
19865
20589
  validate,
19866
- global: true
20590
+ global
19867
20591
  });
19868
20592
  }
19869
20593
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
19870
- if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
19871
20594
  const paths = GrokcliPermissions.getSettablePaths({ global });
19872
20595
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
19873
20596
  const existingContent = await readFileContentOrNull(filePath) ?? "";
@@ -19880,25 +20603,32 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19880
20603
  const config = rulesyncPermissions.getJson();
19881
20604
  const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19882
20605
  const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19883
- parsed[GROKCLI_PERMISSION_KEY] = {
20606
+ const permission = {
19884
20607
  ...existingPermission,
19885
20608
  allow: buckets.allow,
19886
20609
  deny: buckets.deny,
19887
20610
  ask: buckets.ask
19888
20611
  };
19889
- const mode = deriveGrokPermissionMode(config);
19890
- const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
19891
- parsed[GROKCLI_UI_KEY] = {
19892
- ...existingUi,
19893
- [GROKCLI_PERMISSION_MODE_KEY]: mode
19894
- };
20612
+ const uiPatch = global ? { [GROKCLI_UI_KEY]: {
20613
+ ...isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {},
20614
+ [GROKCLI_PERMISSION_MODE_KEY]: deriveGrokPermissionMode(config)
20615
+ } } : {};
19895
20616
  return new GrokcliPermissions({
19896
20617
  outputRoot,
19897
20618
  relativeDirPath: paths.relativeDirPath,
19898
20619
  relativeFilePath: paths.relativeFilePath,
19899
- fileContent: smol_toml.stringify(parsed),
20620
+ fileContent: applySharedConfigPatch({
20621
+ fileKey: sharedConfigFileKey(paths),
20622
+ feature: "permissions",
20623
+ existingContent,
20624
+ patch: {
20625
+ [GROKCLI_PERMISSION_KEY]: permission,
20626
+ ...uiPatch
20627
+ },
20628
+ filePath
20629
+ }),
19900
20630
  validate: true,
19901
- global: true
20631
+ global
19902
20632
  });
19903
20633
  }
19904
20634
  toRulesyncPermissions() {
@@ -19919,14 +20649,14 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
19919
20649
  error: null
19920
20650
  };
19921
20651
  }
19922
- static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
20652
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
19923
20653
  return new GrokcliPermissions({
19924
20654
  outputRoot,
19925
20655
  relativeDirPath,
19926
20656
  relativeFilePath,
19927
20657
  fileContent: "",
19928
20658
  validate: false,
19929
- global: true
20659
+ global
19930
20660
  });
19931
20661
  }
19932
20662
  };
@@ -20599,7 +21329,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
20599
21329
  outputRoot,
20600
21330
  relativeDirPath: paths.relativeDirPath,
20601
21331
  relativeFilePath: paths.relativeFilePath,
20602
- fileContent: JSON.stringify(next, null, 2),
21332
+ fileContent: applySharedConfigPatch({
21333
+ fileKey: sharedConfigFileKey(paths),
21334
+ feature: "permissions",
21335
+ existingContent,
21336
+ patch: {
21337
+ allowedTools: next.allowedTools,
21338
+ toolsSettings: next.toolsSettings
21339
+ },
21340
+ filePath
21341
+ }),
20603
21342
  validate
20604
21343
  });
20605
21344
  }
@@ -20832,6 +21571,22 @@ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
20832
21571
  "notebookedit",
20833
21572
  "agent"
20834
21573
  ]);
21574
+ /**
21575
+ * Translate between rulesync's canonical permission category names and
21576
+ * OpenCode's native permission keys. OpenCode has no `agent` key — subagent
21577
+ * launches are gated by the `task` key (see the documented key list at
21578
+ * https://opencode.ai/docs/permissions/). Without this translation a canonical
21579
+ * `agent: deny` would be written verbatim into `opencode.json` and silently
21580
+ * ignored by OpenCode. Unknown names pass through unchanged.
21581
+ */
21582
+ const CANONICAL_TO_OPENCODE_PERMISSION_KEYS = { agent: "task" };
21583
+ const OPENCODE_TO_CANONICAL_PERMISSION_KEYS = Object.fromEntries(Object.entries(CANONICAL_TO_OPENCODE_PERMISSION_KEYS).map(([canonical, opencode]) => [opencode, canonical]));
21584
+ function toOpencodePermissionKey(canonical) {
21585
+ return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? canonical;
21586
+ }
21587
+ function toCanonicalPermissionKey(opencodeKey) {
21588
+ return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
21589
+ }
20835
21590
  function isSharedPermissionCategory(category) {
20836
21591
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
20837
21592
  }
@@ -20896,21 +21651,24 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
20896
21651
  fileContent = await readFileContentOrNull(jsonPath);
20897
21652
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
20898
21653
  }
20899
- const parsed = (0, jsonc_parser.parse)(fileContent ?? "{}");
20900
21654
  const rulesyncJson = rulesyncPermissions.getJson();
20901
21655
  const overridePermission = rulesyncJson.opencode?.permission ?? {};
20902
- const nextJson = {
20903
- ...parsed,
20904
- permission: {
20905
- ...rulesyncJson.permission,
20906
- ...overridePermission
20907
- }
20908
- };
21656
+ const sharedPermission = {};
21657
+ for (const [category, value] of Object.entries(rulesyncJson.permission ?? {})) sharedPermission[toOpencodePermissionKey(category)] = value;
20909
21658
  return new OpencodePermissions({
20910
21659
  outputRoot,
20911
21660
  relativeDirPath: basePaths.relativeDirPath,
20912
21661
  relativeFilePath,
20913
- fileContent: JSON.stringify(nextJson, null, 2),
21662
+ fileContent: applySharedConfigPatch({
21663
+ fileKey: sharedConfigFileKey(basePaths),
21664
+ feature: "permissions",
21665
+ existingContent: fileContent ?? "",
21666
+ patch: { permission: {
21667
+ ...sharedPermission,
21668
+ ...overridePermission
21669
+ } },
21670
+ filePath: (0, node_path.join)(jsonDir, relativeFilePath)
21671
+ }),
20914
21672
  validate: true
20915
21673
  });
20916
21674
  }
@@ -20922,8 +21680,11 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
20922
21680
  }
20923
21681
  const shared = {};
20924
21682
  const overrideOnly = {};
20925
- for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
20926
- else overrideOnly[category] = value;
21683
+ for (const [category, value] of Object.entries(rawPermission)) {
21684
+ const canonicalCategory = toCanonicalPermissionKey(category);
21685
+ if (isSharedPermissionCategory(canonicalCategory)) shared[canonicalCategory] = typeof value === "string" ? { "*": value } : value;
21686
+ else overrideOnly[category] = value;
21687
+ }
20927
21688
  const json = Object.keys(overrideOnly).length > 0 ? {
20928
21689
  permission: shared,
20929
21690
  opencode: { permission: overrideOnly }
@@ -21048,6 +21809,7 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
21048
21809
  "disabled"
21049
21810
  ];
21050
21811
  const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
21812
+ const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
21051
21813
  function asPlainRecord(value) {
21052
21814
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
21053
21815
  }
@@ -21119,20 +21881,24 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21119
21881
  else delete mergedPermissions.ask;
21120
21882
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21121
21883
  else delete mergedPermissions.deny;
21122
- const merged = {
21123
- ...settings,
21124
- permissions: mergedPermissions
21125
- };
21126
21884
  const override = config.qwencode;
21127
- if (override?.tools !== void 0) merged.tools = {
21885
+ if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
21886
+ const patch = { permissions: mergedPermissions };
21887
+ if (override?.tools !== void 0) patch.tools = {
21128
21888
  ...asPlainRecord(settings.tools),
21129
21889
  ...asPlainRecord(override.tools)
21130
21890
  };
21131
- if (override?.security !== void 0) merged.security = {
21891
+ if (override?.security !== void 0) patch.security = {
21132
21892
  ...asPlainRecord(settings.security),
21133
21893
  ...asPlainRecord(override.security)
21134
21894
  };
21135
- const fileContent = JSON.stringify(merged, null, 2);
21895
+ const fileContent = applySharedConfigPatch({
21896
+ fileKey: sharedConfigFileKey(paths),
21897
+ feature: "permissions",
21898
+ existingContent,
21899
+ patch,
21900
+ filePath
21901
+ });
21136
21902
  return new QwencodePermissions({
21137
21903
  outputRoot,
21138
21904
  relativeDirPath: paths.relativeDirPath,
@@ -21159,9 +21925,11 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
21159
21925
  });
21160
21926
  const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
21161
21927
  const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
21928
+ const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
21162
21929
  const qwencodeOverride = {};
21163
21930
  if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
21164
21931
  if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
21932
+ if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
21165
21933
  const result = { ...config };
21166
21934
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
21167
21935
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
@@ -21367,7 +22135,9 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21367
22135
  }
21368
22136
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21369
22137
  const paths = this.getSettablePaths({ global });
21370
- const parsed = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
22138
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
22139
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
22140
+ const parsed = parseReasonixConfig(existingContent);
21371
22141
  const config = rulesyncPermissions.getJson();
21372
22142
  const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
21373
22143
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
@@ -21392,25 +22162,27 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
21392
22162
  else delete mergedPermissions.ask;
21393
22163
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
21394
22164
  else delete mergedPermissions.deny;
21395
- const merged = {
21396
- ...parsed,
21397
- permissions: mergedPermissions
21398
- };
22165
+ const patch = { permissions: mergedPermissions };
21399
22166
  const override = config.reasonix;
21400
- if (override?.sandbox !== void 0) merged.sandbox = {
22167
+ if (override?.sandbox !== void 0) patch.sandbox = {
21401
22168
  ...asReasonixRecord(parsed.sandbox),
21402
22169
  ...asReasonixRecord(override.sandbox)
21403
22170
  };
21404
- if (override?.agent !== void 0) merged.agent = {
22171
+ if (override?.agent !== void 0) patch.agent = {
21405
22172
  ...asReasonixRecord(parsed.agent),
21406
22173
  ...asReasonixRecord(override.agent)
21407
22174
  };
21408
- const fileContent = smol_toml.stringify(merged);
21409
22175
  return new ReasonixPermissions({
21410
22176
  outputRoot,
21411
22177
  relativeDirPath: paths.relativeDirPath,
21412
22178
  relativeFilePath: paths.relativeFilePath,
21413
- fileContent,
22179
+ fileContent: applySharedConfigPatch({
22180
+ fileKey: sharedConfigFileKey(paths),
22181
+ feature: "permissions",
22182
+ existingContent,
22183
+ patch,
22184
+ filePath
22185
+ }),
21414
22186
  validate
21415
22187
  });
21416
22188
  }
@@ -21981,7 +22753,9 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
21981
22753
  }
21982
22754
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
21983
22755
  const paths = this.getSettablePaths({ global });
21984
- const config = parseVibeConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
22756
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
22757
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
22758
+ const config = parseVibeConfig(existingContent);
21985
22759
  const permission = rulesyncPermissions.getJson().permission;
21986
22760
  const tools = toVibeToolsRecord(config.tools);
21987
22761
  const enabledTools = new Set(toStringArray(config.enabled_tools));
@@ -22026,16 +22800,21 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
22026
22800
  tools[vibeToolName] = nextTool;
22027
22801
  }
22028
22802
  applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
22029
- config.tools = tools;
22030
- if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
22031
- else delete config.enabled_tools;
22032
- if (disabledTools.size > 0) config.disabled_tools = [...disabledTools].toSorted();
22033
- else delete config.disabled_tools;
22034
22803
  return new VibePermissions({
22035
22804
  outputRoot,
22036
22805
  relativeDirPath: paths.relativeDirPath,
22037
22806
  relativeFilePath: paths.relativeFilePath,
22038
- fileContent: smol_toml.stringify(config),
22807
+ fileContent: applySharedConfigPatch({
22808
+ fileKey: sharedConfigFileKey(paths),
22809
+ feature: "permissions",
22810
+ existingContent,
22811
+ patch: {
22812
+ tools,
22813
+ enabled_tools: enabledTools.size > 0 ? [...enabledTools].toSorted() : void 0,
22814
+ disabled_tools: disabledTools.size > 0 ? [...disabledTools].toSorted() : void 0
22815
+ },
22816
+ filePath
22817
+ }),
22039
22818
  validate,
22040
22819
  global
22041
22820
  });
@@ -22498,24 +23277,26 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
22498
23277
  }
22499
23278
  const managedToolNames = new Set(Object.keys(managedTools));
22500
23279
  const preservedTools = Object.fromEntries(Object.entries(existingTools).filter(([toolName]) => !managedToolNames.has(toolName)));
22501
- const mergedSettings = {
22502
- ...settings,
22503
- agent: {
22504
- ...agent,
22505
- tool_permissions: {
22506
- ...toolPermissions,
22507
- tools: {
22508
- ...preservedTools,
22509
- ...managedTools
22510
- }
22511
- }
22512
- }
22513
- };
22514
23280
  return new ZedPermissions({
22515
23281
  outputRoot,
22516
23282
  relativeDirPath: paths.relativeDirPath,
22517
23283
  relativeFilePath: paths.relativeFilePath,
22518
- fileContent: JSON.stringify(mergedSettings, null, 2),
23284
+ fileContent: applySharedConfigPatch({
23285
+ fileKey: sharedConfigFileKey(paths),
23286
+ feature: "permissions",
23287
+ existingContent,
23288
+ patch: { agent: {
23289
+ ...agent,
23290
+ tool_permissions: {
23291
+ ...toolPermissions,
23292
+ tools: {
23293
+ ...preservedTools,
23294
+ ...managedTools
23295
+ }
23296
+ }
23297
+ } },
23298
+ filePath
23299
+ }),
22519
23300
  validate: true
22520
23301
  });
22521
23302
  }
@@ -22657,7 +23438,7 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
22657
23438
  ["grokcli", {
22658
23439
  class: GrokcliPermissions,
22659
23440
  meta: {
22660
- supportsProject: false,
23441
+ supportsProject: true,
22661
23442
  supportsGlobal: true,
22662
23443
  supportsImport: true
22663
23444
  }
@@ -38205,8 +38986,13 @@ const collectFactoryPaths = (factory) => [...settablePathsForScope({
38205
38986
  * Derive, from the processor registry, every on-disk file that two or more
38206
38987
  * features read-modify-write. Source of truth the generation step graph's
38207
38988
  * `writesSharedFile` declarations must match.
38989
+ *
38990
+ * `minWriters: 1` widens the result to single-feature files as well — used to
38991
+ * validate `SHARED_CONFIG_OWNERSHIP` declarations for files that route through
38992
+ * the gateway without being cross-feature shared (e.g. a global-scope twin
38993
+ * only one feature writes).
38208
38994
  */
38209
- const deriveSharedFileWriters = () => {
38995
+ const deriveSharedFileWriters = ({ minWriters = 2 } = {}) => {
38210
38996
  const byKey = /* @__PURE__ */ new Map();
38211
38997
  const pathByKey = /* @__PURE__ */ new Map();
38212
38998
  for (const entry of PROCESSOR_REGISTRY) {
@@ -38232,7 +39018,7 @@ const deriveSharedFileWriters = () => {
38232
39018
  }
38233
39019
  const writers = [];
38234
39020
  for (const [key, features] of byKey) {
38235
- if (features.size < 2) continue;
39021
+ if (features.size < minWriters) continue;
38236
39022
  const path = pathByKey.get(key);
38237
39023
  const toolsByFeature = /* @__PURE__ */ new Map();
38238
39024
  for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());