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.
@@ -114,22 +114,22 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$248 } = node_path.posix;
117
+ const { join: join$247 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
127
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
128
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
129
129
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
130
130
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
131
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
131
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$247(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
133
133
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
134
134
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
135
135
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
@@ -3029,8 +3029,8 @@ const DEVIN_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "skills");
3029
3029
  const DEVIN_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "agents");
3030
3030
  const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
3031
3031
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
3032
+ const DEVIN_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
3032
3033
  const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "global_workflows");
3033
- const CODEIUM_WINDSURF_SKILLS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "skills");
3034
3034
  const DEVIN_CONFIG_FILE_NAME = "config.json";
3035
3035
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
3036
3036
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
@@ -3919,6 +3919,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3919
3919
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3920
3920
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3921
3921
  //#endregion
3922
+ //#region src/constants/takt-paths.ts
3923
+ const TAKT_DIR = ".takt";
3924
+ const TAKT_FACETS_SUBDIR = "facets";
3925
+ const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
3926
+ const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
3927
+ const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
3928
+ const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
3929
+ const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
3930
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
3931
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3932
+ /**
3933
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3934
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3935
+ * profiles (including permission modes), and other Takt settings.
3936
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3937
+ */
3938
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3939
+ /**
3940
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3941
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3942
+ * transport must be explicitly enabled here before any workflow-defined MCP
3943
+ * server using it is permitted to run.
3944
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3945
+ */
3946
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3947
+ //#endregion
3948
+ //#region src/utils/prototype-pollution.ts
3949
+ /**
3950
+ * Keys that, if walked into when constructing or merging objects from
3951
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3952
+ * chain) and propagate state to every other object in the runtime. Any code
3953
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3954
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3955
+ */
3956
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3957
+ "__proto__",
3958
+ "constructor",
3959
+ "prototype"
3960
+ ]);
3961
+ function isPrototypePollutionKey(key) {
3962
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3963
+ }
3964
+ /**
3965
+ * Returns a shallow copy of a record's own entries with every
3966
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3967
+ *
3968
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3969
+ * or `headers` table — into freshly generated config. Carrying such a map by
3970
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3971
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3972
+ * prototype). Walking the entries through this helper severs that path while
3973
+ * preserving every legitimate key.
3974
+ */
3975
+ function omitPrototypePollutionKeys(record) {
3976
+ const sanitized = {};
3977
+ for (const [key, value] of Object.entries(record)) {
3978
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3979
+ sanitized[key] = value;
3980
+ }
3981
+ return sanitized;
3982
+ }
3983
+ //#endregion
3984
+ //#region src/features/shared/shared-config-gateway.ts
3985
+ function sanitizeSharedConfigValue(value) {
3986
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
3987
+ if (!isPlainObject(value)) return value;
3988
+ const result = {};
3989
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
3990
+ return result;
3991
+ }
3992
+ /**
3993
+ * Parse a shared config file into a plain document: an empty/whitespace file
3994
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
3995
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
3996
+ * path when one is given.
3997
+ */
3998
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
3999
+ if (fileContent.trim() === "") return {};
4000
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
4001
+ let parsed;
4002
+ try {
4003
+ if (format === "yaml") parsed = (0, js_yaml.load)(fileContent);
4004
+ else if (format === "json") parsed = JSON.parse(fileContent);
4005
+ else parsed = (0, jsonc_parser.parse)(fileContent);
4006
+ } catch (error) {
4007
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4008
+ }
4009
+ if (parsed === void 0 || parsed === null) return {};
4010
+ if (!isPlainObject(parsed)) {
4011
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
4012
+ return {};
4013
+ }
4014
+ return sanitizeSharedConfigValue(parsed);
4015
+ }
4016
+ /**
4017
+ * Serialize a shared config document. YAML output always ends with exactly one
4018
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4019
+ * writers have always emitted (no trailing newline).
4020
+ */
4021
+ function stringifySharedConfig({ format, document }) {
4022
+ if (format === "yaml") return (0, js_yaml.dump)(document, {
4023
+ noRefs: true,
4024
+ sortKeys: false
4025
+ }).trimEnd() + "\n";
4026
+ return JSON.stringify(document, null, 2);
4027
+ }
4028
+ /**
4029
+ * Shallow merge: every top-level key in `patch` replaces the base key
4030
+ * wholesale; all other base keys are preserved. The policy for a feature that
4031
+ * owns a fixed set of top-level keys.
4032
+ */
4033
+ function mergeSharedConfigShallow({ base, patch }) {
4034
+ return {
4035
+ ...base,
4036
+ ...sanitizeSharedConfigValue(patch)
4037
+ };
4038
+ }
4039
+ /**
4040
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4041
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4042
+ * feature whose contribution interleaves with user-authored siblings at any
4043
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4044
+ * per-provider option tables) — nested sibling keys are preserved by
4045
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4046
+ * keys are dropped.
4047
+ */
4048
+ function mergeSharedConfigDeep({ base, patch }) {
4049
+ const result = { ...base };
4050
+ for (const [key, patchValue] of Object.entries(patch)) {
4051
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4052
+ const baseValue = result[key];
4053
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4054
+ base: baseValue,
4055
+ patch: patchValue
4056
+ });
4057
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4058
+ }
4059
+ return result;
4060
+ }
4061
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4062
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4063
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4064
+ /**
4065
+ * Who owns what in each gateway-managed shared config file, and which policy
4066
+ * resolves conflicts. Keys are `dir/file` tokens matching
4067
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4068
+ * lock-step with the writers derived from the processor registry, so an
4069
+ * undeclared writer fails CI instead of merging by accident.
4070
+ */
4071
+ const SHARED_CONFIG_OWNERSHIP = {
4072
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4073
+ format: "json",
4074
+ features: {
4075
+ ignore: {
4076
+ kind: "custom",
4077
+ policyFunction: "applyIgnoreReadDenies"
4078
+ },
4079
+ hooks: {
4080
+ kind: "replace-owned-keys",
4081
+ ownedKeys: ["hooks"]
4082
+ },
4083
+ permissions: {
4084
+ kind: "custom",
4085
+ policyFunction: "applyPermissions"
4086
+ }
4087
+ }
4088
+ },
4089
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4090
+ format: "yaml",
4091
+ features: {
4092
+ subagents: {
4093
+ kind: "replace-owned-keys",
4094
+ ownedKeys: ["plugins"]
4095
+ },
4096
+ mcp: {
4097
+ kind: "replace-owned-keys",
4098
+ ownedKeys: ["mcp_servers"]
4099
+ },
4100
+ hooks: {
4101
+ kind: "replace-owned-keys",
4102
+ ownedKeys: ["hooks"]
4103
+ },
4104
+ permissions: {
4105
+ kind: "deep-merge",
4106
+ replaceKeys: ["permissions"]
4107
+ }
4108
+ }
4109
+ },
4110
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4111
+ format: "yaml",
4112
+ invalidRootPolicy: "error",
4113
+ features: {
4114
+ mcp: {
4115
+ kind: "replace-owned-keys",
4116
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4117
+ },
4118
+ permissions: { kind: "deep-merge" }
4119
+ }
4120
+ }
4121
+ };
4122
+ /**
4123
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4124
+ * the existing content, merge the patch under the feature's declared policy,
4125
+ * and serialize. Throws when the file or feature is undeclared, when a
4126
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4127
+ * feature's policy is `custom` (those calls go to the named policy function
4128
+ * instead).
4129
+ */
4130
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4131
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4132
+ 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.`);
4133
+ const policy = declaration.features[feature];
4134
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4135
+ 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.`);
4136
+ const base = parseSharedConfig({
4137
+ format: declaration.format,
4138
+ fileContent: existingContent,
4139
+ filePath,
4140
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4141
+ });
4142
+ if (policy.kind === "replace-owned-keys") {
4143
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4144
+ 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.`);
4145
+ return stringifySharedConfig({
4146
+ format: declaration.format,
4147
+ document: mergeSharedConfigShallow({
4148
+ base,
4149
+ patch
4150
+ })
4151
+ });
4152
+ }
4153
+ const merged = mergeSharedConfigDeep({
4154
+ base,
4155
+ patch
4156
+ });
4157
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4158
+ return stringifySharedConfig({
4159
+ format: declaration.format,
4160
+ document: merged
4161
+ });
4162
+ }
4163
+ const READ_TOOL_NAME = "Read";
4164
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4165
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4166
+ const parsePermissionsBlock = (settings) => {
4167
+ const permissions = settings.permissions ?? {};
4168
+ return {
4169
+ allow: permissions.allow ?? [],
4170
+ ask: permissions.ask ?? [],
4171
+ deny: permissions.deny ?? []
4172
+ };
4173
+ };
4174
+ const withPermissions = (settings, next) => {
4175
+ const permissions = { ...settings.permissions };
4176
+ const assign = (key, values) => {
4177
+ if (values.length > 0) permissions[key] = values;
4178
+ else delete permissions[key];
4179
+ };
4180
+ assign("allow", next.allow);
4181
+ assign("ask", next.ask);
4182
+ assign("deny", next.deny);
4183
+ return {
4184
+ ...settings,
4185
+ permissions
4186
+ };
4187
+ };
4188
+ const applyIgnoreReadDenies = (params) => {
4189
+ const { settings, readDenies } = params;
4190
+ const current = parsePermissionsBlock(settings);
4191
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4192
+ return withPermissions(settings, {
4193
+ allow: current.allow,
4194
+ ask: current.ask,
4195
+ deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
4196
+ });
4197
+ };
4198
+ const applyPermissions = (params) => {
4199
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4200
+ const current = parsePermissionsBlock(settings);
4201
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4202
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4203
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4204
+ 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.`);
4205
+ }
4206
+ return withPermissions(settings, {
4207
+ allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
4208
+ ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
4209
+ deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
4210
+ });
4211
+ };
4212
+ //#endregion
3922
4213
  //#region src/features/opencode-config.ts
3923
4214
  /**
3924
4215
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3943,9 +4234,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3943
4234
  });
3944
4235
  const fileContent = await readFileContentOrNull((0, node_path.join)(configDir, "opencode.jsonc")) ?? await readFileContentOrNull((0, node_path.join)(configDir, "opencode.json"));
3945
4236
  if (!fileContent) return {};
3946
- const parsed = (0, jsonc_parser.parse)(fileContent);
3947
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3948
- return parsed;
4237
+ return parseSharedConfig({
4238
+ format: "jsonc",
4239
+ fileContent
4240
+ });
3949
4241
  }
3950
4242
  /**
3951
4243
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4854,32 +5146,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4854
5146
  }
4855
5147
  };
4856
5148
  //#endregion
4857
- //#region src/constants/takt-paths.ts
4858
- const TAKT_DIR = ".takt";
4859
- const TAKT_FACETS_SUBDIR = "facets";
4860
- const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
4861
- const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
4862
- const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
4863
- const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
4864
- const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
4865
- const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
4866
- const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4867
- /**
4868
- * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
4869
- * `~/.takt/config.yaml` (global); it holds the active provider, provider
4870
- * profiles (including permission modes), and other Takt settings.
4871
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4872
- */
4873
- const TAKT_CONFIG_FILE_NAME = "config.yaml";
4874
- /**
4875
- * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4876
- * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4877
- * transport must be explicitly enabled here before any workflow-defined MCP
4878
- * server using it is permitted to run.
4879
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4880
- */
4881
- const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4882
- //#endregion
4883
5149
  //#region src/features/takt-shared.ts
4884
5150
  /**
4885
5151
  * Shared utilities for all TAKT-* tool file classes.
@@ -5715,6 +5981,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
5715
5981
  "sessionEnd",
5716
5982
  "beforeSubmitPrompt",
5717
5983
  "permissionRequest",
5984
+ "preToolUse",
5985
+ "postToolUse",
5718
5986
  "postToolUseFailure",
5719
5987
  "stop",
5720
5988
  "preCompact",
@@ -5740,7 +6008,13 @@ const CODEXCLI_HOOK_EVENTS = [
5740
6008
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
5741
6009
  * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
5742
6010
  * 1:1 canonical equivalent, so no new canonical events are required.
5743
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
6011
+ *
6012
+ * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
6013
+ * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
6014
+ * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
6015
+ * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
6016
+ * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
6017
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
5744
6018
  */
5745
6019
  const GOOSE_HOOK_EVENTS = [
5746
6020
  "sessionStart",
@@ -5753,9 +6027,7 @@ const GOOSE_HOOK_EVENTS = [
5753
6027
  "beforeReadFile",
5754
6028
  "afterFileEdit",
5755
6029
  "beforeShellExecution",
5756
- "afterShellExecution",
5757
- "subagentStart",
5758
- "subagentStop"
6030
+ "afterShellExecution"
5759
6031
  ];
5760
6032
  /** Hook events supported by Kiro CLI. */
5761
6033
  const KIRO_HOOK_EVENTS = [
@@ -6208,9 +6480,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6208
6480
  beforeReadFile: "BeforeReadFile",
6209
6481
  afterFileEdit: "AfterFileEdit",
6210
6482
  beforeShellExecution: "BeforeShellExecution",
6211
- afterShellExecution: "AfterShellExecution",
6212
- subagentStart: "SubagentStart",
6213
- subagentStop: "SubagentStop"
6483
+ afterShellExecution: "AfterShellExecution"
6214
6484
  };
6215
6485
  /**
6216
6486
  * Map Goose PascalCase event names to canonical camelCase.
@@ -6224,6 +6494,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
6224
6494
  sessionEnd: "session.end",
6225
6495
  beforeSubmitPrompt: "user.prompt",
6226
6496
  permissionRequest: "permission.request",
6497
+ preToolUse: "tool.use",
6498
+ postToolUse: "tool.result",
6227
6499
  postToolUseFailure: "tool.error",
6228
6500
  stop: "task.complete",
6229
6501
  preCompact: "context.compact",
@@ -6351,42 +6623,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6351
6623
  */
6352
6624
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6353
6625
  //#endregion
6354
- //#region src/utils/prototype-pollution.ts
6355
- /**
6356
- * Keys that, if walked into when constructing or merging objects from
6357
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
6358
- * chain) and propagate state to every other object in the runtime. Any code
6359
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
6360
- * parsing, MCP config conversion, settings round-trip — should skip these.
6361
- */
6362
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
6363
- "__proto__",
6364
- "constructor",
6365
- "prototype"
6366
- ]);
6367
- function isPrototypePollutionKey(key) {
6368
- return PROTOTYPE_POLLUTION_KEYS.has(key);
6369
- }
6370
- /**
6371
- * Returns a shallow copy of a record's own entries with every
6372
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
6373
- *
6374
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
6375
- * or `headers` table — into freshly generated config. Carrying such a map by
6376
- * reference, or re-assigning its keys via bracket notation, would let a literal
6377
- * `__proto__` key ride along (and re-assigning it would mutate the target's
6378
- * prototype). Walking the entries through this helper severs that path while
6379
- * preserving every legitimate key.
6380
- */
6381
- function omitPrototypePollutionKeys(record) {
6382
- const sanitized = {};
6383
- for (const [key, value] of Object.entries(record)) {
6384
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6385
- sanitized[key] = value;
6386
- }
6387
- return sanitized;
6388
- }
6389
- //#endregion
6390
6626
  //#region src/features/hooks/tool-hooks-converter.ts
6391
6627
  function isToolMatcherEntry(x) {
6392
6628
  if (x === null || typeof x !== "object") return false;
@@ -7045,24 +7281,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7045
7281
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7046
7282
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7047
7283
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7048
- let settings;
7049
- try {
7050
- settings = JSON.parse(existingContent);
7051
- } catch (error) {
7052
- throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
7053
- }
7054
7284
  const config = rulesyncHooks.getJson();
7055
- const claudeHooks = canonicalToToolHooks({
7056
- config,
7057
- toolOverrideHooks: config.claudecode?.hooks,
7058
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7059
- logger
7285
+ const fileContent = applySharedConfigPatch({
7286
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7287
+ feature: "hooks",
7288
+ existingContent,
7289
+ patch: { hooks: canonicalToToolHooks({
7290
+ config,
7291
+ toolOverrideHooks: config.claudecode?.hooks,
7292
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7293
+ logger
7294
+ }) },
7295
+ filePath
7060
7296
  });
7061
- const merged = {
7062
- ...settings,
7063
- hooks: claudeHooks
7064
- };
7065
- const fileContent = JSON.stringify(merged, null, 2);
7066
7297
  return new ClaudecodeHooks({
7067
7298
  outputRoot,
7068
7299
  relativeDirPath: paths.relativeDirPath,
@@ -8218,7 +8449,7 @@ const GOOSE_CONVERTER_CONFIG = {
8218
8449
  *
8219
8450
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
8220
8451
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
8221
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
8452
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
8222
8453
  */
8223
8454
  var GooseHooks = class GooseHooks extends ToolHooks {
8224
8455
  constructor(params) {
@@ -8269,7 +8500,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8269
8500
  throw new Error(`Failed to parse Goose hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
8270
8501
  }
8271
8502
  const hooks = toolHooksToCanonical({
8272
- hooks: parsed.hooks,
8503
+ 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,
8273
8504
  converterConfig: GOOSE_CONVERTER_CONFIG
8274
8505
  });
8275
8506
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
@@ -8294,37 +8525,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8294
8525
  }
8295
8526
  };
8296
8527
  //#endregion
8297
- //#region src/features/hermes-config.ts
8298
- function sanitizeHermesConfigValue(value) {
8299
- if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
8300
- if (!isPlainObject(value)) return value;
8301
- const sanitized = omitPrototypePollutionKeys(value);
8302
- const result = {};
8303
- for (const [key, nestedValue] of Object.entries(sanitized)) {
8304
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8305
- result[key] = sanitizeHermesConfigValue(nestedValue);
8306
- }
8307
- return result;
8308
- }
8309
- function parseHermesConfig(fileContent) {
8310
- if (!fileContent.trim()) return {};
8311
- const parsed = (0, js_yaml.load)(fileContent);
8312
- if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
8313
- return {};
8314
- }
8315
- function stringifyHermesConfig(config) {
8316
- return (0, js_yaml.dump)(config, {
8317
- noRefs: true,
8318
- sortKeys: false
8319
- }).trimEnd() + "\n";
8320
- }
8321
- function mergeHermesConfig(fileContent, patch) {
8322
- return stringifyHermesConfig({
8323
- ...parseHermesConfig(fileContent),
8324
- ...patch
8325
- });
8326
- }
8327
- //#endregion
8328
8528
  //#region src/features/hooks/hermesagent-hooks.ts
8329
8529
  /**
8330
8530
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8452,10 +8652,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8452
8652
  return true;
8453
8653
  }
8454
8654
  setFileContent(fileContent) {
8455
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8655
+ this.fileContent = applySharedConfigPatch({
8656
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8657
+ feature: "hooks",
8658
+ existingContent: fileContent,
8659
+ patch: parseSharedConfig({
8660
+ format: "yaml",
8661
+ fileContent: this.fileContent
8662
+ })
8663
+ });
8456
8664
  }
8457
8665
  toRulesyncHooks() {
8458
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8666
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8667
+ format: "yaml",
8668
+ fileContent: this.getFileContent()
8669
+ }).hooks);
8459
8670
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8460
8671
  version: 1,
8461
8672
  hooks
@@ -8465,11 +8676,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8465
8676
  const config = rulesyncHooks.getJson();
8466
8677
  return new HermesagentHooks({
8467
8678
  outputRoot,
8468
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8469
- config,
8470
- toolOverrideHooks: config.hermesagent?.hooks,
8471
- logger
8472
- }) })
8679
+ fileContent: stringifySharedConfig({
8680
+ format: "yaml",
8681
+ document: { hooks: canonicalToHermesHooks({
8682
+ config,
8683
+ toolOverrideHooks: config.hermesagent?.hooks,
8684
+ logger
8685
+ }) }
8686
+ })
8473
8687
  });
8474
8688
  }
8475
8689
  };
@@ -10489,66 +10703,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10489
10703
  }
10490
10704
  };
10491
10705
  //#endregion
10492
- //#region src/features/claudecode-settings-gateway.ts
10493
- /**
10494
- * Single owner of the `.claude/settings.json` `permissions` block, which both
10495
- * `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
10496
- * (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
10497
- * the merge, and the cross-feature ownership rule (permissions' explicit `Read`
10498
- * rules win over ignore-derived `Read` denies) used to be duplicated across both
10499
- * feature files; they live here once so each feature just states its intent and
10500
- * never reasons about the other's existence.
10501
- */
10502
- const READ_TOOL_NAME = "Read";
10503
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
10504
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
10505
- const parsePermissionsBlock = (settings) => {
10506
- const permissions = settings.permissions ?? {};
10507
- return {
10508
- allow: permissions.allow ?? [],
10509
- ask: permissions.ask ?? [],
10510
- deny: permissions.deny ?? []
10511
- };
10512
- };
10513
- const withPermissions = (settings, next) => {
10514
- const permissions = { ...settings.permissions };
10515
- const assign = (key, values) => {
10516
- if (values.length > 0) permissions[key] = values;
10517
- else delete permissions[key];
10518
- };
10519
- assign("allow", next.allow);
10520
- assign("ask", next.ask);
10521
- assign("deny", next.deny);
10522
- return {
10523
- ...settings,
10524
- permissions
10525
- };
10526
- };
10527
- const applyIgnoreReadDenies = (params) => {
10528
- const { settings, readDenies } = params;
10529
- const current = parsePermissionsBlock(settings);
10530
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
10531
- return withPermissions(settings, {
10532
- allow: current.allow,
10533
- ask: current.ask,
10534
- deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
10535
- });
10536
- };
10537
- const applyPermissions = (params) => {
10538
- const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
10539
- const current = parsePermissionsBlock(settings);
10540
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
10541
- if (logger && managedToolNames.has(READ_TOOL_NAME)) {
10542
- const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
10543
- 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.`);
10544
- }
10545
- return withPermissions(settings, {
10546
- allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
10547
- ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
10548
- deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
10549
- });
10550
- };
10551
- //#endregion
10552
10706
  //#region src/features/ignore/claudecode-ignore.ts
10553
10707
  const DEFAULT_FILE_MODE = "shared";
10554
10708
  /**
@@ -11971,7 +12125,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11971
12125
  };
11972
12126
  //#endregion
11973
12127
  //#region src/features/mcp/augmentcode-mcp.ts
11974
- const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
11975
12128
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
11976
12129
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
11977
12130
  let parsed;
@@ -11986,13 +12139,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
11986
12139
  /**
11987
12140
  * AugmentCode (Auggie CLI) MCP servers.
11988
12141
  *
11989
- * MCP servers are persisted in the shared user settings file
11990
- * `~/.augment/settings.json` (global only the docs do not document a
11991
- * project-level MCP location). That same file also holds `hooks` and
12142
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12143
+ * at either scope: the committed workspace file for team-shared servers (project)
12144
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
11992
12145
  * `toolPermissions`, so generation merges the `mcpServers` block into the
11993
- * existing settings instead of overwriting it, and the file is never deleted.
12146
+ * existing settings instead of overwriting it, and the file is never deleted. On
12147
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12148
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
11994
12149
  *
11995
- * @see https://docs.augmentcode.com/cli/integrations
12150
+ * @see https://docs.augmentcode.com/cli/config
11996
12151
  */
11997
12152
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
11998
12153
  json;
@@ -12014,9 +12169,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12014
12169
  };
12015
12170
  }
12016
12171
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12017
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12018
12172
  const paths = this.getSettablePaths({ global });
12019
- const json = parseAugmentcodeSettings(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12173
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12174
+ outputRoot,
12175
+ relativeDirPath: paths.relativeDirPath,
12176
+ baseFileName: paths.relativeFilePath,
12177
+ baseFallbackContent: "{}",
12178
+ includeLocalOverlay: !global
12179
+ }), paths.relativeDirPath, paths.relativeFilePath);
12020
12180
  const newJson = {
12021
12181
  ...json,
12022
12182
  mcpServers: json.mcpServers ?? {}
@@ -12031,7 +12191,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12031
12191
  });
12032
12192
  }
12033
12193
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12034
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12035
12194
  const paths = this.getSettablePaths({ global });
12036
12195
  const merged = {
12037
12196
  ...parseAugmentcodeSettings(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -12306,6 +12465,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
12306
12465
  envVars: "env_vars"
12307
12466
  };
12308
12467
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
12468
+ /**
12469
+ * Translate a server's `oauth` table from the canonical rulesync shape (Claude
12470
+ * Code style camelCase) into the shape Codex CLI understands. Codex expects the
12471
+ * OAuth client id under snake_case `client_id`; without it `codex mcp login`
12472
+ * falls back to dynamic client registration and fails for providers that do not
12473
+ * support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
12474
+ * the added `client_id` so tools that read the camelCase shape keep working and
12475
+ * the round-trip stays stable.
12476
+ */
12477
+ function mapOauthToCodex(oauth) {
12478
+ const result = omitPrototypePollutionKeys(oauth);
12479
+ if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
12480
+ return result;
12481
+ }
12482
+ /**
12483
+ * Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
12484
+ * the canonical `clientId` on import. When both keys are present (the shape
12485
+ * rulesync itself emits) the canonical `clientId` wins and `client_id` is
12486
+ * dropped so a subsequent generate does not accumulate duplicates.
12487
+ */
12488
+ function mapOauthFromCodex(oauth) {
12489
+ const result = {};
12490
+ for (const [key, value] of Object.entries(oauth)) {
12491
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12492
+ if (key === "client_id") {
12493
+ if (!("clientId" in oauth)) result["clientId"] = value;
12494
+ continue;
12495
+ }
12496
+ result[key] = value;
12497
+ }
12498
+ return result;
12499
+ }
12309
12500
  function convertFromCodexFormat(codexMcp) {
12310
12501
  const result = {};
12311
12502
  for (const [name, config] of Object.entries(codexMcp)) {
@@ -12315,7 +12506,8 @@ function convertFromCodexFormat(codexMcp) {
12315
12506
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12316
12507
  if (key === "enabled") {
12317
12508
  if (value === false) converted["disabled"] = true;
12318
- } else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12509
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
12510
+ else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12319
12511
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
12320
12512
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12321
12513
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
@@ -12335,7 +12527,8 @@ function convertToCodexFormat(mcpServers) {
12335
12527
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12336
12528
  if (key === "disabled") {
12337
12529
  if (value === true) converted["enabled"] = false;
12338
- } else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12530
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
12531
+ else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12339
12532
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
12340
12533
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12341
12534
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
@@ -13551,10 +13744,12 @@ function resolveHermesTimeout(config) {
13551
13744
  *
13552
13745
  * Hermes is close to the MCP spec but not identical: `command` must be a single
13553
13746
  * executable string (an array's tail folds into `args`), a server is disabled
13554
- * via `enabled: false` (not the canonical `disabled: true`), and remote servers
13555
- * use `url`/`headers`. Only fields Hermes understands are emitted, so the shared
13556
- * `config.yaml` is not polluted with canonical-only aliases (`type`, `transport`,
13557
- * `httpUrl`, `networkTimeout`, tool-filter keys, ...).
13747
+ * via `enabled: false` (not the canonical `disabled: true`), remote servers use
13748
+ * `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
13749
+ * exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
13750
+ * fields Hermes understands are emitted, so the shared `config.yaml` is not
13751
+ * polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
13752
+ * `networkTimeout`, ...).
13558
13753
  */
13559
13754
  function convertServerToHermes(config) {
13560
13755
  const out = {};
@@ -13578,6 +13773,10 @@ function convertServerToHermes(config) {
13578
13773
  if (config.disabled === true) out.enabled = false;
13579
13774
  const timeout = resolveHermesTimeout(config);
13580
13775
  if (timeout !== void 0) out.timeout = timeout;
13776
+ const tools = {};
13777
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
13778
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
13779
+ if (Object.keys(tools).length > 0) out.tools = tools;
13581
13780
  return out;
13582
13781
  }
13583
13782
  /**
@@ -13619,6 +13818,10 @@ function convertFromHermesFormat(mcpServers) {
13619
13818
  if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
13620
13819
  if (config.enabled === false) server.disabled = true;
13621
13820
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
13821
+ if (isRecord(config.tools)) {
13822
+ if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
13823
+ if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
13824
+ }
13622
13825
  result[name] = server;
13623
13826
  }
13624
13827
  return result;
@@ -13637,7 +13840,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13637
13840
  config;
13638
13841
  constructor(params) {
13639
13842
  super(params);
13640
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13843
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13844
+ format: "yaml",
13845
+ fileContent: this.fileContent
13846
+ }) : {};
13641
13847
  }
13642
13848
  getConfig() {
13643
13849
  return this.config;
@@ -13646,9 +13852,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13646
13852
  return true;
13647
13853
  }
13648
13854
  setFileContent(fileContent) {
13649
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13855
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13856
+ format: "yaml",
13857
+ fileContent
13858
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13650
13859
  this.config = merged;
13651
- super.setFileContent(stringifyHermesConfig(merged));
13860
+ super.setFileContent(applySharedConfigPatch({
13861
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13862
+ feature: "mcp",
13863
+ existingContent: fileContent,
13864
+ patch: { mcp_servers: merged.mcp_servers }
13865
+ }));
13652
13866
  }
13653
13867
  isDeletable() {
13654
13868
  return false;
@@ -13675,12 +13889,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13675
13889
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13676
13890
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13677
13891
  const paths = this.getSettablePaths({ global });
13678
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13892
+ const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13893
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13894
+ format: "yaml",
13895
+ fileContent
13896
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13679
13897
  return new HermesagentMcp({
13680
13898
  outputRoot,
13681
13899
  relativeDirPath: paths.relativeDirPath,
13682
13900
  relativeFilePath: paths.relativeFilePath,
13683
- fileContent: stringifyHermesConfig(merged),
13901
+ fileContent: applySharedConfigPatch({
13902
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13903
+ feature: "mcp",
13904
+ existingContent: fileContent,
13905
+ patch: { mcp_servers: merged.mcp_servers }
13906
+ }),
13684
13907
  validate,
13685
13908
  global
13686
13909
  });
@@ -14845,28 +15068,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14845
15068
  }
14846
15069
  };
14847
15070
  //#endregion
14848
- //#region src/features/shared/takt-config.ts
14849
- /**
14850
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14851
- *
14852
- * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14853
- * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14854
- * instances are rejected for prototype-pollution hardening; a YAML mapping
14855
- * always parses to a plain object.
14856
- */
14857
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14858
- const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
14859
- let parsed;
14860
- try {
14861
- parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
14862
- } catch (error) {
14863
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14864
- }
14865
- if (parsed === void 0 || parsed === null) return {};
14866
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14867
- return parsed;
14868
- }
14869
- //#endregion
14870
15071
  //#region src/features/mcp/takt-mcp.ts
14871
15072
  /**
14872
15073
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -14932,17 +15133,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
14932
15133
  }
14933
15134
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14934
15135
  const paths = TaktMcp.getSettablePaths({ global });
14935
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15136
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15137
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
14936
15138
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14937
- const merged = {
14938
- ...config,
14939
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14940
- };
14941
15139
  return new TaktMcp({
14942
15140
  outputRoot,
14943
15141
  relativeDirPath: paths.relativeDirPath,
14944
15142
  relativeFilePath: paths.relativeFilePath,
14945
- fileContent: (0, js_yaml.dump)(merged),
15143
+ fileContent: applySharedConfigPatch({
15144
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15145
+ feature: "mcp",
15146
+ existingContent,
15147
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15148
+ filePath
15149
+ }),
14946
15150
  validate,
14947
15151
  global
14948
15152
  });
@@ -15387,7 +15591,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15387
15591
  ["augmentcode", {
15388
15592
  class: AugmentcodeMcp,
15389
15593
  meta: {
15390
- supportsProject: false,
15594
+ supportsProject: true,
15391
15595
  supportsGlobal: true,
15392
15596
  supportsEnabledTools: false,
15393
15597
  supportsDisabledTools: false
@@ -15774,17 +15978,317 @@ const PermissionActionSchema = zod_mini.z.enum([
15774
15978
  */
15775
15979
  const PermissionRulesSchema = zod_mini.z.record(zod_mini.z.string(), PermissionActionSchema);
15776
15980
  /**
15981
+ * OpenCode-specific permission value. Unlike the shared canonical block, which
15982
+ * only accepts a pattern-to-action map, OpenCode also allows a bare action
15983
+ * string that applies to the whole category (e.g. `"external_directory": "deny"`).
15984
+ * This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
15985
+ */
15986
+ const OpencodeOverridePermissionValueSchema = zod_mini.z.union([PermissionActionSchema, PermissionRulesSchema]);
15987
+ /**
15988
+ * Tool-scoped override block for OpenCode. Permission categories placed here
15989
+ * (e.g. OpenCode-only categories such as `external_directory`) are emitted only
15990
+ * into OpenCode's config and never leak into other tools' permission files. It
15991
+ * also lets a shared category be overridden with an OpenCode-specific value.
15992
+ * Kept `looseObject` so future OpenCode categories are accepted.
15993
+ *
15994
+ * @example
15995
+ * { "permission": { "external_directory": "deny", "webfetch": "allow" } }
15996
+ */
15997
+ const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
15998
+ /**
15999
+ * Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
16000
+ * into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
16001
+ * It carries Hermes-specific approval/security controls that have no canonical
16002
+ * permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
16003
+ * `security` (`allow_private_urls`, ...), `skills.write_approval`,
16004
+ * `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
16005
+ * current or future Hermes config key can be authored without modeling each one.
16006
+ *
16007
+ * @example
16008
+ * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
16009
+ */
16010
+ const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
16011
+ /**
16012
+ * Tool-scoped override block for Cline. Cline's `command-permissions.json`
16013
+ * carries a single global `allowRedirects` boolean (gates shell redirection
16014
+ * operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
16015
+ * canonical permission category. Placing it here lets users author it
16016
+ * declaratively; it is emitted only into Cline's config. Kept `looseObject` so
16017
+ * future Cline-only knobs can be added.
16018
+ *
16019
+ * @example
16020
+ * { "allowRedirects": true }
16021
+ */
16022
+ const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean()) });
16023
+ /**
16024
+ * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
16025
+ * object is a free-form record with tool-specific keys that have no canonical
16026
+ * category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
16027
+ * `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
16028
+ * (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
16029
+ * `repo_clone`, `repo_overview`). Placing them here makes them authorable and
16030
+ * portable and keeps them out of other tools' configs. Mirrors the OpenCode
16031
+ * override; each value may be a bare action string or a pattern map.
16032
+ *
16033
+ * @example
16034
+ * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
16035
+ */
16036
+ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
16037
+ /**
16038
+ * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
16039
+ * (in `.claude/settings.json`) carries non-list fields that have no canonical
16040
+ * permission category — `defaultMode` (the session-start permission mode) and
16041
+ * `additionalDirectories` (extra working directories) being the primary ones.
16042
+ * Fields placed under `claudecode.permissions` are merged into the settings
16043
+ * `permissions` object and emitted only for Claude Code, while the shared
16044
+ * `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
16045
+ * `looseObject` passthrough so any current or future `permissions` field can be
16046
+ * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
16047
+ * ignored here (rulesync owns them).
16048
+ *
16049
+ * @example
16050
+ * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
16051
+ */
16052
+ const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})) });
16053
+ /**
16054
+ * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
16055
+ * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
16056
+ * the tool's base permission is ALWAYS (allow). The canonical model can only set
16057
+ * a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
16058
+ * these patterns" escalation cannot be expressed. Entries under
16059
+ * `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
16060
+ * category; the shared `permission` block still drives the base permission and
16061
+ * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
16062
+ *
16063
+ * @example
16064
+ * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
16065
+ */
16066
+ const VibePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({ sensitive_patterns: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))) });
16067
+ /**
16068
+ * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
16069
+ * autonomy settings with no canonical permission category — `approvalMode`
16070
+ * (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
16071
+ * (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
16072
+ * `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
16073
+ * only for Cursor, while the shared `permission` block continues to drive the
16074
+ * `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
16075
+ * `cli.json` keys can be authored (they are merged verbatim on generate);
16076
+ * `sandbox`'s accepted values are not documented so it passes through verbatim.
16077
+ * Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
16078
+ * authored here reach `cli.json` on generate but are not re-extracted.
16079
+ *
16080
+ * @example
16081
+ * { "approvalMode": "auto-review" }
16082
+ */
16083
+ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
16084
+ approvalMode: zod_mini.z.optional(zod_mini.z.string()),
16085
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
16086
+ });
16087
+ /**
16088
+ * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
16089
+ * autonomy/sandbox controls with no canonical permission category — under
16090
+ * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
16091
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
16092
+ * placed here are merged into the matching `settings.json` group and emitted
16093
+ * only for Qwen, while the shared `permission` block continues to drive the
16094
+ * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
16095
+ * passthrough) so any current or future `tools`/`security` key can be authored.
16096
+ *
16097
+ * @example
16098
+ * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
16099
+ */
16100
+ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
16101
+ tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
16102
+ security: zod_mini.z.optional(zod_mini.z.looseObject({}))
16103
+ });
16104
+ /**
16105
+ * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
16106
+ * to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
16107
+ * enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
16108
+ * `network`) and plan-mode read-only trust lists under `[agent]`
16109
+ * (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
16110
+ * are merged into the matching `reasonix.toml` table and emitted only for
16111
+ * Reasonix, while the shared `permission` block continues to drive
16112
+ * `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
16113
+ * Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
16114
+ * re-extracted from `[agent]` on import — other `agent` keys authored here reach
16115
+ * `reasonix.toml` on generate but are not re-extracted back into the override.
16116
+ *
16117
+ * @example
16118
+ * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
16119
+ */
16120
+ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
16121
+ sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
16122
+ agent: zod_mini.z.optional(zod_mini.z.looseObject({}))
16123
+ });
16124
+ /**
16125
+ * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
16126
+ * exposes security controls with no canonical per-command allow/ask/deny slot —
16127
+ * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
16128
+ * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
16129
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
16130
+ * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
16131
+ * `interactionMode`). Fields placed here are merged into `settings.json` and
16132
+ * emitted only for Factory Droid, while the shared `permission` block continues
16133
+ * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
16134
+ *
16135
+ * @example
16136
+ * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
16137
+ */
16138
+ const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) });
16139
+ /**
16140
+ * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
16141
+ * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
16142
+ * slot — `agent_mode_coding_permissions`
16143
+ * (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
16144
+ * `agent_mode_coding_file_read_allowlist` (a path array), and
16145
+ * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
16146
+ * Fields placed here are merged into `[agents.profiles]` of Warp's global
16147
+ * `settings.toml`, while the shared `permission` block continues to drive the
16148
+ * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
16149
+ * Warp permissions are global-only.
16150
+ *
16151
+ * @example
16152
+ * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
16153
+ */
16154
+ const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
16155
+ agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.string()),
16156
+ agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
16157
+ agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean())
16158
+ });
16159
+ /**
16160
+ * Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
16161
+ * two top-level autonomy knobs with no canonical per-glob slot:
16162
+ * `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
16163
+ * `defaultBehavior` (the fallback action applied when no rule matches — Junie
16164
+ * documents only `allow`/`ask`). Fields placed here are merged onto the
16165
+ * top level of `allowlist.json`, while the shared `permission` block continues
16166
+ * to drive the per-category `rules` groups.
16167
+ *
16168
+ * @example
16169
+ * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16170
+ */
16171
+ const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
16172
+ allowReadonlyCommands: zod_mini.z.optional(zod_mini.z.boolean()),
16173
+ defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
16174
+ });
16175
+ /**
16176
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16177
+ * permission surfaces the canonical coarse-mode mapping can't express:
16178
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16179
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16180
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16181
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16182
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16183
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16184
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16185
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16186
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16187
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16188
+ * scope are supported.
16189
+ *
16190
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16191
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16192
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16193
+ * of scope for this override.
16194
+ *
16195
+ * @example
16196
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16197
+ */
16198
+ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
16199
+ step_permission_overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16200
+ provider_options: zod_mini.z.optional(zod_mini.z.looseObject({}))
16201
+ });
16202
+ /**
16203
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16204
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16205
+ * express, so they are authored here and merged into the shared Amp settings
16206
+ * file, while the shared `permission` block continues to drive the canonical
16207
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16208
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16209
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16210
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16211
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16212
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16213
+ * later fallbacks), preserving author order.
16214
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16215
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16216
+ * confirmation).
16217
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16218
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16219
+ * supported.
16220
+ *
16221
+ * @example
16222
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16223
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16224
+ */
16225
+ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
16226
+ permissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
16227
+ tool: zod_mini.z.string(),
16228
+ action: zod_mini.z.string()
16229
+ }))),
16230
+ mcpPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({}))),
16231
+ guardedFiles: zod_mini.z.optional(zod_mini.z.looseObject({ allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
16232
+ dangerouslyAllowAll: zod_mini.z.optional(zod_mini.z.boolean())
16233
+ });
16234
+ /**
16235
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16236
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16237
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16238
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16239
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16240
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16241
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16242
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16243
+ * Fields placed here are merged onto the top level of
16244
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16245
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16246
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16247
+ * Verified against https://antigravity.google/docs/cli/reference and
16248
+ * https://antigravity.google/docs/cli/sandbox.
16249
+ *
16250
+ * @example
16251
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16252
+ */
16253
+ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
16254
+ toolPermission: zod_mini.z.optional(zod_mini.z.string()),
16255
+ enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
16256
+ });
16257
+ /**
15777
16258
  * Permissions configuration.
15778
16259
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
15779
16260
  * Values are pattern-to-action mappings for that tool category.
15780
16261
  *
16262
+ * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
16263
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16264
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16265
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16266
+ * other tool reads the shared `permission` block and ignores them.
16267
+ *
15781
16268
  * @example
15782
16269
  * {
15783
16270
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
15784
16271
  * "edit": { "*": "deny", "src/**": "allow" }
15785
16272
  * }
15786
16273
  */
15787
- const PermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.record(zod_mini.z.string(), PermissionRulesSchema) });
16274
+ const PermissionsConfigSchema = zod_mini.z.looseObject({
16275
+ permission: zod_mini.z.record(zod_mini.z.string(), PermissionRulesSchema),
16276
+ opencode: zod_mini.z.optional(OpencodePermissionsOverrideSchema),
16277
+ hermes: zod_mini.z.optional(HermesPermissionsOverrideSchema),
16278
+ cline: zod_mini.z.optional(ClinePermissionsOverrideSchema),
16279
+ kilo: zod_mini.z.optional(KiloPermissionsOverrideSchema),
16280
+ claudecode: zod_mini.z.optional(ClaudecodePermissionsOverrideSchema),
16281
+ vibe: zod_mini.z.optional(VibePermissionsOverrideSchema),
16282
+ cursor: zod_mini.z.optional(CursorPermissionsOverrideSchema),
16283
+ qwencode: zod_mini.z.optional(QwencodePermissionsOverrideSchema),
16284
+ reasonix: zod_mini.z.optional(ReasonixPermissionsOverrideSchema),
16285
+ factorydroid: zod_mini.z.optional(FactorydroidPermissionsOverrideSchema),
16286
+ warp: zod_mini.z.optional(WarpPermissionsOverrideSchema),
16287
+ junie: zod_mini.z.optional(JuniePermissionsOverrideSchema),
16288
+ takt: zod_mini.z.optional(TaktPermissionsOverrideSchema),
16289
+ amp: zod_mini.z.optional(AmpPermissionsOverrideSchema),
16290
+ "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema)
16291
+ });
15788
16292
  /**
15789
16293
  * Full permissions file schema including optional $schema field.
15790
16294
  */
@@ -15895,6 +16399,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
15895
16399
  * Reference: https://ampcode.com/manual ("amp.permissions").
15896
16400
  */
15897
16401
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16402
+ /**
16403
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16404
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16405
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16406
+ * permissions override. Reference: https://ampcode.com/manual.
16407
+ */
16408
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16409
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16410
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16411
+ /**
16412
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16413
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16414
+ */
16415
+ function ampMatchesCmd(entry) {
16416
+ const cmd = entry.matches?.cmd;
16417
+ return typeof cmd === "string" ? cmd : void 0;
16418
+ }
16419
+ /**
16420
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16421
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16422
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16423
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16424
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16425
+ * override verbatim instead of being flattened with loss or dropped.
16426
+ */
16427
+ function isCanonicalAmpEntry(entry) {
16428
+ if (entry.action === "delegate") return false;
16429
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16430
+ const matches = entry.matches;
16431
+ if (matches === void 0) return true;
16432
+ const keys = Object.keys(matches);
16433
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16434
+ }
15898
16435
  function parseAmpSettings(fileContent) {
15899
16436
  const errors = [];
15900
16437
  const parsed = (0, jsonc_parser.parse)(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -15910,10 +16447,20 @@ function toDisableList(value) {
15910
16447
  return value.filter((entry) => typeof entry === "string");
15911
16448
  }
15912
16449
  /**
16450
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16451
+ * settings, returning `undefined` when the key is absent or not an array so the
16452
+ * override omits it.
16453
+ */
16454
+ function extractAmpGuardedAllowlist(value) {
16455
+ if (!Array.isArray(value)) return void 0;
16456
+ return value.filter((entry) => typeof entry === "string");
16457
+ }
16458
+ /**
15913
16459
  * Read an `amp.permissions` array from untrusted parsed settings. Only entries
15914
- * whose shape rulesync understands are retained; the original objects are kept
15915
- * by reference (with a normalized `action`) so unknown sibling keys survive a
15916
- * round-trip when the entry is preserved.
16460
+ * whose `tool`/`action` rulesync understands are retained; every other key
16461
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16462
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16463
+ * round-trip.
15917
16464
  */
15918
16465
  function toPermissionsList(value) {
15919
16466
  if (!Array.isArray(value)) return [];
@@ -15923,14 +16470,11 @@ function toPermissionsList(value) {
15923
16470
  const { tool, action } = raw;
15924
16471
  if (typeof tool !== "string" || typeof action !== "string") continue;
15925
16472
  if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
15926
- const matches = raw.matches;
15927
- let normalizedMatches;
15928
- if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
15929
16473
  entries.push({
15930
16474
  ...raw,
15931
16475
  tool,
15932
16476
  action,
15933
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16477
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
15934
16478
  });
15935
16479
  }
15936
16480
  return entries;
@@ -15955,6 +16499,13 @@ function toPermissionsList(value) {
15955
16499
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
15956
16500
  * and writes merge into the existing JSON rather than overwriting it, and the
15957
16501
  * file is never deleted.
16502
+ *
16503
+ * Amp shapes with no canonical category are authored and round-tripped through
16504
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16505
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16506
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16507
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16508
+ * `amp.dangerouslyAllowAll` settings.
15958
16509
  */
15959
16510
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
15960
16511
  constructor(params) {
@@ -16024,15 +16575,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16024
16575
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
16025
16576
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
16026
16577
  const json = fileContent ? parseAmpSettings(fileContent) : {};
16027
- const { disable, permissions } = convertRulesyncToAmp(rulesyncPermissions.getJson());
16028
- const preservedDelegates = toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16578
+ const config = rulesyncPermissions.getJson();
16579
+ const { disable, permissions } = convertRulesyncToAmp(config);
16580
+ const override = config.amp;
16581
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16582
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16029
16583
  const newJson = {
16030
16584
  ...json,
16031
16585
  [AMP_TOOLS_DISABLE_KEY]: disable
16032
16586
  };
16033
- const mergedPermissions = [...permissions, ...preservedDelegates];
16587
+ const mergedPermissions = mergeAmpPermissions([
16588
+ ...permissions,
16589
+ ...authoredExtras,
16590
+ ...preservedDelegates
16591
+ ]);
16034
16592
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16035
16593
  else delete newJson[AMP_PERMISSIONS_KEY];
16594
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16595
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16596
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16036
16597
  return new AmpPermissions({
16037
16598
  outputRoot,
16038
16599
  relativeDirPath: basePaths.relativeDirPath,
@@ -16043,11 +16604,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16043
16604
  }
16044
16605
  toRulesyncPermissions() {
16045
16606
  const json = parseAmpSettings(this.getFileContent());
16607
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16608
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16609
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16046
16610
  const config = convertAmpToRulesync({
16047
16611
  disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
16048
- permissions: toPermissionsList(json[AMP_PERMISSIONS_KEY])
16049
- });
16050
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16612
+ permissions: canonicalEntries
16613
+ });
16614
+ const ampOverride = {};
16615
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16616
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16617
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16618
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16619
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16620
+ const result = { ...config };
16621
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16622
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16051
16623
  }
16052
16624
  validate() {
16053
16625
  try {
@@ -16094,6 +16666,37 @@ const ACTION_PRIORITY = {
16094
16666
  allow: 2
16095
16667
  };
16096
16668
  /**
16669
+ * Fail-closed action priority for merging the full `amp.permissions` list
16670
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16671
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16672
+ * it never auto-allows) trails as the final fallback.
16673
+ */
16674
+ const MERGE_ACTION_PRIORITY = {
16675
+ reject: 0,
16676
+ ask: 1,
16677
+ allow: 2,
16678
+ delegate: 3
16679
+ };
16680
+ /**
16681
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16682
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16683
+ * order within an action class. Guarantees an authored `reject` cannot be
16684
+ * shadowed by a generated catch-all `allow`.
16685
+ */
16686
+ function mergeAmpPermissions(entries) {
16687
+ const decorated = entries.map((entry, index) => ({
16688
+ entry,
16689
+ index
16690
+ }));
16691
+ decorated.sort((a, b) => {
16692
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16693
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16694
+ if (ap !== bp) return ap - bp;
16695
+ return a.index - b.index;
16696
+ });
16697
+ return decorated.map((d) => d.entry);
16698
+ }
16699
+ /**
16097
16700
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16098
16701
  *
16099
16702
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16158,14 +16761,14 @@ function sortAmpPermissions(entries) {
16158
16761
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16159
16762
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16160
16763
  if (ap !== bp) return ap - bp;
16161
- const aHasCmd = a.entry.matches?.cmd !== void 0 ? 0 : 1;
16162
- const bHasCmd = b.entry.matches?.cmd !== void 0 ? 0 : 1;
16764
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16765
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16163
16766
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16164
16767
  const at = a.entry.tool;
16165
16768
  const bt = b.entry.tool;
16166
16769
  if (at !== bt) return at < bt ? -1 : 1;
16167
- const ac = a.entry.matches?.cmd ?? "";
16168
- const bc = b.entry.matches?.cmd ?? "";
16770
+ const ac = ampMatchesCmd(a.entry) ?? "";
16771
+ const bc = ampMatchesCmd(b.entry) ?? "";
16169
16772
  if (ac !== bc) return ac < bc ? -1 : 1;
16170
16773
  return a.index - b.index;
16171
16774
  });
@@ -16204,7 +16807,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16204
16807
  for (const tool of disable) assign(tool, "*", "deny");
16205
16808
  for (const entry of permissions) {
16206
16809
  if (entry.action === "delegate") continue;
16207
- const pattern = entry.matches?.cmd ?? "*";
16810
+ const pattern = ampMatchesCmd(entry) ?? "*";
16208
16811
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16209
16812
  assign(entry.tool, pattern, action);
16210
16813
  }
@@ -16286,6 +16889,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16286
16889
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16287
16890
  * file (global scope only). The file holds other CLI settings besides
16288
16891
  * permissions, so it is never deleted.
16892
+ *
16893
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16894
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16895
+ * authored and round-tripped through the `antigravity-cli` permissions override
16896
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16289
16897
  */
16290
16898
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16291
16899
  constructor(params) {
@@ -16346,6 +16954,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16346
16954
  ...settings,
16347
16955
  permissions: mergedPermissions
16348
16956
  };
16957
+ const override = config["antigravity-cli"];
16958
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16959
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16349
16960
  const fileContent = JSON.stringify(merged, null, 2);
16350
16961
  return new AntigravityCliPermissions({
16351
16962
  outputRoot,
@@ -16369,7 +16980,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16369
16980
  ask: permissions.ask ?? [],
16370
16981
  deny: permissions.deny ?? []
16371
16982
  });
16372
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16983
+ const override = {};
16984
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
16985
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
16986
+ const result = { ...config };
16987
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
16988
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16373
16989
  }
16374
16990
  validate() {
16375
16991
  return {
@@ -17155,6 +17771,14 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17155
17771
  }
17156
17772
  const config = rulesyncPermissions.getJson();
17157
17773
  const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
17774
+ const overridePermissions = config.claudecode?.permissions;
17775
+ if (overridePermissions && typeof overridePermissions === "object") {
17776
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
17777
+ settings.permissions = {
17778
+ ...settings.permissions,
17779
+ ...nonListFields
17780
+ };
17781
+ }
17158
17782
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
17159
17783
  const merged = applyPermissions({
17160
17784
  settings,
@@ -17187,6 +17811,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17187
17811
  ask: permissions.ask ?? [],
17188
17812
  deny: permissions.deny ?? []
17189
17813
  });
17814
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
17815
+ if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
17190
17816
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17191
17817
  }
17192
17818
  validate() {
@@ -17360,7 +17986,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17360
17986
  } catch (error) {
17361
17987
  throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
17362
17988
  }
17363
- const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(rulesyncPermissions.getJson().permission);
17989
+ const config = rulesyncPermissions.getJson();
17990
+ const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
17364
17991
  warnClineTranslationNotices({
17365
17992
  droppedCategories,
17366
17993
  translatedAskPatterns,
@@ -17376,7 +18003,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17376
18003
  ...existing,
17377
18004
  allow: dedupedAllow,
17378
18005
  deny: mergedDeny,
17379
- allowRedirects: existing.allowRedirects ?? false
18006
+ allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
17380
18007
  };
17381
18008
  return new ClinePermissions({
17382
18009
  outputRoot,
@@ -17400,6 +18027,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17400
18027
  for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
17401
18028
  for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
17402
18029
  const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
18030
+ if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
17403
18031
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17404
18032
  }
17405
18033
  validate() {
@@ -17836,13 +18464,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
17836
18464
  WebFetch: "webfetch",
17837
18465
  Mcp: "mcp"
17838
18466
  };
17839
- const MCP_CANONICAL_PREFIX = "mcp__";
18467
+ const MCP_CANONICAL_PREFIX$1 = "mcp__";
17840
18468
  /**
17841
18469
  * Returns true if the canonical category is the per-tool MCP form
17842
18470
  * `mcp__<server>__<tool>`.
17843
18471
  */
17844
18472
  function isMcpScopedCategory(canonical) {
17845
- return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
18473
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
17846
18474
  }
17847
18475
  function toCursorType(canonical) {
17848
18476
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -17873,7 +18501,7 @@ function toCursorPattern(canonical, pattern) {
17873
18501
  function toCanonicalCategory$1(cursorType, pattern) {
17874
18502
  if (cursorType === "Mcp") {
17875
18503
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
17876
- if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
18504
+ if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
17877
18505
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
17878
18506
  }
17879
18507
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
@@ -18007,8 +18635,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
18007
18635
  mergedPermissions.allow = mergedAllow;
18008
18636
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
18009
18637
  else delete mergedPermissions.deny;
18638
+ const cursorOverride = config.cursor;
18010
18639
  const merged = {
18011
18640
  ...settings,
18641
+ ...cursorOverride,
18012
18642
  version: settings.version ?? 1,
18013
18643
  editor: {
18014
18644
  ...existingEditor,
@@ -18034,11 +18664,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
18034
18664
  }
18035
18665
  const permissionsRaw = settings.permissions;
18036
18666
  const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
18037
- const config = convertCursorToRulesyncPermissions({
18667
+ const result = { ...convertCursorToRulesyncPermissions({
18038
18668
  allow: asCursorPermissionEntryArray(permissions.allow),
18039
18669
  deny: asCursorPermissionEntryArray(permissions.deny)
18040
- });
18041
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18670
+ }) };
18671
+ const cursorOverride = {};
18672
+ if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
18673
+ if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
18674
+ if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
18675
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18042
18676
  }
18043
18677
  validate() {
18044
18678
  return {
@@ -18097,7 +18731,7 @@ function convertCursorToRulesyncPermissions(params) {
18097
18731
  const { type, pattern } = parseCursorPermissionEntry(entry);
18098
18732
  const canonical = toCanonicalCategory$1(type, pattern);
18099
18733
  if (!permission[canonical]) permission[canonical] = {};
18100
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
18734
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
18101
18735
  permission[canonical][canonicalPattern] = action;
18102
18736
  }
18103
18737
  };
@@ -18346,6 +18980,16 @@ function convertDevinToRulesyncPermissions(params) {
18346
18980
  }
18347
18981
  //#endregion
18348
18982
  //#region src/features/permissions/factorydroid-permissions.ts
18983
+ const FACTORYDROID_OVERRIDE_KEYS = [
18984
+ "commandBlocklist",
18985
+ "networkPolicy",
18986
+ "sandbox",
18987
+ "mcpPolicy",
18988
+ "enableDroidShield",
18989
+ "sessionDefaultSettings",
18990
+ "maxAutonomyLevel",
18991
+ "interactionMode"
18992
+ ];
18349
18993
  /**
18350
18994
  * Permissions adapter for Factory Droid.
18351
18995
  *
@@ -18363,18 +19007,14 @@ function convertDevinToRulesyncPermissions(params) {
18363
19007
  * skipped (with a warning when they carry `deny` rules, to surface the gap).
18364
19008
  *
18365
19009
  * Factory Droid also has a stronger `commandBlocklist` tier — commands that can
18366
- * never run, not even under full autonomy. rulesync's canonical action model
18367
- * has only `allow | ask | deny`, with no equivalent of a hard block that can
18368
- * never be approved. So on **import** a `commandBlocklist` entry is collapsed
18369
- * onto canonical `deny` (lossy: the never-runs guarantee is weakened to a deny
18370
- * the user can still approve), rather than being silently dropped. On **export**
18371
- * there is no canonical `block` to emit one from, so rulesync never writes
18372
- * `commandBlocklist`; an existing one on disk is preserved verbatim as an
18373
- * unmanaged key. (A consequence of the lossy collapse: importing a
18374
- * `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
18375
- * `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
18376
- * reconstructed. Re-running over the original file keeps it intact via the
18377
- * verbatim preservation above.)
19010
+ * never run, not even under full autonomy plus other security controls
19011
+ * (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
19012
+ * settings) that do not fit the canonical `allow | ask | deny` per-command
19013
+ * model. These are authored and round-tripped through the `factorydroid`
19014
+ * override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
19015
+ * they are lifted from `settings.json` into the override, and on **export** they
19016
+ * are merged back in so `commandBlocklist`'s never-runs guarantee is preserved
19017
+ * faithfully rather than being collapsed onto an approvable `deny`.
18378
19018
  */
18379
19019
  var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
18380
19020
  constructor(params) {
@@ -18417,11 +19057,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18417
19057
  } catch (error) {
18418
19058
  throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
18419
19059
  }
19060
+ const config = rulesyncPermissions.getJson();
18420
19061
  const { allow, deny } = convertRulesyncToFactorydroidPermissions({
18421
- config: rulesyncPermissions.getJson(),
19062
+ config,
18422
19063
  logger
18423
19064
  });
18424
- const merged = { ...settings };
19065
+ const override = config.factorydroid;
19066
+ const merged = {
19067
+ ...settings,
19068
+ ...override !== void 0 && typeof override === "object" ? override : {}
19069
+ };
18425
19070
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
18426
19071
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
18427
19072
  if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
@@ -18445,10 +19090,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18445
19090
  }
18446
19091
  const config = convertFactorydroidToRulesyncPermissions({
18447
19092
  allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
18448
- deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : [],
18449
- block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
19093
+ deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
18450
19094
  });
18451
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19095
+ const factorydroidOverride = {};
19096
+ for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
19097
+ const result = { ...config };
19098
+ if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
19099
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18452
19100
  }
18453
19101
  validate() {
18454
19102
  return {
@@ -18495,18 +19143,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
18495
19143
  };
18496
19144
  }
18497
19145
  /**
18498
- * Convert Factory Droid allow/deny/block command lists back to rulesync config
18499
- * under the `bash` category.
19146
+ * Convert Factory Droid allow/deny command lists back to rulesync config under
19147
+ * the `bash` category.
18500
19148
  *
18501
- * `commandBlocklist` (hard block) has no canonical equivalent, so it collapses
18502
- * onto `deny` lossy (a deny can still be approved), but preferable to dropping
18503
- * the rule entirely.
19149
+ * `commandBlocklist` (the hard-block tier) is no longer collapsed here it has
19150
+ * no canonical equivalent and now round-trips through the `factorydroid`
19151
+ * override so the never-runs guarantee is preserved instead of being weakened to
19152
+ * an approvable `deny`.
18504
19153
  */
18505
- function convertFactorydroidToRulesyncPermissions({ allow, deny, block }) {
19154
+ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
18506
19155
  const bash = {};
18507
19156
  for (const pattern of allow) bash[pattern] = "allow";
18508
19157
  for (const pattern of deny) bash[pattern] = "deny";
18509
- for (const pattern of block) bash[pattern] = "deny";
18510
19158
  return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
18511
19159
  }
18512
19160
  //#endregion
@@ -18700,30 +19348,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
18700
19348
  const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
18701
19349
  const GROKCLI_UI_KEY = "ui";
18702
19350
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
19351
+ const GROKCLI_PERMISSION_KEY = "permission";
18703
19352
  const CATCH_ALL_PATTERN$2 = "*";
19353
+ const MCP_CANONICAL_PREFIX = "mcp__";
19354
+ const CATEGORY_TO_GROK_TOOL = {
19355
+ bash: "Bash",
19356
+ read: "Read",
19357
+ edit: "Edit",
19358
+ write: "Edit",
19359
+ grep: "Grep",
19360
+ webfetch: "WebFetch"
19361
+ };
19362
+ const GROK_TOOL_TO_CATEGORY = {
19363
+ Bash: "bash",
19364
+ Read: "read",
19365
+ Edit: "edit",
19366
+ Grep: "grep",
19367
+ WebFetch: "webfetch"
19368
+ };
19369
+ const GROK_MCP_TOOL = "MCPTool";
19370
+ /**
19371
+ * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
19372
+ * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
19373
+ * for categories Grok cannot express so callers can skip them.
19374
+ *
19375
+ * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
19376
+ * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
19377
+ * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
19378
+ * concrete pattern emits `Tool(pattern)`.
19379
+ */
19380
+ function buildGrokEntry(category, pattern) {
19381
+ if (category.startsWith(MCP_CANONICAL_PREFIX)) {
19382
+ const remainder = category.slice(5);
19383
+ return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
19384
+ }
19385
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
19386
+ const tool = CATEGORY_TO_GROK_TOOL[category];
19387
+ if (tool === void 0) return null;
19388
+ return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
19389
+ }
19390
+ /**
19391
+ * Parse a Grok Claude-style permission entry back into a canonical category +
19392
+ * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
19393
+ * `null` for tool prefixes with no canonical equivalent (e.g. `any`).
19394
+ */
19395
+ function parseGrokEntry(entry) {
19396
+ const trimmed = entry.trim();
19397
+ const parenIndex = trimmed.indexOf("(");
19398
+ let tool;
19399
+ let inner;
19400
+ if (parenIndex === -1 || !trimmed.endsWith(")")) {
19401
+ tool = trimmed;
19402
+ inner = "";
19403
+ } else {
19404
+ tool = trimmed.slice(0, parenIndex);
19405
+ inner = trimmed.slice(parenIndex + 1, -1).trim();
19406
+ }
19407
+ if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
19408
+ category: `${MCP_CANONICAL_PREFIX}${inner}`,
19409
+ pattern: CATCH_ALL_PATTERN$2
19410
+ } : {
19411
+ category: "mcp",
19412
+ pattern: CATCH_ALL_PATTERN$2
19413
+ };
19414
+ const category = GROK_TOOL_TO_CATEGORY[tool];
19415
+ if (category === void 0) return null;
19416
+ return {
19417
+ category,
19418
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
19419
+ };
19420
+ }
18704
19421
  /**
18705
19422
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
18706
19423
  *
18707
- * Grok has no per-tool / per-pattern permission rules. Tool gating is a single
18708
- * coarse toggle, `[ui] permission_mode`, in `~/.grok/config.toml`:
18709
- * - `ask` prompt before each tool call (Grok's default).
18710
- * - `always-approve` skip prompts.
18711
- *
18712
- * rulesync's permission model is per-category, per-pattern `allow`/`ask`/`deny`,
18713
- * so the mapping is **lossy** (a single mode cannot express per-pattern rules):
18714
- * - Generate: any `deny` or `ask` rule anywhere `ask` (conservative — keep
18715
- * prompting whenever the user expressed any restriction); otherwise, if at
18716
- * least one `allow` rule exists and nothing is denied/asked
18717
- * `always-approve`; an empty config falls back to the safe default `ask`.
18718
- * - Import: `always-approve` `bash: { "*": "allow" }`; `ask` (or unset)
18719
- * `bash: { "*": "ask" }`. `bash` is used as the representative catch-all
18720
- * because it is the primary permission-gated surface, and this round-trips
18721
- * the generate mapping.
18722
- *
18723
- * This surface is **global only** `permission_mode` lives in the user-level
18724
- * `~/.grok/config.toml`; Grok has no project-scoped permission file. The shared
18725
- * config is merged in place: the `[ui] permission_mode` value is set and every
18726
- * other key (e.g. `[mcp_servers]`, legacy `approval_mode`) is preserved. The
19424
+ * Grok Build CLI ships a Claude-style rule system under `[permission]` in
19425
+ * `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
19426
+ * `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
19427
+ * `WebFetch`, evaluated with precedence `deny > ask > allow`
19428
+ * (https://docs.x.ai/build/settings/reference). rulesync's canonical
19429
+ * per-category, per-pattern model maps almost 1:1:
19430
+ * - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
19431
+ * the matching Grok entry and is bucketed into the `[permission]` array for
19432
+ * that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
19433
+ * `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
19434
+ * `MCPTool(...)` (a scoped MCP category folds its address into the
19435
+ * parentheses, so a non-`*` argument pattern on it is not represented).
19436
+ * Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
19437
+ * `agent`) are skipped (with a warning when they carry a `deny` rule, to
19438
+ * surface the gap). When two canonical rules collapse onto the same Grok
19439
+ * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
19440
+ * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
19441
+ * entry never lands contradictorily in two arrays.
19442
+ * - Import: the `[permission]` arrays are parsed back into canonical
19443
+ * categories. When no `[permission]` section is present (older configs), the
19444
+ * coarse `[ui] permission_mode` is used as a fallback.
19445
+ *
19446
+ * The coarse `[ui] permission_mode` toggle is still written for backward
19447
+ * compatibility with older Grok versions: `always-approve` when the config is
19448
+ * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
19449
+ * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
19450
+ * arrays).
19451
+ *
19452
+ * This surface is **global only** — the adapter syncs the user-level
19453
+ * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
19454
+ * file; project scope is not modeled here.) The shared config is merged in
19455
+ * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
19456
+ * tools it models and the `[ui] permission_mode` value, while every other key
19457
+ * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
19458
+ * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
18727
19459
  * file is never deleted.
18728
19460
  */
18729
19461
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -18755,7 +19487,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18755
19487
  global: true
18756
19488
  });
18757
19489
  }
18758
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
19490
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18759
19491
  if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
18760
19492
  const paths = GrokcliPermissions.getSettablePaths({ global });
18761
19493
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -18766,7 +19498,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18766
19498
  } catch (error) {
18767
19499
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
18768
19500
  }
18769
- const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
19501
+ const config = rulesyncPermissions.getJson();
19502
+ const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19503
+ const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19504
+ parsed[GROKCLI_PERMISSION_KEY] = {
19505
+ ...existingPermission,
19506
+ allow: buckets.allow,
19507
+ deny: buckets.deny,
19508
+ ask: buckets.ask
19509
+ };
19510
+ const mode = deriveGrokPermissionMode(config);
18770
19511
  const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18771
19512
  parsed[GROKCLI_UI_KEY] = {
18772
19513
  ...existingUi,
@@ -18789,8 +19530,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18789
19530
  } catch (error) {
18790
19531
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18791
19532
  }
18792
- const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18793
- const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
19533
+ const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
19534
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
18794
19535
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18795
19536
  }
18796
19537
  validate() {
@@ -18810,6 +19551,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18810
19551
  });
18811
19552
  }
18812
19553
  };
19554
+ const ACTION_RANK = {
19555
+ allow: 0,
19556
+ ask: 1,
19557
+ deny: 2
19558
+ };
19559
+ /**
19560
+ * Collect user-authored entries from an existing `[permission]` array whose
19561
+ * tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
19562
+ * preserved verbatim so replacing the arrays does not silently drop them
19563
+ * (mirrors the Cursor adapter's preservation of unmanaged types).
19564
+ */
19565
+ function unmanagedEntries(existingPermission, key) {
19566
+ return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
19567
+ }
19568
+ /**
19569
+ * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
19570
+ * Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
19571
+ * Categories Grok cannot express are skipped (with a warning when they carry a
19572
+ * `deny` rule); entries the user authored for tools rulesync cannot model are
19573
+ * preserved from `existingPermission`.
19574
+ */
19575
+ function buildGrokPermissionArrays(config, existingPermission, logger) {
19576
+ const ranked = /* @__PURE__ */ new Map();
19577
+ for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19578
+ const entry = buildGrokEntry(category, pattern);
19579
+ if (entry === null) {
19580
+ 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.`);
19581
+ continue;
19582
+ }
19583
+ const existing = ranked.get(entry);
19584
+ 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).`);
19585
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
19586
+ }
19587
+ const allow = unmanagedEntries(existingPermission, "allow");
19588
+ const deny = unmanagedEntries(existingPermission, "deny");
19589
+ const ask = unmanagedEntries(existingPermission, "ask");
19590
+ for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
19591
+ else if (action === "deny") deny.push(entry);
19592
+ else ask.push(entry);
19593
+ return {
19594
+ allow: (0, es_toolkit.uniq)(allow.toSorted()),
19595
+ deny: (0, es_toolkit.uniq)(deny.toSorted()),
19596
+ ask: (0, es_toolkit.uniq)(ask.toSorted())
19597
+ };
19598
+ }
19599
+ /**
19600
+ * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
19601
+ * permission map. Returns `null` when the section defines none of the three
19602
+ * arrays, so the caller can fall back to the coarse `permission_mode`.
19603
+ * Precedence `deny > ask > allow` is applied so a tool listed in multiple
19604
+ * arrays resolves to the strictest action.
19605
+ */
19606
+ function parseGrokPermissionArrays(permission) {
19607
+ const allow = isStringArray(permission.allow) ? permission.allow : void 0;
19608
+ const deny = isStringArray(permission.deny) ? permission.deny : void 0;
19609
+ const ask = isStringArray(permission.ask) ? permission.ask : void 0;
19610
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
19611
+ const result = {};
19612
+ const apply = (entries, action) => {
19613
+ for (const entry of entries ?? []) {
19614
+ const parsed = parseGrokEntry(entry);
19615
+ if (parsed === null) continue;
19616
+ const bucket = result[parsed.category] ??= {};
19617
+ bucket[parsed.pattern] = action;
19618
+ }
19619
+ };
19620
+ apply(allow, "allow");
19621
+ apply(ask, "ask");
19622
+ apply(deny, "deny");
19623
+ return result;
19624
+ }
19625
+ /**
19626
+ * Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
19627
+ * `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
19628
+ */
19629
+ function legacyModeAction(parsed) {
19630
+ return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
19631
+ }
18813
19632
  /**
18814
19633
  * Collapse a rulesync permissions config into Grok's single coarse mode.
18815
19634
  * Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
@@ -18825,6 +19644,10 @@ function deriveGrokPermissionMode(config) {
18825
19644
  }
18826
19645
  //#endregion
18827
19646
  //#region src/features/permissions/hermesagent-permissions.ts
19647
+ /** Collect the glob patterns in a canonical category that carry a given action. */
19648
+ function patternsByAction(category, action) {
19649
+ return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
19650
+ }
18828
19651
  var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
18829
19652
  static getSettablePaths() {
18830
19653
  return {
@@ -18848,10 +19671,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18848
19671
  return true;
18849
19672
  }
18850
19673
  setFileContent(fileContent) {
18851
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
19674
+ this.fileContent = applySharedConfigPatch({
19675
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19676
+ feature: "permissions",
19677
+ existingContent: fileContent,
19678
+ patch: parseSharedConfig({
19679
+ format: "yaml",
19680
+ fileContent: this.fileContent
19681
+ })
19682
+ });
18852
19683
  }
18853
19684
  toRulesyncPermissions() {
18854
- const config = parseHermesConfig(this.getFileContent());
19685
+ const config = parseSharedConfig({
19686
+ format: "yaml",
19687
+ fileContent: this.getFileContent()
19688
+ });
18855
19689
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
18856
19690
  return new RulesyncPermissions({
18857
19691
  relativeDirPath: "",
@@ -18861,11 +19695,27 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18861
19695
  }
18862
19696
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
18863
19697
  const permissions = rulesyncPermissions.getJson();
19698
+ const permissionBlock = permissions.permission ?? {};
19699
+ const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
19700
+ const bashDeny = patternsByAction(permissionBlock.bash, "deny");
19701
+ const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
19702
+ let config = {};
19703
+ if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
19704
+ if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
19705
+ if (webfetchDeny.length > 0) config.security = { website_blocklist: {
19706
+ enabled: true,
19707
+ domains: webfetchDeny
19708
+ } };
19709
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19710
+ base: config,
19711
+ patch: permissions.hermes
19712
+ });
19713
+ config.permissions = { rulesync: permissions };
18864
19714
  return new HermesagentPermissions({
18865
19715
  outputRoot,
18866
- fileContent: stringifyHermesConfig({
18867
- command_allowlist: Object.entries(permissions.permission ?? {}).flatMap(([, patterns]) => Object.entries(patterns).filter(([, action]) => action === "allow").map(([command]) => command)),
18868
- permissions: { rulesync: permissions }
19716
+ fileContent: stringifySharedConfig({
19717
+ format: "yaml",
19718
+ document: config
18869
19719
  })
18870
19720
  });
18871
19721
  }
@@ -18887,14 +19737,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18887
19737
  * "executables": [ { "prefix": "git ", "action": "allow" } ],
18888
19738
  * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18889
19739
  * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18890
- * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
19740
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
18891
19741
  * }
18892
19742
  * }
18893
19743
  * ```
18894
19744
  *
18895
19745
  * Each rule carries a literal `prefix` (matches commands that start with it) or
18896
- * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18897
- * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
19746
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
19747
+ * documents only `allow` and `ask` as valid actions there is **no `deny`**
19748
+ * (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
19749
+ * canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
19750
+ * and is mapped to the nearest valid action, `ask` (still withholds
19751
+ * auto-approval), with a warning so the downgrade is surfaced.
18898
19752
  *
18899
19753
  * Category mapping (rulesync canonical <-> Junie rule group):
18900
19754
  * - `bash` <-> `executables`
@@ -18904,8 +19758,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18904
19758
  *
18905
19759
  * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18906
19760
  * (with a warning when they carry rules). The top-level `defaultBehavior` and
18907
- * `allowReadonlyCommands` settings have no canonical equivalent: they are
18908
- * preserved verbatim on export but not imported into the rulesync model.
19761
+ * `allowReadonlyCommands` settings have no canonical per-glob slot: they are
19762
+ * authored and round-tripped through the `junie` override namespace (see
19763
+ * `JuniePermissionsOverrideSchema`) — lifted into the override on import and
19764
+ * merged back onto the top level on export — and any other unmodeled top-level
19765
+ * key is preserved verbatim.
18909
19766
  *
18910
19767
  * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18911
19768
  */
@@ -18928,7 +19785,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
18928
19785
  mcpTools: "mcp",
18929
19786
  readOutsideProject: "read"
18930
19787
  };
18931
- const JUNIE_DEFAULT_BEHAVIOR = "ask";
18932
19788
  function isPermissionAction$1(value) {
18933
19789
  return PermissionActionSchema.safeParse(value).success;
18934
19790
  }
@@ -18978,13 +19834,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18978
19834
  } catch (error) {
18979
19835
  throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18980
19836
  }
19837
+ const config = rulesyncPermissions.getJson();
18981
19838
  const rules = convertRulesyncToJunieRules({
18982
- config: rulesyncPermissions.getJson(),
19839
+ config,
18983
19840
  logger
18984
19841
  });
19842
+ const override = config.junie;
19843
+ const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
18985
19844
  const merged = {
18986
19845
  ...existing,
18987
- defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
19846
+ ...overrideObj,
18988
19847
  rules
18989
19848
  };
18990
19849
  return new JuniePermissions({
@@ -19004,7 +19863,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
19004
19863
  throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
19005
19864
  }
19006
19865
  const config = convertJunieToRulesyncPermissions({ allowlist });
19007
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19866
+ const junieOverride = {};
19867
+ if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
19868
+ if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
19869
+ const result = { ...config };
19870
+ if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
19871
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19008
19872
  }
19009
19873
  validate() {
19010
19874
  return {
@@ -19036,12 +19900,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
19036
19900
  continue;
19037
19901
  }
19038
19902
  for (const [pattern, action] of Object.entries(patterns)) {
19903
+ const junieAction = toJunieAction(action, category, pattern, logger);
19039
19904
  const rule = isGlobPattern(pattern) ? {
19040
19905
  pattern,
19041
- action
19906
+ action: junieAction
19042
19907
  } : {
19043
19908
  prefix: pattern,
19044
- action
19909
+ action: junieAction
19045
19910
  };
19046
19911
  (rules[group] ??= []).push(rule);
19047
19912
  }
@@ -19049,6 +19914,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
19049
19914
  return rules;
19050
19915
  }
19051
19916
  /**
19917
+ * Map a canonical action onto a valid Junie allowlist action. Junie supports
19918
+ * only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
19919
+ * nearest valid action — both withhold auto-approval) with a warning, so
19920
+ * rulesync never emits a `deny` that Junie would silently ignore.
19921
+ */
19922
+ function toJunieAction(action, category, pattern, logger) {
19923
+ if (action === "deny") {
19924
+ logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
19925
+ return "ask";
19926
+ }
19927
+ return action;
19928
+ }
19929
+ /**
19052
19930
  * Convert a Junie allowlist back into rulesync permissions config. The
19053
19931
  * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
19054
19932
  * canonical equivalent and are not imported.
@@ -19082,6 +19960,31 @@ const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
19082
19960
  ]))]);
19083
19961
  const KiloPermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)) });
19084
19962
  /**
19963
+ * Kilo permission keys that share a name with a canonical rulesync category and
19964
+ * therefore stay in the shared `permission` block. Everything else (Kilo-only
19965
+ * keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
19966
+ * routed into the `kilo` override on import so it does not leak into other
19967
+ * tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
19968
+ * rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
19969
+ * addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
19970
+ * keys in practice — they simply never appear on the shared side.
19971
+ */
19972
+ const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
19973
+ "bash",
19974
+ "read",
19975
+ "edit",
19976
+ "write",
19977
+ "webfetch",
19978
+ "websearch",
19979
+ "grep",
19980
+ "glob",
19981
+ "notebookedit",
19982
+ "agent"
19983
+ ]);
19984
+ function isSharedKiloCategory(key) {
19985
+ return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
19986
+ }
19987
+ /**
19085
19988
  * Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
19086
19989
  * non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
19087
19990
  * coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
@@ -19193,12 +20096,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19193
20096
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
19194
20097
  const parsedPermission = parsed.permission;
19195
20098
  const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
19196
- const rulesyncPermission = rulesyncPermissions.getJson().permission;
20099
+ const rulesyncJson = rulesyncPermissions.getJson();
20100
+ const kiloOverride = rulesyncJson.kilo;
20101
+ const incomingPermission = {
20102
+ ...rulesyncJson.permission,
20103
+ ...kiloOverride?.permission
20104
+ };
19197
20105
  const droppedDenyByKey = {};
19198
- for (const key of Object.keys(rulesyncPermission)) {
19199
- const previous = existingPermission[key];
19200
- const previousDenyPatterns = collectKiloDenyPatterns(previous);
19201
- const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
20106
+ for (const [key, value] of Object.entries(incomingPermission)) {
20107
+ const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
20108
+ const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
19202
20109
  const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
19203
20110
  if (dropped.length > 0) droppedDenyByKey[key] = dropped;
19204
20111
  }
@@ -19206,8 +20113,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19206
20113
  const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
19207
20114
  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'.`);
19208
20115
  }
19209
- const mergedPermission = { ...existingPermission };
19210
- for (const [key, value] of Object.entries(rulesyncPermission)) mergedPermission[key] = value;
20116
+ const mergedPermission = {
20117
+ ...existingPermission,
20118
+ ...incomingPermission
20119
+ };
19211
20120
  const nextJson = {
19212
20121
  ...parsed,
19213
20122
  permission: mergedPermission
@@ -19221,8 +20130,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19221
20130
  });
19222
20131
  }
19223
20132
  toRulesyncPermissions() {
19224
- const permission = this.normalizePermission(this.json.permission);
19225
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20133
+ const rawPermission = this.json.permission ?? {};
20134
+ const shared = {};
20135
+ const overrideOnly = {};
20136
+ for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
20137
+ else overrideOnly[key] = value;
20138
+ const json = Object.keys(overrideOnly).length > 0 ? {
20139
+ permission: shared,
20140
+ kilo: { permission: overrideOnly }
20141
+ } : { permission: shared };
20142
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19226
20143
  }
19227
20144
  validate() {
19228
20145
  try {
@@ -19252,10 +20169,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19252
20169
  validate: false
19253
20170
  });
19254
20171
  }
19255
- normalizePermission(permission) {
19256
- if (!permission) return {};
19257
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
19258
- }
19259
20172
  };
19260
20173
  //#endregion
19261
20174
  //#region src/features/permissions/kiro-permissions.ts
@@ -19313,29 +20226,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19313
20226
  }
19314
20227
  const permission = {};
19315
20228
  const toolsSettings = parsed.toolsSettings ?? {};
19316
- const shellSettings = asRecord$1(toolsSettings.shell);
19317
- const shellAllow = asStringArray(shellSettings.allowedCommands);
19318
- const shellDeny = asStringArray(shellSettings.deniedCommands);
19319
- if (shellAllow.length > 0 || shellDeny.length > 0) {
19320
- permission.bash = {};
19321
- for (const pattern of shellAllow) permission.bash[pattern] = "allow";
19322
- for (const pattern of shellDeny) permission.bash[pattern] = "deny";
19323
- }
19324
- const readSettings = asRecord$1(toolsSettings.read);
19325
- const readAllow = asStringArray(readSettings.allowedPaths);
19326
- const readDeny = asStringArray(readSettings.deniedPaths);
19327
- if (readAllow.length > 0 || readDeny.length > 0) {
19328
- permission.read = {};
19329
- for (const pattern of readAllow) permission.read[pattern] = "allow";
19330
- for (const pattern of readDeny) permission.read[pattern] = "deny";
19331
- }
19332
- const writeSettings = asRecord$1(toolsSettings.write);
19333
- const writeAllow = asStringArray(writeSettings.allowedPaths);
19334
- const writeDeny = asStringArray(writeSettings.deniedPaths);
19335
- if (writeAllow.length > 0 || writeDeny.length > 0) {
19336
- permission.write = {};
19337
- for (const pattern of writeAllow) permission.write[pattern] = "allow";
19338
- for (const pattern of writeDeny) permission.write[pattern] = "deny";
20229
+ const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
20230
+ if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
20231
+ for (const category of [
20232
+ "read",
20233
+ "write",
20234
+ "grep",
20235
+ "glob"
20236
+ ]) {
20237
+ const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
20238
+ if (Object.keys(rules).length > 0) permission[category] = rules;
19339
20239
  }
19340
20240
  const allowedTools = new Set(parsed.allowedTools ?? []);
19341
20241
  if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
@@ -19361,45 +20261,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19361
20261
  function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
19362
20262
  const nextAllowedTools = new Set(existing.allowedTools ?? []);
19363
20263
  const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
20264
+ const pathBuckets = {};
20265
+ const pushPath = (key, action, pattern) => {
20266
+ const bucket = pathBuckets[key] ??= {
20267
+ allow: [],
20268
+ deny: []
20269
+ };
20270
+ (action === "allow" ? bucket.allow : bucket.deny).push(pattern);
20271
+ };
19364
20272
  const shell = {
19365
20273
  allowedCommands: [],
19366
20274
  deniedCommands: []
19367
20275
  };
19368
- const read = {
19369
- allowedPaths: [],
19370
- deniedPaths: []
19371
- };
19372
- const write = {
19373
- allowedPaths: [],
19374
- deniedPaths: []
19375
- };
19376
20276
  for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19377
20277
  if (action === "ask") {
19378
20278
  logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
19379
20279
  continue;
19380
20280
  }
19381
20281
  if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
19382
- else if (category === "read") (action === "allow" ? read.allowedPaths : read.deniedPaths).push(pattern);
19383
- else if (category === "edit" || category === "write") (action === "allow" ? write.allowedPaths : write.deniedPaths).push(pattern);
19384
- else if (category === "webfetch" || category === "websearch") {
19385
- if (pattern !== "*") {
19386
- logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
19387
- continue;
19388
- }
19389
- const toolName = category === "webfetch" ? "web_fetch" : "web_search";
19390
- if (action === "allow") nextAllowedTools.add(toolName);
19391
- else nextAllowedTools.delete(toolName);
19392
- } else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
20282
+ else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
20283
+ else if (category === "edit" || category === "write") pushPath("write", action, pattern);
20284
+ else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
20285
+ category,
20286
+ pattern,
20287
+ action,
20288
+ nextAllowedTools,
20289
+ logger
20290
+ });
20291
+ else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
19393
20292
  }
19394
20293
  nextToolsSettings.shell = shell;
19395
- nextToolsSettings.read = read;
19396
- nextToolsSettings.write = write;
20294
+ nextToolsSettings.read = pathTable(pathBuckets.read);
20295
+ nextToolsSettings.write = pathTable(pathBuckets.write);
20296
+ for (const key of ["grep", "glob"]) {
20297
+ const bucket = pathBuckets[key];
20298
+ if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
20299
+ }
19397
20300
  return {
19398
20301
  ...existing,
19399
20302
  allowedTools: [...nextAllowedTools].toSorted(),
19400
20303
  toolsSettings: nextToolsSettings
19401
20304
  };
19402
20305
  }
20306
+ function pathTable(bucket) {
20307
+ return {
20308
+ allowedPaths: bucket?.allow ?? [],
20309
+ deniedPaths: bucket?.deny ?? []
20310
+ };
20311
+ }
20312
+ function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
20313
+ if (pattern !== "*") {
20314
+ logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
20315
+ return;
20316
+ }
20317
+ const toolName = category === "webfetch" ? "web_fetch" : "web_search";
20318
+ if (action === "allow") nextAllowedTools.add(toolName);
20319
+ else nextAllowedTools.delete(toolName);
20320
+ }
19403
20321
  function asRecord$1(value) {
19404
20322
  const result = UnknownRecordSchema.safeParse(value);
19405
20323
  return result.success ? result.data : {};
@@ -19407,6 +20325,17 @@ function asRecord$1(value) {
19407
20325
  function asStringArray(value) {
19408
20326
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19409
20327
  }
20328
+ /**
20329
+ * Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
20330
+ * allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
20331
+ * `allowedCommands`/`deniedCommands`).
20332
+ */
20333
+ function rulesFromArrays(settings, allowKey, denyKey) {
20334
+ const rules = {};
20335
+ for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
20336
+ for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
20337
+ return rules;
20338
+ }
19410
20339
  //#endregion
19411
20340
  //#region src/features/permissions/opencode-permissions.ts
19412
20341
  const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
@@ -19418,6 +20347,28 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
19418
20347
  "ask",
19419
20348
  "deny"
19420
20349
  ]))]);
20350
+ /**
20351
+ * Canonical rulesync permission categories that carry a cross-tool meaning (see
20352
+ * the "Supported tool categories" list in `docs/reference/file-formats.md`).
20353
+ * On import, any OpenCode category outside this set — plus MCP tool names — is
20354
+ * treated as OpenCode-only and routed into the `opencode` override block so a
20355
+ * subsequent `rulesync generate` does not leak it into other tools' configs.
20356
+ */
20357
+ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
20358
+ "bash",
20359
+ "read",
20360
+ "edit",
20361
+ "write",
20362
+ "webfetch",
20363
+ "websearch",
20364
+ "grep",
20365
+ "glob",
20366
+ "notebookedit",
20367
+ "agent"
20368
+ ]);
20369
+ function isSharedPermissionCategory(category) {
20370
+ return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
20371
+ }
19421
20372
  const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
19422
20373
  "allow",
19423
20374
  "ask",
@@ -19479,9 +20430,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19479
20430
  fileContent = await readFileContentOrNull(jsonPath);
19480
20431
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
19481
20432
  }
20433
+ const parsed = (0, jsonc_parser.parse)(fileContent ?? "{}");
20434
+ const rulesyncJson = rulesyncPermissions.getJson();
20435
+ const overridePermission = rulesyncJson.opencode?.permission ?? {};
19482
20436
  const nextJson = {
19483
- ...(0, jsonc_parser.parse)(fileContent ?? "{}"),
19484
- permission: rulesyncPermissions.getJson().permission
20437
+ ...parsed,
20438
+ permission: {
20439
+ ...rulesyncJson.permission,
20440
+ ...overridePermission
20441
+ }
19485
20442
  };
19486
20443
  return new OpencodePermissions({
19487
20444
  outputRoot,
@@ -19492,8 +20449,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19492
20449
  });
19493
20450
  }
19494
20451
  toRulesyncPermissions() {
19495
- const permission = this.normalizePermission(this.json.permission);
19496
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20452
+ const rawPermission = this.json.permission;
20453
+ if (rawPermission === void 0 || typeof rawPermission === "string") {
20454
+ const permission = this.normalizePermission(rawPermission);
20455
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20456
+ }
20457
+ const shared = {};
20458
+ const overrideOnly = {};
20459
+ for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
20460
+ else overrideOnly[category] = value;
20461
+ const json = Object.keys(overrideOnly).length > 0 ? {
20462
+ permission: shared,
20463
+ opencode: { permission: overrideOnly }
20464
+ } : { permission: shared };
20465
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19497
20466
  }
19498
20467
  validate() {
19499
20468
  try {
@@ -19523,10 +20492,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19523
20492
  validate: false
19524
20493
  });
19525
20494
  }
20495
+ /**
20496
+ * Normalize the uniform/undefined forms of OpenCode's `permission` field into
20497
+ * the canonical rulesync shape. The object form is handled directly in
20498
+ * `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
20499
+ * categories), so this only covers the two remaining cases.
20500
+ */
19526
20501
  normalizePermission(permission) {
19527
20502
  if (!permission) return {};
19528
- if (typeof permission === "string") return { "*": { "*": permission } };
19529
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
20503
+ return { "*": { "*": permission } };
19530
20504
  }
19531
20505
  };
19532
20506
  //#endregion
@@ -19600,6 +20574,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
19600
20574
  if (pattern === "*") return toolName;
19601
20575
  return `${toolName}(${pattern})`;
19602
20576
  }
20577
+ const QWEN_OVERRIDE_TOOLS_KEYS = [
20578
+ "approvalMode",
20579
+ "autoAccept",
20580
+ "sandbox",
20581
+ "sandboxImage",
20582
+ "disabled"
20583
+ ];
20584
+ const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
20585
+ function asPlainRecord(value) {
20586
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20587
+ }
20588
+ /** Pick the override-managed keys out of a settings group into a fresh record. */
20589
+ function pickQwenOverrideKeys(group, keys) {
20590
+ const source = asPlainRecord(group);
20591
+ const picked = {};
20592
+ for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
20593
+ return picked;
20594
+ }
19603
20595
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19604
20596
  constructor(params) {
19605
20597
  super({
@@ -19665,6 +20657,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19665
20657
  ...settings,
19666
20658
  permissions: mergedPermissions
19667
20659
  };
20660
+ const override = config.qwencode;
20661
+ if (override?.tools !== void 0) merged.tools = {
20662
+ ...asPlainRecord(settings.tools),
20663
+ ...asPlainRecord(override.tools)
20664
+ };
20665
+ if (override?.security !== void 0) merged.security = {
20666
+ ...asPlainRecord(settings.security),
20667
+ ...asPlainRecord(override.security)
20668
+ };
19668
20669
  const fileContent = JSON.stringify(merged, null, 2);
19669
20670
  return new QwencodePermissions({
19670
20671
  outputRoot,
@@ -19690,7 +20691,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19690
20691
  ask: permissions.ask ?? [],
19691
20692
  deny: permissions.deny ?? []
19692
20693
  });
19693
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20694
+ const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
20695
+ const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
20696
+ const qwencodeOverride = {};
20697
+ if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
20698
+ if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
20699
+ const result = { ...config };
20700
+ if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
20701
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19694
20702
  }
19695
20703
  validate() {
19696
20704
  try {
@@ -19851,6 +20859,16 @@ function toPermissionsTable(value) {
19851
20859
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19852
20860
  return { ...value };
19853
20861
  }
20862
+ const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
20863
+ function asReasonixRecord(value) {
20864
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20865
+ }
20866
+ function pickReasonixKeys(source, keys) {
20867
+ const record = asReasonixRecord(source);
20868
+ const picked = {};
20869
+ for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
20870
+ return picked;
20871
+ }
19854
20872
  var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19855
20873
  toml;
19856
20874
  constructor(params) {
@@ -19912,6 +20930,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19912
20930
  ...parsed,
19913
20931
  permissions: mergedPermissions
19914
20932
  };
20933
+ const override = config.reasonix;
20934
+ if (override?.sandbox !== void 0) merged.sandbox = {
20935
+ ...asReasonixRecord(parsed.sandbox),
20936
+ ...asReasonixRecord(override.sandbox)
20937
+ };
20938
+ if (override?.agent !== void 0) merged.agent = {
20939
+ ...asReasonixRecord(parsed.agent),
20940
+ ...asReasonixRecord(override.agent)
20941
+ };
19915
20942
  const fileContent = smol_toml.stringify(merged);
19916
20943
  return new ReasonixPermissions({
19917
20944
  outputRoot,
@@ -19928,7 +20955,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19928
20955
  ask: toStringArray$1(permissions.ask),
19929
20956
  deny: toStringArray$1(permissions.deny)
19930
20957
  });
19931
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20958
+ const sandbox = asReasonixRecord(this.toml.sandbox);
20959
+ const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
20960
+ const reasonixOverride = {};
20961
+ if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
20962
+ if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
20963
+ const result = { ...config };
20964
+ if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
20965
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19932
20966
  }
19933
20967
  validate() {
19934
20968
  try {
@@ -20238,6 +21272,8 @@ function isPermissionAction(value) {
20238
21272
  const TAKT_PROVIDER_KEY = "provider";
20239
21273
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20240
21274
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21275
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21276
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20241
21277
  const TAKT_DEFAULT_PROVIDER = "claude";
20242
21278
  const CATCH_ALL_PATTERN = "*";
20243
21279
  /**
@@ -20267,10 +21303,16 @@ const CATCH_ALL_PATTERN = "*";
20267
21303
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20268
21304
  *
20269
21305
  * Both project and global scope are supported. The shared config is merged in
20270
- * place: only `provider_profiles.<provider>.default_permission_mode` is set;
20271
- * the active provider's other keys (e.g. `step_permission_overrides`), every
20272
- * other provider profile, and all other top-level keys are preserved. The file
20273
- * is never deleted.
21306
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21307
+ * derived mode; every other provider profile and all other top-level keys are
21308
+ * preserved. The file is never deleted.
21309
+ *
21310
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21311
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21312
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21313
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21314
+ * (a top-level per-provider sandbox/network table). Both are authored on
21315
+ * generate and re-extracted on import.
20274
21316
  */
20275
21317
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20276
21318
  constructor(params) {
@@ -20302,37 +21344,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20302
21344
  }
20303
21345
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20304
21346
  const paths = TaktPermissions.getSettablePaths({ global });
20305
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21347
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21348
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21349
+ const config = parseSharedConfig({
21350
+ format: "yaml",
21351
+ fileContent: existingContent,
21352
+ filePath,
21353
+ invalidRootPolicy: "error"
21354
+ });
21355
+ const rulesyncJson = rulesyncPermissions.getJson();
20306
21356
  const provider = resolveActiveProvider(config);
20307
- const mode = deriveTaktPermissionMode(rulesyncPermissions.getJson());
20308
- const existingProfiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20309
- const existingProfile = isPlainObject(existingProfiles[provider]) ? existingProfiles[provider] : {};
20310
- const merged = {
20311
- ...config,
20312
- [TAKT_PROVIDER_PROFILES_KEY]: {
20313
- ...existingProfiles,
20314
- [provider]: {
20315
- ...existingProfile,
20316
- [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode
20317
- }
20318
- }
21357
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21358
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21359
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21360
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21361
+ const patch = {
21362
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21363
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21364
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21365
+ } },
21366
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
20319
21367
  };
20320
21368
  return new TaktPermissions({
20321
21369
  outputRoot,
20322
21370
  relativeDirPath: paths.relativeDirPath,
20323
21371
  relativeFilePath: paths.relativeFilePath,
20324
- fileContent: (0, js_yaml.dump)(merged),
21372
+ fileContent: applySharedConfigPatch({
21373
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21374
+ feature: "permissions",
21375
+ existingContent,
21376
+ patch,
21377
+ filePath
21378
+ }),
20325
21379
  validate: true,
20326
21380
  global
20327
21381
  });
20328
21382
  }
20329
21383
  toRulesyncPermissions() {
20330
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21384
+ const config = parseSharedConfig({
21385
+ format: "yaml",
21386
+ fileContent: this.getFileContent(),
21387
+ filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
21388
+ invalidRootPolicy: "error"
21389
+ });
20331
21390
  const provider = resolveActiveProvider(config);
20332
21391
  const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20333
- const mode = (isPlainObject(profiles[provider]) ? profiles[provider] : {})[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21392
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21393
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
20334
21394
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
20335
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21395
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21396
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21397
+ const taktOverride = {};
21398
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21399
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21400
+ const result = { ...rulesyncConfig };
21401
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21402
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20336
21403
  }
20337
21404
  validate() {
20338
21405
  return {
@@ -20492,6 +21559,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20492
21559
  if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
20493
21560
  tools[vibeToolName] = nextTool;
20494
21561
  }
21562
+ applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
20495
21563
  config.tools = tools;
20496
21564
  if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
20497
21565
  else delete config.enabled_tools;
@@ -20508,16 +21576,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20508
21576
  }
20509
21577
  toRulesyncPermissions() {
20510
21578
  const permission = {};
21579
+ const vibeOverridePermission = {};
20511
21580
  for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
20512
21581
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
20513
21582
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
20514
- const rules = ensurePermission(permission, toCanonicalToolName$1(vibeToolName));
21583
+ const category = toCanonicalToolName$1(vibeToolName);
21584
+ const rules = ensurePermission(permission, category);
20515
21585
  const action = fromVibePermission(toolConfig.permission);
20516
21586
  if (action !== void 0) rules["*"] = action;
20517
21587
  for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
20518
21588
  for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
21589
+ const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
21590
+ if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
20519
21591
  }
20520
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
21592
+ for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
21593
+ const json = Object.keys(vibeOverridePermission).length > 0 ? {
21594
+ permission,
21595
+ vibe: { permission: vibeOverridePermission }
21596
+ } : { permission };
21597
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
20521
21598
  }
20522
21599
  validate() {
20523
21600
  try {
@@ -20544,6 +21621,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20544
21621
  });
20545
21622
  }
20546
21623
  };
21624
+ /**
21625
+ * Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
21626
+ * escalate to ASK even when the base permission is ALWAYS). rulesync owns this
21627
+ * list for any category the override names: a present list is set, an empty
21628
+ * one clears it. Categories not named keep whatever the existing file had.
21629
+ */
21630
+ function applyVibeSensitivePatterns(tools, vibeOverride) {
21631
+ for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
21632
+ const vibeToolName = toVibeToolName(category);
21633
+ const nextTool = toVibeToolConfig(tools[vibeToolName]);
21634
+ const patterns = toStringArray(toolOverride.sensitive_patterns);
21635
+ if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
21636
+ else delete nextTool.sensitive_patterns;
21637
+ tools[vibeToolName] = nextTool;
21638
+ }
21639
+ }
20547
21640
  function parseVibeConfig(fileContent) {
20548
21641
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
20549
21642
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -20588,6 +21681,11 @@ function ensurePermission(permission, category) {
20588
21681
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
20589
21682
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
20590
21683
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
21684
+ const WARP_OVERRIDE_KEYS = [
21685
+ "agent_mode_coding_permissions",
21686
+ "agent_mode_coding_file_read_allowlist",
21687
+ "agent_mode_execute_readonly_commands"
21688
+ ];
20591
21689
  /**
20592
21690
  * Warp's `settings.toml` lives in a different directory per platform (Stable
20593
21691
  * channel). The home directory is resolved by the processor through
@@ -20623,8 +21721,16 @@ function warpSettingsDir() {
20623
21721
  * patterns as regexes when targeting Warp (mirrors the Zed permissions
20624
21722
  * adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
20625
21723
  * the command lists only model shell commands, so non-`bash` categories are
20626
- * skipped (with a warning when they carry `deny` rules). MCP allow/deny and the
20627
- * file-read permissions are separate surfaces not modeled here.
21724
+ * skipped (with a warning when they carry `deny` rules).
21725
+ *
21726
+ * Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
21727
+ * knobs that do not fit the canonical `allow | ask | deny` per-command model:
21728
+ * `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
21729
+ * `agent_mode_execute_readonly_commands`. These are authored and round-tripped
21730
+ * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
21731
+ * on **import** they are lifted from `settings.toml` into the override, and on
21732
+ * **export** they are merged back into `[agents.profiles]` (the override wins).
21733
+ * MCP allow/deny is a separate surface not modeled here.
20628
21734
  *
20629
21735
  * The `settings.toml` file holds all of Warp's settings, so the
20630
21736
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -20669,12 +21775,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20669
21775
  } catch (error) {
20670
21776
  throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
20671
21777
  }
21778
+ const config = rulesyncPermissions.getJson();
20672
21779
  const { allow, deny } = convertRulesyncToWarpPermissions({
20673
- config: rulesyncPermissions.getJson(),
21780
+ config,
20674
21781
  logger
20675
21782
  });
20676
21783
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20677
21784
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
21785
+ const override = config.warp;
21786
+ if (isRecord(override)) Object.assign(profiles, override);
20678
21787
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
20679
21788
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
20680
21789
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -20705,7 +21814,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20705
21814
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
20706
21815
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
20707
21816
  });
20708
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
21817
+ const warpOverride = {};
21818
+ for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
21819
+ const result = { ...config };
21820
+ if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
21821
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20709
21822
  }
20710
21823
  validate() {
20711
21824
  return {
@@ -24071,8 +25184,9 @@ const DevinSkillFrontmatterSchema = zod_mini.z.looseObject({
24071
25184
  * Represents a Devin (now Devin Desktop) skill directory.
24072
25185
  * Devin supports skills in both project mode under .devin/skills/
24073
25186
  * (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
24074
- * fallback the tool still reads) and global mode under ~/.codeium/windsurf/skills/
24075
- * (unchanged by the rebrand).
25187
+ * fallback the tool still reads) and global mode under the Devin-native
25188
+ * ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
25189
+ * the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
24076
25190
  */
24077
25191
  var DevinSkill = class DevinSkill extends ToolSkill {
24078
25192
  constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -24094,7 +25208,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
24094
25208
  }
24095
25209
  }
24096
25210
  static getSettablePaths({ global = false } = {}) {
24097
- if (global) return { relativeDirPath: CODEIUM_WINDSURF_SKILLS_DIR_PATH };
25211
+ if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
24098
25212
  return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
24099
25213
  }
24100
25214
  getFrontmatter() {
@@ -27153,7 +28267,7 @@ const QwencodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
27153
28267
  disallowedTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
27154
28268
  maxTurns: zod_mini.z.optional(zod_mini.z.number()),
27155
28269
  color: zod_mini.z.optional(zod_mini.z.string()),
27156
- mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
28270
+ mcpServers: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.array(zod_mini.z.string()), zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({}))])),
27157
28271
  hooks: zod_mini.z.optional(zod_mini.z.unknown())
27158
28272
  });
27159
28273
  var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
@@ -29180,14 +30294,21 @@ def register(ctx):
29180
30294
  `;
29181
30295
  }
29182
30296
  function getEnabledPluginConfigContent(currentContent) {
29183
- const config = parseHermesConfig(currentContent);
30297
+ const config = parseSharedConfig({
30298
+ format: "yaml",
30299
+ fileContent: currentContent
30300
+ });
29184
30301
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29185
30302
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
29186
- config.plugins = {
29187
- ...plugins,
29188
- enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
29189
- };
29190
- return stringifyHermesConfig(config);
30303
+ return applySharedConfigPatch({
30304
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30305
+ feature: "subagents",
30306
+ existingContent: currentContent,
30307
+ patch: { plugins: {
30308
+ ...plugins,
30309
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30310
+ } }
30311
+ });
29191
30312
  }
29192
30313
  function getSubagentSpec(rulesyncSubagent) {
29193
30314
  const json = rulesyncSubagent.getFrontmatter();
@@ -29273,6 +30394,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
29273
30394
  static getSettablePaths() {
29274
30395
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
29275
30396
  }
30397
+ /**
30398
+ * Beyond the subagent spec files, generation also read-modify-writes the
30399
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30400
+ * so the write must be declared for the shared-file order derivation.
30401
+ */
30402
+ static getExtraSharedWritePaths() {
30403
+ return [{
30404
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30405
+ relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
30406
+ }];
30407
+ }
29276
30408
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
29277
30409
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
29278
30410
  }
@@ -31011,7 +32143,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
31011
32143
  })),
31012
32144
  kiro: zod_mini.z.optional(zod_mini.z.looseObject({
31013
32145
  inclusion: zod_mini.z.optional(zod_mini.z.string()),
31014
- fileMatchPattern: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
32146
+ fileMatchPattern: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
32147
+ name: zod_mini.z.optional(zod_mini.z.string()),
32148
+ description: zod_mini.z.optional(zod_mini.z.string())
31015
32149
  })),
31016
32150
  takt: zod_mini.z.optional(zod_mini.z.looseObject({
31017
32151
  name: zod_mini.z.optional(zod_mini.z.string()),
@@ -33927,7 +35061,8 @@ function toFileMatchPattern(globs) {
33927
35061
  *
33928
35062
  * Precedence:
33929
35063
  * 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
33930
- * taken from the block, or derived from `globs` when omitted for `fileMatch`).
35064
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35065
+ * and with the companion `name`/`description` carried through for `auto`).
33931
35066
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
33932
35067
  * to matching files instead of leaving it implicitly always-on.
33933
35068
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -33945,6 +35080,11 @@ function deriveKiroInclusion({ kiro, globs }) {
33945
35080
  fileMatchPattern
33946
35081
  } : { inclusion: "fileMatch" };
33947
35082
  }
35083
+ if (kiro.inclusion === "auto") return {
35084
+ inclusion: "auto",
35085
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35086
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35087
+ };
33948
35088
  return { inclusion: kiro.inclusion };
33949
35089
  }
33950
35090
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -34016,6 +35156,8 @@ var KiroRule = class KiroRule extends ToolRule {
34016
35156
  const rawPattern = frontmatter.fileMatchPattern;
34017
35157
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
34018
35158
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35159
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35160
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
34019
35161
  return new RulesyncRule({
34020
35162
  outputRoot: process.cwd(),
34021
35163
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -34026,7 +35168,9 @@ var KiroRule = class KiroRule extends ToolRule {
34026
35168
  globs,
34027
35169
  kiro: {
34028
35170
  inclusion,
34029
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35171
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35172
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35173
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34030
35174
  }
34031
35175
  },
34032
35176
  body
@@ -36200,6 +37344,197 @@ function buildPermissionsStrategy(ctx) {
36200
37344
  };
36201
37345
  }
36202
37346
  //#endregion
37347
+ //#region src/types/processor-registry.ts
37348
+ const PROCESSOR_REGISTRY = [
37349
+ {
37350
+ feature: "rules",
37351
+ processor: RulesProcessor,
37352
+ schema: RulesProcessorToolTargetSchema,
37353
+ factory: toolRuleFactories
37354
+ },
37355
+ {
37356
+ feature: "ignore",
37357
+ processor: IgnoreProcessor,
37358
+ schema: IgnoreProcessorToolTargetSchema,
37359
+ factory: toolIgnoreFactories
37360
+ },
37361
+ {
37362
+ feature: "mcp",
37363
+ processor: McpProcessor,
37364
+ schema: McpProcessorToolTargetSchema,
37365
+ factory: toolMcpFactories
37366
+ },
37367
+ {
37368
+ feature: "commands",
37369
+ processor: CommandsProcessor,
37370
+ schema: CommandsProcessorToolTargetSchema,
37371
+ factory: toolCommandFactories
37372
+ },
37373
+ {
37374
+ feature: "subagents",
37375
+ processor: SubagentsProcessor,
37376
+ schema: SubagentsProcessorToolTargetSchema,
37377
+ factory: toolSubagentFactories
37378
+ },
37379
+ {
37380
+ feature: "skills",
37381
+ processor: SkillsProcessor,
37382
+ schema: SkillsProcessorToolTargetSchema,
37383
+ factory: toolSkillFactories
37384
+ },
37385
+ {
37386
+ feature: "hooks",
37387
+ processor: HooksProcessor,
37388
+ schema: HooksProcessorToolTargetSchema,
37389
+ factory: toolHooksFactories
37390
+ },
37391
+ {
37392
+ feature: "permissions",
37393
+ processor: PermissionsProcessor,
37394
+ schema: PermissionsProcessorToolTargetSchema,
37395
+ factory: toolPermissionsFactories
37396
+ }
37397
+ ];
37398
+ const getProcessorRegistryEntry = (feature) => {
37399
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37400
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37401
+ return entry;
37402
+ };
37403
+ //#endregion
37404
+ //#region src/lib/shared-file-derive.ts
37405
+ /**
37406
+ * The single declaration of the cross-feature write order for shared
37407
+ * (read-modify-write) config files: when two features write the same on-disk
37408
+ * file, the earlier one writes first and the later one merges on top, so the
37409
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37410
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37411
+ * The generation step graph's `dependsOn` edges are derived from this list
37412
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37413
+ * shared path never requires touching the graph by hand.
37414
+ */
37415
+ const SHARED_WRITE_FEATURE_ORDER = [
37416
+ "ignore",
37417
+ "subagents",
37418
+ "mcp",
37419
+ "hooks",
37420
+ "permissions",
37421
+ "rules"
37422
+ ];
37423
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37424
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37425
+ const sharedFileKey = (path) => {
37426
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37427
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37428
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37429
+ };
37430
+ const settablePathsForScope = (cls, global) => {
37431
+ const paths = [];
37432
+ let settable;
37433
+ try {
37434
+ settable = cls.getSettablePaths?.({ global });
37435
+ } catch {
37436
+ return paths;
37437
+ }
37438
+ if (settable?.relativeFilePath) paths.push({
37439
+ relativeDirPath: settable.relativeDirPath ?? ".",
37440
+ relativeFilePath: settable.relativeFilePath
37441
+ });
37442
+ if (settable?.root) {
37443
+ paths.push(settable.root);
37444
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37445
+ }
37446
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37447
+ return paths;
37448
+ };
37449
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37450
+ /**
37451
+ * Derive, from the processor registry, every on-disk file that two or more
37452
+ * features read-modify-write. Source of truth the generation step graph's
37453
+ * `writesSharedFile` declarations must match.
37454
+ */
37455
+ const deriveSharedFileWriters = () => {
37456
+ const byKey = /* @__PURE__ */ new Map();
37457
+ const pathByKey = /* @__PURE__ */ new Map();
37458
+ for (const entry of PROCESSOR_REGISTRY) {
37459
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37460
+ const factories = entry.factory;
37461
+ for (const [tool, factory] of factories) {
37462
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37463
+ for (const path of collectFactoryPaths(factory)) {
37464
+ const key = sharedFileKey(path);
37465
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37466
+ let features = byKey.get(key);
37467
+ if (!features) {
37468
+ features = /* @__PURE__ */ new Map();
37469
+ byKey.set(key, features);
37470
+ }
37471
+ let tools = features.get(entry.feature);
37472
+ if (!tools) {
37473
+ tools = /* @__PURE__ */ new Set();
37474
+ features.set(entry.feature, tools);
37475
+ }
37476
+ tools.add(tool);
37477
+ }
37478
+ }
37479
+ }
37480
+ const writers = [];
37481
+ for (const [key, features] of byKey) {
37482
+ if (features.size < 2) continue;
37483
+ const path = pathByKey.get(key);
37484
+ const toolsByFeature = /* @__PURE__ */ new Map();
37485
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37486
+ writers.push({
37487
+ key,
37488
+ relativeDirPath: path.relativeDirPath,
37489
+ relativeFilePath: path.relativeFilePath,
37490
+ features: [...features.keys()].toSorted(),
37491
+ toolsByFeature
37492
+ });
37493
+ }
37494
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37495
+ };
37496
+ /**
37497
+ * Derive, per shared-write feature, the shared files it writes and the
37498
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37499
+ * writer depends on all writers that precede it in
37500
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37501
+ * graph consumes, so registry changes (a new tool, a new settable path)
37502
+ * propagate into the execution order without a hand-maintained declaration.
37503
+ *
37504
+ * @throws Error if a feature writes a shared file but has no position in
37505
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37506
+ * whose merge policy wins, so it must be made explicitly there.
37507
+ */
37508
+ const deriveSharedWriteSteps = () => {
37509
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37510
+ const filesByFeature = /* @__PURE__ */ new Map();
37511
+ const depsByFeature = /* @__PURE__ */ new Map();
37512
+ for (const writer of deriveSharedFileWriters()) {
37513
+ 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.`);
37514
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37515
+ for (const [position, feature] of ordered.entries()) {
37516
+ let files = filesByFeature.get(feature);
37517
+ if (!files) {
37518
+ files = /* @__PURE__ */ new Set();
37519
+ filesByFeature.set(feature, files);
37520
+ }
37521
+ files.add(writer.key);
37522
+ let deps = depsByFeature.get(feature);
37523
+ if (!deps) {
37524
+ deps = /* @__PURE__ */ new Set();
37525
+ depsByFeature.set(feature, deps);
37526
+ }
37527
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37528
+ }
37529
+ }
37530
+ const steps = /* @__PURE__ */ new Map();
37531
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37532
+ writesSharedFile: [...files].toSorted(),
37533
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37534
+ });
37535
+ return steps;
37536
+ };
37537
+ //#endregion
36203
37538
  //#region src/lib/generate.ts
36204
37539
  async function processFeatureGeneration(params) {
36205
37540
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -36349,98 +37684,53 @@ function resolveExecutionOrder(steps) {
36349
37684
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36350
37685
  return ordered;
36351
37686
  }
37687
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37688
+ const sharedWriteMeta = (id) => {
37689
+ const step = SHARED_WRITE_STEPS.get(id);
37690
+ return step ? {
37691
+ writesSharedFile: step.writesSharedFile,
37692
+ dependsOn: step.dependsOn
37693
+ } : {};
37694
+ };
36352
37695
  /**
36353
37696
  * The static shape of the generation step graph: which steps write which shared
36354
37697
  * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36355
- * for those writers. Exported (separately from the `run` closures, which need a
36356
- * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36357
- * tested directly against the real graph rather than a hand-copied one. Readonly
36358
- * so a consumer can't mutate this module-level singleton and affect every
36359
- * subsequent `generate()` call in the process.
37698
+ * for those writers. Both are derived from the processor registry's settable
37699
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37700
+ * tool or shared path never requires editing this graph. Exported (separately
37701
+ * from the `run` closures, which need a live `config`/`logger`) so
37702
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37703
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37704
+ * mutate this module-level singleton and affect every subsequent `generate()`
37705
+ * call in the process.
36360
37706
  */
36361
37707
  const GENERATION_STEP_GRAPH = [
36362
37708
  {
36363
37709
  id: "ignore",
36364
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37710
+ ...sharedWriteMeta("ignore")
36365
37711
  },
36366
37712
  {
36367
37713
  id: "mcp",
36368
- writesSharedFile: [
36369
- ".amp/settings.json",
36370
- ".augment/settings.json",
36371
- ".codex/config.toml",
36372
- ".config/amp/settings.json",
36373
- ".config/devin/config.json",
36374
- ".config/opencode/opencode.json",
36375
- ".config/zed/settings.json",
36376
- ".devin/config.json",
36377
- ".grok/config.toml",
36378
- ".hermes/config.yaml",
36379
- ".qwen/settings.json",
36380
- ".reasonix/config.toml",
36381
- ".takt/config.yaml",
36382
- ".vibe/config.toml",
36383
- ".zed/settings.json",
36384
- "kilo.json",
36385
- "opencode.json",
36386
- "reasonix.toml"
36387
- ],
36388
- dependsOn: ["ignore"]
37714
+ ...sharedWriteMeta("mcp")
36389
37715
  },
36390
37716
  { id: "commands" },
36391
- { id: "subagents" },
37717
+ {
37718
+ id: "subagents",
37719
+ ...sharedWriteMeta("subagents")
37720
+ },
36392
37721
  { id: "skills" },
36393
37722
  {
36394
37723
  id: "hooks",
36395
- writesSharedFile: [
36396
- ".augment/settings.json",
36397
- ".claude/settings.json",
36398
- ".codex/config.toml",
36399
- ".config/devin/config.json",
36400
- ".hermes/config.yaml",
36401
- ".kiro/agents/default.json",
36402
- ".qwen/settings.json",
36403
- ".vibe/config.toml"
36404
- ],
36405
- dependsOn: ["ignore", "mcp"]
37724
+ ...sharedWriteMeta("hooks")
36406
37725
  },
36407
37726
  {
36408
37727
  id: "permissions",
36409
- writesSharedFile: [
36410
- ".amp/settings.json",
36411
- ".augment/settings.json",
36412
- ".claude/settings.json",
36413
- ".codex/config.toml",
36414
- ".config/amp/settings.json",
36415
- ".config/devin/config.json",
36416
- ".config/opencode/opencode.json",
36417
- ".config/zed/settings.json",
36418
- ".devin/config.json",
36419
- ".grok/config.toml",
36420
- ".hermes/config.yaml",
36421
- ".kiro/agents/default.json",
36422
- ".qwen/settings.json",
36423
- ".reasonix/config.toml",
36424
- ".takt/config.yaml",
36425
- ".vibe/config.toml",
36426
- ".zed/settings.json",
36427
- "opencode.json",
36428
- "reasonix.toml"
36429
- ],
36430
- dependsOn: [
36431
- "ignore",
36432
- "hooks",
36433
- "mcp"
36434
- ]
37728
+ ...sharedWriteMeta("permissions")
36435
37729
  },
36436
37730
  {
36437
37731
  id: "rules",
36438
- writesSharedFile: ["kilo.json", "opencode.json"],
36439
- dependsOn: [
36440
- "mcp",
36441
- "skills",
36442
- "permissions"
36443
- ]
37732
+ ...sharedWriteMeta("rules"),
37733
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
36444
37734
  }
36445
37735
  ];
36446
37736
  /**
@@ -37198,12 +38488,6 @@ Object.defineProperty(exports, "CommandsProcessor", {
37198
38488
  return CommandsProcessor;
37199
38489
  }
37200
38490
  });
37201
- Object.defineProperty(exports, "CommandsProcessorToolTargetSchema", {
37202
- enumerable: true,
37203
- get: function() {
37204
- return CommandsProcessorToolTargetSchema;
37205
- }
37206
- });
37207
38491
  Object.defineProperty(exports, "ConfigResolver", {
37208
38492
  enumerable: true,
37209
38493
  get: function() {
@@ -37228,24 +38512,12 @@ Object.defineProperty(exports, "HooksProcessor", {
37228
38512
  return HooksProcessor;
37229
38513
  }
37230
38514
  });
37231
- Object.defineProperty(exports, "HooksProcessorToolTargetSchema", {
37232
- enumerable: true,
37233
- get: function() {
37234
- return HooksProcessorToolTargetSchema;
37235
- }
37236
- });
37237
38515
  Object.defineProperty(exports, "IgnoreProcessor", {
37238
38516
  enumerable: true,
37239
38517
  get: function() {
37240
38518
  return IgnoreProcessor;
37241
38519
  }
37242
38520
  });
37243
- Object.defineProperty(exports, "IgnoreProcessorToolTargetSchema", {
37244
- enumerable: true,
37245
- get: function() {
37246
- return IgnoreProcessorToolTargetSchema;
37247
- }
37248
- });
37249
38521
  Object.defineProperty(exports, "JsonLogger", {
37250
38522
  enumerable: true,
37251
38523
  get: function() {
@@ -37264,24 +38536,6 @@ Object.defineProperty(exports, "McpProcessor", {
37264
38536
  return McpProcessor;
37265
38537
  }
37266
38538
  });
37267
- Object.defineProperty(exports, "McpProcessorToolTargetSchema", {
37268
- enumerable: true,
37269
- get: function() {
37270
- return McpProcessorToolTargetSchema;
37271
- }
37272
- });
37273
- Object.defineProperty(exports, "PermissionsProcessor", {
37274
- enumerable: true,
37275
- get: function() {
37276
- return PermissionsProcessor;
37277
- }
37278
- });
37279
- Object.defineProperty(exports, "PermissionsProcessorToolTargetSchema", {
37280
- enumerable: true,
37281
- get: function() {
37282
- return PermissionsProcessorToolTargetSchema;
37283
- }
37284
- });
37285
38539
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
37286
38540
  enumerable: true,
37287
38541
  get: function() {
@@ -37414,12 +38668,6 @@ Object.defineProperty(exports, "RulesProcessor", {
37414
38668
  return RulesProcessor;
37415
38669
  }
37416
38670
  });
37417
- Object.defineProperty(exports, "RulesProcessorToolTargetSchema", {
37418
- enumerable: true,
37419
- get: function() {
37420
- return RulesProcessorToolTargetSchema;
37421
- }
37422
- });
37423
38671
  Object.defineProperty(exports, "RulesyncCommand", {
37424
38672
  enumerable: true,
37425
38673
  get: function() {
@@ -37504,24 +38752,12 @@ Object.defineProperty(exports, "SkillsProcessor", {
37504
38752
  return SkillsProcessor;
37505
38753
  }
37506
38754
  });
37507
- Object.defineProperty(exports, "SkillsProcessorToolTargetSchema", {
37508
- enumerable: true,
37509
- get: function() {
37510
- return SkillsProcessorToolTargetSchema;
37511
- }
37512
- });
37513
38755
  Object.defineProperty(exports, "SubagentsProcessor", {
37514
38756
  enumerable: true,
37515
38757
  get: function() {
37516
38758
  return SubagentsProcessor;
37517
38759
  }
37518
38760
  });
37519
- Object.defineProperty(exports, "SubagentsProcessorToolTargetSchema", {
37520
- enumerable: true,
37521
- get: function() {
37522
- return SubagentsProcessorToolTargetSchema;
37523
- }
37524
- });
37525
38761
  Object.defineProperty(exports, "ToolTargetSchema", {
37526
38762
  enumerable: true,
37527
38763
  get: function() {
@@ -37618,6 +38854,12 @@ Object.defineProperty(exports, "getLocalSkillDirNames", {
37618
38854
  return getLocalSkillDirNames;
37619
38855
  }
37620
38856
  });
38857
+ Object.defineProperty(exports, "getProcessorRegistryEntry", {
38858
+ enumerable: true,
38859
+ get: function() {
38860
+ return getProcessorRegistryEntry;
38861
+ }
38862
+ });
37621
38863
  Object.defineProperty(exports, "importFromTool", {
37622
38864
  enumerable: true,
37623
38865
  get: function() {
@@ -37672,54 +38914,6 @@ Object.defineProperty(exports, "toPosixPath", {
37672
38914
  return toPosixPath;
37673
38915
  }
37674
38916
  });
37675
- Object.defineProperty(exports, "toolCommandFactories", {
37676
- enumerable: true,
37677
- get: function() {
37678
- return toolCommandFactories;
37679
- }
37680
- });
37681
- Object.defineProperty(exports, "toolHooksFactories", {
37682
- enumerable: true,
37683
- get: function() {
37684
- return toolHooksFactories;
37685
- }
37686
- });
37687
- Object.defineProperty(exports, "toolIgnoreFactories", {
37688
- enumerable: true,
37689
- get: function() {
37690
- return toolIgnoreFactories;
37691
- }
37692
- });
37693
- Object.defineProperty(exports, "toolMcpFactories", {
37694
- enumerable: true,
37695
- get: function() {
37696
- return toolMcpFactories;
37697
- }
37698
- });
37699
- Object.defineProperty(exports, "toolPermissionsFactories", {
37700
- enumerable: true,
37701
- get: function() {
37702
- return toolPermissionsFactories;
37703
- }
37704
- });
37705
- Object.defineProperty(exports, "toolRuleFactories", {
37706
- enumerable: true,
37707
- get: function() {
37708
- return toolRuleFactories;
37709
- }
37710
- });
37711
- Object.defineProperty(exports, "toolSkillFactories", {
37712
- enumerable: true,
37713
- get: function() {
37714
- return toolSkillFactories;
37715
- }
37716
- });
37717
- Object.defineProperty(exports, "toolSubagentFactories", {
37718
- enumerable: true,
37719
- get: function() {
37720
- return toolSubagentFactories;
37721
- }
37722
- });
37723
38917
  Object.defineProperty(exports, "writeFileContent", {
37724
38918
  enumerable: true,
37725
38919
  get: function() {