rulesync 9.3.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.
@@ -3892,6 +3892,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3892
3892
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3893
3893
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3894
3894
  //#endregion
3895
+ //#region src/constants/takt-paths.ts
3896
+ const TAKT_DIR = ".takt";
3897
+ const TAKT_FACETS_SUBDIR = "facets";
3898
+ const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
3899
+ const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
3900
+ const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
3901
+ const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
3902
+ const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
3903
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
3904
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3905
+ /**
3906
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3907
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3908
+ * profiles (including permission modes), and other Takt settings.
3909
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3910
+ */
3911
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3912
+ /**
3913
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3914
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3915
+ * transport must be explicitly enabled here before any workflow-defined MCP
3916
+ * server using it is permitted to run.
3917
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3918
+ */
3919
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3920
+ //#endregion
3921
+ //#region src/utils/prototype-pollution.ts
3922
+ /**
3923
+ * Keys that, if walked into when constructing or merging objects from
3924
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3925
+ * chain) and propagate state to every other object in the runtime. Any code
3926
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3927
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3928
+ */
3929
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3930
+ "__proto__",
3931
+ "constructor",
3932
+ "prototype"
3933
+ ]);
3934
+ function isPrototypePollutionKey(key) {
3935
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3936
+ }
3937
+ /**
3938
+ * Returns a shallow copy of a record's own entries with every
3939
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3940
+ *
3941
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3942
+ * or `headers` table — into freshly generated config. Carrying such a map by
3943
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3944
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3945
+ * prototype). Walking the entries through this helper severs that path while
3946
+ * preserving every legitimate key.
3947
+ */
3948
+ function omitPrototypePollutionKeys(record) {
3949
+ const sanitized = {};
3950
+ for (const [key, value] of Object.entries(record)) {
3951
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3952
+ sanitized[key] = value;
3953
+ }
3954
+ return sanitized;
3955
+ }
3956
+ //#endregion
3957
+ //#region src/features/shared/shared-config-gateway.ts
3958
+ function sanitizeSharedConfigValue(value) {
3959
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
3960
+ if (!isPlainObject(value)) return value;
3961
+ const result = {};
3962
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
3963
+ return result;
3964
+ }
3965
+ /**
3966
+ * Parse a shared config file into a plain document: an empty/whitespace file
3967
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
3968
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
3969
+ * path when one is given.
3970
+ */
3971
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
3972
+ if (fileContent.trim() === "") return {};
3973
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
3974
+ let parsed;
3975
+ try {
3976
+ if (format === "yaml") parsed = load(fileContent);
3977
+ else if (format === "json") parsed = JSON.parse(fileContent);
3978
+ else parsed = parse(fileContent);
3979
+ } catch (error) {
3980
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
3981
+ }
3982
+ if (parsed === void 0 || parsed === null) return {};
3983
+ if (!isPlainObject(parsed)) {
3984
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
3985
+ return {};
3986
+ }
3987
+ return sanitizeSharedConfigValue(parsed);
3988
+ }
3989
+ /**
3990
+ * Serialize a shared config document. YAML output always ends with exactly one
3991
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
3992
+ * writers have always emitted (no trailing newline).
3993
+ */
3994
+ function stringifySharedConfig({ format, document }) {
3995
+ if (format === "yaml") return dump(document, {
3996
+ noRefs: true,
3997
+ sortKeys: false
3998
+ }).trimEnd() + "\n";
3999
+ return JSON.stringify(document, null, 2);
4000
+ }
4001
+ /**
4002
+ * Shallow merge: every top-level key in `patch` replaces the base key
4003
+ * wholesale; all other base keys are preserved. The policy for a feature that
4004
+ * owns a fixed set of top-level keys.
4005
+ */
4006
+ function mergeSharedConfigShallow({ base, patch }) {
4007
+ return {
4008
+ ...base,
4009
+ ...sanitizeSharedConfigValue(patch)
4010
+ };
4011
+ }
4012
+ /**
4013
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4014
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4015
+ * feature whose contribution interleaves with user-authored siblings at any
4016
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4017
+ * per-provider option tables) — nested sibling keys are preserved by
4018
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4019
+ * keys are dropped.
4020
+ */
4021
+ function mergeSharedConfigDeep({ base, patch }) {
4022
+ const result = { ...base };
4023
+ for (const [key, patchValue] of Object.entries(patch)) {
4024
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4025
+ const baseValue = result[key];
4026
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4027
+ base: baseValue,
4028
+ patch: patchValue
4029
+ });
4030
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4031
+ }
4032
+ return result;
4033
+ }
4034
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4035
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4036
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4037
+ /**
4038
+ * Who owns what in each gateway-managed shared config file, and which policy
4039
+ * resolves conflicts. Keys are `dir/file` tokens matching
4040
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4041
+ * lock-step with the writers derived from the processor registry, so an
4042
+ * undeclared writer fails CI instead of merging by accident.
4043
+ */
4044
+ const SHARED_CONFIG_OWNERSHIP = {
4045
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4046
+ format: "json",
4047
+ features: {
4048
+ ignore: {
4049
+ kind: "custom",
4050
+ policyFunction: "applyIgnoreReadDenies"
4051
+ },
4052
+ hooks: {
4053
+ kind: "replace-owned-keys",
4054
+ ownedKeys: ["hooks"]
4055
+ },
4056
+ permissions: {
4057
+ kind: "custom",
4058
+ policyFunction: "applyPermissions"
4059
+ }
4060
+ }
4061
+ },
4062
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4063
+ format: "yaml",
4064
+ features: {
4065
+ subagents: {
4066
+ kind: "replace-owned-keys",
4067
+ ownedKeys: ["plugins"]
4068
+ },
4069
+ mcp: {
4070
+ kind: "replace-owned-keys",
4071
+ ownedKeys: ["mcp_servers"]
4072
+ },
4073
+ hooks: {
4074
+ kind: "replace-owned-keys",
4075
+ ownedKeys: ["hooks"]
4076
+ },
4077
+ permissions: {
4078
+ kind: "deep-merge",
4079
+ replaceKeys: ["permissions"]
4080
+ }
4081
+ }
4082
+ },
4083
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4084
+ format: "yaml",
4085
+ invalidRootPolicy: "error",
4086
+ features: {
4087
+ mcp: {
4088
+ kind: "replace-owned-keys",
4089
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4090
+ },
4091
+ permissions: { kind: "deep-merge" }
4092
+ }
4093
+ }
4094
+ };
4095
+ /**
4096
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4097
+ * the existing content, merge the patch under the feature's declared policy,
4098
+ * and serialize. Throws when the file or feature is undeclared, when a
4099
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4100
+ * feature's policy is `custom` (those calls go to the named policy function
4101
+ * instead).
4102
+ */
4103
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4104
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4105
+ if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
4106
+ const policy = declaration.features[feature];
4107
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4108
+ if (policy.kind === "custom") throw new Error(`Feature '${feature}' writes '${fileKey}' through its dedicated policy function '${policy.policyFunction}' in shared-config-gateway.ts, not applySharedConfigPatch.`);
4109
+ const base = parseSharedConfig({
4110
+ format: declaration.format,
4111
+ fileContent: existingContent,
4112
+ filePath,
4113
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4114
+ });
4115
+ if (policy.kind === "replace-owned-keys") {
4116
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4117
+ if (unowned.length > 0) throw new Error(`Feature '${feature}' tried to write undeclared keys [${unowned.join(", ")}] to '${fileKey}'; extend its ownedKeys declaration if that ownership is intended.`);
4118
+ return stringifySharedConfig({
4119
+ format: declaration.format,
4120
+ document: mergeSharedConfigShallow({
4121
+ base,
4122
+ patch
4123
+ })
4124
+ });
4125
+ }
4126
+ const merged = mergeSharedConfigDeep({
4127
+ base,
4128
+ patch
4129
+ });
4130
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4131
+ return stringifySharedConfig({
4132
+ format: declaration.format,
4133
+ document: merged
4134
+ });
4135
+ }
4136
+ const READ_TOOL_NAME = "Read";
4137
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4138
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4139
+ const parsePermissionsBlock = (settings) => {
4140
+ const permissions = settings.permissions ?? {};
4141
+ return {
4142
+ allow: permissions.allow ?? [],
4143
+ ask: permissions.ask ?? [],
4144
+ deny: permissions.deny ?? []
4145
+ };
4146
+ };
4147
+ const withPermissions = (settings, next) => {
4148
+ const permissions = { ...settings.permissions };
4149
+ const assign = (key, values) => {
4150
+ if (values.length > 0) permissions[key] = values;
4151
+ else delete permissions[key];
4152
+ };
4153
+ assign("allow", next.allow);
4154
+ assign("ask", next.ask);
4155
+ assign("deny", next.deny);
4156
+ return {
4157
+ ...settings,
4158
+ permissions
4159
+ };
4160
+ };
4161
+ const applyIgnoreReadDenies = (params) => {
4162
+ const { settings, readDenies } = params;
4163
+ const current = parsePermissionsBlock(settings);
4164
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4165
+ return withPermissions(settings, {
4166
+ allow: current.allow,
4167
+ ask: current.ask,
4168
+ deny: uniq([...preservedDeny, ...readDenies].toSorted())
4169
+ });
4170
+ };
4171
+ const applyPermissions = (params) => {
4172
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4173
+ const current = parsePermissionsBlock(settings);
4174
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4175
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4176
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4177
+ if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
4178
+ }
4179
+ return withPermissions(settings, {
4180
+ allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
4181
+ ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
4182
+ deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
4183
+ });
4184
+ };
4185
+ //#endregion
3895
4186
  //#region src/features/opencode-config.ts
3896
4187
  /**
3897
4188
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3916,9 +4207,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3916
4207
  });
3917
4208
  const fileContent = await readFileContentOrNull(join(configDir, "opencode.jsonc")) ?? await readFileContentOrNull(join(configDir, "opencode.json"));
3918
4209
  if (!fileContent) return {};
3919
- const parsed = parse(fileContent);
3920
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3921
- return parsed;
4210
+ return parseSharedConfig({
4211
+ format: "jsonc",
4212
+ fileContent
4213
+ });
3922
4214
  }
3923
4215
  /**
3924
4216
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4827,32 +5119,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4827
5119
  }
4828
5120
  };
4829
5121
  //#endregion
4830
- //#region src/constants/takt-paths.ts
4831
- const TAKT_DIR = ".takt";
4832
- const TAKT_FACETS_SUBDIR = "facets";
4833
- const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
4834
- const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
4835
- const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
4836
- const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
4837
- const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
4838
- const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
4839
- const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4840
- /**
4841
- * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
4842
- * `~/.takt/config.yaml` (global); it holds the active provider, provider
4843
- * profiles (including permission modes), and other Takt settings.
4844
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4845
- */
4846
- const TAKT_CONFIG_FILE_NAME = "config.yaml";
4847
- /**
4848
- * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4849
- * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4850
- * transport must be explicitly enabled here before any workflow-defined MCP
4851
- * server using it is permitted to run.
4852
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4853
- */
4854
- const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4855
- //#endregion
4856
5122
  //#region src/features/takt-shared.ts
4857
5123
  /**
4858
5124
  * Shared utilities for all TAKT-* tool file classes.
@@ -6330,42 +6596,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6330
6596
  */
6331
6597
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6332
6598
  //#endregion
6333
- //#region src/utils/prototype-pollution.ts
6334
- /**
6335
- * Keys that, if walked into when constructing or merging objects from
6336
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
6337
- * chain) and propagate state to every other object in the runtime. Any code
6338
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
6339
- * parsing, MCP config conversion, settings round-trip — should skip these.
6340
- */
6341
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
6342
- "__proto__",
6343
- "constructor",
6344
- "prototype"
6345
- ]);
6346
- function isPrototypePollutionKey(key) {
6347
- return PROTOTYPE_POLLUTION_KEYS.has(key);
6348
- }
6349
- /**
6350
- * Returns a shallow copy of a record's own entries with every
6351
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
6352
- *
6353
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
6354
- * or `headers` table — into freshly generated config. Carrying such a map by
6355
- * reference, or re-assigning its keys via bracket notation, would let a literal
6356
- * `__proto__` key ride along (and re-assigning it would mutate the target's
6357
- * prototype). Walking the entries through this helper severs that path while
6358
- * preserving every legitimate key.
6359
- */
6360
- function omitPrototypePollutionKeys(record) {
6361
- const sanitized = {};
6362
- for (const [key, value] of Object.entries(record)) {
6363
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6364
- sanitized[key] = value;
6365
- }
6366
- return sanitized;
6367
- }
6368
- //#endregion
6369
6599
  //#region src/features/hooks/tool-hooks-converter.ts
6370
6600
  function isToolMatcherEntry(x) {
6371
6601
  if (x === null || typeof x !== "object") return false;
@@ -7024,24 +7254,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7024
7254
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7025
7255
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7026
7256
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7027
- let settings;
7028
- try {
7029
- settings = JSON.parse(existingContent);
7030
- } catch (error) {
7031
- throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
7032
- }
7033
7257
  const config = rulesyncHooks.getJson();
7034
- const claudeHooks = canonicalToToolHooks({
7035
- config,
7036
- toolOverrideHooks: config.claudecode?.hooks,
7037
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7038
- logger
7258
+ const fileContent = applySharedConfigPatch({
7259
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7260
+ feature: "hooks",
7261
+ existingContent,
7262
+ patch: { hooks: canonicalToToolHooks({
7263
+ config,
7264
+ toolOverrideHooks: config.claudecode?.hooks,
7265
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7266
+ logger
7267
+ }) },
7268
+ filePath
7039
7269
  });
7040
- const merged = {
7041
- ...settings,
7042
- hooks: claudeHooks
7043
- };
7044
- const fileContent = JSON.stringify(merged, null, 2);
7045
7270
  return new ClaudecodeHooks({
7046
7271
  outputRoot,
7047
7272
  relativeDirPath: paths.relativeDirPath,
@@ -8273,55 +8498,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8273
8498
  }
8274
8499
  };
8275
8500
  //#endregion
8276
- //#region src/features/hermes-config.ts
8277
- function sanitizeHermesConfigValue(value) {
8278
- if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
8279
- if (!isPlainObject(value)) return value;
8280
- const sanitized = omitPrototypePollutionKeys(value);
8281
- const result = {};
8282
- for (const [key, nestedValue] of Object.entries(sanitized)) {
8283
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8284
- result[key] = sanitizeHermesConfigValue(nestedValue);
8285
- }
8286
- return result;
8287
- }
8288
- function parseHermesConfig(fileContent) {
8289
- if (!fileContent.trim()) return {};
8290
- const parsed = load(fileContent);
8291
- if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
8292
- return {};
8293
- }
8294
- function stringifyHermesConfig(config) {
8295
- return dump(config, {
8296
- noRefs: true,
8297
- sortKeys: false
8298
- }).trimEnd() + "\n";
8299
- }
8300
- function mergeHermesConfig(fileContent, patch) {
8301
- return stringifyHermesConfig({
8302
- ...parseHermesConfig(fileContent),
8303
- ...patch
8304
- });
8305
- }
8306
- /**
8307
- * Recursively merge `patch` into `base` (patch wins). Nested plain objects are
8308
- * merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
8309
- * Used to overlay the Hermes-scoped permission override onto the natively
8310
- * emitted `approvals`/`security` structures without one clobbering the other
8311
- * (e.g. `approvals.deny` from canonical deny rules coexisting with an
8312
- * `approvals.mode` from the override). Prototype-pollution keys are dropped.
8313
- */
8314
- function deepMergeHermesConfig(base, patch) {
8315
- const result = { ...base };
8316
- for (const [key, patchValue] of Object.entries(patch)) {
8317
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8318
- const baseValue = result[key];
8319
- if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
8320
- else result[key] = sanitizeHermesConfigValue(patchValue);
8321
- }
8322
- return result;
8323
- }
8324
- //#endregion
8325
8501
  //#region src/features/hooks/hermesagent-hooks.ts
8326
8502
  /**
8327
8503
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8449,10 +8625,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8449
8625
  return true;
8450
8626
  }
8451
8627
  setFileContent(fileContent) {
8452
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8628
+ this.fileContent = applySharedConfigPatch({
8629
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8630
+ feature: "hooks",
8631
+ existingContent: fileContent,
8632
+ patch: parseSharedConfig({
8633
+ format: "yaml",
8634
+ fileContent: this.fileContent
8635
+ })
8636
+ });
8453
8637
  }
8454
8638
  toRulesyncHooks() {
8455
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8639
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8640
+ format: "yaml",
8641
+ fileContent: this.getFileContent()
8642
+ }).hooks);
8456
8643
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8457
8644
  version: 1,
8458
8645
  hooks
@@ -8462,11 +8649,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8462
8649
  const config = rulesyncHooks.getJson();
8463
8650
  return new HermesagentHooks({
8464
8651
  outputRoot,
8465
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8466
- config,
8467
- toolOverrideHooks: config.hermesagent?.hooks,
8468
- logger
8469
- }) })
8652
+ fileContent: stringifySharedConfig({
8653
+ format: "yaml",
8654
+ document: { hooks: canonicalToHermesHooks({
8655
+ config,
8656
+ toolOverrideHooks: config.hermesagent?.hooks,
8657
+ logger
8658
+ }) }
8659
+ })
8470
8660
  });
8471
8661
  }
8472
8662
  };
@@ -10486,66 +10676,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10486
10676
  }
10487
10677
  };
10488
10678
  //#endregion
10489
- //#region src/features/claudecode-settings-gateway.ts
10490
- /**
10491
- * Single owner of the `.claude/settings.json` `permissions` block, which both
10492
- * `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
10493
- * (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
10494
- * the merge, and the cross-feature ownership rule (permissions' explicit `Read`
10495
- * rules win over ignore-derived `Read` denies) used to be duplicated across both
10496
- * feature files; they live here once so each feature just states its intent and
10497
- * never reasons about the other's existence.
10498
- */
10499
- const READ_TOOL_NAME = "Read";
10500
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
10501
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
10502
- const parsePermissionsBlock = (settings) => {
10503
- const permissions = settings.permissions ?? {};
10504
- return {
10505
- allow: permissions.allow ?? [],
10506
- ask: permissions.ask ?? [],
10507
- deny: permissions.deny ?? []
10508
- };
10509
- };
10510
- const withPermissions = (settings, next) => {
10511
- const permissions = { ...settings.permissions };
10512
- const assign = (key, values) => {
10513
- if (values.length > 0) permissions[key] = values;
10514
- else delete permissions[key];
10515
- };
10516
- assign("allow", next.allow);
10517
- assign("ask", next.ask);
10518
- assign("deny", next.deny);
10519
- return {
10520
- ...settings,
10521
- permissions
10522
- };
10523
- };
10524
- const applyIgnoreReadDenies = (params) => {
10525
- const { settings, readDenies } = params;
10526
- const current = parsePermissionsBlock(settings);
10527
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
10528
- return withPermissions(settings, {
10529
- allow: current.allow,
10530
- ask: current.ask,
10531
- deny: uniq([...preservedDeny, ...readDenies].toSorted())
10532
- });
10533
- };
10534
- const applyPermissions = (params) => {
10535
- const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
10536
- const current = parsePermissionsBlock(settings);
10537
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
10538
- if (logger && managedToolNames.has(READ_TOOL_NAME)) {
10539
- const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
10540
- 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.`);
10541
- }
10542
- return withPermissions(settings, {
10543
- allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
10544
- ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
10545
- deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
10546
- });
10547
- };
10548
- //#endregion
10549
10679
  //#region src/features/ignore/claudecode-ignore.ts
10550
10680
  const DEFAULT_FILE_MODE = "shared";
10551
10681
  /**
@@ -11968,7 +12098,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11968
12098
  };
11969
12099
  //#endregion
11970
12100
  //#region src/features/mcp/augmentcode-mcp.ts
11971
- const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
11972
12101
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
11973
12102
  const configPath = join(relativeDirPath, relativeFilePath);
11974
12103
  let parsed;
@@ -11983,13 +12112,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
11983
12112
  /**
11984
12113
  * AugmentCode (Auggie CLI) MCP servers.
11985
12114
  *
11986
- * MCP servers are persisted in the shared user settings file
11987
- * `~/.augment/settings.json` (global only the docs do not document a
11988
- * project-level MCP location). That same file also holds `hooks` and
12115
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12116
+ * at either scope: the committed workspace file for team-shared servers (project)
12117
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
11989
12118
  * `toolPermissions`, so generation merges the `mcpServers` block into the
11990
- * existing settings instead of overwriting it, and the file is never deleted.
12119
+ * existing settings instead of overwriting it, and the file is never deleted. On
12120
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12121
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
11991
12122
  *
11992
- * @see https://docs.augmentcode.com/cli/integrations
12123
+ * @see https://docs.augmentcode.com/cli/config
11993
12124
  */
11994
12125
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
11995
12126
  json;
@@ -12011,9 +12142,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12011
12142
  };
12012
12143
  }
12013
12144
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12014
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12015
12145
  const paths = this.getSettablePaths({ global });
12016
- const json = parseAugmentcodeSettings(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12146
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12147
+ outputRoot,
12148
+ relativeDirPath: paths.relativeDirPath,
12149
+ baseFileName: paths.relativeFilePath,
12150
+ baseFallbackContent: "{}",
12151
+ includeLocalOverlay: !global
12152
+ }), paths.relativeDirPath, paths.relativeFilePath);
12017
12153
  const newJson = {
12018
12154
  ...json,
12019
12155
  mcpServers: json.mcpServers ?? {}
@@ -12028,7 +12164,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12028
12164
  });
12029
12165
  }
12030
12166
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12031
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12032
12167
  const paths = this.getSettablePaths({ global });
12033
12168
  const merged = {
12034
12169
  ...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -13678,7 +13813,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13678
13813
  config;
13679
13814
  constructor(params) {
13680
13815
  super(params);
13681
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13816
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13817
+ format: "yaml",
13818
+ fileContent: this.fileContent
13819
+ }) : {};
13682
13820
  }
13683
13821
  getConfig() {
13684
13822
  return this.config;
@@ -13687,9 +13825,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13687
13825
  return true;
13688
13826
  }
13689
13827
  setFileContent(fileContent) {
13690
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13828
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13829
+ format: "yaml",
13830
+ fileContent
13831
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13691
13832
  this.config = merged;
13692
- super.setFileContent(stringifyHermesConfig(merged));
13833
+ super.setFileContent(applySharedConfigPatch({
13834
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13835
+ feature: "mcp",
13836
+ existingContent: fileContent,
13837
+ patch: { mcp_servers: merged.mcp_servers }
13838
+ }));
13693
13839
  }
13694
13840
  isDeletable() {
13695
13841
  return false;
@@ -13716,12 +13862,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13716
13862
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13717
13863
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13718
13864
  const paths = this.getSettablePaths({ global });
13719
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13865
+ const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13866
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13867
+ format: "yaml",
13868
+ fileContent
13869
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13720
13870
  return new HermesagentMcp({
13721
13871
  outputRoot,
13722
13872
  relativeDirPath: paths.relativeDirPath,
13723
13873
  relativeFilePath: paths.relativeFilePath,
13724
- fileContent: stringifyHermesConfig(merged),
13874
+ fileContent: applySharedConfigPatch({
13875
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13876
+ feature: "mcp",
13877
+ existingContent: fileContent,
13878
+ patch: { mcp_servers: merged.mcp_servers }
13879
+ }),
13725
13880
  validate,
13726
13881
  global
13727
13882
  });
@@ -14886,28 +15041,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14886
15041
  }
14887
15042
  };
14888
15043
  //#endregion
14889
- //#region src/features/shared/takt-config.ts
14890
- /**
14891
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14892
- *
14893
- * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14894
- * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14895
- * instances are rejected for prototype-pollution hardening; a YAML mapping
14896
- * always parses to a plain object.
14897
- */
14898
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14899
- const configPath = join(relativeDirPath, relativeFilePath);
14900
- let parsed;
14901
- try {
14902
- parsed = fileContent.trim() === "" ? {} : load(fileContent);
14903
- } catch (error) {
14904
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14905
- }
14906
- if (parsed === void 0 || parsed === null) return {};
14907
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14908
- return parsed;
14909
- }
14910
- //#endregion
14911
15044
  //#region src/features/mcp/takt-mcp.ts
14912
15045
  /**
14913
15046
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -14973,17 +15106,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
14973
15106
  }
14974
15107
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14975
15108
  const paths = TaktMcp.getSettablePaths({ global });
14976
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15109
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15110
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
14977
15111
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14978
- const merged = {
14979
- ...config,
14980
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14981
- };
14982
15112
  return new TaktMcp({
14983
15113
  outputRoot,
14984
15114
  relativeDirPath: paths.relativeDirPath,
14985
15115
  relativeFilePath: paths.relativeFilePath,
14986
- fileContent: dump(merged),
15116
+ fileContent: applySharedConfigPatch({
15117
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15118
+ feature: "mcp",
15119
+ existingContent,
15120
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15121
+ filePath
15122
+ }),
14987
15123
  validate,
14988
15124
  global
14989
15125
  });
@@ -15428,7 +15564,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15428
15564
  ["augmentcode", {
15429
15565
  class: AugmentcodeMcp,
15430
15566
  meta: {
15431
- supportsProject: false,
15567
+ supportsProject: true,
15432
15568
  supportsGlobal: true,
15433
15569
  supportsEnabledTools: false,
15434
15570
  supportsDisabledTools: false
@@ -16010,15 +16146,97 @@ const JuniePermissionsOverrideSchema = z.looseObject({
16010
16146
  defaultBehavior: z.optional(z.string())
16011
16147
  });
16012
16148
  /**
16149
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16150
+ * permission surfaces the canonical coarse-mode mapping can't express:
16151
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16152
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16153
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16154
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16155
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16156
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16157
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16158
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16159
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16160
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16161
+ * scope are supported.
16162
+ *
16163
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16164
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16165
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16166
+ * of scope for this override.
16167
+ *
16168
+ * @example
16169
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16170
+ */
16171
+ const TaktPermissionsOverrideSchema = z.looseObject({
16172
+ step_permission_overrides: z.optional(z.record(z.string(), z.string())),
16173
+ provider_options: z.optional(z.looseObject({}))
16174
+ });
16175
+ /**
16176
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16177
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16178
+ * express, so they are authored here and merged into the shared Amp settings
16179
+ * file, while the shared `permission` block continues to drive the canonical
16180
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16181
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16182
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16183
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16184
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16185
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16186
+ * later fallbacks), preserving author order.
16187
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16188
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16189
+ * confirmation).
16190
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16191
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16192
+ * supported.
16193
+ *
16194
+ * @example
16195
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16196
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16197
+ */
16198
+ const AmpPermissionsOverrideSchema = z.looseObject({
16199
+ permissions: z.optional(z.array(z.looseObject({
16200
+ tool: z.string(),
16201
+ action: z.string()
16202
+ }))),
16203
+ mcpPermissions: z.optional(z.array(z.looseObject({}))),
16204
+ guardedFiles: z.optional(z.looseObject({ allowlist: z.optional(z.array(z.string())) })),
16205
+ dangerouslyAllowAll: z.optional(z.boolean())
16206
+ });
16207
+ /**
16208
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16209
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16210
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16211
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16212
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16213
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16214
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16215
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16216
+ * Fields placed here are merged onto the top level of
16217
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16218
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16219
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16220
+ * Verified against https://antigravity.google/docs/cli/reference and
16221
+ * https://antigravity.google/docs/cli/sandbox.
16222
+ *
16223
+ * @example
16224
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16225
+ */
16226
+ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
16227
+ toolPermission: z.optional(z.string()),
16228
+ enableTerminalSandbox: z.optional(z.boolean())
16229
+ });
16230
+ /**
16013
16231
  * Permissions configuration.
16014
16232
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
16015
16233
  * Values are pattern-to-action mappings for that tool category.
16016
16234
  *
16017
16235
  * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
16018
- * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie` keys are tool-scoped
16019
- * overrides consumed only by their respective translator (see the matching
16020
- * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
16021
- * block and ignores them.
16236
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16237
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16238
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16239
+ * other tool reads the shared `permission` block and ignores them.
16022
16240
  *
16023
16241
  * @example
16024
16242
  * {
@@ -16039,7 +16257,10 @@ const PermissionsConfigSchema = z.looseObject({
16039
16257
  reasonix: z.optional(ReasonixPermissionsOverrideSchema),
16040
16258
  factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
16041
16259
  warp: z.optional(WarpPermissionsOverrideSchema),
16042
- junie: z.optional(JuniePermissionsOverrideSchema)
16260
+ junie: z.optional(JuniePermissionsOverrideSchema),
16261
+ takt: z.optional(TaktPermissionsOverrideSchema),
16262
+ amp: z.optional(AmpPermissionsOverrideSchema),
16263
+ "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema)
16043
16264
  });
16044
16265
  /**
16045
16266
  * Full permissions file schema including optional $schema field.
@@ -16151,6 +16372,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
16151
16372
  * Reference: https://ampcode.com/manual ("amp.permissions").
16152
16373
  */
16153
16374
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16375
+ /**
16376
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16377
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16378
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16379
+ * permissions override. Reference: https://ampcode.com/manual.
16380
+ */
16381
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16382
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16383
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16384
+ /**
16385
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16386
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16387
+ */
16388
+ function ampMatchesCmd(entry) {
16389
+ const cmd = entry.matches?.cmd;
16390
+ return typeof cmd === "string" ? cmd : void 0;
16391
+ }
16392
+ /**
16393
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16394
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16395
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16396
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16397
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16398
+ * override verbatim instead of being flattened with loss or dropped.
16399
+ */
16400
+ function isCanonicalAmpEntry(entry) {
16401
+ if (entry.action === "delegate") return false;
16402
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16403
+ const matches = entry.matches;
16404
+ if (matches === void 0) return true;
16405
+ const keys = Object.keys(matches);
16406
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16407
+ }
16154
16408
  function parseAmpSettings(fileContent) {
16155
16409
  const errors = [];
16156
16410
  const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -16166,10 +16420,20 @@ function toDisableList(value) {
16166
16420
  return value.filter((entry) => typeof entry === "string");
16167
16421
  }
16168
16422
  /**
16423
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16424
+ * settings, returning `undefined` when the key is absent or not an array so the
16425
+ * override omits it.
16426
+ */
16427
+ function extractAmpGuardedAllowlist(value) {
16428
+ if (!Array.isArray(value)) return void 0;
16429
+ return value.filter((entry) => typeof entry === "string");
16430
+ }
16431
+ /**
16169
16432
  * Read an `amp.permissions` array from untrusted parsed settings. Only entries
16170
- * whose shape rulesync understands are retained; the original objects are kept
16171
- * by reference (with a normalized `action`) so unknown sibling keys survive a
16172
- * round-trip when the entry is preserved.
16433
+ * whose `tool`/`action` rulesync understands are retained; every other key
16434
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16435
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16436
+ * round-trip.
16173
16437
  */
16174
16438
  function toPermissionsList(value) {
16175
16439
  if (!Array.isArray(value)) return [];
@@ -16179,14 +16443,11 @@ function toPermissionsList(value) {
16179
16443
  const { tool, action } = raw;
16180
16444
  if (typeof tool !== "string" || typeof action !== "string") continue;
16181
16445
  if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
16182
- const matches = raw.matches;
16183
- let normalizedMatches;
16184
- if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
16185
16446
  entries.push({
16186
16447
  ...raw,
16187
16448
  tool,
16188
16449
  action,
16189
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16450
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
16190
16451
  });
16191
16452
  }
16192
16453
  return entries;
@@ -16211,6 +16472,13 @@ function toPermissionsList(value) {
16211
16472
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
16212
16473
  * and writes merge into the existing JSON rather than overwriting it, and the
16213
16474
  * file is never deleted.
16475
+ *
16476
+ * Amp shapes with no canonical category are authored and round-tripped through
16477
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16478
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16479
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16480
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16481
+ * `amp.dangerouslyAllowAll` settings.
16214
16482
  */
16215
16483
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
16216
16484
  constructor(params) {
@@ -16280,15 +16548,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16280
16548
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
16281
16549
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
16282
16550
  const json = fileContent ? parseAmpSettings(fileContent) : {};
16283
- const { disable, permissions } = convertRulesyncToAmp(rulesyncPermissions.getJson());
16284
- const preservedDelegates = toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16551
+ const config = rulesyncPermissions.getJson();
16552
+ const { disable, permissions } = convertRulesyncToAmp(config);
16553
+ const override = config.amp;
16554
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16555
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16285
16556
  const newJson = {
16286
16557
  ...json,
16287
16558
  [AMP_TOOLS_DISABLE_KEY]: disable
16288
16559
  };
16289
- const mergedPermissions = [...permissions, ...preservedDelegates];
16560
+ const mergedPermissions = mergeAmpPermissions([
16561
+ ...permissions,
16562
+ ...authoredExtras,
16563
+ ...preservedDelegates
16564
+ ]);
16290
16565
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16291
16566
  else delete newJson[AMP_PERMISSIONS_KEY];
16567
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16568
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16569
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16292
16570
  return new AmpPermissions({
16293
16571
  outputRoot,
16294
16572
  relativeDirPath: basePaths.relativeDirPath,
@@ -16299,11 +16577,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16299
16577
  }
16300
16578
  toRulesyncPermissions() {
16301
16579
  const json = parseAmpSettings(this.getFileContent());
16580
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16581
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16582
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16302
16583
  const config = convertAmpToRulesync({
16303
16584
  disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
16304
- permissions: toPermissionsList(json[AMP_PERMISSIONS_KEY])
16305
- });
16306
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16585
+ permissions: canonicalEntries
16586
+ });
16587
+ const ampOverride = {};
16588
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16589
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16590
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16591
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16592
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16593
+ const result = { ...config };
16594
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16595
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16307
16596
  }
16308
16597
  validate() {
16309
16598
  try {
@@ -16350,6 +16639,37 @@ const ACTION_PRIORITY = {
16350
16639
  allow: 2
16351
16640
  };
16352
16641
  /**
16642
+ * Fail-closed action priority for merging the full `amp.permissions` list
16643
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16644
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16645
+ * it never auto-allows) trails as the final fallback.
16646
+ */
16647
+ const MERGE_ACTION_PRIORITY = {
16648
+ reject: 0,
16649
+ ask: 1,
16650
+ allow: 2,
16651
+ delegate: 3
16652
+ };
16653
+ /**
16654
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16655
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16656
+ * order within an action class. Guarantees an authored `reject` cannot be
16657
+ * shadowed by a generated catch-all `allow`.
16658
+ */
16659
+ function mergeAmpPermissions(entries) {
16660
+ const decorated = entries.map((entry, index) => ({
16661
+ entry,
16662
+ index
16663
+ }));
16664
+ decorated.sort((a, b) => {
16665
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16666
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16667
+ if (ap !== bp) return ap - bp;
16668
+ return a.index - b.index;
16669
+ });
16670
+ return decorated.map((d) => d.entry);
16671
+ }
16672
+ /**
16353
16673
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16354
16674
  *
16355
16675
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16414,14 +16734,14 @@ function sortAmpPermissions(entries) {
16414
16734
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16415
16735
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16416
16736
  if (ap !== bp) return ap - bp;
16417
- const aHasCmd = a.entry.matches?.cmd !== void 0 ? 0 : 1;
16418
- const bHasCmd = b.entry.matches?.cmd !== void 0 ? 0 : 1;
16737
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16738
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16419
16739
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16420
16740
  const at = a.entry.tool;
16421
16741
  const bt = b.entry.tool;
16422
16742
  if (at !== bt) return at < bt ? -1 : 1;
16423
- const ac = a.entry.matches?.cmd ?? "";
16424
- const bc = b.entry.matches?.cmd ?? "";
16743
+ const ac = ampMatchesCmd(a.entry) ?? "";
16744
+ const bc = ampMatchesCmd(b.entry) ?? "";
16425
16745
  if (ac !== bc) return ac < bc ? -1 : 1;
16426
16746
  return a.index - b.index;
16427
16747
  });
@@ -16460,7 +16780,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16460
16780
  for (const tool of disable) assign(tool, "*", "deny");
16461
16781
  for (const entry of permissions) {
16462
16782
  if (entry.action === "delegate") continue;
16463
- const pattern = entry.matches?.cmd ?? "*";
16783
+ const pattern = ampMatchesCmd(entry) ?? "*";
16464
16784
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16465
16785
  assign(entry.tool, pattern, action);
16466
16786
  }
@@ -16542,6 +16862,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16542
16862
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16543
16863
  * file (global scope only). The file holds other CLI settings besides
16544
16864
  * permissions, so it is never deleted.
16865
+ *
16866
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16867
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16868
+ * authored and round-tripped through the `antigravity-cli` permissions override
16869
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16545
16870
  */
16546
16871
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16547
16872
  constructor(params) {
@@ -16602,6 +16927,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16602
16927
  ...settings,
16603
16928
  permissions: mergedPermissions
16604
16929
  };
16930
+ const override = config["antigravity-cli"];
16931
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16932
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16605
16933
  const fileContent = JSON.stringify(merged, null, 2);
16606
16934
  return new AntigravityCliPermissions({
16607
16935
  outputRoot,
@@ -16625,7 +16953,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16625
16953
  ask: permissions.ask ?? [],
16626
16954
  deny: permissions.deny ?? []
16627
16955
  });
16628
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16956
+ const override = {};
16957
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
16958
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
16959
+ const result = { ...config };
16960
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
16961
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16629
16962
  }
16630
16963
  validate() {
16631
16964
  return {
@@ -19311,14 +19644,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19311
19644
  return true;
19312
19645
  }
19313
19646
  setFileContent(fileContent) {
19314
- const existing = parseHermesConfig(fileContent);
19315
- const generated = parseHermesConfig(this.fileContent);
19316
- const merged = deepMergeHermesConfig(existing, generated);
19317
- if (generated.permissions !== void 0) merged.permissions = generated.permissions;
19318
- this.fileContent = stringifyHermesConfig(merged);
19647
+ this.fileContent = applySharedConfigPatch({
19648
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19649
+ feature: "permissions",
19650
+ existingContent: fileContent,
19651
+ patch: parseSharedConfig({
19652
+ format: "yaml",
19653
+ fileContent: this.fileContent
19654
+ })
19655
+ });
19319
19656
  }
19320
19657
  toRulesyncPermissions() {
19321
- const config = parseHermesConfig(this.getFileContent());
19658
+ const config = parseSharedConfig({
19659
+ format: "yaml",
19660
+ fileContent: this.getFileContent()
19661
+ });
19322
19662
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
19323
19663
  return new RulesyncPermissions({
19324
19664
  relativeDirPath: "",
@@ -19339,11 +19679,17 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19339
19679
  enabled: true,
19340
19680
  domains: webfetchDeny
19341
19681
  } };
19342
- if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
19682
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19683
+ base: config,
19684
+ patch: permissions.hermes
19685
+ });
19343
19686
  config.permissions = { rulesync: permissions };
19344
19687
  return new HermesagentPermissions({
19345
19688
  outputRoot,
19346
- fileContent: stringifyHermesConfig(config)
19689
+ fileContent: stringifySharedConfig({
19690
+ format: "yaml",
19691
+ document: config
19692
+ })
19347
19693
  });
19348
19694
  }
19349
19695
  };
@@ -20899,6 +21245,8 @@ function isPermissionAction(value) {
20899
21245
  const TAKT_PROVIDER_KEY = "provider";
20900
21246
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20901
21247
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21248
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21249
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20902
21250
  const TAKT_DEFAULT_PROVIDER = "claude";
20903
21251
  const CATCH_ALL_PATTERN = "*";
20904
21252
  /**
@@ -20928,10 +21276,16 @@ const CATCH_ALL_PATTERN = "*";
20928
21276
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20929
21277
  *
20930
21278
  * Both project and global scope are supported. The shared config is merged in
20931
- * place: only `provider_profiles.<provider>.default_permission_mode` is set;
20932
- * the active provider's other keys (e.g. `step_permission_overrides`), every
20933
- * other provider profile, and all other top-level keys are preserved. The file
20934
- * is never deleted.
21279
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21280
+ * derived mode; every other provider profile and all other top-level keys are
21281
+ * preserved. The file is never deleted.
21282
+ *
21283
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21284
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21285
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21286
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21287
+ * (a top-level per-provider sandbox/network table). Both are authored on
21288
+ * generate and re-extracted on import.
20935
21289
  */
20936
21290
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20937
21291
  constructor(params) {
@@ -20963,37 +21317,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20963
21317
  }
20964
21318
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20965
21319
  const paths = TaktPermissions.getSettablePaths({ global });
20966
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21320
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21321
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21322
+ const config = parseSharedConfig({
21323
+ format: "yaml",
21324
+ fileContent: existingContent,
21325
+ filePath,
21326
+ invalidRootPolicy: "error"
21327
+ });
21328
+ const rulesyncJson = rulesyncPermissions.getJson();
20967
21329
  const provider = resolveActiveProvider(config);
20968
- const mode = deriveTaktPermissionMode(rulesyncPermissions.getJson());
20969
- const existingProfiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20970
- const existingProfile = isPlainObject(existingProfiles[provider]) ? existingProfiles[provider] : {};
20971
- const merged = {
20972
- ...config,
20973
- [TAKT_PROVIDER_PROFILES_KEY]: {
20974
- ...existingProfiles,
20975
- [provider]: {
20976
- ...existingProfile,
20977
- [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode
20978
- }
20979
- }
21330
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21331
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21332
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21333
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21334
+ const patch = {
21335
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21336
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21337
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21338
+ } },
21339
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
20980
21340
  };
20981
21341
  return new TaktPermissions({
20982
21342
  outputRoot,
20983
21343
  relativeDirPath: paths.relativeDirPath,
20984
21344
  relativeFilePath: paths.relativeFilePath,
20985
- fileContent: dump(merged),
21345
+ fileContent: applySharedConfigPatch({
21346
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21347
+ feature: "permissions",
21348
+ existingContent,
21349
+ patch,
21350
+ filePath
21351
+ }),
20986
21352
  validate: true,
20987
21353
  global
20988
21354
  });
20989
21355
  }
20990
21356
  toRulesyncPermissions() {
20991
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21357
+ const config = parseSharedConfig({
21358
+ format: "yaml",
21359
+ fileContent: this.getFileContent(),
21360
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
21361
+ invalidRootPolicy: "error"
21362
+ });
20992
21363
  const provider = resolveActiveProvider(config);
20993
21364
  const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20994
- const mode = (isPlainObject(profiles[provider]) ? profiles[provider] : {})[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21365
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21366
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
20995
21367
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
20996
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21368
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21369
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21370
+ const taktOverride = {};
21371
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21372
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21373
+ const result = { ...rulesyncConfig };
21374
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21375
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20997
21376
  }
20998
21377
  validate() {
20999
21378
  return {
@@ -29888,14 +30267,21 @@ def register(ctx):
29888
30267
  `;
29889
30268
  }
29890
30269
  function getEnabledPluginConfigContent(currentContent) {
29891
- const config = parseHermesConfig(currentContent);
30270
+ const config = parseSharedConfig({
30271
+ format: "yaml",
30272
+ fileContent: currentContent
30273
+ });
29892
30274
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29893
30275
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
29894
- config.plugins = {
29895
- ...plugins,
29896
- enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
29897
- };
29898
- return stringifyHermesConfig(config);
30276
+ return applySharedConfigPatch({
30277
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30278
+ feature: "subagents",
30279
+ existingContent: currentContent,
30280
+ patch: { plugins: {
30281
+ ...plugins,
30282
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30283
+ } }
30284
+ });
29899
30285
  }
29900
30286
  function getSubagentSpec(rulesyncSubagent) {
29901
30287
  const json = rulesyncSubagent.getFrontmatter();
@@ -29981,6 +30367,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
29981
30367
  static getSettablePaths() {
29982
30368
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
29983
30369
  }
30370
+ /**
30371
+ * Beyond the subagent spec files, generation also read-modify-writes the
30372
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30373
+ * so the write must be declared for the shared-file order derivation.
30374
+ */
30375
+ static getExtraSharedWritePaths() {
30376
+ return [{
30377
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30378
+ relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
30379
+ }];
30380
+ }
29984
30381
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
29985
30382
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
29986
30383
  }
@@ -31719,7 +32116,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
31719
32116
  })),
31720
32117
  kiro: z.optional(z.looseObject({
31721
32118
  inclusion: z.optional(z.string()),
31722
- fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())]))
32119
+ fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())])),
32120
+ name: z.optional(z.string()),
32121
+ description: z.optional(z.string())
31723
32122
  })),
31724
32123
  takt: z.optional(z.looseObject({
31725
32124
  name: z.optional(z.string()),
@@ -34635,7 +35034,8 @@ function toFileMatchPattern(globs) {
34635
35034
  *
34636
35035
  * Precedence:
34637
35036
  * 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
34638
- * taken from the block, or derived from `globs` when omitted for `fileMatch`).
35037
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35038
+ * and with the companion `name`/`description` carried through for `auto`).
34639
35039
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
34640
35040
  * to matching files instead of leaving it implicitly always-on.
34641
35041
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -34653,6 +35053,11 @@ function deriveKiroInclusion({ kiro, globs }) {
34653
35053
  fileMatchPattern
34654
35054
  } : { inclusion: "fileMatch" };
34655
35055
  }
35056
+ if (kiro.inclusion === "auto") return {
35057
+ inclusion: "auto",
35058
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35059
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35060
+ };
34656
35061
  return { inclusion: kiro.inclusion };
34657
35062
  }
34658
35063
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -34724,6 +35129,8 @@ var KiroRule = class KiroRule extends ToolRule {
34724
35129
  const rawPattern = frontmatter.fileMatchPattern;
34725
35130
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
34726
35131
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35132
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35133
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
34727
35134
  return new RulesyncRule({
34728
35135
  outputRoot: process.cwd(),
34729
35136
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -34734,7 +35141,9 @@ var KiroRule = class KiroRule extends ToolRule {
34734
35141
  globs,
34735
35142
  kiro: {
34736
35143
  inclusion,
34737
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35144
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35145
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35146
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34738
35147
  }
34739
35148
  },
34740
35149
  body
@@ -36908,6 +37317,197 @@ function buildPermissionsStrategy(ctx) {
36908
37317
  };
36909
37318
  }
36910
37319
  //#endregion
37320
+ //#region src/types/processor-registry.ts
37321
+ const PROCESSOR_REGISTRY = [
37322
+ {
37323
+ feature: "rules",
37324
+ processor: RulesProcessor,
37325
+ schema: RulesProcessorToolTargetSchema,
37326
+ factory: toolRuleFactories
37327
+ },
37328
+ {
37329
+ feature: "ignore",
37330
+ processor: IgnoreProcessor,
37331
+ schema: IgnoreProcessorToolTargetSchema,
37332
+ factory: toolIgnoreFactories
37333
+ },
37334
+ {
37335
+ feature: "mcp",
37336
+ processor: McpProcessor,
37337
+ schema: McpProcessorToolTargetSchema,
37338
+ factory: toolMcpFactories
37339
+ },
37340
+ {
37341
+ feature: "commands",
37342
+ processor: CommandsProcessor,
37343
+ schema: CommandsProcessorToolTargetSchema,
37344
+ factory: toolCommandFactories
37345
+ },
37346
+ {
37347
+ feature: "subagents",
37348
+ processor: SubagentsProcessor,
37349
+ schema: SubagentsProcessorToolTargetSchema,
37350
+ factory: toolSubagentFactories
37351
+ },
37352
+ {
37353
+ feature: "skills",
37354
+ processor: SkillsProcessor,
37355
+ schema: SkillsProcessorToolTargetSchema,
37356
+ factory: toolSkillFactories
37357
+ },
37358
+ {
37359
+ feature: "hooks",
37360
+ processor: HooksProcessor,
37361
+ schema: HooksProcessorToolTargetSchema,
37362
+ factory: toolHooksFactories
37363
+ },
37364
+ {
37365
+ feature: "permissions",
37366
+ processor: PermissionsProcessor,
37367
+ schema: PermissionsProcessorToolTargetSchema,
37368
+ factory: toolPermissionsFactories
37369
+ }
37370
+ ];
37371
+ const getProcessorRegistryEntry = (feature) => {
37372
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37373
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37374
+ return entry;
37375
+ };
37376
+ //#endregion
37377
+ //#region src/lib/shared-file-derive.ts
37378
+ /**
37379
+ * The single declaration of the cross-feature write order for shared
37380
+ * (read-modify-write) config files: when two features write the same on-disk
37381
+ * file, the earlier one writes first and the later one merges on top, so the
37382
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37383
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37384
+ * The generation step graph's `dependsOn` edges are derived from this list
37385
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37386
+ * shared path never requires touching the graph by hand.
37387
+ */
37388
+ const SHARED_WRITE_FEATURE_ORDER = [
37389
+ "ignore",
37390
+ "subagents",
37391
+ "mcp",
37392
+ "hooks",
37393
+ "permissions",
37394
+ "rules"
37395
+ ];
37396
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37397
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37398
+ const sharedFileKey = (path) => {
37399
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37400
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37401
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37402
+ };
37403
+ const settablePathsForScope = (cls, global) => {
37404
+ const paths = [];
37405
+ let settable;
37406
+ try {
37407
+ settable = cls.getSettablePaths?.({ global });
37408
+ } catch {
37409
+ return paths;
37410
+ }
37411
+ if (settable?.relativeFilePath) paths.push({
37412
+ relativeDirPath: settable.relativeDirPath ?? ".",
37413
+ relativeFilePath: settable.relativeFilePath
37414
+ });
37415
+ if (settable?.root) {
37416
+ paths.push(settable.root);
37417
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37418
+ }
37419
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37420
+ return paths;
37421
+ };
37422
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37423
+ /**
37424
+ * Derive, from the processor registry, every on-disk file that two or more
37425
+ * features read-modify-write. Source of truth the generation step graph's
37426
+ * `writesSharedFile` declarations must match.
37427
+ */
37428
+ const deriveSharedFileWriters = () => {
37429
+ const byKey = /* @__PURE__ */ new Map();
37430
+ const pathByKey = /* @__PURE__ */ new Map();
37431
+ for (const entry of PROCESSOR_REGISTRY) {
37432
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37433
+ const factories = entry.factory;
37434
+ for (const [tool, factory] of factories) {
37435
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37436
+ for (const path of collectFactoryPaths(factory)) {
37437
+ const key = sharedFileKey(path);
37438
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37439
+ let features = byKey.get(key);
37440
+ if (!features) {
37441
+ features = /* @__PURE__ */ new Map();
37442
+ byKey.set(key, features);
37443
+ }
37444
+ let tools = features.get(entry.feature);
37445
+ if (!tools) {
37446
+ tools = /* @__PURE__ */ new Set();
37447
+ features.set(entry.feature, tools);
37448
+ }
37449
+ tools.add(tool);
37450
+ }
37451
+ }
37452
+ }
37453
+ const writers = [];
37454
+ for (const [key, features] of byKey) {
37455
+ if (features.size < 2) continue;
37456
+ const path = pathByKey.get(key);
37457
+ const toolsByFeature = /* @__PURE__ */ new Map();
37458
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37459
+ writers.push({
37460
+ key,
37461
+ relativeDirPath: path.relativeDirPath,
37462
+ relativeFilePath: path.relativeFilePath,
37463
+ features: [...features.keys()].toSorted(),
37464
+ toolsByFeature
37465
+ });
37466
+ }
37467
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37468
+ };
37469
+ /**
37470
+ * Derive, per shared-write feature, the shared files it writes and the
37471
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37472
+ * writer depends on all writers that precede it in
37473
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37474
+ * graph consumes, so registry changes (a new tool, a new settable path)
37475
+ * propagate into the execution order without a hand-maintained declaration.
37476
+ *
37477
+ * @throws Error if a feature writes a shared file but has no position in
37478
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37479
+ * whose merge policy wins, so it must be made explicitly there.
37480
+ */
37481
+ const deriveSharedWriteSteps = () => {
37482
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37483
+ const filesByFeature = /* @__PURE__ */ new Map();
37484
+ const depsByFeature = /* @__PURE__ */ new Map();
37485
+ for (const writer of deriveSharedFileWriters()) {
37486
+ for (const feature of writer.features) if (!orderIndex.has(feature)) throw new Error(`Feature '${feature}' writes the shared file '${writer.key}' but has no position in SHARED_WRITE_FEATURE_ORDER. Decide where its writes merge relative to the other features and add it to the order.`);
37487
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37488
+ for (const [position, feature] of ordered.entries()) {
37489
+ let files = filesByFeature.get(feature);
37490
+ if (!files) {
37491
+ files = /* @__PURE__ */ new Set();
37492
+ filesByFeature.set(feature, files);
37493
+ }
37494
+ files.add(writer.key);
37495
+ let deps = depsByFeature.get(feature);
37496
+ if (!deps) {
37497
+ deps = /* @__PURE__ */ new Set();
37498
+ depsByFeature.set(feature, deps);
37499
+ }
37500
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37501
+ }
37502
+ }
37503
+ const steps = /* @__PURE__ */ new Map();
37504
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37505
+ writesSharedFile: [...files].toSorted(),
37506
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37507
+ });
37508
+ return steps;
37509
+ };
37510
+ //#endregion
36911
37511
  //#region src/lib/generate.ts
36912
37512
  async function processFeatureGeneration(params) {
36913
37513
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -37057,98 +37657,53 @@ function resolveExecutionOrder(steps) {
37057
37657
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
37058
37658
  return ordered;
37059
37659
  }
37660
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37661
+ const sharedWriteMeta = (id) => {
37662
+ const step = SHARED_WRITE_STEPS.get(id);
37663
+ return step ? {
37664
+ writesSharedFile: step.writesSharedFile,
37665
+ dependsOn: step.dependsOn
37666
+ } : {};
37667
+ };
37060
37668
  /**
37061
37669
  * The static shape of the generation step graph: which steps write which shared
37062
37670
  * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
37063
- * for those writers. Exported (separately from the `run` closures, which need a
37064
- * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
37065
- * tested directly against the real graph rather than a hand-copied one. Readonly
37066
- * so a consumer can't mutate this module-level singleton and affect every
37067
- * subsequent `generate()` call in the process.
37671
+ * for those writers. Both are derived from the processor registry's settable
37672
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37673
+ * tool or shared path never requires editing this graph. Exported (separately
37674
+ * from the `run` closures, which need a live `config`/`logger`) so
37675
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37676
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37677
+ * mutate this module-level singleton and affect every subsequent `generate()`
37678
+ * call in the process.
37068
37679
  */
37069
37680
  const GENERATION_STEP_GRAPH = [
37070
37681
  {
37071
37682
  id: "ignore",
37072
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37683
+ ...sharedWriteMeta("ignore")
37073
37684
  },
37074
37685
  {
37075
37686
  id: "mcp",
37076
- writesSharedFile: [
37077
- ".amp/settings.json",
37078
- ".augment/settings.json",
37079
- ".codex/config.toml",
37080
- ".config/amp/settings.json",
37081
- ".config/devin/config.json",
37082
- ".config/opencode/opencode.json",
37083
- ".config/zed/settings.json",
37084
- ".devin/config.json",
37085
- ".grok/config.toml",
37086
- ".hermes/config.yaml",
37087
- ".qwen/settings.json",
37088
- ".reasonix/config.toml",
37089
- ".takt/config.yaml",
37090
- ".vibe/config.toml",
37091
- ".zed/settings.json",
37092
- "kilo.json",
37093
- "opencode.json",
37094
- "reasonix.toml"
37095
- ],
37096
- dependsOn: ["ignore"]
37687
+ ...sharedWriteMeta("mcp")
37097
37688
  },
37098
37689
  { id: "commands" },
37099
- { id: "subagents" },
37690
+ {
37691
+ id: "subagents",
37692
+ ...sharedWriteMeta("subagents")
37693
+ },
37100
37694
  { id: "skills" },
37101
37695
  {
37102
37696
  id: "hooks",
37103
- writesSharedFile: [
37104
- ".augment/settings.json",
37105
- ".claude/settings.json",
37106
- ".codex/config.toml",
37107
- ".config/devin/config.json",
37108
- ".hermes/config.yaml",
37109
- ".kiro/agents/default.json",
37110
- ".qwen/settings.json",
37111
- ".vibe/config.toml"
37112
- ],
37113
- dependsOn: ["ignore", "mcp"]
37697
+ ...sharedWriteMeta("hooks")
37114
37698
  },
37115
37699
  {
37116
37700
  id: "permissions",
37117
- writesSharedFile: [
37118
- ".amp/settings.json",
37119
- ".augment/settings.json",
37120
- ".claude/settings.json",
37121
- ".codex/config.toml",
37122
- ".config/amp/settings.json",
37123
- ".config/devin/config.json",
37124
- ".config/opencode/opencode.json",
37125
- ".config/zed/settings.json",
37126
- ".devin/config.json",
37127
- ".grok/config.toml",
37128
- ".hermes/config.yaml",
37129
- ".kiro/agents/default.json",
37130
- ".qwen/settings.json",
37131
- ".reasonix/config.toml",
37132
- ".takt/config.yaml",
37133
- ".vibe/config.toml",
37134
- ".zed/settings.json",
37135
- "opencode.json",
37136
- "reasonix.toml"
37137
- ],
37138
- dependsOn: [
37139
- "ignore",
37140
- "hooks",
37141
- "mcp"
37142
- ]
37701
+ ...sharedWriteMeta("permissions")
37143
37702
  },
37144
37703
  {
37145
37704
  id: "rules",
37146
- writesSharedFile: ["kilo.json", "opencode.json"],
37147
- dependsOn: [
37148
- "mcp",
37149
- "skills",
37150
- "permissions"
37151
- ]
37705
+ ...sharedWriteMeta("rules"),
37706
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
37152
37707
  }
37153
37708
  ];
37154
37709
  /**
@@ -37840,6 +38395,6 @@ async function importPermissionsCore(params) {
37840
38395
  return writtenCount;
37841
38396
  }
37842
38397
  //#endregion
37843
- export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
38398
+ export { toPosixPath as $, RulesyncCommand as A, createTempDirectory as B, RulesyncHooks as C, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, ALL_FEATURES as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, formatError as Et, ConsoleLogger as F, getFileSize as G, ensureDir as H, JsonLogger as I, listDirectoryFiles as J, getHomeDirectory as K, CLIError as L, stringifyFrontmatter as M, ConfigResolver as N, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_FEATURES_WITH_WILDCARD as Ot, findControlCharacter as P, removeTempDirectory as Q, ErrorCodes as R, HooksProcessor as S, RULESYNC_RULES_RELATIVE_DIR_PATH as St, CLAUDECODE_DIR as T, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Tt, fileExists as U, directoryExists as V, findFilesByGlobs as W, removeDirectory as X, readFileContent as Y, removeFile as Z, RulesyncPermissions as _, RULESYNC_MCP_SCHEMA_URL as _t, convertFromTool as a, RULESYNC_AIIGNORE_FILE_NAME as at, IgnoreProcessor as b, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_HOOKS_FILE_NAME as dt, writeFileContent as et, SkillsProcessor as f, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_MCP_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_MCP_FILE_NAME as ht, getProcessorRegistryEntry as i, MAX_FILE_SIZE as it, RulesyncCommandFrontmatterSchema as j, CLAUDECODE_SKILLS_DIR_PATH as k, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as lt, RulesyncSkill as m, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as mt, checkRulesyncDirExists as n, ALL_TOOL_TARGETS_WITH_WILDCARD as nt, RulesProcessor as o, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ot, getLocalSkillDirNames as p, RULESYNC_IGNORE_RELATIVE_FILE_PATH as pt, isSymlink as q, generate as r, ToolTargetSchema as rt, RulesyncRule as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as st, importFromTool as t, ALL_TOOL_TARGETS as tt, RulesyncSubagent as u, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ut, McpProcessor as v, RULESYNC_OVERVIEW_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as wt, RulesyncIgnore as x, RULESYNC_RELATIVE_DIR_PATH as xt, RulesyncMcp as y, RULESYNC_PERMISSIONS_FILE_NAME as yt, checkPathTraversal as z };
37844
38399
 
37845
- //# sourceMappingURL=import-B7VzSVUK.js.map
38400
+ //# sourceMappingURL=import-BxqZVTKb.js.map