rulesync 9.3.0 → 9.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -3919,6 +3919,297 @@ const OPENCODE_JSON_FILE_NAME = "opencode.json";
3919
3919
  const OPENCODE_RULE_FILE_NAME = "AGENTS.md";
3920
3920
  const OPENCODE_HOOKS_FILE_NAME = "rulesync-hooks.js";
3921
3921
  //#endregion
3922
+ //#region src/constants/takt-paths.ts
3923
+ const TAKT_DIR = ".takt";
3924
+ const TAKT_FACETS_SUBDIR = "facets";
3925
+ const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
3926
+ const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
3927
+ const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
3928
+ const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
3929
+ const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
3930
+ const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
3931
+ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
3932
+ /**
3933
+ * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
3934
+ * `~/.takt/config.yaml` (global); it holds the active provider, provider
3935
+ * profiles (including permission modes), and other Takt settings.
3936
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3937
+ */
3938
+ const TAKT_CONFIG_FILE_NAME = "config.yaml";
3939
+ /**
3940
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
3941
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
3942
+ * transport must be explicitly enabled here before any workflow-defined MCP
3943
+ * server using it is permitted to run.
3944
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
3945
+ */
3946
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
3947
+ //#endregion
3948
+ //#region src/utils/prototype-pollution.ts
3949
+ /**
3950
+ * Keys that, if walked into when constructing or merging objects from
3951
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
3952
+ * chain) and propagate state to every other object in the runtime. Any code
3953
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
3954
+ * parsing, MCP config conversion, settings round-trip — should skip these.
3955
+ */
3956
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
3957
+ "__proto__",
3958
+ "constructor",
3959
+ "prototype"
3960
+ ]);
3961
+ function isPrototypePollutionKey(key) {
3962
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
3963
+ }
3964
+ /**
3965
+ * Returns a shallow copy of a record's own entries with every
3966
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
3967
+ *
3968
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
3969
+ * or `headers` table — into freshly generated config. Carrying such a map by
3970
+ * reference, or re-assigning its keys via bracket notation, would let a literal
3971
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
3972
+ * prototype). Walking the entries through this helper severs that path while
3973
+ * preserving every legitimate key.
3974
+ */
3975
+ function omitPrototypePollutionKeys(record) {
3976
+ const sanitized = {};
3977
+ for (const [key, value] of Object.entries(record)) {
3978
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
3979
+ sanitized[key] = value;
3980
+ }
3981
+ return sanitized;
3982
+ }
3983
+ //#endregion
3984
+ //#region src/features/shared/shared-config-gateway.ts
3985
+ function sanitizeSharedConfigValue(value) {
3986
+ if (Array.isArray(value)) return value.map(sanitizeSharedConfigValue);
3987
+ if (!isPlainObject(value)) return value;
3988
+ const result = {};
3989
+ for (const [key, nested] of Object.entries(omitPrototypePollutionKeys(value))) result[key] = sanitizeSharedConfigValue(nested);
3990
+ return result;
3991
+ }
3992
+ /**
3993
+ * Parse a shared config file into a plain document: an empty/whitespace file
3994
+ * is `{}`, prototype-pollution keys are dropped recursively, and a non-mapping
3995
+ * root follows `invalidRootPolicy`. Syntax errors are wrapped with the file
3996
+ * path when one is given.
3997
+ */
3998
+ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy = "coerce-empty" }) {
3999
+ if (fileContent.trim() === "") return {};
4000
+ const at = filePath === void 0 ? "" : ` at ${filePath}`;
4001
+ let parsed;
4002
+ try {
4003
+ if (format === "yaml") parsed = (0, js_yaml.load)(fileContent);
4004
+ else if (format === "json") parsed = JSON.parse(fileContent);
4005
+ else parsed = (0, jsonc_parser.parse)(fileContent);
4006
+ } catch (error) {
4007
+ throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
4008
+ }
4009
+ if (parsed === void 0 || parsed === null) return {};
4010
+ if (!isPlainObject(parsed)) {
4011
+ if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
4012
+ return {};
4013
+ }
4014
+ return sanitizeSharedConfigValue(parsed);
4015
+ }
4016
+ /**
4017
+ * Serialize a shared config document. YAML output always ends with exactly one
4018
+ * newline; JSON output matches the 2-space `JSON.stringify` shape the JSON
4019
+ * writers have always emitted (no trailing newline).
4020
+ */
4021
+ function stringifySharedConfig({ format, document }) {
4022
+ if (format === "yaml") return (0, js_yaml.dump)(document, {
4023
+ noRefs: true,
4024
+ sortKeys: false
4025
+ }).trimEnd() + "\n";
4026
+ return JSON.stringify(document, null, 2);
4027
+ }
4028
+ /**
4029
+ * Shallow merge: every top-level key in `patch` replaces the base key
4030
+ * wholesale; all other base keys are preserved. The policy for a feature that
4031
+ * owns a fixed set of top-level keys.
4032
+ */
4033
+ function mergeSharedConfigShallow({ base, patch }) {
4034
+ return {
4035
+ ...base,
4036
+ ...sanitizeSharedConfigValue(patch)
4037
+ };
4038
+ }
4039
+ /**
4040
+ * Deep merge (`patch` wins): nested plain objects are merged key-by-key; every
4041
+ * other value (arrays, scalars) is replaced wholesale. The policy for a
4042
+ * feature whose contribution interleaves with user-authored siblings at any
4043
+ * depth (e.g. permissions overlays onto `approvals`/`security` structures, or
4044
+ * per-provider option tables) — nested sibling keys are preserved by
4045
+ * construction instead of by per-tool re-implementation. Prototype-pollution
4046
+ * keys are dropped.
4047
+ */
4048
+ function mergeSharedConfigDeep({ base, patch }) {
4049
+ const result = { ...base };
4050
+ for (const [key, patchValue] of Object.entries(patch)) {
4051
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4052
+ const baseValue = result[key];
4053
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = mergeSharedConfigDeep({
4054
+ base: baseValue,
4055
+ patch: patchValue
4056
+ });
4057
+ else result[key] = sanitizeSharedConfigValue(patchValue);
4058
+ }
4059
+ return result;
4060
+ }
4061
+ const CLAUDE_SETTINGS_SHARED_FILE_KEY = ".claude/settings.json";
4062
+ const HERMES_CONFIG_SHARED_FILE_KEY = ".hermes/config.yaml";
4063
+ const TAKT_CONFIG_SHARED_FILE_KEY = ".takt/config.yaml";
4064
+ /**
4065
+ * Who owns what in each gateway-managed shared config file, and which policy
4066
+ * resolves conflicts. Keys are `dir/file` tokens matching
4067
+ * `deriveSharedFileWriters()`; a test keeps each entry's feature set in
4068
+ * lock-step with the writers derived from the processor registry, so an
4069
+ * undeclared writer fails CI instead of merging by accident.
4070
+ */
4071
+ const SHARED_CONFIG_OWNERSHIP = {
4072
+ [CLAUDE_SETTINGS_SHARED_FILE_KEY]: {
4073
+ format: "json",
4074
+ features: {
4075
+ ignore: {
4076
+ kind: "custom",
4077
+ policyFunction: "applyIgnoreReadDenies"
4078
+ },
4079
+ hooks: {
4080
+ kind: "replace-owned-keys",
4081
+ ownedKeys: ["hooks"]
4082
+ },
4083
+ permissions: {
4084
+ kind: "custom",
4085
+ policyFunction: "applyPermissions"
4086
+ }
4087
+ }
4088
+ },
4089
+ [HERMES_CONFIG_SHARED_FILE_KEY]: {
4090
+ format: "yaml",
4091
+ features: {
4092
+ subagents: {
4093
+ kind: "replace-owned-keys",
4094
+ ownedKeys: ["plugins"]
4095
+ },
4096
+ mcp: {
4097
+ kind: "replace-owned-keys",
4098
+ ownedKeys: ["mcp_servers"]
4099
+ },
4100
+ hooks: {
4101
+ kind: "replace-owned-keys",
4102
+ ownedKeys: ["hooks"]
4103
+ },
4104
+ permissions: {
4105
+ kind: "deep-merge",
4106
+ replaceKeys: ["permissions"]
4107
+ }
4108
+ }
4109
+ },
4110
+ [TAKT_CONFIG_SHARED_FILE_KEY]: {
4111
+ format: "yaml",
4112
+ invalidRootPolicy: "error",
4113
+ features: {
4114
+ mcp: {
4115
+ kind: "replace-owned-keys",
4116
+ ownedKeys: [TAKT_WORKFLOW_MCP_SERVERS_KEY]
4117
+ },
4118
+ permissions: { kind: "deep-merge" }
4119
+ }
4120
+ }
4121
+ };
4122
+ /**
4123
+ * Execute a feature's declared write to a gateway-managed shared file: parse
4124
+ * the existing content, merge the patch under the feature's declared policy,
4125
+ * and serialize. Throws when the file or feature is undeclared, when a
4126
+ * `replace-owned-keys` patch strays outside its owned keys, or when the
4127
+ * feature's policy is `custom` (those calls go to the named policy function
4128
+ * instead).
4129
+ */
4130
+ function applySharedConfigPatch({ fileKey, feature, existingContent, patch, filePath }) {
4131
+ const declaration = SHARED_CONFIG_OWNERSHIP[fileKey];
4132
+ if (!declaration) throw new Error(`Shared config file '${fileKey}' has no SHARED_CONFIG_OWNERSHIP declaration; declare its writers and policies before writing it through the gateway.`);
4133
+ const policy = declaration.features[feature];
4134
+ if (!policy) throw new Error(`Feature '${feature}' declares no ownership of '${fileKey}'; add it to SHARED_CONFIG_OWNERSHIP before writing.`);
4135
+ if (policy.kind === "custom") throw new Error(`Feature '${feature}' writes '${fileKey}' through its dedicated policy function '${policy.policyFunction}' in shared-config-gateway.ts, not applySharedConfigPatch.`);
4136
+ const base = parseSharedConfig({
4137
+ format: declaration.format,
4138
+ fileContent: existingContent,
4139
+ filePath,
4140
+ ...declaration.invalidRootPolicy !== void 0 && { invalidRootPolicy: declaration.invalidRootPolicy }
4141
+ });
4142
+ if (policy.kind === "replace-owned-keys") {
4143
+ const unowned = Object.keys(patch).filter((key) => !policy.ownedKeys.includes(key));
4144
+ if (unowned.length > 0) throw new Error(`Feature '${feature}' tried to write undeclared keys [${unowned.join(", ")}] to '${fileKey}'; extend its ownedKeys declaration if that ownership is intended.`);
4145
+ return stringifySharedConfig({
4146
+ format: declaration.format,
4147
+ document: mergeSharedConfigShallow({
4148
+ base,
4149
+ patch
4150
+ })
4151
+ });
4152
+ }
4153
+ const merged = mergeSharedConfigDeep({
4154
+ base,
4155
+ patch
4156
+ });
4157
+ for (const key of policy.replaceKeys ?? []) if (patch[key] !== void 0) merged[key] = sanitizeSharedConfigValue(patch[key]);
4158
+ return stringifySharedConfig({
4159
+ format: declaration.format,
4160
+ document: merged
4161
+ });
4162
+ }
4163
+ const READ_TOOL_NAME = "Read";
4164
+ const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
4165
+ const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
4166
+ const parsePermissionsBlock = (settings) => {
4167
+ const permissions = settings.permissions ?? {};
4168
+ return {
4169
+ allow: permissions.allow ?? [],
4170
+ ask: permissions.ask ?? [],
4171
+ deny: permissions.deny ?? []
4172
+ };
4173
+ };
4174
+ const withPermissions = (settings, next) => {
4175
+ const permissions = { ...settings.permissions };
4176
+ const assign = (key, values) => {
4177
+ if (values.length > 0) permissions[key] = values;
4178
+ else delete permissions[key];
4179
+ };
4180
+ assign("allow", next.allow);
4181
+ assign("ask", next.ask);
4182
+ assign("deny", next.deny);
4183
+ return {
4184
+ ...settings,
4185
+ permissions
4186
+ };
4187
+ };
4188
+ const applyIgnoreReadDenies = (params) => {
4189
+ const { settings, readDenies } = params;
4190
+ const current = parsePermissionsBlock(settings);
4191
+ const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
4192
+ return withPermissions(settings, {
4193
+ allow: current.allow,
4194
+ ask: current.ask,
4195
+ deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
4196
+ });
4197
+ };
4198
+ const applyPermissions = (params) => {
4199
+ const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
4200
+ const current = parsePermissionsBlock(settings);
4201
+ const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
4202
+ if (logger && managedToolNames.has(READ_TOOL_NAME)) {
4203
+ const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
4204
+ if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
4205
+ }
4206
+ return withPermissions(settings, {
4207
+ allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
4208
+ ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
4209
+ deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
4210
+ });
4211
+ };
4212
+ //#endregion
3922
4213
  //#region src/features/opencode-config.ts
3923
4214
  /**
3924
4215
  * Reads and parses the OpenCode config (`opencode.jsonc` preferred, then
@@ -3943,9 +4234,10 @@ async function readOpencodeConfig({ outputRoot, global = false }) {
3943
4234
  });
3944
4235
  const fileContent = await readFileContentOrNull((0, node_path.join)(configDir, "opencode.jsonc")) ?? await readFileContentOrNull((0, node_path.join)(configDir, "opencode.json"));
3945
4236
  if (!fileContent) return {};
3946
- const parsed = (0, jsonc_parser.parse)(fileContent);
3947
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return {};
3948
- return parsed;
4237
+ return parseSharedConfig({
4238
+ format: "jsonc",
4239
+ fileContent
4240
+ });
3949
4241
  }
3950
4242
  /**
3951
4243
  * Narrows an unknown value to a plain record of entries keyed by name, as used
@@ -4854,32 +5146,6 @@ var RovodevPromptsManifest = class extends ToolFile {
4854
5146
  }
4855
5147
  };
4856
5148
  //#endregion
4857
- //#region src/constants/takt-paths.ts
4858
- const TAKT_DIR = ".takt";
4859
- const TAKT_FACETS_SUBDIR = "facets";
4860
- const TAKT_FACETS_DIR_PATH = (0, node_path.join)(TAKT_DIR, TAKT_FACETS_SUBDIR);
4861
- const TAKT_RULES_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "policies");
4862
- const TAKT_COMMANDS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "instructions");
4863
- const TAKT_SKILLS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "knowledge");
4864
- const TAKT_SUBAGENTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "personas");
4865
- const TAKT_OUTPUT_CONTRACTS_DIR_PATH = (0, node_path.join)(TAKT_FACETS_DIR_PATH, "output-contracts");
4866
- const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4867
- /**
4868
- * Takt's shared config file. Lives at `.takt/config.yaml` (project) and
4869
- * `~/.takt/config.yaml` (global); it holds the active provider, provider
4870
- * profiles (including permission modes), and other Takt settings.
4871
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4872
- */
4873
- const TAKT_CONFIG_FILE_NAME = "config.yaml";
4874
- /**
4875
- * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4876
- * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4877
- * transport must be explicitly enabled here before any workflow-defined MCP
4878
- * server using it is permitted to run.
4879
- * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4880
- */
4881
- const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4882
- //#endregion
4883
5149
  //#region src/features/takt-shared.ts
4884
5150
  /**
4885
5151
  * Shared utilities for all TAKT-* tool file classes.
@@ -6357,42 +6623,6 @@ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6357
6623
  */
6358
6624
  const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
6359
6625
  //#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
6626
  //#region src/features/hooks/tool-hooks-converter.ts
6397
6627
  function isToolMatcherEntry(x) {
6398
6628
  if (x === null || typeof x !== "object") return false;
@@ -7051,24 +7281,19 @@ var ClaudecodeHooks = class ClaudecodeHooks extends ToolHooks {
7051
7281
  const paths = ClaudecodeHooks.getSettablePaths({ global });
7052
7282
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7053
7283
  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
7284
  const config = rulesyncHooks.getJson();
7061
- const claudeHooks = canonicalToToolHooks({
7062
- config,
7063
- toolOverrideHooks: config.claudecode?.hooks,
7064
- converterConfig: CLAUDE_CONVERTER_CONFIG,
7065
- logger
7285
+ const fileContent = applySharedConfigPatch({
7286
+ fileKey: CLAUDE_SETTINGS_SHARED_FILE_KEY,
7287
+ feature: "hooks",
7288
+ existingContent,
7289
+ patch: { hooks: canonicalToToolHooks({
7290
+ config,
7291
+ toolOverrideHooks: config.claudecode?.hooks,
7292
+ converterConfig: CLAUDE_CONVERTER_CONFIG,
7293
+ logger
7294
+ }) },
7295
+ filePath
7066
7296
  });
7067
- const merged = {
7068
- ...settings,
7069
- hooks: claudeHooks
7070
- };
7071
- const fileContent = JSON.stringify(merged, null, 2);
7072
7297
  return new ClaudecodeHooks({
7073
7298
  outputRoot,
7074
7299
  relativeDirPath: paths.relativeDirPath,
@@ -8300,55 +8525,6 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8300
8525
  }
8301
8526
  };
8302
8527
  //#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
8528
  //#region src/features/hooks/hermesagent-hooks.ts
8353
8529
  /**
8354
8530
  * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
@@ -8476,10 +8652,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8476
8652
  return true;
8477
8653
  }
8478
8654
  setFileContent(fileContent) {
8479
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
8655
+ this.fileContent = applySharedConfigPatch({
8656
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
8657
+ feature: "hooks",
8658
+ existingContent: fileContent,
8659
+ patch: parseSharedConfig({
8660
+ format: "yaml",
8661
+ fileContent: this.fileContent
8662
+ })
8663
+ });
8480
8664
  }
8481
8665
  toRulesyncHooks() {
8482
- const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8666
+ const hooks = hermesHooksToCanonical(parseSharedConfig({
8667
+ format: "yaml",
8668
+ fileContent: this.getFileContent()
8669
+ }).hooks);
8483
8670
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8484
8671
  version: 1,
8485
8672
  hooks
@@ -8489,11 +8676,14 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
8489
8676
  const config = rulesyncHooks.getJson();
8490
8677
  return new HermesagentHooks({
8491
8678
  outputRoot,
8492
- fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8493
- config,
8494
- toolOverrideHooks: config.hermesagent?.hooks,
8495
- logger
8496
- }) })
8679
+ fileContent: stringifySharedConfig({
8680
+ format: "yaml",
8681
+ document: { hooks: canonicalToHermesHooks({
8682
+ config,
8683
+ toolOverrideHooks: config.hermesagent?.hooks,
8684
+ logger
8685
+ }) }
8686
+ })
8497
8687
  });
8498
8688
  }
8499
8689
  };
@@ -10513,66 +10703,6 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
10513
10703
  }
10514
10704
  };
10515
10705
  //#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
10706
  //#region src/features/ignore/claudecode-ignore.ts
10577
10707
  const DEFAULT_FILE_MODE = "shared";
10578
10708
  /**
@@ -11995,7 +12125,6 @@ var AntigravityIdeMcp = class extends AntigravityMcp {
11995
12125
  };
11996
12126
  //#endregion
11997
12127
  //#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
12128
  function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath) {
12000
12129
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
12001
12130
  let parsed;
@@ -12010,13 +12139,15 @@ function parseAugmentcodeSettings(fileContent, relativeDirPath, relativeFilePath
12010
12139
  /**
12011
12140
  * AugmentCode (Auggie CLI) MCP servers.
12012
12141
  *
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
12142
+ * MCP servers are persisted in the shared settings file `.augment/settings.json`
12143
+ * at either scope: the committed workspace file for team-shared servers (project)
12144
+ * or `~/.augment/settings.json` (global). That same file also holds `hooks` and
12016
12145
  * `toolPermissions`, so generation merges the `mcpServers` block into the
12017
- * existing settings instead of overwriting it, and the file is never deleted.
12146
+ * existing settings instead of overwriting it, and the file is never deleted. On
12147
+ * project-scope import the gitignored `.augment/settings.local.json` overrides
12148
+ * file is overlaid on top (the same layering the hooks/permissions adapters use).
12018
12149
  *
12019
- * @see https://docs.augmentcode.com/cli/integrations
12150
+ * @see https://docs.augmentcode.com/cli/config
12020
12151
  */
12021
12152
  var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12022
12153
  json;
@@ -12038,9 +12169,14 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12038
12169
  };
12039
12170
  }
12040
12171
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12041
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12042
12172
  const paths = this.getSettablePaths({ global });
12043
- const json = parseAugmentcodeSettings(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}", paths.relativeDirPath, paths.relativeFilePath);
12173
+ const json = parseAugmentcodeSettings(await readAugmentcodeSettingsWithLocalOverlay({
12174
+ outputRoot,
12175
+ relativeDirPath: paths.relativeDirPath,
12176
+ baseFileName: paths.relativeFilePath,
12177
+ baseFallbackContent: "{}",
12178
+ includeLocalOverlay: !global
12179
+ }), paths.relativeDirPath, paths.relativeFilePath);
12044
12180
  const newJson = {
12045
12181
  ...json,
12046
12182
  mcpServers: json.mcpServers ?? {}
@@ -12055,7 +12191,6 @@ var AugmentcodeMcp = class AugmentcodeMcp extends ToolMcp {
12055
12191
  });
12056
12192
  }
12057
12193
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12058
- if (!global) throw new Error(AUGMENTCODE_GLOBAL_ONLY_MESSAGE);
12059
12194
  const paths = this.getSettablePaths({ global });
12060
12195
  const merged = {
12061
12196
  ...parseAugmentcodeSettings(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({}, null, 2)), paths.relativeDirPath, paths.relativeFilePath),
@@ -13705,7 +13840,10 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13705
13840
  config;
13706
13841
  constructor(params) {
13707
13842
  super(params);
13708
- this.config = this.fileContent !== void 0 ? parseHermesConfig(this.fileContent) : {};
13843
+ this.config = this.fileContent !== void 0 ? parseSharedConfig({
13844
+ format: "yaml",
13845
+ fileContent: this.fileContent
13846
+ }) : {};
13709
13847
  }
13710
13848
  getConfig() {
13711
13849
  return this.config;
@@ -13714,9 +13852,17 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13714
13852
  return true;
13715
13853
  }
13716
13854
  setFileContent(fileContent) {
13717
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13855
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13856
+ format: "yaml",
13857
+ fileContent
13858
+ }), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
13718
13859
  this.config = merged;
13719
- super.setFileContent(stringifyHermesConfig(merged));
13860
+ super.setFileContent(applySharedConfigPatch({
13861
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13862
+ feature: "mcp",
13863
+ existingContent: fileContent,
13864
+ patch: { mcp_servers: merged.mcp_servers }
13865
+ }));
13720
13866
  }
13721
13867
  isDeletable() {
13722
13868
  return false;
@@ -13743,12 +13889,21 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
13743
13889
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13744
13890
  if (!global) throw new Error(HERMESAGENT_GLOBAL_ONLY_MESSAGE);
13745
13891
  const paths = this.getSettablePaths({ global });
13746
- const merged = mergeHermesMcpServers(parseHermesConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "")), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13892
+ const fileContent = await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), "");
13893
+ const merged = mergeHermesMcpServers(parseSharedConfig({
13894
+ format: "yaml",
13895
+ fileContent
13896
+ }), convertToHermesFormat(rulesyncMcp.getMcpServers()));
13747
13897
  return new HermesagentMcp({
13748
13898
  outputRoot,
13749
13899
  relativeDirPath: paths.relativeDirPath,
13750
13900
  relativeFilePath: paths.relativeFilePath,
13751
- fileContent: stringifyHermesConfig(merged),
13901
+ fileContent: applySharedConfigPatch({
13902
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
13903
+ feature: "mcp",
13904
+ existingContent: fileContent,
13905
+ patch: { mcp_servers: merged.mcp_servers }
13906
+ }),
13752
13907
  validate,
13753
13908
  global
13754
13909
  });
@@ -14913,28 +15068,6 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
14913
15068
  }
14914
15069
  };
14915
15070
  //#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
15071
  //#region src/features/mcp/takt-mcp.ts
14939
15072
  /**
14940
15073
  * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
@@ -15000,17 +15133,20 @@ var TaktMcp = class TaktMcp extends ToolMcp {
15000
15133
  }
15001
15134
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
15002
15135
  const paths = TaktMcp.getSettablePaths({ global });
15003
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
15136
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
15137
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
15004
15138
  const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
15005
- const merged = {
15006
- ...config,
15007
- [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
15008
- };
15009
15139
  return new TaktMcp({
15010
15140
  outputRoot,
15011
15141
  relativeDirPath: paths.relativeDirPath,
15012
15142
  relativeFilePath: paths.relativeFilePath,
15013
- fileContent: (0, js_yaml.dump)(merged),
15143
+ fileContent: applySharedConfigPatch({
15144
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
15145
+ feature: "mcp",
15146
+ existingContent,
15147
+ patch: { [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist },
15148
+ filePath
15149
+ }),
15014
15150
  validate,
15015
15151
  global
15016
15152
  });
@@ -15455,7 +15591,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
15455
15591
  ["augmentcode", {
15456
15592
  class: AugmentcodeMcp,
15457
15593
  meta: {
15458
- supportsProject: false,
15594
+ supportsProject: true,
15459
15595
  supportsGlobal: true,
15460
15596
  supportsEnabledTools: false,
15461
15597
  supportsDisabledTools: false
@@ -16037,15 +16173,97 @@ const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
16037
16173
  defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
16038
16174
  });
16039
16175
  /**
16176
+ * Tool-scoped override block for Takt. Takt's `config.yaml` carries two
16177
+ * permission surfaces the canonical coarse-mode mapping can't express:
16178
+ * `step_permission_overrides` — a per-workflow-step map (`<step>` →
16179
+ * `readonly`/`edit`/`full`) that lives inside the active provider profile
16180
+ * alongside `default_permission_mode` and layers on top of it at that step; and
16181
+ * `provider_options` — a top-level, per-provider table of sandbox/network knobs
16182
+ * orthogonal to the permission mode (e.g. `codex.network_access`,
16183
+ * `claude.sandbox.allow_unsandboxed_commands`, `opencode.allowed_tools`). Fields
16184
+ * placed here are merged into `config.yaml` and emitted only for Takt, while the
16185
+ * shared `permission` block continues to drive `default_permission_mode`. Kept
16186
+ * `looseObject` (verbatim passthrough); Takt validates its own value sets (e.g.
16187
+ * `provider_options.<p>.base_url` must be loopback). Both project and global
16188
+ * scope are supported.
16189
+ *
16190
+ * Note: Takt's config loader hard-rejects unknown top-level keys, so only keys
16191
+ * Takt actually recognizes belong here. `required_permission_mode` is NOT one —
16192
+ * it is a per-step field of the workflow YAML (not `config.yaml`), so it is out
16193
+ * of scope for this override.
16194
+ *
16195
+ * @example
16196
+ * { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
16197
+ */
16198
+ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
16199
+ step_permission_overrides: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
16200
+ provider_options: zod_mini.z.optional(zod_mini.z.looseObject({}))
16201
+ });
16202
+ /**
16203
+ * Tool-scoped override block for Amp. Amp's `amp.permissions` array and sibling
16204
+ * settings carry shapes the canonical per-command allow/ask/deny model can't
16205
+ * express, so they are authored here and merged into the shared Amp settings
16206
+ * file, while the shared `permission` block continues to drive the canonical
16207
+ * `amp.permissions` (allow/ask/reject) + `amp.tools.disable` entries:
16208
+ * - `permissions` — extra `amp.permissions` entries with non-`cmd` matchers
16209
+ * (`path`/`url`/`query`/…), regex/array match values, `context`
16210
+ * (`thread`/`subagent`), `delegate` (+`to`), or `reject` (+`message`). These
16211
+ * are appended AFTER the canonical-generated entries (Amp is first-match-wins,
16212
+ * so generated allow/ask/reject rules take precedence; authored entries act as
16213
+ * later fallbacks), preserving author order.
16214
+ * - `mcpPermissions` — Amp's `amp.mcpPermissions` array (`{ matches, action }`).
16215
+ * - `guardedFiles` — `amp.guardedFiles.allowlist` (globs allowed without
16216
+ * confirmation).
16217
+ * - `dangerouslyAllowAll` — `amp.dangerouslyAllowAll` (disable all confirmation).
16218
+ * Kept `looseObject` (verbatim passthrough). Both project and global scope are
16219
+ * supported.
16220
+ *
16221
+ * @example
16222
+ * { "dangerouslyAllowAll": false, "guardedFiles": { "allowlist": ["docs/**"] },
16223
+ * "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
16224
+ */
16225
+ const AmpPermissionsOverrideSchema = zod_mini.z.looseObject({
16226
+ permissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({
16227
+ tool: zod_mini.z.string(),
16228
+ action: zod_mini.z.string()
16229
+ }))),
16230
+ mcpPermissions: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.looseObject({}))),
16231
+ guardedFiles: zod_mini.z.optional(zod_mini.z.looseObject({ allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })),
16232
+ dangerouslyAllowAll: zod_mini.z.optional(zod_mini.z.boolean())
16233
+ });
16234
+ /**
16235
+ * Tool-scoped override block for the Google Antigravity CLI. Antigravity's CLI
16236
+ * `settings.json` carries two global autonomy/sandbox knobs outside the
16237
+ * `permissions.allow/ask/deny` arrays rulesync manages: `toolPermission` (the
16238
+ * global autonomy preset — `request-review` (default) / `proceed-in-sandbox` /
16239
+ * `always-proceed` / `strict`) and `enableTerminalSandbox` (a boolean confining
16240
+ * agent-run commands to OS containment). Antigravity applies the allow/deny
16241
+ * lists as per-rule exceptions to the preset at runtime, so rulesync only
16242
+ * authors these keys verbatim — no precedence modeling is needed on our side.
16243
+ * Fields placed here are merged onto the top level of
16244
+ * `~/.gemini/antigravity-cli/settings.json` (global-only) and emitted only for
16245
+ * the CLI. The Antigravity IDE exposes the same concepts through a GUI (no
16246
+ * documented JSON schema), so this override does NOT apply to `antigravity-ide`.
16247
+ * Verified against https://antigravity.google/docs/cli/reference and
16248
+ * https://antigravity.google/docs/cli/sandbox.
16249
+ *
16250
+ * @example
16251
+ * { "toolPermission": "strict", "enableTerminalSandbox": true }
16252
+ */
16253
+ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
16254
+ toolPermission: zod_mini.z.optional(zod_mini.z.string()),
16255
+ enableTerminalSandbox: zod_mini.z.optional(zod_mini.z.boolean())
16256
+ });
16257
+ /**
16040
16258
  * Permissions configuration.
16041
16259
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
16042
16260
  * Values are pattern-to-action mappings for that tool category.
16043
16261
  *
16044
16262
  * 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.
16263
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie`/`takt`/`amp`/
16264
+ * `antigravity-cli` keys are tool-scoped overrides consumed only by their
16265
+ * respective translator (see the matching `*PermissionsOverrideSchema`); every
16266
+ * other tool reads the shared `permission` block and ignores them.
16049
16267
  *
16050
16268
  * @example
16051
16269
  * {
@@ -16066,7 +16284,10 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
16066
16284
  reasonix: zod_mini.z.optional(ReasonixPermissionsOverrideSchema),
16067
16285
  factorydroid: zod_mini.z.optional(FactorydroidPermissionsOverrideSchema),
16068
16286
  warp: zod_mini.z.optional(WarpPermissionsOverrideSchema),
16069
- junie: zod_mini.z.optional(JuniePermissionsOverrideSchema)
16287
+ junie: zod_mini.z.optional(JuniePermissionsOverrideSchema),
16288
+ takt: zod_mini.z.optional(TaktPermissionsOverrideSchema),
16289
+ amp: zod_mini.z.optional(AmpPermissionsOverrideSchema),
16290
+ "antigravity-cli": zod_mini.z.optional(AntigravityCliPermissionsOverrideSchema)
16070
16291
  });
16071
16292
  /**
16072
16293
  * Full permissions file schema including optional $schema field.
@@ -16178,6 +16399,39 @@ const AMP_TOOLS_DISABLE_KEY = "amp.tools.disable";
16178
16399
  * Reference: https://ampcode.com/manual ("amp.permissions").
16179
16400
  */
16180
16401
  const AMP_PERMISSIONS_KEY = "amp.permissions";
16402
+ /**
16403
+ * The `amp.guardedFiles.allowlist` array (file globs allowed without
16404
+ * confirmation), `amp.dangerouslyAllowAll` boolean (disable all confirmation),
16405
+ * and `amp.mcpPermissions` array — sibling settings authored through the `amp`
16406
+ * permissions override. Reference: https://ampcode.com/manual.
16407
+ */
16408
+ const AMP_GUARDED_FILES_ALLOWLIST_KEY = "amp.guardedFiles.allowlist";
16409
+ const AMP_DANGEROUSLY_ALLOW_ALL_KEY = "amp.dangerouslyAllowAll";
16410
+ const AMP_MCP_PERMISSIONS_KEY = "amp.mcpPermissions";
16411
+ /**
16412
+ * The read-only `cmd` string of an entry's `matches`, or `undefined` when the
16413
+ * matcher is absent or uses a non-`cmd` key / non-string value.
16414
+ */
16415
+ function ampMatchesCmd(entry) {
16416
+ const cmd = entry.matches?.cmd;
16417
+ return typeof cmd === "string" ? cmd : void 0;
16418
+ }
16419
+ /**
16420
+ * Whether an `amp.permissions` entry is fully expressible in the canonical
16421
+ * per-command allow/ask/deny model: a plain `allow`/`ask`/`reject` action with
16422
+ * no `context`/`message`/`to` and a matcher that is either absent or exactly a
16423
+ * single string `cmd`. Everything else (non-`cmd` matchers, regex/array match
16424
+ * values, `delegate`, `reject`+`message`, `context`) is routed to the `amp`
16425
+ * override verbatim instead of being flattened with loss or dropped.
16426
+ */
16427
+ function isCanonicalAmpEntry(entry) {
16428
+ if (entry.action === "delegate") return false;
16429
+ if (entry.context !== void 0 || entry.message !== void 0 || entry.to !== void 0) return false;
16430
+ const matches = entry.matches;
16431
+ if (matches === void 0) return true;
16432
+ const keys = Object.keys(matches);
16433
+ return keys.length === 0 || keys.length === 1 && typeof matches.cmd === "string";
16434
+ }
16181
16435
  function parseAmpSettings(fileContent) {
16182
16436
  const errors = [];
16183
16437
  const parsed = (0, jsonc_parser.parse)(fileContent || "{}", errors, { allowTrailingComma: true });
@@ -16193,10 +16447,20 @@ function toDisableList(value) {
16193
16447
  return value.filter((entry) => typeof entry === "string");
16194
16448
  }
16195
16449
  /**
16450
+ * Read `amp.guardedFiles.allowlist` (an array of file glob strings) from parsed
16451
+ * settings, returning `undefined` when the key is absent or not an array so the
16452
+ * override omits it.
16453
+ */
16454
+ function extractAmpGuardedAllowlist(value) {
16455
+ if (!Array.isArray(value)) return void 0;
16456
+ return value.filter((entry) => typeof entry === "string");
16457
+ }
16458
+ /**
16196
16459
  * 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.
16460
+ * whose `tool`/`action` rulesync understands are retained; every other key
16461
+ * including the full `matches` object (so non-`cmd` matchers, regex/array
16462
+ * values, `context`, `to`, `message`) is kept verbatim so it survives a
16463
+ * round-trip.
16200
16464
  */
16201
16465
  function toPermissionsList(value) {
16202
16466
  if (!Array.isArray(value)) return [];
@@ -16206,14 +16470,11 @@ function toPermissionsList(value) {
16206
16470
  const { tool, action } = raw;
16207
16471
  if (typeof tool !== "string" || typeof action !== "string") continue;
16208
16472
  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
16473
  entries.push({
16213
16474
  ...raw,
16214
16475
  tool,
16215
16476
  action,
16216
- ...normalizedMatches ? { matches: normalizedMatches } : {}
16477
+ ...isPlainObject(raw.matches) ? { matches: raw.matches } : {}
16217
16478
  });
16218
16479
  }
16219
16480
  return entries;
@@ -16238,6 +16499,13 @@ function toPermissionsList(value) {
16238
16499
  * The settings file is shared with the MCP feature (`amp.mcpServers`), so reads
16239
16500
  * and writes merge into the existing JSON rather than overwriting it, and the
16240
16501
  * file is never deleted.
16502
+ *
16503
+ * Amp shapes with no canonical category are authored and round-tripped through
16504
+ * the `amp` permissions override (see `AmpPermissionsOverrideSchema`): extra
16505
+ * `amp.permissions` entries with non-`cmd` matchers / `context` / `delegate` /
16506
+ * `reject`+`message` (appended after the generated canonical entries), plus the
16507
+ * sibling `amp.mcpPermissions`, `amp.guardedFiles.allowlist`, and
16508
+ * `amp.dangerouslyAllowAll` settings.
16241
16509
  */
16242
16510
  var AmpPermissions = class AmpPermissions extends ToolPermissions {
16243
16511
  constructor(params) {
@@ -16307,15 +16575,25 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16307
16575
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
16308
16576
  const { fileContent, relativeFilePath } = await this.resolveSettingsFile(jsonDir);
16309
16577
  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");
16578
+ const config = rulesyncPermissions.getJson();
16579
+ const { disable, permissions } = convertRulesyncToAmp(config);
16580
+ const override = config.amp;
16581
+ const authoredExtras = override?.permissions ? toPermissionsList(override.permissions) : [];
16582
+ const preservedDelegates = override?.permissions ? [] : toPermissionsList(json[AMP_PERMISSIONS_KEY]).filter((entry) => entry.action === "delegate");
16312
16583
  const newJson = {
16313
16584
  ...json,
16314
16585
  [AMP_TOOLS_DISABLE_KEY]: disable
16315
16586
  };
16316
- const mergedPermissions = [...permissions, ...preservedDelegates];
16587
+ const mergedPermissions = mergeAmpPermissions([
16588
+ ...permissions,
16589
+ ...authoredExtras,
16590
+ ...preservedDelegates
16591
+ ]);
16317
16592
  if (mergedPermissions.length > 0) newJson[AMP_PERMISSIONS_KEY] = mergedPermissions;
16318
16593
  else delete newJson[AMP_PERMISSIONS_KEY];
16594
+ if (override?.guardedFiles?.allowlist !== void 0) newJson[AMP_GUARDED_FILES_ALLOWLIST_KEY] = override.guardedFiles.allowlist;
16595
+ if (override?.dangerouslyAllowAll !== void 0) newJson[AMP_DANGEROUSLY_ALLOW_ALL_KEY] = override.dangerouslyAllowAll;
16596
+ if (override?.mcpPermissions !== void 0) newJson[AMP_MCP_PERMISSIONS_KEY] = override.mcpPermissions;
16319
16597
  return new AmpPermissions({
16320
16598
  outputRoot,
16321
16599
  relativeDirPath: basePaths.relativeDirPath,
@@ -16326,11 +16604,22 @@ var AmpPermissions = class AmpPermissions extends ToolPermissions {
16326
16604
  }
16327
16605
  toRulesyncPermissions() {
16328
16606
  const json = parseAmpSettings(this.getFileContent());
16607
+ const allPermissions = toPermissionsList(json[AMP_PERMISSIONS_KEY]);
16608
+ const canonicalEntries = allPermissions.filter(isCanonicalAmpEntry);
16609
+ const overrideEntries = allPermissions.filter((entry) => !isCanonicalAmpEntry(entry));
16329
16610
  const config = convertAmpToRulesync({
16330
16611
  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) });
16612
+ permissions: canonicalEntries
16613
+ });
16614
+ const ampOverride = {};
16615
+ if (overrideEntries.length > 0) ampOverride.permissions = overrideEntries;
16616
+ if (Array.isArray(json[AMP_MCP_PERMISSIONS_KEY])) ampOverride.mcpPermissions = json[AMP_MCP_PERMISSIONS_KEY];
16617
+ const allowlist = extractAmpGuardedAllowlist(json[AMP_GUARDED_FILES_ALLOWLIST_KEY]);
16618
+ if (allowlist !== void 0) ampOverride.guardedFiles = { allowlist };
16619
+ if (typeof json[AMP_DANGEROUSLY_ALLOW_ALL_KEY] === "boolean") ampOverride.dangerouslyAllowAll = json[AMP_DANGEROUSLY_ALLOW_ALL_KEY];
16620
+ const result = { ...config };
16621
+ if (Object.keys(ampOverride).length > 0) result.amp = ampOverride;
16622
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16334
16623
  }
16335
16624
  validate() {
16336
16625
  try {
@@ -16377,6 +16666,37 @@ const ACTION_PRIORITY = {
16377
16666
  allow: 2
16378
16667
  };
16379
16668
  /**
16669
+ * Fail-closed action priority for merging the full `amp.permissions` list
16670
+ * (generated + authored/preserved). `reject` leads so no `allow` can shadow it
16671
+ * under first-match-wins; `delegate` (which defers to an external approver, so
16672
+ * it never auto-allows) trails as the final fallback.
16673
+ */
16674
+ const MERGE_ACTION_PRIORITY = {
16675
+ reject: 0,
16676
+ ask: 1,
16677
+ allow: 2,
16678
+ delegate: 3
16679
+ };
16680
+ /**
16681
+ * Stable fail-closed merge of generated and authored `amp.permissions` entries:
16682
+ * order by {@link MERGE_ACTION_PRIORITY} only, preserving each source's relative
16683
+ * order within an action class. Guarantees an authored `reject` cannot be
16684
+ * shadowed by a generated catch-all `allow`.
16685
+ */
16686
+ function mergeAmpPermissions(entries) {
16687
+ const decorated = entries.map((entry, index) => ({
16688
+ entry,
16689
+ index
16690
+ }));
16691
+ decorated.sort((a, b) => {
16692
+ const ap = MERGE_ACTION_PRIORITY[a.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16693
+ const bp = MERGE_ACTION_PRIORITY[b.entry.action] ?? MERGE_ACTION_PRIORITY.allow;
16694
+ if (ap !== bp) return ap - bp;
16695
+ return a.index - b.index;
16696
+ });
16697
+ return decorated.map((d) => d.entry);
16698
+ }
16699
+ /**
16380
16700
  * Convert a rulesync permissions config into Amp's two permission surfaces.
16381
16701
  *
16382
16702
  * For each `(category, pattern, action)` — where the category name IS the Amp
@@ -16441,14 +16761,14 @@ function sortAmpPermissions(entries) {
16441
16761
  const ap = ACTION_PRIORITY[a.entry.action] ?? 0;
16442
16762
  const bp = ACTION_PRIORITY[b.entry.action] ?? 0;
16443
16763
  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;
16764
+ const aHasCmd = ampMatchesCmd(a.entry) !== void 0 ? 0 : 1;
16765
+ const bHasCmd = ampMatchesCmd(b.entry) !== void 0 ? 0 : 1;
16446
16766
  if (aHasCmd !== bHasCmd) return aHasCmd - bHasCmd;
16447
16767
  const at = a.entry.tool;
16448
16768
  const bt = b.entry.tool;
16449
16769
  if (at !== bt) return at < bt ? -1 : 1;
16450
- const ac = a.entry.matches?.cmd ?? "";
16451
- const bc = b.entry.matches?.cmd ?? "";
16770
+ const ac = ampMatchesCmd(a.entry) ?? "";
16771
+ const bc = ampMatchesCmd(b.entry) ?? "";
16452
16772
  if (ac !== bc) return ac < bc ? -1 : 1;
16453
16773
  return a.index - b.index;
16454
16774
  });
@@ -16487,7 +16807,7 @@ function convertAmpToRulesync({ disable, permissions }) {
16487
16807
  for (const tool of disable) assign(tool, "*", "deny");
16488
16808
  for (const entry of permissions) {
16489
16809
  if (entry.action === "delegate") continue;
16490
- const pattern = entry.matches?.cmd ?? "*";
16810
+ const pattern = ampMatchesCmd(entry) ?? "*";
16491
16811
  const action = entry.action === "reject" ? "deny" : entry.action === "ask" ? "ask" : "allow";
16492
16812
  assign(entry.tool, pattern, action);
16493
16813
  }
@@ -16569,6 +16889,11 @@ function buildPermissionEntry$1(toolName, pattern) {
16569
16889
  * Permissions are written to the global `~/.gemini/antigravity-cli/settings.json`
16570
16890
  * file (global scope only). The file holds other CLI settings besides
16571
16891
  * permissions, so it is never deleted.
16892
+ *
16893
+ * Two CLI-only autonomy/sandbox knobs outside the allow/ask/deny arrays —
16894
+ * `toolPermission` (the global autonomy preset) and `enableTerminalSandbox` — are
16895
+ * authored and round-tripped through the `antigravity-cli` permissions override
16896
+ * (see `AntigravityCliPermissionsOverrideSchema`).
16572
16897
  */
16573
16898
  var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPermissions {
16574
16899
  constructor(params) {
@@ -16629,6 +16954,9 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16629
16954
  ...settings,
16630
16955
  permissions: mergedPermissions
16631
16956
  };
16957
+ const override = config["antigravity-cli"];
16958
+ if (override?.toolPermission !== void 0) merged.toolPermission = override.toolPermission;
16959
+ if (override?.enableTerminalSandbox !== void 0) merged.enableTerminalSandbox = override.enableTerminalSandbox;
16632
16960
  const fileContent = JSON.stringify(merged, null, 2);
16633
16961
  return new AntigravityCliPermissions({
16634
16962
  outputRoot,
@@ -16652,7 +16980,12 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
16652
16980
  ask: permissions.ask ?? [],
16653
16981
  deny: permissions.deny ?? []
16654
16982
  });
16655
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
16983
+ const override = {};
16984
+ if (typeof settings.toolPermission === "string") override.toolPermission = settings.toolPermission;
16985
+ if (typeof settings.enableTerminalSandbox === "boolean") override.enableTerminalSandbox = settings.enableTerminalSandbox;
16986
+ const result = { ...config };
16987
+ if (Object.keys(override).length > 0) result["antigravity-cli"] = override;
16988
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
16656
16989
  }
16657
16990
  validate() {
16658
16991
  return {
@@ -19338,14 +19671,21 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19338
19671
  return true;
19339
19672
  }
19340
19673
  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);
19674
+ this.fileContent = applySharedConfigPatch({
19675
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
19676
+ feature: "permissions",
19677
+ existingContent: fileContent,
19678
+ patch: parseSharedConfig({
19679
+ format: "yaml",
19680
+ fileContent: this.fileContent
19681
+ })
19682
+ });
19346
19683
  }
19347
19684
  toRulesyncPermissions() {
19348
- const config = parseHermesConfig(this.getFileContent());
19685
+ const config = parseSharedConfig({
19686
+ format: "yaml",
19687
+ fileContent: this.getFileContent()
19688
+ });
19349
19689
  const permissions = config.permissions && typeof config.permissions === "object" ? config.permissions.rulesync : {};
19350
19690
  return new RulesyncPermissions({
19351
19691
  relativeDirPath: "",
@@ -19366,11 +19706,17 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
19366
19706
  enabled: true,
19367
19707
  domains: webfetchDeny
19368
19708
  } };
19369
- if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
19709
+ if (permissions.hermes && typeof permissions.hermes === "object") config = mergeSharedConfigDeep({
19710
+ base: config,
19711
+ patch: permissions.hermes
19712
+ });
19370
19713
  config.permissions = { rulesync: permissions };
19371
19714
  return new HermesagentPermissions({
19372
19715
  outputRoot,
19373
- fileContent: stringifyHermesConfig(config)
19716
+ fileContent: stringifySharedConfig({
19717
+ format: "yaml",
19718
+ document: config
19719
+ })
19374
19720
  });
19375
19721
  }
19376
19722
  };
@@ -20926,6 +21272,8 @@ function isPermissionAction(value) {
20926
21272
  const TAKT_PROVIDER_KEY = "provider";
20927
21273
  const TAKT_PROVIDER_PROFILES_KEY = "provider_profiles";
20928
21274
  const TAKT_DEFAULT_PERMISSION_MODE_KEY = "default_permission_mode";
21275
+ const TAKT_STEP_PERMISSION_OVERRIDES_KEY = "step_permission_overrides";
21276
+ const TAKT_PROVIDER_OPTIONS_KEY = "provider_options";
20929
21277
  const TAKT_DEFAULT_PROVIDER = "claude";
20930
21278
  const CATCH_ALL_PATTERN = "*";
20931
21279
  /**
@@ -20955,10 +21303,16 @@ const CATCH_ALL_PATTERN = "*";
20955
21303
  * `bash: { "*": "deny" }`. These round-trip the generate mapping.
20956
21304
  *
20957
21305
  * 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.
21306
+ * place: `provider_profiles.<provider>.default_permission_mode` is set from the
21307
+ * derived mode; every other provider profile and all other top-level keys are
21308
+ * preserved. The file is never deleted.
21309
+ *
21310
+ * Two Takt-specific surfaces with no canonical category round-trip through the
21311
+ * `takt` override (see `TaktPermissionsOverrideSchema`):
21312
+ * `step_permission_overrides` (a per-step mode map inside the active provider
21313
+ * profile, layered on top of `default_permission_mode`) and `provider_options`
21314
+ * (a top-level per-provider sandbox/network table). Both are authored on
21315
+ * generate and re-extracted on import.
20962
21316
  */
20963
21317
  var TaktPermissions = class TaktPermissions extends ToolPermissions {
20964
21318
  constructor(params) {
@@ -20990,37 +21344,62 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
20990
21344
  }
20991
21345
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
20992
21346
  const paths = TaktPermissions.getSettablePaths({ global });
20993
- const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
21347
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
21348
+ const existingContent = await readFileContentOrNull(filePath) ?? "";
21349
+ const config = parseSharedConfig({
21350
+ format: "yaml",
21351
+ fileContent: existingContent,
21352
+ filePath,
21353
+ invalidRootPolicy: "error"
21354
+ });
21355
+ const rulesyncJson = rulesyncPermissions.getJson();
20994
21356
  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
- }
21357
+ const mode = deriveTaktPermissionMode(rulesyncJson);
21358
+ const override = isPlainObject(rulesyncJson.takt) ? rulesyncJson.takt : void 0;
21359
+ const stepOverrides = isPlainObject(override?.[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? override[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21360
+ const overrideProviderOptions = isPlainObject(override?.[TAKT_PROVIDER_OPTIONS_KEY]) ? override[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21361
+ const patch = {
21362
+ [TAKT_PROVIDER_PROFILES_KEY]: { [provider]: {
21363
+ [TAKT_DEFAULT_PERMISSION_MODE_KEY]: mode,
21364
+ ...stepOverrides !== void 0 && { [TAKT_STEP_PERMISSION_OVERRIDES_KEY]: stepOverrides }
21365
+ } },
21366
+ ...overrideProviderOptions !== void 0 && { [TAKT_PROVIDER_OPTIONS_KEY]: overrideProviderOptions }
21007
21367
  };
21008
21368
  return new TaktPermissions({
21009
21369
  outputRoot,
21010
21370
  relativeDirPath: paths.relativeDirPath,
21011
21371
  relativeFilePath: paths.relativeFilePath,
21012
- fileContent: (0, js_yaml.dump)(merged),
21372
+ fileContent: applySharedConfigPatch({
21373
+ fileKey: TAKT_CONFIG_SHARED_FILE_KEY,
21374
+ feature: "permissions",
21375
+ existingContent,
21376
+ patch,
21377
+ filePath
21378
+ }),
21013
21379
  validate: true,
21014
21380
  global
21015
21381
  });
21016
21382
  }
21017
21383
  toRulesyncPermissions() {
21018
- const config = parseTaktConfig(this.getFileContent(), this.getRelativeDirPath(), this.getRelativeFilePath());
21384
+ const config = parseSharedConfig({
21385
+ format: "yaml",
21386
+ fileContent: this.getFileContent(),
21387
+ filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
21388
+ invalidRootPolicy: "error"
21389
+ });
21019
21390
  const provider = resolveActiveProvider(config);
21020
21391
  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];
21392
+ const profile = isPlainObject(profiles[provider]) ? profiles[provider] : {};
21393
+ const mode = profile[TAKT_DEFAULT_PERMISSION_MODE_KEY];
21022
21394
  const rulesyncConfig = taktModeToRulesyncConfig(mode);
21023
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
21395
+ const stepOverrides = isPlainObject(profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY]) ? profile[TAKT_STEP_PERMISSION_OVERRIDES_KEY] : void 0;
21396
+ const providerOptions = isPlainObject(config[TAKT_PROVIDER_OPTIONS_KEY]) ? config[TAKT_PROVIDER_OPTIONS_KEY] : void 0;
21397
+ const taktOverride = {};
21398
+ if (stepOverrides && Object.keys(stepOverrides).length > 0) taktOverride[TAKT_STEP_PERMISSION_OVERRIDES_KEY] = stepOverrides;
21399
+ if (providerOptions && Object.keys(providerOptions).length > 0) taktOverride[TAKT_PROVIDER_OPTIONS_KEY] = providerOptions;
21400
+ const result = { ...rulesyncConfig };
21401
+ if (Object.keys(taktOverride).length > 0) result.takt = taktOverride;
21402
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
21024
21403
  }
21025
21404
  validate() {
21026
21405
  return {
@@ -29915,14 +30294,21 @@ def register(ctx):
29915
30294
  `;
29916
30295
  }
29917
30296
  function getEnabledPluginConfigContent(currentContent) {
29918
- const config = parseHermesConfig(currentContent);
30297
+ const config = parseSharedConfig({
30298
+ format: "yaml",
30299
+ fileContent: currentContent
30300
+ });
29919
30301
  const plugins = config.plugins && typeof config.plugins === "object" ? config.plugins : {};
29920
30302
  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);
30303
+ return applySharedConfigPatch({
30304
+ fileKey: HERMES_CONFIG_SHARED_FILE_KEY,
30305
+ feature: "subagents",
30306
+ existingContent: currentContent,
30307
+ patch: { plugins: {
30308
+ ...plugins,
30309
+ enabled: Array.from(/* @__PURE__ */ new Set([...enabled, "rulesync-subagents"]))
30310
+ } }
30311
+ });
29926
30312
  }
29927
30313
  function getSubagentSpec(rulesyncSubagent) {
29928
30314
  const json = rulesyncSubagent.getFrontmatter();
@@ -30008,6 +30394,17 @@ var HermesagentSubagent = class HermesagentSubagent extends ToolSubagent {
30008
30394
  static getSettablePaths() {
30009
30395
  return { relativeDirPath: HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH };
30010
30396
  }
30397
+ /**
30398
+ * Beyond the subagent spec files, generation also read-modify-writes the
30399
+ * shared `~/.hermes/config.yaml` (enabling the `rulesync-subagents` plugin),
30400
+ * so the write must be declared for the shared-file order derivation.
30401
+ */
30402
+ static getExtraSharedWritePaths() {
30403
+ return [{
30404
+ relativeDirPath: HERMESAGENT_GLOBAL_DIR,
30405
+ relativeFilePath: (0, node_path.basename)(HERMESAGENT_CONFIG_FILE_PATH)
30406
+ }];
30407
+ }
30011
30408
  static getSettablePathsForRulesyncSubagent(rulesyncSubagent) {
30012
30409
  return [(0, node_path.join)(HERMESAGENT_RULESYNC_SUBAGENTS_DIR_PATH, `${subagentSlug(rulesyncSubagent.getRelativePathFromCwd())}.json`)];
30013
30410
  }
@@ -31746,7 +32143,9 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
31746
32143
  })),
31747
32144
  kiro: zod_mini.z.optional(zod_mini.z.looseObject({
31748
32145
  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())]))
32146
+ fileMatchPattern: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
32147
+ name: zod_mini.z.optional(zod_mini.z.string()),
32148
+ description: zod_mini.z.optional(zod_mini.z.string())
31750
32149
  })),
31751
32150
  takt: zod_mini.z.optional(zod_mini.z.looseObject({
31752
32151
  name: zod_mini.z.optional(zod_mini.z.string()),
@@ -34662,7 +35061,8 @@ function toFileMatchPattern(globs) {
34662
35061
  *
34663
35062
  * Precedence:
34664
35063
  * 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`).
35064
+ * taken from the block, or derived from `globs` when omitted for `fileMatch`;
35065
+ * and with the companion `name`/`description` carried through for `auto`).
34666
35066
  * 2. Otherwise specific (non-wildcard) globs map to `fileMatch`, scoping the rule
34667
35067
  * to matching files instead of leaving it implicitly always-on.
34668
35068
  * 3. Otherwise the rule stays `always` — represented by omitting the block so the
@@ -34680,6 +35080,11 @@ function deriveKiroInclusion({ kiro, globs }) {
34680
35080
  fileMatchPattern
34681
35081
  } : { inclusion: "fileMatch" };
34682
35082
  }
35083
+ if (kiro.inclusion === "auto") return {
35084
+ inclusion: "auto",
35085
+ ...kiro.name !== void 0 ? { name: kiro.name } : {},
35086
+ ...kiro.description !== void 0 ? { description: kiro.description } : {}
35087
+ };
34683
35088
  return { inclusion: kiro.inclusion };
34684
35089
  }
34685
35090
  const fileMatchPattern = toFileMatchPattern(specificGlobs);
@@ -34751,6 +35156,8 @@ var KiroRule = class KiroRule extends ToolRule {
34751
35156
  const rawPattern = frontmatter.fileMatchPattern;
34752
35157
  const fileMatchPattern = Array.isArray(rawPattern) ? rawPattern.filter((p) => typeof p === "string") : typeof rawPattern === "string" ? rawPattern : void 0;
34753
35158
  const globs = inclusion === "fileMatch" ? fileMatchPattern === void 0 ? [] : Array.isArray(fileMatchPattern) ? fileMatchPattern : [fileMatchPattern] : [];
35159
+ const name = typeof frontmatter.name === "string" ? frontmatter.name : void 0;
35160
+ const description = typeof frontmatter.description === "string" ? frontmatter.description : void 0;
34754
35161
  return new RulesyncRule({
34755
35162
  outputRoot: process.cwd(),
34756
35163
  relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
@@ -34761,7 +35168,9 @@ var KiroRule = class KiroRule extends ToolRule {
34761
35168
  globs,
34762
35169
  kiro: {
34763
35170
  inclusion,
34764
- ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {}
35171
+ ...fileMatchPattern !== void 0 ? { fileMatchPattern } : {},
35172
+ ...inclusion === "auto" && name !== void 0 ? { name } : {},
35173
+ ...inclusion === "auto" && description !== void 0 ? { description } : {}
34765
35174
  }
34766
35175
  },
34767
35176
  body
@@ -36935,6 +37344,197 @@ function buildPermissionsStrategy(ctx) {
36935
37344
  };
36936
37345
  }
36937
37346
  //#endregion
37347
+ //#region src/types/processor-registry.ts
37348
+ const PROCESSOR_REGISTRY = [
37349
+ {
37350
+ feature: "rules",
37351
+ processor: RulesProcessor,
37352
+ schema: RulesProcessorToolTargetSchema,
37353
+ factory: toolRuleFactories
37354
+ },
37355
+ {
37356
+ feature: "ignore",
37357
+ processor: IgnoreProcessor,
37358
+ schema: IgnoreProcessorToolTargetSchema,
37359
+ factory: toolIgnoreFactories
37360
+ },
37361
+ {
37362
+ feature: "mcp",
37363
+ processor: McpProcessor,
37364
+ schema: McpProcessorToolTargetSchema,
37365
+ factory: toolMcpFactories
37366
+ },
37367
+ {
37368
+ feature: "commands",
37369
+ processor: CommandsProcessor,
37370
+ schema: CommandsProcessorToolTargetSchema,
37371
+ factory: toolCommandFactories
37372
+ },
37373
+ {
37374
+ feature: "subagents",
37375
+ processor: SubagentsProcessor,
37376
+ schema: SubagentsProcessorToolTargetSchema,
37377
+ factory: toolSubagentFactories
37378
+ },
37379
+ {
37380
+ feature: "skills",
37381
+ processor: SkillsProcessor,
37382
+ schema: SkillsProcessorToolTargetSchema,
37383
+ factory: toolSkillFactories
37384
+ },
37385
+ {
37386
+ feature: "hooks",
37387
+ processor: HooksProcessor,
37388
+ schema: HooksProcessorToolTargetSchema,
37389
+ factory: toolHooksFactories
37390
+ },
37391
+ {
37392
+ feature: "permissions",
37393
+ processor: PermissionsProcessor,
37394
+ schema: PermissionsProcessorToolTargetSchema,
37395
+ factory: toolPermissionsFactories
37396
+ }
37397
+ ];
37398
+ const getProcessorRegistryEntry = (feature) => {
37399
+ const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
37400
+ if (!entry) throw new Error(`No processor registered for feature: ${feature}`);
37401
+ return entry;
37402
+ };
37403
+ //#endregion
37404
+ //#region src/lib/shared-file-derive.ts
37405
+ /**
37406
+ * The single declaration of the cross-feature write order for shared
37407
+ * (read-modify-write) config files: when two features write the same on-disk
37408
+ * file, the earlier one writes first and the later one merges on top, so the
37409
+ * later feature's conflict policy decides what survives (e.g. `permissions`
37410
+ * overriding `ignore`-derived `Read(...)` denies in `.claude/settings.json`).
37411
+ * The generation step graph's `dependsOn` edges are derived from this list
37412
+ * plus the registry's `getSettablePaths` declarations — adding a tool or a
37413
+ * shared path never requires touching the graph by hand.
37414
+ */
37415
+ const SHARED_WRITE_FEATURE_ORDER = [
37416
+ "ignore",
37417
+ "subagents",
37418
+ "mcp",
37419
+ "hooks",
37420
+ "permissions",
37421
+ "rules"
37422
+ ];
37423
+ const SHARED_WRITE_FEATURES = new Set(SHARED_WRITE_FEATURE_ORDER);
37424
+ const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set(["augmentcode-legacy", "claudecode-legacy"]);
37425
+ const sharedFileKey = (path) => {
37426
+ const dir = path.relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "");
37427
+ const file = path.relativeFilePath.replace(/\\/g, "/");
37428
+ return dir === "" || dir === "." ? file : `${dir}/${file}`;
37429
+ };
37430
+ const settablePathsForScope = (cls, global) => {
37431
+ const paths = [];
37432
+ let settable;
37433
+ try {
37434
+ settable = cls.getSettablePaths?.({ global });
37435
+ } catch {
37436
+ return paths;
37437
+ }
37438
+ if (settable?.relativeFilePath) paths.push({
37439
+ relativeDirPath: settable.relativeDirPath ?? ".",
37440
+ relativeFilePath: settable.relativeFilePath
37441
+ });
37442
+ if (settable?.root) {
37443
+ paths.push(settable.root);
37444
+ for (const alt of settable.alternativeRoots ?? []) paths.push(alt);
37445
+ }
37446
+ for (const path of cls.getExtraSharedWritePaths?.({ global }) ?? []) if (path.relativeFilePath) paths.push(path);
37447
+ return paths;
37448
+ };
37449
+ const collectFactoryPaths = (factory) => [...settablePathsForScope(factory.class, false), ...settablePathsForScope(factory.class, true)];
37450
+ /**
37451
+ * Derive, from the processor registry, every on-disk file that two or more
37452
+ * features read-modify-write. Source of truth the generation step graph's
37453
+ * `writesSharedFile` declarations must match.
37454
+ */
37455
+ const deriveSharedFileWriters = () => {
37456
+ const byKey = /* @__PURE__ */ new Map();
37457
+ const pathByKey = /* @__PURE__ */ new Map();
37458
+ for (const entry of PROCESSOR_REGISTRY) {
37459
+ if (!SHARED_WRITE_FEATURES.has(entry.feature)) continue;
37460
+ const factories = entry.factory;
37461
+ for (const [tool, factory] of factories) {
37462
+ if (TARGETS_NOT_DERIVED.has(tool)) continue;
37463
+ for (const path of collectFactoryPaths(factory)) {
37464
+ const key = sharedFileKey(path);
37465
+ if (!pathByKey.has(key)) pathByKey.set(key, path);
37466
+ let features = byKey.get(key);
37467
+ if (!features) {
37468
+ features = /* @__PURE__ */ new Map();
37469
+ byKey.set(key, features);
37470
+ }
37471
+ let tools = features.get(entry.feature);
37472
+ if (!tools) {
37473
+ tools = /* @__PURE__ */ new Set();
37474
+ features.set(entry.feature, tools);
37475
+ }
37476
+ tools.add(tool);
37477
+ }
37478
+ }
37479
+ }
37480
+ const writers = [];
37481
+ for (const [key, features] of byKey) {
37482
+ if (features.size < 2) continue;
37483
+ const path = pathByKey.get(key);
37484
+ const toolsByFeature = /* @__PURE__ */ new Map();
37485
+ for (const [feature, tools] of features) toolsByFeature.set(feature, [...tools].toSorted());
37486
+ writers.push({
37487
+ key,
37488
+ relativeDirPath: path.relativeDirPath,
37489
+ relativeFilePath: path.relativeFilePath,
37490
+ features: [...features.keys()].toSorted(),
37491
+ toolsByFeature
37492
+ });
37493
+ }
37494
+ return writers.toSorted((a, b) => a.key.localeCompare(b.key));
37495
+ };
37496
+ /**
37497
+ * Derive, per shared-write feature, the shared files it writes and the
37498
+ * `dependsOn` edges that fix a safe write order: for every shared file, each
37499
+ * writer depends on all writers that precede it in
37500
+ * {@link SHARED_WRITE_FEATURE_ORDER}. This is the source the generation step
37501
+ * graph consumes, so registry changes (a new tool, a new settable path)
37502
+ * propagate into the execution order without a hand-maintained declaration.
37503
+ *
37504
+ * @throws Error if a feature writes a shared file but has no position in
37505
+ * `SHARED_WRITE_FEATURE_ORDER` — ordering it is a deliberate decision about
37506
+ * whose merge policy wins, so it must be made explicitly there.
37507
+ */
37508
+ const deriveSharedWriteSteps = () => {
37509
+ const orderIndex = new Map(SHARED_WRITE_FEATURE_ORDER.map((feature, index) => [feature, index]));
37510
+ const filesByFeature = /* @__PURE__ */ new Map();
37511
+ const depsByFeature = /* @__PURE__ */ new Map();
37512
+ for (const writer of deriveSharedFileWriters()) {
37513
+ for (const feature of writer.features) if (!orderIndex.has(feature)) throw new Error(`Feature '${feature}' writes the shared file '${writer.key}' but has no position in SHARED_WRITE_FEATURE_ORDER. Decide where its writes merge relative to the other features and add it to the order.`);
37514
+ const ordered = [...writer.features].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b));
37515
+ for (const [position, feature] of ordered.entries()) {
37516
+ let files = filesByFeature.get(feature);
37517
+ if (!files) {
37518
+ files = /* @__PURE__ */ new Set();
37519
+ filesByFeature.set(feature, files);
37520
+ }
37521
+ files.add(writer.key);
37522
+ let deps = depsByFeature.get(feature);
37523
+ if (!deps) {
37524
+ deps = /* @__PURE__ */ new Set();
37525
+ depsByFeature.set(feature, deps);
37526
+ }
37527
+ for (const earlier of ordered.slice(0, position)) deps.add(earlier);
37528
+ }
37529
+ }
37530
+ const steps = /* @__PURE__ */ new Map();
37531
+ for (const [feature, files] of filesByFeature) steps.set(feature, {
37532
+ writesSharedFile: [...files].toSorted(),
37533
+ dependsOn: [...depsByFeature.get(feature) ?? []].toSorted((a, b) => orderIndex.get(a) - orderIndex.get(b))
37534
+ });
37535
+ return steps;
37536
+ };
37537
+ //#endregion
36938
37538
  //#region src/lib/generate.ts
36939
37539
  async function processFeatureGeneration(params) {
36940
37540
  const { config, processor, toolFiles, skipFilePaths } = params;
@@ -37084,98 +37684,53 @@ function resolveExecutionOrder(steps) {
37084
37684
  if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
37085
37685
  return ordered;
37086
37686
  }
37687
+ const SHARED_WRITE_STEPS = deriveSharedWriteSteps();
37688
+ const sharedWriteMeta = (id) => {
37689
+ const step = SHARED_WRITE_STEPS.get(id);
37690
+ return step ? {
37691
+ writesSharedFile: step.writesSharedFile,
37692
+ dependsOn: step.dependsOn
37693
+ } : {};
37694
+ };
37087
37695
  /**
37088
37696
  * The static shape of the generation step graph: which steps write which shared
37089
37697
  * (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.
37698
+ * for those writers. Both are derived from the processor registry's settable
37699
+ * paths and `SHARED_WRITE_FEATURE_ORDER` (see `shared-file-derive.ts`), so a new
37700
+ * tool or shared path never requires editing this graph. Exported (separately
37701
+ * from the `run` closures, which need a live `config`/`logger`) so
37702
+ * `resolveExecutionOrder`'s ordering guarantee can be tested directly against
37703
+ * the real graph rather than a hand-copied one. Readonly so a consumer can't
37704
+ * mutate this module-level singleton and affect every subsequent `generate()`
37705
+ * call in the process.
37095
37706
  */
37096
37707
  const GENERATION_STEP_GRAPH = [
37097
37708
  {
37098
37709
  id: "ignore",
37099
- writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
37710
+ ...sharedWriteMeta("ignore")
37100
37711
  },
37101
37712
  {
37102
37713
  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"]
37714
+ ...sharedWriteMeta("mcp")
37124
37715
  },
37125
37716
  { id: "commands" },
37126
- { id: "subagents" },
37717
+ {
37718
+ id: "subagents",
37719
+ ...sharedWriteMeta("subagents")
37720
+ },
37127
37721
  { id: "skills" },
37128
37722
  {
37129
37723
  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"]
37724
+ ...sharedWriteMeta("hooks")
37141
37725
  },
37142
37726
  {
37143
37727
  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
- ]
37728
+ ...sharedWriteMeta("permissions")
37170
37729
  },
37171
37730
  {
37172
37731
  id: "rules",
37173
- writesSharedFile: ["kilo.json", "opencode.json"],
37174
- dependsOn: [
37175
- "mcp",
37176
- "skills",
37177
- "permissions"
37178
- ]
37732
+ ...sharedWriteMeta("rules"),
37733
+ dependsOn: [...sharedWriteMeta("rules").dependsOn ?? [], "skills"]
37179
37734
  }
37180
37735
  ];
37181
37736
  /**
@@ -37933,12 +38488,6 @@ Object.defineProperty(exports, "CommandsProcessor", {
37933
38488
  return CommandsProcessor;
37934
38489
  }
37935
38490
  });
37936
- Object.defineProperty(exports, "CommandsProcessorToolTargetSchema", {
37937
- enumerable: true,
37938
- get: function() {
37939
- return CommandsProcessorToolTargetSchema;
37940
- }
37941
- });
37942
38491
  Object.defineProperty(exports, "ConfigResolver", {
37943
38492
  enumerable: true,
37944
38493
  get: function() {
@@ -37963,24 +38512,12 @@ Object.defineProperty(exports, "HooksProcessor", {
37963
38512
  return HooksProcessor;
37964
38513
  }
37965
38514
  });
37966
- Object.defineProperty(exports, "HooksProcessorToolTargetSchema", {
37967
- enumerable: true,
37968
- get: function() {
37969
- return HooksProcessorToolTargetSchema;
37970
- }
37971
- });
37972
38515
  Object.defineProperty(exports, "IgnoreProcessor", {
37973
38516
  enumerable: true,
37974
38517
  get: function() {
37975
38518
  return IgnoreProcessor;
37976
38519
  }
37977
38520
  });
37978
- Object.defineProperty(exports, "IgnoreProcessorToolTargetSchema", {
37979
- enumerable: true,
37980
- get: function() {
37981
- return IgnoreProcessorToolTargetSchema;
37982
- }
37983
- });
37984
38521
  Object.defineProperty(exports, "JsonLogger", {
37985
38522
  enumerable: true,
37986
38523
  get: function() {
@@ -37999,24 +38536,6 @@ Object.defineProperty(exports, "McpProcessor", {
37999
38536
  return McpProcessor;
38000
38537
  }
38001
38538
  });
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
38539
  Object.defineProperty(exports, "RULESYNC_AIIGNORE_FILE_NAME", {
38021
38540
  enumerable: true,
38022
38541
  get: function() {
@@ -38149,12 +38668,6 @@ Object.defineProperty(exports, "RulesProcessor", {
38149
38668
  return RulesProcessor;
38150
38669
  }
38151
38670
  });
38152
- Object.defineProperty(exports, "RulesProcessorToolTargetSchema", {
38153
- enumerable: true,
38154
- get: function() {
38155
- return RulesProcessorToolTargetSchema;
38156
- }
38157
- });
38158
38671
  Object.defineProperty(exports, "RulesyncCommand", {
38159
38672
  enumerable: true,
38160
38673
  get: function() {
@@ -38239,24 +38752,12 @@ Object.defineProperty(exports, "SkillsProcessor", {
38239
38752
  return SkillsProcessor;
38240
38753
  }
38241
38754
  });
38242
- Object.defineProperty(exports, "SkillsProcessorToolTargetSchema", {
38243
- enumerable: true,
38244
- get: function() {
38245
- return SkillsProcessorToolTargetSchema;
38246
- }
38247
- });
38248
38755
  Object.defineProperty(exports, "SubagentsProcessor", {
38249
38756
  enumerable: true,
38250
38757
  get: function() {
38251
38758
  return SubagentsProcessor;
38252
38759
  }
38253
38760
  });
38254
- Object.defineProperty(exports, "SubagentsProcessorToolTargetSchema", {
38255
- enumerable: true,
38256
- get: function() {
38257
- return SubagentsProcessorToolTargetSchema;
38258
- }
38259
- });
38260
38761
  Object.defineProperty(exports, "ToolTargetSchema", {
38261
38762
  enumerable: true,
38262
38763
  get: function() {
@@ -38353,6 +38854,12 @@ Object.defineProperty(exports, "getLocalSkillDirNames", {
38353
38854
  return getLocalSkillDirNames;
38354
38855
  }
38355
38856
  });
38857
+ Object.defineProperty(exports, "getProcessorRegistryEntry", {
38858
+ enumerable: true,
38859
+ get: function() {
38860
+ return getProcessorRegistryEntry;
38861
+ }
38862
+ });
38356
38863
  Object.defineProperty(exports, "importFromTool", {
38357
38864
  enumerable: true,
38358
38865
  get: function() {
@@ -38407,54 +38914,6 @@ Object.defineProperty(exports, "toPosixPath", {
38407
38914
  return toPosixPath;
38408
38915
  }
38409
38916
  });
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
38917
  Object.defineProperty(exports, "writeFileContent", {
38459
38918
  enumerable: true,
38460
38919
  get: function() {