rulesync 9.0.2 → 9.1.1

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$238 } = node_path.posix;
117
+ const { join: join$248 } = 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$238(RULESYNC_RELATIVE_DIR_PATH, "rules");
122
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, "commands");
123
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, "subagents");
124
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
125
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
126
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
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");
127
127
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
128
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$238(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
128
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$248(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$238(RULESYNC_RELATIVE_DIR_PATH, "skills");
132
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$238(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
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");
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";
@@ -224,6 +224,7 @@ const mcpProcessorToolTargetTuple = [
224
224
  "reasonix",
225
225
  "roo",
226
226
  "rovodev",
227
+ "takt",
227
228
  "vibe",
228
229
  "warp",
229
230
  "devin",
@@ -231,6 +232,7 @@ const mcpProcessorToolTargetTuple = [
231
232
  ];
232
233
  const commandsProcessorToolTargetTuple = [
233
234
  "agentsmd",
235
+ "antigravity-cli",
234
236
  "antigravity-ide",
235
237
  "augmentcode",
236
238
  "claudecode",
@@ -250,7 +252,9 @@ const commandsProcessorToolTargetTuple = [
250
252
  "opencode",
251
253
  "pi",
252
254
  "qwencode",
255
+ "reasonix",
253
256
  "roo",
257
+ "rovodev",
254
258
  "takt",
255
259
  "devin"
256
260
  ];
@@ -335,11 +339,13 @@ const hooksProcessorToolTargetTuple = [
335
339
  "deepagents",
336
340
  "kiro",
337
341
  "kiro-cli",
342
+ "kiro-ide",
338
343
  "devin",
339
344
  "augmentcode",
340
345
  "junie",
341
346
  "vibe",
342
- "qwencode"
347
+ "qwencode",
348
+ "reasonix"
343
349
  ];
344
350
  const permissionsProcessorToolTargetTuple = [
345
351
  "amp",
@@ -350,16 +356,19 @@ const permissionsProcessorToolTargetTuple = [
350
356
  "cline",
351
357
  "codexcli",
352
358
  "cursor",
359
+ "devin",
353
360
  "factorydroid",
354
361
  "goose",
355
362
  "grokcli",
356
363
  "hermesagent",
364
+ "junie",
357
365
  "kilo",
358
366
  "kiro",
359
367
  "kiro-cli",
360
368
  "kiro-ide",
361
369
  "opencode",
362
370
  "qwencode",
371
+ "reasonix",
363
372
  "rovodev",
364
373
  "takt",
365
374
  "vibe",
@@ -1399,7 +1408,7 @@ function extractConfigFileTargets(targets) {
1399
1408
  * Type guard to check if a value is a plain object (Record<string, unknown>).
1400
1409
  * This excludes arrays and null values.
1401
1410
  */
1402
- function isRecord$1(value) {
1411
+ function isRecord(value) {
1403
1412
  return typeof value === "object" && value !== null && !Array.isArray(value);
1404
1413
  }
1405
1414
  /**
@@ -1414,7 +1423,7 @@ function isRecord$1(value) {
1414
1423
  * malicious accessor descriptors.
1415
1424
  */
1416
1425
  function isPlainObject(value) {
1417
- if (!isRecord$1(value)) return false;
1426
+ if (!isRecord(value)) return false;
1418
1427
  const proto = Object.getPrototypeOf(value);
1419
1428
  return proto === null || proto === Object.prototype;
1420
1429
  }
@@ -1765,6 +1774,17 @@ var ToolCommand = class extends AiFile {
1765
1774
  throw new Error("Please implement this method in the subclass.");
1766
1775
  }
1767
1776
  /**
1777
+ * Optional hook for tools whose commands are not purely one-file-per-command
1778
+ * (e.g. Rovo Dev's `prompts.yml` manifest that indexes every saved-prompt
1779
+ * content file). Given the full set of ToolCommand instances just generated
1780
+ * for this target, returns any additional shared/aggregate files that must
1781
+ * be written alongside them. Most tools don't need this and inherit the
1782
+ * empty default (mirrors `ToolHooks.getAuxiliaryFiles`).
1783
+ */
1784
+ static async getAuxiliaryFiles(_params) {
1785
+ return [];
1786
+ }
1787
+ /**
1768
1788
  * Convert a RulesyncCommand to the tool-specific command format.
1769
1789
  *
1770
1790
  * This method should:
@@ -1923,16 +1943,25 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
1923
1943
  }
1924
1944
  };
1925
1945
  //#endregion
1926
- //#region src/constants/antigravity-ide-paths.ts
1927
- const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
1928
- const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
1929
- const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
1930
- const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
1931
- const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
1932
- const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
1933
- const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
1934
- const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
1935
- const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
1946
+ //#region src/constants/antigravity-paths.ts
1947
+ const ANTIGRAVITY_DIR = ".agents";
1948
+ const ANTIGRAVITY_SKILLS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "skills");
1949
+ const ANTIGRAVITY_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "workflows");
1950
+ const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
1951
+ const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
1952
+ const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
1953
+ const ANTIGRAVITY_GEMINI_DIR = ".gemini";
1954
+ const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
1955
+ const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
1956
+ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
1957
+ const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
1958
+ const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
1959
+ const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
1960
+ //#endregion
1961
+ //#region src/constants/antigravity-cli-paths.ts
1962
+ const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
1963
+ const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
1964
+ const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
1936
1965
  //#endregion
1937
1966
  //#region src/features/commands/antigravity-command.ts
1938
1967
  const AntigravityWorkflowFrontmatterSchema = zod_mini.z.looseObject({
@@ -2106,7 +2135,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
2106
2135
  }
2107
2136
  static extractAntigravityConfig(rulesyncCommand) {
2108
2137
  const antigravity = rulesyncCommand.getFrontmatter().antigravity;
2109
- return isRecord$1(antigravity) ? antigravity : void 0;
2138
+ return isRecord(antigravity) ? antigravity : void 0;
2110
2139
  }
2111
2140
  static resolveTrigger(rulesyncCommand, antigravityConfig) {
2112
2141
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
@@ -2188,6 +2217,45 @@ var AntigravitySharedCommand = class extends ToolCommand {
2188
2217
  }
2189
2218
  };
2190
2219
  //#endregion
2220
+ //#region src/features/commands/antigravity-cli-command.ts
2221
+ /**
2222
+ * Command (workflow) generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
2223
+ *
2224
+ * Shares all body and frontmatter handling with {@link AntigravitySharedCommand};
2225
+ * the CLI reads project workflows from the same `.agents/workflows/` directory as
2226
+ * the IDE (the shared Antigravity 2.0 harness), but keeps its own global
2227
+ * workflows tree at `~/.gemini/antigravity-cli/global_workflows/` (mirroring the
2228
+ * CLI's global skills tree). It answers to the `antigravity-cli` target.
2229
+ */
2230
+ var AntigravityCliCommand = class extends AntigravitySharedCommand {
2231
+ static getProjectRelativeDirPath() {
2232
+ return ANTIGRAVITY_WORKFLOWS_DIR_PATH;
2233
+ }
2234
+ static getGlobalRelativeDirPath() {
2235
+ return ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH;
2236
+ }
2237
+ getToolTargetName() {
2238
+ return "antigravity-cli";
2239
+ }
2240
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
2241
+ return this.isTargetedByRulesyncCommandDefault({
2242
+ rulesyncCommand,
2243
+ toolTarget: "antigravity-cli"
2244
+ });
2245
+ }
2246
+ };
2247
+ //#endregion
2248
+ //#region src/constants/antigravity-ide-paths.ts
2249
+ const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
2250
+ const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
2251
+ const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
2252
+ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
2253
+ const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
2254
+ const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
2255
+ const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
2256
+ const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
2257
+ const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
2258
+ //#endregion
2191
2259
  //#region src/features/commands/antigravity-ide-command.ts
2192
2260
  /**
2193
2261
  * Command (workflow) generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -2572,6 +2640,16 @@ const CodexcliCommandFrontmatterSchema = zod_mini.z.looseObject({
2572
2640
  description: zod_mini.z.optional(zod_mini.z.string()),
2573
2641
  "argument-hint": zod_mini.z.optional(zod_mini.z.string())
2574
2642
  });
2643
+ /**
2644
+ * Generates Codex CLI's global-only custom prompts under `~/.codex/prompts/*.md`.
2645
+ *
2646
+ * Note: upstream Codex docs now state "Custom prompts are deprecated. Use skills for
2647
+ * reusable instructions" (https://developers.openai.com/codex/custom-prompts). No removal
2648
+ * date has been announced and custom prompts remain functional, so this class's
2649
+ * generation behavior is unchanged. Prefer rulesync's `codexcli` skills support
2650
+ * (see `src/features/skills/codexcli-skill.ts`) for new reusable instructions going
2651
+ * forward; this class is kept for users who still rely on custom prompts.
2652
+ */
2575
2653
  var CodexcliCommand = class CodexcliCommand extends ToolCommand {
2576
2654
  frontmatter;
2577
2655
  body;
@@ -2695,7 +2773,7 @@ const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
2695
2773
  const COPILOTCLI_AGENTS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "agents");
2696
2774
  const COPILOTCLI_HOOKS_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "hooks");
2697
2775
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
2698
- const COPILOTCLI_SKILLS_GLOBAL_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "skills");
2776
+ const COPILOT_SKILLS_GLOBAL_DIR_PATH = (0, node_path.join)(COPILOT_DIR, "skills");
2699
2777
  //#endregion
2700
2778
  //#region src/features/commands/copilot-command.ts
2701
2779
  const CopilotCommandFrontmatterSchema = zod_mini.z.looseObject({
@@ -2945,20 +3023,17 @@ var CursorCommand = class CursorCommand extends ToolCommand {
2945
3023
  //#endregion
2946
3024
  //#region src/constants/devin-paths.ts
2947
3025
  const DEVIN_DIR = ".devin";
2948
- const WINDSURF_DIR = ".windsurf";
2949
- const CODEIUM_DIR = ".codeium";
2950
- const WINDSURF_SUBDIR = "windsurf";
2951
- const CODEIUM_WINDSURF_DIR = (0, node_path.join)(CODEIUM_DIR, WINDSURF_SUBDIR);
2952
- const WINDSURF_MEMORIES_SUBDIR = (0, node_path.join)(WINDSURF_SUBDIR, "memories");
3026
+ const CODEIUM_WINDSURF_DIR = (0, node_path.join)(".codeium", "windsurf");
2953
3027
  const DEVIN_WORKFLOWS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "workflows");
2954
3028
  const DEVIN_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "skills");
2955
3029
  const DEVIN_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "agents");
2956
- const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(".config", "devin", "agents");
3030
+ const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
3031
+ const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
2957
3032
  const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "global_workflows");
2958
3033
  const CODEIUM_WINDSURF_SKILLS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "skills");
2959
- const DEVIN_MCP_FILE_NAME = "mcp_config.json";
2960
- const DEVIN_HOOKS_FILE_NAME = "hooks.json";
2961
- const DEVIN_GLOBAL_RULES_FILE_NAME = "global_rules.md";
3034
+ const DEVIN_CONFIG_FILE_NAME = "config.json";
3035
+ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
3036
+ const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
2962
3037
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
2963
3038
  const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
2964
3039
  //#endregion
@@ -3207,6 +3282,8 @@ const GOOSE_MCP_FILE_NAME = "config.yaml";
3207
3282
  const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
3208
3283
  const GOOSE_HOOKS_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync", "hooks");
3209
3284
  const GOOSE_HOOKS_FILE_NAME = "hooks.json";
3285
+ const GOOSE_PLUGIN_MCP_DIR_PATH = (0, node_path.join)(".agents", "plugins", "rulesync");
3286
+ const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
3210
3287
  const GOOSE_SKILLS_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "skills");
3211
3288
  const GOOSE_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_DIR, "recipes");
3212
3289
  const GOOSE_GLOBAL_RECIPES_DIR_PATH = (0, node_path.join)(GOOSE_GLOBAL_DIR, "recipes");
@@ -3465,6 +3542,7 @@ const JUNIE_ALT_AGENTS_DIR_PATH = ".agents";
3465
3542
  const JUNIE_MCP_DIR_PATH = (0, node_path.join)(JUNIE_DIR, "mcp");
3466
3543
  const JUNIE_MCP_FILE_NAME = "mcp.json";
3467
3544
  const JUNIE_HOOKS_FILE_NAME = "config.json";
3545
+ const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
3468
3546
  const JUNIE_IGNORE_FILE_NAME = ".aiignore";
3469
3547
  const JUNIE_RULE_FILE_NAME = "AGENTS.md";
3470
3548
  const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
@@ -3717,6 +3795,15 @@ const KIRO_SKILLS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "skills");
3717
3795
  const KIRO_SETTINGS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "settings");
3718
3796
  const KIRO_AGENTS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "agents");
3719
3797
  const KIRO_HOOKS_FILE_NAME = "default.json";
3798
+ /**
3799
+ * Kiro IDE 1.0 stores hooks as structured JSON files in `.kiro/hooks/`
3800
+ * (workspace) and `~/.kiro/hooks/` (user). A single file may declare multiple
3801
+ * hooks in its `hooks` array, so rulesync emits all generated hooks into one
3802
+ * `rulesync.json` file per scope.
3803
+ * @see https://kiro.dev/docs/hooks/
3804
+ */
3805
+ const KIRO_IDE_HOOKS_DIR_PATH = (0, node_path.join)(KIRO_DIR, "hooks");
3806
+ const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
3720
3807
  const KIRO_MCP_FILE_NAME = "mcp.json";
3721
3808
  const KIRO_IGNORE_FILE_NAME = ".kiroignore";
3722
3809
  //#endregion
@@ -4296,6 +4383,135 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
4296
4383
  }
4297
4384
  };
4298
4385
  //#endregion
4386
+ //#region src/constants/reasonix-paths.ts
4387
+ const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
4388
+ const REASONIX_GLOBAL_DIR = ".reasonix";
4389
+ const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
4390
+ const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
4391
+ const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
4392
+ const REASONIX_DIR = REASONIX_GLOBAL_DIR;
4393
+ const REASONIX_SETTINGS_FILE_NAME = "settings.json";
4394
+ const REASONIX_COMMANDS_DIR_PATH = (0, node_path.join)(REASONIX_DIR, "commands");
4395
+ //#endregion
4396
+ //#region src/features/commands/reasonix-command.ts
4397
+ /**
4398
+ * Reasonix custom slash commands are Markdown files under `.reasonix/commands/`
4399
+ * (project) / `~/.reasonix/commands/` (global) — directly analogous to Claude
4400
+ * Code's `.claude/commands/` (Reasonix explicitly copies Claude Code's
4401
+ * conventions). Frontmatter supports `description` and `argument-hint`; the
4402
+ * body uses the same `$ARGUMENTS` / `$1`…`$N` placeholder syntax rulesync's
4403
+ * universal command-body syntax already targets.
4404
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/GUIDE.md
4405
+ */
4406
+ const ReasonixCommandFrontmatterSchema = zod_mini.z.looseObject({
4407
+ description: zod_mini.z.optional(zod_mini.z.string()),
4408
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
4409
+ });
4410
+ var ReasonixCommand = class ReasonixCommand extends ToolCommand {
4411
+ frontmatter;
4412
+ body;
4413
+ constructor({ frontmatter, body, ...rest }) {
4414
+ if (rest.validate) {
4415
+ const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
4416
+ if (!result.success) throw new Error(`Invalid frontmatter in ${(0, node_path.join)(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
4417
+ }
4418
+ super({
4419
+ ...rest,
4420
+ fileContent: stringifyFrontmatter(body, frontmatter)
4421
+ });
4422
+ this.frontmatter = frontmatter;
4423
+ this.body = body;
4424
+ }
4425
+ static getSettablePaths(_options = {}) {
4426
+ return { relativeDirPath: REASONIX_COMMANDS_DIR_PATH };
4427
+ }
4428
+ getBody() {
4429
+ return this.body;
4430
+ }
4431
+ getFrontmatter() {
4432
+ return this.frontmatter;
4433
+ }
4434
+ toRulesyncCommand() {
4435
+ const { description, ...restFields } = this.frontmatter;
4436
+ const rulesyncFrontmatter = {
4437
+ targets: ["*"],
4438
+ description,
4439
+ ...Object.keys(restFields).length > 0 && { reasonix: restFields }
4440
+ };
4441
+ const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
4442
+ return new RulesyncCommand({
4443
+ outputRoot: ".",
4444
+ frontmatter: rulesyncFrontmatter,
4445
+ body: this.body,
4446
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
4447
+ relativeFilePath: this.relativeFilePath,
4448
+ fileContent,
4449
+ validate: true
4450
+ });
4451
+ }
4452
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
4453
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
4454
+ const reasonixFields = rulesyncFrontmatter.reasonix ?? {};
4455
+ return new ReasonixCommand({
4456
+ outputRoot,
4457
+ frontmatter: {
4458
+ description: rulesyncFrontmatter.description,
4459
+ ...reasonixFields
4460
+ },
4461
+ body: rulesyncCommand.getBody(),
4462
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
4463
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
4464
+ validate
4465
+ });
4466
+ }
4467
+ validate() {
4468
+ if (!this.frontmatter) return {
4469
+ success: true,
4470
+ error: null
4471
+ };
4472
+ const result = ReasonixCommandFrontmatterSchema.safeParse(this.frontmatter);
4473
+ if (result.success) return {
4474
+ success: true,
4475
+ error: null
4476
+ };
4477
+ else return {
4478
+ success: false,
4479
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
4480
+ };
4481
+ }
4482
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
4483
+ return this.isTargetedByRulesyncCommandDefault({
4484
+ rulesyncCommand,
4485
+ toolTarget: "reasonix"
4486
+ });
4487
+ }
4488
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
4489
+ const paths = this.getSettablePaths({ global });
4490
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
4491
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
4492
+ const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
4493
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
4494
+ return new ReasonixCommand({
4495
+ outputRoot,
4496
+ relativeDirPath: paths.relativeDirPath,
4497
+ relativeFilePath,
4498
+ frontmatter: result.data,
4499
+ body: content.trim(),
4500
+ validate
4501
+ });
4502
+ }
4503
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
4504
+ return new ReasonixCommand({
4505
+ outputRoot,
4506
+ relativeDirPath,
4507
+ relativeFilePath,
4508
+ frontmatter: { description: "" },
4509
+ body: "",
4510
+ validate: false
4511
+ });
4512
+ }
4513
+ };
4514
+ //#endregion
4299
4515
  //#region src/constants/roo-paths.ts
4300
4516
  const ROO_DIR = ".roo";
4301
4517
  const ROO_COMMANDS_DIR_PATH = (0, node_path.join)(ROO_DIR, "commands");
@@ -4427,6 +4643,217 @@ var RooCommand = class RooCommand extends ToolCommand {
4427
4643
  }
4428
4644
  };
4429
4645
  //#endregion
4646
+ //#region src/constants/rovodev-paths.ts
4647
+ const ROVODEV_DIR = ".rovodev";
4648
+ const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
4649
+ const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
4650
+ const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
4651
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
4652
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
4653
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
4654
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
4655
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
4656
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
4657
+ const ROVODEV_PROMPTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "prompts");
4658
+ //#endregion
4659
+ //#region src/types/tool-file.ts
4660
+ var ToolFile = class extends AiFile {};
4661
+ //#endregion
4662
+ //#region src/features/commands/rovodev-command.ts
4663
+ /**
4664
+ * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
4665
+ * `prompts.yml` manifest (`{ name, description, content_file }` entries) plus
4666
+ * per-prompt Markdown content files, discovered in repo-root `.rovodev/`, cwd
4667
+ * `.rovodev/`, and global `~/.rovodev/`, and invoked via `/prompts [title] [extra]`.
4668
+ *
4669
+ * This class represents a single prompt's **content file** — pure Markdown,
4670
+ * no frontmatter — written to `.rovodev/prompts/<name>.md` (project) or
4671
+ * `~/.rovodev/prompts/<name>.md` (global). The `name`/`description` are kept
4672
+ * on the instance (not serialized into the content file itself) so that
4673
+ * {@link RovodevCommand.getAuxiliaryFiles} can build the sibling
4674
+ * `prompts.yml` manifest that indexes every generated prompt.
4675
+ *
4676
+ * @see https://support.atlassian.com/rovo/docs/save-and-reuse-a-prompt-in-rovo-dev-cli/
4677
+ * @see https://support.atlassian.com/rovo/docs/rovo-dev-cli-commands/
4678
+ */
4679
+ var RovodevCommand = class RovodevCommand extends ToolCommand {
4680
+ name;
4681
+ description;
4682
+ body;
4683
+ constructor({ name, description, body, ...rest }) {
4684
+ super({
4685
+ ...rest,
4686
+ fileContent: body
4687
+ });
4688
+ this.name = name;
4689
+ this.description = description;
4690
+ this.body = body;
4691
+ if (rest.validate) {
4692
+ const result = this.validate();
4693
+ if (!result.success) throw result.error;
4694
+ }
4695
+ }
4696
+ static getSettablePaths(_options = {}) {
4697
+ return { relativeDirPath: ROVODEV_PROMPTS_DIR_PATH };
4698
+ }
4699
+ getName() {
4700
+ return this.name;
4701
+ }
4702
+ getDescription() {
4703
+ return this.description;
4704
+ }
4705
+ getBody() {
4706
+ return this.body;
4707
+ }
4708
+ validate() {
4709
+ if (!this.name) return {
4710
+ success: false,
4711
+ error: /* @__PURE__ */ new Error(`${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: Rovo Dev saved-prompt name must not be empty`)
4712
+ };
4713
+ return {
4714
+ success: true,
4715
+ error: null
4716
+ };
4717
+ }
4718
+ toRulesyncCommand() {
4719
+ const rulesyncFrontmatter = {
4720
+ targets: ["*"],
4721
+ description: this.description
4722
+ };
4723
+ const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
4724
+ return new RulesyncCommand({
4725
+ outputRoot: ".",
4726
+ frontmatter: rulesyncFrontmatter,
4727
+ body: this.body,
4728
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
4729
+ relativeFilePath: this.getRelativeFilePath(),
4730
+ fileContent,
4731
+ validate: true
4732
+ });
4733
+ }
4734
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
4735
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
4736
+ const relativeFilePath = rulesyncCommand.getRelativeFilePath();
4737
+ const name = (0, node_path.basename)(relativeFilePath, ".md");
4738
+ const paths = this.getSettablePaths({ global });
4739
+ return new RovodevCommand({
4740
+ outputRoot,
4741
+ name,
4742
+ description: rulesyncFrontmatter.description ?? "",
4743
+ body: rulesyncCommand.getBody(),
4744
+ relativeDirPath: paths.relativeDirPath,
4745
+ relativeFilePath,
4746
+ validate,
4747
+ global
4748
+ });
4749
+ }
4750
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
4751
+ return this.isTargetedByRulesyncCommandDefault({
4752
+ rulesyncCommand,
4753
+ toolTarget: "rovodev"
4754
+ });
4755
+ }
4756
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
4757
+ const paths = this.getSettablePaths({ global });
4758
+ const body = (await readFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath))).trim();
4759
+ const name = (0, node_path.basename)(relativeFilePath, ".md");
4760
+ return new RovodevCommand({
4761
+ outputRoot,
4762
+ name,
4763
+ description: await lookupPromptDescription({
4764
+ outputRoot,
4765
+ relativeFilePath,
4766
+ name
4767
+ }),
4768
+ body,
4769
+ relativeDirPath: paths.relativeDirPath,
4770
+ relativeFilePath,
4771
+ validate,
4772
+ global
4773
+ });
4774
+ }
4775
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
4776
+ return new RovodevCommand({
4777
+ outputRoot,
4778
+ name: (0, node_path.basename)(relativeFilePath, ".md"),
4779
+ description: "",
4780
+ body: "",
4781
+ relativeDirPath,
4782
+ relativeFilePath,
4783
+ validate: false,
4784
+ global
4785
+ });
4786
+ }
4787
+ /**
4788
+ * Rebuilds `.rovodev/prompts.yml` (project) / `~/.rovodev/prompts.yml`
4789
+ * (global) from every `RovodevCommand` generated in this pass. The `prompts`
4790
+ * array is fully replaced with the current set of rulesync-managed prompts
4791
+ * (mirrors `RovodevMcp` fully replacing `mcpServers`); any other top-level
4792
+ * key in an existing manifest is preserved.
4793
+ */
4794
+ static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false }) {
4795
+ const rovodevCommands = toolCommands.filter((command) => command instanceof RovodevCommand);
4796
+ if (rovodevCommands.length === 0) return [];
4797
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
4798
+ let existing = {};
4799
+ if (existingContent) try {
4800
+ const parsed = (0, js_yaml.load)(existingContent);
4801
+ if (isPlainObject(parsed)) existing = parsed;
4802
+ } catch {}
4803
+ const prompts = rovodevCommands.map((command) => ({
4804
+ name: command.getName(),
4805
+ description: command.getDescription(),
4806
+ content_file: toPosixPath((0, node_path.join)("prompts", command.getRelativeFilePath()))
4807
+ })).toSorted((a, b) => a.name.localeCompare(b.name));
4808
+ return [new RovodevPromptsManifest({
4809
+ outputRoot,
4810
+ relativeDirPath: ROVODEV_DIR,
4811
+ relativeFilePath: ROVODEV_PROMPTS_FILE_NAME,
4812
+ fileContent: (0, js_yaml.dump)({
4813
+ ...existing,
4814
+ prompts
4815
+ }),
4816
+ global
4817
+ })];
4818
+ }
4819
+ };
4820
+ /**
4821
+ * The `prompts` entry shape read back from `content_file`, matched against
4822
+ * either the resolved path (relative to `prompts.yml`, i.e. the `.rovodev/`
4823
+ * directory) or the entry `name`, to recover the `description` that isn't
4824
+ * stored in the content file itself.
4825
+ */
4826
+ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
4827
+ const manifestContent = await readFileContentOrNull((0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
4828
+ if (!manifestContent) return "";
4829
+ let parsed;
4830
+ try {
4831
+ parsed = (0, js_yaml.load)(manifestContent);
4832
+ } catch {
4833
+ return "";
4834
+ }
4835
+ if (!isPlainObject(parsed) || !Array.isArray(parsed.prompts)) return "";
4836
+ const expectedContentFile = toPosixPath((0, node_path.join)("prompts", relativeFilePath));
4837
+ const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
4838
+ return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
4839
+ }
4840
+ /**
4841
+ * The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
4842
+ * Never deleted by orphan cleanup: it is regenerated (not individually
4843
+ * discovered) alongside the per-prompt content files it references.
4844
+ */
4845
+ var RovodevPromptsManifest = class extends ToolFile {
4846
+ isDeletable() {
4847
+ return false;
4848
+ }
4849
+ validate() {
4850
+ return {
4851
+ success: true,
4852
+ error: null
4853
+ };
4854
+ }
4855
+ };
4856
+ //#endregion
4430
4857
  //#region src/constants/takt-paths.ts
4431
4858
  const TAKT_DIR = ".takt";
4432
4859
  const TAKT_FACETS_SUBDIR = "facets";
@@ -4444,6 +4871,14 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4444
4871
  * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4445
4872
  */
4446
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";
4447
4882
  //#endregion
4448
4883
  //#region src/features/takt-shared.ts
4449
4884
  /**
@@ -4619,6 +5054,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4619
5054
  supportsSubdirectory: false
4620
5055
  }
4621
5056
  }],
5057
+ ["antigravity-cli", {
5058
+ class: AntigravityCliCommand,
5059
+ meta: {
5060
+ extension: "md",
5061
+ supportsProject: true,
5062
+ supportsGlobal: true,
5063
+ isSimulated: false,
5064
+ supportsSubdirectory: false
5065
+ }
5066
+ }],
4622
5067
  ["antigravity-ide", {
4623
5068
  class: AntigravityIdeCommand,
4624
5069
  meta: {
@@ -4764,7 +5209,7 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4764
5209
  meta: {
4765
5210
  extension: "md",
4766
5211
  supportsProject: true,
4767
- supportsGlobal: false,
5212
+ supportsGlobal: true,
4768
5213
  isSimulated: false,
4769
5214
  supportsSubdirectory: false
4770
5215
  }
@@ -4809,6 +5254,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4809
5254
  supportsSubdirectory: true
4810
5255
  }
4811
5256
  }],
5257
+ ["reasonix", {
5258
+ class: ReasonixCommand,
5259
+ meta: {
5260
+ extension: "md",
5261
+ supportsProject: true,
5262
+ supportsGlobal: true,
5263
+ isSimulated: false,
5264
+ supportsSubdirectory: true
5265
+ }
5266
+ }],
4812
5267
  ["roo", {
4813
5268
  class: RooCommand,
4814
5269
  meta: {
@@ -4819,6 +5274,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4819
5274
  supportsSubdirectory: true
4820
5275
  }
4821
5276
  }],
5277
+ ["rovodev", {
5278
+ class: RovodevCommand,
5279
+ meta: {
5280
+ extension: "md",
5281
+ supportsProject: true,
5282
+ supportsGlobal: true,
5283
+ isSimulated: false,
5284
+ supportsSubdirectory: false
5285
+ }
5286
+ }],
4822
5287
  ["takt", {
4823
5288
  class: TaktCommand,
4824
5289
  meta: {
@@ -4876,7 +5341,7 @@ var CommandsProcessor = class extends FeatureProcessor {
4876
5341
  const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
4877
5342
  const factory = this.getFactory(this.toolTarget);
4878
5343
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
4879
- return rulesyncCommands.map((rulesyncCommand) => {
5344
+ const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
4880
5345
  if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) return null;
4881
5346
  const originalRelativePath = rulesyncCommand.getRelativeFilePath();
4882
5347
  const commandToConvert = factory.meta.supportsSubdirectory ? rulesyncCommand : this.flattenRelativeFilePath(rulesyncCommand);
@@ -4892,6 +5357,14 @@ var CommandsProcessor = class extends FeatureProcessor {
4892
5357
  global: this.global
4893
5358
  });
4894
5359
  }).filter((command) => command !== null);
5360
+ const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
5361
+ toolCommands,
5362
+ outputRoot: this.outputRoot,
5363
+ global: this.global
5364
+ });
5365
+ const result = [...toolCommands];
5366
+ if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
5367
+ return result;
4895
5368
  }
4896
5369
  async convertToolFilesToRulesyncFiles(toolFiles) {
4897
5370
  return toolFiles.filter((file) => file instanceof ToolCommand).map((toolCommand) => {
@@ -5050,7 +5523,14 @@ const HookDefinitionSchema = zod_mini.z.looseObject({
5050
5523
  name: zod_mini.z.optional(safeString),
5051
5524
  description: zod_mini.z.optional(safeString),
5052
5525
  failClosed: zod_mini.z.optional(zod_mini.z.boolean()),
5053
- sequential: zod_mini.z.optional(zod_mini.z.boolean())
5526
+ sequential: zod_mini.z.optional(zod_mini.z.boolean()),
5527
+ async: zod_mini.z.optional(zod_mini.z.boolean()),
5528
+ env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
5529
+ shell: zod_mini.z.optional(safeString),
5530
+ statusMessage: zod_mini.z.optional(safeString),
5531
+ headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), safeString)),
5532
+ allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
5533
+ once: zod_mini.z.optional(zod_mini.z.boolean())
5054
5534
  });
5055
5535
  /** Hook events supported by Cursor. */
5056
5536
  const CURSOR_HOOK_EVENTS = [
@@ -5114,6 +5594,30 @@ const CLAUDE_HOOK_EVENTS = [
5114
5594
  "elicitation",
5115
5595
  "elicitationResult"
5116
5596
  ];
5597
+ /**
5598
+ * Hook events supported by Devin Local (native `.devin/` hooks).
5599
+ *
5600
+ * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
5601
+ * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
5602
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
5603
+ * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
5604
+ * a `matcher` (regex against `tool_name`); the session/turn events do not.
5605
+ *
5606
+ * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
5607
+ * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
5608
+ * `~/.config/devin/config.json`.
5609
+ *
5610
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
5611
+ */
5612
+ const DEVIN_HOOK_EVENTS = [
5613
+ "sessionStart",
5614
+ "sessionEnd",
5615
+ "preToolUse",
5616
+ "postToolUse",
5617
+ "beforeSubmitPrompt",
5618
+ "stop",
5619
+ "permissionRequest"
5620
+ ];
5117
5621
  /** Hook events supported by OpenCode. */
5118
5622
  const OPENCODE_HOOK_EVENTS = [
5119
5623
  "sessionStart",
@@ -5156,7 +5660,11 @@ const COPILOT_HOOK_EVENTS = [
5156
5660
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
5157
5661
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
5158
5662
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
5159
- * `notification`.
5663
+ * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
5664
+ *
5665
+ * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
5666
+ * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
5667
+ * metadata. https://github.com/github/copilot-cli/blob/main/changelog.md
5160
5668
  *
5161
5669
  * @see https://docs.github.com/en/copilot/reference/hooks-configuration
5162
5670
  */
@@ -5173,7 +5681,8 @@ const COPILOTCLI_HOOK_EVENTS = [
5173
5681
  "afterError",
5174
5682
  "preCompact",
5175
5683
  "permissionRequest",
5176
- "notification"
5684
+ "notification",
5685
+ "beforeMCPExecution"
5177
5686
  ];
5178
5687
  /**
5179
5688
  * Hook events supported by Factory Droid.
@@ -5258,6 +5767,25 @@ const KIRO_HOOK_EVENTS = [
5258
5767
  "stop"
5259
5768
  ];
5260
5769
  /**
5770
+ * Hook events supported by the Kiro IDE (`.kiro/hooks/*.json` v1).
5771
+ *
5772
+ * Kiro IDE 1.0 exposes PascalCase triggers. rulesync maps the canonical
5773
+ * lifecycle events that have a clean 1:1 IDE equivalent: `SessionStart`,
5774
+ * `Stop`, `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. The IDE also
5775
+ * documents file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and
5776
+ * spec-task (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical
5777
+ * equivalent; those can still be emitted verbatim via a `kiro-ide` override
5778
+ * block (unknown event keys pass through unchanged).
5779
+ * @see https://kiro.dev/docs/hooks/types/
5780
+ */
5781
+ const KIRO_IDE_HOOK_EVENTS = [
5782
+ "sessionStart",
5783
+ "beforeSubmitPrompt",
5784
+ "preToolUse",
5785
+ "postToolUse",
5786
+ "stop"
5787
+ ];
5788
+ /**
5261
5789
  * Hook events supported by Google Antigravity (both the IDE and the CLI).
5262
5790
  *
5263
5791
  * Antigravity exposes a Claude-style hooks surface covering the five
@@ -5308,18 +5836,23 @@ const VIBE_HOOK_EVENTS = [
5308
5836
  /**
5309
5837
  * Hook events supported by JetBrains Junie CLI.
5310
5838
  *
5311
- * Junie CLI exposes four lifecycle events under the `"hooks"` key of
5312
- * `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `Stop`, and
5313
- * `SessionEnd`. Matchers apply only to `SessionStart` / `SessionEnd`
5314
- * (e.g. `startup` / `resume`); `UserPromptSubmit` and `Stop` are
5315
- * matcher-less. Only `type: "command"` hooks are supported. Project-local
5316
- * hooks are ignored for safety.
5839
+ * Junie CLI exposes seven lifecycle events under the `"hooks"` key of
5840
+ * `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
5841
+ * `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`. Matchers apply
5842
+ * to `SessionStart` (source), `PreToolUse` (tool name), `StopFailure` (error
5843
+ * type), `PermissionRequest` (tool name), and `SessionEnd` (reason);
5844
+ * `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
5845
+ * `type: "command"` hooks are supported. Project-local hooks are ignored for
5846
+ * safety.
5317
5847
  * @see https://junie.jetbrains.com/docs/junie-cli-hooks.html
5318
5848
  */
5319
5849
  const JUNIE_HOOK_EVENTS = [
5320
5850
  "sessionStart",
5321
5851
  "beforeSubmitPrompt",
5852
+ "preToolUse",
5322
5853
  "stop",
5854
+ "stopFailure",
5855
+ "permissionRequest",
5323
5856
  "sessionEnd"
5324
5857
  ];
5325
5858
  /**
@@ -5351,6 +5884,69 @@ const QWENCODE_HOOK_EVENTS = [
5351
5884
  "todoCreated",
5352
5885
  "todoCompleted"
5353
5886
  ];
5887
+ /**
5888
+ * Hook events supported by Reasonix.
5889
+ *
5890
+ * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
5891
+ * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
5892
+ * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
5893
+ * `SubagentStop`, `Notification`, `PreCompact`), but only the four events the
5894
+ * upstream issue scoped in are mapped here: `PreToolUse`, `PostToolUse`,
5895
+ * `UserPromptSubmit` ← `beforeSubmitPrompt`, and `Stop`. `match` (Reasonix's
5896
+ * matcher field name) is honored only on `PreToolUse`/`PostToolUse`, matching
5897
+ * the canonical `matcher` field's tool-event scoping used by other adapters.
5898
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
5899
+ */
5900
+ const REASONIX_HOOK_EVENTS = [
5901
+ "preToolUse",
5902
+ "postToolUse",
5903
+ "beforeSubmitPrompt",
5904
+ "stop"
5905
+ ];
5906
+ /**
5907
+ * Hook events supported by Hermes Agent's native Shell Hooks system.
5908
+ *
5909
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set:
5910
+ * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
5911
+ * `pre_verify`, `on_session_start`, `on_session_end`, `on_session_finalize`,
5912
+ * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
5913
+ * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
5914
+ * `transform_terminal_output`, `transform_llm_output`. Only the events with a
5915
+ * clean 1:1 canonical equivalent are mapped here; the remaining `VALID_HOOKS`
5916
+ * entries (`pre_verify`, `on_session_finalize`, `on_session_reset`,
5917
+ * `pre_gateway_dispatch`, `pre_approval_request`, `post_approval_response`, the
5918
+ * `transform_*` result-rewriting hooks) have no canonical rulesync equivalent,
5919
+ * so no canonical event maps to them.
5920
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
5921
+ */
5922
+ const HERMESAGENT_HOOK_EVENTS = [
5923
+ "sessionStart",
5924
+ "sessionEnd",
5925
+ "preToolUse",
5926
+ "postToolUse",
5927
+ "preModelInvocation",
5928
+ "postModelInvocation",
5929
+ "subagentStart",
5930
+ "subagentStop"
5931
+ ];
5932
+ /**
5933
+ * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
5934
+ * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
5935
+ */
5936
+ const CANONICAL_TO_HERMESAGENT_EVENT_NAMES = {
5937
+ sessionStart: "on_session_start",
5938
+ sessionEnd: "on_session_end",
5939
+ preToolUse: "pre_tool_call",
5940
+ postToolUse: "post_tool_call",
5941
+ preModelInvocation: "pre_llm_call",
5942
+ postModelInvocation: "post_llm_call",
5943
+ subagentStart: "subagent_start",
5944
+ subagentStop: "subagent_stop"
5945
+ };
5946
+ /**
5947
+ * Map Hermes Agent's native `VALID_HOOKS` keys back to canonical camelCase.
5948
+ */
5949
+ const HERMESAGENT_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_HERMESAGENT_EVENT_NAMES).map(([k, v]) => [v, k]));
5354
5950
  const hooksRecordSchema = zod_mini.z.record(zod_mini.z.string(), zod_mini.z.array(HookDefinitionSchema));
5355
5951
  /**
5356
5952
  * Canonical hooks config (canonical event names in camelCase).
@@ -5370,12 +5966,15 @@ const HooksConfigSchema = zod_mini.z.looseObject({
5370
5966
  deepagents: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5371
5967
  kiro: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5372
5968
  "kiro-cli": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5969
+ "kiro-ide": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5373
5970
  devin: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5374
5971
  augmentcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5375
5972
  "antigravity-ide": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5376
5973
  "antigravity-cli": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5974
+ hermesagent: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5377
5975
  junie: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5378
5976
  vibe: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5977
+ reasonix: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
5379
5978
  qwencode: zod_mini.z.optional(zod_mini.z.looseObject({
5380
5979
  hooks: zod_mini.z.optional(hooksRecordSchema),
5381
5980
  disableAllHooks: zod_mini.z.optional(zod_mini.z.boolean())
@@ -5421,6 +6020,26 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
5421
6020
  */
5422
6021
  const CLAUDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CLAUDE_EVENT_NAMES).map(([k, v]) => [v, k]));
5423
6022
  /**
6023
+ * Map canonical camelCase event names to Devin Local PascalCase.
6024
+ *
6025
+ * Devin Local reuses the same Claude-style PascalCase event names for the
6026
+ * subset of events it supports.
6027
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
6028
+ */
6029
+ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6030
+ sessionStart: "SessionStart",
6031
+ sessionEnd: "SessionEnd",
6032
+ preToolUse: "PreToolUse",
6033
+ postToolUse: "PostToolUse",
6034
+ beforeSubmitPrompt: "UserPromptSubmit",
6035
+ stop: "Stop",
6036
+ permissionRequest: "PermissionRequest"
6037
+ };
6038
+ /**
6039
+ * Map Devin Local PascalCase event names to canonical camelCase.
6040
+ */
6041
+ const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
6042
+ /**
5424
6043
  * Map canonical camelCase event names to AugmentCode PascalCase.
5425
6044
  * Auggie reuses the same PascalCase names as Claude for the events it supports.
5426
6045
  */
@@ -5551,7 +6170,8 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
5551
6170
  afterError: "errorOccurred",
5552
6171
  preCompact: "preCompact",
5553
6172
  permissionRequest: "permissionRequest",
5554
- notification: "notification"
6173
+ notification: "notification",
6174
+ beforeMCPExecution: "preMcpToolCall"
5555
6175
  };
5556
6176
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
5557
6177
  const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COPILOTCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
@@ -5633,13 +6253,35 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
5633
6253
  */
5634
6254
  const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
5635
6255
  /**
6256
+ * Map canonical camelCase event names to Kiro IDE PascalCase triggers.
6257
+ *
6258
+ * Only the canonical lifecycle events with a clean IDE equivalent are mapped.
6259
+ * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
6260
+ * override) pass through unchanged.
6261
+ * @see https://kiro.dev/docs/hooks/types/
6262
+ */
6263
+ const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
6264
+ sessionStart: "SessionStart",
6265
+ beforeSubmitPrompt: "UserPromptSubmit",
6266
+ preToolUse: "PreToolUse",
6267
+ postToolUse: "PostToolUse",
6268
+ stop: "Stop"
6269
+ };
6270
+ /**
6271
+ * Map Kiro IDE PascalCase trigger names to canonical camelCase.
6272
+ */
6273
+ const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_IDE_EVENT_NAMES).map(([k, v]) => [v, k]));
6274
+ /**
5636
6275
  * Map canonical camelCase event names to Junie PascalCase.
5637
6276
  * Junie reuses the same PascalCase names as Claude for the events it supports.
5638
6277
  */
5639
6278
  const CANONICAL_TO_JUNIE_EVENT_NAMES = {
5640
6279
  sessionStart: "SessionStart",
5641
6280
  beforeSubmitPrompt: "UserPromptSubmit",
6281
+ preToolUse: "PreToolUse",
5642
6282
  stop: "Stop",
6283
+ stopFailure: "StopFailure",
6284
+ permissionRequest: "PermissionRequest",
5643
6285
  sessionEnd: "SessionEnd"
5644
6286
  };
5645
6287
  /**
@@ -5692,19 +6334,22 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
5692
6334
  * Map Qwen Code PascalCase event names to canonical camelCase.
5693
6335
  */
5694
6336
  const QWENCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_QWENCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
5695
- //#endregion
5696
- //#region src/constants/antigravity-paths.ts
5697
- const ANTIGRAVITY_DIR = ".agents";
5698
- const ANTIGRAVITY_SKILLS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_DIR, "skills");
5699
- const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
5700
- const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
5701
- const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
5702
- const ANTIGRAVITY_GEMINI_DIR = ".gemini";
5703
- const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
5704
- const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
5705
- const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
5706
- const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
5707
- const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
6337
+ /**
6338
+ * Map canonical camelCase event names to Reasonix PascalCase.
6339
+ * Reasonix explicitly mirrors Claude Code's hooks model, so it reuses the same
6340
+ * PascalCase names for the four events rulesync maps.
6341
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
6342
+ */
6343
+ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6344
+ preToolUse: "PreToolUse",
6345
+ postToolUse: "PostToolUse",
6346
+ beforeSubmitPrompt: "UserPromptSubmit",
6347
+ stop: "Stop"
6348
+ };
6349
+ /**
6350
+ * Map Reasonix PascalCase event names to canonical camelCase.
6351
+ */
6352
+ const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
5708
6353
  //#endregion
5709
6354
  //#region src/utils/prototype-pollution.ts
5710
6355
  /**
@@ -5752,7 +6397,7 @@ function isToolMatcherEntry(x) {
5752
6397
  /**
5753
6398
  * Filter the shared canonical hooks to the supported events and merge tool overrides on top.
5754
6399
  */
5755
- function buildEffectiveHooks({ config, toolOverrideHooks, supportedEvents }) {
6400
+ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
5756
6401
  const supported = new Set(supportedEvents);
5757
6402
  const sharedHooks = {};
5758
6403
  for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
@@ -5781,7 +6426,7 @@ function groupDefinitionsByMatcher(definitions) {
5781
6426
  function applyCommandPrefix({ def, converterConfig }) {
5782
6427
  const commandText = def.command;
5783
6428
  const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
5784
- return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `${converterConfig.projectDirVar}/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
6429
+ return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `"${converterConfig.projectDirVar}"/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
5785
6430
  }
5786
6431
  /**
5787
6432
  * Convert the definitions of a single matcher group into tool hook entries,
@@ -5814,7 +6459,7 @@ function buildToolHooks({ defs, converterConfig }) {
5814
6459
  * (e.g. beforeSubmitPrompt → UserPromptSubmit).
5815
6460
  */
5816
6461
  function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logger }) {
5817
- const effectiveHooks = buildEffectiveHooks({
6462
+ const effectiveHooks = buildEffectiveHooks$1({
5818
6463
  config,
5819
6464
  toolOverrideHooks,
5820
6465
  supportedEvents: converterConfig.supportedEvents
@@ -5857,7 +6502,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
5857
6502
  */
5858
6503
  function stripCommandPrefix({ command, converterConfig }) {
5859
6504
  const cmd = typeof command === "string" ? command : void 0;
5860
- if (converterConfig.projectDirVar !== "" && typeof cmd === "string" && cmd.includes(`${converterConfig.projectDirVar}/`)) return cmd.replace(new RegExp(`^${converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/?`), "./");
6505
+ if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
6506
+ const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
6507
+ if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
6508
+ if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
6509
+ const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6510
+ return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
6511
+ }
5861
6512
  return cmd;
5862
6513
  }
5863
6514
  /**
@@ -5910,9 +6561,6 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
5910
6561
  return canonical;
5911
6562
  }
5912
6563
  //#endregion
5913
- //#region src/types/tool-file.ts
5914
- var ToolFile = class extends AiFile {};
5915
- //#endregion
5916
6564
  //#region src/features/hooks/rulesync-hooks.ts
5917
6565
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
5918
6566
  json;
@@ -6466,9 +7114,15 @@ const CODEXCLI_CONVERTER_CONFIG = {
6466
7114
  passthroughFields: ["name", "description"]
6467
7115
  };
6468
7116
  /**
6469
- * Build the content for `.codex/config.toml` with `[features] hooks = true`.
6470
- * Reads the existing file (if any), parses TOML, sets the flag, and returns the content
6471
- * without writing to disk. The caller is responsible for writing via the normal write phase.
7117
+ * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
7118
+ * Reads the existing file (if any), parses TOML, and returns the content without writing to
7119
+ * disk. The caller is responsible for writing via the normal write phase.
7120
+ *
7121
+ * Hooks are GA and enabled by default in Codex CLI, so `[features] hooks = true` is no longer
7122
+ * required and is intentionally NOT force-written here (doing so used to be harmless/idempotent,
7123
+ * but is now redundant and could mask a user's own `hooks = false` opt-out on a later edit).
7124
+ * See https://developers.openai.com/codex/hooks. Only the legacy `codex_hooks` alias — superseded
7125
+ * by `hooks` — is still cleaned up.
6472
7126
  */
6473
7127
  async function buildCodexConfigTomlContent({ outputRoot }) {
6474
7128
  const configPath = (0, node_path.join)(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
@@ -6479,10 +7133,7 @@ async function buildCodexConfigTomlContent({ outputRoot }) {
6479
7133
  } catch (error) {
6480
7134
  throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
6481
7135
  }
6482
- if (typeof configToml.features !== "object" || configToml.features === null) configToml.features = {};
6483
- const features = configToml.features;
6484
- delete features.codex_hooks;
6485
- features.hooks = true;
7136
+ if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
6486
7137
  return smol_toml.stringify(configToml);
6487
7138
  }
6488
7139
  /**
@@ -6518,6 +7169,12 @@ var CodexcliHooks = class CodexcliHooks extends ToolHooks {
6518
7169
  relativeFilePath: CODEXCLI_HOOKS_FILE_NAME
6519
7170
  };
6520
7171
  }
7172
+ static getExtraSharedWritePaths() {
7173
+ return [{
7174
+ relativeDirPath: CODEXCLI_DIR,
7175
+ relativeFilePath: CODEXCLI_MCP_FILE_NAME
7176
+ }];
7177
+ }
6521
7178
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
6522
7179
  const paths = CodexcliHooks.getSettablePaths({ global });
6523
7180
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
@@ -6750,6 +7407,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6750
7407
  }
6751
7408
  };
6752
7409
  //#endregion
7410
+ //#region src/utils/object.ts
7411
+ /**
7412
+ * Return a shallow copy of `obj` keeping only the entries whose value is
7413
+ * neither `undefined` nor `null`.
7414
+ *
7415
+ * Used to assemble generated config objects without one conditional spread per
7416
+ * optional field (which would otherwise exceed the lint complexity budget).
7417
+ */
7418
+ function compact(obj) {
7419
+ const result = {};
7420
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
7421
+ return result;
7422
+ }
7423
+ //#endregion
6753
7424
  //#region src/features/hooks/copilotcli-hooks.ts
6754
7425
  /**
6755
7426
  * GitHub Copilot CLI hooks.
@@ -6761,7 +7432,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6761
7432
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
6762
7433
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
6763
7434
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
6764
- * `notification`). Each entry supports three hook types:
7435
+ * `notification`, `preMcpToolCall`). Each entry supports three hook types:
6765
7436
  *
6766
7437
  * - `command` — the `bash` / `powershell` command-field shape with optional
6767
7438
  * `timeoutSec`, plus optional `cwd` / `env`.
@@ -6874,20 +7545,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
6874
7545
  } else if (hookType === "http") entries.push({
6875
7546
  type: "http",
6876
7547
  ...matcherPart,
6877
- ...def.url !== void 0 && def.url !== null && { url: def.url },
7548
+ ...compact({
7549
+ url: def.url,
7550
+ headers: def.headers,
7551
+ allowedEnvVars: def.allowedEnvVars
7552
+ }),
7553
+ ...timeoutPart,
7554
+ ...rest
7555
+ });
7556
+ else entries.push({
7557
+ type: "command",
7558
+ ...matcherPart,
7559
+ ...compact({
7560
+ [commandField]: def.command,
7561
+ env: def.env
7562
+ }),
6878
7563
  ...timeoutPart,
6879
7564
  ...rest
6880
7565
  });
6881
- else {
6882
- const command = def.command;
6883
- entries.push({
6884
- type: "command",
6885
- ...matcherPart,
6886
- ...command !== void 0 && command !== null && { [commandField]: command },
6887
- ...timeoutPart,
6888
- ...rest
6889
- });
6890
- }
6891
7566
  }
6892
7567
  return entries;
6893
7568
  }
@@ -7296,148 +7971,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
7296
7971
  };
7297
7972
  //#endregion
7298
7973
  //#region src/features/hooks/devin-hooks.ts
7974
+ const DEVIN_CONVERTER_CONFIG = {
7975
+ supportedEvents: DEVIN_HOOK_EVENTS,
7976
+ canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
7977
+ toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
7978
+ projectDirVar: "",
7979
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
7980
+ noMatcherEvents: /* @__PURE__ */ new Set([
7981
+ "sessionStart",
7982
+ "sessionEnd",
7983
+ "stop",
7984
+ "beforeSubmitPrompt"
7985
+ ])
7986
+ };
7299
7987
  /**
7300
- * Map canonical camelCase event names to Devin event names.
7301
- *
7302
- * The mapping is bijective for every event with a Devin equivalent, so the
7303
- * conversion round-trips cleanly. Devin splits the generic tool lifecycle
7304
- * into file/command/MCP specific events, so we line them up with the closest
7305
- * file/command/MCP specific canonical events rather than the generic
7306
- * preToolUse/postToolUse pair:
7307
- *
7308
- * - beforeReadFile ⇄ pre_read_code
7309
- * - beforeTabFileRead ⇄ post_read_code
7310
- * - afterTabFileEdit ⇄ pre_write_code
7311
- * - afterFileEdit ⇄ post_write_code
7312
- * - beforeShellExecution⇄ pre_run_command
7313
- * - afterShellExecution ⇄ post_run_command
7314
- * - beforeMCPExecution ⇄ pre_mcp_tool_use
7315
- * - afterMCPExecution ⇄ post_mcp_tool_use
7316
- * - beforeSubmitPrompt ⇄ pre_user_prompt
7317
- * - afterAgentResponse ⇄ post_cascade_response
7318
- * - beforeAgentResponse ⇄ post_cascade_response_with_transcript
7319
- * - worktreeCreate ⇄ post_setup_worktree
7320
- *
7321
- * NOTE on the before/after prefix mismatch: rulesync's canonical vocabulary has
7322
- * no `afterReadFile` or `beforeWriteFile`/`beforeFileEdit` event — the only read
7323
- * events are `beforeReadFile`/`beforeTabFileRead` (both read-side) and the only
7324
- * edit events are `afterFileEdit`/`afterTabFileEdit` (both edit-side). To cover
7325
- * all twelve Devin events bijectively we therefore pair the second read/write
7326
- * event by DOMAIN (read↔read, write↔write) rather than by timing, which inverts
7327
- * the prefix on `post_read_code` (⇄ beforeTabFileRead) and `pre_write_code`
7328
- * (⇄ afterTabFileEdit). The alternative — pairing by timing — would leave two
7329
- * Devin events unmapped and drop user hooks. The round-trip stays lossless;
7330
- * only the human-readable prefix differs, so this is a deliberate trade-off.
7331
- *
7332
- * Canonical events that have no Devin equivalent (e.g. sessionStart, stop)
7333
- * are dropped with a logged warning during export.
7334
- */
7335
- const CANONICAL_TO_DEVIN_EVENT_NAMES = {
7336
- beforeReadFile: "pre_read_code",
7337
- beforeTabFileRead: "post_read_code",
7338
- afterTabFileEdit: "pre_write_code",
7339
- afterFileEdit: "post_write_code",
7340
- beforeShellExecution: "pre_run_command",
7341
- afterShellExecution: "post_run_command",
7342
- beforeMCPExecution: "pre_mcp_tool_use",
7343
- afterMCPExecution: "post_mcp_tool_use",
7344
- beforeSubmitPrompt: "pre_user_prompt",
7345
- afterAgentResponse: "post_cascade_response",
7346
- beforeAgentResponse: "post_cascade_response_with_transcript",
7347
- worktreeCreate: "post_setup_worktree"
7348
- };
7349
- /**
7350
- * Map Devin event names back to canonical camelCase event names.
7351
- */
7352
- const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
7353
- /**
7354
- * Canonical hook events supported by Devin (the keys of the bijective map).
7988
+ * Hooks generator for Devin Local (native `.devin/` hooks).
7355
7989
  *
7356
- * Listed explicitly (rather than derived via `Object.keys`) so each value is
7357
- * type-checked against `HookEvent` and stays in sync with
7358
- * `CANONICAL_TO_DEVIN_EVENT_NAMES`.
7359
- */
7360
- const DEVIN_HOOK_EVENTS = [
7361
- "beforeReadFile",
7362
- "beforeTabFileRead",
7363
- "afterTabFileEdit",
7364
- "afterFileEdit",
7365
- "beforeShellExecution",
7366
- "afterShellExecution",
7367
- "beforeMCPExecution",
7368
- "afterMCPExecution",
7369
- "beforeSubmitPrompt",
7370
- "afterAgentResponse",
7371
- "beforeAgentResponse",
7372
- "worktreeCreate"
7373
- ];
7374
- function isRecord(value) {
7375
- return value !== null && typeof value === "object" && !Array.isArray(value);
7376
- }
7377
- /**
7378
- * Read the per-tool `devin` override hooks from the rulesync hooks config.
7990
+ * Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
7991
+ * to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
7992
+ * matcher groups.
7379
7993
  *
7380
- * `HooksConfigSchema` surfaces an optional `devin` block (mirroring every
7381
- * other hooks target), so the override hooks are read directly off the typed
7382
- * config without any cast.
7383
- */
7384
- function getDevinOverrideHooks(config) {
7385
- return config.devin?.hooks ?? {};
7386
- }
7387
- /**
7388
- * Convert a canonical hook definition into a Devin hook object, keeping only
7389
- * the Devin-supported fields. Returns null when neither command nor
7390
- * powershell is present (Devin requires at least one of them).
7391
- */
7392
- function canonicalDefToDevinHook(def) {
7393
- const hook = {};
7394
- if (typeof def.command === "string") hook.command = def.command;
7395
- if (typeof def.powershell === "string") hook.powershell = def.powershell;
7396
- if (typeof def.show_output === "boolean") hook.show_output = def.show_output;
7397
- if (typeof def.working_directory === "string") hook.working_directory = def.working_directory;
7398
- if (hook.command === void 0 && hook.powershell === void 0) return null;
7399
- return hook;
7400
- }
7401
- /**
7402
- * Convert a Devin hook object into a canonical hook definition.
7403
- */
7404
- function devinHookToCanonicalDef(hook) {
7405
- const def = { type: "command" };
7406
- if (typeof hook.command === "string") def.command = hook.command;
7407
- if (typeof hook.powershell === "string") def.powershell = hook.powershell;
7408
- if (typeof hook.show_output === "boolean") def.show_output = hook.show_output;
7409
- if (typeof hook.working_directory === "string") def.working_directory = hook.working_directory;
7410
- return def;
7411
- }
7412
- /**
7413
- * Hooks generator for Devin Cascade (GA).
7994
+ * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
7995
+ * level IS the event map (no wrapper key).
7996
+ * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
7997
+ * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
7998
+ * features, so reads and writes merge into the existing JSON and the file is
7999
+ * never deleted in global mode.
7414
8000
  *
7415
- * Writes a dedicated `hooks.json` whose top-level `hooks` key maps each
7416
- * Devin event name to a flat array of hook objects. Project and global modes
7417
- * share the same shape; only the location differs (`.windsurf/hooks.json` vs
7418
- * `~/.codeium/windsurf/hooks.json`). The harness overrides `outputRoot` with the
7419
- * home directory in global mode.
8001
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
7420
8002
  */
7421
8003
  var DevinHooks = class DevinHooks extends ToolHooks {
7422
8004
  constructor(params) {
7423
8005
  super({
7424
8006
  ...params,
7425
- fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
8007
+ fileContent: params.fileContent ?? "{}"
7426
8008
  });
7427
8009
  }
8010
+ /**
8011
+ * The project standalone `hooks.v1.json` is owned wholesale by rulesync and
8012
+ * may be deleted as an orphan. The global `config.json` is shared with the MCP
8013
+ * and permissions features, so it must never be deleted.
8014
+ */
8015
+ isDeletable() {
8016
+ return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
8017
+ }
7428
8018
  static getSettablePaths({ global = false } = {}) {
7429
8019
  if (global) return {
7430
- relativeDirPath: CODEIUM_WINDSURF_DIR,
7431
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
8020
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
8021
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
7432
8022
  };
7433
8023
  return {
7434
- relativeDirPath: WINDSURF_DIR,
7435
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
8024
+ relativeDirPath: DEVIN_DIR,
8025
+ relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
7436
8026
  };
7437
8027
  }
7438
8028
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
7439
8029
  const paths = DevinHooks.getSettablePaths({ global });
7440
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} });
8030
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
7441
8031
  return new DevinHooks({
7442
8032
  outputRoot,
7443
8033
  relativeDirPath: paths.relativeDirPath,
@@ -7448,31 +8038,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7448
8038
  }
7449
8039
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
7450
8040
  const paths = DevinHooks.getSettablePaths({ global });
7451
- await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ hooks: {} }, null, 2));
8041
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7452
8042
  const config = rulesyncHooks.getJson();
7453
- const supported = new Set(DEVIN_HOOK_EVENTS);
7454
- const sharedHooks = {};
7455
- for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
7456
- const overrideHooks = getDevinOverrideHooks(config);
7457
- const effectiveHooks = {
7458
- ...sharedHooks,
7459
- ...overrideHooks
7460
- };
7461
- const devinHooks = {};
7462
- for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
7463
- const devinEvent = CANONICAL_TO_DEVIN_EVENT_NAMES[canonicalEvent];
7464
- if (devinEvent === void 0) {
7465
- logger?.warn(`Skipped hook event "${canonicalEvent}" for devin (no Devin equivalent)`);
7466
- continue;
7467
- }
7468
- const objects = [];
7469
- for (const def of defs) {
7470
- const hook = canonicalDefToDevinHook(def);
7471
- if (hook !== null) objects.push(hook);
8043
+ const devinHooks = canonicalToToolHooks({
8044
+ config,
8045
+ toolOverrideHooks: config.devin?.hooks,
8046
+ converterConfig: DEVIN_CONVERTER_CONFIG,
8047
+ logger
8048
+ });
8049
+ let fileContent;
8050
+ if (global) {
8051
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8052
+ let parsedSettings;
8053
+ try {
8054
+ parsedSettings = JSON.parse(existingContent);
8055
+ } catch (error) {
8056
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
7472
8057
  }
7473
- if (objects.length > 0) devinHooks[devinEvent] = objects;
8058
+ const merged = {
8059
+ ...isRecord(parsedSettings) ? parsedSettings : {},
8060
+ hooks: devinHooks
8061
+ };
8062
+ fileContent = JSON.stringify(merged, null, 2);
8063
+ } else {
8064
+ await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8065
+ fileContent = JSON.stringify(devinHooks, null, 2);
7474
8066
  }
7475
- const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
7476
8067
  return new DevinHooks({
7477
8068
  outputRoot,
7478
8069
  relativeDirPath: paths.relativeDirPath,
@@ -7488,22 +8079,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7488
8079
  } catch (error) {
7489
8080
  throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
7490
8081
  }
7491
- const canonicalHooks = {};
7492
- const devinHooks = parsed.hooks;
7493
- if (isRecord(devinHooks)) for (const [devinEvent, rawObjects] of Object.entries(devinHooks)) {
7494
- if (!Array.isArray(rawObjects)) continue;
7495
- const canonicalEvent = DEVIN_TO_CANONICAL_EVENT_NAMES[devinEvent];
7496
- if (canonicalEvent === void 0) continue;
7497
- const defs = [];
7498
- for (const rawObject of rawObjects) {
7499
- if (!isRecord(rawObject)) continue;
7500
- defs.push(devinHookToCanonicalDef(rawObject));
7501
- }
7502
- if (defs.length > 0) canonicalHooks[canonicalEvent] = defs;
7503
- }
8082
+ const hooks = toolHooksToCanonical({
8083
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
8084
+ converterConfig: DEVIN_CONVERTER_CONFIG
8085
+ });
7504
8086
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
7505
8087
  version: 1,
7506
- hooks: canonicalHooks
8088
+ hooks
7507
8089
  }, null, 2) });
7508
8090
  }
7509
8091
  validate() {
@@ -7517,7 +8099,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7517
8099
  outputRoot,
7518
8100
  relativeDirPath,
7519
8101
  relativeFilePath,
7520
- fileContent: JSON.stringify({ hooks: {} }, null, 2),
8102
+ fileContent: "{}",
7521
8103
  validate: false
7522
8104
  });
7523
8105
  }
@@ -7744,6 +8326,106 @@ function mergeHermesConfig(fileContent, patch) {
7744
8326
  }
7745
8327
  //#endregion
7746
8328
  //#region src/features/hooks/hermesagent-hooks.ts
8329
+ /**
8330
+ * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
8331
+ * `post_tool_call`) and therefore carry a `matcher`. Every other supported
8332
+ * canonical event maps to a Hermes lifecycle event, which never accepts a
8333
+ * `matcher`.
8334
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8335
+ */
8336
+ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
8337
+ /**
8338
+ * Filter the shared canonical hooks to the events Hermes understands and merge
8339
+ * the `hermesagent`-specific override block on top.
8340
+ */
8341
+ function buildEffectiveHooks(config, toolOverrideHooks) {
8342
+ const supported = new Set(HERMESAGENT_HOOK_EVENTS);
8343
+ const shared = {};
8344
+ for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
8345
+ return {
8346
+ ...shared,
8347
+ ...toolOverrideHooks
8348
+ };
8349
+ }
8350
+ /**
8351
+ * Convert the canonical hooks config into Hermes's native
8352
+ * `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
8353
+ *
8354
+ * Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
8355
+ * via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
8356
+ * equivalent and are skipped (the shared `HooksProcessor` already warns about
8357
+ * unsupported hook types centrally). `matcher` is only carried through for
8358
+ * `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
8359
+ * warning, mirroring how other adapters (e.g. AugmentCode) handle
8360
+ * matcher-less lifecycle events.
8361
+ */
8362
+ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
8363
+ const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
8364
+ const result = {};
8365
+ for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
8366
+ const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
8367
+ if (!nativeEvent) continue;
8368
+ const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
8369
+ const entries = [];
8370
+ for (const def of defs) {
8371
+ if ((def.type ?? "command") !== "command") continue;
8372
+ if (typeof def.command !== "string" || def.command === "") continue;
8373
+ const entry = { command: def.command };
8374
+ if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
8375
+ else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
8376
+ if (typeof def.timeout === "number") entry.timeout = def.timeout;
8377
+ entries.push(entry);
8378
+ }
8379
+ if (entries.length > 0) result[nativeEvent] = entries;
8380
+ }
8381
+ return result;
8382
+ }
8383
+ /**
8384
+ * Reverse {@link canonicalToHermesHooks}: parse Hermes's native
8385
+ * `hooks: { <event>: [...] }` map back into a canonical event → definition[]
8386
+ * record. Native events with no canonical equivalent (`pre_verify`,
8387
+ * `transform_tool_result`, ...) are skipped since there is nothing to round
8388
+ * -trip them into.
8389
+ */
8390
+ function hermesHooksToCanonical(hooks) {
8391
+ const canonical = {};
8392
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
8393
+ for (const [nativeEvent, entries] of Object.entries(hooks)) {
8394
+ if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
8395
+ const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
8396
+ if (!canonicalEvent) continue;
8397
+ const defs = [];
8398
+ for (const raw of entries) {
8399
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
8400
+ const entry = raw;
8401
+ if (typeof entry.command !== "string") continue;
8402
+ const def = {
8403
+ type: "command",
8404
+ command: entry.command
8405
+ };
8406
+ if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
8407
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout;
8408
+ defs.push(def);
8409
+ }
8410
+ if (defs.length > 0) canonical[canonicalEvent] = defs;
8411
+ }
8412
+ return canonical;
8413
+ }
8414
+ /**
8415
+ * Hermes Agent shell hooks.
8416
+ *
8417
+ * Hermes Agent registers shell-command hooks under the `hooks:` key of the
8418
+ * shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
8419
+ * global only — Hermes has no project-scoped hooks location). Hermes only runs
8420
+ * hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
8421
+ * `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
8422
+ * `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
8423
+ * silently ignored. Generation therefore maps canonical events onto the real
8424
+ * `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
8425
+ * config instead of overwriting it, since that file also holds other Hermes
8426
+ * settings (model, `mcp_servers`, `command_allowlist`, ...).
8427
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8428
+ */
7747
8429
  var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7748
8430
  static getSettablePaths() {
7749
8431
  return {
@@ -7763,6 +8445,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7763
8445
  error: null
7764
8446
  };
7765
8447
  }
8448
+ isDeletable() {
8449
+ return false;
8450
+ }
7766
8451
  shouldMergeExistingFileContent() {
7767
8452
  return true;
7768
8453
  }
@@ -7770,18 +8455,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7770
8455
  this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
7771
8456
  }
7772
8457
  toRulesyncHooks() {
7773
- const config = parseHermesConfig(this.getFileContent());
7774
- const hooks = config.hooks && typeof config.hooks === "object" ? config.hooks.rulesync : {};
7775
- return new RulesyncHooks({
7776
- relativeDirPath: "",
7777
- relativeFilePath: ".rulesync/hooks.json",
7778
- fileContent: JSON.stringify(hooks ?? {}, null, 2)
7779
- });
8458
+ const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8459
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8460
+ version: 1,
8461
+ hooks
8462
+ }, null, 2) });
7780
8463
  }
7781
- static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
8464
+ static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
8465
+ const config = rulesyncHooks.getJson();
7782
8466
  return new HermesagentHooks({
7783
8467
  outputRoot,
7784
- fileContent: stringifyHermesConfig({ hooks: { rulesync: rulesyncHooks.getJson() } })
8468
+ fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8469
+ config,
8470
+ toolOverrideHooks: config.hermesagent?.hooks,
8471
+ logger
8472
+ }) })
7785
8473
  });
7786
8474
  }
7787
8475
  };
@@ -8251,9 +8939,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
8251
8939
  * the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
8252
8940
  * the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
8253
8941
  *
8254
- * (The Kiro IDE uses multi-file `.kiro/hooks/*.kiro.hook` hooks, which the
8255
- * single-file hooks pipeline does not emit yet, so `kiro-ide` has no hooks
8256
- * adapter.)
8942
+ * (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
8943
+ * {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
8257
8944
  */
8258
8945
  var KiroCliHooks = class extends KiroHooks {
8259
8946
  static getOverrideKey() {
@@ -8261,6 +8948,203 @@ var KiroCliHooks = class extends KiroHooks {
8261
8948
  }
8262
8949
  };
8263
8950
  //#endregion
8951
+ //#region src/features/hooks/kiro-ide-hooks.ts
8952
+ /**
8953
+ * One hook entry inside the Kiro IDE v1 `hooks` array.
8954
+ *
8955
+ * `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
8956
+ * imports do not drop data they do not yet understand.
8957
+ * @see https://kiro.dev/docs/hooks/types/
8958
+ */
8959
+ const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
8960
+ type: zod_mini.z.literal("command"),
8961
+ command: zod_mini.z.optional(safeString)
8962
+ }), zod_mini.z.looseObject({
8963
+ type: zod_mini.z.literal("agent"),
8964
+ prompt: zod_mini.z.optional(safeString)
8965
+ })]);
8966
+ const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
8967
+ name: zod_mini.z.optional(zod_mini.z.string()),
8968
+ description: zod_mini.z.optional(zod_mini.z.string()),
8969
+ trigger: zod_mini.z.optional(zod_mini.z.string()),
8970
+ matcher: zod_mini.z.optional(zod_mini.z.string()),
8971
+ action: zod_mini.z.optional(KiroIdeHookActionSchema),
8972
+ timeout: zod_mini.z.optional(zod_mini.z.number()),
8973
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
8974
+ });
8975
+ const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
8976
+ version: zod_mini.z.optional(zod_mini.z.string()),
8977
+ hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
8978
+ });
8979
+ /**
8980
+ * Build the Kiro IDE hook entries for a single canonical event's definitions.
8981
+ *
8982
+ * `command`-type definitions become `{ type: "command", command }` actions and
8983
+ * `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
8984
+ * types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
8985
+ * already warns about unsupported types).
8986
+ */
8987
+ function buildKiroIdeEntriesForEvent(trigger, definitions) {
8988
+ const entries = [];
8989
+ for (const def of definitions) {
8990
+ const type = def.type ?? "command";
8991
+ let action;
8992
+ if (type === "command") {
8993
+ if (def.command === void 0) continue;
8994
+ action = {
8995
+ type: "command",
8996
+ command: def.command
8997
+ };
8998
+ } else if (type === "prompt") {
8999
+ if (def.prompt === void 0) continue;
9000
+ action = {
9001
+ type: "agent",
9002
+ prompt: def.prompt
9003
+ };
9004
+ } else continue;
9005
+ entries.push({
9006
+ name: def.name ?? trigger,
9007
+ ...def.description !== void 0 && def.description !== null && { description: def.description },
9008
+ trigger,
9009
+ ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
9010
+ action,
9011
+ ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
9012
+ enabled: true
9013
+ });
9014
+ }
9015
+ return entries;
9016
+ }
9017
+ function canonicalToKiroIdeHooks(config) {
9018
+ const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
9019
+ const sharedHooks = {};
9020
+ for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
9021
+ const effectiveHooks = {
9022
+ ...sharedHooks,
9023
+ ...config["kiro-ide"]?.hooks
9024
+ };
9025
+ const entries = [];
9026
+ for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
9027
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
9028
+ entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
9029
+ }
9030
+ return entries;
9031
+ }
9032
+ function kiroIdeHooksToCanonical(entries) {
9033
+ const canonical = {};
9034
+ for (const entry of entries) {
9035
+ if (entry.trigger === void 0 || entry.action === void 0) continue;
9036
+ const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
9037
+ if (isPrototypePollutionKey(eventName)) continue;
9038
+ const def = {};
9039
+ if (entry.action.type === "command") {
9040
+ if (!entry.action.command) continue;
9041
+ def.type = "command";
9042
+ def.command = entry.action.command;
9043
+ } else {
9044
+ if (!entry.action.prompt) continue;
9045
+ def.type = "prompt";
9046
+ def.prompt = entry.action.prompt;
9047
+ }
9048
+ if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
9049
+ if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
9050
+ if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
9051
+ if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
9052
+ (canonical[eventName] ??= []).push(def);
9053
+ }
9054
+ return canonical;
9055
+ }
9056
+ /**
9057
+ * Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
9058
+ *
9059
+ * Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
9060
+ * `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
9061
+ * `hooks` array, so rulesync emits every generated hook into one
9062
+ * `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
9063
+ * which keeps it within the single-file hooks architecture.
9064
+ *
9065
+ * This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
9066
+ * KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
9067
+ *
9068
+ * @see https://kiro.dev/docs/hooks/
9069
+ */
9070
+ var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
9071
+ constructor(params) {
9072
+ super({
9073
+ ...params,
9074
+ fileContent: params.fileContent ?? JSON.stringify({
9075
+ version: "v1",
9076
+ hooks: []
9077
+ }, null, 2)
9078
+ });
9079
+ }
9080
+ static getSettablePaths(_options = {}) {
9081
+ return {
9082
+ relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
9083
+ relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
9084
+ };
9085
+ }
9086
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9087
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9088
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
9089
+ version: "v1",
9090
+ hooks: []
9091
+ }, null, 2);
9092
+ return new KiroIdeHooks({
9093
+ outputRoot,
9094
+ relativeDirPath: paths.relativeDirPath,
9095
+ relativeFilePath: paths.relativeFilePath,
9096
+ fileContent,
9097
+ validate
9098
+ });
9099
+ }
9100
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
9101
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9102
+ const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
9103
+ const fileContent = JSON.stringify({
9104
+ version: "v1",
9105
+ hooks
9106
+ }, null, 2);
9107
+ return new KiroIdeHooks({
9108
+ outputRoot,
9109
+ relativeDirPath: paths.relativeDirPath,
9110
+ relativeFilePath: paths.relativeFilePath,
9111
+ fileContent,
9112
+ validate
9113
+ });
9114
+ }
9115
+ toRulesyncHooks() {
9116
+ let parsed;
9117
+ try {
9118
+ parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
9119
+ } catch (error) {
9120
+ throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9121
+ }
9122
+ const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
9123
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9124
+ version: 1,
9125
+ hooks
9126
+ }, null, 2) });
9127
+ }
9128
+ validate() {
9129
+ return {
9130
+ success: true,
9131
+ error: null
9132
+ };
9133
+ }
9134
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9135
+ return new KiroIdeHooks({
9136
+ outputRoot,
9137
+ relativeDirPath,
9138
+ relativeFilePath,
9139
+ fileContent: JSON.stringify({
9140
+ version: "v1",
9141
+ hooks: []
9142
+ }, null, 2),
9143
+ validate: false
9144
+ });
9145
+ }
9146
+ };
9147
+ //#endregion
8264
9148
  //#region src/features/hooks/opencode-hooks.ts
8265
9149
  var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8266
9150
  constructor(params) {
@@ -8319,6 +9203,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8319
9203
  //#endregion
8320
9204
  //#region src/features/hooks/qwencode-hooks.ts
8321
9205
  /**
9206
+ * Build a single Qwen Code hook object from a canonical hook definition.
9207
+ * Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
9208
+ * and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
9209
+ * matching upstream. `statusMessage` applies to both.
9210
+ * https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
9211
+ */
9212
+ function canonicalDefToQwencodeHook(def) {
9213
+ const type = def.type ?? "command";
9214
+ const isHttp = type === "http";
9215
+ const isCommand = type === "command";
9216
+ return {
9217
+ type,
9218
+ ...compact({
9219
+ command: def.command,
9220
+ url: def.url,
9221
+ timeout: def.timeout,
9222
+ name: def.name,
9223
+ description: def.description,
9224
+ statusMessage: def.statusMessage,
9225
+ async: isCommand ? def.async : void 0,
9226
+ env: isCommand ? def.env : void 0,
9227
+ shell: isCommand ? def.shell : void 0,
9228
+ headers: isHttp ? def.headers : void 0,
9229
+ allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
9230
+ once: isHttp ? def.once : void 0
9231
+ })
9232
+ };
9233
+ }
9234
+ /**
8322
9235
  * Convert canonical hooks config to Qwen Code format.
8323
9236
  * Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
8324
9237
  * then converts to PascalCase and Qwen Code matcher/hooks structure.
@@ -8346,16 +9259,7 @@ function canonicalToQwencodeHooks(config) {
8346
9259
  }
8347
9260
  const entries = [];
8348
9261
  for (const [matcherKey, defs] of byMatcher) {
8349
- const hooks = defs.map((def) => {
8350
- return {
8351
- type: def.type ?? "command",
8352
- ...def.command !== void 0 && def.command !== null && { command: def.command },
8353
- ...def.url !== void 0 && def.url !== null && { url: def.url },
8354
- ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
8355
- ...def.name !== void 0 && def.name !== null && { name: def.name },
8356
- ...def.description !== void 0 && def.description !== null && { description: def.description }
8357
- };
8358
- });
9262
+ const hooks = defs.map(canonicalDefToQwencodeHook);
8359
9263
  const sequential = defs.some((def) => def.sequential === true);
8360
9264
  const group = matcherKey ? {
8361
9265
  matcher: matcherKey,
@@ -8379,7 +9283,14 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
8379
9283
  url: zod_mini.z.optional(zod_mini.z.string()),
8380
9284
  timeout: zod_mini.z.optional(zod_mini.z.number()),
8381
9285
  name: zod_mini.z.optional(zod_mini.z.string()),
8382
- description: zod_mini.z.optional(zod_mini.z.string())
9286
+ description: zod_mini.z.optional(zod_mini.z.string()),
9287
+ statusMessage: zod_mini.z.optional(zod_mini.z.string()),
9288
+ async: zod_mini.z.optional(zod_mini.z.boolean()),
9289
+ env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
9290
+ shell: zod_mini.z.optional(zod_mini.z.string()),
9291
+ headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
9292
+ allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
9293
+ once: zod_mini.z.optional(zod_mini.z.boolean())
8383
9294
  });
8384
9295
  /**
8385
9296
  * A matcher group entry in a Qwen Code event array.
@@ -8398,18 +9309,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
8398
9309
  const defs = [];
8399
9310
  const hooks = entry.hooks ?? [];
8400
9311
  const sequential = entry.sequential === true;
9312
+ const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
8401
9313
  for (const h of hooks) {
8402
- const command = h.command;
8403
9314
  const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
9315
+ const isHttp = hookType === "http";
9316
+ const isCommand = hookType === "command";
8404
9317
  defs.push({
8405
9318
  type: hookType,
8406
- ...command !== void 0 && command !== null && { command },
8407
- ...h.url !== void 0 && h.url !== null && { url: h.url },
8408
- ...h.timeout !== void 0 && h.timeout !== null && { timeout: h.timeout },
8409
- ...h.name !== void 0 && h.name !== null && { name: h.name },
8410
- ...h.description !== void 0 && h.description !== null && { description: h.description },
8411
- ...sequential && { sequential: true },
8412
- ...entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && { matcher: entry.matcher }
9319
+ ...compact({
9320
+ command: h.command,
9321
+ url: h.url,
9322
+ timeout: h.timeout,
9323
+ name: h.name,
9324
+ description: h.description,
9325
+ statusMessage: h.statusMessage,
9326
+ async: isCommand ? h.async : void 0,
9327
+ env: isCommand ? h.env : void 0,
9328
+ shell: isCommand ? h.shell : void 0,
9329
+ headers: isHttp ? h.headers : void 0,
9330
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
9331
+ once: isHttp ? h.once : void 0,
9332
+ sequential: sequential ? true : void 0,
9333
+ matcher
9334
+ })
8413
9335
  });
8414
9336
  }
8415
9337
  return defs;
@@ -8522,6 +9444,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
8522
9444
  }
8523
9445
  };
8524
9446
  //#endregion
9447
+ //#region src/features/hooks/reasonix-hooks.ts
9448
+ /**
9449
+ * Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
9450
+ * against the tool name); it is ignored on every other event.
9451
+ */
9452
+ const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
9453
+ const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
9454
+ /**
9455
+ * Convert canonical hooks config to the Reasonix `hooks` object.
9456
+ * Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
9457
+ * then maps event names and emits flat per-event hook-entry arrays.
9458
+ */
9459
+ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
9460
+ const sharedHooks = {};
9461
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
9462
+ const effectiveHooks = {
9463
+ ...sharedHooks,
9464
+ ...toolOverrideHooks
9465
+ };
9466
+ const result = {};
9467
+ for (const [event, defs] of Object.entries(effectiveHooks)) {
9468
+ if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
9469
+ const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
9470
+ const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
9471
+ const entries = [];
9472
+ for (const def of defs) {
9473
+ if ((def.type ?? "command") !== "command") continue;
9474
+ if (typeof def.command !== "string") continue;
9475
+ const entry = { command: def.command };
9476
+ if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
9477
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
9478
+ if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
9479
+ if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
9480
+ entries.push(entry);
9481
+ }
9482
+ if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
9483
+ }
9484
+ return result;
9485
+ }
9486
+ /**
9487
+ * Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
9488
+ * back into a canonical event -> definition[] record.
9489
+ */
9490
+ function reasonixHooksToCanonical(hooks) {
9491
+ const canonical = {};
9492
+ if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
9493
+ for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
9494
+ if (!Array.isArray(rawEntries)) continue;
9495
+ const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
9496
+ const defs = [];
9497
+ for (const rawEntry of rawEntries) {
9498
+ if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
9499
+ const entry = rawEntry;
9500
+ if (typeof entry.command !== "string") continue;
9501
+ const def = {
9502
+ type: "command",
9503
+ command: entry.command
9504
+ };
9505
+ if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
9506
+ if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
9507
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
9508
+ defs.push(def);
9509
+ }
9510
+ if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
9511
+ }
9512
+ return canonical;
9513
+ }
9514
+ /**
9515
+ * Reasonix hooks adapter.
9516
+ *
9517
+ * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9518
+ * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9519
+ * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9520
+ * Only the four events documented in the upstream issue are mapped:
9521
+ * PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
9522
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9523
+ */
9524
+ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
9525
+ constructor(params) {
9526
+ super({
9527
+ ...params,
9528
+ fileContent: params.fileContent ?? "{}"
9529
+ });
9530
+ }
9531
+ isDeletable() {
9532
+ return false;
9533
+ }
9534
+ static getSettablePaths(_options = {}) {
9535
+ return {
9536
+ relativeDirPath: REASONIX_DIR,
9537
+ relativeFilePath: REASONIX_SETTINGS_FILE_NAME
9538
+ };
9539
+ }
9540
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9541
+ const paths = ReasonixHooks.getSettablePaths({ global });
9542
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9543
+ return new ReasonixHooks({
9544
+ outputRoot,
9545
+ relativeDirPath: paths.relativeDirPath,
9546
+ relativeFilePath: paths.relativeFilePath,
9547
+ fileContent,
9548
+ validate
9549
+ });
9550
+ }
9551
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9552
+ const paths = ReasonixHooks.getSettablePaths({ global });
9553
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9554
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9555
+ let settings;
9556
+ try {
9557
+ settings = JSON.parse(existingContent);
9558
+ } catch (error) {
9559
+ throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
9560
+ }
9561
+ const config = rulesyncHooks.getJson();
9562
+ const reasonixHooks = canonicalToReasonixHooks({
9563
+ config,
9564
+ toolOverrideHooks: config.reasonix?.hooks,
9565
+ logger
9566
+ });
9567
+ const merged = {
9568
+ ...settings,
9569
+ hooks: reasonixHooks
9570
+ };
9571
+ const fileContent = JSON.stringify(merged, null, 2);
9572
+ return new ReasonixHooks({
9573
+ outputRoot,
9574
+ relativeDirPath: paths.relativeDirPath,
9575
+ relativeFilePath: paths.relativeFilePath,
9576
+ fileContent,
9577
+ validate
9578
+ });
9579
+ }
9580
+ toRulesyncHooks() {
9581
+ let settings;
9582
+ try {
9583
+ settings = JSON.parse(this.getFileContent());
9584
+ } catch (error) {
9585
+ throw new Error(`Failed to parse Reasonix hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9586
+ }
9587
+ const hooks = reasonixHooksToCanonical(settings.hooks);
9588
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9589
+ version: 1,
9590
+ hooks
9591
+ }, null, 2) });
9592
+ }
9593
+ validate() {
9594
+ return {
9595
+ success: true,
9596
+ error: null
9597
+ };
9598
+ }
9599
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9600
+ return new ReasonixHooks({
9601
+ outputRoot,
9602
+ relativeDirPath,
9603
+ relativeFilePath,
9604
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9605
+ validate: false
9606
+ });
9607
+ }
9608
+ };
9609
+ //#endregion
8525
9610
  //#region src/features/hooks/vibe-hooks.ts
8526
9611
  const VIBE_DIR = ".vibe";
8527
9612
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -8680,6 +9765,12 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8680
9765
  relativeFilePath: VIBE_HOOKS_FILE_NAME
8681
9766
  };
8682
9767
  }
9768
+ static getExtraSharedWritePaths() {
9769
+ return [{
9770
+ relativeDirPath: VIBE_DIR,
9771
+ relativeFilePath: VIBE_CONFIG_FILE_NAME
9772
+ }];
9773
+ }
8683
9774
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
8684
9775
  const paths = VibeHooks.getSettablePaths({ global });
8685
9776
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
@@ -8747,6 +9838,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8747
9838
  //#endregion
8748
9839
  //#region src/features/hooks/hooks-processor.ts
8749
9840
  const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetTuple);
9841
+ /**
9842
+ * Event names present in the config that the target's adapter cannot emit.
9843
+ *
9844
+ * When the factory passes override-block keys through verbatim
9845
+ * (`passthroughOverrideEvents`), those keys are excluded from the check so
9846
+ * documented passthrough triggers aren't falsely reported as skipped.
9847
+ */
9848
+ function unsupportedEventNames(params) {
9849
+ const { factory, sharedHooks, effectiveHooks } = params;
9850
+ const supportedEvents = new Set(factory.supportedEvents);
9851
+ const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
9852
+ return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
9853
+ }
8750
9854
  const toolHooksFactories = /* @__PURE__ */ new Map([
8751
9855
  ["antigravity-cli", {
8752
9856
  class: AntigravityCliHooks,
@@ -8880,12 +9984,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8880
9984
  supportsGlobal: true,
8881
9985
  supportsImport: true
8882
9986
  },
8883
- supportedEvents: CLAUDE_HOOK_EVENTS,
8884
- supportedHookTypes: [
8885
- "command",
8886
- "prompt",
8887
- "http"
8888
- ],
9987
+ supportedEvents: HERMESAGENT_HOOK_EVENTS,
9988
+ supportedHookTypes: ["command"],
8889
9989
  supportsMatcher: true
8890
9990
  }],
8891
9991
  ["deepagents", {
@@ -8921,6 +10021,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8921
10021
  supportedHookTypes: ["command"],
8922
10022
  supportsMatcher: true
8923
10023
  }],
10024
+ ["kiro-ide", {
10025
+ class: KiroIdeHooks,
10026
+ meta: {
10027
+ supportsProject: true,
10028
+ supportsGlobal: true,
10029
+ supportsImport: true
10030
+ },
10031
+ supportedEvents: KIRO_IDE_HOOK_EVENTS,
10032
+ supportedHookTypes: ["command", "prompt"],
10033
+ supportsMatcher: true,
10034
+ passthroughOverrideEvents: true
10035
+ }],
8924
10036
  ["devin", {
8925
10037
  class: DevinHooks,
8926
10038
  meta: {
@@ -8929,8 +10041,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8929
10041
  supportsImport: true
8930
10042
  },
8931
10043
  supportedEvents: DEVIN_HOOK_EVENTS,
8932
- supportedHookTypes: ["command"],
8933
- supportsMatcher: false
10044
+ supportedHookTypes: ["command", "prompt"],
10045
+ supportsMatcher: true
8934
10046
  }],
8935
10047
  ["augmentcode", {
8936
10048
  class: AugmentcodeHooks,
@@ -8975,6 +10087,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8975
10087
  supportedEvents: QWENCODE_HOOK_EVENTS,
8976
10088
  supportedHookTypes: ["command"],
8977
10089
  supportsMatcher: true
10090
+ }],
10091
+ ["reasonix", {
10092
+ class: ReasonixHooks,
10093
+ meta: {
10094
+ supportsProject: true,
10095
+ supportsGlobal: true,
10096
+ supportsImport: true
10097
+ },
10098
+ supportedEvents: REASONIX_HOOK_EVENTS,
10099
+ supportedHookTypes: ["command"],
10100
+ supportsMatcher: true
8978
10101
  }]
8979
10102
  ]);
8980
10103
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -9050,8 +10173,11 @@ var HooksProcessor = class extends FeatureProcessor {
9050
10173
  ...overrideHooks
9051
10174
  };
9052
10175
  {
9053
- const supportedEvents = new Set(factory.supportedEvents);
9054
- const skipped = [...new Set(Object.keys(effectiveHooks))].filter((e) => !supportedEvents.has(e));
10176
+ const skipped = unsupportedEventNames({
10177
+ factory,
10178
+ sharedHooks,
10179
+ effectiveHooks
10180
+ });
9055
10181
  if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
9056
10182
  }
9057
10183
  {
@@ -9247,11 +10373,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
9247
10373
  }
9248
10374
  };
9249
10375
  //#endregion
9250
- //#region src/constants/antigravity-cli-paths.ts
9251
- const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
9252
- const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
9253
- const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
9254
- //#endregion
9255
10376
  //#region src/features/ignore/antigravity-cli-ignore.ts
9256
10377
  /**
9257
10378
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -10483,9 +11604,9 @@ function parseAmpSettingsJsonc(fileContent) {
10483
11604
  }
10484
11605
  function filterMcpServers(mcpServers) {
10485
11606
  const filtered = {};
10486
- if (!isRecord$1(mcpServers)) return filtered;
11607
+ if (!isRecord(mcpServers)) return filtered;
10487
11608
  for (const [name, config] of Object.entries(mcpServers)) {
10488
- if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
11609
+ if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
10489
11610
  const filteredConfig = {};
10490
11611
  for (const [key, value] of Object.entries(config)) {
10491
11612
  if (isPrototypePollutionKey(key)) continue;
@@ -10600,7 +11721,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10600
11721
  success: true,
10601
11722
  error: null
10602
11723
  };
10603
- if (!isRecord$1(mcpServers)) return {
11724
+ if (!isRecord(mcpServers)) return {
10604
11725
  success: false,
10605
11726
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
10606
11727
  };
@@ -10609,7 +11730,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10609
11730
  success: false,
10610
11731
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
10611
11732
  };
10612
- if (!isRecord$1(serverConfig)) return {
11733
+ if (!isRecord(serverConfig)) return {
10613
11734
  success: false,
10614
11735
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
10615
11736
  };
@@ -11130,7 +12251,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
11130
12251
  function convertFromCodexFormat(codexMcp) {
11131
12252
  const result = {};
11132
12253
  for (const [name, config] of Object.entries(codexMcp)) {
11133
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
12254
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11134
12255
  const converted = {};
11135
12256
  for (const [key, value] of Object.entries(config)) {
11136
12257
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11150,7 +12271,7 @@ function convertToCodexFormat(mcpServers) {
11150
12271
  const result = {};
11151
12272
  for (const [name, config] of Object.entries(mcpServers)) {
11152
12273
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
11153
- if (!isRecord$1(config)) continue;
12274
+ if (!isRecord(config)) continue;
11154
12275
  const converted = {};
11155
12276
  for (const [key, value] of Object.entries(config)) {
11156
12277
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11212,19 +12333,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
11212
12333
  const strippedMcpServers = rulesyncMcp.getMcpServers();
11213
12334
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
11214
12335
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
11215
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
12336
+ const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
11216
12337
  return [serverName, {
11217
12338
  ...serverConfig,
11218
- ...isRecord$1(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
12339
+ ...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
11219
12340
  }];
11220
12341
  })));
11221
12342
  const filteredMcpServers = this.removeEmptyEntries(converted);
11222
12343
  for (const name of Object.keys(converted)) if (!Object.hasOwn(filteredMcpServers, name)) warnWithFallback(void 0, `MCP server "${name}" had no non-empty configuration and was dropped from the codex CLI config`);
11223
- const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
12344
+ const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
11224
12345
  configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
11225
- const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12346
+ const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
11226
12347
  const serverRecord = serverConfig;
11227
- if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
12348
+ if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
11228
12349
  ...serverRecord,
11229
12350
  tools: existingServer["tools"]
11230
12351
  }];
@@ -11734,18 +12855,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
11734
12855
  //#endregion
11735
12856
  //#region src/features/mcp/devin-mcp.ts
11736
12857
  /**
11737
- * MCP generator for Devin (Cascade).
12858
+ * MCP generator for Devin Local (native `.devin/` configuration).
12859
+ *
12860
+ * Devin reads MCP servers from the `mcpServers` key of its native config file:
12861
+ * - Project scope: `.devin/config.json`
12862
+ * - Global scope: `~/.config/devin/config.json`
11738
12863
  *
11739
- * Devin reads file-based MCP configuration from:
11740
- * - Project scope: `.windsurf/mcp_config.json`
11741
- * - Global scope: `~/.codeium/windsurf/mcp_config.json`
12864
+ * The config file is shared with the permissions feature (`permissions` key)
12865
+ * and, in global mode, the hooks feature (`hooks` key), so reads and writes
12866
+ * merge into the existing JSON rather than overwriting it, and the file is
12867
+ * never deleted. Each server is a stdio entry ({ command, args, env }) or a
12868
+ * remote entry ({ serverUrl | url, headers }), and may carry an optional
12869
+ * `disabledTools` array.
11742
12870
  *
11743
- * The official docs document only the global path; the project path mirrors
11744
- * the same `mcp_config.json` filename so both scopes fit the processor
11745
- * framework cleanly. The top-level key is `mcpServers`; each server is a
11746
- * stdio entry ({ command, args, env }) or a remote entry
11747
- * ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
11748
- * array.
12871
+ * @see https://docs.devin.ai/cli/extensibility/configuration
11749
12872
  */
11750
12873
  var DevinMcp = class DevinMcp extends ToolMcp {
11751
12874
  json;
@@ -11763,14 +12886,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
11763
12886
  throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
11764
12887
  }
11765
12888
  }
12889
+ /**
12890
+ * config.json may carry the permissions/hooks features' keys, so it is never
12891
+ * deleted; only the managed `mcpServers` key is rewritten.
12892
+ */
12893
+ isDeletable() {
12894
+ return false;
12895
+ }
11766
12896
  static getSettablePaths({ global = false } = {}) {
11767
12897
  if (global) return {
11768
- relativeDirPath: CODEIUM_WINDSURF_DIR,
11769
- relativeFilePath: DEVIN_MCP_FILE_NAME
12898
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
12899
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11770
12900
  };
11771
12901
  return {
11772
- relativeDirPath: WINDSURF_DIR,
11773
- relativeFilePath: DEVIN_MCP_FILE_NAME
12902
+ relativeDirPath: DEVIN_DIR,
12903
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11774
12904
  };
11775
12905
  }
11776
12906
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
@@ -11885,7 +13015,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
11885
13015
  };
11886
13016
  //#endregion
11887
13017
  //#region src/features/mcp/goose-mcp.ts
11888
- const GOOSE_GLOBAL_ONLY_MESSAGE$1 = "Goose MCP is global-only; use --global to sync ~/.config/goose/config.yaml";
13018
+ const GOOSE_PLUGIN_MCP_RELATIVE_PATH = (0, node_path.join)(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
11889
13019
  function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
11890
13020
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
11891
13021
  let parsed;
@@ -11972,7 +13102,7 @@ function convertServerToGooseExtension(name, config) {
11972
13102
  function convertToGooseFormat(mcpServers) {
11973
13103
  const extensions = {};
11974
13104
  for (const [name, config] of Object.entries(mcpServers)) {
11975
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13105
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11976
13106
  extensions[name] = convertServerToGooseExtension(name, config);
11977
13107
  }
11978
13108
  return extensions;
@@ -11989,7 +13119,7 @@ function convertToGooseFormat(mcpServers) {
11989
13119
  function convertFromGooseFormat(extensions) {
11990
13120
  const result = {};
11991
13121
  for (const [name, ext] of Object.entries(extensions)) {
11992
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
13122
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
11993
13123
  const server = {};
11994
13124
  const type = typeof ext.type === "string" ? ext.type : void 0;
11995
13125
  if (type === "sse") server.type = "sse";
@@ -12007,39 +13137,102 @@ function convertFromGooseFormat(extensions) {
12007
13137
  return result;
12008
13138
  }
12009
13139
  /**
13140
+ * Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
13141
+ * open-plugins `.mcp.json` manifest.
13142
+ */
13143
+ function buildGoosePluginStdioServer(config) {
13144
+ const server = {};
13145
+ const command = config.command;
13146
+ if (Array.isArray(command)) {
13147
+ if (typeof command[0] === "string") server.command = command[0];
13148
+ const rest = command.slice(1).filter((c) => typeof c === "string");
13149
+ const args = isStringArray(config.args) ? config.args : [];
13150
+ if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
13151
+ } else if (typeof command === "string") {
13152
+ server.command = command;
13153
+ if (isStringArray(config.args)) server.args = config.args;
13154
+ }
13155
+ if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
13156
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
13157
+ return server;
13158
+ }
13159
+ /**
13160
+ * Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
13161
+ * used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
13162
+ *
13163
+ * The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
13164
+ * (http/sse/streamable_http) and `builtin` servers cannot be represented. They
13165
+ * are skipped with a warning rather than silently dropped — remote servers stay
13166
+ * global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
13167
+ */
13168
+ function convertToGoosePluginMcpServers(mcpServers, logger) {
13169
+ const result = {};
13170
+ for (const [name, config] of Object.entries(mcpServers)) {
13171
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
13172
+ const gooseType = resolveGooseType(config, resolveGooseUrl(config));
13173
+ if (gooseType !== "stdio") {
13174
+ warnWithFallback(logger, `Goose open-plugin MCP manifest (${GOOSE_PLUGIN_MCP_RELATIVE_PATH}) is stdio-only; skipping "${name}" (${gooseType}). Sync it with --global to ~/.config/goose/config.yaml instead.`);
13175
+ continue;
13176
+ }
13177
+ result[name] = buildGoosePluginStdioServer(config);
13178
+ }
13179
+ return result;
13180
+ }
13181
+ /**
12010
13182
  * Goose MCP servers.
12011
13183
  *
12012
- * Goose configures MCP servers as "extensions" in the shared user config file
12013
- * `~/.config/goose/config.yaml` (global only — Goose has no project-scoped MCP
12014
- * location). That file also holds other Goose settings (model, provider, ...),
12015
- * so generation merges the `extensions:` block into the existing config instead
12016
- * of overwriting it, and the file is never deleted.
13184
+ * Goose configures MCP servers in two locations:
13185
+ *
13186
+ * - **Global** (`--global`): "extensions" in the shared user config
13187
+ * `~/.config/goose/config.yaml`. That file also holds other Goose settings
13188
+ * (model, provider, ...), so generation merges the `extensions:` block into the
13189
+ * existing config instead of overwriting it, and the file is never deleted.
13190
+ * This location supports both stdio and remote (http/sse) servers.
13191
+ * - **Project**: a stdio-only open-plugin manifest at
13192
+ * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
13193
+ * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
13194
+ * Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
13195
+ * shape and cannot express `url`/`headers`, so remote servers are skipped with
13196
+ * a warning in project mode (use `--global` to sync them instead).
12017
13197
  *
12018
13198
  * @see https://block.github.io/goose/docs/getting-started/using-extensions/
13199
+ * @see https://github.com/block/goose/pull/9471
12019
13200
  */
12020
13201
  var GooseMcp = class GooseMcp extends ToolMcp {
12021
13202
  config;
12022
13203
  constructor(params) {
12023
13204
  super(params);
12024
- if (this.fileContent !== void 0) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
12025
- else this.config = {};
13205
+ if (this.fileContent === void 0) this.config = {};
13206
+ else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
13207
+ else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
13208
+ }
13209
+ parsePluginManifest(fileContent) {
13210
+ try {
13211
+ const parsed = JSON.parse(fileContent);
13212
+ return isRecord(parsed) ? parsed : {};
13213
+ } catch (error) {
13214
+ throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
13215
+ }
12026
13216
  }
12027
13217
  getConfig() {
12028
13218
  return this.config;
12029
13219
  }
12030
13220
  isDeletable() {
12031
- return false;
13221
+ return !this.global;
12032
13222
  }
12033
- static getSettablePaths(_options) {
12034
- return {
13223
+ static getSettablePaths({ global = false } = {}) {
13224
+ if (global) return {
12035
13225
  relativeDirPath: GOOSE_GLOBAL_DIR,
12036
13226
  relativeFilePath: GOOSE_MCP_FILE_NAME
12037
13227
  };
13228
+ return {
13229
+ relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
13230
+ relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
13231
+ };
12038
13232
  }
12039
13233
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12040
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
12041
13234
  const paths = this.getSettablePaths({ global });
12042
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
13235
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
12043
13236
  return new GooseMcp({
12044
13237
  outputRoot,
12045
13238
  relativeDirPath: paths.relativeDirPath,
@@ -12049,9 +13242,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12049
13242
  global
12050
13243
  });
12051
13244
  }
12052
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12053
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
13245
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
12054
13246
  const paths = this.getSettablePaths({ global });
13247
+ if (!global) {
13248
+ const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
13249
+ return new GooseMcp({
13250
+ outputRoot,
13251
+ relativeDirPath: paths.relativeDirPath,
13252
+ relativeFilePath: paths.relativeFilePath,
13253
+ fileContent: JSON.stringify({ mcpServers }, null, 2),
13254
+ validate,
13255
+ global
13256
+ });
13257
+ }
12055
13258
  const merged = {
12056
13259
  ...parseGooseConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
12057
13260
  extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
@@ -12066,7 +13269,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12066
13269
  });
12067
13270
  }
12068
13271
  toRulesyncMcp() {
12069
- const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
13272
+ if (!this.global) {
13273
+ const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
13274
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
13275
+ }
13276
+ const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
12070
13277
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
12071
13278
  }
12072
13279
  validate() {
@@ -12143,7 +13350,7 @@ function convertToGrokFormat(mcpServers) {
12143
13350
  const result = {};
12144
13351
  for (const [name, config] of Object.entries(mcpServers)) {
12145
13352
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
12146
- if (!isRecord$1(config)) continue;
13353
+ if (!isRecord(config)) continue;
12147
13354
  const converted = {};
12148
13355
  for (const [key, value] of Object.entries(config)) {
12149
13356
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12158,7 +13365,7 @@ function convertToGrokFormat(mcpServers) {
12158
13365
  function convertFromGrokFormat(grokMcp) {
12159
13366
  const result = {};
12160
13367
  for (const [name, config] of Object.entries(grokMcp)) {
12161
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13368
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12162
13369
  const converted = {};
12163
13370
  for (const [key, value] of Object.entries(config)) {
12164
13371
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12321,13 +13528,13 @@ function convertServerToHermes(config) {
12321
13528
  function convertToHermesFormat(mcpServers) {
12322
13529
  const result = {};
12323
13530
  for (const [name, config] of Object.entries(mcpServers)) {
12324
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13531
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12325
13532
  result[name] = convertServerToHermes(config);
12326
13533
  }
12327
13534
  return result;
12328
13535
  }
12329
13536
  function mergeHermesMcpServers(config, mcpServers) {
12330
- const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
13537
+ const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
12331
13538
  return {
12332
13539
  ...config,
12333
13540
  mcp_servers: {
@@ -12345,7 +13552,7 @@ function mergeHermesMcpServers(config, mcpServers) {
12345
13552
  function convertFromHermesFormat(mcpServers) {
12346
13553
  const result = {};
12347
13554
  for (const [name, config] of Object.entries(mcpServers)) {
12348
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13555
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12349
13556
  const server = {};
12350
13557
  if (typeof config.command === "string") server.command = config.command;
12351
13558
  if (isStringArray(config.args)) server.args = config.args;
@@ -12381,7 +13588,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12381
13588
  return true;
12382
13589
  }
12383
13590
  setFileContent(fileContent) {
12384
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13591
+ const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12385
13592
  this.config = merged;
12386
13593
  super.setFileContent(stringifyHermesConfig(merged));
12387
13594
  }
@@ -12421,7 +13628,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12421
13628
  });
12422
13629
  }
12423
13630
  toRulesyncMcp() {
12424
- const servers = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13631
+ const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12425
13632
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
12426
13633
  }
12427
13634
  validate() {
@@ -13256,11 +14463,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
13256
14463
  }
13257
14464
  };
13258
14465
  //#endregion
13259
- //#region src/constants/reasonix-paths.ts
13260
- const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
13261
- const REASONIX_GLOBAL_DIR = ".reasonix";
13262
- const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
13263
- //#endregion
13264
14466
  //#region src/features/mcp/reasonix-mcp.ts
13265
14467
  const REASONIX_PLUGIN_FIELDS = [
13266
14468
  "type",
@@ -13268,13 +14470,14 @@ const REASONIX_PLUGIN_FIELDS = [
13268
14470
  "args",
13269
14471
  "env",
13270
14472
  "url",
13271
- "headers"
14473
+ "headers",
14474
+ "trusted_read_only_tools"
13272
14475
  ];
13273
14476
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13274
14477
  toml;
13275
14478
  constructor(params) {
13276
14479
  super(params);
13277
- this.toml = parseReasonixConfig(this.fileContent);
14480
+ this.toml = parseReasonixConfig$1(this.fileContent);
13278
14481
  }
13279
14482
  getToml() {
13280
14483
  return this.toml;
@@ -13298,7 +14501,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13298
14501
  }
13299
14502
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
13300
14503
  const paths = this.getSettablePaths({ global });
13301
- const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
14504
+ const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
13302
14505
  config.plugins = normalizePluginsArray(config.plugins);
13303
14506
  return new ReasonixMcp({
13304
14507
  outputRoot,
@@ -13311,7 +14514,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13311
14514
  }
13312
14515
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13313
14516
  const paths = this.getSettablePaths({ global });
13314
- const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
14517
+ const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
13315
14518
  config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
13316
14519
  return new ReasonixMcp({
13317
14520
  outputRoot,
@@ -13328,7 +14531,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13328
14531
  }
13329
14532
  validate() {
13330
14533
  try {
13331
- parseReasonixConfig(this.fileContent);
14534
+ parseReasonixConfig$1(this.fileContent);
13332
14535
  return {
13333
14536
  success: true,
13334
14537
  error: null
@@ -13351,7 +14554,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13351
14554
  });
13352
14555
  }
13353
14556
  };
13354
- function parseReasonixConfig(fileContent) {
14557
+ function parseReasonixConfig$1(fileContent) {
13355
14558
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
13356
14559
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
13357
14560
  return { ...parsed };
@@ -13491,17 +14694,6 @@ var RooMcp = class RooMcp extends ToolMcp {
13491
14694
  }
13492
14695
  };
13493
14696
  //#endregion
13494
- //#region src/constants/rovodev-paths.ts
13495
- const ROVODEV_DIR = ".rovodev";
13496
- const ROVODEV_SKILLS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "skills");
13497
- const ROVODEV_SUBAGENTS_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, "subagents");
13498
- const ROVODEV_MODULAR_RULES_DIR_PATH = (0, node_path.join)(ROVODEV_DIR, ".rulesync", "modular-rules");
13499
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
13500
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
13501
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
13502
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
13503
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = (0, node_path.join)(".agents", "skills");
13504
- //#endregion
13505
14697
  //#region src/features/mcp/rovodev-mcp.ts
13506
14698
  function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
13507
14699
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
@@ -13595,6 +14787,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
13595
14787
  }
13596
14788
  };
13597
14789
  //#endregion
14790
+ //#region src/features/shared/takt-config.ts
14791
+ /**
14792
+ * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14793
+ *
14794
+ * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14795
+ * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14796
+ * instances are rejected for prototype-pollution hardening; a YAML mapping
14797
+ * always parses to a plain object.
14798
+ */
14799
+ function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14800
+ const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
14801
+ let parsed;
14802
+ try {
14803
+ parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
14804
+ } catch (error) {
14805
+ throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14806
+ }
14807
+ if (parsed === void 0 || parsed === null) return {};
14808
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14809
+ return parsed;
14810
+ }
14811
+ //#endregion
14812
+ //#region src/features/mcp/takt-mcp.ts
14813
+ /**
14814
+ * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
14815
+ * global).
14816
+ *
14817
+ * IMPORTANT — what this adapter can and cannot represent.
14818
+ *
14819
+ * Takt does NOT have a project- or global-level registry of MCP *server
14820
+ * definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
14821
+ * or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
14822
+ * YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
14823
+ * config loader hard-rejects unknown top-level keys
14824
+ * (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
14825
+ * would therefore both be ignored and break the user's config.
14826
+ *
14827
+ * What `config.yaml` *does* hold is the default-deny transport allowlist
14828
+ * `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
14829
+ * servers are refused regardless of how they are declared. So this adapter emits
14830
+ * the transport allowlist derived from the transports present in
14831
+ * `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
14832
+ *
14833
+ * Lossiness (documented, intentional): the per-server names, commands, env, URLs
14834
+ * and headers are NOT representable in `config.yaml` and are intentionally not
14835
+ * written. Users still declare the concrete servers in their workflow YAML
14836
+ * steps; rulesync only opens the transport gate that permits them. As a
14837
+ * corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
14838
+ * a transport allowlist and yields an empty `mcpServers` map.
14839
+ *
14840
+ * The shared `config.yaml` is merged in place: only the
14841
+ * `workflow_mcp_servers` key is set; every other top-level key (provider,
14842
+ * provider_profiles, etc.) is preserved. The file is never deleted.
14843
+ *
14844
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
14845
+ * @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
14846
+ */
14847
+ var TaktMcp = class TaktMcp extends ToolMcp {
14848
+ constructor(params) {
14849
+ super({
14850
+ ...params,
14851
+ fileContent: params.fileContent ?? ""
14852
+ });
14853
+ }
14854
+ isDeletable() {
14855
+ return false;
14856
+ }
14857
+ static getSettablePaths(_options) {
14858
+ return {
14859
+ relativeDirPath: TAKT_DIR,
14860
+ relativeFilePath: TAKT_CONFIG_FILE_NAME
14861
+ };
14862
+ }
14863
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
14864
+ const paths = TaktMcp.getSettablePaths({ global });
14865
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
14866
+ return new TaktMcp({
14867
+ outputRoot,
14868
+ relativeDirPath: paths.relativeDirPath,
14869
+ relativeFilePath: paths.relativeFilePath,
14870
+ fileContent,
14871
+ validate,
14872
+ global
14873
+ });
14874
+ }
14875
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14876
+ const paths = TaktMcp.getSettablePaths({ global });
14877
+ const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
14878
+ const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14879
+ const merged = {
14880
+ ...config,
14881
+ [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14882
+ };
14883
+ return new TaktMcp({
14884
+ outputRoot,
14885
+ relativeDirPath: paths.relativeDirPath,
14886
+ relativeFilePath: paths.relativeFilePath,
14887
+ fileContent: (0, js_yaml.dump)(merged),
14888
+ validate,
14889
+ global
14890
+ });
14891
+ }
14892
+ /**
14893
+ * A transport allowlist cannot reconstruct the per-step server definitions,
14894
+ * so import yields an empty `mcpServers` map. This keeps the round-trip honest
14895
+ * rather than fabricating placeholder servers.
14896
+ */
14897
+ toRulesyncMcp() {
14898
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
14899
+ }
14900
+ validate() {
14901
+ return {
14902
+ success: true,
14903
+ error: null
14904
+ };
14905
+ }
14906
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
14907
+ return new TaktMcp({
14908
+ outputRoot,
14909
+ relativeDirPath,
14910
+ relativeFilePath,
14911
+ fileContent: "",
14912
+ validate: false,
14913
+ global
14914
+ });
14915
+ }
14916
+ };
14917
+ /**
14918
+ * Map a rulesync MCP server onto the single Takt transport its allowlist gates.
14919
+ *
14920
+ * Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
14921
+ * folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
14922
+ * explicit transport is treated as stdio when it carries a `command`, else as a
14923
+ * remote `http` server (it must have a `url`). Returns `undefined` only when the
14924
+ * shape is too ambiguous to classify.
14925
+ */
14926
+ function transportOf(server) {
14927
+ switch (server.type ?? server.transport) {
14928
+ case "stdio":
14929
+ case "local": return "stdio";
14930
+ case "sse": return "sse";
14931
+ case "http":
14932
+ case "streamable-http":
14933
+ case "ws": return "http";
14934
+ default: break;
14935
+ }
14936
+ if (server.command !== void 0) return "stdio";
14937
+ if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
14938
+ }
14939
+ /**
14940
+ * Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
14941
+ * the rulesync servers. All three keys are emitted explicitly (default-deny made
14942
+ * visible): a transport is `true` only when at least one server uses it.
14943
+ *
14944
+ * Server entries are read defensively (record guard); prototype-pollution server
14945
+ * names are irrelevant here because no user-controlled key or value is written —
14946
+ * only the three fixed boolean keys are.
14947
+ */
14948
+ function deriveTransportAllowlist(servers) {
14949
+ const allowlist = {
14950
+ stdio: false,
14951
+ sse: false,
14952
+ http: false
14953
+ };
14954
+ for (const server of Object.values(servers)) {
14955
+ if (!isRecord(server)) continue;
14956
+ const transport = transportOf(server);
14957
+ if (transport) allowlist[transport] = true;
14958
+ }
14959
+ return allowlist;
14960
+ }
14961
+ //#endregion
13598
14962
  //#region src/features/mcp/vibe-mcp.ts
13599
14963
  const VIBE_MCP_SERVER_FIELDS = [
13600
14964
  "transport",
@@ -14055,7 +15419,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14055
15419
  ["goose", {
14056
15420
  class: GooseMcp,
14057
15421
  meta: {
14058
- supportsProject: false,
15422
+ supportsProject: true,
14059
15423
  supportsGlobal: true,
14060
15424
  supportsEnabledTools: false,
14061
15425
  supportsDisabledTools: false
@@ -14169,6 +15533,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14169
15533
  supportsDisabledTools: false
14170
15534
  }
14171
15535
  }],
15536
+ ["takt", {
15537
+ class: TaktMcp,
15538
+ meta: {
15539
+ supportsProject: true,
15540
+ supportsGlobal: true,
15541
+ supportsEnabledTools: false,
15542
+ supportsDisabledTools: false
15543
+ }
15544
+ }],
14172
15545
  ["vibe", {
14173
15546
  class: VibeMcp,
14174
15547
  meta: {
@@ -14297,7 +15670,8 @@ var McpProcessor = class extends FeatureProcessor {
14297
15670
  return await factory.class.fromRulesyncMcp({
14298
15671
  outputRoot: this.outputRoot,
14299
15672
  rulesyncMcp: filteredRulesyncMcp,
14300
- global: this.global
15673
+ global: this.global,
15674
+ logger: this.logger
14301
15675
  });
14302
15676
  }));
14303
15677
  }
@@ -14811,7 +16185,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
14811
16185
  function toAntigravityCliToolName(canonical) {
14812
16186
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
14813
16187
  }
14814
- function toCanonicalToolName$5(cliName) {
16188
+ function toCanonicalToolName$6(cliName) {
14815
16189
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
14816
16190
  }
14817
16191
  /**
@@ -14994,7 +16368,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
14994
16368
  const processEntries = (entries, action) => {
14995
16369
  for (const entry of entries) {
14996
16370
  const { toolName, pattern } = parsePermissionEntry$1(entry);
14997
- const canonical = toCanonicalToolName$5(toolName);
16371
+ const canonical = toCanonicalToolName$6(toolName);
14998
16372
  if (!permission[canonical]) permission[canonical] = {};
14999
16373
  permission[canonical][pattern] = action;
15000
16374
  }
@@ -15033,7 +16407,7 @@ const IDE_ACTION_TO_CANONICAL = {
15033
16407
  function toIdeAction(canonical) {
15034
16408
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
15035
16409
  }
15036
- function toCanonicalCategory$1(ideAction) {
16410
+ function toCanonicalCategory$2(ideAction) {
15037
16411
  return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
15038
16412
  }
15039
16413
  /**
@@ -15207,7 +16581,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
15207
16581
  const processEntries = (entries, action) => {
15208
16582
  for (const entry of entries) {
15209
16583
  const { action: ideAction, pattern } = parsePermissionEntry(entry);
15210
- const canonical = toCanonicalCategory$1(ideAction);
16584
+ const canonical = toCanonicalCategory$2(ideAction);
15211
16585
  if (!permission[canonical]) permission[canonical] = {};
15212
16586
  permission[canonical][pattern] = action;
15213
16587
  }
@@ -15256,7 +16630,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
15256
16630
  function toAugmentToolName(canonical) {
15257
16631
  return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
15258
16632
  }
15259
- function toCanonicalToolName$4(augmentName) {
16633
+ function toCanonicalToolName$5(augmentName) {
15260
16634
  return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
15261
16635
  }
15262
16636
  function actionToAugmentType(action) {
@@ -15600,7 +16974,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
15600
16974
  }
15601
16975
  const type = entry.permission.type;
15602
16976
  if (!isBasicAugmentType(type)) continue;
15603
- const canonical = toCanonicalToolName$4(entry.toolName);
16977
+ const canonical = toCanonicalToolName$5(entry.toolName);
15604
16978
  if (forbiddenMapKeys.has(canonical)) {
15605
16979
  logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
15606
16980
  continue;
@@ -15653,7 +17027,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
15653
17027
  function toClaudeToolName(canonical) {
15654
17028
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
15655
17029
  }
15656
- function toCanonicalToolName$3(claudeName) {
17030
+ function toCanonicalToolName$4(claudeName) {
15657
17031
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
15658
17032
  }
15659
17033
  /**
@@ -15827,7 +17201,7 @@ function convertClaudeToRulesyncPermissions(params) {
15827
17201
  const processEntries = (entries, action) => {
15828
17202
  for (const entry of entries) {
15829
17203
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
15830
- const canonical = toCanonicalToolName$3(toolName);
17204
+ const canonical = toCanonicalToolName$4(toolName);
15831
17205
  if (!permission[canonical]) permission[canonical] = {};
15832
17206
  permission[canonical][pattern] = action;
15833
17207
  }
@@ -16454,7 +17828,7 @@ function toCursorPattern(canonical, pattern) {
16454
17828
  }
16455
17829
  return pattern;
16456
17830
  }
16457
- function toCanonicalCategory(cursorType, pattern) {
17831
+ function toCanonicalCategory$1(cursorType, pattern) {
16458
17832
  if (cursorType === "Mcp") {
16459
17833
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
16460
17834
  if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
@@ -16679,7 +18053,7 @@ function convertCursorToRulesyncPermissions(params) {
16679
18053
  const processEntries = (entries, action) => {
16680
18054
  for (const entry of entries) {
16681
18055
  const { type, pattern } = parseCursorPermissionEntry(entry);
16682
- const canonical = toCanonicalCategory(type, pattern);
18056
+ const canonical = toCanonicalCategory$1(type, pattern);
16683
18057
  if (!permission[canonical]) permission[canonical] = {};
16684
18058
  const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
16685
18059
  permission[canonical][canonicalPattern] = action;
@@ -16690,6 +18064,245 @@ function convertCursorToRulesyncPermissions(params) {
16690
18064
  return { permission };
16691
18065
  }
16692
18066
  //#endregion
18067
+ //#region src/features/permissions/devin-permissions.ts
18068
+ /**
18069
+ * Mapping from rulesync canonical tool category names to Devin Local permission
18070
+ * scope matchers.
18071
+ *
18072
+ * Devin expresses permissions with scope-based matchers — `Read(glob)`,
18073
+ * `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
18074
+ * (`mcp__server__tool`). The canonical `edit` and `write` categories both map
18075
+ * onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
18076
+ * `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
18077
+ * names (e.g. `mcp__github__list_issues`) pass through verbatim.
18078
+ *
18079
+ * @see https://docs.devin.ai/cli/reference/permissions
18080
+ */
18081
+ const CANONICAL_TO_DEVIN_SCOPE = {
18082
+ read: "Read",
18083
+ write: "Write",
18084
+ edit: "Write",
18085
+ bash: "Exec",
18086
+ webfetch: "Fetch"
18087
+ };
18088
+ /**
18089
+ * Reverse mapping from Devin scope matchers to rulesync canonical names.
18090
+ */
18091
+ const DEVIN_SCOPE_TO_CANONICAL = {
18092
+ Read: "read",
18093
+ Write: "write",
18094
+ Exec: "bash",
18095
+ Fetch: "webfetch"
18096
+ };
18097
+ function toDevinScope(canonical) {
18098
+ return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
18099
+ }
18100
+ function toCanonicalCategory(devinScope) {
18101
+ return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
18102
+ }
18103
+ /**
18104
+ * Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
18105
+ * Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
18106
+ */
18107
+ function parseDevinPermissionEntry(entry) {
18108
+ const parenIndex = entry.indexOf("(");
18109
+ if (parenIndex === -1) return {
18110
+ scope: entry,
18111
+ pattern: "*"
18112
+ };
18113
+ const scope = entry.slice(0, parenIndex);
18114
+ if (!entry.endsWith(")")) return {
18115
+ scope,
18116
+ pattern: "*"
18117
+ };
18118
+ return {
18119
+ scope,
18120
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
18121
+ };
18122
+ }
18123
+ /**
18124
+ * Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
18125
+ * the bare scope (`Read`), matching the whole scope.
18126
+ */
18127
+ function buildDevinPermissionEntry(scope, pattern) {
18128
+ if (pattern === "*") return scope;
18129
+ return `${scope}(${pattern})`;
18130
+ }
18131
+ /**
18132
+ * Permissions generator for Devin Local (native `.devin/` configuration).
18133
+ *
18134
+ * Maps rulesync permission actions onto Devin's `permissions` block inside its
18135
+ * native config file — `allow` / `deny` / `ask` arrays of scope matchers
18136
+ * (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
18137
+ * `mcp__server__tool` patterns). Devin evaluates the arrays with strict
18138
+ * precedence: `deny` is checked before `ask`, which is checked before `allow`,
18139
+ * so a deny rule always wins.
18140
+ *
18141
+ * - Project scope: `.devin/config.json`
18142
+ * - Global scope: `~/.config/devin/config.json`
18143
+ *
18144
+ * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
18145
+ * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
18146
+ * the file is never deleted; only the managed `permissions` key is rewritten.
18147
+ *
18148
+ * @see https://docs.devin.ai/cli/reference/permissions
18149
+ */
18150
+ var DevinPermissions = class DevinPermissions extends ToolPermissions {
18151
+ constructor(params) {
18152
+ super({
18153
+ ...params,
18154
+ fileContent: params.fileContent ?? "{}"
18155
+ });
18156
+ }
18157
+ /**
18158
+ * config.json may carry the MCP/hooks features' keys, so it is never deleted;
18159
+ * only the managed `permissions` key is rewritten.
18160
+ */
18161
+ isDeletable() {
18162
+ return false;
18163
+ }
18164
+ static getSettablePaths({ global = false } = {}) {
18165
+ if (global) return {
18166
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
18167
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18168
+ };
18169
+ return {
18170
+ relativeDirPath: DEVIN_DIR,
18171
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18172
+ };
18173
+ }
18174
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18175
+ const paths = DevinPermissions.getSettablePaths({ global });
18176
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
18177
+ return new DevinPermissions({
18178
+ outputRoot,
18179
+ relativeDirPath: paths.relativeDirPath,
18180
+ relativeFilePath: paths.relativeFilePath,
18181
+ fileContent,
18182
+ validate
18183
+ });
18184
+ }
18185
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
18186
+ const paths = DevinPermissions.getSettablePaths({ global });
18187
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18188
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
18189
+ let settings;
18190
+ try {
18191
+ const parsed = JSON.parse(existingContent);
18192
+ settings = isRecord(parsed) ? parsed : {};
18193
+ } catch (error) {
18194
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
18195
+ }
18196
+ const config = rulesyncPermissions.getJson();
18197
+ const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
18198
+ const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
18199
+ const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
18200
+ const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
18201
+ const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
18202
+ const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
18203
+ const mergedDeny = (0, es_toolkit.uniq)([...preserve(existingPermissions.deny), ...deny].toSorted());
18204
+ const mergedPermissions = { ...existingPermissions };
18205
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
18206
+ else delete mergedPermissions.allow;
18207
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
18208
+ else delete mergedPermissions.ask;
18209
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
18210
+ else delete mergedPermissions.deny;
18211
+ const merged = {
18212
+ ...settings,
18213
+ permissions: mergedPermissions
18214
+ };
18215
+ return new DevinPermissions({
18216
+ outputRoot,
18217
+ relativeDirPath: paths.relativeDirPath,
18218
+ relativeFilePath: paths.relativeFilePath,
18219
+ fileContent: JSON.stringify(merged, null, 2),
18220
+ validate
18221
+ });
18222
+ }
18223
+ toRulesyncPermissions() {
18224
+ let settings;
18225
+ try {
18226
+ const parsed = JSON.parse(this.getFileContent());
18227
+ settings = isRecord(parsed) ? parsed : {};
18228
+ } catch (error) {
18229
+ throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18230
+ }
18231
+ const permissions = isRecord(settings.permissions) ? settings.permissions : {};
18232
+ const config = convertDevinToRulesyncPermissions({
18233
+ allow: Array.isArray(permissions.allow) ? permissions.allow : [],
18234
+ ask: Array.isArray(permissions.ask) ? permissions.ask : [],
18235
+ deny: Array.isArray(permissions.deny) ? permissions.deny : []
18236
+ });
18237
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18238
+ }
18239
+ validate() {
18240
+ return {
18241
+ success: true,
18242
+ error: null
18243
+ };
18244
+ }
18245
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18246
+ return new DevinPermissions({
18247
+ outputRoot,
18248
+ relativeDirPath,
18249
+ relativeFilePath,
18250
+ fileContent: JSON.stringify({ permissions: {} }, null, 2),
18251
+ validate: false
18252
+ });
18253
+ }
18254
+ };
18255
+ /**
18256
+ * Convert rulesync permissions config to Devin allow/ask/deny arrays.
18257
+ */
18258
+ function convertRulesyncToDevinPermissions(config) {
18259
+ const allow = [];
18260
+ const ask = [];
18261
+ const deny = [];
18262
+ for (const [category, rules] of Object.entries(config.permission)) {
18263
+ const scope = toDevinScope(category);
18264
+ for (const [pattern, action] of Object.entries(rules)) {
18265
+ const entry = buildDevinPermissionEntry(scope, pattern);
18266
+ switch (action) {
18267
+ case "allow":
18268
+ allow.push(entry);
18269
+ break;
18270
+ case "ask":
18271
+ ask.push(entry);
18272
+ break;
18273
+ case "deny":
18274
+ deny.push(entry);
18275
+ break;
18276
+ }
18277
+ }
18278
+ }
18279
+ return {
18280
+ allow,
18281
+ ask,
18282
+ deny
18283
+ };
18284
+ }
18285
+ /**
18286
+ * Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
18287
+ * are applied allow → ask → deny so the most restrictive action wins for a
18288
+ * given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
18289
+ */
18290
+ function convertDevinToRulesyncPermissions(params) {
18291
+ const permission = {};
18292
+ const processEntries = (entries, action) => {
18293
+ for (const entry of entries) {
18294
+ const { scope, pattern } = parseDevinPermissionEntry(entry);
18295
+ if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
18296
+ const canonical = toCanonicalCategory(scope);
18297
+ (permission[canonical] ??= {})[pattern] = action;
18298
+ }
18299
+ };
18300
+ processEntries(params.allow, "allow");
18301
+ processEntries(params.ask, "ask");
18302
+ processEntries(params.deny, "deny");
18303
+ return { permission };
18304
+ }
18305
+ //#endregion
16693
18306
  //#region src/features/permissions/factorydroid-permissions.ts
16694
18307
  /**
16695
18308
  * Permissions adapter for Factory Droid.
@@ -16949,7 +18562,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16949
18562
  } catch (error) {
16950
18563
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
16951
18564
  }
16952
- const config = isRecord$1(parsed) ? { ...parsed } : {};
18565
+ const config = isRecord(parsed) ? { ...parsed } : {};
16953
18566
  config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
16954
18567
  config: rulesyncPermissions.getJson(),
16955
18568
  logger
@@ -16971,8 +18584,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16971
18584
  } catch (error) {
16972
18585
  throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16973
18586
  }
16974
- const config = isRecord$1(parsed) ? parsed : {};
16975
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
18587
+ const config = isRecord(parsed) ? parsed : {};
18588
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
16976
18589
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
16977
18590
  }
16978
18591
  validate() {
@@ -17112,7 +18725,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17112
18725
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
17113
18726
  }
17114
18727
  const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
17115
- const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18728
+ const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
17116
18729
  parsed[GROKCLI_UI_KEY] = {
17117
18730
  ...existingUi,
17118
18731
  [GROKCLI_PERMISSION_MODE_KEY]: mode
@@ -17134,7 +18747,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17134
18747
  } catch (error) {
17135
18748
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17136
18749
  }
17137
- const action = (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18750
+ const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
17138
18751
  const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
17139
18752
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
17140
18753
  }
@@ -17216,6 +18829,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
17216
18829
  }
17217
18830
  };
17218
18831
  //#endregion
18832
+ //#region src/features/permissions/junie-permissions.ts
18833
+ /**
18834
+ * JetBrains Junie CLI Action Allowlist (`allowlist.json`).
18835
+ *
18836
+ * Junie gates actions through an allowlist evaluated top-to-bottom (first match
18837
+ * wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
18838
+ * `~/.junie/allowlist.json`.
18839
+ *
18840
+ * ```json
18841
+ * {
18842
+ * "defaultBehavior": "ask",
18843
+ * "allowReadonlyCommands": true,
18844
+ * "rules": {
18845
+ * "executables": [ { "prefix": "git ", "action": "allow" } ],
18846
+ * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18847
+ * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18848
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
18849
+ * }
18850
+ * }
18851
+ * ```
18852
+ *
18853
+ * Each rule carries a literal `prefix` (matches commands that start with it) or
18854
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18855
+ * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
18856
+ *
18857
+ * Category mapping (rulesync canonical <-> Junie rule group):
18858
+ * - `bash` <-> `executables`
18859
+ * - `edit`/`write` -> `fileEditing` (imported back as `edit`)
18860
+ * - `read` <-> `readOutsideProject`
18861
+ * - `mcp` <-> `mcpTools`
18862
+ *
18863
+ * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18864
+ * (with a warning when they carry rules). The top-level `defaultBehavior` and
18865
+ * `allowReadonlyCommands` settings have no canonical equivalent: they are
18866
+ * preserved verbatim on export but not imported into the rulesync model.
18867
+ *
18868
+ * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18869
+ */
18870
+ const JUNIE_RULE_GROUPS = [
18871
+ "executables",
18872
+ "fileEditing",
18873
+ "mcpTools",
18874
+ "readOutsideProject"
18875
+ ];
18876
+ const CANONICAL_TO_JUNIE_GROUP = {
18877
+ bash: "executables",
18878
+ edit: "fileEditing",
18879
+ write: "fileEditing",
18880
+ read: "readOutsideProject",
18881
+ mcp: "mcpTools"
18882
+ };
18883
+ const JUNIE_GROUP_TO_CANONICAL = {
18884
+ executables: "bash",
18885
+ fileEditing: "edit",
18886
+ mcpTools: "mcp",
18887
+ readOutsideProject: "read"
18888
+ };
18889
+ const JUNIE_DEFAULT_BEHAVIOR = "ask";
18890
+ function isPermissionAction$1(value) {
18891
+ return PermissionActionSchema.safeParse(value).success;
18892
+ }
18893
+ /**
18894
+ * Whether a rulesync pattern uses glob syntax. Junie expresses literal
18895
+ * "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
18896
+ * containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
18897
+ */
18898
+ function isGlobPattern(pattern) {
18899
+ return /[*?[]/.test(pattern);
18900
+ }
18901
+ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18902
+ constructor(params) {
18903
+ super({
18904
+ ...params,
18905
+ fileContent: params.fileContent ?? "{}"
18906
+ });
18907
+ }
18908
+ isDeletable() {
18909
+ return false;
18910
+ }
18911
+ static getSettablePaths(_options = {}) {
18912
+ return {
18913
+ relativeDirPath: JUNIE_DIR,
18914
+ relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
18915
+ };
18916
+ }
18917
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18918
+ const paths = JuniePermissions.getSettablePaths({ global });
18919
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
18920
+ return new JuniePermissions({
18921
+ outputRoot,
18922
+ relativeDirPath: paths.relativeDirPath,
18923
+ relativeFilePath: paths.relativeFilePath,
18924
+ fileContent,
18925
+ validate
18926
+ });
18927
+ }
18928
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18929
+ const paths = JuniePermissions.getSettablePaths({ global });
18930
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18931
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
18932
+ let existing;
18933
+ try {
18934
+ const parsed = JSON.parse(existingContent);
18935
+ existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18936
+ } catch (error) {
18937
+ throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18938
+ }
18939
+ const rules = convertRulesyncToJunieRules({
18940
+ config: rulesyncPermissions.getJson(),
18941
+ logger
18942
+ });
18943
+ const merged = {
18944
+ ...existing,
18945
+ defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
18946
+ rules
18947
+ };
18948
+ return new JuniePermissions({
18949
+ outputRoot,
18950
+ relativeDirPath: paths.relativeDirPath,
18951
+ relativeFilePath: paths.relativeFilePath,
18952
+ fileContent: JSON.stringify(merged, null, 2),
18953
+ validate: true
18954
+ });
18955
+ }
18956
+ toRulesyncPermissions() {
18957
+ let allowlist;
18958
+ try {
18959
+ const parsed = JSON.parse(this.getFileContent());
18960
+ allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18961
+ } catch (error) {
18962
+ throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18963
+ }
18964
+ const config = convertJunieToRulesyncPermissions({ allowlist });
18965
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18966
+ }
18967
+ validate() {
18968
+ return {
18969
+ success: true,
18970
+ error: null
18971
+ };
18972
+ }
18973
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18974
+ return new JuniePermissions({
18975
+ outputRoot,
18976
+ relativeDirPath,
18977
+ relativeFilePath,
18978
+ fileContent: "{}",
18979
+ validate: false
18980
+ });
18981
+ }
18982
+ };
18983
+ /**
18984
+ * Convert rulesync permissions config into Junie's `rules` object. Categories
18985
+ * with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
18986
+ * they carry any rule so the gap is surfaced.
18987
+ */
18988
+ function convertRulesyncToJunieRules({ config, logger }) {
18989
+ const rules = {};
18990
+ for (const [category, patterns] of Object.entries(config.permission)) {
18991
+ const group = CANONICAL_TO_JUNIE_GROUP[category];
18992
+ if (!group) {
18993
+ if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
18994
+ continue;
18995
+ }
18996
+ for (const [pattern, action] of Object.entries(patterns)) {
18997
+ const rule = isGlobPattern(pattern) ? {
18998
+ pattern,
18999
+ action
19000
+ } : {
19001
+ prefix: pattern,
19002
+ action
19003
+ };
19004
+ (rules[group] ??= []).push(rule);
19005
+ }
19006
+ }
19007
+ return rules;
19008
+ }
19009
+ /**
19010
+ * Convert a Junie allowlist back into rulesync permissions config. The
19011
+ * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
19012
+ * canonical equivalent and are not imported.
19013
+ */
19014
+ function convertJunieToRulesyncPermissions({ allowlist }) {
19015
+ const permission = {};
19016
+ const rules = allowlist.rules;
19017
+ if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
19018
+ const list = rules[group];
19019
+ if (!Array.isArray(list)) continue;
19020
+ const category = JUNIE_GROUP_TO_CANONICAL[group];
19021
+ for (const rule of list) {
19022
+ if (!rule || typeof rule !== "object") continue;
19023
+ const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
19024
+ if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
19025
+ (permission[category] ??= {})[pattern] = rule.action;
19026
+ }
19027
+ }
19028
+ return { permission };
19029
+ }
19030
+ //#endregion
17219
19031
  //#region src/features/permissions/kilo-permissions.ts
17220
19032
  const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
17221
19033
  "allow",
@@ -17564,7 +19376,11 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
17564
19376
  "ask",
17565
19377
  "deny"
17566
19378
  ]))]);
17567
- const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)) });
19379
+ const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
19380
+ "allow",
19381
+ "ask",
19382
+ "deny"
19383
+ ]), zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)])) });
17568
19384
  var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17569
19385
  json;
17570
19386
  constructor(params) {
@@ -17667,6 +19483,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17667
19483
  }
17668
19484
  normalizePermission(permission) {
17669
19485
  if (!permission) return {};
19486
+ if (typeof permission === "string") return { "*": { "*": permission } };
17670
19487
  return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
17671
19488
  }
17672
19489
  };
@@ -17703,7 +19520,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
17703
19520
  function toQwenToolName(canonical) {
17704
19521
  return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
17705
19522
  }
17706
- function toCanonicalToolName$2(qwenName) {
19523
+ function toCanonicalToolName$3(qwenName) {
17707
19524
  return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
17708
19525
  }
17709
19526
  function parseQwenPermissionEntry(entry, options = {}) {
@@ -17897,13 +19714,242 @@ function convertQwenToRulesyncPermissions(params) {
17897
19714
  const parsed = parseQwenPermissionEntry(entry, { logger });
17898
19715
  if (!parsed.ok) {
17899
19716
  if (action === "deny") {
17900
- const canonical = toCanonicalToolName$2(parsed.toolName);
19717
+ const canonical = toCanonicalToolName$3(parsed.toolName);
17901
19718
  if (!permission[canonical]) permission[canonical] = {};
17902
19719
  permission[canonical]["*"] = action;
17903
19720
  }
17904
19721
  continue;
17905
19722
  }
17906
19723
  const { toolName, pattern } = parsed;
19724
+ const canonical = toCanonicalToolName$3(toolName);
19725
+ if (!permission[canonical]) permission[canonical] = {};
19726
+ permission[canonical][pattern] = action;
19727
+ }
19728
+ };
19729
+ processEntries(params.allow, "allow");
19730
+ processEntries(params.ask, "ask");
19731
+ processEntries(params.deny, "deny");
19732
+ return { permission };
19733
+ }
19734
+ //#endregion
19735
+ //#region src/features/permissions/reasonix-permissions.ts
19736
+ /**
19737
+ * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
19738
+ * permission-rule tool families (PascalCase).
19739
+ *
19740
+ * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
19741
+ * documented as "Claude Code-style": "Bash and file mutation approvals use
19742
+ * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
19743
+ * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
19744
+ * IDs for compatibility, but new rules are saved using these PascalCase
19745
+ * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
19746
+ * uses (the closest documented precedent for this syntax).
19747
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
19748
+ */
19749
+ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
19750
+ bash: "Bash",
19751
+ read: "Read",
19752
+ edit: "Edit",
19753
+ write: "Write",
19754
+ webfetch: "WebFetch",
19755
+ websearch: "WebSearch",
19756
+ grep: "Grep",
19757
+ glob: "Glob",
19758
+ notebookedit: "NotebookEdit",
19759
+ agent: "Agent"
19760
+ };
19761
+ /**
19762
+ * Reverse mapping from Reasonix tool names to rulesync canonical names.
19763
+ */
19764
+ const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
19765
+ function toReasonixToolName(canonical) {
19766
+ return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
19767
+ }
19768
+ function toCanonicalToolName$2(reasonixName) {
19769
+ return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
19770
+ }
19771
+ /**
19772
+ * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
19773
+ * If no parentheses, returns the tool name with "*" as the pattern.
19774
+ */
19775
+ function parseReasonixPermissionEntry(entry) {
19776
+ const parenIndex = entry.indexOf("(");
19777
+ if (parenIndex === -1) return {
19778
+ toolName: entry,
19779
+ pattern: "*"
19780
+ };
19781
+ const toolName = entry.slice(0, parenIndex);
19782
+ if (!entry.endsWith(")")) return {
19783
+ toolName,
19784
+ pattern: "*"
19785
+ };
19786
+ return {
19787
+ toolName,
19788
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
19789
+ };
19790
+ }
19791
+ /**
19792
+ * Build a Reasonix permission entry like "Bash(npm run *)".
19793
+ * If the pattern is "*", returns just the tool name.
19794
+ */
19795
+ function buildReasonixPermissionEntry(toolName, pattern) {
19796
+ if (pattern === "*") return toolName;
19797
+ return `${toolName}(${pattern})`;
19798
+ }
19799
+ function parseReasonixConfig(fileContent) {
19800
+ const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
19801
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
19802
+ return { ...parsed };
19803
+ }
19804
+ function toStringArray$1(value) {
19805
+ if (!Array.isArray(value)) return [];
19806
+ return value.filter((entry) => typeof entry === "string");
19807
+ }
19808
+ function toPermissionsTable(value) {
19809
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19810
+ return { ...value };
19811
+ }
19812
+ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19813
+ toml;
19814
+ constructor(params) {
19815
+ super(params);
19816
+ this.toml = parseReasonixConfig(this.getFileContent());
19817
+ }
19818
+ isDeletable() {
19819
+ return false;
19820
+ }
19821
+ static getSettablePaths({ global } = {}) {
19822
+ if (global) return {
19823
+ relativeDirPath: REASONIX_GLOBAL_DIR,
19824
+ relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
19825
+ };
19826
+ return {
19827
+ relativeDirPath: ".",
19828
+ relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
19829
+ };
19830
+ }
19831
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19832
+ const paths = this.getSettablePaths({ global });
19833
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
19834
+ return new ReasonixPermissions({
19835
+ outputRoot,
19836
+ relativeDirPath: paths.relativeDirPath,
19837
+ relativeFilePath: paths.relativeFilePath,
19838
+ fileContent,
19839
+ validate
19840
+ });
19841
+ }
19842
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
19843
+ const paths = this.getSettablePaths({ global });
19844
+ const parsed = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
19845
+ const config = rulesyncPermissions.getJson();
19846
+ const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
19847
+ const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
19848
+ const existingPermissions = toPermissionsTable(parsed.permissions);
19849
+ const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19850
+ const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19851
+ const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19852
+ if (logger && managedToolNames.has("Read")) {
19853
+ const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
19854
+ const { toolName } = parseReasonixPermissionEntry(entry);
19855
+ return toolName === "Read";
19856
+ });
19857
+ if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages 'Read' tool and will overwrite ${droppedReadDenyEntries.length} existing Read deny entries (possibly from ignore feature). Permissions take precedence.`);
19858
+ }
19859
+ const mergedPermissions = { ...existingPermissions };
19860
+ const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
19861
+ const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
19862
+ const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
19863
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
19864
+ else delete mergedPermissions.allow;
19865
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
19866
+ else delete mergedPermissions.ask;
19867
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19868
+ else delete mergedPermissions.deny;
19869
+ const merged = {
19870
+ ...parsed,
19871
+ permissions: mergedPermissions
19872
+ };
19873
+ const fileContent = smol_toml.stringify(merged);
19874
+ return new ReasonixPermissions({
19875
+ outputRoot,
19876
+ relativeDirPath: paths.relativeDirPath,
19877
+ relativeFilePath: paths.relativeFilePath,
19878
+ fileContent,
19879
+ validate
19880
+ });
19881
+ }
19882
+ toRulesyncPermissions() {
19883
+ const permissions = toPermissionsTable(this.toml.permissions);
19884
+ const config = convertReasonixToRulesyncPermissions({
19885
+ allow: toStringArray$1(permissions.allow),
19886
+ ask: toStringArray$1(permissions.ask),
19887
+ deny: toStringArray$1(permissions.deny)
19888
+ });
19889
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19890
+ }
19891
+ validate() {
19892
+ try {
19893
+ parseReasonixConfig(this.getFileContent());
19894
+ return {
19895
+ success: true,
19896
+ error: null
19897
+ };
19898
+ } catch (error) {
19899
+ return {
19900
+ success: false,
19901
+ error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
19902
+ };
19903
+ }
19904
+ }
19905
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
19906
+ return new ReasonixPermissions({
19907
+ outputRoot,
19908
+ relativeDirPath,
19909
+ relativeFilePath,
19910
+ fileContent: smol_toml.stringify({}),
19911
+ validate: false
19912
+ });
19913
+ }
19914
+ };
19915
+ /**
19916
+ * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
19917
+ */
19918
+ function convertRulesyncToReasonixPermissions(config) {
19919
+ const allow = [];
19920
+ const ask = [];
19921
+ const deny = [];
19922
+ for (const [category, rules] of Object.entries(config.permission)) {
19923
+ const reasonixToolName = toReasonixToolName(category);
19924
+ for (const [pattern, action] of Object.entries(rules)) {
19925
+ const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
19926
+ switch (action) {
19927
+ case "allow":
19928
+ allow.push(entry);
19929
+ break;
19930
+ case "ask":
19931
+ ask.push(entry);
19932
+ break;
19933
+ case "deny":
19934
+ deny.push(entry);
19935
+ break;
19936
+ }
19937
+ }
19938
+ }
19939
+ return {
19940
+ allow,
19941
+ ask,
19942
+ deny
19943
+ };
19944
+ }
19945
+ /**
19946
+ * Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
19947
+ */
19948
+ function convertReasonixToRulesyncPermissions(params) {
19949
+ const permission = {};
19950
+ const processEntries = (entries, action) => {
19951
+ for (const entry of entries) {
19952
+ const { toolName, pattern } = parseReasonixPermissionEntry(entry);
17907
19953
  const canonical = toCanonicalToolName$2(toolName);
17908
19954
  if (!permission[canonical]) permission[canonical] = {};
17909
19955
  permission[canonical][pattern] = action;
@@ -18016,13 +20062,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18016
20062
  } catch (error) {
18017
20063
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
18018
20064
  }
18019
- const config = isRecord$1(parsed) ? { ...parsed } : {};
20065
+ const config = isRecord(parsed) ? { ...parsed } : {};
18020
20066
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
18021
20067
  config: rulesyncPermissions.getJson(),
18022
20068
  logger
18023
20069
  });
18024
20070
  config.toolPermissions = {
18025
- ...isRecord$1(config.toolPermissions) ? { ...config.toolPermissions } : {},
20071
+ ...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
18026
20072
  ...toolPermissions
18027
20073
  };
18028
20074
  return new RovodevPermissions({
@@ -18042,8 +20088,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18042
20088
  } catch (error) {
18043
20089
  throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18044
20090
  }
18045
- const config = isRecord$1(parsed) ? parsed : {};
18046
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
20091
+ const config = isRecord(parsed) ? parsed : {};
20092
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
18047
20093
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18048
20094
  }
18049
20095
  validate() {
@@ -18121,11 +20167,11 @@ function convertBashRules(rules) {
18121
20167
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
18122
20168
  const permission = {};
18123
20169
  const bash = toolPermissions.bash;
18124
- if (isRecord$1(bash)) {
20170
+ if (isRecord(bash)) {
18125
20171
  const bashRules = {};
18126
20172
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
18127
20173
  if (Array.isArray(bash.commands)) {
18128
- for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
20174
+ for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
18129
20175
  }
18130
20176
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
18131
20177
  }
@@ -18264,21 +20310,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
18264
20310
  }
18265
20311
  };
18266
20312
  /**
18267
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
18268
- */
18269
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
18270
- const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
18271
- let parsed;
18272
- try {
18273
- parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
18274
- } catch (error) {
18275
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
18276
- }
18277
- if (parsed === void 0 || parsed === null) return {};
18278
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
18279
- return parsed;
18280
- }
18281
- /**
18282
20313
  * Resolve the active Takt provider: the top-level `provider:` value, else the
18283
20314
  * sole key in `provider_profiles`, else the `claude` default.
18284
20315
  */
@@ -18600,8 +20631,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18600
20631
  config: rulesyncPermissions.getJson(),
18601
20632
  logger
18602
20633
  });
18603
- const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
18604
- const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
20634
+ const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20635
+ const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
18605
20636
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
18606
20637
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
18607
20638
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -18626,8 +20657,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18626
20657
  } catch (error) {
18627
20658
  throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18628
20659
  }
18629
- const agents = isRecord$1(settings.agents) ? settings.agents : {};
18630
- const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
20660
+ const agents = isRecord(settings.agents) ? settings.agents : {};
20661
+ const profiles = isRecord(agents.profiles) ? agents.profiles : {};
18631
20662
  const config = convertWarpToRulesyncPermissions({
18632
20663
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
18633
20664
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -18978,6 +21009,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18978
21009
  supportsImport: true
18979
21010
  }
18980
21011
  }],
21012
+ ["devin", {
21013
+ class: DevinPermissions,
21014
+ meta: {
21015
+ supportsProject: true,
21016
+ supportsGlobal: true,
21017
+ supportsImport: true
21018
+ }
21019
+ }],
18981
21020
  ["factorydroid", {
18982
21021
  class: FactorydroidPermissions,
18983
21022
  meta: {
@@ -19010,6 +21049,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19010
21049
  supportsImport: true
19011
21050
  }
19012
21051
  }],
21052
+ ["junie", {
21053
+ class: JuniePermissions,
21054
+ meta: {
21055
+ supportsProject: true,
21056
+ supportsGlobal: true,
21057
+ supportsImport: true
21058
+ }
21059
+ }],
19013
21060
  ["kilo", {
19014
21061
  class: KiloPermissions,
19015
21062
  meta: {
@@ -19058,6 +21105,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19058
21105
  supportsImport: true
19059
21106
  }
19060
21107
  }],
21108
+ ["reasonix", {
21109
+ class: ReasonixPermissions,
21110
+ meta: {
21111
+ supportsProject: true,
21112
+ supportsGlobal: true,
21113
+ supportsImport: true
21114
+ }
21115
+ }],
19061
21116
  ["rovodev", {
19062
21117
  class: RovodevPermissions,
19063
21118
  meta: {
@@ -19610,7 +21665,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
19610
21665
  opencode: zod_mini.z.optional(zod_mini.z.looseObject({
19611
21666
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
19612
21667
  license: zod_mini.z.optional(zod_mini.z.string()),
19613
- compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
21668
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
19614
21669
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
19615
21670
  })),
19616
21671
  kilo: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -19631,7 +21686,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
19631
21686
  })),
19632
21687
  copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
19633
21688
  license: zod_mini.z.optional(zod_mini.z.string()),
19634
- "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
21689
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
21690
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
19635
21691
  })),
19636
21692
  pi: zod_mini.z.optional(zod_mini.z.looseObject({
19637
21693
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -21385,7 +23441,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
21385
23441
  });
21386
23442
  /**
21387
23443
  * Represents a GitHub Copilot skill directory.
21388
- * Skills are stored under the .github/skills directory with SKILL.md files.
23444
+ *
23445
+ * Copilot discovers project skills from `.github/skills/` and personal/global
23446
+ * skills from `~/.copilot/skills/`. Each skill is a directory containing a
23447
+ * `SKILL.md` file with `name`/`description` frontmatter.
23448
+ * https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
21389
23449
  */
21390
23450
  var CopilotSkill = class CopilotSkill extends ToolSkill {
21391
23451
  constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -21407,7 +23467,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
21407
23467
  }
21408
23468
  }
21409
23469
  static getSettablePaths(options) {
21410
- if (options?.global) throw new Error("CopilotSkill does not support global mode.");
23470
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21411
23471
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21412
23472
  }
21413
23473
  getFrontmatter() {
@@ -21522,7 +23582,8 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
21522
23582
  name: zod_mini.z.string(),
21523
23583
  description: zod_mini.z.string(),
21524
23584
  license: zod_mini.z.optional(zod_mini.z.string()),
21525
- "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())]))
23585
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
23586
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
21526
23587
  });
21527
23588
  /**
21528
23589
  * Represents a GitHub Copilot CLI skill directory.
@@ -21552,7 +23613,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21552
23613
  }
21553
23614
  }
21554
23615
  static getSettablePaths(options) {
21555
- if (options?.global) return { relativeDirPath: COPILOTCLI_SKILLS_GLOBAL_DIR_PATH };
23616
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21556
23617
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21557
23618
  }
21558
23619
  getFrontmatter() {
@@ -21580,7 +23641,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21580
23641
  const frontmatter = this.getFrontmatter();
21581
23642
  const copilotcliSection = {
21582
23643
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
21583
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
23644
+ ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
23645
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
21584
23646
  };
21585
23647
  const rulesyncFrontmatter = {
21586
23648
  name: frontmatter.name,
@@ -21606,7 +23668,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21606
23668
  name: rulesyncFrontmatter.name,
21607
23669
  description: rulesyncFrontmatter.description,
21608
23670
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
21609
- ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
23671
+ ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
23672
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
21610
23673
  };
21611
23674
  return new CopilotcliSkill({
21612
23675
  outputRoot,
@@ -22832,8 +24895,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
22832
24895
  if (!result.success) throw result.error;
22833
24896
  }
22834
24897
  }
22835
- static getSettablePaths(options) {
22836
- if (options?.global) throw new Error("KiroSkill does not support global mode.");
24898
+ static getSettablePaths(_options) {
22837
24899
  return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
22838
24900
  }
22839
24901
  getFrontmatter() {
@@ -22975,10 +25037,19 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
22975
25037
  name: zod_mini.z.string(),
22976
25038
  description: zod_mini.z.string(),
22977
25039
  license: zod_mini.z.optional(zod_mini.z.string()),
22978
- compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
25040
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
22979
25041
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
22980
25042
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
22981
25043
  });
25044
+ /**
25045
+ * Reads a top-level `compatibility` value from rulesync frontmatter, accepting
25046
+ * both the documented string form (e.g. `compatibility: opencode`) and the
25047
+ * legacy object form. Returns `undefined` for any other shape.
25048
+ */
25049
+ function readTopLevelCompatibility(value) {
25050
+ if (typeof value === "string") return value;
25051
+ if (typeof value === "object" && value !== null) return value;
25052
+ }
22982
25053
  var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
22983
25054
  constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
22984
25055
  super({
@@ -23055,7 +25126,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
23055
25126
  const opencodeSection = rulesyncFrontmatter.opencode;
23056
25127
  const looseTopLevel = rulesyncFrontmatter;
23057
25128
  const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
23058
- const topLevelCompatibility = typeof looseTopLevel.compatibility === "object" && looseTopLevel.compatibility !== null ? looseTopLevel.compatibility : void 0;
25129
+ const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
23059
25130
  const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
23060
25131
  const license = opencodeSection?.license ?? topLevelLicense;
23061
25132
  const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
@@ -24409,7 +26480,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24409
26480
  meta: {
24410
26481
  supportsProject: true,
24411
26482
  supportsSimulated: false,
24412
- supportsGlobal: false
26483
+ supportsGlobal: true
24413
26484
  }
24414
26485
  }],
24415
26486
  ["copilotcli", {
@@ -24497,7 +26568,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24497
26568
  meta: {
24498
26569
  supportsProject: true,
24499
26570
  supportsSimulated: false,
24500
- supportsGlobal: false
26571
+ supportsGlobal: true
24501
26572
  }
24502
26573
  }],
24503
26574
  ["kiro-ide", {
@@ -24505,7 +26576,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24505
26576
  meta: {
24506
26577
  supportsProject: true,
24507
26578
  supportsSimulated: false,
24508
- supportsGlobal: false
26579
+ supportsGlobal: true
24509
26580
  }
24510
26581
  }],
24511
26582
  ["opencode", {
@@ -28593,7 +30664,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28593
30664
  class: KiroCliSubagent,
28594
30665
  meta: {
28595
30666
  supportsSimulated: false,
28596
- supportsGlobal: false,
30667
+ supportsGlobal: true,
28597
30668
  filePattern: "*.json"
28598
30669
  }
28599
30670
  }],
@@ -28601,7 +30672,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28601
30672
  class: KiroIdeSubagent,
28602
30673
  meta: {
28603
30674
  supportsSimulated: false,
28604
- supportsGlobal: false,
30675
+ supportsGlobal: true,
28605
30676
  filePattern: "*.md"
28606
30677
  }
28607
30678
  }],
@@ -31138,7 +33209,7 @@ const STRATEGIES = [
31138
33209
  * plus companion `globs`/`description` fields. (`.devin/rules/` is the
31139
33210
  * pre-rebrand legacy location the tool still reads.)
31140
33211
  * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
31141
- * `~/.codeium/windsurf/memories/global_rules.md` (unchanged by the rebrand).
33212
+ * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
31142
33213
  *
31143
33214
  * Trigger inference (when no explicit devin trigger is persisted):
31144
33215
  * - Specific globs (non wildcard) → glob
@@ -31165,8 +33236,8 @@ var DevinRule = class DevinRule extends ToolRule {
31165
33236
  }
31166
33237
  static getGlobalRootPath(excludeToolDir) {
31167
33238
  return {
31168
- relativeDirPath: buildToolPath(CODEIUM_DIR, WINDSURF_MEMORIES_SUBDIR, excludeToolDir),
31169
- relativeFilePath: DEVIN_GLOBAL_RULES_FILE_NAME
33239
+ relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
33240
+ relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
31170
33241
  };
31171
33242
  }
31172
33243
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
@@ -31729,6 +33800,9 @@ var KiloRule = class KiloRule extends ToolRule {
31729
33800
  nonRoot: { relativeDirPath: buildToolPath(KILO_DIR, KILO_RULES_DIR_NAME, excludeToolDir) }
31730
33801
  };
31731
33802
  }
33803
+ static getExtraSharedWritePaths({ global = false } = {}) {
33804
+ return global ? [] : [KiloMcp.getSettablePaths({ global: false })];
33805
+ }
31732
33806
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
31733
33807
  const paths = this.getSettablePaths({ global });
31734
33808
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -31990,6 +34064,9 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
31990
34064
  nonRoot: { relativeDirPath: buildToolPath(OPENCODE_DIR, "memories", excludeToolDir) }
31991
34065
  };
31992
34066
  }
34067
+ static getExtraSharedWritePaths({ global = false } = {}) {
34068
+ return global ? [] : [OpencodeMcp.getSettablePaths({ global: false })];
34069
+ }
31993
34070
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
31994
34071
  const paths = this.getSettablePaths({ global });
31995
34072
  if (relativeFilePath === paths.root.relativeFilePath) {
@@ -32056,7 +34133,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32056
34133
  /**
32057
34134
  * Rule generator for Pi Coding Agent.
32058
34135
  *
32059
- * Pi loads instruction context only from the `AGENTS.md` / `CLAUDE.md` family —
34136
+ * Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
32060
34137
  * the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
32061
34138
  * directory tree from the current working directory. It does NOT resolve
32062
34139
  * `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
@@ -32067,6 +34144,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32067
34144
  * to map onto; their bodies are folded into the single root `AGENTS.md` by the
32068
34145
  * RulesProcessor (there is no separate non-root output location — `nonRoot` is
32069
34146
  * `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
34147
+ *
34148
+ * Pi also loads two system-prompt instruction files that rulesync does NOT emit:
34149
+ * `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
34150
+ * prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
34151
+ * `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
34152
+ * routes a designated `root` rule to a single context file and has no convention
34153
+ * for marking a rule as "replace" vs "append" the system prompt, so these
34154
+ * surfaces are documented in docs/reference/file-formats.md and left to be
34155
+ * authored by hand rather than mapped to a speculative new frontmatter flag.
32070
34156
  */
32071
34157
  var PiRule = class PiRule extends ToolRule {
32072
34158
  constructor({ fileContent, root, ...rest }) {
@@ -32657,6 +34743,37 @@ var RovodevRule = class RovodevRule extends ToolRule {
32657
34743
  toolTarget: "rovodev"
32658
34744
  });
32659
34745
  }
34746
+ /**
34747
+ * Mirror the primary `.rovodev/AGENTS.md` root rule to `./AGENTS.md` so project
34748
+ * memory stays discoverable at the repo root. Empty when `rootRule` is not the primary root.
34749
+ */
34750
+ static getRootMirrorFiles({ outputRoot, rootRule, content }) {
34751
+ if (!(rootRule instanceof RovodevRule)) return [];
34752
+ const primary = this.getSettablePaths({ global: false }).root;
34753
+ if (rootRule.getRelativeDirPath() !== primary.relativeDirPath || rootRule.getRelativeFilePath() !== primary.relativeFilePath) return [];
34754
+ return [new RovodevRule({
34755
+ outputRoot,
34756
+ relativeDirPath: ".",
34757
+ relativeFilePath: ROVODEV_RULE_FILE_NAME,
34758
+ fileContent: content,
34759
+ validate: true,
34760
+ root: true
34761
+ })];
34762
+ }
34763
+ /**
34764
+ * Globs for mirror deletion: the `./AGENTS.md` mirror (`mirrorGlob`) is deleted
34765
+ * only when the primary `.rovodev/AGENTS.md` (`primaryGlob`) still exists.
34766
+ */
34767
+ static getRootMirrorDeletionGlobs({ outputRoot }) {
34768
+ return {
34769
+ primaryGlob: (0, node_path.join)(outputRoot, ROVODEV_DIR, ROVODEV_RULE_FILE_NAME),
34770
+ mirrorGlob: (0, node_path.join)(outputRoot, ROVODEV_RULE_FILE_NAME)
34771
+ };
34772
+ }
34773
+ /** Glob for the `separate-local-file` deletion; rovodev writes it at project root, not under `.rovodev/`. */
34774
+ static getLocalRootDeletionGlob({ outputRoot, fileName }) {
34775
+ return (0, node_path.join)(outputRoot, fileName);
34776
+ }
32660
34777
  };
32661
34778
  //#endregion
32662
34779
  //#region src/features/rules/takt-rule.ts
@@ -33379,7 +35496,7 @@ var RulesProcessor = class extends FeatureProcessor {
33379
35496
  });
33380
35497
  this.applyRootRuleSections({
33381
35498
  toolRules,
33382
- meta
35499
+ factory
33383
35500
  });
33384
35501
  return [...toolRules, ...extraFiles];
33385
35502
  }
@@ -33441,31 +35558,16 @@ var RulesProcessor = class extends FeatureProcessor {
33441
35558
  * reference and conventions sections to the root rule content. Mutates the
33442
35559
  * root rule in place.
33443
35560
  */
33444
- applyRootRuleSections({ toolRules, meta }) {
35561
+ applyRootRuleSections({ toolRules, factory }) {
35562
+ const { meta } = factory;
33445
35563
  const rootRule = toolRules.find((rule) => rule.isRoot());
33446
35564
  if (!rootRule) return;
33447
35565
  const newContent = this.generateReferenceSectionFromMeta(meta, toolRules) + (!meta.createsSeparateConventionsRule && meta.additionalConventions ? this.generateAdditionalConventionsSectionFromMeta(meta) : "") + rootRule.getFileContent();
33448
35566
  rootRule.setFileContent(newContent);
33449
- if (meta.mirrorsRootToAgentsMd && !this.global) this.mirrorRootRuleToAgentsMd({
33450
- toolRules,
35567
+ if (meta.mirrorsRootToAgentsMd && !this.global && factory.class.getRootMirrorFiles) toolRules.push(...factory.class.getRootMirrorFiles({
35568
+ outputRoot: this.outputRoot,
33451
35569
  rootRule,
33452
35570
  content: newContent
33453
- });
33454
- }
33455
- /**
33456
- * Mirror the primary root rule to a project-root `AGENTS.md` for tools whose
33457
- * primary root lives in a subdirectory (rovodev: `.rovodev/AGENTS.md`).
33458
- */
33459
- mirrorRootRuleToAgentsMd({ toolRules, rootRule, content }) {
33460
- if (!(rootRule instanceof RovodevRule)) return;
33461
- const primary = RovodevRule.getSettablePaths({ global: false }).root;
33462
- if (rootRule.getRelativeDirPath() === primary.relativeDirPath && rootRule.getRelativeFilePath() === primary.relativeFilePath) toolRules.push(new RovodevRule({
33463
- outputRoot: this.outputRoot,
33464
- relativeDirPath: ".",
33465
- relativeFilePath: "AGENTS.md",
33466
- fileContent: content,
33467
- validate: true,
33468
- root: true
33469
35571
  }));
33470
35572
  }
33471
35573
  buildSkillList(skillClass) {
@@ -33691,15 +35793,19 @@ As this project's AI coding tool, you must follow the additional conventions bel
33691
35793
  const localRootToolRules = await (async () => {
33692
35794
  if (!forDeletion || this.global || factory.meta.localRootMode !== "separate-local-file" || !factory.meta.localRootFileName) return [];
33693
35795
  const fileName = factory.meta.localRootFileName;
33694
- if (factory.class === RovodevRule) return buildDeletionRulesFromPaths(await findFilesByGlobs((0, node_path.join)(this.outputRoot, fileName)));
35796
+ if (factory.class.getLocalRootDeletionGlob) return buildDeletionRulesFromPaths(await findFilesByGlobs(factory.class.getLocalRootDeletionGlob({
35797
+ outputRoot: this.outputRoot,
35798
+ fileName
35799
+ })));
33695
35800
  if (!settablePaths.root) return [];
33696
35801
  return buildDeletionRulesFromPaths(await findFilesWithFallback((0, node_path.join)(this.outputRoot, settablePaths.root.relativeDirPath ?? ".", fileName), settablePaths.alternativeRoots, (alt) => (0, node_path.join)(this.outputRoot, alt.relativeDirPath, fileName)));
33697
35802
  })();
33698
35803
  this.logger.debug(`Found ${localRootToolRules.length} local root tool rule files for deletion`);
33699
35804
  const rootMirrorDeletionRules = await (async () => {
33700
- if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || factory.class !== RovodevRule) return [];
33701
- if ((await findFilesByGlobs((0, node_path.join)(this.outputRoot, ".rovodev", "AGENTS.md"))).length === 0) return [];
33702
- return buildDeletionRulesFromPaths(await findFilesByGlobs((0, node_path.join)(this.outputRoot, "AGENTS.md")));
35805
+ if (!forDeletion || this.global || !factory.meta.mirrorsRootToAgentsMd || !factory.class.getRootMirrorDeletionGlobs) return [];
35806
+ const { primaryGlob, mirrorGlob } = factory.class.getRootMirrorDeletionGlobs({ outputRoot: this.outputRoot });
35807
+ if ((await findFilesByGlobs(primaryGlob)).length === 0) return [];
35808
+ return buildDeletionRulesFromPaths(await findFilesByGlobs(mirrorGlob));
33703
35809
  })();
33704
35810
  const nonRootToolRules = await (async () => {
33705
35811
  if (!settablePaths.nonRoot) return [];
@@ -34148,63 +36254,228 @@ function warnUnsupportedTargets(params) {
34148
36254
  async function checkRulesyncDirExists(params) {
34149
36255
  return fileExists((0, node_path.join)(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
34150
36256
  }
36257
+ function dependsOnReachable(byId, from, target) {
36258
+ const seen = /* @__PURE__ */ new Set();
36259
+ const stack = [from];
36260
+ while (stack.length > 0) {
36261
+ const current = stack.pop();
36262
+ if (current === void 0 || seen.has(current)) continue;
36263
+ seen.add(current);
36264
+ if (current === target) return true;
36265
+ for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
36266
+ }
36267
+ return false;
36268
+ }
36269
+ function assertSharedFilesOrdered(steps, byId) {
36270
+ const writersByFile = /* @__PURE__ */ new Map();
36271
+ for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
36272
+ for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
36273
+ const a = writers[i];
36274
+ const b = writers[j];
36275
+ if (!dependsOnReachable(byId, a, b) && !dependsOnReachable(byId, b, a)) throw new Error(`Generation steps '${a}' and '${b}' both write the shared file '${file}' but neither declares a 'dependsOn' the other. Add a 'dependsOn' so the read-modify-write order is fixed; otherwise one step silently drops the other's keys.`);
36276
+ }
36277
+ }
36278
+ /**
36279
+ * Topologically sort generation steps and reject ordering hazards: a shared file
36280
+ * with two writers not ordered by `dependsOn` (a silent data-loss trap), an
36281
+ * unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
36282
+ *
36283
+ * @throws Error if a shared file has unordered writers, a dependency is unknown,
36284
+ * or the dependency graph contains a cycle.
36285
+ */
36286
+ function resolveExecutionOrder(steps) {
36287
+ const byId = new Map(steps.map((step) => [step.id, step]));
36288
+ assertSharedFilesOrdered(steps, byId);
36289
+ const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
36290
+ const dependents = /* @__PURE__ */ new Map();
36291
+ for (const step of steps) for (const dep of step.dependsOn ?? []) {
36292
+ if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
36293
+ unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
36294
+ dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
36295
+ }
36296
+ const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
36297
+ const ordered = [];
36298
+ while (ready.length > 0) {
36299
+ const id = ready.shift();
36300
+ ordered.push(byId.get(id));
36301
+ for (const dependent of dependents.get(id) ?? []) {
36302
+ const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
36303
+ unresolvedDeps.set(dependent, next);
36304
+ if (next === 0) ready.push(dependent);
36305
+ }
36306
+ }
36307
+ if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36308
+ return ordered;
36309
+ }
36310
+ /**
36311
+ * The static shape of the generation step graph: which steps write which shared
36312
+ * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36313
+ * for those writers. Exported (separately from the `run` closures, which need a
36314
+ * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36315
+ * tested directly against the real graph rather than a hand-copied one. Readonly
36316
+ * so a consumer can't mutate this module-level singleton and affect every
36317
+ * subsequent `generate()` call in the process.
36318
+ */
36319
+ const GENERATION_STEP_GRAPH = [
36320
+ {
36321
+ id: "ignore",
36322
+ writesSharedFile: [".claude/settings.json", ".zed/settings.json"]
36323
+ },
36324
+ {
36325
+ id: "mcp",
36326
+ writesSharedFile: [
36327
+ ".amp/settings.json",
36328
+ ".augment/settings.json",
36329
+ ".codex/config.toml",
36330
+ ".config/amp/settings.json",
36331
+ ".config/devin/config.json",
36332
+ ".config/opencode/opencode.json",
36333
+ ".config/zed/settings.json",
36334
+ ".devin/config.json",
36335
+ ".grok/config.toml",
36336
+ ".hermes/config.yaml",
36337
+ ".qwen/settings.json",
36338
+ ".reasonix/config.toml",
36339
+ ".takt/config.yaml",
36340
+ ".vibe/config.toml",
36341
+ ".zed/settings.json",
36342
+ "kilo.json",
36343
+ "opencode.json",
36344
+ "reasonix.toml"
36345
+ ],
36346
+ dependsOn: ["ignore"]
36347
+ },
36348
+ { id: "commands" },
36349
+ { id: "subagents" },
36350
+ { id: "skills" },
36351
+ {
36352
+ id: "hooks",
36353
+ writesSharedFile: [
36354
+ ".augment/settings.json",
36355
+ ".claude/settings.json",
36356
+ ".codex/config.toml",
36357
+ ".config/devin/config.json",
36358
+ ".hermes/config.yaml",
36359
+ ".kiro/agents/default.json",
36360
+ ".qwen/settings.json",
36361
+ ".vibe/config.toml"
36362
+ ],
36363
+ dependsOn: ["ignore", "mcp"]
36364
+ },
36365
+ {
36366
+ id: "permissions",
36367
+ writesSharedFile: [
36368
+ ".amp/settings.json",
36369
+ ".augment/settings.json",
36370
+ ".claude/settings.json",
36371
+ ".codex/config.toml",
36372
+ ".config/amp/settings.json",
36373
+ ".config/devin/config.json",
36374
+ ".config/opencode/opencode.json",
36375
+ ".config/zed/settings.json",
36376
+ ".devin/config.json",
36377
+ ".grok/config.toml",
36378
+ ".hermes/config.yaml",
36379
+ ".kiro/agents/default.json",
36380
+ ".qwen/settings.json",
36381
+ ".reasonix/config.toml",
36382
+ ".takt/config.yaml",
36383
+ ".vibe/config.toml",
36384
+ ".zed/settings.json",
36385
+ "opencode.json",
36386
+ "reasonix.toml"
36387
+ ],
36388
+ dependsOn: [
36389
+ "ignore",
36390
+ "hooks",
36391
+ "mcp"
36392
+ ]
36393
+ },
36394
+ {
36395
+ id: "rules",
36396
+ writesSharedFile: ["kilo.json", "opencode.json"],
36397
+ dependsOn: [
36398
+ "mcp",
36399
+ "skills",
36400
+ "permissions"
36401
+ ]
36402
+ }
36403
+ ];
34151
36404
  /**
34152
36405
  * Generate configuration files for AI tools.
34153
36406
  * @throws Error if generation fails
34154
36407
  */
34155
36408
  async function generate(params) {
34156
36409
  const { config, logger } = params;
34157
- const ignoreResult = await generateIgnoreCore({
34158
- config,
34159
- logger
34160
- });
34161
- const mcpResult = await generateMcpCore({
34162
- config,
34163
- logger
34164
- });
34165
- const commandsResult = await generateCommandsCore({
34166
- config,
34167
- logger
34168
- });
34169
- const subagentsResult = await generateSubagentsCore({
34170
- config,
34171
- logger
34172
- });
34173
- const skillsResult = await generateSkillsCore({
34174
- config,
34175
- logger
34176
- });
34177
- const hooksResult = await generateHooksCore({
34178
- config,
34179
- logger
34180
- });
34181
- const permissionsResult = await generatePermissionsCore({
34182
- config,
34183
- logger
34184
- });
34185
- const rulesResult = await generateRulesCore({
34186
- config,
34187
- logger,
34188
- skills: skillsResult.skills
34189
- });
34190
- const hasDiff = ignoreResult.hasDiff || mcpResult.hasDiff || commandsResult.hasDiff || subagentsResult.hasDiff || skillsResult.hasDiff || hooksResult.hasDiff || permissionsResult.hasDiff || rulesResult.hasDiff;
36410
+ let skillsResult;
36411
+ const runners = {
36412
+ ignore: () => generateIgnoreCore({
36413
+ config,
36414
+ logger
36415
+ }),
36416
+ mcp: () => generateMcpCore({
36417
+ config,
36418
+ logger
36419
+ }),
36420
+ commands: () => generateCommandsCore({
36421
+ config,
36422
+ logger
36423
+ }),
36424
+ subagents: () => generateSubagentsCore({
36425
+ config,
36426
+ logger
36427
+ }),
36428
+ skills: async () => {
36429
+ skillsResult = await generateSkillsCore({
36430
+ config,
36431
+ logger
36432
+ });
36433
+ return skillsResult;
36434
+ },
36435
+ hooks: () => generateHooksCore({
36436
+ config,
36437
+ logger
36438
+ }),
36439
+ permissions: () => generatePermissionsCore({
36440
+ config,
36441
+ logger
36442
+ }),
36443
+ rules: () => generateRulesCore({
36444
+ config,
36445
+ logger,
36446
+ skills: skillsResult?.skills
36447
+ })
36448
+ };
36449
+ const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
36450
+ ...meta,
36451
+ run: runners[meta.id]
36452
+ })));
36453
+ const resultsById = /* @__PURE__ */ new Map();
36454
+ for (const step of orderedSteps) resultsById.set(step.id, await step.run());
36455
+ if (!skillsResult) throw new Error("Skills generation step did not run.");
36456
+ const get = (id) => {
36457
+ const result = resultsById.get(id);
36458
+ if (!result) throw new Error(`Missing generation result for step '${id}'.`);
36459
+ return result;
36460
+ };
36461
+ const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
34191
36462
  return {
34192
- rulesCount: rulesResult.count,
34193
- rulesPaths: rulesResult.paths,
34194
- ignoreCount: ignoreResult.count,
34195
- ignorePaths: ignoreResult.paths,
34196
- mcpCount: mcpResult.count,
34197
- mcpPaths: mcpResult.paths,
34198
- commandsCount: commandsResult.count,
34199
- commandsPaths: commandsResult.paths,
34200
- subagentsCount: subagentsResult.count,
34201
- subagentsPaths: subagentsResult.paths,
36463
+ rulesCount: get("rules").count,
36464
+ rulesPaths: get("rules").paths,
36465
+ ignoreCount: get("ignore").count,
36466
+ ignorePaths: get("ignore").paths,
36467
+ mcpCount: get("mcp").count,
36468
+ mcpPaths: get("mcp").paths,
36469
+ commandsCount: get("commands").count,
36470
+ commandsPaths: get("commands").paths,
36471
+ subagentsCount: get("subagents").count,
36472
+ subagentsPaths: get("subagents").paths,
34202
36473
  skillsCount: skillsResult.count,
34203
36474
  skillsPaths: skillsResult.paths,
34204
- hooksCount: hooksResult.count,
34205
- hooksPaths: hooksResult.paths,
34206
- permissionsCount: permissionsResult.count,
34207
- permissionsPaths: permissionsResult.paths,
36475
+ hooksCount: get("hooks").count,
36476
+ hooksPaths: get("hooks").paths,
36477
+ permissionsCount: get("permissions").count,
36478
+ permissionsPaths: get("permissions").paths,
34208
36479
  skills: skillsResult.skills,
34209
36480
  hasDiff
34210
36481
  };