rulesync 9.2.0 → 9.4.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.
@@ -3003,8 +3003,8 @@ const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
3003
3003
  const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
3004
3004
  const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
3005
3005
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
3006
+ const DEVIN_GLOBAL_SKILLS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
3006
3007
  const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
3007
- const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
3008
3008
  const DEVIN_CONFIG_FILE_NAME = "config.json";
3009
3009
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
3010
3010
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
@@ -3892,6 +3892,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3892
3892
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3893
3893
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3894
3894
  //#endregion
3895
+ //#region src/constants/takt-paths.ts
3896
+ const TAKT_DIR = ".takt";
3897
+ const TAKT_FACETS_SUBDIR = "facets";
3898
+ const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
3899
+ const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
3900
+ const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
3901
+ const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
3902
+ const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
3903
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
3904
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3905
+ /**
3906
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3907
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3908
+ * profiles (including permission modes), and other Takt settings.
3909
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3910
+ */
3911
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3912
+ /**
3913
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3914
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3915
+ * transport must be explicitly enabled here before any workflow-defined MCP
3916
+ * server using it is permitted to run.
3917
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3918
+ */
3919
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3920
+ //#endregion
3921
+ //#region src/utils/prototype-pollution.ts
3922
+ /**
3923
+ * Keys that, if walked into when constructing or merging objects from
3924
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3925
+ * chain) and propagate state to every other object in the runtime. Any code
3926
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3927
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3928
+ */
3929
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3930
+ "__proto__",
3931
+ "constructor",
3932
+ "prototype"
3933
+ ]);
3934
+ function isPrototypePollutionKey(key) {
3935
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3936
+ }
3937
+ /**
3938
+ * Returns a shallow copy of a record's own entries with every
3939
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3940
+ *
3941
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3942
+ * or `headers` table — into freshly generated config. Carrying such a map by
3943
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3944
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3945
+ * prototype). Walking the entries through this helper severs that path while
3946
+ * preserving every legitimate key.
3947
+ */
3948
+ function omitPrototypePollutionKeys(record) {
3949
+ const sanitized = {};
3950
+ for (const [key, value] of Object.entries(record)) {
3951
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3952
+ sanitized[key] = value;
3953
+ }
3954
+ return sanitized;
3955
+ }
3956
+ //#endregion
3957
+ //#region src/features/shared/shared-config-gateway.ts
3958
+ function sanitizeSharedConfigValue(value) {
3959
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
3960
+ if (!isPlainObject(value)) return value;
3961
+ const result = {};
3962
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
3963
+ return result;
3964
+ }
3965
+ /**
3966
+ * Parse a shared config file into a plain document: an empty/whitespace file
3967
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
3968
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
3969
+ * path when one is given.
3970
+ */
3971
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
3972
+ if (fileContent.trim() === "") return {};
3973
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
3974
+ let parsed;
3975
+ try {
3976
+ if (format === "yaml") parsed = load(fileContent);
3977
+ else if (format === "json") parsed = JSON.parse(fileContent);
3978
+ else parsed = parse(fileContent);
3979
+ } catch (error) {
3980
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
3981
+ }
3982
+ if (parsed === void 0 || parsed === null) return {};
3983
+ if (!isPlainObject(parsed)) {
3984
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
3985
+ return {};
3986
+ }
3987
+ return sanitizeSharedConfigValue(parsed);
3988
+ }
3989
+ /**
3990
+ * Serialize a shared config document. YAML output always ends with exactly one
3991
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
3992
+ * writers have always emitted (no trailing newline).
3993
+ */
3994
+ function stringifySharedConfig({ format, document }) {
3995
+ if (format === "yaml") return dump(document, {
3996
+ noRefs: true,
3997
+ sortKeys: false
3998
+ }).trimEnd() + "\n";
3999
+ return JSON.stringify(document, null, 2);
4000
+ }
4001
+ /**
4002
+ * Shallow merge: every top-level key in `patch` replaces the base key
4003
+ * wholesale; all other base keys are preserved. The policy for a feature that
4004
+ * owns a fixed set of top-level keys.
4005
+ */
4006
+ function mergeSharedConfigShallow({ base, patch }) {
4007
+ return {
4008
+ ...base,
4009
+ ...sanitizeSharedConfigValue(patch)
4010
+ };
4011
+ }
4012
+ /**
4013
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4014
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4015
+ * feature whose contribution interleaves with user-authored siblings at any
4016
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4017
+ * per-provider option tables) — nested sibling keys are preserved by
4018
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4019
+ * keys are dropped.
4020
+ */
4021
+ function mergeSharedConfigDeep({ base, patch }) {
4022
+ const result = { ...base };
4023
+ for (const [key, patchValue] of Object.entries(patch)) {
4024
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4025
+ const baseValue = result[key];
4026
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4027
+ base: baseValue,
4028
+ patch: patchValue
4029
+ });
4030
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4031
+ }
4032
+ return result;
4033
+ }
4034
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4035
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4036
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4037
+ /**
4038
+ * Who owns what in each gateway-managed shared config file, and which policy
4039
+ * resolves conflicts. Keys are `dir/file` tokens matching
4040
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4041
+ * lock-step with the writers derived from the processor registry, so an
4042
+ * undeclared writer fails CI instead of merging by accident.
4043
+ */
4044
+ const SHARED_CONFIG_OWNERSHIP = {
4045
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4046
+ format: "json",
4047
+ features: {
4048
+ ignore: {
4049
+ kind: "custom",
4050
+ policyFunction: "applyIgnoreReadDenies"
4051
+ },
4052
+ hooks: {
4053
+ kind: "replace-owned-keys",
4054
+ ownedKeys: ["hooks"]
4055
+ },
4056
+ permissions: {
4057
+ kind: "custom",
4058
+ policyFunction: "applyPermissions"
4059
+ }
4060
+ }
4061
+ },
4062
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4063
+ format: "yaml",
4064
+ features: {
4065
+ subagents: {
4066
+ kind: "replace-owned-keys",
4067
+ ownedKeys: ["plugins"]
4068
+ },
4069
+ mcp: {
4070
+ kind: "replace-owned-keys",
4071
+ ownedKeys: ["mcp_servers"]
4072
+ },
4073
+ hooks: {
4074
+ kind: "replace-owned-keys",
4075
+ ownedKeys: ["hooks"]
4076
+ },
4077
+ permissions: {
4078
+ kind: "deep-merge",
4079
+ replaceKeys: ["permissions"]
4080
+ }
4081
+ }
4082
+ },
4083
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4084
+ format: "yaml",
4085
+ invalidRootPolicy: "error",
4086
+ features: {
4087
+ mcp: {
4088
+ kind: "replace-owned-keys",
4089
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4090
+ },
4091
+ permissions: { kind: "deep-merge" }
4092
+ }
4093
+ }
4094
+ };
4095
+ /**
4096
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4097
+ * the existing content, merge the patch under the feature's declared policy,
4098
+ * and serialize. Throws when the file or feature is undeclared, when a
4099
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4100
+ * feature's policy is `custom` (those calls go to the named policy function
4101
+ * instead).
4102
+ */
4103
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4104
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4105
+ if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
4106
+ const policy = declaration.features[feature];
4107
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4108
+ if (policy.kind === "custom") throw new Error(`Feature '${feature}' writes '${fileKey}' through its dedicated policy function '${policy.policyFunction}' in shared-config-gateway.ts, not applySharedConfigPatch.`);
4109
+ const base = parseSharedConfig({
4110
+ format: declaration.format,
4111
+ fileContent: existingContent,
4112
+ filePath,
4113
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4114
+ });
4115
+ if (policy.kind === "replace-owned-keys") {
4116
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4117
+ 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.`);
4118
+ return stringifySharedConfig({
4119
+ format: declaration.format,
4120
+ document: mergeSharedConfigShallow({
4121
+ base,
4122
+ patch
4123
+ })
4124
+ });
4125
+ }
4126
+ const merged = mergeSharedConfigDeep({
4127
+ base,
4128
+ patch
4129
+ });
4130
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4131
+ return stringifySharedConfig({
4132
+ format: declaration.format,
4133
+ document: merged
4134
+ });
4135
+ }
4136
+ const READ_TOOL_NAME = "Read";
4137
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4138
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4139
+ const parsePermissionsBlock = (settings) => {
4140
+ const permissions = settings.permissions ?? {};
4141
+ return {
4142
+ allow: permissions.allow ?? [],
4143
+ ask: permissions.ask ?? [],
4144
+ deny: permissions.deny ?? []
4145
+ };
4146
+ };
4147
+ const withPermissions = (settings, next) => {
4148
+ const permissions = { ...settings.permissions };
4149
+ const assign = (key, values) => {
4150
+ if (values.length > 0) permissions[key] = values;
4151
+ else delete permissions[key];
4152
+ };
4153
+ assign("allow", next.allow);
4154
+ assign("ask", next.ask);
4155
+ assign("deny", next.deny);
4156
+ return {
4157
+ ...settings,
4158
+ permissions
4159
+ };
4160
+ };
4161
+ const applyIgnoreReadDenies = (params) => {
4162
+ const { settings, readDenies } = params;
4163
+ const current = parsePermissionsBlock(settings);
4164
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4165
+ return withPermissions(settings, {
4166
+ allow: current.allow,
4167
+ ask: current.ask,
4168
+ deny: uniq([...preservedDeny, ...readDenies].toSorted())
4169
+ });
4170
+ };
4171
+ const applyPermissions = (params) => {
4172
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4173
+ const current = parsePermissionsBlock(settings);
4174
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4175
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4176
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4177
+ if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
4178
+ }
4179
+ return withPermissions(settings, {
4180
+ allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
4181
+ ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
4182
+ deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
4183
+ });
4184
+ };
4185
+ //#endregion
3895
4186
  //#region src/features/opencode-config.ts
3896
4187
  /**
3897
4188
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3916,9 +4207,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3916
4207
  });
3917
4208
  const fileContent = await readFileContentOrNull(join(configDir, "opencode.jsonc")) ?? await readFileContentOrNull(join(configDir, "opencode.json"));
3918
4209
  if (!fileContent) return {};
3919
- const parsed = parse(fileContent);
3920
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3921
- return parsed;
4210
+ return parseSharedConfig({
4211
+ format: "jsonc",
4212
+ fileContent
4213
+ });
3922
4214
  }
3923
4215
  /**
3924
4216
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4827,32 +5119,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4827
5119
  }
4828
5120
  };
4829
5121
  //#endregion
4830
- //#region src/constants/takt-paths.ts
4831
- const TAKT_DIR = ".takt";
4832
- const TAKT_FACETS_SUBDIR = "facets";
4833
- const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
4834
- const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
4835
- const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
4836
- const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
4837
- const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
4838
- const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
4839
- const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4840
- /**
4841
- * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
4842
- * `~/.takt/config.yaml` (global); it holds the active provider, provider
4843
- * profiles (including permission modes), and other Takt settings.
4844
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4845
- */
4846
- const TAKT_CONFIG_FILE_NAME = "config.yaml";
4847
- /**
4848
- * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4849
- * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4850
- * transport must be explicitly enabled here before any workflow-defined MCP
4851
- * server using it is permitted to run.
4852
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4853
- */
4854
- const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4855
- //#endregion
4856
5122
  //#region src/features/takt-shared.ts
4857
5123
  /**
4858
5124
  * Shared utilities for all TAKT-* tool file classes.
@@ -5688,6 +5954,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
5688
5954
  "sessionEnd",
5689
5955
  "beforeSubmitPrompt",
5690
5956
  "permissionRequest",
5957
+ "preToolUse",
5958
+ "postToolUse",
5691
5959
  "postToolUseFailure",
5692
5960
  "stop",
5693
5961
  "preCompact",
@@ -5713,7 +5981,13 @@ const CODEXCLI_HOOK_EVENTS = [
5713
5981
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
5714
5982
  * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
5715
5983
  * 1:1 canonical equivalent, so no new canonical events are required.
5716
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
5984
+ *
5985
+ * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
5986
+ * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
5987
+ * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
5988
+ * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
5989
+ * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
5990
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
5717
5991
  */
5718
5992
  const GOOSE_HOOK_EVENTS = [
5719
5993
  "sessionStart",
@@ -5726,9 +6000,7 @@ const GOOSE_HOOK_EVENTS = [
5726
6000
  "beforeReadFile",
5727
6001
  "afterFileEdit",
5728
6002
  "beforeShellExecution",
5729
- "afterShellExecution",
5730
- "subagentStart",
5731
- "subagentStop"
6003
+ "afterShellExecution"
5732
6004
  ];
5733
6005
  /** Hook events supported by Kiro CLI. */
5734
6006
  const KIRO_HOOK_EVENTS = [
@@ -6181,9 +6453,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6181
6453
  beforeReadFile: "BeforeReadFile",
6182
6454
  afterFileEdit: "AfterFileEdit",
6183
6455
  beforeShellExecution: "BeforeShellExecution",
6184
- afterShellExecution: "AfterShellExecution",
6185
- subagentStart: "SubagentStart",
6186
- subagentStop: "SubagentStop"
6456
+ afterShellExecution: "AfterShellExecution"
6187
6457
  };
6188
6458
  /**
6189
6459
  * Map Goose PascalCase event names to canonical camelCase.
@@ -6197,6 +6467,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
6197
6467
  sessionEnd: "session.end",
6198
6468
  beforeSubmitPrompt: "user.prompt",
6199
6469
  permissionRequest: "permission.request",
6470
+ preToolUse: "tool.use",
6471
+ postToolUse: "tool.result",
6200
6472
  postToolUseFailure: "tool.error",
6201
6473
  stop: "task.complete",
6202
6474
  preCompact: "context.compact",
@@ -6324,42 +6596,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6324
6596
  */
6325
6597
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6326
6598
  //#endregion
6327
- //#region src/utils/prototype-pollution.ts
6328
- /**
6329
- * Keys that, if walked into when constructing or merging objects from
6330
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
6331
- * chain) and propagate state to every other object in the runtime. Any code
6332
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
6333
- * parsing, MCP config conversion, settings round-trip — should skip these.
6334
- */
6335
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
6336
- "__proto__",
6337
- "constructor",
6338
- "prototype"
6339
- ]);
6340
- function isPrototypePollutionKey(key) {
6341
- return PROTOTYPE_POLLUTION_KEYS.has(key);
6342
- }
6343
- /**
6344
- * Returns a shallow copy of a record's own entries with every
6345
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
6346
- *
6347
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
6348
- * or `headers` table — into freshly generated config. Carrying such a map by
6349
- * reference, or re-assigning its keys via bracket notation, would let a literal
6350
- * `__proto__` key ride along (and re-assigning it would mutate the target's
6351
- * prototype). Walking the entries through this helper severs that path while
6352
- * preserving every legitimate key.
6353
- */
6354
- function omitPrototypePollutionKeys(record) {
6355
- const sanitized = {};
6356
- for (const [key, value] of Object.entries(record)) {
6357
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6358
- sanitized[key] = value;
6359
- }
6360
- return sanitized;
6361
- }
6362
- //#endregion
6363
6599
  //#region src/features/hooks/tool-hooks-converter.ts
6364
6600
  function isToolMatcherEntry(x) {
6365
6601
  if (x === null || typeof x !== "object") return false;
@@ -7018,24 +7254,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7018
7254
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7019
7255
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7020
7256
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7021
- let settings;
7022
- try {
7023
- settings = JSON.parse(existingContent);
7024
- } catch (error) {
7025
- throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
7026
- }
7027
7257
  const config = rulesyncHooks.getJson();
7028
- const claudeHooks = canonicalToToolHooks({
7029
- config,
7030
- toolOverrideHooks: config.claudecode?.hooks,
7031
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7032
- logger
7258
+ const fileContent = applySharedConfigPatch({
7259
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7260
+ feature: "hooks",
7261
+ existingContent,
7262
+ patch: { hooks: canonicalToToolHooks({
7263
+ config,
7264
+ toolOverrideHooks: config.claudecode?.hooks,
7265
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7266
+ logger
7267
+ }) },
7268
+ filePath
7033
7269
  });
7034
- const merged = {
7035
- ...settings,
7036
- hooks: claudeHooks
7037
- };
7038
- const fileContent = JSON.stringify(merged, null, 2);
7039
7270
  return new ClaudecodeHooks({
7040
7271
  outputRoot,
7041
7272
  relativeDirPath: paths.relativeDirPath,
@@ -8191,7 +8422,7 @@ const GOOSE_CONVERTER_CONFIG = {
8191
8422
  *
8192
8423
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
8193
8424
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
8194
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
8425
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
8195
8426
  */
8196
8427
  var GooseHooks = class GooseHooks extends ToolHooks {
8197
8428
  constructor(params) {
@@ -8242,7 +8473,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8242
8473
  throw new Error(`Failed to parse Goose hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
8243
8474
  }
8244
8475
  const hooks = toolHooksToCanonical({
8245
- hooks: parsed.hooks,
8476
+ hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
8246
8477
  converterConfig: GOOSE_CONVERTER_CONFIG
8247
8478
  });
8248
8479
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
@@ -8267,37 +8498,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8267
8498
  }
8268
8499
  };
8269
8500
  //#endregion
8270
- //#region src/features/hermes-config.ts
8271
- function sanitizeHermesConfigValue(value) {
8272
- if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
8273
- if (!isPlainObject(value)) return value;
8274
- const sanitized = omitPrototypePollutionKeys(value);
8275
- const result = {};
8276
- for (const [key, nestedValue] of Object.entries(sanitized)) {
8277
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8278
- result[key] = sanitizeHermesConfigValue(nestedValue);
8279
- }
8280
- return result;
8281
- }
8282
- function parseHermesConfig(fileContent) {
8283
- if (!fileContent.trim()) return {};
8284
- const parsed = load(fileContent);
8285
- if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
8286
- return {};
8287
- }
8288
- function stringifyHermesConfig(config) {
8289
- return dump(config, {
8290
- noRefs: true,
8291
- sortKeys: false
8292
- }).trimEnd() + "\n";
8293
- }
8294
- function mergeHermesConfig(fileContent, patch) {
8295
- return stringifyHermesConfig({
8296
- ...parseHermesConfig(fileContent),
8297
- ...patch
8298
- });
8299
- }
8300
- //#endregion
8301
8501
  //#region src/features/hooks/hermesagent-hooks.ts
8302
8502
  /**
8303
8503
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8425,10 +8625,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8425
8625
  return true;
8426
8626
  }
8427
8627
  setFileContent(fileContent) {
8428
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8628
+ this.fileContent = applySharedConfigPatch({
8629
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8630
+ feature: "hooks",
8631
+ existingContent: fileContent,
8632
+ patch: parseSharedConfig({
8633
+ format: "yaml",
8634
+ fileContent: this.fileContent
8635
+ })
8636
+ });
8429
8637
  }
8430
8638
  toRulesyncHooks() {
8431
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8639
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8640
+ format: "yaml",
8641
+ fileContent: this.getFileContent()
8642
+ }).hooks);
8432
8643
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8433
8644
  version: 1,
8434
8645
  hooks
@@ -8438,11 +8649,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8438
8649
  const config = rulesyncHooks.getJson();
8439
8650
  return new HermesagentHooks({
8440
8651
  outputRoot,
8441
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8442
- config,
8443
- toolOverrideHooks: config.hermesagent?.hooks,
8444
- logger
8445
- }) })
8652
+ fileContent: stringifySharedConfig({
8653
+ format: "yaml",
8654
+ document: { hooks: canonicalToHermesHooks({
8655
+ config,
8656
+ toolOverrideHooks: config.hermesagent?.hooks,
8657
+ logger
8658
+ }) }
8659
+ })
8446
8660
  });
8447
8661
  }
8448
8662
  };
@@ -10462,66 +10676,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10462
10676
  }
10463
10677
  };
10464
10678
  //#endregion
10465
- //#region src/features/claudecode-settings-gateway.ts
10466
- /**
10467
- * Single owner of the `.claude/settings.json` `permissions` block, which both
10468
- * `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
10469
- * (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
10470
- * the merge, and the cross-feature ownership rule (permissions' explicit `Read`
10471
- * rules win over ignore-derived `Read` denies) used to be duplicated across both
10472
- * feature files; they live here once so each feature just states its intent and
10473
- * never reasons about the other's existence.
10474
- */
10475
- const READ_TOOL_NAME = "Read";
10476
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
10477
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
10478
- const parsePermissionsBlock = (settings) => {
10479
- const permissions = settings.permissions ?? {};
10480
- return {
10481
- allow: permissions.allow ?? [],
10482
- ask: permissions.ask ?? [],
10483
- deny: permissions.deny ?? []
10484
- };
10485
- };
10486
- const withPermissions = (settings, next) => {
10487
- const permissions = { ...settings.permissions };
10488
- const assign = (key, values) => {
10489
- if (values.length > 0) permissions[key] = values;
10490
- else delete permissions[key];
10491
- };
10492
- assign("allow", next.allow);
10493
- assign("ask", next.ask);
10494
- assign("deny", next.deny);
10495
- return {
10496
- ...settings,
10497
- permissions
10498
- };
10499
- };
10500
- const applyIgnoreReadDenies = (params) => {
10501
- const { settings, readDenies } = params;
10502
- const current = parsePermissionsBlock(settings);
10503
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
10504
- return withPermissions(settings, {
10505
- allow: current.allow,
10506
- ask: current.ask,
10507
- deny: uniq([...preservedDeny, ...readDenies].toSorted())
10508
- });
10509
- };
10510
- const applyPermissions = (params) => {
10511
- const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
10512
- const current = parsePermissionsBlock(settings);
10513
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
10514
- if (logger && managedToolNames.has(READ_TOOL_NAME)) {
10515
- const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
10516
- if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
10517
- }
10518
- return withPermissions(settings, {
10519
- allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
10520
- ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
10521
- deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
10522
- });
10523
- };
10524
- //#endregion
10525
10679
  //#region src/features/ignore/claudecode-ignore.ts
10526
10680
  const DEFAULT_FILE_MODE = "shared";
10527
10681
  /**
@@ -11944,7 +12098,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11944
12098
  };
11945
12099
  //#endregion
11946
12100
  //#region src/features/mcp/augmentcode-mcp.ts
11947
- const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
11948
12101
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
11949
12102
  const configPath = join(relativeDirPath, relativeFilePath);
11950
12103
  let parsed;
@@ -11959,13 +12112,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
11959
12112
  /**
11960
12113
  * AugmentCode (Auggie CLI) MCP servers.
11961
12114
  *
11962
- * MCP servers are persisted in the shared user settings file
11963
- * `~/.augment/settings.json` (global only the docs do not document a
11964
- * project-level MCP location). That same file also holds `hooks` and
12115
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12116
+ * at either scope: the committed workspace file for team-shared servers (project)
12117
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
11965
12118
  * `toolPermissions`, so generation merges the `mcpServers` block into the
11966
- * existing settings instead of overwriting it, and the file is never deleted.
12119
+ * existing settings instead of overwriting it, and the file is never deleted. On
12120
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12121
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
11967
12122
  *
11968
- * @see https://docs.augmentcode.com/cli/integrations
12123
+ * @see https://docs.augmentcode.com/cli/config
11969
12124
  */
11970
12125
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
11971
12126
  json;
@@ -11987,9 +12142,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
11987
12142
  };
11988
12143
  }
11989
12144
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
11990
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
11991
12145
  const paths = this.getSettablePaths({ global });
11992
- const json = parseAugmentcodeSettings(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12146
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12147
+ outputRoot,
12148
+ relativeDirPath: paths.relativeDirPath,
12149
+ baseFileName: paths.relativeFilePath,
12150
+ baseFallbackContent: "{}",
12151
+ includeLocalOverlay: !global
12152
+ }), paths.relativeDirPath, paths.relativeFilePath);
11993
12153
  const newJson = {
11994
12154
  ...json,
11995
12155
  mcpServers: json.mcpServers ?? {}
@@ -12004,7 +12164,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12004
12164
  });
12005
12165
  }
12006
12166
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12007
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12008
12167
  const paths = this.getSettablePaths({ global });
12009
12168
  const merged = {
12010
12169
  ...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -12279,6 +12438,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
12279
12438
  envVars: "env_vars"
12280
12439
  };
12281
12440
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
12441
+ /**
12442
+ * Translate a server's `oauth` table from the canonical rulesync shape (Claude
12443
+ * Code style camelCase) into the shape Codex CLI understands. Codex expects the
12444
+ * OAuth client id under snake_case `client_id`; without it `codex mcp login`
12445
+ * falls back to dynamic client registration and fails for providers that do not
12446
+ * support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
12447
+ * the added `client_id` so tools that read the camelCase shape keep working and
12448
+ * the round-trip stays stable.
12449
+ */
12450
+ function mapOauthToCodex(oauth) {
12451
+ const result = omitPrototypePollutionKeys(oauth);
12452
+ if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
12453
+ return result;
12454
+ }
12455
+ /**
12456
+ * Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
12457
+ * the canonical `clientId` on import. When both keys are present (the shape
12458
+ * rulesync itself emits) the canonical `clientId` wins and `client_id` is
12459
+ * dropped so a subsequent generate does not accumulate duplicates.
12460
+ */
12461
+ function mapOauthFromCodex(oauth) {
12462
+ const result = {};
12463
+ for (const [key, value] of Object.entries(oauth)) {
12464
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12465
+ if (key === "client_id") {
12466
+ if (!("clientId" in oauth)) result["clientId"] = value;
12467
+ continue;
12468
+ }
12469
+ result[key] = value;
12470
+ }
12471
+ return result;
12472
+ }
12282
12473
  function convertFromCodexFormat(codexMcp) {
12283
12474
  const result = {};
12284
12475
  for (const [name, config] of Object.entries(codexMcp)) {
@@ -12288,7 +12479,8 @@ function convertFromCodexFormat(codexMcp) {
12288
12479
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12289
12480
  if (key === "enabled") {
12290
12481
  if (value === false) converted["disabled"] = true;
12291
- } else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12482
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
12483
+ else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12292
12484
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
12293
12485
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12294
12486
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
@@ -12308,7 +12500,8 @@ function convertToCodexFormat(mcpServers) {
12308
12500
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12309
12501
  if (key === "disabled") {
12310
12502
  if (value === true) converted["enabled"] = false;
12311
- } else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12503
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
12504
+ else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12312
12505
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
12313
12506
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12314
12507
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
@@ -13524,10 +13717,12 @@ function resolveHermesTimeout(config) {
13524
13717
  *
13525
13718
  * Hermes is close to the MCP spec but not identical: `command` must be a single
13526
13719
  * executable string (an array's tail folds into `args`), a server is disabled
13527
- * via `enabled: false` (not the canonical `disabled: true`), and remote servers
13528
- * use `url`/`headers`. Only fields Hermes understands are emitted, so the shared
13529
- * `config.yaml` is not polluted with canonical-only aliases (`type`, `transport`,
13530
- * `httpUrl`, `networkTimeout`, tool-filter keys, ...).
13720
+ * via `enabled: false` (not the canonical `disabled: true`), remote servers use
13721
+ * `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
13722
+ * exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
13723
+ * fields Hermes understands are emitted, so the shared `config.yaml` is not
13724
+ * polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
13725
+ * `networkTimeout`, ...).
13531
13726
  */
13532
13727
  function convertServerToHermes(config) {
13533
13728
  const out = {};
@@ -13551,6 +13746,10 @@ function convertServerToHermes(config) {
13551
13746
  if (config.disabled === true) out.enabled = false;
13552
13747
  const timeout = resolveHermesTimeout(config);
13553
13748
  if (timeout !== void 0) out.timeout = timeout;
13749
+ const tools = {};
13750
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
13751
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
13752
+ if (Object.keys(tools).length > 0) out.tools = tools;
13554
13753
  return out;
13555
13754
  }
13556
13755
  /**
@@ -13592,6 +13791,10 @@ function convertFromHermesFormat(mcpServers) {
13592
13791
  if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
13593
13792
  if (config.enabled === false) server.disabled = true;
13594
13793
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
13794
+ if (isRecord(config.tools)) {
13795
+ if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
13796
+ if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
13797
+ }
13595
13798
  result[name] = server;
13596
13799
  }
13597
13800
  return result;
@@ -13610,7 +13813,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13610
13813
  config;
13611
13814
  constructor(params) {
13612
13815
  super(params);
13613
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13816
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13817
+ format: "yaml",
13818
+ fileContent: this.fileContent
13819
+ }) : {};
13614
13820
  }
13615
13821
  getConfig() {
13616
13822
  return this.config;
@@ -13619,9 +13825,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13619
13825
  return true;
13620
13826
  }
13621
13827
  setFileContent(fileContent) {
13622
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13828
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13829
+ format: "yaml",
13830
+ fileContent
13831
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13623
13832
  this.config = merged;
13624
- super.setFileContent(stringifyHermesConfig(merged));
13833
+ super.setFileContent(applySharedConfigPatch({
13834
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13835
+ feature: "mcp",
13836
+ existingContent: fileContent,
13837
+ patch: { mcp_servers: merged.mcp_servers }
13838
+ }));
13625
13839
  }
13626
13840
  isDeletable() {
13627
13841
  return false;
@@ -13648,12 +13862,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13648
13862
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13649
13863
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13650
13864
  const paths = this.getSettablePaths({ global });
13651
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13865
+ const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13866
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13867
+ format: "yaml",
13868
+ fileContent
13869
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13652
13870
  return new HermesagentMcp({
13653
13871
  outputRoot,
13654
13872
  relativeDirPath: paths.relativeDirPath,
13655
13873
  relativeFilePath: paths.relativeFilePath,
13656
- fileContent: stringifyHermesConfig(merged),
13874
+ fileContent: applySharedConfigPatch({
13875
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13876
+ feature: "mcp",
13877
+ existingContent: fileContent,
13878
+ patch: { mcp_servers: merged.mcp_servers }
13879
+ }),
13657
13880
  validate,
13658
13881
  global
13659
13882
  });
@@ -14818,28 +15041,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14818
15041
  }
14819
15042
  };
14820
15043
  //#endregion
14821
- //#region src/features/shared/takt-config.ts
14822
- /**
14823
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14824
- *
14825
- * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14826
- * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14827
- * instances are rejected for prototype-pollution hardening; a YAML mapping
14828
- * always parses to a plain object.
14829
- */
14830
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14831
- const configPath = join(relativeDirPath, relativeFilePath);
14832
- let parsed;
14833
- try {
14834
- parsed = fileContent.trim() === "" ? {} : load(fileContent);
14835
- } catch (error) {
14836
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14837
- }
14838
- if (parsed === void 0 || parsed === null) return {};
14839
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14840
- return parsed;
14841
- }
14842
- //#endregion
14843
15044
  //#region src/features/mcp/takt-mcp.ts
14844
15045
  /**
14845
15046
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -14905,17 +15106,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
14905
15106
  }
14906
15107
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14907
15108
  const paths = TaktMcp.getSettablePaths({ global });
14908
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15109
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15110
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
14909
15111
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14910
- const merged = {
14911
- ...config,
14912
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14913
- };
14914
15112
  return new TaktMcp({
14915
15113
  outputRoot,
14916
15114
  relativeDirPath: paths.relativeDirPath,
14917
15115
  relativeFilePath: paths.relativeFilePath,
14918
- fileContent: dump(merged),
15116
+ fileContent: applySharedConfigPatch({
15117
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15118
+ feature: "mcp",
15119
+ existingContent,
15120
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15121
+ filePath
15122
+ }),
14919
15123
  validate,
14920
15124
  global
14921
15125
  });
@@ -15360,7 +15564,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15360
15564
  ["augmentcode", {
15361
15565
  class: AugmentcodeMcp,
15362
15566
  meta: {
15363
- supportsProject: false,
15567
+ supportsProject: true,
15364
15568
  supportsGlobal: true,
15365
15569
  supportsEnabledTools: false,
15366
15570
  supportsDisabledTools: false
@@ -15747,17 +15951,317 @@ const PermissionActionSchema = z.enum([
15747
15951
  */
15748
15952
  const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
15749
15953
  /**
15954
+ * OpenCode-specific permission value. Unlike the shared canonical block, which
15955
+ * only accepts a pattern-to-action map, OpenCode also allows a bare action
15956
+ * string that applies to the whole category (e.g. `"external_directory": "deny"`).
15957
+ * This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
15958
+ */
15959
+ const OpencodeOverridePermissionValueSchema = z.union([PermissionActionSchema, PermissionRulesSchema]);
15960
+ /**
15961
+ * Tool-scoped override block for OpenCode. Permission categories placed here
15962
+ * (e.g. OpenCode-only categories such as `external_directory`) are emitted only
15963
+ * into OpenCode's config and never leak into other tools' permission files. It
15964
+ * also lets a shared category be overridden with an OpenCode-specific value.
15965
+ * Kept `looseObject` so future OpenCode categories are accepted.
15966
+ *
15967
+ * @example
15968
+ * { "permission": { "external_directory": "deny", "webfetch": "allow" } }
15969
+ */
15970
+ const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
15971
+ /**
15972
+ * Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
15973
+ * into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
15974
+ * It carries Hermes-specific approval/security controls that have no canonical
15975
+ * permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
15976
+ * `security` (`allow_private_urls`, ...), `skills.write_approval`,
15977
+ * `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
15978
+ * current or future Hermes config key can be authored without modeling each one.
15979
+ *
15980
+ * @example
15981
+ * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
15982
+ */
15983
+ const HermesPermissionsOverrideSchema = z.looseObject({});
15984
+ /**
15985
+ * Tool-scoped override block for Cline. Cline's `command-permissions.json`
15986
+ * carries a single global `allowRedirects` boolean (gates shell redirection
15987
+ * operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
15988
+ * canonical permission category. Placing it here lets users author it
15989
+ * declaratively; it is emitted only into Cline's config. Kept `looseObject` so
15990
+ * future Cline-only knobs can be added.
15991
+ *
15992
+ * @example
15993
+ * { "allowRedirects": true }
15994
+ */
15995
+ const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
15996
+ /**
15997
+ * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
15998
+ * object is a free-form record with tool-specific keys that have no canonical
15999
+ * category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
16000
+ * `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
16001
+ * (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
16002
+ * `repo_clone`, `repo_overview`). Placing them here makes them authorable and
16003
+ * portable and keeps them out of other tools' configs. Mirrors the OpenCode
16004
+ * override; each value may be a bare action string or a pattern map.
16005
+ *
16006
+ * @example
16007
+ * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
16008
+ */
16009
+ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
16010
+ /**
16011
+ * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
16012
+ * (in `.claude/settings.json`) carries non-list fields that have no canonical
16013
+ * permission category — `defaultMode` (the session-start permission mode) and
16014
+ * `additionalDirectories` (extra working directories) being the primary ones.
16015
+ * Fields placed under `claudecode.permissions` are merged into the settings
16016
+ * `permissions` object and emitted only for Claude Code, while the shared
16017
+ * `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
16018
+ * `looseObject` passthrough so any current or future `permissions` field can be
16019
+ * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
16020
+ * ignored here (rulesync owns them).
16021
+ *
16022
+ * @example
16023
+ * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16024
+ */
16025
+ const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
16026
+ /**
16027
+ * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16028
+ * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
16029
+ * the tool's base permission is ALWAYS (allow). The canonical model can only set
16030
+ * a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
16031
+ * these patterns" escalation cannot be expressed. Entries under
16032
+ * `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
16033
+ * category; the shared `permission` block still drives the base permission and
16034
+ * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
16035
+ *
16036
+ * @example
16037
+ * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
16038
+ */
16039
+ const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))) });
16040
+ /**
16041
+ * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
16042
+ * autonomy settings with no canonical permission category — `approvalMode`
16043
+ * (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
16044
+ * (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
16045
+ * `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
16046
+ * only for Cursor, while the shared `permission` block continues to drive the
16047
+ * `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
16048
+ * `cli.json` keys can be authored (they are merged verbatim on generate);
16049
+ * `sandbox`'s accepted values are not documented so it passes through verbatim.
16050
+ * Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
16051
+ * authored here reach `cli.json` on generate but are not re-extracted.
16052
+ *
16053
+ * @example
16054
+ * { "approvalMode": "auto-review" }
16055
+ */
16056
+ const CursorPermissionsOverrideSchema = z.looseObject({
16057
+ approvalMode: z.optional(z.string()),
16058
+ sandbox: z.optional(z.looseObject({}))
16059
+ });
16060
+ /**
16061
+ * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16062
+ * autonomy/sandbox controls with no canonical permission category — under
16063
+ * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16064
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16065
+ * placed here are merged into the matching `settings.json` group and emitted
16066
+ * only for Qwen, while the shared `permission` block continues to drive the
16067
+ * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16068
+ * passthrough) so any current or future `tools`/`security` key can be authored.
16069
+ *
16070
+ * @example
16071
+ * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
16072
+ */
16073
+ const QwencodePermissionsOverrideSchema = z.looseObject({
16074
+ tools: z.optional(z.looseObject({})),
16075
+ security: z.optional(z.looseObject({}))
16076
+ });
16077
+ /**
16078
+ * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
16079
+ * to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
16080
+ * enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
16081
+ * `network`) and plan-mode read-only trust lists under `[agent]`
16082
+ * (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
16083
+ * are merged into the matching `reasonix.toml` table and emitted only for
16084
+ * Reasonix, while the shared `permission` block continues to drive
16085
+ * `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
16086
+ * Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
16087
+ * re-extracted from `[agent]` on import — other `agent` keys authored here reach
16088
+ * `reasonix.toml` on generate but are not re-extracted back into the override.
16089
+ *
16090
+ * @example
16091
+ * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16092
+ */
16093
+ const ReasonixPermissionsOverrideSchema = z.looseObject({
16094
+ sandbox: z.optional(z.looseObject({})),
16095
+ agent: z.optional(z.looseObject({}))
16096
+ });
16097
+ /**
16098
+ * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
16099
+ * exposes security controls with no canonical per-command allow/ask/deny slot —
16100
+ * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16101
+ * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16102
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
16103
+ * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16104
+ * `interactionMode`). Fields placed here are merged into `settings.json` and
16105
+ * emitted only for Factory Droid, while the shared `permission` block continues
16106
+ * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16107
+ *
16108
+ * @example
16109
+ * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16110
+ */
16111
+ const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
16112
+ /**
16113
+ * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16114
+ * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
16115
+ * slot — `agent_mode_coding_permissions`
16116
+ * (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
16117
+ * `agent_mode_coding_file_read_allowlist` (a path array), and
16118
+ * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
16119
+ * Fields placed here are merged into `[agents.profiles]` of Warp's global
16120
+ * `settings.toml`, while the shared `permission` block continues to drive the
16121
+ * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
16122
+ * Warp permissions are global-only.
16123
+ *
16124
+ * @example
16125
+ * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16126
+ */
16127
+ const WarpPermissionsOverrideSchema = z.looseObject({
16128
+ agent_mode_coding_permissions: z.optional(z.string()),
16129
+ agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
16130
+ agent_mode_execute_readonly_commands: z.optional(z.boolean())
16131
+ });
16132
+ /**
16133
+ * Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
16134
+ * two top-level autonomy knobs with no canonical per-glob slot:
16135
+ * `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
16136
+ * `defaultBehavior` (the fallback action applied when no rule matches — Junie
16137
+ * documents only `allow`/`ask`). Fields placed here are merged onto the
16138
+ * top level of `allowlist.json`, while the shared `permission` block continues
16139
+ * to drive the per-category `rules` groups.
16140
+ *
16141
+ * @example
16142
+ * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16143
+ */
16144
+ const JuniePermissionsOverrideSchema = z.looseObject({
16145
+ allowReadonlyCommands: z.optional(z.boolean()),
16146
+ defaultBehavior: z.optional(z.string())
16147
+ });
16148
+ /**
16149
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16150
+ * permission surfaces the canonical coarse-mode mapping can't express:
16151
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16152
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16153
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16154
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16155
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16156
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16157
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16158
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16159
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16160
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16161
+ * scope are supported.
16162
+ *
16163
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16164
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16165
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16166
+ * of scope for this override.
16167
+ *
16168
+ * @example
16169
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16170
+ */
16171
+ const TaktPermissionsOverrideSchema = z.looseObject({
16172
+ step_permission_overrides: z.optional(z.record(z.string(), z.string())),
16173
+ provider_options: z.optional(z.looseObject({}))
16174
+ });
16175
+ /**
16176
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16177
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16178
+ * express, so they are authored here and merged into the shared Amp settings
16179
+ * file, while the shared `permission` block continues to drive the canonical
16180
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16181
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16182
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16183
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16184
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16185
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16186
+ * later fallbacks), preserving author order.
16187
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16188
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16189
+ * confirmation).
16190
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16191
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16192
+ * supported.
16193
+ *
16194
+ * @example
16195
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16196
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16197
+ */
16198
+ const AmpPermissionsOverrideSchema = z.looseObject({
16199
+ permissions: z.optional(z.array(z.looseObject({
16200
+ tool: z.string(),
16201
+ action: z.string()
16202
+ }))),
16203
+ mcpPermissions: z.optional(z.array(z.looseObject({}))),
16204
+ guardedFiles: z.optional(z.looseObject({ allowlist: z.optional(z.array(z.string())) })),
16205
+ dangerouslyAllowAll: z.optional(z.boolean())
16206
+ });
16207
+ /**
16208
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16209
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16210
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16211
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16212
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16213
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16214
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16215
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16216
+ * Fields placed here are merged onto the top level of
16217
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16218
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16219
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16220
+ * Verified against https://antigravity.google/docs/cli/reference and
16221
+ * https://antigravity.google/docs/cli/sandbox.
16222
+ *
16223
+ * @example
16224
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16225
+ */
16226
+ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
16227
+ toolPermission: z.optional(z.string()),
16228
+ enableTerminalSandbox: z.optional(z.boolean())
16229
+ });
16230
+ /**
15750
16231
  * Permissions configuration.
15751
16232
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
15752
16233
  * Values are pattern-to-action mappings for that tool category.
15753
16234
  *
16235
+ * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
16236
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16237
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16238
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16239
+ * other tool reads the shared `permission` block and ignores them.
16240
+ *
15754
16241
  * @example
15755
16242
  * {
15756
16243
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
15757
16244
  * "edit": { "*": "deny", "src/**": "allow" }
15758
16245
  * }
15759
16246
  */
15760
- const PermissionsConfigSchema = z.looseObject({ permission: z.record(z.string(), PermissionRulesSchema) });
16247
+ const PermissionsConfigSchema = z.looseObject({
16248
+ permission: z.record(z.string(), PermissionRulesSchema),
16249
+ opencode: z.optional(OpencodePermissionsOverrideSchema),
16250
+ hermes: z.optional(HermesPermissionsOverrideSchema),
16251
+ cline: z.optional(ClinePermissionsOverrideSchema),
16252
+ kilo: z.optional(KiloPermissionsOverrideSchema),
16253
+ claudecode: z.optional(ClaudecodePermissionsOverrideSchema),
16254
+ vibe: z.optional(VibePermissionsOverrideSchema),
16255
+ cursor: z.optional(CursorPermissionsOverrideSchema),
16256
+ qwencode: z.optional(QwencodePermissionsOverrideSchema),
16257
+ reasonix: z.optional(ReasonixPermissionsOverrideSchema),
16258
+ factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
16259
+ warp: z.optional(WarpPermissionsOverrideSchema),
16260
+ junie: z.optional(JuniePermissionsOverrideSchema),
16261
+ takt: z.optional(TaktPermissionsOverrideSchema),
16262
+ amp: z.optional(AmpPermissionsOverrideSchema),
16263
+ "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema)
16264
+ });
15761
16265
  /**
15762
16266
  * Full permissions file schema including optional $schema field.
15763
16267
  */
@@ -15868,6 +16372,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
15868
16372
  * Reference: https://ampcode.com/manual ("amp.permissions").
15869
16373
  */
15870
16374
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16375
+ /**
16376
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16377
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16378
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16379
+ * permissions override. Reference: https://ampcode.com/manual.
16380
+ */
16381
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16382
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16383
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16384
+ /**
16385
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16386
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16387
+ */
16388
+ function ampMatchesCmd(entry) {
16389
+ const cmd = entry.matches?.cmd;
16390
+ return typeof cmd === "string" ? cmd : void 0;
16391
+ }
16392
+ /**
16393
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16394
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16395
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16396
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16397
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16398
+ * override verbatim instead of being flattened with loss or dropped.
16399
+ */
16400
+ function isCanonicalAmpEntry(entry) {
16401
+ if (entry.action === "delegate") return false;
16402
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16403
+ const matches = entry.matches;
16404
+ if (matches === void 0) return true;
16405
+ const keys = Object.keys(matches);
16406
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16407
+ }
15871
16408
  function parseAmpSettings(fileContent) {
15872
16409
  const errors = [];
15873
16410
  const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -15883,10 +16420,20 @@ function toDisableList(value) {
15883
16420
  return value.filter((entry) => typeof entry === "string");
15884
16421
  }
15885
16422
  /**
16423
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16424
+ * settings, returning `undefined` when the key is absent or not an array so the
16425
+ * override omits it.
16426
+ */
16427
+ function extractAmpGuardedAllowlist(value) {
16428
+ if (!Array.isArray(value)) return void 0;
16429
+ return value.filter((entry) => typeof entry === "string");
16430
+ }
16431
+ /**
15886
16432
  * Read an `amp.permissions` array from untrusted parsed settings. Only entries
15887
- * whose shape rulesync understands are retained; the original objects are kept
15888
- * by reference (with a normalized `action`) so unknown sibling keys survive a
15889
- * round-trip when the entry is preserved.
16433
+ * whose `tool`/`action` rulesync understands are retained; every other key
16434
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16435
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16436
+ * round-trip.
15890
16437
  */
15891
16438
  function toPermissionsList(value) {
15892
16439
  if (!Array.isArray(value)) return [];
@@ -15896,14 +16443,11 @@ function toPermissionsList(value) {
15896
16443
  const { tool, action } = raw;
15897
16444
  if (typeof tool !== "string" || typeof action !== "string") continue;
15898
16445
  if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
15899
- const matches = raw.matches;
15900
- let normalizedMatches;
15901
- if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
15902
16446
  entries.push({
15903
16447
  ...raw,
15904
16448
  tool,
15905
16449
  action,
15906
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16450
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
15907
16451
  });
15908
16452
  }
15909
16453
  return entries;
@@ -15928,6 +16472,13 @@ function toPermissionsList(value) {
15928
16472
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
15929
16473
  * and writes merge into the existing JSON rather than overwriting it, and the
15930
16474
  * file is never deleted.
16475
+ *
16476
+ * Amp shapes with no canonical category are authored and round-tripped through
16477
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16478
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16479
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16480
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16481
+ * `amp.dangerouslyAllowAll` settings.
15931
16482
  */
15932
16483
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
15933
16484
  constructor(params) {
@@ -15997,15 +16548,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
15997
16548
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
15998
16549
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
15999
16550
  const json = fileContent ? parseAmpSettings(fileContent) : {};
16000
- const { disable, permissions } = convertRulesyncToAmp(rulesyncPermissions.getJson());
16001
- const preservedDelegates = toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16551
+ const config = rulesyncPermissions.getJson();
16552
+ const { disable, permissions } = convertRulesyncToAmp(config);
16553
+ const override = config.amp;
16554
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16555
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16002
16556
  const newJson = {
16003
16557
  ...json,
16004
16558
  [AMP_TOOLS_DISABLE_KEY]: disable
16005
16559
  };
16006
- const mergedPermissions = [...permissions, ...preservedDelegates];
16560
+ const mergedPermissions = mergeAmpPermissions([
16561
+ ...permissions,
16562
+ ...authoredExtras,
16563
+ ...preservedDelegates
16564
+ ]);
16007
16565
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16008
16566
  else delete newJson[AMP_PERMISSIONS_KEY];
16567
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16568
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16569
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16009
16570
  return new AmpPermissions({
16010
16571
  outputRoot,
16011
16572
  relativeDirPath: basePaths.relativeDirPath,
@@ -16016,11 +16577,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16016
16577
  }
16017
16578
  toRulesyncPermissions() {
16018
16579
  const json = parseAmpSettings(this.getFileContent());
16580
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16581
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16582
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16019
16583
  const config = convertAmpToRulesync({
16020
16584
  disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
16021
- permissions: toPermissionsList(json[AMP_PERMISSIONS_KEY])
16022
- });
16023
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16585
+ permissions: canonicalEntries
16586
+ });
16587
+ const ampOverride = {};
16588
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16589
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16590
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16591
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16592
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16593
+ const result = { ...config };
16594
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16595
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16024
16596
  }
16025
16597
  validate() {
16026
16598
  try {
@@ -16067,6 +16639,37 @@ const ACTION_PRIORITY = {
16067
16639
  allow: 2
16068
16640
  };
16069
16641
  /**
16642
+ * Fail-closed action priority for merging the full `amp.permissions` list
16643
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16644
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16645
+ * it never auto-allows) trails as the final fallback.
16646
+ */
16647
+ const MERGE_ACTION_PRIORITY = {
16648
+ reject: 0,
16649
+ ask: 1,
16650
+ allow: 2,
16651
+ delegate: 3
16652
+ };
16653
+ /**
16654
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16655
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16656
+ * order within an action class. Guarantees an authored `reject` cannot be
16657
+ * shadowed by a generated catch-all `allow`.
16658
+ */
16659
+ function mergeAmpPermissions(entries) {
16660
+ const decorated = entries.map((entry, index) => ({
16661
+ entry,
16662
+ index
16663
+ }));
16664
+ decorated.sort((a, b) => {
16665
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16666
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16667
+ if (ap !== bp) return ap - bp;
16668
+ return a.index - b.index;
16669
+ });
16670
+ return decorated.map((d) => d.entry);
16671
+ }
16672
+ /**
16070
16673
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16071
16674
  *
16072
16675
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16131,14 +16734,14 @@ function sortAmpPermissions(entries) {
16131
16734
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16132
16735
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16133
16736
  if (ap !== bp) return ap - bp;
16134
- const aHasCmd = a.entry.matches?.cmd !== void 0 ? 0 : 1;
16135
- const bHasCmd = b.entry.matches?.cmd !== void 0 ? 0 : 1;
16737
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16738
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16136
16739
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16137
16740
  const at = a.entry.tool;
16138
16741
  const bt = b.entry.tool;
16139
16742
  if (at !== bt) return at < bt ? -1 : 1;
16140
- const ac = a.entry.matches?.cmd ?? "";
16141
- const bc = b.entry.matches?.cmd ?? "";
16743
+ const ac = ampMatchesCmd(a.entry) ?? "";
16744
+ const bc = ampMatchesCmd(b.entry) ?? "";
16142
16745
  if (ac !== bc) return ac < bc ? -1 : 1;
16143
16746
  return a.index - b.index;
16144
16747
  });
@@ -16177,7 +16780,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16177
16780
  for (const tool of disable) assign(tool, "*", "deny");
16178
16781
  for (const entry of permissions) {
16179
16782
  if (entry.action === "delegate") continue;
16180
- const pattern = entry.matches?.cmd ?? "*";
16783
+ const pattern = ampMatchesCmd(entry) ?? "*";
16181
16784
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16182
16785
  assign(entry.tool, pattern, action);
16183
16786
  }
@@ -16259,6 +16862,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16259
16862
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16260
16863
  * file (global scope only). The file holds other CLI settings besides
16261
16864
  * permissions, so it is never deleted.
16865
+ *
16866
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16867
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16868
+ * authored and round-tripped through the `antigravity-cli` permissions override
16869
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16262
16870
  */
16263
16871
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16264
16872
  constructor(params) {
@@ -16319,6 +16927,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16319
16927
  ...settings,
16320
16928
  permissions: mergedPermissions
16321
16929
  };
16930
+ const override = config["antigravity-cli"];
16931
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16932
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16322
16933
  const fileContent = JSON.stringify(merged, null, 2);
16323
16934
  return new AntigravityCliPermissions({
16324
16935
  outputRoot,
@@ -16342,7 +16953,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16342
16953
  ask: permissions.ask ?? [],
16343
16954
  deny: permissions.deny ?? []
16344
16955
  });
16345
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16956
+ const override = {};
16957
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
16958
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
16959
+ const result = { ...config };
16960
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
16961
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16346
16962
  }
16347
16963
  validate() {
16348
16964
  return {
@@ -17128,6 +17744,14 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17128
17744
  }
17129
17745
  const config = rulesyncPermissions.getJson();
17130
17746
  const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
17747
+ const overridePermissions = config.claudecode?.permissions;
17748
+ if (overridePermissions && typeof overridePermissions === "object") {
17749
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
17750
+ settings.permissions = {
17751
+ ...settings.permissions,
17752
+ ...nonListFields
17753
+ };
17754
+ }
17131
17755
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
17132
17756
  const merged = applyPermissions({
17133
17757
  settings,
@@ -17160,6 +17784,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17160
17784
  ask: permissions.ask ?? [],
17161
17785
  deny: permissions.deny ?? []
17162
17786
  });
17787
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
17788
+ if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
17163
17789
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17164
17790
  }
17165
17791
  validate() {
@@ -17333,7 +17959,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17333
17959
  } catch (error) {
17334
17960
  throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
17335
17961
  }
17336
- const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(rulesyncPermissions.getJson().permission);
17962
+ const config = rulesyncPermissions.getJson();
17963
+ const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
17337
17964
  warnClineTranslationNotices({
17338
17965
  droppedCategories,
17339
17966
  translatedAskPatterns,
@@ -17349,7 +17976,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17349
17976
  ...existing,
17350
17977
  allow: dedupedAllow,
17351
17978
  deny: mergedDeny,
17352
- allowRedirects: existing.allowRedirects ?? false
17979
+ allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
17353
17980
  };
17354
17981
  return new ClinePermissions({
17355
17982
  outputRoot,
@@ -17373,6 +18000,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17373
18000
  for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
17374
18001
  for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
17375
18002
  const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
18003
+ if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
17376
18004
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17377
18005
  }
17378
18006
  validate() {
@@ -17809,13 +18437,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
17809
18437
  WebFetch: "webfetch",
17810
18438
  Mcp: "mcp"
17811
18439
  };
17812
- const MCP_CANONICAL_PREFIX = "mcp__";
18440
+ const MCP_CANONICAL_PREFIX$1 = "mcp__";
17813
18441
  /**
17814
18442
  * Returns true if the canonical category is the per-tool MCP form
17815
18443
  * `mcp__<server>__<tool>`.
17816
18444
  */
17817
18445
  function isMcpScopedCategory(canonical) {
17818
- return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
18446
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
17819
18447
  }
17820
18448
  function toCursorType(canonical) {
17821
18449
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -17846,7 +18474,7 @@ function toCursorPattern(canonical, pattern) {
17846
18474
  function toCanonicalCategory$1(cursorType, pattern) {
17847
18475
  if (cursorType === "Mcp") {
17848
18476
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
17849
- if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
18477
+ if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
17850
18478
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
17851
18479
  }
17852
18480
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
@@ -17980,8 +18608,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
17980
18608
  mergedPermissions.allow = mergedAllow;
17981
18609
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
17982
18610
  else delete mergedPermissions.deny;
18611
+ const cursorOverride = config.cursor;
17983
18612
  const merged = {
17984
18613
  ...settings,
18614
+ ...cursorOverride,
17985
18615
  version: settings.version ?? 1,
17986
18616
  editor: {
17987
18617
  ...existingEditor,
@@ -18007,11 +18637,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
18007
18637
  }
18008
18638
  const permissionsRaw = settings.permissions;
18009
18639
  const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
18010
- const config = convertCursorToRulesyncPermissions({
18640
+ const result = { ...convertCursorToRulesyncPermissions({
18011
18641
  allow: asCursorPermissionEntryArray(permissions.allow),
18012
18642
  deny: asCursorPermissionEntryArray(permissions.deny)
18013
- });
18014
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18643
+ }) };
18644
+ const cursorOverride = {};
18645
+ if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
18646
+ if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
18647
+ if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
18648
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18015
18649
  }
18016
18650
  validate() {
18017
18651
  return {
@@ -18070,7 +18704,7 @@ function convertCursorToRulesyncPermissions(params) {
18070
18704
  const { type, pattern } = parseCursorPermissionEntry(entry);
18071
18705
  const canonical = toCanonicalCategory$1(type, pattern);
18072
18706
  if (!permission[canonical]) permission[canonical] = {};
18073
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
18707
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
18074
18708
  permission[canonical][canonicalPattern] = action;
18075
18709
  }
18076
18710
  };
@@ -18319,6 +18953,16 @@ function convertDevinToRulesyncPermissions(params) {
18319
18953
  }
18320
18954
  //#endregion
18321
18955
  //#region src/features/permissions/factorydroid-permissions.ts
18956
+ const FACTORYDROID_OVERRIDE_KEYS = [
18957
+ "commandBlocklist",
18958
+ "networkPolicy",
18959
+ "sandbox",
18960
+ "mcpPolicy",
18961
+ "enableDroidShield",
18962
+ "sessionDefaultSettings",
18963
+ "maxAutonomyLevel",
18964
+ "interactionMode"
18965
+ ];
18322
18966
  /**
18323
18967
  * Permissions adapter for Factory Droid.
18324
18968
  *
@@ -18336,18 +18980,14 @@ function convertDevinToRulesyncPermissions(params) {
18336
18980
  * skipped (with a warning when they carry `deny` rules, to surface the gap).
18337
18981
  *
18338
18982
  * Factory Droid also has a stronger `commandBlocklist` tier — commands that can
18339
- * never run, not even under full autonomy. rulesync's canonical action model
18340
- * has only `allow | ask | deny`, with no equivalent of a hard block that can
18341
- * never be approved. So on **import** a `commandBlocklist` entry is collapsed
18342
- * onto canonical `deny` (lossy: the never-runs guarantee is weakened to a deny
18343
- * the user can still approve), rather than being silently dropped. On **export**
18344
- * there is no canonical `block` to emit one from, so rulesync never writes
18345
- * `commandBlocklist`; an existing one on disk is preserved verbatim as an
18346
- * unmanaged key. (A consequence of the lossy collapse: importing a
18347
- * `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
18348
- * `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
18349
- * reconstructed. Re-running over the original file keeps it intact via the
18350
- * verbatim preservation above.)
18983
+ * never run, not even under full autonomy plus other security controls
18984
+ * (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
18985
+ * settings) that do not fit the canonical `allow | ask | deny` per-command
18986
+ * model. These are authored and round-tripped through the `factorydroid`
18987
+ * override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
18988
+ * they are lifted from `settings.json` into the override, and on **export** they
18989
+ * are merged back in so `commandBlocklist`'s never-runs guarantee is preserved
18990
+ * faithfully rather than being collapsed onto an approvable `deny`.
18351
18991
  */
18352
18992
  var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
18353
18993
  constructor(params) {
@@ -18390,11 +19030,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18390
19030
  } catch (error) {
18391
19031
  throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
18392
19032
  }
19033
+ const config = rulesyncPermissions.getJson();
18393
19034
  const { allow, deny } = convertRulesyncToFactorydroidPermissions({
18394
- config: rulesyncPermissions.getJson(),
19035
+ config,
18395
19036
  logger
18396
19037
  });
18397
- const merged = { ...settings };
19038
+ const override = config.factorydroid;
19039
+ const merged = {
19040
+ ...settings,
19041
+ ...override !== void 0 && typeof override === "object" ? override : {}
19042
+ };
18398
19043
  const mergedAllow = uniq(allow.toSorted());
18399
19044
  const mergedDeny = uniq(deny.toSorted());
18400
19045
  if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
@@ -18418,10 +19063,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18418
19063
  }
18419
19064
  const config = convertFactorydroidToRulesyncPermissions({
18420
19065
  allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
18421
- deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : [],
18422
- block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
19066
+ deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
18423
19067
  });
18424
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19068
+ const factorydroidOverride = {};
19069
+ for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
19070
+ const result = { ...config };
19071
+ if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
19072
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18425
19073
  }
18426
19074
  validate() {
18427
19075
  return {
@@ -18468,18 +19116,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
18468
19116
  };
18469
19117
  }
18470
19118
  /**
18471
- * Convert Factory Droid allow/deny/block command lists back to rulesync config
18472
- * under the `bash` category.
19119
+ * Convert Factory Droid allow/deny command lists back to rulesync config under
19120
+ * the `bash` category.
18473
19121
  *
18474
- * `commandBlocklist` (hard block) has no canonical equivalent, so it collapses
18475
- * onto `deny` lossy (a deny can still be approved), but preferable to dropping
18476
- * the rule entirely.
19122
+ * `commandBlocklist` (the hard-block tier) is no longer collapsed here it has
19123
+ * no canonical equivalent and now round-trips through the `factorydroid`
19124
+ * override so the never-runs guarantee is preserved instead of being weakened to
19125
+ * an approvable `deny`.
18477
19126
  */
18478
- function convertFactorydroidToRulesyncPermissions({ allow, deny, block }) {
19127
+ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
18479
19128
  const bash = {};
18480
19129
  for (const pattern of allow) bash[pattern] = "allow";
18481
19130
  for (const pattern of deny) bash[pattern] = "deny";
18482
- for (const pattern of block) bash[pattern] = "deny";
18483
19131
  return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
18484
19132
  }
18485
19133
  //#endregion
@@ -18673,30 +19321,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
18673
19321
  const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
18674
19322
  const GROKCLI_UI_KEY = "ui";
18675
19323
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
19324
+ const GROKCLI_PERMISSION_KEY = "permission";
18676
19325
  const CATCH_ALL_PATTERN$2 = "*";
19326
+ const MCP_CANONICAL_PREFIX = "mcp__";
19327
+ const CATEGORY_TO_GROK_TOOL = {
19328
+ bash: "Bash",
19329
+ read: "Read",
19330
+ edit: "Edit",
19331
+ write: "Edit",
19332
+ grep: "Grep",
19333
+ webfetch: "WebFetch"
19334
+ };
19335
+ const GROK_TOOL_TO_CATEGORY = {
19336
+ Bash: "bash",
19337
+ Read: "read",
19338
+ Edit: "edit",
19339
+ Grep: "grep",
19340
+ WebFetch: "webfetch"
19341
+ };
19342
+ const GROK_MCP_TOOL = "MCPTool";
19343
+ /**
19344
+ * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
19345
+ * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
19346
+ * for categories Grok cannot express so callers can skip them.
19347
+ *
19348
+ * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
19349
+ * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
19350
+ * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
19351
+ * concrete pattern emits `Tool(pattern)`.
19352
+ */
19353
+ function buildGrokEntry(category, pattern) {
19354
+ if (category.startsWith(MCP_CANONICAL_PREFIX)) {
19355
+ const remainder = category.slice(5);
19356
+ return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
19357
+ }
19358
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
19359
+ const tool = CATEGORY_TO_GROK_TOOL[category];
19360
+ if (tool === void 0) return null;
19361
+ return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
19362
+ }
19363
+ /**
19364
+ * Parse a Grok Claude-style permission entry back into a canonical category +
19365
+ * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
19366
+ * `null` for tool prefixes with no canonical equivalent (e.g. `any`).
19367
+ */
19368
+ function parseGrokEntry(entry) {
19369
+ const trimmed = entry.trim();
19370
+ const parenIndex = trimmed.indexOf("(");
19371
+ let tool;
19372
+ let inner;
19373
+ if (parenIndex === -1 || !trimmed.endsWith(")")) {
19374
+ tool = trimmed;
19375
+ inner = "";
19376
+ } else {
19377
+ tool = trimmed.slice(0, parenIndex);
19378
+ inner = trimmed.slice(parenIndex + 1, -1).trim();
19379
+ }
19380
+ if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
19381
+ category: `${MCP_CANONICAL_PREFIX}${inner}`,
19382
+ pattern: CATCH_ALL_PATTERN$2
19383
+ } : {
19384
+ category: "mcp",
19385
+ pattern: CATCH_ALL_PATTERN$2
19386
+ };
19387
+ const category = GROK_TOOL_TO_CATEGORY[tool];
19388
+ if (category === void 0) return null;
19389
+ return {
19390
+ category,
19391
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
19392
+ };
19393
+ }
18677
19394
  /**
18678
19395
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
18679
19396
  *
18680
- * Grok has no per-tool / per-pattern permission rules. Tool gating is a single
18681
- * coarse toggle, `[ui] permission_mode`, in `~/.grok/config.toml`:
18682
- * - `ask` prompt before each tool call (Grok's default).
18683
- * - `always-approve` skip prompts.
18684
- *
18685
- * rulesync's permission model is per-category, per-pattern `allow`/`ask`/`deny`,
18686
- * so the mapping is **lossy** (a single mode cannot express per-pattern rules):
18687
- * - Generate: any `deny` or `ask` rule anywhere `ask` (conservative — keep
18688
- * prompting whenever the user expressed any restriction); otherwise, if at
18689
- * least one `allow` rule exists and nothing is denied/asked
18690
- * `always-approve`; an empty config falls back to the safe default `ask`.
18691
- * - Import: `always-approve` `bash: { "*": "allow" }`; `ask` (or unset)
18692
- * `bash: { "*": "ask" }`. `bash` is used as the representative catch-all
18693
- * because it is the primary permission-gated surface, and this round-trips
18694
- * the generate mapping.
18695
- *
18696
- * This surface is **global only** `permission_mode` lives in the user-level
18697
- * `~/.grok/config.toml`; Grok has no project-scoped permission file. The shared
18698
- * config is merged in place: the `[ui] permission_mode` value is set and every
18699
- * other key (e.g. `[mcp_servers]`, legacy `approval_mode`) is preserved. The
19397
+ * Grok Build CLI ships a Claude-style rule system under `[permission]` in
19398
+ * `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
19399
+ * `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
19400
+ * `WebFetch`, evaluated with precedence `deny > ask > allow`
19401
+ * (https://docs.x.ai/build/settings/reference). rulesync's canonical
19402
+ * per-category, per-pattern model maps almost 1:1:
19403
+ * - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
19404
+ * the matching Grok entry and is bucketed into the `[permission]` array for
19405
+ * that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
19406
+ * `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
19407
+ * `MCPTool(...)` (a scoped MCP category folds its address into the
19408
+ * parentheses, so a non-`*` argument pattern on it is not represented).
19409
+ * Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
19410
+ * `agent`) are skipped (with a warning when they carry a `deny` rule, to
19411
+ * surface the gap). When two canonical rules collapse onto the same Grok
19412
+ * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
19413
+ * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
19414
+ * entry never lands contradictorily in two arrays.
19415
+ * - Import: the `[permission]` arrays are parsed back into canonical
19416
+ * categories. When no `[permission]` section is present (older configs), the
19417
+ * coarse `[ui] permission_mode` is used as a fallback.
19418
+ *
19419
+ * The coarse `[ui] permission_mode` toggle is still written for backward
19420
+ * compatibility with older Grok versions: `always-approve` when the config is
19421
+ * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
19422
+ * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
19423
+ * arrays).
19424
+ *
19425
+ * This surface is **global only** — the adapter syncs the user-level
19426
+ * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
19427
+ * file; project scope is not modeled here.) The shared config is merged in
19428
+ * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
19429
+ * tools it models and the `[ui] permission_mode` value, while every other key
19430
+ * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
19431
+ * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
18700
19432
  * file is never deleted.
18701
19433
  */
18702
19434
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -18728,7 +19460,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18728
19460
  global: true
18729
19461
  });
18730
19462
  }
18731
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
19463
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18732
19464
  if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
18733
19465
  const paths = GrokcliPermissions.getSettablePaths({ global });
18734
19466
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -18739,7 +19471,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18739
19471
  } catch (error) {
18740
19472
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
18741
19473
  }
18742
- const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
19474
+ const config = rulesyncPermissions.getJson();
19475
+ const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19476
+ const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19477
+ parsed[GROKCLI_PERMISSION_KEY] = {
19478
+ ...existingPermission,
19479
+ allow: buckets.allow,
19480
+ deny: buckets.deny,
19481
+ ask: buckets.ask
19482
+ };
19483
+ const mode = deriveGrokPermissionMode(config);
18743
19484
  const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18744
19485
  parsed[GROKCLI_UI_KEY] = {
18745
19486
  ...existingUi,
@@ -18762,8 +19503,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18762
19503
  } catch (error) {
18763
19504
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18764
19505
  }
18765
- const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18766
- const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
19506
+ const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
19507
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
18767
19508
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18768
19509
  }
18769
19510
  validate() {
@@ -18783,6 +19524,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18783
19524
  });
18784
19525
  }
18785
19526
  };
19527
+ const ACTION_RANK = {
19528
+ allow: 0,
19529
+ ask: 1,
19530
+ deny: 2
19531
+ };
19532
+ /**
19533
+ * Collect user-authored entries from an existing `[permission]` array whose
19534
+ * tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
19535
+ * preserved verbatim so replacing the arrays does not silently drop them
19536
+ * (mirrors the Cursor adapter's preservation of unmanaged types).
19537
+ */
19538
+ function unmanagedEntries(existingPermission, key) {
19539
+ return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
19540
+ }
19541
+ /**
19542
+ * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
19543
+ * Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
19544
+ * Categories Grok cannot express are skipped (with a warning when they carry a
19545
+ * `deny` rule); entries the user authored for tools rulesync cannot model are
19546
+ * preserved from `existingPermission`.
19547
+ */
19548
+ function buildGrokPermissionArrays(config, existingPermission, logger) {
19549
+ const ranked = /* @__PURE__ */ new Map();
19550
+ for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19551
+ const entry = buildGrokEntry(category, pattern);
19552
+ if (entry === null) {
19553
+ if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
19554
+ continue;
19555
+ }
19556
+ const existing = ranked.get(entry);
19557
+ if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
19558
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
19559
+ }
19560
+ const allow = unmanagedEntries(existingPermission, "allow");
19561
+ const deny = unmanagedEntries(existingPermission, "deny");
19562
+ const ask = unmanagedEntries(existingPermission, "ask");
19563
+ for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
19564
+ else if (action === "deny") deny.push(entry);
19565
+ else ask.push(entry);
19566
+ return {
19567
+ allow: uniq(allow.toSorted()),
19568
+ deny: uniq(deny.toSorted()),
19569
+ ask: uniq(ask.toSorted())
19570
+ };
19571
+ }
19572
+ /**
19573
+ * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
19574
+ * permission map. Returns `null` when the section defines none of the three
19575
+ * arrays, so the caller can fall back to the coarse `permission_mode`.
19576
+ * Precedence `deny > ask > allow` is applied so a tool listed in multiple
19577
+ * arrays resolves to the strictest action.
19578
+ */
19579
+ function parseGrokPermissionArrays(permission) {
19580
+ const allow = isStringArray(permission.allow) ? permission.allow : void 0;
19581
+ const deny = isStringArray(permission.deny) ? permission.deny : void 0;
19582
+ const ask = isStringArray(permission.ask) ? permission.ask : void 0;
19583
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
19584
+ const result = {};
19585
+ const apply = (entries, action) => {
19586
+ for (const entry of entries ?? []) {
19587
+ const parsed = parseGrokEntry(entry);
19588
+ if (parsed === null) continue;
19589
+ const bucket = result[parsed.category] ??= {};
19590
+ bucket[parsed.pattern] = action;
19591
+ }
19592
+ };
19593
+ apply(allow, "allow");
19594
+ apply(ask, "ask");
19595
+ apply(deny, "deny");
19596
+ return result;
19597
+ }
19598
+ /**
19599
+ * Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
19600
+ * `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
19601
+ */
19602
+ function legacyModeAction(parsed) {
19603
+ return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
19604
+ }
18786
19605
  /**
18787
19606
  * Collapse a rulesync permissions config into Grok's single coarse mode.
18788
19607
  * Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
@@ -18798,6 +19617,10 @@ function deriveGrokPermissionMode(config) {
18798
19617
  }
18799
19618
  //#endregion
18800
19619
  //#region src/features/permissions/hermesagent-permissions.ts
19620
+ /** Collect the glob patterns in a canonical category that carry a given action. */
19621
+ function patternsByAction(category, action) {
19622
+ return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
19623
+ }
18801
19624
  var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
18802
19625
  static getSettablePaths() {
18803
19626
  return {
@@ -18821,10 +19644,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18821
19644
  return true;
18822
19645
  }
18823
19646
  setFileContent(fileContent) {
18824
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
19647
+ this.fileContent = applySharedConfigPatch({
19648
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19649
+ feature: "permissions",
19650
+ existingContent: fileContent,
19651
+ patch: parseSharedConfig({
19652
+ format: "yaml",
19653
+ fileContent: this.fileContent
19654
+ })
19655
+ });
18825
19656
  }
18826
19657
  toRulesyncPermissions() {
18827
- const config = parseHermesConfig(this.getFileContent());
19658
+ const config = parseSharedConfig({
19659
+ format: "yaml",
19660
+ fileContent: this.getFileContent()
19661
+ });
18828
19662
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
18829
19663
  return new RulesyncPermissions({
18830
19664
  relativeDirPath: "",
@@ -18834,11 +19668,27 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18834
19668
  }
18835
19669
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
18836
19670
  const permissions = rulesyncPermissions.getJson();
19671
+ const permissionBlock = permissions.permission ?? {};
19672
+ const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
19673
+ const bashDeny = patternsByAction(permissionBlock.bash, "deny");
19674
+ const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
19675
+ let config = {};
19676
+ if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
19677
+ if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
19678
+ if (webfetchDeny.length > 0) config.security = { website_blocklist: {
19679
+ enabled: true,
19680
+ domains: webfetchDeny
19681
+ } };
19682
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19683
+ base: config,
19684
+ patch: permissions.hermes
19685
+ });
19686
+ config.permissions = { rulesync: permissions };
18837
19687
  return new HermesagentPermissions({
18838
19688
  outputRoot,
18839
- fileContent: stringifyHermesConfig({
18840
- command_allowlist: Object.entries(permissions.permission ?? {}).flatMap(([, patterns]) => Object.entries(patterns).filter(([, action]) => action === "allow").map(([command]) => command)),
18841
- permissions: { rulesync: permissions }
19689
+ fileContent: stringifySharedConfig({
19690
+ format: "yaml",
19691
+ document: config
18842
19692
  })
18843
19693
  });
18844
19694
  }
@@ -18860,14 +19710,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18860
19710
  * "executables": [ { "prefix": "git ", "action": "allow" } ],
18861
19711
  * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18862
19712
  * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18863
- * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
19713
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
18864
19714
  * }
18865
19715
  * }
18866
19716
  * ```
18867
19717
  *
18868
19718
  * Each rule carries a literal `prefix` (matches commands that start with it) or
18869
- * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18870
- * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
19719
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
19720
+ * documents only `allow` and `ask` as valid actions there is **no `deny`**
19721
+ * (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
19722
+ * canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
19723
+ * and is mapped to the nearest valid action, `ask` (still withholds
19724
+ * auto-approval), with a warning so the downgrade is surfaced.
18871
19725
  *
18872
19726
  * Category mapping (rulesync canonical <-> Junie rule group):
18873
19727
  * - `bash` <-> `executables`
@@ -18877,8 +19731,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18877
19731
  *
18878
19732
  * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18879
19733
  * (with a warning when they carry rules). The top-level `defaultBehavior` and
18880
- * `allowReadonlyCommands` settings have no canonical equivalent: they are
18881
- * preserved verbatim on export but not imported into the rulesync model.
19734
+ * `allowReadonlyCommands` settings have no canonical per-glob slot: they are
19735
+ * authored and round-tripped through the `junie` override namespace (see
19736
+ * `JuniePermissionsOverrideSchema`) — lifted into the override on import and
19737
+ * merged back onto the top level on export — and any other unmodeled top-level
19738
+ * key is preserved verbatim.
18882
19739
  *
18883
19740
  * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18884
19741
  */
@@ -18901,7 +19758,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
18901
19758
  mcpTools: "mcp",
18902
19759
  readOutsideProject: "read"
18903
19760
  };
18904
- const JUNIE_DEFAULT_BEHAVIOR = "ask";
18905
19761
  function isPermissionAction$1(value) {
18906
19762
  return PermissionActionSchema.safeParse(value).success;
18907
19763
  }
@@ -18951,13 +19807,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18951
19807
  } catch (error) {
18952
19808
  throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18953
19809
  }
19810
+ const config = rulesyncPermissions.getJson();
18954
19811
  const rules = convertRulesyncToJunieRules({
18955
- config: rulesyncPermissions.getJson(),
19812
+ config,
18956
19813
  logger
18957
19814
  });
19815
+ const override = config.junie;
19816
+ const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
18958
19817
  const merged = {
18959
19818
  ...existing,
18960
- defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
19819
+ ...overrideObj,
18961
19820
  rules
18962
19821
  };
18963
19822
  return new JuniePermissions({
@@ -18977,7 +19836,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18977
19836
  throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18978
19837
  }
18979
19838
  const config = convertJunieToRulesyncPermissions({ allowlist });
18980
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19839
+ const junieOverride = {};
19840
+ if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
19841
+ if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
19842
+ const result = { ...config };
19843
+ if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
19844
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18981
19845
  }
18982
19846
  validate() {
18983
19847
  return {
@@ -19009,12 +19873,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
19009
19873
  continue;
19010
19874
  }
19011
19875
  for (const [pattern, action] of Object.entries(patterns)) {
19876
+ const junieAction = toJunieAction(action, category, pattern, logger);
19012
19877
  const rule = isGlobPattern(pattern) ? {
19013
19878
  pattern,
19014
- action
19879
+ action: junieAction
19015
19880
  } : {
19016
19881
  prefix: pattern,
19017
- action
19882
+ action: junieAction
19018
19883
  };
19019
19884
  (rules[group] ??= []).push(rule);
19020
19885
  }
@@ -19022,6 +19887,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
19022
19887
  return rules;
19023
19888
  }
19024
19889
  /**
19890
+ * Map a canonical action onto a valid Junie allowlist action. Junie supports
19891
+ * only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
19892
+ * nearest valid action — both withhold auto-approval) with a warning, so
19893
+ * rulesync never emits a `deny` that Junie would silently ignore.
19894
+ */
19895
+ function toJunieAction(action, category, pattern, logger) {
19896
+ if (action === "deny") {
19897
+ logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
19898
+ return "ask";
19899
+ }
19900
+ return action;
19901
+ }
19902
+ /**
19025
19903
  * Convert a Junie allowlist back into rulesync permissions config. The
19026
19904
  * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
19027
19905
  * canonical equivalent and are not imported.
@@ -19055,6 +19933,31 @@ const KiloPermissionSchema = z.union([z.enum([
19055
19933
  ]))]);
19056
19934
  const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
19057
19935
  /**
19936
+ * Kilo permission keys that share a name with a canonical rulesync category and
19937
+ * therefore stay in the shared `permission` block. Everything else (Kilo-only
19938
+ * keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
19939
+ * routed into the `kilo` override on import so it does not leak into other
19940
+ * tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
19941
+ * rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
19942
+ * addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
19943
+ * keys in practice — they simply never appear on the shared side.
19944
+ */
19945
+ const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
19946
+ "bash",
19947
+ "read",
19948
+ "edit",
19949
+ "write",
19950
+ "webfetch",
19951
+ "websearch",
19952
+ "grep",
19953
+ "glob",
19954
+ "notebookedit",
19955
+ "agent"
19956
+ ]);
19957
+ function isSharedKiloCategory(key) {
19958
+ return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
19959
+ }
19960
+ /**
19058
19961
  * Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
19059
19962
  * non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
19060
19963
  * coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
@@ -19166,12 +20069,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19166
20069
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
19167
20070
  const parsedPermission = parsed.permission;
19168
20071
  const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
19169
- const rulesyncPermission = rulesyncPermissions.getJson().permission;
20072
+ const rulesyncJson = rulesyncPermissions.getJson();
20073
+ const kiloOverride = rulesyncJson.kilo;
20074
+ const incomingPermission = {
20075
+ ...rulesyncJson.permission,
20076
+ ...kiloOverride?.permission
20077
+ };
19170
20078
  const droppedDenyByKey = {};
19171
- for (const key of Object.keys(rulesyncPermission)) {
19172
- const previous = existingPermission[key];
19173
- const previousDenyPatterns = collectKiloDenyPatterns(previous);
19174
- const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
20079
+ for (const [key, value] of Object.entries(incomingPermission)) {
20080
+ const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
20081
+ const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
19175
20082
  const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
19176
20083
  if (dropped.length > 0) droppedDenyByKey[key] = dropped;
19177
20084
  }
@@ -19179,8 +20086,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19179
20086
  const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
19180
20087
  logger?.warn(`WARNING: Kilo permissions regeneration drops existing 'deny' rule(s) because rulesync output owns these tool keys. Dropped — ${summary}. To preserve these denies, add them to '.rulesync/permissions.json'.`);
19181
20088
  }
19182
- const mergedPermission = { ...existingPermission };
19183
- for (const [key, value] of Object.entries(rulesyncPermission)) mergedPermission[key] = value;
20089
+ const mergedPermission = {
20090
+ ...existingPermission,
20091
+ ...incomingPermission
20092
+ };
19184
20093
  const nextJson = {
19185
20094
  ...parsed,
19186
20095
  permission: mergedPermission
@@ -19194,8 +20103,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19194
20103
  });
19195
20104
  }
19196
20105
  toRulesyncPermissions() {
19197
- const permission = this.normalizePermission(this.json.permission);
19198
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20106
+ const rawPermission = this.json.permission ?? {};
20107
+ const shared = {};
20108
+ const overrideOnly = {};
20109
+ for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
20110
+ else overrideOnly[key] = value;
20111
+ const json = Object.keys(overrideOnly).length > 0 ? {
20112
+ permission: shared,
20113
+ kilo: { permission: overrideOnly }
20114
+ } : { permission: shared };
20115
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19199
20116
  }
19200
20117
  validate() {
19201
20118
  try {
@@ -19225,10 +20142,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19225
20142
  validate: false
19226
20143
  });
19227
20144
  }
19228
- normalizePermission(permission) {
19229
- if (!permission) return {};
19230
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
19231
- }
19232
20145
  };
19233
20146
  //#endregion
19234
20147
  //#region src/features/permissions/kiro-permissions.ts
@@ -19286,29 +20199,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19286
20199
  }
19287
20200
  const permission = {};
19288
20201
  const toolsSettings = parsed.toolsSettings ?? {};
19289
- const shellSettings = asRecord$1(toolsSettings.shell);
19290
- const shellAllow = asStringArray(shellSettings.allowedCommands);
19291
- const shellDeny = asStringArray(shellSettings.deniedCommands);
19292
- if (shellAllow.length > 0 || shellDeny.length > 0) {
19293
- permission.bash = {};
19294
- for (const pattern of shellAllow) permission.bash[pattern] = "allow";
19295
- for (const pattern of shellDeny) permission.bash[pattern] = "deny";
19296
- }
19297
- const readSettings = asRecord$1(toolsSettings.read);
19298
- const readAllow = asStringArray(readSettings.allowedPaths);
19299
- const readDeny = asStringArray(readSettings.deniedPaths);
19300
- if (readAllow.length > 0 || readDeny.length > 0) {
19301
- permission.read = {};
19302
- for (const pattern of readAllow) permission.read[pattern] = "allow";
19303
- for (const pattern of readDeny) permission.read[pattern] = "deny";
19304
- }
19305
- const writeSettings = asRecord$1(toolsSettings.write);
19306
- const writeAllow = asStringArray(writeSettings.allowedPaths);
19307
- const writeDeny = asStringArray(writeSettings.deniedPaths);
19308
- if (writeAllow.length > 0 || writeDeny.length > 0) {
19309
- permission.write = {};
19310
- for (const pattern of writeAllow) permission.write[pattern] = "allow";
19311
- for (const pattern of writeDeny) permission.write[pattern] = "deny";
20202
+ const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
20203
+ if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
20204
+ for (const category of [
20205
+ "read",
20206
+ "write",
20207
+ "grep",
20208
+ "glob"
20209
+ ]) {
20210
+ const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
20211
+ if (Object.keys(rules).length > 0) permission[category] = rules;
19312
20212
  }
19313
20213
  const allowedTools = new Set(parsed.allowedTools ?? []);
19314
20214
  if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
@@ -19334,45 +20234,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19334
20234
  function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
19335
20235
  const nextAllowedTools = new Set(existing.allowedTools ?? []);
19336
20236
  const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
20237
+ const pathBuckets = {};
20238
+ const pushPath = (key, action, pattern) => {
20239
+ const bucket = pathBuckets[key] ??= {
20240
+ allow: [],
20241
+ deny: []
20242
+ };
20243
+ (action === "allow" ? bucket.allow : bucket.deny).push(pattern);
20244
+ };
19337
20245
  const shell = {
19338
20246
  allowedCommands: [],
19339
20247
  deniedCommands: []
19340
20248
  };
19341
- const read = {
19342
- allowedPaths: [],
19343
- deniedPaths: []
19344
- };
19345
- const write = {
19346
- allowedPaths: [],
19347
- deniedPaths: []
19348
- };
19349
20249
  for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19350
20250
  if (action === "ask") {
19351
20251
  logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
19352
20252
  continue;
19353
20253
  }
19354
20254
  if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
19355
- else if (category === "read") (action === "allow" ? read.allowedPaths : read.deniedPaths).push(pattern);
19356
- else if (category === "edit" || category === "write") (action === "allow" ? write.allowedPaths : write.deniedPaths).push(pattern);
19357
- else if (category === "webfetch" || category === "websearch") {
19358
- if (pattern !== "*") {
19359
- logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
19360
- continue;
19361
- }
19362
- const toolName = category === "webfetch" ? "web_fetch" : "web_search";
19363
- if (action === "allow") nextAllowedTools.add(toolName);
19364
- else nextAllowedTools.delete(toolName);
19365
- } else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
20255
+ else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
20256
+ else if (category === "edit" || category === "write") pushPath("write", action, pattern);
20257
+ else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
20258
+ category,
20259
+ pattern,
20260
+ action,
20261
+ nextAllowedTools,
20262
+ logger
20263
+ });
20264
+ else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
19366
20265
  }
19367
20266
  nextToolsSettings.shell = shell;
19368
- nextToolsSettings.read = read;
19369
- nextToolsSettings.write = write;
20267
+ nextToolsSettings.read = pathTable(pathBuckets.read);
20268
+ nextToolsSettings.write = pathTable(pathBuckets.write);
20269
+ for (const key of ["grep", "glob"]) {
20270
+ const bucket = pathBuckets[key];
20271
+ if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
20272
+ }
19370
20273
  return {
19371
20274
  ...existing,
19372
20275
  allowedTools: [...nextAllowedTools].toSorted(),
19373
20276
  toolsSettings: nextToolsSettings
19374
20277
  };
19375
20278
  }
20279
+ function pathTable(bucket) {
20280
+ return {
20281
+ allowedPaths: bucket?.allow ?? [],
20282
+ deniedPaths: bucket?.deny ?? []
20283
+ };
20284
+ }
20285
+ function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
20286
+ if (pattern !== "*") {
20287
+ logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
20288
+ return;
20289
+ }
20290
+ const toolName = category === "webfetch" ? "web_fetch" : "web_search";
20291
+ if (action === "allow") nextAllowedTools.add(toolName);
20292
+ else nextAllowedTools.delete(toolName);
20293
+ }
19376
20294
  function asRecord$1(value) {
19377
20295
  const result = UnknownRecordSchema.safeParse(value);
19378
20296
  return result.success ? result.data : {};
@@ -19380,6 +20298,17 @@ function asRecord$1(value) {
19380
20298
  function asStringArray(value) {
19381
20299
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19382
20300
  }
20301
+ /**
20302
+ * Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
20303
+ * allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
20304
+ * `allowedCommands`/`deniedCommands`).
20305
+ */
20306
+ function rulesFromArrays(settings, allowKey, denyKey) {
20307
+ const rules = {};
20308
+ for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
20309
+ for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
20310
+ return rules;
20311
+ }
19383
20312
  //#endregion
19384
20313
  //#region src/features/permissions/opencode-permissions.ts
19385
20314
  const OpencodePermissionSchema = z.union([z.enum([
@@ -19391,6 +20320,28 @@ const OpencodePermissionSchema = z.union([z.enum([
19391
20320
  "ask",
19392
20321
  "deny"
19393
20322
  ]))]);
20323
+ /**
20324
+ * Canonical rulesync permission categories that carry a cross-tool meaning (see
20325
+ * the "Supported tool categories" list in `docs/reference/file-formats.md`).
20326
+ * On import, any OpenCode category outside this set — plus MCP tool names — is
20327
+ * treated as OpenCode-only and routed into the `opencode` override block so a
20328
+ * subsequent `rulesync generate` does not leak it into other tools' configs.
20329
+ */
20330
+ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
20331
+ "bash",
20332
+ "read",
20333
+ "edit",
20334
+ "write",
20335
+ "webfetch",
20336
+ "websearch",
20337
+ "grep",
20338
+ "glob",
20339
+ "notebookedit",
20340
+ "agent"
20341
+ ]);
20342
+ function isSharedPermissionCategory(category) {
20343
+ return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
20344
+ }
19394
20345
  const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
19395
20346
  "allow",
19396
20347
  "ask",
@@ -19452,9 +20403,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19452
20403
  fileContent = await readFileContentOrNull(jsonPath);
19453
20404
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
19454
20405
  }
20406
+ const parsed = parse(fileContent ?? "{}");
20407
+ const rulesyncJson = rulesyncPermissions.getJson();
20408
+ const overridePermission = rulesyncJson.opencode?.permission ?? {};
19455
20409
  const nextJson = {
19456
- ...parse(fileContent ?? "{}"),
19457
- permission: rulesyncPermissions.getJson().permission
20410
+ ...parsed,
20411
+ permission: {
20412
+ ...rulesyncJson.permission,
20413
+ ...overridePermission
20414
+ }
19458
20415
  };
19459
20416
  return new OpencodePermissions({
19460
20417
  outputRoot,
@@ -19465,8 +20422,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19465
20422
  });
19466
20423
  }
19467
20424
  toRulesyncPermissions() {
19468
- const permission = this.normalizePermission(this.json.permission);
19469
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20425
+ const rawPermission = this.json.permission;
20426
+ if (rawPermission === void 0 || typeof rawPermission === "string") {
20427
+ const permission = this.normalizePermission(rawPermission);
20428
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20429
+ }
20430
+ const shared = {};
20431
+ const overrideOnly = {};
20432
+ for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
20433
+ else overrideOnly[category] = value;
20434
+ const json = Object.keys(overrideOnly).length > 0 ? {
20435
+ permission: shared,
20436
+ opencode: { permission: overrideOnly }
20437
+ } : { permission: shared };
20438
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19470
20439
  }
19471
20440
  validate() {
19472
20441
  try {
@@ -19496,10 +20465,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19496
20465
  validate: false
19497
20466
  });
19498
20467
  }
20468
+ /**
20469
+ * Normalize the uniform/undefined forms of OpenCode's `permission` field into
20470
+ * the canonical rulesync shape. The object form is handled directly in
20471
+ * `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
20472
+ * categories), so this only covers the two remaining cases.
20473
+ */
19499
20474
  normalizePermission(permission) {
19500
20475
  if (!permission) return {};
19501
- if (typeof permission === "string") return { "*": { "*": permission } };
19502
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
20476
+ return { "*": { "*": permission } };
19503
20477
  }
19504
20478
  };
19505
20479
  //#endregion
@@ -19573,6 +20547,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
19573
20547
  if (pattern === "*") return toolName;
19574
20548
  return `${toolName}(${pattern})`;
19575
20549
  }
20550
+ const QWEN_OVERRIDE_TOOLS_KEYS = [
20551
+ "approvalMode",
20552
+ "autoAccept",
20553
+ "sandbox",
20554
+ "sandboxImage",
20555
+ "disabled"
20556
+ ];
20557
+ const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
20558
+ function asPlainRecord(value) {
20559
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20560
+ }
20561
+ /** Pick the override-managed keys out of a settings group into a fresh record. */
20562
+ function pickQwenOverrideKeys(group, keys) {
20563
+ const source = asPlainRecord(group);
20564
+ const picked = {};
20565
+ for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
20566
+ return picked;
20567
+ }
19576
20568
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19577
20569
  constructor(params) {
19578
20570
  super({
@@ -19638,6 +20630,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19638
20630
  ...settings,
19639
20631
  permissions: mergedPermissions
19640
20632
  };
20633
+ const override = config.qwencode;
20634
+ if (override?.tools !== void 0) merged.tools = {
20635
+ ...asPlainRecord(settings.tools),
20636
+ ...asPlainRecord(override.tools)
20637
+ };
20638
+ if (override?.security !== void 0) merged.security = {
20639
+ ...asPlainRecord(settings.security),
20640
+ ...asPlainRecord(override.security)
20641
+ };
19641
20642
  const fileContent = JSON.stringify(merged, null, 2);
19642
20643
  return new QwencodePermissions({
19643
20644
  outputRoot,
@@ -19663,7 +20664,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19663
20664
  ask: permissions.ask ?? [],
19664
20665
  deny: permissions.deny ?? []
19665
20666
  });
19666
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20667
+ const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
20668
+ const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
20669
+ const qwencodeOverride = {};
20670
+ if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
20671
+ if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
20672
+ const result = { ...config };
20673
+ if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
20674
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19667
20675
  }
19668
20676
  validate() {
19669
20677
  try {
@@ -19824,6 +20832,16 @@ function toPermissionsTable(value) {
19824
20832
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19825
20833
  return { ...value };
19826
20834
  }
20835
+ const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
20836
+ function asReasonixRecord(value) {
20837
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20838
+ }
20839
+ function pickReasonixKeys(source, keys) {
20840
+ const record = asReasonixRecord(source);
20841
+ const picked = {};
20842
+ for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
20843
+ return picked;
20844
+ }
19827
20845
  var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19828
20846
  toml;
19829
20847
  constructor(params) {
@@ -19885,6 +20903,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19885
20903
  ...parsed,
19886
20904
  permissions: mergedPermissions
19887
20905
  };
20906
+ const override = config.reasonix;
20907
+ if (override?.sandbox !== void 0) merged.sandbox = {
20908
+ ...asReasonixRecord(parsed.sandbox),
20909
+ ...asReasonixRecord(override.sandbox)
20910
+ };
20911
+ if (override?.agent !== void 0) merged.agent = {
20912
+ ...asReasonixRecord(parsed.agent),
20913
+ ...asReasonixRecord(override.agent)
20914
+ };
19888
20915
  const fileContent = smolToml.stringify(merged);
19889
20916
  return new ReasonixPermissions({
19890
20917
  outputRoot,
@@ -19901,7 +20928,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19901
20928
  ask: toStringArray$1(permissions.ask),
19902
20929
  deny: toStringArray$1(permissions.deny)
19903
20930
  });
19904
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20931
+ const sandbox = asReasonixRecord(this.toml.sandbox);
20932
+ const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
20933
+ const reasonixOverride = {};
20934
+ if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
20935
+ if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
20936
+ const result = { ...config };
20937
+ if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
20938
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19905
20939
  }
19906
20940
  validate() {
19907
20941
  try {
@@ -20211,6 +21245,8 @@ function isPermissionAction(value) {
20211
21245
  const TAKT_PROVIDER_KEY = "provider";
20212
21246
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20213
21247
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21248
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21249
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20214
21250
  const TAKT_DEFAULT_PROVIDER = "claude";
20215
21251
  const CATCH_ALL_PATTERN = "*";
20216
21252
  /**
@@ -20240,10 +21276,16 @@ const CATCH_ALL_PATTERN = "*";
20240
21276
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20241
21277
  *
20242
21278
  * Both project and global scope are supported. The shared config is merged in
20243
- * place: only `provider_profiles.<provider>.default_permission_mode` is set;
20244
- * the active provider's other keys (e.g. `step_permission_overrides`), every
20245
- * other provider profile, and all other top-level keys are preserved. The file
20246
- * is never deleted.
21279
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21280
+ * derived mode; every other provider profile and all other top-level keys are
21281
+ * preserved. The file is never deleted.
21282
+ *
21283
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21284
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21285
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21286
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21287
+ * (a top-level per-provider sandbox/network table). Both are authored on
21288
+ * generate and re-extracted on import.
20247
21289
  */
20248
21290
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20249
21291
  constructor(params) {
@@ -20275,37 +21317,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20275
21317
  }
20276
21318
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20277
21319
  const paths = TaktPermissions.getSettablePaths({ global });
20278
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21320
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21321
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21322
+ const config = parseSharedConfig({
21323
+ format: "yaml",
21324
+ fileContent: existingContent,
21325
+ filePath,
21326
+ invalidRootPolicy: "error"
21327
+ });
21328
+ const rulesyncJson = rulesyncPermissions.getJson();
20279
21329
  const provider = resolveActiveProvider(config);
20280
- const mode = deriveTaktPermissionMode(rulesyncPermissions.getJson());
20281
- const existingProfiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20282
- const existingProfile = isPlainObject(existingProfiles[provider]) ? existingProfiles[provider] : {};
20283
- const merged = {
20284
- ...config,
20285
- [TAKT_PROVIDER_PROFILES_KEY]: {
20286
- ...existingProfiles,
20287
- [provider]: {
20288
- ...existingProfile,
20289
- [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode
20290
- }
20291
- }
21330
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21331
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21332
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21333
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21334
+ const patch = {
21335
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21336
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21337
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21338
+ } },
21339
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
20292
21340
  };
20293
21341
  return new TaktPermissions({
20294
21342
  outputRoot,
20295
21343
  relativeDirPath: paths.relativeDirPath,
20296
21344
  relativeFilePath: paths.relativeFilePath,
20297
- fileContent: dump(merged),
21345
+ fileContent: applySharedConfigPatch({
21346
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21347
+ feature: "permissions",
21348
+ existingContent,
21349
+ patch,
21350
+ filePath
21351
+ }),
20298
21352
  validate: true,
20299
21353
  global
20300
21354
  });
20301
21355
  }
20302
21356
  toRulesyncPermissions() {
20303
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21357
+ const config = parseSharedConfig({
21358
+ format: "yaml",
21359
+ fileContent: this.getFileContent(),
21360
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
21361
+ invalidRootPolicy: "error"
21362
+ });
20304
21363
  const provider = resolveActiveProvider(config);
20305
21364
  const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20306
- const mode = (isPlainObject(profiles[provider]) ? profiles[provider] : {})[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21365
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21366
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
20307
21367
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
20308
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21368
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21369
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21370
+ const taktOverride = {};
21371
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21372
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21373
+ const result = { ...rulesyncConfig };
21374
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21375
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20309
21376
  }
20310
21377
  validate() {
20311
21378
  return {
@@ -20465,6 +21532,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20465
21532
  if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
20466
21533
  tools[vibeToolName] = nextTool;
20467
21534
  }
21535
+ applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
20468
21536
  config.tools = tools;
20469
21537
  if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
20470
21538
  else delete config.enabled_tools;
@@ -20481,16 +21549,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20481
21549
  }
20482
21550
  toRulesyncPermissions() {
20483
21551
  const permission = {};
21552
+ const vibeOverridePermission = {};
20484
21553
  for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
20485
21554
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
20486
21555
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
20487
- const rules = ensurePermission(permission, toCanonicalToolName$1(vibeToolName));
21556
+ const category = toCanonicalToolName$1(vibeToolName);
21557
+ const rules = ensurePermission(permission, category);
20488
21558
  const action = fromVibePermission(toolConfig.permission);
20489
21559
  if (action !== void 0) rules["*"] = action;
20490
21560
  for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
20491
21561
  for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
21562
+ const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
21563
+ if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
20492
21564
  }
20493
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
21565
+ for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
21566
+ const json = Object.keys(vibeOverridePermission).length > 0 ? {
21567
+ permission,
21568
+ vibe: { permission: vibeOverridePermission }
21569
+ } : { permission };
21570
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
20494
21571
  }
20495
21572
  validate() {
20496
21573
  try {
@@ -20517,6 +21594,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20517
21594
  });
20518
21595
  }
20519
21596
  };
21597
+ /**
21598
+ * Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
21599
+ * escalate to ASK even when the base permission is ALWAYS). rulesync owns this
21600
+ * list for any category the override names: a present list is set, an empty
21601
+ * one clears it. Categories not named keep whatever the existing file had.
21602
+ */
21603
+ function applyVibeSensitivePatterns(tools, vibeOverride) {
21604
+ for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
21605
+ const vibeToolName = toVibeToolName(category);
21606
+ const nextTool = toVibeToolConfig(tools[vibeToolName]);
21607
+ const patterns = toStringArray(toolOverride.sensitive_patterns);
21608
+ if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
21609
+ else delete nextTool.sensitive_patterns;
21610
+ tools[vibeToolName] = nextTool;
21611
+ }
21612
+ }
20520
21613
  function parseVibeConfig(fileContent) {
20521
21614
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
20522
21615
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -20561,6 +21654,11 @@ function ensurePermission(permission, category) {
20561
21654
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
20562
21655
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
20563
21656
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
21657
+ const WARP_OVERRIDE_KEYS = [
21658
+ "agent_mode_coding_permissions",
21659
+ "agent_mode_coding_file_read_allowlist",
21660
+ "agent_mode_execute_readonly_commands"
21661
+ ];
20564
21662
  /**
20565
21663
  * Warp's `settings.toml` lives in a different directory per platform (Stable
20566
21664
  * channel). The home directory is resolved by the processor through
@@ -20596,8 +21694,16 @@ function warpSettingsDir() {
20596
21694
  * patterns as regexes when targeting Warp (mirrors the Zed permissions
20597
21695
  * adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
20598
21696
  * the command lists only model shell commands, so non-`bash` categories are
20599
- * skipped (with a warning when they carry `deny` rules). MCP allow/deny and the
20600
- * file-read permissions are separate surfaces not modeled here.
21697
+ * skipped (with a warning when they carry `deny` rules).
21698
+ *
21699
+ * Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
21700
+ * knobs that do not fit the canonical `allow | ask | deny` per-command model:
21701
+ * `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
21702
+ * `agent_mode_execute_readonly_commands`. These are authored and round-tripped
21703
+ * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
21704
+ * on **import** they are lifted from `settings.toml` into the override, and on
21705
+ * **export** they are merged back into `[agents.profiles]` (the override wins).
21706
+ * MCP allow/deny is a separate surface not modeled here.
20601
21707
  *
20602
21708
  * The `settings.toml` file holds all of Warp's settings, so the
20603
21709
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -20642,12 +21748,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20642
21748
  } catch (error) {
20643
21749
  throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
20644
21750
  }
21751
+ const config = rulesyncPermissions.getJson();
20645
21752
  const { allow, deny } = convertRulesyncToWarpPermissions({
20646
- config: rulesyncPermissions.getJson(),
21753
+ config,
20647
21754
  logger
20648
21755
  });
20649
21756
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20650
21757
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
21758
+ const override = config.warp;
21759
+ if (isRecord(override)) Object.assign(profiles, override);
20651
21760
  const mergedAllow = uniq(allow.toSorted());
20652
21761
  const mergedDeny = uniq(deny.toSorted());
20653
21762
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -20678,7 +21787,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20678
21787
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
20679
21788
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
20680
21789
  });
20681
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
21790
+ const warpOverride = {};
21791
+ for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
21792
+ const result = { ...config };
21793
+ if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
21794
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20682
21795
  }
20683
21796
  validate() {
20684
21797
  return {
@@ -24044,8 +25157,9 @@ const DevinSkillFrontmatterSchema = z.looseObject({
24044
25157
  * Represents a Devin (now Devin Desktop) skill directory.
24045
25158
  * Devin supports skills in both project mode under .devin/skills/
24046
25159
  * (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
24047
- * fallback the tool still reads) and global mode under ~/.codeium/windsurf/skills/
24048
- * (unchanged by the rebrand).
25160
+ * fallback the tool still reads) and global mode under the Devin-native
25161
+ * ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
25162
+ * the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
24049
25163
  */
24050
25164
  var DevinSkill = class DevinSkill extends ToolSkill {
24051
25165
  constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -24067,7 +25181,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
24067
25181
  }
24068
25182
  }
24069
25183
  static getSettablePaths({ global = false } = {}) {
24070
- if (global) return { relativeDirPath: CODEIUM_WINDSURF_SKILLS_DIR_PATH };
25184
+ if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
24071
25185
  return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
24072
25186
  }
24073
25187
  getFrontmatter() {
@@ -27126,7 +28240,7 @@ const QwencodeSubagentFrontmatterSchema = z.looseObject({
27126
28240
  disallowedTools: z.optional(z.array(z.string())),
27127
28241
  maxTurns: z.optional(z.number()),
27128
28242
  color: z.optional(z.string()),
27129
- mcpServers: z.optional(z.array(z.string())),
28243
+ mcpServers: z.optional(z.union([z.array(z.string()), z.record(z.string(), z.looseObject({}))])),
27130
28244
  hooks: z.optional(z.unknown())
27131
28245
  });
27132
28246
  var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
@@ -29153,14 +30267,21 @@ def register(ctx):
29153
30267
  `;
29154
30268
  }
29155
30269
  function getEnabledPluginConfigContent(currentContent) {
29156
- const config = parseHermesConfig(currentContent);
30270
+ const config = parseSharedConfig({
30271
+ format: "yaml",
30272
+ fileContent: currentContent
30273
+ });
29157
30274
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29158
30275
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
29159
- config.plugins = {
29160
- ...plugins,
29161
- enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
29162
- };
29163
- return stringifyHermesConfig(config);
30276
+ return applySharedConfigPatch({
30277
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30278
+ feature: "subagents",
30279
+ existingContent: currentContent,
30280
+ patch: { plugins: {
30281
+ ...plugins,
30282
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30283
+ } }
30284
+ });
29164
30285
  }
29165
30286
  function getSubagentSpec(rulesyncSubagent) {
29166
30287
  const json = rulesyncSubagent.getFrontmatter();
@@ -29246,6 +30367,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
29246
30367
  static getSettablePaths() {
29247
30368
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
29248
30369
  }
30370
+ /**
30371
+ * Beyond the subagent spec files, generation also read-modify-writes the
30372
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30373
+ * so the write must be declared for the shared-file order derivation.
30374
+ */
30375
+ static getExtraSharedWritePaths() {
30376
+ return [{
30377
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30378
+ relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
30379
+ }];
30380
+ }
29249
30381
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
29250
30382
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
29251
30383
  }
@@ -30984,7 +32116,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
30984
32116
  })),
30985
32117
  kiro: z.optional(z.looseObject({
30986
32118
  inclusion: z.optional(z.string()),
30987
- fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())]))
32119
+ fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())])),
32120
+ name: z.optional(z.string()),
32121
+ description: z.optional(z.string())
30988
32122
  })),
30989
32123
  takt: z.optional(z.looseObject({
30990
32124
  name: z.optional(z.string()),
@@ -33900,7 +35034,8 @@ function toFileMatchPattern(globs) {
33900
35034
  *
33901
35035
  * Precedence:
33902
35036
  * 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
33903
- * taken from the block, or derived from `globs` when omitted for `fileMatch`).
35037
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35038
+ * and with the companion `name`/`description` carried through for `auto`).
33904
35039
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
33905
35040
  * to matching files instead of leaving it implicitly always-on.
33906
35041
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -33918,6 +35053,11 @@ function deriveKiroInclusion({ kiro, globs }) {
33918
35053
  fileMatchPattern
33919
35054
  } : { inclusion: "fileMatch" };
33920
35055
  }
35056
+ if (kiro.inclusion === "auto") return {
35057
+ inclusion: "auto",
35058
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35059
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35060
+ };
33921
35061
  return { inclusion: kiro.inclusion };
33922
35062
  }
33923
35063
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -33989,6 +35129,8 @@ var KiroRule = class KiroRule extends ToolRule {
33989
35129
  const rawPattern = frontmatter.fileMatchPattern;
33990
35130
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
33991
35131
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35132
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35133
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
33992
35134
  return new RulesyncRule({
33993
35135
  outputRoot: process.cwd(),
33994
35136
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -33999,7 +35141,9 @@ var KiroRule = class KiroRule extends ToolRule {
33999
35141
  globs,
34000
35142
  kiro: {
34001
35143
  inclusion,
34002
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35144
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35145
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35146
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34003
35147
  }
34004
35148
  },
34005
35149
  body
@@ -36173,6 +37317,197 @@ function buildPermissionsStrategy(ctx) {
36173
37317
  };
36174
37318
  }
36175
37319
  //#endregion
37320
+ //#region src/types/processor-registry.ts
37321
+ const PROCESSOR_REGISTRY = [
37322
+ {
37323
+ feature: "rules",
37324
+ processor: RulesProcessor,
37325
+ schema: RulesProcessorToolTargetSchema,
37326
+ factory: toolRuleFactories
37327
+ },
37328
+ {
37329
+ feature: "ignore",
37330
+ processor: IgnoreProcessor,
37331
+ schema: IgnoreProcessorToolTargetSchema,
37332
+ factory: toolIgnoreFactories
37333
+ },
37334
+ {
37335
+ feature: "mcp",
37336
+ processor: McpProcessor,
37337
+ schema: McpProcessorToolTargetSchema,
37338
+ factory: toolMcpFactories
37339
+ },
37340
+ {
37341
+ feature: "commands",
37342
+ processor: CommandsProcessor,
37343
+ schema: CommandsProcessorToolTargetSchema,
37344
+ factory: toolCommandFactories
37345
+ },
37346
+ {
37347
+ feature: "subagents",
37348
+ processor: SubagentsProcessor,
37349
+ schema: SubagentsProcessorToolTargetSchema,
37350
+ factory: toolSubagentFactories
37351
+ },
37352
+ {
37353
+ feature: "skills",
37354
+ processor: SkillsProcessor,
37355
+ schema: SkillsProcessorToolTargetSchema,
37356
+ factory: toolSkillFactories
37357
+ },
37358
+ {
37359
+ feature: "hooks",
37360
+ processor: HooksProcessor,
37361
+ schema: HooksProcessorToolTargetSchema,
37362
+ factory: toolHooksFactories
37363
+ },
37364
+ {
37365
+ feature: "permissions",
37366
+ processor: PermissionsProcessor,
37367
+ schema: PermissionsProcessorToolTargetSchema,
37368
+ factory: toolPermissionsFactories
37369
+ }
37370
+ ];
37371
+ const getProcessorRegistryEntry = (feature) => {
37372
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37373
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37374
+ return entry;
37375
+ };
37376
+ //#endregion
37377
+ //#region src/lib/shared-file-derive.ts
37378
+ /**
37379
+ * The single declaration of the cross-feature write order for shared
37380
+ * (read-modify-write) config files: when two features write the same on-disk
37381
+ * file, the earlier one writes first and the later one merges on top, so the
37382
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37383
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37384
+ * The generation step graph's `dependsOn` edges are derived from this list
37385
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37386
+ * shared path never requires touching the graph by hand.
37387
+ */
37388
+ const SHARED_WRITE_FEATURE_ORDER = [
37389
+ "ignore",
37390
+ "subagents",
37391
+ "mcp",
37392
+ "hooks",
37393
+ "permissions",
37394
+ "rules"
37395
+ ];
37396
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37397
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37398
+ const sharedFileKey = (path) => {
37399
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37400
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37401
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37402
+ };
37403
+ const settablePathsForScope = (cls, global) => {
37404
+ const paths = [];
37405
+ let settable;
37406
+ try {
37407
+ settable = cls.getSettablePaths?.({ global });
37408
+ } catch {
37409
+ return paths;
37410
+ }
37411
+ if (settable?.relativeFilePath) paths.push({
37412
+ relativeDirPath: settable.relativeDirPath ?? ".",
37413
+ relativeFilePath: settable.relativeFilePath
37414
+ });
37415
+ if (settable?.root) {
37416
+ paths.push(settable.root);
37417
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37418
+ }
37419
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37420
+ return paths;
37421
+ };
37422
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37423
+ /**
37424
+ * Derive, from the processor registry, every on-disk file that two or more
37425
+ * features read-modify-write. Source of truth the generation step graph's
37426
+ * `writesSharedFile` declarations must match.
37427
+ */
37428
+ const deriveSharedFileWriters = () => {
37429
+ const byKey = /* @__PURE__ */ new Map();
37430
+ const pathByKey = /* @__PURE__ */ new Map();
37431
+ for (const entry of PROCESSOR_REGISTRY) {
37432
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37433
+ const factories = entry.factory;
37434
+ for (const [tool, factory] of factories) {
37435
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37436
+ for (const path of collectFactoryPaths(factory)) {
37437
+ const key = sharedFileKey(path);
37438
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37439
+ let features = byKey.get(key);
37440
+ if (!features) {
37441
+ features = /* @__PURE__ */ new Map();
37442
+ byKey.set(key, features);
37443
+ }
37444
+ let tools = features.get(entry.feature);
37445
+ if (!tools) {
37446
+ tools = /* @__PURE__ */ new Set();
37447
+ features.set(entry.feature, tools);
37448
+ }
37449
+ tools.add(tool);
37450
+ }
37451
+ }
37452
+ }
37453
+ const writers = [];
37454
+ for (const [key, features] of byKey) {
37455
+ if (features.size < 2) continue;
37456
+ const path = pathByKey.get(key);
37457
+ const toolsByFeature = /* @__PURE__ */ new Map();
37458
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37459
+ writers.push({
37460
+ key,
37461
+ relativeDirPath: path.relativeDirPath,
37462
+ relativeFilePath: path.relativeFilePath,
37463
+ features: [...features.keys()].toSorted(),
37464
+ toolsByFeature
37465
+ });
37466
+ }
37467
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37468
+ };
37469
+ /**
37470
+ * Derive, per shared-write feature, the shared files it writes and the
37471
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37472
+ * writer depends on all writers that precede it in
37473
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37474
+ * graph consumes, so registry changes (a new tool, a new settable path)
37475
+ * propagate into the execution order without a hand-maintained declaration.
37476
+ *
37477
+ * @throws Error if a feature writes a shared file but has no position in
37478
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37479
+ * whose merge policy wins, so it must be made explicitly there.
37480
+ */
37481
+ const deriveSharedWriteSteps = () => {
37482
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37483
+ const filesByFeature = /* @__PURE__ */ new Map();
37484
+ const depsByFeature = /* @__PURE__ */ new Map();
37485
+ for (const writer of deriveSharedFileWriters()) {
37486
+ for (const feature of writer.features) if (!orderIndex.has(feature)) throw new Error(`Feature '${feature}' writes the shared file '${writer.key}' but has no position in SHARED_WRITE_FEATURE_ORDER. Decide where its writes merge relative to the other features and add it to the order.`);
37487
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37488
+ for (const [position, feature] of ordered.entries()) {
37489
+ let files = filesByFeature.get(feature);
37490
+ if (!files) {
37491
+ files = /* @__PURE__ */ new Set();
37492
+ filesByFeature.set(feature, files);
37493
+ }
37494
+ files.add(writer.key);
37495
+ let deps = depsByFeature.get(feature);
37496
+ if (!deps) {
37497
+ deps = /* @__PURE__ */ new Set();
37498
+ depsByFeature.set(feature, deps);
37499
+ }
37500
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37501
+ }
37502
+ }
37503
+ const steps = /* @__PURE__ */ new Map();
37504
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37505
+ writesSharedFile: [...files].toSorted(),
37506
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37507
+ });
37508
+ return steps;
37509
+ };
37510
+ //#endregion
36176
37511
  //#region src/lib/generate.ts
36177
37512
  async function processFeatureGeneration(params) {
36178
37513
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -36322,98 +37657,53 @@ function resolveExecutionOrder(steps) {
36322
37657
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36323
37658
  return ordered;
36324
37659
  }
37660
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37661
+ const sharedWriteMeta = (id) => {
37662
+ const step = SHARED_WRITE_STEPS.get(id);
37663
+ return step ? {
37664
+ writesSharedFile: step.writesSharedFile,
37665
+ dependsOn: step.dependsOn
37666
+ } : {};
37667
+ };
36325
37668
  /**
36326
37669
  * The static shape of the generation step graph: which steps write which shared
36327
37670
  * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36328
- * for those writers. Exported (separately from the `run` closures, which need a
36329
- * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36330
- * tested directly against the real graph rather than a hand-copied one. Readonly
36331
- * so a consumer can't mutate this module-level singleton and affect every
36332
- * subsequent `generate()` call in the process.
37671
+ * for those writers. Both are derived from the processor registry's settable
37672
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37673
+ * tool or shared path never requires editing this graph. Exported (separately
37674
+ * from the `run` closures, which need a live `config`/`logger`) so
37675
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37676
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37677
+ * mutate this module-level singleton and affect every subsequent `generate()`
37678
+ * call in the process.
36333
37679
  */
36334
37680
  const GENERATION_STEP_GRAPH = [
36335
37681
  {
36336
37682
  id: "ignore",
36337
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37683
+ ...sharedWriteMeta("ignore")
36338
37684
  },
36339
37685
  {
36340
37686
  id: "mcp",
36341
- writesSharedFile: [
36342
- ".amp/settings.json",
36343
- ".augment/settings.json",
36344
- ".codex/config.toml",
36345
- ".config/amp/settings.json",
36346
- ".config/devin/config.json",
36347
- ".config/opencode/opencode.json",
36348
- ".config/zed/settings.json",
36349
- ".devin/config.json",
36350
- ".grok/config.toml",
36351
- ".hermes/config.yaml",
36352
- ".qwen/settings.json",
36353
- ".reasonix/config.toml",
36354
- ".takt/config.yaml",
36355
- ".vibe/config.toml",
36356
- ".zed/settings.json",
36357
- "kilo.json",
36358
- "opencode.json",
36359
- "reasonix.toml"
36360
- ],
36361
- dependsOn: ["ignore"]
37687
+ ...sharedWriteMeta("mcp")
36362
37688
  },
36363
37689
  { id: "commands" },
36364
- { id: "subagents" },
37690
+ {
37691
+ id: "subagents",
37692
+ ...sharedWriteMeta("subagents")
37693
+ },
36365
37694
  { id: "skills" },
36366
37695
  {
36367
37696
  id: "hooks",
36368
- writesSharedFile: [
36369
- ".augment/settings.json",
36370
- ".claude/settings.json",
36371
- ".codex/config.toml",
36372
- ".config/devin/config.json",
36373
- ".hermes/config.yaml",
36374
- ".kiro/agents/default.json",
36375
- ".qwen/settings.json",
36376
- ".vibe/config.toml"
36377
- ],
36378
- dependsOn: ["ignore", "mcp"]
37697
+ ...sharedWriteMeta("hooks")
36379
37698
  },
36380
37699
  {
36381
37700
  id: "permissions",
36382
- writesSharedFile: [
36383
- ".amp/settings.json",
36384
- ".augment/settings.json",
36385
- ".claude/settings.json",
36386
- ".codex/config.toml",
36387
- ".config/amp/settings.json",
36388
- ".config/devin/config.json",
36389
- ".config/opencode/opencode.json",
36390
- ".config/zed/settings.json",
36391
- ".devin/config.json",
36392
- ".grok/config.toml",
36393
- ".hermes/config.yaml",
36394
- ".kiro/agents/default.json",
36395
- ".qwen/settings.json",
36396
- ".reasonix/config.toml",
36397
- ".takt/config.yaml",
36398
- ".vibe/config.toml",
36399
- ".zed/settings.json",
36400
- "opencode.json",
36401
- "reasonix.toml"
36402
- ],
36403
- dependsOn: [
36404
- "ignore",
36405
- "hooks",
36406
- "mcp"
36407
- ]
37701
+ ...sharedWriteMeta("permissions")
36408
37702
  },
36409
37703
  {
36410
37704
  id: "rules",
36411
- writesSharedFile: ["kilo.json", "opencode.json"],
36412
- dependsOn: [
36413
- "mcp",
36414
- "skills",
36415
- "permissions"
36416
- ]
37705
+ ...sharedWriteMeta("rules"),
37706
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
36417
37707
  }
36418
37708
  ];
36419
37709
  /**
@@ -37105,6 +38395,6 @@ async function importPermissionsCore(params) {
37105
38395
  return writtenCount;
37106
38396
  }
37107
38397
  //#endregion
37108
- export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
38398
+ export { toPosixPath as $, RulesyncCommand as A, createTempDirectory as B, RulesyncHooks as C, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, ALL_FEATURES as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, formatError as Et, ConsoleLogger as F, getFileSize as G, ensureDir as H, JsonLogger as I, listDirectoryFiles as J, getHomeDirectory as K, CLIError as L, stringifyFrontmatter as M, ConfigResolver as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES_WITH_WILDCARD as Ot, findControlCharacter as P, removeTempDirectory as Q, ErrorCodes as R, HooksProcessor as S, RULESYNC_RULES_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Tt, fileExists as U, directoryExists as V, findFilesByGlobs as W, removeDirectory as X, readFileContent as Y, removeFile as Z, RulesyncPermissions as _, RULESYNC_MCP_SCHEMA_URL as _t, convertFromTool as a, RULESYNC_AIIGNORE_FILE_NAME as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_HOOKS_FILE_NAME as dt, writeFileContent as et, SkillsProcessor as f, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_MCP_FILE_NAME as ht, getProcessorRegistryEntry as i, MAX_FILE_SIZE as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as lt, RulesyncSkill as m, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS_WITH_WILDCARD as nt, RulesProcessor as o, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ot, getLocalSkillDirNames as p, RULESYNC_IGNORE_RELATIVE_FILE_PATH as pt, isSymlink as q, generate as r, ToolTargetSchema as rt, RulesyncRule as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as st, importFromTool as t, ALL_TOOL_TARGETS as tt, RulesyncSubagent as u, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ut, McpProcessor as v, RULESYNC_OVERVIEW_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as wt, RulesyncIgnore as x, RULESYNC_RELATIVE_DIR_PATH as xt, RulesyncMcp as y, RULESYNC_PERMISSIONS_FILE_NAME as yt, checkPathTraversal as z };
37109
38399
 
37110
- //# sourceMappingURL=import-BkxwFCDM.js.map
38400
+ //# sourceMappingURL=import-BxqZVTKb.js.map