rulesync 9.3.0 → 9.5.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.
@@ -852,7 +852,7 @@ const SourceEntrySchema = z.object({
852
852
  scope: optional(z.enum(["project", "user"]))
853
853
  });
854
854
  const ConfigParamsSchema = z.object({
855
- outputRoots: z.array(z.string()),
855
+ outputRoots: z.union([z.array(z.string()), z.record(z.string(), z.union([z.string(), z.array(z.string())]))]),
856
856
  targets: RulesyncConfigTargetsSchema,
857
857
  features: RulesyncFeaturesSchema,
858
858
  verbose: z.boolean(),
@@ -957,6 +957,7 @@ var Config = class Config {
957
957
  const resolvedTargets = targets ?? [];
958
958
  const resolvedFeatures = features ?? [];
959
959
  this.validateObjectFormTargetKeys(resolvedTargets);
960
+ this.validateObjectFormOutputRootKeys(outputRoots);
960
961
  this.validateConflictingTargets(resolvedTargets);
961
962
  if (dryRun && check) throw new Error("--dry-run and --check cannot be used together");
962
963
  this.outputRoots = outputRoots;
@@ -994,6 +995,11 @@ var Config = class Config {
994
995
  if (!validTargets.has(key)) throw new Error(`Unknown target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
995
996
  }
996
997
  }
998
+ validateObjectFormOutputRootKeys(outputRoots) {
999
+ if (Array.isArray(outputRoots)) return;
1000
+ const validTargets = new Set(ALL_TOOL_TARGETS);
1001
+ for (const key of Object.keys(outputRoots)) if (!validTargets.has(key)) throw new Error(`Unknown outputRoots target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
1002
+ }
997
1003
  validateConflictingTargets(targets) {
998
1004
  const has = (target) => {
999
1005
  if (Array.isArray(targets)) return targets.includes(target);
@@ -1001,8 +1007,19 @@ var Config = class Config {
1001
1007
  };
1002
1008
  for (const [target1, target2] of CONFLICTING_TARGET_PAIRS) if (has(target1) && has(target2)) throw new Error(`Conflicting targets: '${target1}' and '${target2}' cannot be used together. Please choose one.`);
1003
1009
  }
1004
- getOutputRoots() {
1005
- return this.outputRoots;
1010
+ getOutputRoots(target) {
1011
+ if (Array.isArray(this.outputRoots)) return this.outputRoots;
1012
+ if (target) {
1013
+ const targetOutputRoots = this.outputRoots[target];
1014
+ if (targetOutputRoots === void 0) return [];
1015
+ return Array.isArray(targetOutputRoots) ? targetOutputRoots : [targetOutputRoots];
1016
+ }
1017
+ const allRoots = [];
1018
+ for (const value of Object.values(this.outputRoots)) {
1019
+ if (value === void 0) continue;
1020
+ allRoots.push(...Array.isArray(value) ? value : [value]);
1021
+ }
1022
+ return [...new Set(allRoots)];
1006
1023
  }
1007
1024
  /**
1008
1025
  * Filter an arbitrary string-key list down to the known `ToolTarget` set,
@@ -1364,10 +1381,21 @@ var ConfigResolver = class {
1364
1381
  };
1365
1382
  function getOutputRootsInLightOfGlobal({ outputRoots, global }) {
1366
1383
  if (global) return [getHomeDirectory()];
1367
- outputRoots.forEach((outputRoot) => {
1368
- validateOutputRoot(outputRoot);
1369
- });
1370
- return outputRoots.map((outputRoot) => resolve(outputRoot));
1384
+ if (Array.isArray(outputRoots)) {
1385
+ outputRoots.forEach((outputRoot) => {
1386
+ validateOutputRoot(outputRoot);
1387
+ });
1388
+ return outputRoots.map((outputRoot) => resolve(outputRoot));
1389
+ }
1390
+ const resolvedOutputRoots = {};
1391
+ for (const [target, targetOutputRoots] of Object.entries(outputRoots)) {
1392
+ const roots = Array.isArray(targetOutputRoots) ? targetOutputRoots : [targetOutputRoots];
1393
+ roots.forEach((outputRoot) => {
1394
+ validateOutputRoot(outputRoot);
1395
+ });
1396
+ resolvedOutputRoots[target] = Array.isArray(targetOutputRoots) ? roots.map((outputRoot) => resolve(outputRoot)) : resolve(targetOutputRoots);
1397
+ }
1398
+ return resolvedOutputRoots;
1371
1399
  }
1372
1400
  function extractConfigFileTargets(targets) {
1373
1401
  if (targets === void 0) return void 0;
@@ -3892,6 +3920,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3892
3920
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3893
3921
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3894
3922
  //#endregion
3923
+ //#region src/constants/takt-paths.ts
3924
+ const TAKT_DIR = ".takt";
3925
+ const TAKT_FACETS_SUBDIR = "facets";
3926
+ const TAKT_FACETS_DIR_PATH = join(TAKT_DIR, TAKT_FACETS_SUBDIR);
3927
+ const TAKT_RULES_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "policies");
3928
+ const TAKT_COMMANDS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "instructions");
3929
+ const TAKT_SKILLS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "knowledge");
3930
+ const TAKT_SUBAGENTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "personas");
3931
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = join(TAKT_FACETS_DIR_PATH, "output-contracts");
3932
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3933
+ /**
3934
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3935
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3936
+ * profiles (including permission modes), and other Takt settings.
3937
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3938
+ */
3939
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3940
+ /**
3941
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3942
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3943
+ * transport must be explicitly enabled here before any workflow-defined MCP
3944
+ * server using it is permitted to run.
3945
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3946
+ */
3947
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3948
+ //#endregion
3949
+ //#region src/utils/prototype-pollution.ts
3950
+ /**
3951
+ * Keys that, if walked into when constructing or merging objects from
3952
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3953
+ * chain) and propagate state to every other object in the runtime. Any code
3954
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3955
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3956
+ */
3957
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3958
+ "__proto__",
3959
+ "constructor",
3960
+ "prototype"
3961
+ ]);
3962
+ function isPrototypePollutionKey(key) {
3963
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3964
+ }
3965
+ /**
3966
+ * Returns a shallow copy of a record's own entries with every
3967
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3968
+ *
3969
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3970
+ * or `headers` table — into freshly generated config. Carrying such a map by
3971
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3972
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3973
+ * prototype). Walking the entries through this helper severs that path while
3974
+ * preserving every legitimate key.
3975
+ */
3976
+ function omitPrototypePollutionKeys(record) {
3977
+ const sanitized = {};
3978
+ for (const [key, value] of Object.entries(record)) {
3979
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3980
+ sanitized[key] = value;
3981
+ }
3982
+ return sanitized;
3983
+ }
3984
+ //#endregion
3985
+ //#region src/features/shared/shared-config-gateway.ts
3986
+ function sanitizeSharedConfigValue(value) {
3987
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
3988
+ if (!isPlainObject(value)) return value;
3989
+ const result = {};
3990
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
3991
+ return result;
3992
+ }
3993
+ /**
3994
+ * Parse a shared config file into a plain document: an empty/whitespace file
3995
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
3996
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
3997
+ * path when one is given.
3998
+ */
3999
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
4000
+ if (fileContent.trim() === "") return {};
4001
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
4002
+ let parsed;
4003
+ try {
4004
+ if (format === "yaml") parsed = load(fileContent);
4005
+ else if (format === "json") parsed = JSON.parse(fileContent);
4006
+ else parsed = parse(fileContent);
4007
+ } catch (error) {
4008
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4009
+ }
4010
+ if (parsed === void 0 || parsed === null) return {};
4011
+ if (!isPlainObject(parsed)) {
4012
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
4013
+ return {};
4014
+ }
4015
+ return sanitizeSharedConfigValue(parsed);
4016
+ }
4017
+ /**
4018
+ * Serialize a shared config document. YAML output always ends with exactly one
4019
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4020
+ * writers have always emitted (no trailing newline).
4021
+ */
4022
+ function stringifySharedConfig({ format, document }) {
4023
+ if (format === "yaml") return dump(document, {
4024
+ noRefs: true,
4025
+ sortKeys: false
4026
+ }).trimEnd() + "\n";
4027
+ return JSON.stringify(document, null, 2);
4028
+ }
4029
+ /**
4030
+ * Shallow merge: every top-level key in `patch` replaces the base key
4031
+ * wholesale; all other base keys are preserved. The policy for a feature that
4032
+ * owns a fixed set of top-level keys.
4033
+ */
4034
+ function mergeSharedConfigShallow({ base, patch }) {
4035
+ return {
4036
+ ...base,
4037
+ ...sanitizeSharedConfigValue(patch)
4038
+ };
4039
+ }
4040
+ /**
4041
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4042
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4043
+ * feature whose contribution interleaves with user-authored siblings at any
4044
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4045
+ * per-provider option tables) — nested sibling keys are preserved by
4046
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4047
+ * keys are dropped.
4048
+ */
4049
+ function mergeSharedConfigDeep({ base, patch }) {
4050
+ const result = { ...base };
4051
+ for (const [key, patchValue] of Object.entries(patch)) {
4052
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4053
+ const baseValue = result[key];
4054
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4055
+ base: baseValue,
4056
+ patch: patchValue
4057
+ });
4058
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4059
+ }
4060
+ return result;
4061
+ }
4062
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4063
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4064
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4065
+ /**
4066
+ * Who owns what in each gateway-managed shared config file, and which policy
4067
+ * resolves conflicts. Keys are `dir/file` tokens matching
4068
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4069
+ * lock-step with the writers derived from the processor registry, so an
4070
+ * undeclared writer fails CI instead of merging by accident.
4071
+ */
4072
+ const SHARED_CONFIG_OWNERSHIP = {
4073
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4074
+ format: "json",
4075
+ features: {
4076
+ ignore: {
4077
+ kind: "custom",
4078
+ policyFunction: "applyIgnoreReadDenies"
4079
+ },
4080
+ hooks: {
4081
+ kind: "replace-owned-keys",
4082
+ ownedKeys: ["hooks"]
4083
+ },
4084
+ permissions: {
4085
+ kind: "custom",
4086
+ policyFunction: "applyPermissions"
4087
+ }
4088
+ }
4089
+ },
4090
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4091
+ format: "yaml",
4092
+ features: {
4093
+ subagents: {
4094
+ kind: "replace-owned-keys",
4095
+ ownedKeys: ["plugins"]
4096
+ },
4097
+ mcp: {
4098
+ kind: "replace-owned-keys",
4099
+ ownedKeys: ["mcp_servers"]
4100
+ },
4101
+ hooks: {
4102
+ kind: "replace-owned-keys",
4103
+ ownedKeys: ["hooks"]
4104
+ },
4105
+ permissions: {
4106
+ kind: "deep-merge",
4107
+ replaceKeys: ["permissions"]
4108
+ }
4109
+ }
4110
+ },
4111
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4112
+ format: "yaml",
4113
+ invalidRootPolicy: "error",
4114
+ features: {
4115
+ mcp: {
4116
+ kind: "replace-owned-keys",
4117
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4118
+ },
4119
+ permissions: { kind: "deep-merge" }
4120
+ }
4121
+ }
4122
+ };
4123
+ /**
4124
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4125
+ * the existing content, merge the patch under the feature's declared policy,
4126
+ * and serialize. Throws when the file or feature is undeclared, when a
4127
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4128
+ * feature's policy is `custom` (those calls go to the named policy function
4129
+ * instead).
4130
+ */
4131
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4132
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4133
+ 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.`);
4134
+ const policy = declaration.features[feature];
4135
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4136
+ 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.`);
4137
+ const base = parseSharedConfig({
4138
+ format: declaration.format,
4139
+ fileContent: existingContent,
4140
+ filePath,
4141
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4142
+ });
4143
+ if (policy.kind === "replace-owned-keys") {
4144
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4145
+ 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.`);
4146
+ return stringifySharedConfig({
4147
+ format: declaration.format,
4148
+ document: mergeSharedConfigShallow({
4149
+ base,
4150
+ patch
4151
+ })
4152
+ });
4153
+ }
4154
+ const merged = mergeSharedConfigDeep({
4155
+ base,
4156
+ patch
4157
+ });
4158
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4159
+ return stringifySharedConfig({
4160
+ format: declaration.format,
4161
+ document: merged
4162
+ });
4163
+ }
4164
+ const READ_TOOL_NAME = "Read";
4165
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4166
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4167
+ const parsePermissionsBlock = (settings) => {
4168
+ const permissions = settings.permissions ?? {};
4169
+ return {
4170
+ allow: permissions.allow ?? [],
4171
+ ask: permissions.ask ?? [],
4172
+ deny: permissions.deny ?? []
4173
+ };
4174
+ };
4175
+ const withPermissions = (settings, next) => {
4176
+ const permissions = { ...settings.permissions };
4177
+ const assign = (key, values) => {
4178
+ if (values.length > 0) permissions[key] = values;
4179
+ else delete permissions[key];
4180
+ };
4181
+ assign("allow", next.allow);
4182
+ assign("ask", next.ask);
4183
+ assign("deny", next.deny);
4184
+ return {
4185
+ ...settings,
4186
+ permissions
4187
+ };
4188
+ };
4189
+ const applyIgnoreReadDenies = (params) => {
4190
+ const { settings, readDenies } = params;
4191
+ const current = parsePermissionsBlock(settings);
4192
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4193
+ return withPermissions(settings, {
4194
+ allow: current.allow,
4195
+ ask: current.ask,
4196
+ deny: uniq([...preservedDeny, ...readDenies].toSorted())
4197
+ });
4198
+ };
4199
+ const applyPermissions = (params) => {
4200
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4201
+ const current = parsePermissionsBlock(settings);
4202
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4203
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4204
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4205
+ 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.`);
4206
+ }
4207
+ return withPermissions(settings, {
4208
+ allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
4209
+ ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
4210
+ deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
4211
+ });
4212
+ };
4213
+ //#endregion
3895
4214
  //#region src/features/opencode-config.ts
3896
4215
  /**
3897
4216
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3916,9 +4235,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3916
4235
  });
3917
4236
  const fileContent = await readFileContentOrNull(join(configDir, "opencode.jsonc")) ?? await readFileContentOrNull(join(configDir, "opencode.json"));
3918
4237
  if (!fileContent) return {};
3919
- const parsed = parse(fileContent);
3920
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3921
- return parsed;
4238
+ return parseSharedConfig({
4239
+ format: "jsonc",
4240
+ fileContent
4241
+ });
3922
4242
  }
3923
4243
  /**
3924
4244
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4827,32 +5147,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4827
5147
  }
4828
5148
  };
4829
5149
  //#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
5150
  //#region src/features/takt-shared.ts
4857
5151
  /**
4858
5152
  * Shared utilities for all TAKT-* tool file classes.
@@ -6330,42 +6624,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6330
6624
  */
6331
6625
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6332
6626
  //#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
6627
  //#region src/features/hooks/tool-hooks-converter.ts
6370
6628
  function isToolMatcherEntry(x) {
6371
6629
  if (x === null || typeof x !== "object") return false;
@@ -7024,24 +7282,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7024
7282
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7025
7283
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7026
7284
  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
7285
  const config = rulesyncHooks.getJson();
7034
- const claudeHooks = canonicalToToolHooks({
7035
- config,
7036
- toolOverrideHooks: config.claudecode?.hooks,
7037
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7038
- logger
7286
+ const fileContent = applySharedConfigPatch({
7287
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7288
+ feature: "hooks",
7289
+ existingContent,
7290
+ patch: { hooks: canonicalToToolHooks({
7291
+ config,
7292
+ toolOverrideHooks: config.claudecode?.hooks,
7293
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7294
+ logger
7295
+ }) },
7296
+ filePath
7039
7297
  });
7040
- const merged = {
7041
- ...settings,
7042
- hooks: claudeHooks
7043
- };
7044
- const fileContent = JSON.stringify(merged, null, 2);
7045
7298
  return new ClaudecodeHooks({
7046
7299
  outputRoot,
7047
7300
  relativeDirPath: paths.relativeDirPath,
@@ -8273,55 +8526,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8273
8526
  }
8274
8527
  };
8275
8528
  //#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
8529
  //#region src/features/hooks/hermesagent-hooks.ts
8326
8530
  /**
8327
8531
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8449,10 +8653,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8449
8653
  return true;
8450
8654
  }
8451
8655
  setFileContent(fileContent) {
8452
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8656
+ this.fileContent = applySharedConfigPatch({
8657
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8658
+ feature: "hooks",
8659
+ existingContent: fileContent,
8660
+ patch: parseSharedConfig({
8661
+ format: "yaml",
8662
+ fileContent: this.fileContent
8663
+ })
8664
+ });
8453
8665
  }
8454
8666
  toRulesyncHooks() {
8455
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8667
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8668
+ format: "yaml",
8669
+ fileContent: this.getFileContent()
8670
+ }).hooks);
8456
8671
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8457
8672
  version: 1,
8458
8673
  hooks
@@ -8462,11 +8677,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8462
8677
  const config = rulesyncHooks.getJson();
8463
8678
  return new HermesagentHooks({
8464
8679
  outputRoot,
8465
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8466
- config,
8467
- toolOverrideHooks: config.hermesagent?.hooks,
8468
- logger
8469
- }) })
8680
+ fileContent: stringifySharedConfig({
8681
+ format: "yaml",
8682
+ document: { hooks: canonicalToHermesHooks({
8683
+ config,
8684
+ toolOverrideHooks: config.hermesagent?.hooks,
8685
+ logger
8686
+ }) }
8687
+ })
8470
8688
  });
8471
8689
  }
8472
8690
  };
@@ -10486,66 +10704,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10486
10704
  }
10487
10705
  };
10488
10706
  //#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
10707
  //#region src/features/ignore/claudecode-ignore.ts
10550
10708
  const DEFAULT_FILE_MODE = "shared";
10551
10709
  /**
@@ -11968,7 +12126,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11968
12126
  };
11969
12127
  //#endregion
11970
12128
  //#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
12129
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
11973
12130
  const configPath = join(relativeDirPath, relativeFilePath);
11974
12131
  let parsed;
@@ -11983,13 +12140,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
11983
12140
  /**
11984
12141
  * AugmentCode (Auggie CLI) MCP servers.
11985
12142
  *
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
12143
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12144
+ * at either scope: the committed workspace file for team-shared servers (project)
12145
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
11989
12146
  * `toolPermissions`, so generation merges the `mcpServers` block into the
11990
- * existing settings instead of overwriting it, and the file is never deleted.
12147
+ * existing settings instead of overwriting it, and the file is never deleted. On
12148
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12149
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
11991
12150
  *
11992
- * @see https://docs.augmentcode.com/cli/integrations
12151
+ * @see https://docs.augmentcode.com/cli/config
11993
12152
  */
11994
12153
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
11995
12154
  json;
@@ -12011,9 +12170,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12011
12170
  };
12012
12171
  }
12013
12172
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12014
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12015
12173
  const paths = this.getSettablePaths({ global });
12016
- const json = parseAugmentcodeSettings(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12174
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12175
+ outputRoot,
12176
+ relativeDirPath: paths.relativeDirPath,
12177
+ baseFileName: paths.relativeFilePath,
12178
+ baseFallbackContent: "{}",
12179
+ includeLocalOverlay: !global
12180
+ }), paths.relativeDirPath, paths.relativeFilePath);
12017
12181
  const newJson = {
12018
12182
  ...json,
12019
12183
  mcpServers: json.mcpServers ?? {}
@@ -12028,7 +12192,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12028
12192
  });
12029
12193
  }
12030
12194
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12031
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12032
12195
  const paths = this.getSettablePaths({ global });
12033
12196
  const merged = {
12034
12197
  ...parseAugmentcodeSettings(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -13678,7 +13841,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13678
13841
  config;
13679
13842
  constructor(params) {
13680
13843
  super(params);
13681
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13844
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13845
+ format: "yaml",
13846
+ fileContent: this.fileContent
13847
+ }) : {};
13682
13848
  }
13683
13849
  getConfig() {
13684
13850
  return this.config;
@@ -13687,9 +13853,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13687
13853
  return true;
13688
13854
  }
13689
13855
  setFileContent(fileContent) {
13690
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13856
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13857
+ format: "yaml",
13858
+ fileContent
13859
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13691
13860
  this.config = merged;
13692
- super.setFileContent(stringifyHermesConfig(merged));
13861
+ super.setFileContent(applySharedConfigPatch({
13862
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13863
+ feature: "mcp",
13864
+ existingContent: fileContent,
13865
+ patch: { mcp_servers: merged.mcp_servers }
13866
+ }));
13693
13867
  }
13694
13868
  isDeletable() {
13695
13869
  return false;
@@ -13716,12 +13890,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13716
13890
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13717
13891
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13718
13892
  const paths = this.getSettablePaths({ global });
13719
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13893
+ const fileContent = await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13894
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13895
+ format: "yaml",
13896
+ fileContent
13897
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13720
13898
  return new HermesagentMcp({
13721
13899
  outputRoot,
13722
13900
  relativeDirPath: paths.relativeDirPath,
13723
13901
  relativeFilePath: paths.relativeFilePath,
13724
- fileContent: stringifyHermesConfig(merged),
13902
+ fileContent: applySharedConfigPatch({
13903
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13904
+ feature: "mcp",
13905
+ existingContent: fileContent,
13906
+ patch: { mcp_servers: merged.mcp_servers }
13907
+ }),
13725
13908
  validate,
13726
13909
  global
13727
13910
  });
@@ -14886,28 +15069,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14886
15069
  }
14887
15070
  };
14888
15071
  //#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
15072
  //#region src/features/mcp/takt-mcp.ts
14912
15073
  /**
14913
15074
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -14973,17 +15134,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
14973
15134
  }
14974
15135
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14975
15136
  const paths = TaktMcp.getSettablePaths({ global });
14976
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15137
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15138
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
14977
15139
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14978
- const merged = {
14979
- ...config,
14980
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14981
- };
14982
15140
  return new TaktMcp({
14983
15141
  outputRoot,
14984
15142
  relativeDirPath: paths.relativeDirPath,
14985
15143
  relativeFilePath: paths.relativeFilePath,
14986
- fileContent: dump(merged),
15144
+ fileContent: applySharedConfigPatch({
15145
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15146
+ feature: "mcp",
15147
+ existingContent,
15148
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15149
+ filePath
15150
+ }),
14987
15151
  validate,
14988
15152
  global
14989
15153
  });
@@ -15428,7 +15592,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15428
15592
  ["augmentcode", {
15429
15593
  class: AugmentcodeMcp,
15430
15594
  meta: {
15431
- supportsProject: false,
15595
+ supportsProject: true,
15432
15596
  supportsGlobal: true,
15433
15597
  supportsEnabledTools: false,
15434
15598
  supportsDisabledTools: false
@@ -16010,15 +16174,97 @@ const JuniePermissionsOverrideSchema = z.looseObject({
16010
16174
  defaultBehavior: z.optional(z.string())
16011
16175
  });
16012
16176
  /**
16177
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16178
+ * permission surfaces the canonical coarse-mode mapping can't express:
16179
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16180
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16181
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16182
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16183
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16184
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16185
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16186
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16187
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16188
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16189
+ * scope are supported.
16190
+ *
16191
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16192
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16193
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16194
+ * of scope for this override.
16195
+ *
16196
+ * @example
16197
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16198
+ */
16199
+ const TaktPermissionsOverrideSchema = z.looseObject({
16200
+ step_permission_overrides: z.optional(z.record(z.string(), z.string())),
16201
+ provider_options: z.optional(z.looseObject({}))
16202
+ });
16203
+ /**
16204
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16205
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16206
+ * express, so they are authored here and merged into the shared Amp settings
16207
+ * file, while the shared `permission` block continues to drive the canonical
16208
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16209
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16210
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16211
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16212
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16213
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16214
+ * later fallbacks), preserving author order.
16215
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16216
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16217
+ * confirmation).
16218
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16219
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16220
+ * supported.
16221
+ *
16222
+ * @example
16223
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16224
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16225
+ */
16226
+ const AmpPermissionsOverrideSchema = z.looseObject({
16227
+ permissions: z.optional(z.array(z.looseObject({
16228
+ tool: z.string(),
16229
+ action: z.string()
16230
+ }))),
16231
+ mcpPermissions: z.optional(z.array(z.looseObject({}))),
16232
+ guardedFiles: z.optional(z.looseObject({ allowlist: z.optional(z.array(z.string())) })),
16233
+ dangerouslyAllowAll: z.optional(z.boolean())
16234
+ });
16235
+ /**
16236
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16237
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16238
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16239
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16240
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16241
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16242
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16243
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16244
+ * Fields placed here are merged onto the top level of
16245
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16246
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16247
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16248
+ * Verified against https://antigravity.google/docs/cli/reference and
16249
+ * https://antigravity.google/docs/cli/sandbox.
16250
+ *
16251
+ * @example
16252
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16253
+ */
16254
+ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
16255
+ toolPermission: z.optional(z.string()),
16256
+ enableTerminalSandbox: z.optional(z.boolean())
16257
+ });
16258
+ /**
16013
16259
  * Permissions configuration.
16014
16260
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
16015
16261
  * Values are pattern-to-action mappings for that tool category.
16016
16262
  *
16017
16263
  * 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.
16264
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16265
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16266
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16267
+ * other tool reads the shared `permission` block and ignores them.
16022
16268
  *
16023
16269
  * @example
16024
16270
  * {
@@ -16039,7 +16285,10 @@ const PermissionsConfigSchema = z.looseObject({
16039
16285
  reasonix: z.optional(ReasonixPermissionsOverrideSchema),
16040
16286
  factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
16041
16287
  warp: z.optional(WarpPermissionsOverrideSchema),
16042
- junie: z.optional(JuniePermissionsOverrideSchema)
16288
+ junie: z.optional(JuniePermissionsOverrideSchema),
16289
+ takt: z.optional(TaktPermissionsOverrideSchema),
16290
+ amp: z.optional(AmpPermissionsOverrideSchema),
16291
+ "antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema)
16043
16292
  });
16044
16293
  /**
16045
16294
  * Full permissions file schema including optional $schema field.
@@ -16151,6 +16400,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
16151
16400
  * Reference: https://ampcode.com/manual ("amp.permissions").
16152
16401
  */
16153
16402
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16403
+ /**
16404
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16405
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16406
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16407
+ * permissions override. Reference: https://ampcode.com/manual.
16408
+ */
16409
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16410
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16411
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16412
+ /**
16413
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16414
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16415
+ */
16416
+ function ampMatchesCmd(entry) {
16417
+ const cmd = entry.matches?.cmd;
16418
+ return typeof cmd === "string" ? cmd : void 0;
16419
+ }
16420
+ /**
16421
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16422
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16423
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16424
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16425
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16426
+ * override verbatim instead of being flattened with loss or dropped.
16427
+ */
16428
+ function isCanonicalAmpEntry(entry) {
16429
+ if (entry.action === "delegate") return false;
16430
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16431
+ const matches = entry.matches;
16432
+ if (matches === void 0) return true;
16433
+ const keys = Object.keys(matches);
16434
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16435
+ }
16154
16436
  function parseAmpSettings(fileContent) {
16155
16437
  const errors = [];
16156
16438
  const parsed = parse(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -16166,10 +16448,20 @@ function toDisableList(value) {
16166
16448
  return value.filter((entry) => typeof entry === "string");
16167
16449
  }
16168
16450
  /**
16451
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16452
+ * settings, returning `undefined` when the key is absent or not an array so the
16453
+ * override omits it.
16454
+ */
16455
+ function extractAmpGuardedAllowlist(value) {
16456
+ if (!Array.isArray(value)) return void 0;
16457
+ return value.filter((entry) => typeof entry === "string");
16458
+ }
16459
+ /**
16169
16460
  * 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.
16461
+ * whose `tool`/`action` rulesync understands are retained; every other key
16462
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16463
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16464
+ * round-trip.
16173
16465
  */
16174
16466
  function toPermissionsList(value) {
16175
16467
  if (!Array.isArray(value)) return [];
@@ -16179,14 +16471,11 @@ function toPermissionsList(value) {
16179
16471
  const { tool, action } = raw;
16180
16472
  if (typeof tool !== "string" || typeof action !== "string") continue;
16181
16473
  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
16474
  entries.push({
16186
16475
  ...raw,
16187
16476
  tool,
16188
16477
  action,
16189
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16478
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
16190
16479
  });
16191
16480
  }
16192
16481
  return entries;
@@ -16211,6 +16500,13 @@ function toPermissionsList(value) {
16211
16500
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
16212
16501
  * and writes merge into the existing JSON rather than overwriting it, and the
16213
16502
  * file is never deleted.
16503
+ *
16504
+ * Amp shapes with no canonical category are authored and round-tripped through
16505
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16506
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16507
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16508
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16509
+ * `amp.dangerouslyAllowAll` settings.
16214
16510
  */
16215
16511
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
16216
16512
  constructor(params) {
@@ -16280,15 +16576,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16280
16576
  const jsonDir = join(outputRoot, basePaths.relativeDirPath);
16281
16577
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
16282
16578
  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");
16579
+ const config = rulesyncPermissions.getJson();
16580
+ const { disable, permissions } = convertRulesyncToAmp(config);
16581
+ const override = config.amp;
16582
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16583
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16285
16584
  const newJson = {
16286
16585
  ...json,
16287
16586
  [AMP_TOOLS_DISABLE_KEY]: disable
16288
16587
  };
16289
- const mergedPermissions = [...permissions, ...preservedDelegates];
16588
+ const mergedPermissions = mergeAmpPermissions([
16589
+ ...permissions,
16590
+ ...authoredExtras,
16591
+ ...preservedDelegates
16592
+ ]);
16290
16593
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16291
16594
  else delete newJson[AMP_PERMISSIONS_KEY];
16595
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16596
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16597
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16292
16598
  return new AmpPermissions({
16293
16599
  outputRoot,
16294
16600
  relativeDirPath: basePaths.relativeDirPath,
@@ -16299,11 +16605,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16299
16605
  }
16300
16606
  toRulesyncPermissions() {
16301
16607
  const json = parseAmpSettings(this.getFileContent());
16608
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16609
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16610
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16302
16611
  const config = convertAmpToRulesync({
16303
16612
  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) });
16613
+ permissions: canonicalEntries
16614
+ });
16615
+ const ampOverride = {};
16616
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16617
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16618
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16619
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16620
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16621
+ const result = { ...config };
16622
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16623
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16307
16624
  }
16308
16625
  validate() {
16309
16626
  try {
@@ -16350,6 +16667,37 @@ const ACTION_PRIORITY = {
16350
16667
  allow: 2
16351
16668
  };
16352
16669
  /**
16670
+ * Fail-closed action priority for merging the full `amp.permissions` list
16671
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16672
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16673
+ * it never auto-allows) trails as the final fallback.
16674
+ */
16675
+ const MERGE_ACTION_PRIORITY = {
16676
+ reject: 0,
16677
+ ask: 1,
16678
+ allow: 2,
16679
+ delegate: 3
16680
+ };
16681
+ /**
16682
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16683
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16684
+ * order within an action class. Guarantees an authored `reject` cannot be
16685
+ * shadowed by a generated catch-all `allow`.
16686
+ */
16687
+ function mergeAmpPermissions(entries) {
16688
+ const decorated = entries.map((entry, index) => ({
16689
+ entry,
16690
+ index
16691
+ }));
16692
+ decorated.sort((a, b) => {
16693
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16694
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16695
+ if (ap !== bp) return ap - bp;
16696
+ return a.index - b.index;
16697
+ });
16698
+ return decorated.map((d) => d.entry);
16699
+ }
16700
+ /**
16353
16701
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16354
16702
  *
16355
16703
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16414,14 +16762,14 @@ function sortAmpPermissions(entries) {
16414
16762
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16415
16763
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16416
16764
  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;
16765
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16766
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16419
16767
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16420
16768
  const at = a.entry.tool;
16421
16769
  const bt = b.entry.tool;
16422
16770
  if (at !== bt) return at < bt ? -1 : 1;
16423
- const ac = a.entry.matches?.cmd ?? "";
16424
- const bc = b.entry.matches?.cmd ?? "";
16771
+ const ac = ampMatchesCmd(a.entry) ?? "";
16772
+ const bc = ampMatchesCmd(b.entry) ?? "";
16425
16773
  if (ac !== bc) return ac < bc ? -1 : 1;
16426
16774
  return a.index - b.index;
16427
16775
  });
@@ -16460,7 +16808,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16460
16808
  for (const tool of disable) assign(tool, "*", "deny");
16461
16809
  for (const entry of permissions) {
16462
16810
  if (entry.action === "delegate") continue;
16463
- const pattern = entry.matches?.cmd ?? "*";
16811
+ const pattern = ampMatchesCmd(entry) ?? "*";
16464
16812
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16465
16813
  assign(entry.tool, pattern, action);
16466
16814
  }
@@ -16542,6 +16890,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16542
16890
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16543
16891
  * file (global scope only). The file holds other CLI settings besides
16544
16892
  * permissions, so it is never deleted.
16893
+ *
16894
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16895
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16896
+ * authored and round-tripped through the `antigravity-cli` permissions override
16897
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16545
16898
  */
16546
16899
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16547
16900
  constructor(params) {
@@ -16602,6 +16955,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16602
16955
  ...settings,
16603
16956
  permissions: mergedPermissions
16604
16957
  };
16958
+ const override = config["antigravity-cli"];
16959
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16960
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16605
16961
  const fileContent = JSON.stringify(merged, null, 2);
16606
16962
  return new AntigravityCliPermissions({
16607
16963
  outputRoot,
@@ -16625,7 +16981,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16625
16981
  ask: permissions.ask ?? [],
16626
16982
  deny: permissions.deny ?? []
16627
16983
  });
16628
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16984
+ const override = {};
16985
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
16986
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
16987
+ const result = { ...config };
16988
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
16989
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16629
16990
  }
16630
16991
  validate() {
16631
16992
  return {
@@ -19311,14 +19672,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19311
19672
  return true;
19312
19673
  }
19313
19674
  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);
19675
+ this.fileContent = applySharedConfigPatch({
19676
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19677
+ feature: "permissions",
19678
+ existingContent: fileContent,
19679
+ patch: parseSharedConfig({
19680
+ format: "yaml",
19681
+ fileContent: this.fileContent
19682
+ })
19683
+ });
19319
19684
  }
19320
19685
  toRulesyncPermissions() {
19321
- const config = parseHermesConfig(this.getFileContent());
19686
+ const config = parseSharedConfig({
19687
+ format: "yaml",
19688
+ fileContent: this.getFileContent()
19689
+ });
19322
19690
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
19323
19691
  return new RulesyncPermissions({
19324
19692
  relativeDirPath: "",
@@ -19339,11 +19707,17 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19339
19707
  enabled: true,
19340
19708
  domains: webfetchDeny
19341
19709
  } };
19342
- if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
19710
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19711
+ base: config,
19712
+ patch: permissions.hermes
19713
+ });
19343
19714
  config.permissions = { rulesync: permissions };
19344
19715
  return new HermesagentPermissions({
19345
19716
  outputRoot,
19346
- fileContent: stringifyHermesConfig(config)
19717
+ fileContent: stringifySharedConfig({
19718
+ format: "yaml",
19719
+ document: config
19720
+ })
19347
19721
  });
19348
19722
  }
19349
19723
  };
@@ -20899,6 +21273,8 @@ function isPermissionAction(value) {
20899
21273
  const TAKT_PROVIDER_KEY = "provider";
20900
21274
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20901
21275
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21276
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21277
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20902
21278
  const TAKT_DEFAULT_PROVIDER = "claude";
20903
21279
  const CATCH_ALL_PATTERN = "*";
20904
21280
  /**
@@ -20928,10 +21304,16 @@ const CATCH_ALL_PATTERN = "*";
20928
21304
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20929
21305
  *
20930
21306
  * 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.
21307
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21308
+ * derived mode; every other provider profile and all other top-level keys are
21309
+ * preserved. The file is never deleted.
21310
+ *
21311
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21312
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21313
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21314
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21315
+ * (a top-level per-provider sandbox/network table). Both are authored on
21316
+ * generate and re-extracted on import.
20935
21317
  */
20936
21318
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20937
21319
  constructor(params) {
@@ -20963,37 +21345,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20963
21345
  }
20964
21346
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20965
21347
  const paths = TaktPermissions.getSettablePaths({ global });
20966
- const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21348
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21349
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21350
+ const config = parseSharedConfig({
21351
+ format: "yaml",
21352
+ fileContent: existingContent,
21353
+ filePath,
21354
+ invalidRootPolicy: "error"
21355
+ });
21356
+ const rulesyncJson = rulesyncPermissions.getJson();
20967
21357
  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
- }
21358
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21359
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21360
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21361
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21362
+ const patch = {
21363
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21364
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21365
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21366
+ } },
21367
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
20980
21368
  };
20981
21369
  return new TaktPermissions({
20982
21370
  outputRoot,
20983
21371
  relativeDirPath: paths.relativeDirPath,
20984
21372
  relativeFilePath: paths.relativeFilePath,
20985
- fileContent: dump(merged),
21373
+ fileContent: applySharedConfigPatch({
21374
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21375
+ feature: "permissions",
21376
+ existingContent,
21377
+ patch,
21378
+ filePath
21379
+ }),
20986
21380
  validate: true,
20987
21381
  global
20988
21382
  });
20989
21383
  }
20990
21384
  toRulesyncPermissions() {
20991
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21385
+ const config = parseSharedConfig({
21386
+ format: "yaml",
21387
+ fileContent: this.getFileContent(),
21388
+ filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath()),
21389
+ invalidRootPolicy: "error"
21390
+ });
20992
21391
  const provider = resolveActiveProvider(config);
20993
21392
  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];
21393
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21394
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
20995
21395
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
20996
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21396
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21397
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21398
+ const taktOverride = {};
21399
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21400
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21401
+ const result = { ...rulesyncConfig };
21402
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21403
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20997
21404
  }
20998
21405
  validate() {
20999
21406
  return {
@@ -29888,14 +30295,21 @@ def register(ctx):
29888
30295
  `;
29889
30296
  }
29890
30297
  function getEnabledPluginConfigContent(currentContent) {
29891
- const config = parseHermesConfig(currentContent);
30298
+ const config = parseSharedConfig({
30299
+ format: "yaml",
30300
+ fileContent: currentContent
30301
+ });
29892
30302
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29893
30303
  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);
30304
+ return applySharedConfigPatch({
30305
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30306
+ feature: "subagents",
30307
+ existingContent: currentContent,
30308
+ patch: { plugins: {
30309
+ ...plugins,
30310
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30311
+ } }
30312
+ });
29899
30313
  }
29900
30314
  function getSubagentSpec(rulesyncSubagent) {
29901
30315
  const json = rulesyncSubagent.getFrontmatter();
@@ -29981,6 +30395,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
29981
30395
  static getSettablePaths() {
29982
30396
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
29983
30397
  }
30398
+ /**
30399
+ * Beyond the subagent spec files, generation also read-modify-writes the
30400
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30401
+ * so the write must be declared for the shared-file order derivation.
30402
+ */
30403
+ static getExtraSharedWritePaths() {
30404
+ return [{
30405
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30406
+ relativeFilePath: basename(HERMESAGENT_CONFIG_FILE_PATH)
30407
+ }];
30408
+ }
29984
30409
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
29985
30410
  return [join(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
29986
30411
  }
@@ -31719,7 +32144,9 @@ const RulesyncRuleFrontmatterSchema = z.object({
31719
32144
  })),
31720
32145
  kiro: z.optional(z.looseObject({
31721
32146
  inclusion: z.optional(z.string()),
31722
- fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())]))
32147
+ fileMatchPattern: z.optional(z.union([z.string(), z.array(z.string())])),
32148
+ name: z.optional(z.string()),
32149
+ description: z.optional(z.string())
31723
32150
  })),
31724
32151
  takt: z.optional(z.looseObject({
31725
32152
  name: z.optional(z.string()),
@@ -34635,7 +35062,8 @@ function toFileMatchPattern(globs) {
34635
35062
  *
34636
35063
  * Precedence:
34637
35064
  * 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`).
35065
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35066
+ * and with the companion `name`/`description` carried through for `auto`).
34639
35067
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
34640
35068
  * to matching files instead of leaving it implicitly always-on.
34641
35069
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -34653,6 +35081,11 @@ function deriveKiroInclusion({ kiro, globs }) {
34653
35081
  fileMatchPattern
34654
35082
  } : { inclusion: "fileMatch" };
34655
35083
  }
35084
+ if (kiro.inclusion === "auto") return {
35085
+ inclusion: "auto",
35086
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35087
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35088
+ };
34656
35089
  return { inclusion: kiro.inclusion };
34657
35090
  }
34658
35091
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -34724,6 +35157,8 @@ var KiroRule = class KiroRule extends ToolRule {
34724
35157
  const rawPattern = frontmatter.fileMatchPattern;
34725
35158
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
34726
35159
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35160
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35161
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
34727
35162
  return new RulesyncRule({
34728
35163
  outputRoot: process.cwd(),
34729
35164
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -34734,7 +35169,9 @@ var KiroRule = class KiroRule extends ToolRule {
34734
35169
  globs,
34735
35170
  kiro: {
34736
35171
  inclusion,
34737
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35172
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35173
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35174
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34738
35175
  }
34739
35176
  },
34740
35177
  body
@@ -36908,6 +37345,197 @@ function buildPermissionsStrategy(ctx) {
36908
37345
  };
36909
37346
  }
36910
37347
  //#endregion
37348
+ //#region src/types/processor-registry.ts
37349
+ const PROCESSOR_REGISTRY = [
37350
+ {
37351
+ feature: "rules",
37352
+ processor: RulesProcessor,
37353
+ schema: RulesProcessorToolTargetSchema,
37354
+ factory: toolRuleFactories
37355
+ },
37356
+ {
37357
+ feature: "ignore",
37358
+ processor: IgnoreProcessor,
37359
+ schema: IgnoreProcessorToolTargetSchema,
37360
+ factory: toolIgnoreFactories
37361
+ },
37362
+ {
37363
+ feature: "mcp",
37364
+ processor: McpProcessor,
37365
+ schema: McpProcessorToolTargetSchema,
37366
+ factory: toolMcpFactories
37367
+ },
37368
+ {
37369
+ feature: "commands",
37370
+ processor: CommandsProcessor,
37371
+ schema: CommandsProcessorToolTargetSchema,
37372
+ factory: toolCommandFactories
37373
+ },
37374
+ {
37375
+ feature: "subagents",
37376
+ processor: SubagentsProcessor,
37377
+ schema: SubagentsProcessorToolTargetSchema,
37378
+ factory: toolSubagentFactories
37379
+ },
37380
+ {
37381
+ feature: "skills",
37382
+ processor: SkillsProcessor,
37383
+ schema: SkillsProcessorToolTargetSchema,
37384
+ factory: toolSkillFactories
37385
+ },
37386
+ {
37387
+ feature: "hooks",
37388
+ processor: HooksProcessor,
37389
+ schema: HooksProcessorToolTargetSchema,
37390
+ factory: toolHooksFactories
37391
+ },
37392
+ {
37393
+ feature: "permissions",
37394
+ processor: PermissionsProcessor,
37395
+ schema: PermissionsProcessorToolTargetSchema,
37396
+ factory: toolPermissionsFactories
37397
+ }
37398
+ ];
37399
+ const getProcessorRegistryEntry = (feature) => {
37400
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37401
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37402
+ return entry;
37403
+ };
37404
+ //#endregion
37405
+ //#region src/lib/shared-file-derive.ts
37406
+ /**
37407
+ * The single declaration of the cross-feature write order for shared
37408
+ * (read-modify-write) config files: when two features write the same on-disk
37409
+ * file, the earlier one writes first and the later one merges on top, so the
37410
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37411
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37412
+ * The generation step graph's `dependsOn` edges are derived from this list
37413
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37414
+ * shared path never requires touching the graph by hand.
37415
+ */
37416
+ const SHARED_WRITE_FEATURE_ORDER = [
37417
+ "ignore",
37418
+ "subagents",
37419
+ "mcp",
37420
+ "hooks",
37421
+ "permissions",
37422
+ "rules"
37423
+ ];
37424
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37425
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37426
+ const sharedFileKey = (path) => {
37427
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37428
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37429
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37430
+ };
37431
+ const settablePathsForScope = (cls, global) => {
37432
+ const paths = [];
37433
+ let settable;
37434
+ try {
37435
+ settable = cls.getSettablePaths?.({ global });
37436
+ } catch {
37437
+ return paths;
37438
+ }
37439
+ if (settable?.relativeFilePath) paths.push({
37440
+ relativeDirPath: settable.relativeDirPath ?? ".",
37441
+ relativeFilePath: settable.relativeFilePath
37442
+ });
37443
+ if (settable?.root) {
37444
+ paths.push(settable.root);
37445
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37446
+ }
37447
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37448
+ return paths;
37449
+ };
37450
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37451
+ /**
37452
+ * Derive, from the processor registry, every on-disk file that two or more
37453
+ * features read-modify-write. Source of truth the generation step graph's
37454
+ * `writesSharedFile` declarations must match.
37455
+ */
37456
+ const deriveSharedFileWriters = () => {
37457
+ const byKey = /* @__PURE__ */ new Map();
37458
+ const pathByKey = /* @__PURE__ */ new Map();
37459
+ for (const entry of PROCESSOR_REGISTRY) {
37460
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37461
+ const factories = entry.factory;
37462
+ for (const [tool, factory] of factories) {
37463
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37464
+ for (const path of collectFactoryPaths(factory)) {
37465
+ const key = sharedFileKey(path);
37466
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37467
+ let features = byKey.get(key);
37468
+ if (!features) {
37469
+ features = /* @__PURE__ */ new Map();
37470
+ byKey.set(key, features);
37471
+ }
37472
+ let tools = features.get(entry.feature);
37473
+ if (!tools) {
37474
+ tools = /* @__PURE__ */ new Set();
37475
+ features.set(entry.feature, tools);
37476
+ }
37477
+ tools.add(tool);
37478
+ }
37479
+ }
37480
+ }
37481
+ const writers = [];
37482
+ for (const [key, features] of byKey) {
37483
+ if (features.size < 2) continue;
37484
+ const path = pathByKey.get(key);
37485
+ const toolsByFeature = /* @__PURE__ */ new Map();
37486
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37487
+ writers.push({
37488
+ key,
37489
+ relativeDirPath: path.relativeDirPath,
37490
+ relativeFilePath: path.relativeFilePath,
37491
+ features: [...features.keys()].toSorted(),
37492
+ toolsByFeature
37493
+ });
37494
+ }
37495
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37496
+ };
37497
+ /**
37498
+ * Derive, per shared-write feature, the shared files it writes and the
37499
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37500
+ * writer depends on all writers that precede it in
37501
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37502
+ * graph consumes, so registry changes (a new tool, a new settable path)
37503
+ * propagate into the execution order without a hand-maintained declaration.
37504
+ *
37505
+ * @throws Error if a feature writes a shared file but has no position in
37506
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37507
+ * whose merge policy wins, so it must be made explicitly there.
37508
+ */
37509
+ const deriveSharedWriteSteps = () => {
37510
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37511
+ const filesByFeature = /* @__PURE__ */ new Map();
37512
+ const depsByFeature = /* @__PURE__ */ new Map();
37513
+ for (const writer of deriveSharedFileWriters()) {
37514
+ 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.`);
37515
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37516
+ for (const [position, feature] of ordered.entries()) {
37517
+ let files = filesByFeature.get(feature);
37518
+ if (!files) {
37519
+ files = /* @__PURE__ */ new Set();
37520
+ filesByFeature.set(feature, files);
37521
+ }
37522
+ files.add(writer.key);
37523
+ let deps = depsByFeature.get(feature);
37524
+ if (!deps) {
37525
+ deps = /* @__PURE__ */ new Set();
37526
+ depsByFeature.set(feature, deps);
37527
+ }
37528
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37529
+ }
37530
+ }
37531
+ const steps = /* @__PURE__ */ new Map();
37532
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37533
+ writesSharedFile: [...files].toSorted(),
37534
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37535
+ });
37536
+ return steps;
37537
+ };
37538
+ //#endregion
36911
37539
  //#region src/lib/generate.ts
36912
37540
  async function processFeatureGeneration(params) {
36913
37541
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -37057,98 +37685,53 @@ function resolveExecutionOrder(steps) {
37057
37685
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
37058
37686
  return ordered;
37059
37687
  }
37688
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37689
+ const sharedWriteMeta = (id) => {
37690
+ const step = SHARED_WRITE_STEPS.get(id);
37691
+ return step ? {
37692
+ writesSharedFile: step.writesSharedFile,
37693
+ dependsOn: step.dependsOn
37694
+ } : {};
37695
+ };
37060
37696
  /**
37061
37697
  * The static shape of the generation step graph: which steps write which shared
37062
37698
  * (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.
37699
+ * for those writers. Both are derived from the processor registry's settable
37700
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37701
+ * tool or shared path never requires editing this graph. Exported (separately
37702
+ * from the `run` closures, which need a live `config`/`logger`) so
37703
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37704
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37705
+ * mutate this module-level singleton and affect every subsequent `generate()`
37706
+ * call in the process.
37068
37707
  */
37069
37708
  const GENERATION_STEP_GRAPH = [
37070
37709
  {
37071
37710
  id: "ignore",
37072
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37711
+ ...sharedWriteMeta("ignore")
37073
37712
  },
37074
37713
  {
37075
37714
  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"]
37715
+ ...sharedWriteMeta("mcp")
37097
37716
  },
37098
37717
  { id: "commands" },
37099
- { id: "subagents" },
37718
+ {
37719
+ id: "subagents",
37720
+ ...sharedWriteMeta("subagents")
37721
+ },
37100
37722
  { id: "skills" },
37101
37723
  {
37102
37724
  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"]
37725
+ ...sharedWriteMeta("hooks")
37114
37726
  },
37115
37727
  {
37116
37728
  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
- ]
37729
+ ...sharedWriteMeta("permissions")
37143
37730
  },
37144
37731
  {
37145
37732
  id: "rules",
37146
- writesSharedFile: ["kilo.json", "opencode.json"],
37147
- dependsOn: [
37148
- "mcp",
37149
- "skills",
37150
- "permissions"
37151
- ]
37733
+ ...sharedWriteMeta("rules"),
37734
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
37152
37735
  }
37153
37736
  ];
37154
37737
  /**
@@ -37263,7 +37846,7 @@ async function generateRulesCore(params) {
37263
37846
  targets: config.getConfigFileTargets(),
37264
37847
  global: config.getGlobal()
37265
37848
  }) : /* @__PURE__ */ new Map();
37266
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
37849
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37267
37850
  if (!config.getFeatures(toolTarget).includes("rules")) continue;
37268
37851
  const processor = new RulesProcessor({
37269
37852
  outputRoot,
@@ -37318,7 +37901,7 @@ async function generateIgnoreCore(params) {
37318
37901
  let hasDiff = false;
37319
37902
  for (const toolTarget of intersection(config.getTargets(), supportedIgnoreTargets)) {
37320
37903
  if (!config.getFeatures(toolTarget).includes("ignore")) continue;
37321
- for (const outputRoot of config.getOutputRoots()) try {
37904
+ for (const outputRoot of config.getOutputRoots(toolTarget)) try {
37322
37905
  const processor = new IgnoreProcessor({
37323
37906
  outputRoot,
37324
37907
  inputRoot: config.getInputRoot(),
@@ -37359,7 +37942,7 @@ async function generateMcpCore(params) {
37359
37942
  featureName: "mcp",
37360
37943
  logger
37361
37944
  });
37362
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
37945
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37363
37946
  if (!config.getFeatures(toolTarget).includes("mcp")) continue;
37364
37947
  const processor = new McpProcessor({
37365
37948
  outputRoot,
@@ -37401,7 +37984,7 @@ async function generateCommandsCore(params) {
37401
37984
  featureName: "commands",
37402
37985
  logger
37403
37986
  });
37404
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
37987
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37405
37988
  if (!config.getFeatures(toolTarget).includes("commands")) continue;
37406
37989
  const processor = new CommandsProcessor({
37407
37990
  outputRoot,
@@ -37443,7 +38026,7 @@ async function generateSubagentsCore(params) {
37443
38026
  featureName: "subagents",
37444
38027
  logger
37445
38028
  });
37446
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38029
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37447
38030
  if (!config.getFeatures(toolTarget).includes("subagents")) continue;
37448
38031
  const processor = new SubagentsProcessor({
37449
38032
  outputRoot,
@@ -37486,7 +38069,7 @@ async function generateSkillsCore(params) {
37486
38069
  featureName: "skills",
37487
38070
  logger
37488
38071
  });
37489
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38072
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37490
38073
  if (!config.getFeatures(toolTarget).includes("skills")) continue;
37491
38074
  const processor = new SkillsProcessor({
37492
38075
  outputRoot,
@@ -37527,7 +38110,7 @@ async function generateHooksCore(params) {
37527
38110
  featureName: "hooks",
37528
38111
  logger
37529
38112
  });
37530
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38113
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37531
38114
  if (!config.getFeatures(toolTarget).includes("hooks")) continue;
37532
38115
  const processor = new HooksProcessor({
37533
38116
  outputRoot,
@@ -37564,7 +38147,7 @@ async function generatePermissionsCore(params) {
37564
38147
  let totalCount = 0;
37565
38148
  const allPaths = [];
37566
38149
  let hasDiff = false;
37567
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of intersection(config.getTargets(), supportedPermissionsTargets)) {
38150
+ for (const toolTarget of intersection(config.getTargets(), supportedPermissionsTargets)) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37568
38151
  if (!config.getFeatures(toolTarget).includes("permissions")) continue;
37569
38152
  try {
37570
38153
  const processor = new PermissionsProcessor({
@@ -37840,6 +38423,6 @@ async function importPermissionsCore(params) {
37840
38423
  return writtenCount;
37841
38424
  }
37842
38425
  //#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 };
38426
+ 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
38427
 
37845
- //# sourceMappingURL=import-B7VzSVUK.js.map
38428
+ //# sourceMappingURL=import-BdJG1fyb.js.map