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.
@@ -114,22 +114,22 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$248 } = node_path.posix;
117
+ const { join: join$247 } = node_path.posix;
118
118
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
119
119
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
120
120
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
121
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
121
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
127
127
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
128
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
129
129
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
130
130
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
131
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$248(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
131
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$247(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$247(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
133
133
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
134
134
  const RULESYNC_MCP_FILE_NAME = "mcp.json";
135
135
  const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
@@ -878,7 +878,7 @@ const SourceEntrySchema = zod_mini.z.object({
878
878
  scope: (0, zod_mini.optional)(zod_mini.z.enum(["project", "user"]))
879
879
  });
880
880
  const ConfigParamsSchema = zod_mini.z.object({
881
- outputRoots: zod_mini.z.array(zod_mini.z.string()),
881
+ outputRoots: zod_mini.z.union([zod_mini.z.array(zod_mini.z.string()), zod_mini.z.record(zod_mini.z.string(), zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))]),
882
882
  targets: RulesyncConfigTargetsSchema,
883
883
  features: RulesyncFeaturesSchema,
884
884
  verbose: zod_mini.z.boolean(),
@@ -983,6 +983,7 @@ var Config = class Config {
983
983
  const resolvedTargets = targets ?? [];
984
984
  const resolvedFeatures = features ?? [];
985
985
  this.validateObjectFormTargetKeys(resolvedTargets);
986
+ this.validateObjectFormOutputRootKeys(outputRoots);
986
987
  this.validateConflictingTargets(resolvedTargets);
987
988
  if (dryRun && check) throw new Error("--dry-run and --check cannot be used together");
988
989
  this.outputRoots = outputRoots;
@@ -1020,6 +1021,11 @@ var Config = class Config {
1020
1021
  if (!validTargets.has(key)) throw new Error(`Unknown target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
1021
1022
  }
1022
1023
  }
1024
+ validateObjectFormOutputRootKeys(outputRoots) {
1025
+ if (Array.isArray(outputRoots)) return;
1026
+ const validTargets = new Set(ALL_TOOL_TARGETS);
1027
+ for (const key of Object.keys(outputRoots)) if (!validTargets.has(key)) throw new Error(`Unknown outputRoots target '${key}'. Valid targets: ${ALL_TOOL_TARGETS.join(", ")}.`);
1028
+ }
1023
1029
  validateConflictingTargets(targets) {
1024
1030
  const has = (target) => {
1025
1031
  if (Array.isArray(targets)) return targets.includes(target);
@@ -1027,8 +1033,19 @@ var Config = class Config {
1027
1033
  };
1028
1034
  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.`);
1029
1035
  }
1030
- getOutputRoots() {
1031
- return this.outputRoots;
1036
+ getOutputRoots(target) {
1037
+ if (Array.isArray(this.outputRoots)) return this.outputRoots;
1038
+ if (target) {
1039
+ const targetOutputRoots = this.outputRoots[target];
1040
+ if (targetOutputRoots === void 0) return [];
1041
+ return Array.isArray(targetOutputRoots) ? targetOutputRoots : [targetOutputRoots];
1042
+ }
1043
+ const allRoots = [];
1044
+ for (const value of Object.values(this.outputRoots)) {
1045
+ if (value === void 0) continue;
1046
+ allRoots.push(...Array.isArray(value) ? value : [value]);
1047
+ }
1048
+ return [...new Set(allRoots)];
1032
1049
  }
1033
1050
  /**
1034
1051
  * Filter an arbitrary string-key list down to the known `ToolTarget` set,
@@ -1390,10 +1407,21 @@ var ConfigResolver = class {
1390
1407
  };
1391
1408
  function getOutputRootsInLightOfGlobal({ outputRoots, global }) {
1392
1409
  if (global) return [getHomeDirectory()];
1393
- outputRoots.forEach((outputRoot) => {
1394
- validateOutputRoot(outputRoot);
1395
- });
1396
- return outputRoots.map((outputRoot) => (0, node_path.resolve)(outputRoot));
1410
+ if (Array.isArray(outputRoots)) {
1411
+ outputRoots.forEach((outputRoot) => {
1412
+ validateOutputRoot(outputRoot);
1413
+ });
1414
+ return outputRoots.map((outputRoot) => (0, node_path.resolve)(outputRoot));
1415
+ }
1416
+ const resolvedOutputRoots = {};
1417
+ for (const [target, targetOutputRoots] of Object.entries(outputRoots)) {
1418
+ const roots = Array.isArray(targetOutputRoots) ? targetOutputRoots : [targetOutputRoots];
1419
+ roots.forEach((outputRoot) => {
1420
+ validateOutputRoot(outputRoot);
1421
+ });
1422
+ resolvedOutputRoots[target] = Array.isArray(targetOutputRoots) ? roots.map((outputRoot) => (0, node_path.resolve)(outputRoot)) : (0, node_path.resolve)(targetOutputRoots);
1423
+ }
1424
+ return resolvedOutputRoots;
1397
1425
  }
1398
1426
  function extractConfigFileTargets(targets) {
1399
1427
  if (targets === void 0) return void 0;
@@ -3919,6 +3947,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3919
3947
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3920
3948
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3921
3949
  //#endregion
3950
+ //#region src/constants/takt-paths.ts
3951
+ const TAKT_DIR = ".takt";
3952
+ const TAKT_FACETS_SUBDIR = "facets";
3953
+ const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
3954
+ const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
3955
+ const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
3956
+ const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
3957
+ const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
3958
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
3959
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3960
+ /**
3961
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3962
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3963
+ * profiles (including permission modes), and other Takt settings.
3964
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3965
+ */
3966
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3967
+ /**
3968
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3969
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3970
+ * transport must be explicitly enabled here before any workflow-defined MCP
3971
+ * server using it is permitted to run.
3972
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3973
+ */
3974
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3975
+ //#endregion
3976
+ //#region src/utils/prototype-pollution.ts
3977
+ /**
3978
+ * Keys that, if walked into when constructing or merging objects from
3979
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3980
+ * chain) and propagate state to every other object in the runtime. Any code
3981
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3982
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3983
+ */
3984
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3985
+ "__proto__",
3986
+ "constructor",
3987
+ "prototype"
3988
+ ]);
3989
+ function isPrototypePollutionKey(key) {
3990
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3991
+ }
3992
+ /**
3993
+ * Returns a shallow copy of a record's own entries with every
3994
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3995
+ *
3996
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3997
+ * or `headers` table — into freshly generated config. Carrying such a map by
3998
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3999
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
4000
+ * prototype). Walking the entries through this helper severs that path while
4001
+ * preserving every legitimate key.
4002
+ */
4003
+ function omitPrototypePollutionKeys(record) {
4004
+ const sanitized = {};
4005
+ for (const [key, value] of Object.entries(record)) {
4006
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4007
+ sanitized[key] = value;
4008
+ }
4009
+ return sanitized;
4010
+ }
4011
+ //#endregion
4012
+ //#region src/features/shared/shared-config-gateway.ts
4013
+ function sanitizeSharedConfigValue(value) {
4014
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
4015
+ if (!isPlainObject(value)) return value;
4016
+ const result = {};
4017
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
4018
+ return result;
4019
+ }
4020
+ /**
4021
+ * Parse a shared config file into a plain document: an empty/whitespace file
4022
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
4023
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
4024
+ * path when one is given.
4025
+ */
4026
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
4027
+ if (fileContent.trim() === "") return {};
4028
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
4029
+ let parsed;
4030
+ try {
4031
+ if (format === "yaml") parsed = (0, js_yaml.load)(fileContent);
4032
+ else if (format === "json") parsed = JSON.parse(fileContent);
4033
+ else parsed = (0, jsonc_parser.parse)(fileContent);
4034
+ } catch (error) {
4035
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4036
+ }
4037
+ if (parsed === void 0 || parsed === null) return {};
4038
+ if (!isPlainObject(parsed)) {
4039
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
4040
+ return {};
4041
+ }
4042
+ return sanitizeSharedConfigValue(parsed);
4043
+ }
4044
+ /**
4045
+ * Serialize a shared config document. YAML output always ends with exactly one
4046
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4047
+ * writers have always emitted (no trailing newline).
4048
+ */
4049
+ function stringifySharedConfig({ format, document }) {
4050
+ if (format === "yaml") return (0, js_yaml.dump)(document, {
4051
+ noRefs: true,
4052
+ sortKeys: false
4053
+ }).trimEnd() + "\n";
4054
+ return JSON.stringify(document, null, 2);
4055
+ }
4056
+ /**
4057
+ * Shallow merge: every top-level key in `patch` replaces the base key
4058
+ * wholesale; all other base keys are preserved. The policy for a feature that
4059
+ * owns a fixed set of top-level keys.
4060
+ */
4061
+ function mergeSharedConfigShallow({ base, patch }) {
4062
+ return {
4063
+ ...base,
4064
+ ...sanitizeSharedConfigValue(patch)
4065
+ };
4066
+ }
4067
+ /**
4068
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4069
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4070
+ * feature whose contribution interleaves with user-authored siblings at any
4071
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4072
+ * per-provider option tables) — nested sibling keys are preserved by
4073
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4074
+ * keys are dropped.
4075
+ */
4076
+ function mergeSharedConfigDeep({ base, patch }) {
4077
+ const result = { ...base };
4078
+ for (const [key, patchValue] of Object.entries(patch)) {
4079
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4080
+ const baseValue = result[key];
4081
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4082
+ base: baseValue,
4083
+ patch: patchValue
4084
+ });
4085
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4086
+ }
4087
+ return result;
4088
+ }
4089
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4090
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4091
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4092
+ /**
4093
+ * Who owns what in each gateway-managed shared config file, and which policy
4094
+ * resolves conflicts. Keys are `dir/file` tokens matching
4095
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4096
+ * lock-step with the writers derived from the processor registry, so an
4097
+ * undeclared writer fails CI instead of merging by accident.
4098
+ */
4099
+ const SHARED_CONFIG_OWNERSHIP = {
4100
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4101
+ format: "json",
4102
+ features: {
4103
+ ignore: {
4104
+ kind: "custom",
4105
+ policyFunction: "applyIgnoreReadDenies"
4106
+ },
4107
+ hooks: {
4108
+ kind: "replace-owned-keys",
4109
+ ownedKeys: ["hooks"]
4110
+ },
4111
+ permissions: {
4112
+ kind: "custom",
4113
+ policyFunction: "applyPermissions"
4114
+ }
4115
+ }
4116
+ },
4117
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4118
+ format: "yaml",
4119
+ features: {
4120
+ subagents: {
4121
+ kind: "replace-owned-keys",
4122
+ ownedKeys: ["plugins"]
4123
+ },
4124
+ mcp: {
4125
+ kind: "replace-owned-keys",
4126
+ ownedKeys: ["mcp_servers"]
4127
+ },
4128
+ hooks: {
4129
+ kind: "replace-owned-keys",
4130
+ ownedKeys: ["hooks"]
4131
+ },
4132
+ permissions: {
4133
+ kind: "deep-merge",
4134
+ replaceKeys: ["permissions"]
4135
+ }
4136
+ }
4137
+ },
4138
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4139
+ format: "yaml",
4140
+ invalidRootPolicy: "error",
4141
+ features: {
4142
+ mcp: {
4143
+ kind: "replace-owned-keys",
4144
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4145
+ },
4146
+ permissions: { kind: "deep-merge" }
4147
+ }
4148
+ }
4149
+ };
4150
+ /**
4151
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4152
+ * the existing content, merge the patch under the feature's declared policy,
4153
+ * and serialize. Throws when the file or feature is undeclared, when a
4154
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4155
+ * feature's policy is `custom` (those calls go to the named policy function
4156
+ * instead).
4157
+ */
4158
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4159
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4160
+ 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.`);
4161
+ const policy = declaration.features[feature];
4162
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4163
+ 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.`);
4164
+ const base = parseSharedConfig({
4165
+ format: declaration.format,
4166
+ fileContent: existingContent,
4167
+ filePath,
4168
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4169
+ });
4170
+ if (policy.kind === "replace-owned-keys") {
4171
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4172
+ 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.`);
4173
+ return stringifySharedConfig({
4174
+ format: declaration.format,
4175
+ document: mergeSharedConfigShallow({
4176
+ base,
4177
+ patch
4178
+ })
4179
+ });
4180
+ }
4181
+ const merged = mergeSharedConfigDeep({
4182
+ base,
4183
+ patch
4184
+ });
4185
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4186
+ return stringifySharedConfig({
4187
+ format: declaration.format,
4188
+ document: merged
4189
+ });
4190
+ }
4191
+ const READ_TOOL_NAME = "Read";
4192
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4193
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4194
+ const parsePermissionsBlock = (settings) => {
4195
+ const permissions = settings.permissions ?? {};
4196
+ return {
4197
+ allow: permissions.allow ?? [],
4198
+ ask: permissions.ask ?? [],
4199
+ deny: permissions.deny ?? []
4200
+ };
4201
+ };
4202
+ const withPermissions = (settings, next) => {
4203
+ const permissions = { ...settings.permissions };
4204
+ const assign = (key, values) => {
4205
+ if (values.length > 0) permissions[key] = values;
4206
+ else delete permissions[key];
4207
+ };
4208
+ assign("allow", next.allow);
4209
+ assign("ask", next.ask);
4210
+ assign("deny", next.deny);
4211
+ return {
4212
+ ...settings,
4213
+ permissions
4214
+ };
4215
+ };
4216
+ const applyIgnoreReadDenies = (params) => {
4217
+ const { settings, readDenies } = params;
4218
+ const current = parsePermissionsBlock(settings);
4219
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4220
+ return withPermissions(settings, {
4221
+ allow: current.allow,
4222
+ ask: current.ask,
4223
+ deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
4224
+ });
4225
+ };
4226
+ const applyPermissions = (params) => {
4227
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4228
+ const current = parsePermissionsBlock(settings);
4229
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4230
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4231
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4232
+ 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.`);
4233
+ }
4234
+ return withPermissions(settings, {
4235
+ allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
4236
+ ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
4237
+ deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
4238
+ });
4239
+ };
4240
+ //#endregion
3922
4241
  //#region src/features/opencode-config.ts
3923
4242
  /**
3924
4243
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3943,9 +4262,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3943
4262
  });
3944
4263
  const fileContent = await readFileContentOrNull((0, node_path.join)(configDir, "opencode.jsonc")) ?? await readFileContentOrNull((0, node_path.join)(configDir, "opencode.json"));
3945
4264
  if (!fileContent) return {};
3946
- const parsed = (0, jsonc_parser.parse)(fileContent);
3947
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3948
- return parsed;
4265
+ return parseSharedConfig({
4266
+ format: "jsonc",
4267
+ fileContent
4268
+ });
3949
4269
  }
3950
4270
  /**
3951
4271
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4854,32 +5174,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4854
5174
  }
4855
5175
  };
4856
5176
  //#endregion
4857
- //#region src/constants/takt-paths.ts
4858
- const TAKT_DIR = ".takt";
4859
- const TAKT_FACETS_SUBDIR = "facets";
4860
- const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
4861
- const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
4862
- const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
4863
- const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
4864
- const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
4865
- const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
4866
- const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4867
- /**
4868
- * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
4869
- * `~/.takt/config.yaml` (global); it holds the active provider, provider
4870
- * profiles (including permission modes), and other Takt settings.
4871
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4872
- */
4873
- const TAKT_CONFIG_FILE_NAME = "config.yaml";
4874
- /**
4875
- * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4876
- * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4877
- * transport must be explicitly enabled here before any workflow-defined MCP
4878
- * server using it is permitted to run.
4879
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4880
- */
4881
- const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4882
- //#endregion
4883
5177
  //#region src/features/takt-shared.ts
4884
5178
  /**
4885
5179
  * Shared utilities for all TAKT-* tool file classes.
@@ -6357,42 +6651,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6357
6651
  */
6358
6652
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6359
6653
  //#endregion
6360
- //#region src/utils/prototype-pollution.ts
6361
- /**
6362
- * Keys that, if walked into when constructing or merging objects from
6363
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
6364
- * chain) and propagate state to every other object in the runtime. Any code
6365
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
6366
- * parsing, MCP config conversion, settings round-trip — should skip these.
6367
- */
6368
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
6369
- "__proto__",
6370
- "constructor",
6371
- "prototype"
6372
- ]);
6373
- function isPrototypePollutionKey(key) {
6374
- return PROTOTYPE_POLLUTION_KEYS.has(key);
6375
- }
6376
- /**
6377
- * Returns a shallow copy of a record's own entries with every
6378
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
6379
- *
6380
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
6381
- * or `headers` table — into freshly generated config. Carrying such a map by
6382
- * reference, or re-assigning its keys via bracket notation, would let a literal
6383
- * `__proto__` key ride along (and re-assigning it would mutate the target's
6384
- * prototype). Walking the entries through this helper severs that path while
6385
- * preserving every legitimate key.
6386
- */
6387
- function omitPrototypePollutionKeys(record) {
6388
- const sanitized = {};
6389
- for (const [key, value] of Object.entries(record)) {
6390
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6391
- sanitized[key] = value;
6392
- }
6393
- return sanitized;
6394
- }
6395
- //#endregion
6396
6654
  //#region src/features/hooks/tool-hooks-converter.ts
6397
6655
  function isToolMatcherEntry(x) {
6398
6656
  if (x === null || typeof x !== "object") return false;
@@ -7051,24 +7309,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7051
7309
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7052
7310
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7053
7311
  const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
7054
- let settings;
7055
- try {
7056
- settings = JSON.parse(existingContent);
7057
- } catch (error) {
7058
- throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
7059
- }
7060
7312
  const config = rulesyncHooks.getJson();
7061
- const claudeHooks = canonicalToToolHooks({
7062
- config,
7063
- toolOverrideHooks: config.claudecode?.hooks,
7064
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7065
- logger
7313
+ const fileContent = applySharedConfigPatch({
7314
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7315
+ feature: "hooks",
7316
+ existingContent,
7317
+ patch: { hooks: canonicalToToolHooks({
7318
+ config,
7319
+ toolOverrideHooks: config.claudecode?.hooks,
7320
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7321
+ logger
7322
+ }) },
7323
+ filePath
7066
7324
  });
7067
- const merged = {
7068
- ...settings,
7069
- hooks: claudeHooks
7070
- };
7071
- const fileContent = JSON.stringify(merged, null, 2);
7072
7325
  return new ClaudecodeHooks({
7073
7326
  outputRoot,
7074
7327
  relativeDirPath: paths.relativeDirPath,
@@ -8300,55 +8553,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8300
8553
  }
8301
8554
  };
8302
8555
  //#endregion
8303
- //#region src/features/hermes-config.ts
8304
- function sanitizeHermesConfigValue(value) {
8305
- if (Array.isArray(value)) return value.map(sanitizeHermesConfigValue);
8306
- if (!isPlainObject(value)) return value;
8307
- const sanitized = omitPrototypePollutionKeys(value);
8308
- const result = {};
8309
- for (const [key, nestedValue] of Object.entries(sanitized)) {
8310
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8311
- result[key] = sanitizeHermesConfigValue(nestedValue);
8312
- }
8313
- return result;
8314
- }
8315
- function parseHermesConfig(fileContent) {
8316
- if (!fileContent.trim()) return {};
8317
- const parsed = (0, js_yaml.load)(fileContent);
8318
- if (isPlainObject(parsed)) return sanitizeHermesConfigValue(parsed);
8319
- return {};
8320
- }
8321
- function stringifyHermesConfig(config) {
8322
- return (0, js_yaml.dump)(config, {
8323
- noRefs: true,
8324
- sortKeys: false
8325
- }).trimEnd() + "\n";
8326
- }
8327
- function mergeHermesConfig(fileContent, patch) {
8328
- return stringifyHermesConfig({
8329
- ...parseHermesConfig(fileContent),
8330
- ...patch
8331
- });
8332
- }
8333
- /**
8334
- * Recursively merge `patch` into `base` (patch wins). Nested plain objects are
8335
- * merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
8336
- * Used to overlay the Hermes-scoped permission override onto the natively
8337
- * emitted `approvals`/`security` structures without one clobbering the other
8338
- * (e.g. `approvals.deny` from canonical deny rules coexisting with an
8339
- * `approvals.mode` from the override). Prototype-pollution keys are dropped.
8340
- */
8341
- function deepMergeHermesConfig(base, patch) {
8342
- const result = { ...base };
8343
- for (const [key, patchValue] of Object.entries(patch)) {
8344
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8345
- const baseValue = result[key];
8346
- if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
8347
- else result[key] = sanitizeHermesConfigValue(patchValue);
8348
- }
8349
- return result;
8350
- }
8351
- //#endregion
8352
8556
  //#region src/features/hooks/hermesagent-hooks.ts
8353
8557
  /**
8354
8558
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8476,10 +8680,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8476
8680
  return true;
8477
8681
  }
8478
8682
  setFileContent(fileContent) {
8479
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8683
+ this.fileContent = applySharedConfigPatch({
8684
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8685
+ feature: "hooks",
8686
+ existingContent: fileContent,
8687
+ patch: parseSharedConfig({
8688
+ format: "yaml",
8689
+ fileContent: this.fileContent
8690
+ })
8691
+ });
8480
8692
  }
8481
8693
  toRulesyncHooks() {
8482
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8694
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8695
+ format: "yaml",
8696
+ fileContent: this.getFileContent()
8697
+ }).hooks);
8483
8698
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8484
8699
  version: 1,
8485
8700
  hooks
@@ -8489,11 +8704,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8489
8704
  const config = rulesyncHooks.getJson();
8490
8705
  return new HermesagentHooks({
8491
8706
  outputRoot,
8492
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8493
- config,
8494
- toolOverrideHooks: config.hermesagent?.hooks,
8495
- logger
8496
- }) })
8707
+ fileContent: stringifySharedConfig({
8708
+ format: "yaml",
8709
+ document: { hooks: canonicalToHermesHooks({
8710
+ config,
8711
+ toolOverrideHooks: config.hermesagent?.hooks,
8712
+ logger
8713
+ }) }
8714
+ })
8497
8715
  });
8498
8716
  }
8499
8717
  };
@@ -10513,66 +10731,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10513
10731
  }
10514
10732
  };
10515
10733
  //#endregion
10516
- //#region src/features/claudecode-settings-gateway.ts
10517
- /**
10518
- * Single owner of the `.claude/settings.json` `permissions` block, which both
10519
- * `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
10520
- * (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
10521
- * the merge, and the cross-feature ownership rule (permissions' explicit `Read`
10522
- * rules win over ignore-derived `Read` denies) used to be duplicated across both
10523
- * feature files; they live here once so each feature just states its intent and
10524
- * never reasons about the other's existence.
10525
- */
10526
- const READ_TOOL_NAME = "Read";
10527
- const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
10528
- const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
10529
- const parsePermissionsBlock = (settings) => {
10530
- const permissions = settings.permissions ?? {};
10531
- return {
10532
- allow: permissions.allow ?? [],
10533
- ask: permissions.ask ?? [],
10534
- deny: permissions.deny ?? []
10535
- };
10536
- };
10537
- const withPermissions = (settings, next) => {
10538
- const permissions = { ...settings.permissions };
10539
- const assign = (key, values) => {
10540
- if (values.length > 0) permissions[key] = values;
10541
- else delete permissions[key];
10542
- };
10543
- assign("allow", next.allow);
10544
- assign("ask", next.ask);
10545
- assign("deny", next.deny);
10546
- return {
10547
- ...settings,
10548
- permissions
10549
- };
10550
- };
10551
- const applyIgnoreReadDenies = (params) => {
10552
- const { settings, readDenies } = params;
10553
- const current = parsePermissionsBlock(settings);
10554
- const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
10555
- return withPermissions(settings, {
10556
- allow: current.allow,
10557
- ask: current.ask,
10558
- deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
10559
- });
10560
- };
10561
- const applyPermissions = (params) => {
10562
- const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
10563
- const current = parsePermissionsBlock(settings);
10564
- const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
10565
- if (logger && managedToolNames.has(READ_TOOL_NAME)) {
10566
- const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
10567
- 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.`);
10568
- }
10569
- return withPermissions(settings, {
10570
- allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
10571
- ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
10572
- deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
10573
- });
10574
- };
10575
- //#endregion
10576
10734
  //#region src/features/ignore/claudecode-ignore.ts
10577
10735
  const DEFAULT_FILE_MODE = "shared";
10578
10736
  /**
@@ -11995,7 +12153,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11995
12153
  };
11996
12154
  //#endregion
11997
12155
  //#region src/features/mcp/augmentcode-mcp.ts
11998
- const AUGMENTCODE_GLOBAL_ONLY_MESSAGE = "AugmentCode MCP is global-only; use --global to sync ~/.augment/settings.json";
11999
12156
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
12000
12157
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
12001
12158
  let parsed;
@@ -12010,13 +12167,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
12010
12167
  /**
12011
12168
  * AugmentCode (Auggie CLI) MCP servers.
12012
12169
  *
12013
- * MCP servers are persisted in the shared user settings file
12014
- * `~/.augment/settings.json` (global only the docs do not document a
12015
- * project-level MCP location). That same file also holds `hooks` and
12170
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12171
+ * at either scope: the committed workspace file for team-shared servers (project)
12172
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
12016
12173
  * `toolPermissions`, so generation merges the `mcpServers` block into the
12017
- * existing settings instead of overwriting it, and the file is never deleted.
12174
+ * existing settings instead of overwriting it, and the file is never deleted. On
12175
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12176
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
12018
12177
  *
12019
- * @see https://docs.augmentcode.com/cli/integrations
12178
+ * @see https://docs.augmentcode.com/cli/config
12020
12179
  */
12021
12180
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12022
12181
  json;
@@ -12038,9 +12197,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12038
12197
  };
12039
12198
  }
12040
12199
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12041
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12042
12200
  const paths = this.getSettablePaths({ global });
12043
- const json = parseAugmentcodeSettings(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12201
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12202
+ outputRoot,
12203
+ relativeDirPath: paths.relativeDirPath,
12204
+ baseFileName: paths.relativeFilePath,
12205
+ baseFallbackContent: "{}",
12206
+ includeLocalOverlay: !global
12207
+ }), paths.relativeDirPath, paths.relativeFilePath);
12044
12208
  const newJson = {
12045
12209
  ...json,
12046
12210
  mcpServers: json.mcpServers ?? {}
@@ -12055,7 +12219,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12055
12219
  });
12056
12220
  }
12057
12221
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12058
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12059
12222
  const paths = this.getSettablePaths({ global });
12060
12223
  const merged = {
12061
12224
  ...parseAugmentcodeSettings(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -13705,7 +13868,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13705
13868
  config;
13706
13869
  constructor(params) {
13707
13870
  super(params);
13708
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13871
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13872
+ format: "yaml",
13873
+ fileContent: this.fileContent
13874
+ }) : {};
13709
13875
  }
13710
13876
  getConfig() {
13711
13877
  return this.config;
@@ -13714,9 +13880,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13714
13880
  return true;
13715
13881
  }
13716
13882
  setFileContent(fileContent) {
13717
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13883
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13884
+ format: "yaml",
13885
+ fileContent
13886
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13718
13887
  this.config = merged;
13719
- super.setFileContent(stringifyHermesConfig(merged));
13888
+ super.setFileContent(applySharedConfigPatch({
13889
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13890
+ feature: "mcp",
13891
+ existingContent: fileContent,
13892
+ patch: { mcp_servers: merged.mcp_servers }
13893
+ }));
13720
13894
  }
13721
13895
  isDeletable() {
13722
13896
  return false;
@@ -13743,12 +13917,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13743
13917
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13744
13918
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13745
13919
  const paths = this.getSettablePaths({ global });
13746
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13920
+ const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13921
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13922
+ format: "yaml",
13923
+ fileContent
13924
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13747
13925
  return new HermesagentMcp({
13748
13926
  outputRoot,
13749
13927
  relativeDirPath: paths.relativeDirPath,
13750
13928
  relativeFilePath: paths.relativeFilePath,
13751
- fileContent: stringifyHermesConfig(merged),
13929
+ fileContent: applySharedConfigPatch({
13930
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13931
+ feature: "mcp",
13932
+ existingContent: fileContent,
13933
+ patch: { mcp_servers: merged.mcp_servers }
13934
+ }),
13752
13935
  validate,
13753
13936
  global
13754
13937
  });
@@ -14913,28 +15096,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14913
15096
  }
14914
15097
  };
14915
15098
  //#endregion
14916
- //#region src/features/shared/takt-config.ts
14917
- /**
14918
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14919
- *
14920
- * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14921
- * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14922
- * instances are rejected for prototype-pollution hardening; a YAML mapping
14923
- * always parses to a plain object.
14924
- */
14925
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14926
- const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
14927
- let parsed;
14928
- try {
14929
- parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
14930
- } catch (error) {
14931
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14932
- }
14933
- if (parsed === void 0 || parsed === null) return {};
14934
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14935
- return parsed;
14936
- }
14937
- //#endregion
14938
15099
  //#region src/features/mcp/takt-mcp.ts
14939
15100
  /**
14940
15101
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -15000,17 +15161,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
15000
15161
  }
15001
15162
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15002
15163
  const paths = TaktMcp.getSettablePaths({ global });
15003
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15164
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15165
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15004
15166
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
15005
- const merged = {
15006
- ...config,
15007
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
15008
- };
15009
15167
  return new TaktMcp({
15010
15168
  outputRoot,
15011
15169
  relativeDirPath: paths.relativeDirPath,
15012
15170
  relativeFilePath: paths.relativeFilePath,
15013
- fileContent: (0, js_yaml.dump)(merged),
15171
+ fileContent: applySharedConfigPatch({
15172
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15173
+ feature: "mcp",
15174
+ existingContent,
15175
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15176
+ filePath
15177
+ }),
15014
15178
  validate,
15015
15179
  global
15016
15180
  });
@@ -15455,7 +15619,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15455
15619
  ["augmentcode", {
15456
15620
  class: AugmentcodeMcp,
15457
15621
  meta: {
15458
- supportsProject: false,
15622
+ supportsProject: true,
15459
15623
  supportsGlobal: true,
15460
15624
  supportsEnabledTools: false,
15461
15625
  supportsDisabledTools: false
@@ -16037,15 +16201,97 @@ const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
16037
16201
  defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
16038
16202
  });
16039
16203
  /**
16204
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16205
+ * permission surfaces the canonical coarse-mode mapping can't express:
16206
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16207
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16208
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16209
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16210
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16211
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16212
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16213
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16214
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16215
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16216
+ * scope are supported.
16217
+ *
16218
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16219
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16220
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16221
+ * of scope for this override.
16222
+ *
16223
+ * @example
16224
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16225
+ */
16226
+ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
16227
+ step_permission_overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16228
+ provider_options: zod_mini.z.optional(zod_mini.z.looseObject({}))
16229
+ });
16230
+ /**
16231
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16232
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16233
+ * express, so they are authored here and merged into the shared Amp settings
16234
+ * file, while the shared `permission` block continues to drive the canonical
16235
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16236
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16237
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16238
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16239
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16240
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16241
+ * later fallbacks), preserving author order.
16242
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16243
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16244
+ * confirmation).
16245
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16246
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16247
+ * supported.
16248
+ *
16249
+ * @example
16250
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16251
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16252
+ */
16253
+ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
16254
+ permissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
16255
+ tool: zod_mini.z.string(),
16256
+ action: zod_mini.z.string()
16257
+ }))),
16258
+ mcpPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({}))),
16259
+ guardedFiles: zod_mini.z.optional(zod_mini.z.looseObject({ allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
16260
+ dangerouslyAllowAll: zod_mini.z.optional(zod_mini.z.boolean())
16261
+ });
16262
+ /**
16263
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16264
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16265
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16266
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16267
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16268
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16269
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16270
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16271
+ * Fields placed here are merged onto the top level of
16272
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16273
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16274
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16275
+ * Verified against https://antigravity.google/docs/cli/reference and
16276
+ * https://antigravity.google/docs/cli/sandbox.
16277
+ *
16278
+ * @example
16279
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16280
+ */
16281
+ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
16282
+ toolPermission: zod_mini.z.optional(zod_mini.z.string()),
16283
+ enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
16284
+ });
16285
+ /**
16040
16286
  * Permissions configuration.
16041
16287
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
16042
16288
  * Values are pattern-to-action mappings for that tool category.
16043
16289
  *
16044
16290
  * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
16045
- * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie` keys are tool-scoped
16046
- * overrides consumed only by their respective translator (see the matching
16047
- * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
16048
- * block and ignores them.
16291
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16292
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16293
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16294
+ * other tool reads the shared `permission` block and ignores them.
16049
16295
  *
16050
16296
  * @example
16051
16297
  * {
@@ -16066,7 +16312,10 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
16066
16312
  reasonix: zod_mini.z.optional(ReasonixPermissionsOverrideSchema),
16067
16313
  factorydroid: zod_mini.z.optional(FactorydroidPermissionsOverrideSchema),
16068
16314
  warp: zod_mini.z.optional(WarpPermissionsOverrideSchema),
16069
- junie: zod_mini.z.optional(JuniePermissionsOverrideSchema)
16315
+ junie: zod_mini.z.optional(JuniePermissionsOverrideSchema),
16316
+ takt: zod_mini.z.optional(TaktPermissionsOverrideSchema),
16317
+ amp: zod_mini.z.optional(AmpPermissionsOverrideSchema),
16318
+ "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema)
16070
16319
  });
16071
16320
  /**
16072
16321
  * Full permissions file schema including optional $schema field.
@@ -16178,6 +16427,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
16178
16427
  * Reference: https://ampcode.com/manual ("amp.permissions").
16179
16428
  */
16180
16429
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16430
+ /**
16431
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16432
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16433
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16434
+ * permissions override. Reference: https://ampcode.com/manual.
16435
+ */
16436
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16437
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16438
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16439
+ /**
16440
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16441
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16442
+ */
16443
+ function ampMatchesCmd(entry) {
16444
+ const cmd = entry.matches?.cmd;
16445
+ return typeof cmd === "string" ? cmd : void 0;
16446
+ }
16447
+ /**
16448
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16449
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16450
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16451
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16452
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16453
+ * override verbatim instead of being flattened with loss or dropped.
16454
+ */
16455
+ function isCanonicalAmpEntry(entry) {
16456
+ if (entry.action === "delegate") return false;
16457
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16458
+ const matches = entry.matches;
16459
+ if (matches === void 0) return true;
16460
+ const keys = Object.keys(matches);
16461
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16462
+ }
16181
16463
  function parseAmpSettings(fileContent) {
16182
16464
  const errors = [];
16183
16465
  const parsed = (0, jsonc_parser.parse)(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -16193,10 +16475,20 @@ function toDisableList(value) {
16193
16475
  return value.filter((entry) => typeof entry === "string");
16194
16476
  }
16195
16477
  /**
16478
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16479
+ * settings, returning `undefined` when the key is absent or not an array so the
16480
+ * override omits it.
16481
+ */
16482
+ function extractAmpGuardedAllowlist(value) {
16483
+ if (!Array.isArray(value)) return void 0;
16484
+ return value.filter((entry) => typeof entry === "string");
16485
+ }
16486
+ /**
16196
16487
  * Read an `amp.permissions` array from untrusted parsed settings. Only entries
16197
- * whose shape rulesync understands are retained; the original objects are kept
16198
- * by reference (with a normalized `action`) so unknown sibling keys survive a
16199
- * round-trip when the entry is preserved.
16488
+ * whose `tool`/`action` rulesync understands are retained; every other key
16489
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16490
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16491
+ * round-trip.
16200
16492
  */
16201
16493
  function toPermissionsList(value) {
16202
16494
  if (!Array.isArray(value)) return [];
@@ -16206,14 +16498,11 @@ function toPermissionsList(value) {
16206
16498
  const { tool, action } = raw;
16207
16499
  if (typeof tool !== "string" || typeof action !== "string") continue;
16208
16500
  if (action !== "allow" && action !== "reject" && action !== "ask" && action !== "delegate") continue;
16209
- const matches = raw.matches;
16210
- let normalizedMatches;
16211
- if (isPlainObject(matches) && typeof matches.cmd === "string") normalizedMatches = { cmd: matches.cmd };
16212
16501
  entries.push({
16213
16502
  ...raw,
16214
16503
  tool,
16215
16504
  action,
16216
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16505
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
16217
16506
  });
16218
16507
  }
16219
16508
  return entries;
@@ -16238,6 +16527,13 @@ function toPermissionsList(value) {
16238
16527
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
16239
16528
  * and writes merge into the existing JSON rather than overwriting it, and the
16240
16529
  * file is never deleted.
16530
+ *
16531
+ * Amp shapes with no canonical category are authored and round-tripped through
16532
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16533
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16534
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16535
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16536
+ * `amp.dangerouslyAllowAll` settings.
16241
16537
  */
16242
16538
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
16243
16539
  constructor(params) {
@@ -16307,15 +16603,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16307
16603
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
16308
16604
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
16309
16605
  const json = fileContent ? parseAmpSettings(fileContent) : {};
16310
- const { disable, permissions } = convertRulesyncToAmp(rulesyncPermissions.getJson());
16311
- const preservedDelegates = toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16606
+ const config = rulesyncPermissions.getJson();
16607
+ const { disable, permissions } = convertRulesyncToAmp(config);
16608
+ const override = config.amp;
16609
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16610
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16312
16611
  const newJson = {
16313
16612
  ...json,
16314
16613
  [AMP_TOOLS_DISABLE_KEY]: disable
16315
16614
  };
16316
- const mergedPermissions = [...permissions, ...preservedDelegates];
16615
+ const mergedPermissions = mergeAmpPermissions([
16616
+ ...permissions,
16617
+ ...authoredExtras,
16618
+ ...preservedDelegates
16619
+ ]);
16317
16620
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16318
16621
  else delete newJson[AMP_PERMISSIONS_KEY];
16622
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16623
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16624
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16319
16625
  return new AmpPermissions({
16320
16626
  outputRoot,
16321
16627
  relativeDirPath: basePaths.relativeDirPath,
@@ -16326,11 +16632,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16326
16632
  }
16327
16633
  toRulesyncPermissions() {
16328
16634
  const json = parseAmpSettings(this.getFileContent());
16635
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16636
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16637
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16329
16638
  const config = convertAmpToRulesync({
16330
16639
  disable: toDisableList(json[AMP_TOOLS_DISABLE_KEY]),
16331
- permissions: toPermissionsList(json[AMP_PERMISSIONS_KEY])
16332
- });
16333
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16640
+ permissions: canonicalEntries
16641
+ });
16642
+ const ampOverride = {};
16643
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16644
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16645
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16646
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16647
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16648
+ const result = { ...config };
16649
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16650
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16334
16651
  }
16335
16652
  validate() {
16336
16653
  try {
@@ -16377,6 +16694,37 @@ const ACTION_PRIORITY = {
16377
16694
  allow: 2
16378
16695
  };
16379
16696
  /**
16697
+ * Fail-closed action priority for merging the full `amp.permissions` list
16698
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16699
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16700
+ * it never auto-allows) trails as the final fallback.
16701
+ */
16702
+ const MERGE_ACTION_PRIORITY = {
16703
+ reject: 0,
16704
+ ask: 1,
16705
+ allow: 2,
16706
+ delegate: 3
16707
+ };
16708
+ /**
16709
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16710
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16711
+ * order within an action class. Guarantees an authored `reject` cannot be
16712
+ * shadowed by a generated catch-all `allow`.
16713
+ */
16714
+ function mergeAmpPermissions(entries) {
16715
+ const decorated = entries.map((entry, index) => ({
16716
+ entry,
16717
+ index
16718
+ }));
16719
+ decorated.sort((a, b) => {
16720
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16721
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16722
+ if (ap !== bp) return ap - bp;
16723
+ return a.index - b.index;
16724
+ });
16725
+ return decorated.map((d) => d.entry);
16726
+ }
16727
+ /**
16380
16728
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16381
16729
  *
16382
16730
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16441,14 +16789,14 @@ function sortAmpPermissions(entries) {
16441
16789
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16442
16790
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16443
16791
  if (ap !== bp) return ap - bp;
16444
- const aHasCmd = a.entry.matches?.cmd !== void 0 ? 0 : 1;
16445
- const bHasCmd = b.entry.matches?.cmd !== void 0 ? 0 : 1;
16792
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16793
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16446
16794
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16447
16795
  const at = a.entry.tool;
16448
16796
  const bt = b.entry.tool;
16449
16797
  if (at !== bt) return at < bt ? -1 : 1;
16450
- const ac = a.entry.matches?.cmd ?? "";
16451
- const bc = b.entry.matches?.cmd ?? "";
16798
+ const ac = ampMatchesCmd(a.entry) ?? "";
16799
+ const bc = ampMatchesCmd(b.entry) ?? "";
16452
16800
  if (ac !== bc) return ac < bc ? -1 : 1;
16453
16801
  return a.index - b.index;
16454
16802
  });
@@ -16487,7 +16835,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16487
16835
  for (const tool of disable) assign(tool, "*", "deny");
16488
16836
  for (const entry of permissions) {
16489
16837
  if (entry.action === "delegate") continue;
16490
- const pattern = entry.matches?.cmd ?? "*";
16838
+ const pattern = ampMatchesCmd(entry) ?? "*";
16491
16839
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16492
16840
  assign(entry.tool, pattern, action);
16493
16841
  }
@@ -16569,6 +16917,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16569
16917
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16570
16918
  * file (global scope only). The file holds other CLI settings besides
16571
16919
  * permissions, so it is never deleted.
16920
+ *
16921
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16922
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16923
+ * authored and round-tripped through the `antigravity-cli` permissions override
16924
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16572
16925
  */
16573
16926
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16574
16927
  constructor(params) {
@@ -16629,6 +16982,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16629
16982
  ...settings,
16630
16983
  permissions: mergedPermissions
16631
16984
  };
16985
+ const override = config["antigravity-cli"];
16986
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16987
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16632
16988
  const fileContent = JSON.stringify(merged, null, 2);
16633
16989
  return new AntigravityCliPermissions({
16634
16990
  outputRoot,
@@ -16652,7 +17008,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16652
17008
  ask: permissions.ask ?? [],
16653
17009
  deny: permissions.deny ?? []
16654
17010
  });
16655
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17011
+ const override = {};
17012
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
17013
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
17014
+ const result = { ...config };
17015
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
17016
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16656
17017
  }
16657
17018
  validate() {
16658
17019
  return {
@@ -19338,14 +19699,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19338
19699
  return true;
19339
19700
  }
19340
19701
  setFileContent(fileContent) {
19341
- const existing = parseHermesConfig(fileContent);
19342
- const generated = parseHermesConfig(this.fileContent);
19343
- const merged = deepMergeHermesConfig(existing, generated);
19344
- if (generated.permissions !== void 0) merged.permissions = generated.permissions;
19345
- this.fileContent = stringifyHermesConfig(merged);
19702
+ this.fileContent = applySharedConfigPatch({
19703
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19704
+ feature: "permissions",
19705
+ existingContent: fileContent,
19706
+ patch: parseSharedConfig({
19707
+ format: "yaml",
19708
+ fileContent: this.fileContent
19709
+ })
19710
+ });
19346
19711
  }
19347
19712
  toRulesyncPermissions() {
19348
- const config = parseHermesConfig(this.getFileContent());
19713
+ const config = parseSharedConfig({
19714
+ format: "yaml",
19715
+ fileContent: this.getFileContent()
19716
+ });
19349
19717
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
19350
19718
  return new RulesyncPermissions({
19351
19719
  relativeDirPath: "",
@@ -19366,11 +19734,17 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19366
19734
  enabled: true,
19367
19735
  domains: webfetchDeny
19368
19736
  } };
19369
- if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
19737
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19738
+ base: config,
19739
+ patch: permissions.hermes
19740
+ });
19370
19741
  config.permissions = { rulesync: permissions };
19371
19742
  return new HermesagentPermissions({
19372
19743
  outputRoot,
19373
- fileContent: stringifyHermesConfig(config)
19744
+ fileContent: stringifySharedConfig({
19745
+ format: "yaml",
19746
+ document: config
19747
+ })
19374
19748
  });
19375
19749
  }
19376
19750
  };
@@ -20926,6 +21300,8 @@ function isPermissionAction(value) {
20926
21300
  const TAKT_PROVIDER_KEY = "provider";
20927
21301
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20928
21302
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21303
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21304
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20929
21305
  const TAKT_DEFAULT_PROVIDER = "claude";
20930
21306
  const CATCH_ALL_PATTERN = "*";
20931
21307
  /**
@@ -20955,10 +21331,16 @@ const CATCH_ALL_PATTERN = "*";
20955
21331
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20956
21332
  *
20957
21333
  * Both project and global scope are supported. The shared config is merged in
20958
- * place: only `provider_profiles.<provider>.default_permission_mode` is set;
20959
- * the active provider's other keys (e.g. `step_permission_overrides`), every
20960
- * other provider profile, and all other top-level keys are preserved. The file
20961
- * is never deleted.
21334
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21335
+ * derived mode; every other provider profile and all other top-level keys are
21336
+ * preserved. The file is never deleted.
21337
+ *
21338
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21339
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21340
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21341
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21342
+ * (a top-level per-provider sandbox/network table). Both are authored on
21343
+ * generate and re-extracted on import.
20962
21344
  */
20963
21345
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20964
21346
  constructor(params) {
@@ -20990,37 +21372,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20990
21372
  }
20991
21373
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20992
21374
  const paths = TaktPermissions.getSettablePaths({ global });
20993
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21375
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21376
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21377
+ const config = parseSharedConfig({
21378
+ format: "yaml",
21379
+ fileContent: existingContent,
21380
+ filePath,
21381
+ invalidRootPolicy: "error"
21382
+ });
21383
+ const rulesyncJson = rulesyncPermissions.getJson();
20994
21384
  const provider = resolveActiveProvider(config);
20995
- const mode = deriveTaktPermissionMode(rulesyncPermissions.getJson());
20996
- const existingProfiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
20997
- const existingProfile = isPlainObject(existingProfiles[provider]) ? existingProfiles[provider] : {};
20998
- const merged = {
20999
- ...config,
21000
- [TAKT_PROVIDER_PROFILES_KEY]: {
21001
- ...existingProfiles,
21002
- [provider]: {
21003
- ...existingProfile,
21004
- [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode
21005
- }
21006
- }
21385
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21386
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21387
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21388
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21389
+ const patch = {
21390
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21391
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21392
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21393
+ } },
21394
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
21007
21395
  };
21008
21396
  return new TaktPermissions({
21009
21397
  outputRoot,
21010
21398
  relativeDirPath: paths.relativeDirPath,
21011
21399
  relativeFilePath: paths.relativeFilePath,
21012
- fileContent: (0, js_yaml.dump)(merged),
21400
+ fileContent: applySharedConfigPatch({
21401
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21402
+ feature: "permissions",
21403
+ existingContent,
21404
+ patch,
21405
+ filePath
21406
+ }),
21013
21407
  validate: true,
21014
21408
  global
21015
21409
  });
21016
21410
  }
21017
21411
  toRulesyncPermissions() {
21018
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21412
+ const config = parseSharedConfig({
21413
+ format: "yaml",
21414
+ fileContent: this.getFileContent(),
21415
+ filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
21416
+ invalidRootPolicy: "error"
21417
+ });
21019
21418
  const provider = resolveActiveProvider(config);
21020
21419
  const profiles = isPlainObject(config[TAKT_PROVIDER_PROFILES_KEY]) ? config[TAKT_PROVIDER_PROFILES_KEY] : {};
21021
- const mode = (isPlainObject(profiles[provider]) ? profiles[provider] : {})[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21420
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21421
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21022
21422
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
21023
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21423
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21424
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21425
+ const taktOverride = {};
21426
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21427
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21428
+ const result = { ...rulesyncConfig };
21429
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21430
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
21024
21431
  }
21025
21432
  validate() {
21026
21433
  return {
@@ -29915,14 +30322,21 @@ def register(ctx):
29915
30322
  `;
29916
30323
  }
29917
30324
  function getEnabledPluginConfigContent(currentContent) {
29918
- const config = parseHermesConfig(currentContent);
30325
+ const config = parseSharedConfig({
30326
+ format: "yaml",
30327
+ fileContent: currentContent
30328
+ });
29919
30329
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29920
30330
  const enabled = Array.isArray(plugins.enabled) ? plugins.enabled : [];
29921
- config.plugins = {
29922
- ...plugins,
29923
- enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
29924
- };
29925
- return stringifyHermesConfig(config);
30331
+ return applySharedConfigPatch({
30332
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30333
+ feature: "subagents",
30334
+ existingContent: currentContent,
30335
+ patch: { plugins: {
30336
+ ...plugins,
30337
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30338
+ } }
30339
+ });
29926
30340
  }
29927
30341
  function getSubagentSpec(rulesyncSubagent) {
29928
30342
  const json = rulesyncSubagent.getFrontmatter();
@@ -30008,6 +30422,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
30008
30422
  static getSettablePaths() {
30009
30423
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
30010
30424
  }
30425
+ /**
30426
+ * Beyond the subagent spec files, generation also read-modify-writes the
30427
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30428
+ * so the write must be declared for the shared-file order derivation.
30429
+ */
30430
+ static getExtraSharedWritePaths() {
30431
+ return [{
30432
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30433
+ relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
30434
+ }];
30435
+ }
30011
30436
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
30012
30437
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
30013
30438
  }
@@ -31746,7 +32171,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
31746
32171
  })),
31747
32172
  kiro: zod_mini.z.optional(zod_mini.z.looseObject({
31748
32173
  inclusion: zod_mini.z.optional(zod_mini.z.string()),
31749
- fileMatchPattern: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
32174
+ fileMatchPattern: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
32175
+ name: zod_mini.z.optional(zod_mini.z.string()),
32176
+ description: zod_mini.z.optional(zod_mini.z.string())
31750
32177
  })),
31751
32178
  takt: zod_mini.z.optional(zod_mini.z.looseObject({
31752
32179
  name: zod_mini.z.optional(zod_mini.z.string()),
@@ -34662,7 +35089,8 @@ function toFileMatchPattern(globs) {
34662
35089
  *
34663
35090
  * Precedence:
34664
35091
  * 1. An explicit `kiro.inclusion` block round-trips as-is (with `fileMatchPattern`
34665
- * taken from the block, or derived from `globs` when omitted for `fileMatch`).
35092
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35093
+ * and with the companion `name`/`description` carried through for `auto`).
34666
35094
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
34667
35095
  * to matching files instead of leaving it implicitly always-on.
34668
35096
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -34680,6 +35108,11 @@ function deriveKiroInclusion({ kiro, globs }) {
34680
35108
  fileMatchPattern
34681
35109
  } : { inclusion: "fileMatch" };
34682
35110
  }
35111
+ if (kiro.inclusion === "auto") return {
35112
+ inclusion: "auto",
35113
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35114
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35115
+ };
34683
35116
  return { inclusion: kiro.inclusion };
34684
35117
  }
34685
35118
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -34751,6 +35184,8 @@ var KiroRule = class KiroRule extends ToolRule {
34751
35184
  const rawPattern = frontmatter.fileMatchPattern;
34752
35185
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
34753
35186
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35187
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35188
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
34754
35189
  return new RulesyncRule({
34755
35190
  outputRoot: process.cwd(),
34756
35191
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -34761,7 +35196,9 @@ var KiroRule = class KiroRule extends ToolRule {
34761
35196
  globs,
34762
35197
  kiro: {
34763
35198
  inclusion,
34764
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35199
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35200
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35201
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34765
35202
  }
34766
35203
  },
34767
35204
  body
@@ -36935,6 +37372,197 @@ function buildPermissionsStrategy(ctx) {
36935
37372
  };
36936
37373
  }
36937
37374
  //#endregion
37375
+ //#region src/types/processor-registry.ts
37376
+ const PROCESSOR_REGISTRY = [
37377
+ {
37378
+ feature: "rules",
37379
+ processor: RulesProcessor,
37380
+ schema: RulesProcessorToolTargetSchema,
37381
+ factory: toolRuleFactories
37382
+ },
37383
+ {
37384
+ feature: "ignore",
37385
+ processor: IgnoreProcessor,
37386
+ schema: IgnoreProcessorToolTargetSchema,
37387
+ factory: toolIgnoreFactories
37388
+ },
37389
+ {
37390
+ feature: "mcp",
37391
+ processor: McpProcessor,
37392
+ schema: McpProcessorToolTargetSchema,
37393
+ factory: toolMcpFactories
37394
+ },
37395
+ {
37396
+ feature: "commands",
37397
+ processor: CommandsProcessor,
37398
+ schema: CommandsProcessorToolTargetSchema,
37399
+ factory: toolCommandFactories
37400
+ },
37401
+ {
37402
+ feature: "subagents",
37403
+ processor: SubagentsProcessor,
37404
+ schema: SubagentsProcessorToolTargetSchema,
37405
+ factory: toolSubagentFactories
37406
+ },
37407
+ {
37408
+ feature: "skills",
37409
+ processor: SkillsProcessor,
37410
+ schema: SkillsProcessorToolTargetSchema,
37411
+ factory: toolSkillFactories
37412
+ },
37413
+ {
37414
+ feature: "hooks",
37415
+ processor: HooksProcessor,
37416
+ schema: HooksProcessorToolTargetSchema,
37417
+ factory: toolHooksFactories
37418
+ },
37419
+ {
37420
+ feature: "permissions",
37421
+ processor: PermissionsProcessor,
37422
+ schema: PermissionsProcessorToolTargetSchema,
37423
+ factory: toolPermissionsFactories
37424
+ }
37425
+ ];
37426
+ const getProcessorRegistryEntry = (feature) => {
37427
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37428
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37429
+ return entry;
37430
+ };
37431
+ //#endregion
37432
+ //#region src/lib/shared-file-derive.ts
37433
+ /**
37434
+ * The single declaration of the cross-feature write order for shared
37435
+ * (read-modify-write) config files: when two features write the same on-disk
37436
+ * file, the earlier one writes first and the later one merges on top, so the
37437
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37438
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37439
+ * The generation step graph's `dependsOn` edges are derived from this list
37440
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37441
+ * shared path never requires touching the graph by hand.
37442
+ */
37443
+ const SHARED_WRITE_FEATURE_ORDER = [
37444
+ "ignore",
37445
+ "subagents",
37446
+ "mcp",
37447
+ "hooks",
37448
+ "permissions",
37449
+ "rules"
37450
+ ];
37451
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37452
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37453
+ const sharedFileKey = (path) => {
37454
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37455
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37456
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37457
+ };
37458
+ const settablePathsForScope = (cls, global) => {
37459
+ const paths = [];
37460
+ let settable;
37461
+ try {
37462
+ settable = cls.getSettablePaths?.({ global });
37463
+ } catch {
37464
+ return paths;
37465
+ }
37466
+ if (settable?.relativeFilePath) paths.push({
37467
+ relativeDirPath: settable.relativeDirPath ?? ".",
37468
+ relativeFilePath: settable.relativeFilePath
37469
+ });
37470
+ if (settable?.root) {
37471
+ paths.push(settable.root);
37472
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37473
+ }
37474
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37475
+ return paths;
37476
+ };
37477
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37478
+ /**
37479
+ * Derive, from the processor registry, every on-disk file that two or more
37480
+ * features read-modify-write. Source of truth the generation step graph's
37481
+ * `writesSharedFile` declarations must match.
37482
+ */
37483
+ const deriveSharedFileWriters = () => {
37484
+ const byKey = /* @__PURE__ */ new Map();
37485
+ const pathByKey = /* @__PURE__ */ new Map();
37486
+ for (const entry of PROCESSOR_REGISTRY) {
37487
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37488
+ const factories = entry.factory;
37489
+ for (const [tool, factory] of factories) {
37490
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37491
+ for (const path of collectFactoryPaths(factory)) {
37492
+ const key = sharedFileKey(path);
37493
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37494
+ let features = byKey.get(key);
37495
+ if (!features) {
37496
+ features = /* @__PURE__ */ new Map();
37497
+ byKey.set(key, features);
37498
+ }
37499
+ let tools = features.get(entry.feature);
37500
+ if (!tools) {
37501
+ tools = /* @__PURE__ */ new Set();
37502
+ features.set(entry.feature, tools);
37503
+ }
37504
+ tools.add(tool);
37505
+ }
37506
+ }
37507
+ }
37508
+ const writers = [];
37509
+ for (const [key, features] of byKey) {
37510
+ if (features.size < 2) continue;
37511
+ const path = pathByKey.get(key);
37512
+ const toolsByFeature = /* @__PURE__ */ new Map();
37513
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37514
+ writers.push({
37515
+ key,
37516
+ relativeDirPath: path.relativeDirPath,
37517
+ relativeFilePath: path.relativeFilePath,
37518
+ features: [...features.keys()].toSorted(),
37519
+ toolsByFeature
37520
+ });
37521
+ }
37522
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37523
+ };
37524
+ /**
37525
+ * Derive, per shared-write feature, the shared files it writes and the
37526
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37527
+ * writer depends on all writers that precede it in
37528
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37529
+ * graph consumes, so registry changes (a new tool, a new settable path)
37530
+ * propagate into the execution order without a hand-maintained declaration.
37531
+ *
37532
+ * @throws Error if a feature writes a shared file but has no position in
37533
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37534
+ * whose merge policy wins, so it must be made explicitly there.
37535
+ */
37536
+ const deriveSharedWriteSteps = () => {
37537
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37538
+ const filesByFeature = /* @__PURE__ */ new Map();
37539
+ const depsByFeature = /* @__PURE__ */ new Map();
37540
+ for (const writer of deriveSharedFileWriters()) {
37541
+ 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.`);
37542
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37543
+ for (const [position, feature] of ordered.entries()) {
37544
+ let files = filesByFeature.get(feature);
37545
+ if (!files) {
37546
+ files = /* @__PURE__ */ new Set();
37547
+ filesByFeature.set(feature, files);
37548
+ }
37549
+ files.add(writer.key);
37550
+ let deps = depsByFeature.get(feature);
37551
+ if (!deps) {
37552
+ deps = /* @__PURE__ */ new Set();
37553
+ depsByFeature.set(feature, deps);
37554
+ }
37555
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37556
+ }
37557
+ }
37558
+ const steps = /* @__PURE__ */ new Map();
37559
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37560
+ writesSharedFile: [...files].toSorted(),
37561
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37562
+ });
37563
+ return steps;
37564
+ };
37565
+ //#endregion
36938
37566
  //#region src/lib/generate.ts
36939
37567
  async function processFeatureGeneration(params) {
36940
37568
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -37084,98 +37712,53 @@ function resolveExecutionOrder(steps) {
37084
37712
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
37085
37713
  return ordered;
37086
37714
  }
37715
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37716
+ const sharedWriteMeta = (id) => {
37717
+ const step = SHARED_WRITE_STEPS.get(id);
37718
+ return step ? {
37719
+ writesSharedFile: step.writesSharedFile,
37720
+ dependsOn: step.dependsOn
37721
+ } : {};
37722
+ };
37087
37723
  /**
37088
37724
  * The static shape of the generation step graph: which steps write which shared
37089
37725
  * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
37090
- * for those writers. Exported (separately from the `run` closures, which need a
37091
- * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
37092
- * tested directly against the real graph rather than a hand-copied one. Readonly
37093
- * so a consumer can't mutate this module-level singleton and affect every
37094
- * subsequent `generate()` call in the process.
37726
+ * for those writers. Both are derived from the processor registry's settable
37727
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37728
+ * tool or shared path never requires editing this graph. Exported (separately
37729
+ * from the `run` closures, which need a live `config`/`logger`) so
37730
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37731
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37732
+ * mutate this module-level singleton and affect every subsequent `generate()`
37733
+ * call in the process.
37095
37734
  */
37096
37735
  const GENERATION_STEP_GRAPH = [
37097
37736
  {
37098
37737
  id: "ignore",
37099
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37738
+ ...sharedWriteMeta("ignore")
37100
37739
  },
37101
37740
  {
37102
37741
  id: "mcp",
37103
- writesSharedFile: [
37104
- ".amp/settings.json",
37105
- ".augment/settings.json",
37106
- ".codex/config.toml",
37107
- ".config/amp/settings.json",
37108
- ".config/devin/config.json",
37109
- ".config/opencode/opencode.json",
37110
- ".config/zed/settings.json",
37111
- ".devin/config.json",
37112
- ".grok/config.toml",
37113
- ".hermes/config.yaml",
37114
- ".qwen/settings.json",
37115
- ".reasonix/config.toml",
37116
- ".takt/config.yaml",
37117
- ".vibe/config.toml",
37118
- ".zed/settings.json",
37119
- "kilo.json",
37120
- "opencode.json",
37121
- "reasonix.toml"
37122
- ],
37123
- dependsOn: ["ignore"]
37742
+ ...sharedWriteMeta("mcp")
37124
37743
  },
37125
37744
  { id: "commands" },
37126
- { id: "subagents" },
37745
+ {
37746
+ id: "subagents",
37747
+ ...sharedWriteMeta("subagents")
37748
+ },
37127
37749
  { id: "skills" },
37128
37750
  {
37129
37751
  id: "hooks",
37130
- writesSharedFile: [
37131
- ".augment/settings.json",
37132
- ".claude/settings.json",
37133
- ".codex/config.toml",
37134
- ".config/devin/config.json",
37135
- ".hermes/config.yaml",
37136
- ".kiro/agents/default.json",
37137
- ".qwen/settings.json",
37138
- ".vibe/config.toml"
37139
- ],
37140
- dependsOn: ["ignore", "mcp"]
37752
+ ...sharedWriteMeta("hooks")
37141
37753
  },
37142
37754
  {
37143
37755
  id: "permissions",
37144
- writesSharedFile: [
37145
- ".amp/settings.json",
37146
- ".augment/settings.json",
37147
- ".claude/settings.json",
37148
- ".codex/config.toml",
37149
- ".config/amp/settings.json",
37150
- ".config/devin/config.json",
37151
- ".config/opencode/opencode.json",
37152
- ".config/zed/settings.json",
37153
- ".devin/config.json",
37154
- ".grok/config.toml",
37155
- ".hermes/config.yaml",
37156
- ".kiro/agents/default.json",
37157
- ".qwen/settings.json",
37158
- ".reasonix/config.toml",
37159
- ".takt/config.yaml",
37160
- ".vibe/config.toml",
37161
- ".zed/settings.json",
37162
- "opencode.json",
37163
- "reasonix.toml"
37164
- ],
37165
- dependsOn: [
37166
- "ignore",
37167
- "hooks",
37168
- "mcp"
37169
- ]
37756
+ ...sharedWriteMeta("permissions")
37170
37757
  },
37171
37758
  {
37172
37759
  id: "rules",
37173
- writesSharedFile: ["kilo.json", "opencode.json"],
37174
- dependsOn: [
37175
- "mcp",
37176
- "skills",
37177
- "permissions"
37178
- ]
37760
+ ...sharedWriteMeta("rules"),
37761
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
37179
37762
  }
37180
37763
  ];
37181
37764
  /**
@@ -37290,7 +37873,7 @@ async function generateRulesCore(params) {
37290
37873
  targets: config.getConfigFileTargets(),
37291
37874
  global: config.getGlobal()
37292
37875
  }) : /* @__PURE__ */ new Map();
37293
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
37876
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37294
37877
  if (!config.getFeatures(toolTarget).includes("rules")) continue;
37295
37878
  const processor = new RulesProcessor({
37296
37879
  outputRoot,
@@ -37345,7 +37928,7 @@ async function generateIgnoreCore(params) {
37345
37928
  let hasDiff = false;
37346
37929
  for (const toolTarget of (0, es_toolkit.intersection)(config.getTargets(), supportedIgnoreTargets)) {
37347
37930
  if (!config.getFeatures(toolTarget).includes("ignore")) continue;
37348
- for (const outputRoot of config.getOutputRoots()) try {
37931
+ for (const outputRoot of config.getOutputRoots(toolTarget)) try {
37349
37932
  const processor = new IgnoreProcessor({
37350
37933
  outputRoot,
37351
37934
  inputRoot: config.getInputRoot(),
@@ -37386,7 +37969,7 @@ async function generateMcpCore(params) {
37386
37969
  featureName: "mcp",
37387
37970
  logger
37388
37971
  });
37389
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
37972
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37390
37973
  if (!config.getFeatures(toolTarget).includes("mcp")) continue;
37391
37974
  const processor = new McpProcessor({
37392
37975
  outputRoot,
@@ -37428,7 +38011,7 @@ async function generateCommandsCore(params) {
37428
38011
  featureName: "commands",
37429
38012
  logger
37430
38013
  });
37431
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38014
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37432
38015
  if (!config.getFeatures(toolTarget).includes("commands")) continue;
37433
38016
  const processor = new CommandsProcessor({
37434
38017
  outputRoot,
@@ -37470,7 +38053,7 @@ async function generateSubagentsCore(params) {
37470
38053
  featureName: "subagents",
37471
38054
  logger
37472
38055
  });
37473
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38056
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37474
38057
  if (!config.getFeatures(toolTarget).includes("subagents")) continue;
37475
38058
  const processor = new SubagentsProcessor({
37476
38059
  outputRoot,
@@ -37513,7 +38096,7 @@ async function generateSkillsCore(params) {
37513
38096
  featureName: "skills",
37514
38097
  logger
37515
38098
  });
37516
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38099
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37517
38100
  if (!config.getFeatures(toolTarget).includes("skills")) continue;
37518
38101
  const processor = new SkillsProcessor({
37519
38102
  outputRoot,
@@ -37554,7 +38137,7 @@ async function generateHooksCore(params) {
37554
38137
  featureName: "hooks",
37555
38138
  logger
37556
38139
  });
37557
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of toolTargets) {
38140
+ for (const toolTarget of toolTargets) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37558
38141
  if (!config.getFeatures(toolTarget).includes("hooks")) continue;
37559
38142
  const processor = new HooksProcessor({
37560
38143
  outputRoot,
@@ -37591,7 +38174,7 @@ async function generatePermissionsCore(params) {
37591
38174
  let totalCount = 0;
37592
38175
  const allPaths = [];
37593
38176
  let hasDiff = false;
37594
- for (const outputRoot of config.getOutputRoots()) for (const toolTarget of (0, es_toolkit.intersection)(config.getTargets(), supportedPermissionsTargets)) {
38177
+ for (const toolTarget of (0, es_toolkit.intersection)(config.getTargets(), supportedPermissionsTargets)) for (const outputRoot of config.getOutputRoots(toolTarget)) {
37595
38178
  if (!config.getFeatures(toolTarget).includes("permissions")) continue;
37596
38179
  try {
37597
38180
  const processor = new PermissionsProcessor({
@@ -37933,12 +38516,6 @@ Object.defineProperty(exports, "CommandsProcessor", {
37933
38516
  return CommandsProcessor;
37934
38517
  }
37935
38518
  });
37936
- Object.defineProperty(exports, "CommandsProcessorToolTargetSchema", {
37937
- enumerable: true,
37938
- get: function() {
37939
- return CommandsProcessorToolTargetSchema;
37940
- }
37941
- });
37942
38519
  Object.defineProperty(exports, "ConfigResolver", {
37943
38520
  enumerable: true,
37944
38521
  get: function() {
@@ -37963,24 +38540,12 @@ Object.defineProperty(exports, "HooksProcessor", {
37963
38540
  return HooksProcessor;
37964
38541
  }
37965
38542
  });
37966
- Object.defineProperty(exports, "HooksProcessorToolTargetSchema", {
37967
- enumerable: true,
37968
- get: function() {
37969
- return HooksProcessorToolTargetSchema;
37970
- }
37971
- });
37972
38543
  Object.defineProperty(exports, "IgnoreProcessor", {
37973
38544
  enumerable: true,
37974
38545
  get: function() {
37975
38546
  return IgnoreProcessor;
37976
38547
  }
37977
38548
  });
37978
- Object.defineProperty(exports, "IgnoreProcessorToolTargetSchema", {
37979
- enumerable: true,
37980
- get: function() {
37981
- return IgnoreProcessorToolTargetSchema;
37982
- }
37983
- });
37984
38549
  Object.defineProperty(exports, "JsonLogger", {
37985
38550
  enumerable: true,
37986
38551
  get: function() {
@@ -37999,24 +38564,6 @@ Object.defineProperty(exports, "McpProcessor", {
37999
38564
  return McpProcessor;
38000
38565
  }
38001
38566
  });
38002
- Object.defineProperty(exports, "McpProcessorToolTargetSchema", {
38003
- enumerable: true,
38004
- get: function() {
38005
- return McpProcessorToolTargetSchema;
38006
- }
38007
- });
38008
- Object.defineProperty(exports, "PermissionsProcessor", {
38009
- enumerable: true,
38010
- get: function() {
38011
- return PermissionsProcessor;
38012
- }
38013
- });
38014
- Object.defineProperty(exports, "PermissionsProcessorToolTargetSchema", {
38015
- enumerable: true,
38016
- get: function() {
38017
- return PermissionsProcessorToolTargetSchema;
38018
- }
38019
- });
38020
38567
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
38021
38568
  enumerable: true,
38022
38569
  get: function() {
@@ -38149,12 +38696,6 @@ Object.defineProperty(exports, "RulesProcessor", {
38149
38696
  return RulesProcessor;
38150
38697
  }
38151
38698
  });
38152
- Object.defineProperty(exports, "RulesProcessorToolTargetSchema", {
38153
- enumerable: true,
38154
- get: function() {
38155
- return RulesProcessorToolTargetSchema;
38156
- }
38157
- });
38158
38699
  Object.defineProperty(exports, "RulesyncCommand", {
38159
38700
  enumerable: true,
38160
38701
  get: function() {
@@ -38239,24 +38780,12 @@ Object.defineProperty(exports, "SkillsProcessor", {
38239
38780
  return SkillsProcessor;
38240
38781
  }
38241
38782
  });
38242
- Object.defineProperty(exports, "SkillsProcessorToolTargetSchema", {
38243
- enumerable: true,
38244
- get: function() {
38245
- return SkillsProcessorToolTargetSchema;
38246
- }
38247
- });
38248
38783
  Object.defineProperty(exports, "SubagentsProcessor", {
38249
38784
  enumerable: true,
38250
38785
  get: function() {
38251
38786
  return SubagentsProcessor;
38252
38787
  }
38253
38788
  });
38254
- Object.defineProperty(exports, "SubagentsProcessorToolTargetSchema", {
38255
- enumerable: true,
38256
- get: function() {
38257
- return SubagentsProcessorToolTargetSchema;
38258
- }
38259
- });
38260
38789
  Object.defineProperty(exports, "ToolTargetSchema", {
38261
38790
  enumerable: true,
38262
38791
  get: function() {
@@ -38353,6 +38882,12 @@ Object.defineProperty(exports, "getLocalSkillDirNames", {
38353
38882
  return getLocalSkillDirNames;
38354
38883
  }
38355
38884
  });
38885
+ Object.defineProperty(exports, "getProcessorRegistryEntry", {
38886
+ enumerable: true,
38887
+ get: function() {
38888
+ return getProcessorRegistryEntry;
38889
+ }
38890
+ });
38356
38891
  Object.defineProperty(exports, "importFromTool", {
38357
38892
  enumerable: true,
38358
38893
  get: function() {
@@ -38407,54 +38942,6 @@ Object.defineProperty(exports, "toPosixPath", {
38407
38942
  return toPosixPath;
38408
38943
  }
38409
38944
  });
38410
- Object.defineProperty(exports, "toolCommandFactories", {
38411
- enumerable: true,
38412
- get: function() {
38413
- return toolCommandFactories;
38414
- }
38415
- });
38416
- Object.defineProperty(exports, "toolHooksFactories", {
38417
- enumerable: true,
38418
- get: function() {
38419
- return toolHooksFactories;
38420
- }
38421
- });
38422
- Object.defineProperty(exports, "toolIgnoreFactories", {
38423
- enumerable: true,
38424
- get: function() {
38425
- return toolIgnoreFactories;
38426
- }
38427
- });
38428
- Object.defineProperty(exports, "toolMcpFactories", {
38429
- enumerable: true,
38430
- get: function() {
38431
- return toolMcpFactories;
38432
- }
38433
- });
38434
- Object.defineProperty(exports, "toolPermissionsFactories", {
38435
- enumerable: true,
38436
- get: function() {
38437
- return toolPermissionsFactories;
38438
- }
38439
- });
38440
- Object.defineProperty(exports, "toolRuleFactories", {
38441
- enumerable: true,
38442
- get: function() {
38443
- return toolRuleFactories;
38444
- }
38445
- });
38446
- Object.defineProperty(exports, "toolSkillFactories", {
38447
- enumerable: true,
38448
- get: function() {
38449
- return toolSkillFactories;
38450
- }
38451
- });
38452
- Object.defineProperty(exports, "toolSubagentFactories", {
38453
- enumerable: true,
38454
- get: function() {
38455
- return toolSubagentFactories;
38456
- }
38457
- });
38458
38945
  Object.defineProperty(exports, "writeFileContent", {
38459
38946
  enumerable: true,
38460
38947
  get: function() {