rulesync 9.0.2 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -114,22 +114,22 @@ function formatError(error) {
114
114
  }
115
115
  //#endregion
116
116
  //#region src/constants/rulesync-paths.ts
117
- const { join: join$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
  /**
@@ -6750,6 +7401,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6750
7401
  }
6751
7402
  };
6752
7403
  //#endregion
7404
+ //#region src/utils/object.ts
7405
+ /**
7406
+ * Return a shallow copy of `obj` keeping only the entries whose value is
7407
+ * neither `undefined` nor `null`.
7408
+ *
7409
+ * Used to assemble generated config objects without one conditional spread per
7410
+ * optional field (which would otherwise exceed the lint complexity budget).
7411
+ */
7412
+ function compact(obj) {
7413
+ const result = {};
7414
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
7415
+ return result;
7416
+ }
7417
+ //#endregion
6753
7418
  //#region src/features/hooks/copilotcli-hooks.ts
6754
7419
  /**
6755
7420
  * GitHub Copilot CLI hooks.
@@ -6761,7 +7426,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6761
7426
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
6762
7427
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
6763
7428
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
6764
- * `notification`). Each entry supports three hook types:
7429
+ * `notification`, `preMcpToolCall`). Each entry supports three hook types:
6765
7430
  *
6766
7431
  * - `command` — the `bash` / `powershell` command-field shape with optional
6767
7432
  * `timeoutSec`, plus optional `cwd` / `env`.
@@ -6874,20 +7539,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
6874
7539
  } else if (hookType === "http") entries.push({
6875
7540
  type: "http",
6876
7541
  ...matcherPart,
6877
- ...def.url !== void 0 && def.url !== null && { url: def.url },
7542
+ ...compact({
7543
+ url: def.url,
7544
+ headers: def.headers,
7545
+ allowedEnvVars: def.allowedEnvVars
7546
+ }),
7547
+ ...timeoutPart,
7548
+ ...rest
7549
+ });
7550
+ else entries.push({
7551
+ type: "command",
7552
+ ...matcherPart,
7553
+ ...compact({
7554
+ [commandField]: def.command,
7555
+ env: def.env
7556
+ }),
6878
7557
  ...timeoutPart,
6879
7558
  ...rest
6880
7559
  });
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
7560
  }
6892
7561
  return entries;
6893
7562
  }
@@ -7296,148 +7965,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
7296
7965
  };
7297
7966
  //#endregion
7298
7967
  //#region src/features/hooks/devin-hooks.ts
7968
+ const DEVIN_CONVERTER_CONFIG = {
7969
+ supportedEvents: DEVIN_HOOK_EVENTS,
7970
+ canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
7971
+ toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
7972
+ projectDirVar: "",
7973
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
7974
+ noMatcherEvents: /* @__PURE__ */ new Set([
7975
+ "sessionStart",
7976
+ "sessionEnd",
7977
+ "stop",
7978
+ "beforeSubmitPrompt"
7979
+ ])
7980
+ };
7299
7981
  /**
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).
7982
+ * Hooks generator for Devin Local (native `.devin/` hooks).
7355
7983
  *
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.
7984
+ * Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
7985
+ * to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
7986
+ * matcher groups.
7379
7987
  *
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).
7988
+ * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
7989
+ * level IS the event map (no wrapper key).
7990
+ * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
7991
+ * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
7992
+ * features, so reads and writes merge into the existing JSON and the file is
7993
+ * never deleted in global mode.
7414
7994
  *
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.
7995
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
7420
7996
  */
7421
7997
  var DevinHooks = class DevinHooks extends ToolHooks {
7422
7998
  constructor(params) {
7423
7999
  super({
7424
8000
  ...params,
7425
- fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
8001
+ fileContent: params.fileContent ?? "{}"
7426
8002
  });
7427
8003
  }
8004
+ /**
8005
+ * The project standalone `hooks.v1.json` is owned wholesale by rulesync and
8006
+ * may be deleted as an orphan. The global `config.json` is shared with the MCP
8007
+ * and permissions features, so it must never be deleted.
8008
+ */
8009
+ isDeletable() {
8010
+ return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
8011
+ }
7428
8012
  static getSettablePaths({ global = false } = {}) {
7429
8013
  if (global) return {
7430
- relativeDirPath: CODEIUM_WINDSURF_DIR,
7431
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
8014
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
8015
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
7432
8016
  };
7433
8017
  return {
7434
- relativeDirPath: WINDSURF_DIR,
7435
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
8018
+ relativeDirPath: DEVIN_DIR,
8019
+ relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
7436
8020
  };
7437
8021
  }
7438
8022
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
7439
8023
  const paths = DevinHooks.getSettablePaths({ global });
7440
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} });
8024
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
7441
8025
  return new DevinHooks({
7442
8026
  outputRoot,
7443
8027
  relativeDirPath: paths.relativeDirPath,
@@ -7448,31 +8032,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7448
8032
  }
7449
8033
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
7450
8034
  const paths = DevinHooks.getSettablePaths({ global });
7451
- await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ hooks: {} }, null, 2));
8035
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7452
8036
  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);
8037
+ const devinHooks = canonicalToToolHooks({
8038
+ config,
8039
+ toolOverrideHooks: config.devin?.hooks,
8040
+ converterConfig: DEVIN_CONVERTER_CONFIG,
8041
+ logger
8042
+ });
8043
+ let fileContent;
8044
+ if (global) {
8045
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8046
+ let parsedSettings;
8047
+ try {
8048
+ parsedSettings = JSON.parse(existingContent);
8049
+ } catch (error) {
8050
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
7472
8051
  }
7473
- if (objects.length > 0) devinHooks[devinEvent] = objects;
8052
+ const merged = {
8053
+ ...isRecord(parsedSettings) ? parsedSettings : {},
8054
+ hooks: devinHooks
8055
+ };
8056
+ fileContent = JSON.stringify(merged, null, 2);
8057
+ } else {
8058
+ await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8059
+ fileContent = JSON.stringify(devinHooks, null, 2);
7474
8060
  }
7475
- const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
7476
8061
  return new DevinHooks({
7477
8062
  outputRoot,
7478
8063
  relativeDirPath: paths.relativeDirPath,
@@ -7488,22 +8073,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7488
8073
  } catch (error) {
7489
8074
  throw new Error(`Failed to parse Devin hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
7490
8075
  }
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
- }
8076
+ const hooks = toolHooksToCanonical({
8077
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
8078
+ converterConfig: DEVIN_CONVERTER_CONFIG
8079
+ });
7504
8080
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
7505
8081
  version: 1,
7506
- hooks: canonicalHooks
8082
+ hooks
7507
8083
  }, null, 2) });
7508
8084
  }
7509
8085
  validate() {
@@ -7517,7 +8093,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7517
8093
  outputRoot,
7518
8094
  relativeDirPath,
7519
8095
  relativeFilePath,
7520
- fileContent: JSON.stringify({ hooks: {} }, null, 2),
8096
+ fileContent: "{}",
7521
8097
  validate: false
7522
8098
  });
7523
8099
  }
@@ -7744,6 +8320,106 @@ function mergeHermesConfig(fileContent, patch) {
7744
8320
  }
7745
8321
  //#endregion
7746
8322
  //#region src/features/hooks/hermesagent-hooks.ts
8323
+ /**
8324
+ * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
8325
+ * `post_tool_call`) and therefore carry a `matcher`. Every other supported
8326
+ * canonical event maps to a Hermes lifecycle event, which never accepts a
8327
+ * `matcher`.
8328
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8329
+ */
8330
+ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
8331
+ /**
8332
+ * Filter the shared canonical hooks to the events Hermes understands and merge
8333
+ * the `hermesagent`-specific override block on top.
8334
+ */
8335
+ function buildEffectiveHooks(config, toolOverrideHooks) {
8336
+ const supported = new Set(HERMESAGENT_HOOK_EVENTS);
8337
+ const shared = {};
8338
+ for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
8339
+ return {
8340
+ ...shared,
8341
+ ...toolOverrideHooks
8342
+ };
8343
+ }
8344
+ /**
8345
+ * Convert the canonical hooks config into Hermes's native
8346
+ * `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
8347
+ *
8348
+ * Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
8349
+ * via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
8350
+ * equivalent and are skipped (the shared `HooksProcessor` already warns about
8351
+ * unsupported hook types centrally). `matcher` is only carried through for
8352
+ * `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
8353
+ * warning, mirroring how other adapters (e.g. AugmentCode) handle
8354
+ * matcher-less lifecycle events.
8355
+ */
8356
+ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
8357
+ const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
8358
+ const result = {};
8359
+ for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
8360
+ const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
8361
+ if (!nativeEvent) continue;
8362
+ const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
8363
+ const entries = [];
8364
+ for (const def of defs) {
8365
+ if ((def.type ?? "command") !== "command") continue;
8366
+ if (typeof def.command !== "string" || def.command === "") continue;
8367
+ const entry = { command: def.command };
8368
+ if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
8369
+ else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
8370
+ if (typeof def.timeout === "number") entry.timeout = def.timeout;
8371
+ entries.push(entry);
8372
+ }
8373
+ if (entries.length > 0) result[nativeEvent] = entries;
8374
+ }
8375
+ return result;
8376
+ }
8377
+ /**
8378
+ * Reverse {@link canonicalToHermesHooks}: parse Hermes's native
8379
+ * `hooks: { <event>: [...] }` map back into a canonical event → definition[]
8380
+ * record. Native events with no canonical equivalent (`pre_verify`,
8381
+ * `transform_tool_result`, ...) are skipped since there is nothing to round
8382
+ * -trip them into.
8383
+ */
8384
+ function hermesHooksToCanonical(hooks) {
8385
+ const canonical = {};
8386
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
8387
+ for (const [nativeEvent, entries] of Object.entries(hooks)) {
8388
+ if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
8389
+ const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
8390
+ if (!canonicalEvent) continue;
8391
+ const defs = [];
8392
+ for (const raw of entries) {
8393
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
8394
+ const entry = raw;
8395
+ if (typeof entry.command !== "string") continue;
8396
+ const def = {
8397
+ type: "command",
8398
+ command: entry.command
8399
+ };
8400
+ if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
8401
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout;
8402
+ defs.push(def);
8403
+ }
8404
+ if (defs.length > 0) canonical[canonicalEvent] = defs;
8405
+ }
8406
+ return canonical;
8407
+ }
8408
+ /**
8409
+ * Hermes Agent shell hooks.
8410
+ *
8411
+ * Hermes Agent registers shell-command hooks under the `hooks:` key of the
8412
+ * shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
8413
+ * global only — Hermes has no project-scoped hooks location). Hermes only runs
8414
+ * hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
8415
+ * `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
8416
+ * `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
8417
+ * silently ignored. Generation therefore maps canonical events onto the real
8418
+ * `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
8419
+ * config instead of overwriting it, since that file also holds other Hermes
8420
+ * settings (model, `mcp_servers`, `command_allowlist`, ...).
8421
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8422
+ */
7747
8423
  var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7748
8424
  static getSettablePaths() {
7749
8425
  return {
@@ -7763,6 +8439,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7763
8439
  error: null
7764
8440
  };
7765
8441
  }
8442
+ isDeletable() {
8443
+ return false;
8444
+ }
7766
8445
  shouldMergeExistingFileContent() {
7767
8446
  return true;
7768
8447
  }
@@ -7770,18 +8449,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7770
8449
  this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
7771
8450
  }
7772
8451
  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
- });
8452
+ const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8453
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8454
+ version: 1,
8455
+ hooks
8456
+ }, null, 2) });
7780
8457
  }
7781
- static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
8458
+ static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
8459
+ const config = rulesyncHooks.getJson();
7782
8460
  return new HermesagentHooks({
7783
8461
  outputRoot,
7784
- fileContent: stringifyHermesConfig({ hooks: { rulesync: rulesyncHooks.getJson() } })
8462
+ fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8463
+ config,
8464
+ toolOverrideHooks: config.hermesagent?.hooks,
8465
+ logger
8466
+ }) })
7785
8467
  });
7786
8468
  }
7787
8469
  };
@@ -8251,9 +8933,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
8251
8933
  * the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
8252
8934
  * the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
8253
8935
  *
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.)
8936
+ * (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
8937
+ * {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
8257
8938
  */
8258
8939
  var KiroCliHooks = class extends KiroHooks {
8259
8940
  static getOverrideKey() {
@@ -8261,6 +8942,203 @@ var KiroCliHooks = class extends KiroHooks {
8261
8942
  }
8262
8943
  };
8263
8944
  //#endregion
8945
+ //#region src/features/hooks/kiro-ide-hooks.ts
8946
+ /**
8947
+ * One hook entry inside the Kiro IDE v1 `hooks` array.
8948
+ *
8949
+ * `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
8950
+ * imports do not drop data they do not yet understand.
8951
+ * @see https://kiro.dev/docs/hooks/types/
8952
+ */
8953
+ const KiroIdeHookActionSchema = zod_mini.z.union([zod_mini.z.looseObject({
8954
+ type: zod_mini.z.literal("command"),
8955
+ command: zod_mini.z.optional(safeString)
8956
+ }), zod_mini.z.looseObject({
8957
+ type: zod_mini.z.literal("agent"),
8958
+ prompt: zod_mini.z.optional(safeString)
8959
+ })]);
8960
+ const KiroIdeHookEntrySchema = zod_mini.z.looseObject({
8961
+ name: zod_mini.z.optional(zod_mini.z.string()),
8962
+ description: zod_mini.z.optional(zod_mini.z.string()),
8963
+ trigger: zod_mini.z.optional(zod_mini.z.string()),
8964
+ matcher: zod_mini.z.optional(zod_mini.z.string()),
8965
+ action: zod_mini.z.optional(KiroIdeHookActionSchema),
8966
+ timeout: zod_mini.z.optional(zod_mini.z.number()),
8967
+ enabled: zod_mini.z.optional(zod_mini.z.boolean())
8968
+ });
8969
+ const KiroIdeHooksFileSchema = zod_mini.z.looseObject({
8970
+ version: zod_mini.z.optional(zod_mini.z.string()),
8971
+ hooks: zod_mini.z.optional(zod_mini.z.array(KiroIdeHookEntrySchema))
8972
+ });
8973
+ /**
8974
+ * Build the Kiro IDE hook entries for a single canonical event's definitions.
8975
+ *
8976
+ * `command`-type definitions become `{ type: "command", command }` actions and
8977
+ * `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
8978
+ * types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
8979
+ * already warns about unsupported types).
8980
+ */
8981
+ function buildKiroIdeEntriesForEvent(trigger, definitions) {
8982
+ const entries = [];
8983
+ for (const def of definitions) {
8984
+ const type = def.type ?? "command";
8985
+ let action;
8986
+ if (type === "command") {
8987
+ if (def.command === void 0) continue;
8988
+ action = {
8989
+ type: "command",
8990
+ command: def.command
8991
+ };
8992
+ } else if (type === "prompt") {
8993
+ if (def.prompt === void 0) continue;
8994
+ action = {
8995
+ type: "agent",
8996
+ prompt: def.prompt
8997
+ };
8998
+ } else continue;
8999
+ entries.push({
9000
+ name: def.name ?? trigger,
9001
+ ...def.description !== void 0 && def.description !== null && { description: def.description },
9002
+ trigger,
9003
+ ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
9004
+ action,
9005
+ ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
9006
+ enabled: true
9007
+ });
9008
+ }
9009
+ return entries;
9010
+ }
9011
+ function canonicalToKiroIdeHooks(config) {
9012
+ const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
9013
+ const sharedHooks = {};
9014
+ for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
9015
+ const effectiveHooks = {
9016
+ ...sharedHooks,
9017
+ ...config["kiro-ide"]?.hooks
9018
+ };
9019
+ const entries = [];
9020
+ for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
9021
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
9022
+ entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
9023
+ }
9024
+ return entries;
9025
+ }
9026
+ function kiroIdeHooksToCanonical(entries) {
9027
+ const canonical = {};
9028
+ for (const entry of entries) {
9029
+ if (entry.trigger === void 0 || entry.action === void 0) continue;
9030
+ const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
9031
+ if (isPrototypePollutionKey(eventName)) continue;
9032
+ const def = {};
9033
+ if (entry.action.type === "command") {
9034
+ if (!entry.action.command) continue;
9035
+ def.type = "command";
9036
+ def.command = entry.action.command;
9037
+ } else {
9038
+ if (!entry.action.prompt) continue;
9039
+ def.type = "prompt";
9040
+ def.prompt = entry.action.prompt;
9041
+ }
9042
+ if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
9043
+ if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
9044
+ if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
9045
+ if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
9046
+ (canonical[eventName] ??= []).push(def);
9047
+ }
9048
+ return canonical;
9049
+ }
9050
+ /**
9051
+ * Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
9052
+ *
9053
+ * Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
9054
+ * `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
9055
+ * `hooks` array, so rulesync emits every generated hook into one
9056
+ * `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
9057
+ * which keeps it within the single-file hooks architecture.
9058
+ *
9059
+ * This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
9060
+ * KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
9061
+ *
9062
+ * @see https://kiro.dev/docs/hooks/
9063
+ */
9064
+ var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
9065
+ constructor(params) {
9066
+ super({
9067
+ ...params,
9068
+ fileContent: params.fileContent ?? JSON.stringify({
9069
+ version: "v1",
9070
+ hooks: []
9071
+ }, null, 2)
9072
+ });
9073
+ }
9074
+ static getSettablePaths(_options = {}) {
9075
+ return {
9076
+ relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
9077
+ relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
9078
+ };
9079
+ }
9080
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9081
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9082
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
9083
+ version: "v1",
9084
+ hooks: []
9085
+ }, null, 2);
9086
+ return new KiroIdeHooks({
9087
+ outputRoot,
9088
+ relativeDirPath: paths.relativeDirPath,
9089
+ relativeFilePath: paths.relativeFilePath,
9090
+ fileContent,
9091
+ validate
9092
+ });
9093
+ }
9094
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
9095
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9096
+ const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
9097
+ const fileContent = JSON.stringify({
9098
+ version: "v1",
9099
+ hooks
9100
+ }, null, 2);
9101
+ return new KiroIdeHooks({
9102
+ outputRoot,
9103
+ relativeDirPath: paths.relativeDirPath,
9104
+ relativeFilePath: paths.relativeFilePath,
9105
+ fileContent,
9106
+ validate
9107
+ });
9108
+ }
9109
+ toRulesyncHooks() {
9110
+ let parsed;
9111
+ try {
9112
+ parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
9113
+ } catch (error) {
9114
+ throw new Error(`Failed to parse Kiro IDE hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9115
+ }
9116
+ const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
9117
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9118
+ version: 1,
9119
+ hooks
9120
+ }, null, 2) });
9121
+ }
9122
+ validate() {
9123
+ return {
9124
+ success: true,
9125
+ error: null
9126
+ };
9127
+ }
9128
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9129
+ return new KiroIdeHooks({
9130
+ outputRoot,
9131
+ relativeDirPath,
9132
+ relativeFilePath,
9133
+ fileContent: JSON.stringify({
9134
+ version: "v1",
9135
+ hooks: []
9136
+ }, null, 2),
9137
+ validate: false
9138
+ });
9139
+ }
9140
+ };
9141
+ //#endregion
8264
9142
  //#region src/features/hooks/opencode-hooks.ts
8265
9143
  var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8266
9144
  constructor(params) {
@@ -8319,6 +9197,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8319
9197
  //#endregion
8320
9198
  //#region src/features/hooks/qwencode-hooks.ts
8321
9199
  /**
9200
+ * Build a single Qwen Code hook object from a canonical hook definition.
9201
+ * Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
9202
+ * and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
9203
+ * matching upstream. `statusMessage` applies to both.
9204
+ * https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
9205
+ */
9206
+ function canonicalDefToQwencodeHook(def) {
9207
+ const type = def.type ?? "command";
9208
+ const isHttp = type === "http";
9209
+ const isCommand = type === "command";
9210
+ return {
9211
+ type,
9212
+ ...compact({
9213
+ command: def.command,
9214
+ url: def.url,
9215
+ timeout: def.timeout,
9216
+ name: def.name,
9217
+ description: def.description,
9218
+ statusMessage: def.statusMessage,
9219
+ async: isCommand ? def.async : void 0,
9220
+ env: isCommand ? def.env : void 0,
9221
+ shell: isCommand ? def.shell : void 0,
9222
+ headers: isHttp ? def.headers : void 0,
9223
+ allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
9224
+ once: isHttp ? def.once : void 0
9225
+ })
9226
+ };
9227
+ }
9228
+ /**
8322
9229
  * Convert canonical hooks config to Qwen Code format.
8323
9230
  * Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
8324
9231
  * then converts to PascalCase and Qwen Code matcher/hooks structure.
@@ -8346,16 +9253,7 @@ function canonicalToQwencodeHooks(config) {
8346
9253
  }
8347
9254
  const entries = [];
8348
9255
  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
- });
9256
+ const hooks = defs.map(canonicalDefToQwencodeHook);
8359
9257
  const sequential = defs.some((def) => def.sequential === true);
8360
9258
  const group = matcherKey ? {
8361
9259
  matcher: matcherKey,
@@ -8379,7 +9277,14 @@ const QwencodeHookEntrySchema = zod_mini.z.looseObject({
8379
9277
  url: zod_mini.z.optional(zod_mini.z.string()),
8380
9278
  timeout: zod_mini.z.optional(zod_mini.z.number()),
8381
9279
  name: zod_mini.z.optional(zod_mini.z.string()),
8382
- description: zod_mini.z.optional(zod_mini.z.string())
9280
+ description: zod_mini.z.optional(zod_mini.z.string()),
9281
+ statusMessage: zod_mini.z.optional(zod_mini.z.string()),
9282
+ async: zod_mini.z.optional(zod_mini.z.boolean()),
9283
+ env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
9284
+ shell: zod_mini.z.optional(zod_mini.z.string()),
9285
+ headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
9286
+ allowedEnvVars: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
9287
+ once: zod_mini.z.optional(zod_mini.z.boolean())
8383
9288
  });
8384
9289
  /**
8385
9290
  * A matcher group entry in a Qwen Code event array.
@@ -8398,18 +9303,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
8398
9303
  const defs = [];
8399
9304
  const hooks = entry.hooks ?? [];
8400
9305
  const sequential = entry.sequential === true;
9306
+ const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
8401
9307
  for (const h of hooks) {
8402
- const command = h.command;
8403
9308
  const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
9309
+ const isHttp = hookType === "http";
9310
+ const isCommand = hookType === "command";
8404
9311
  defs.push({
8405
9312
  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 }
9313
+ ...compact({
9314
+ command: h.command,
9315
+ url: h.url,
9316
+ timeout: h.timeout,
9317
+ name: h.name,
9318
+ description: h.description,
9319
+ statusMessage: h.statusMessage,
9320
+ async: isCommand ? h.async : void 0,
9321
+ env: isCommand ? h.env : void 0,
9322
+ shell: isCommand ? h.shell : void 0,
9323
+ headers: isHttp ? h.headers : void 0,
9324
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
9325
+ once: isHttp ? h.once : void 0,
9326
+ sequential: sequential ? true : void 0,
9327
+ matcher
9328
+ })
8413
9329
  });
8414
9330
  }
8415
9331
  return defs;
@@ -8522,6 +9438,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
8522
9438
  }
8523
9439
  };
8524
9440
  //#endregion
9441
+ //#region src/features/hooks/reasonix-hooks.ts
9442
+ /**
9443
+ * Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
9444
+ * against the tool name); it is ignored on every other event.
9445
+ */
9446
+ const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
9447
+ const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
9448
+ /**
9449
+ * Convert canonical hooks config to the Reasonix `hooks` object.
9450
+ * Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
9451
+ * then maps event names and emits flat per-event hook-entry arrays.
9452
+ */
9453
+ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
9454
+ const sharedHooks = {};
9455
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
9456
+ const effectiveHooks = {
9457
+ ...sharedHooks,
9458
+ ...toolOverrideHooks
9459
+ };
9460
+ const result = {};
9461
+ for (const [event, defs] of Object.entries(effectiveHooks)) {
9462
+ if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
9463
+ const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
9464
+ const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
9465
+ const entries = [];
9466
+ for (const def of defs) {
9467
+ if ((def.type ?? "command") !== "command") continue;
9468
+ if (typeof def.command !== "string") continue;
9469
+ const entry = { command: def.command };
9470
+ if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
9471
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
9472
+ if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
9473
+ if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
9474
+ entries.push(entry);
9475
+ }
9476
+ if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
9477
+ }
9478
+ return result;
9479
+ }
9480
+ /**
9481
+ * Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
9482
+ * back into a canonical event -> definition[] record.
9483
+ */
9484
+ function reasonixHooksToCanonical(hooks) {
9485
+ const canonical = {};
9486
+ if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
9487
+ for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
9488
+ if (!Array.isArray(rawEntries)) continue;
9489
+ const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
9490
+ const defs = [];
9491
+ for (const rawEntry of rawEntries) {
9492
+ if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
9493
+ const entry = rawEntry;
9494
+ if (typeof entry.command !== "string") continue;
9495
+ const def = {
9496
+ type: "command",
9497
+ command: entry.command
9498
+ };
9499
+ if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
9500
+ if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
9501
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
9502
+ defs.push(def);
9503
+ }
9504
+ if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
9505
+ }
9506
+ return canonical;
9507
+ }
9508
+ /**
9509
+ * Reasonix hooks adapter.
9510
+ *
9511
+ * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9512
+ * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9513
+ * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9514
+ * Only the four events documented in the upstream issue are mapped:
9515
+ * PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
9516
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9517
+ */
9518
+ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
9519
+ constructor(params) {
9520
+ super({
9521
+ ...params,
9522
+ fileContent: params.fileContent ?? "{}"
9523
+ });
9524
+ }
9525
+ isDeletable() {
9526
+ return false;
9527
+ }
9528
+ static getSettablePaths(_options = {}) {
9529
+ return {
9530
+ relativeDirPath: REASONIX_DIR,
9531
+ relativeFilePath: REASONIX_SETTINGS_FILE_NAME
9532
+ };
9533
+ }
9534
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9535
+ const paths = ReasonixHooks.getSettablePaths({ global });
9536
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9537
+ return new ReasonixHooks({
9538
+ outputRoot,
9539
+ relativeDirPath: paths.relativeDirPath,
9540
+ relativeFilePath: paths.relativeFilePath,
9541
+ fileContent,
9542
+ validate
9543
+ });
9544
+ }
9545
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9546
+ const paths = ReasonixHooks.getSettablePaths({ global });
9547
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9548
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9549
+ let settings;
9550
+ try {
9551
+ settings = JSON.parse(existingContent);
9552
+ } catch (error) {
9553
+ throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
9554
+ }
9555
+ const config = rulesyncHooks.getJson();
9556
+ const reasonixHooks = canonicalToReasonixHooks({
9557
+ config,
9558
+ toolOverrideHooks: config.reasonix?.hooks,
9559
+ logger
9560
+ });
9561
+ const merged = {
9562
+ ...settings,
9563
+ hooks: reasonixHooks
9564
+ };
9565
+ const fileContent = JSON.stringify(merged, null, 2);
9566
+ return new ReasonixHooks({
9567
+ outputRoot,
9568
+ relativeDirPath: paths.relativeDirPath,
9569
+ relativeFilePath: paths.relativeFilePath,
9570
+ fileContent,
9571
+ validate
9572
+ });
9573
+ }
9574
+ toRulesyncHooks() {
9575
+ let settings;
9576
+ try {
9577
+ settings = JSON.parse(this.getFileContent());
9578
+ } catch (error) {
9579
+ throw new Error(`Failed to parse Reasonix hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9580
+ }
9581
+ const hooks = reasonixHooksToCanonical(settings.hooks);
9582
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9583
+ version: 1,
9584
+ hooks
9585
+ }, null, 2) });
9586
+ }
9587
+ validate() {
9588
+ return {
9589
+ success: true,
9590
+ error: null
9591
+ };
9592
+ }
9593
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9594
+ return new ReasonixHooks({
9595
+ outputRoot,
9596
+ relativeDirPath,
9597
+ relativeFilePath,
9598
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9599
+ validate: false
9600
+ });
9601
+ }
9602
+ };
9603
+ //#endregion
8525
9604
  //#region src/features/hooks/vibe-hooks.ts
8526
9605
  const VIBE_DIR = ".vibe";
8527
9606
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -8747,6 +9826,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8747
9826
  //#endregion
8748
9827
  //#region src/features/hooks/hooks-processor.ts
8749
9828
  const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetTuple);
9829
+ /**
9830
+ * Event names present in the config that the target's adapter cannot emit.
9831
+ *
9832
+ * When the factory passes override-block keys through verbatim
9833
+ * (`passthroughOverrideEvents`), those keys are excluded from the check so
9834
+ * documented passthrough triggers aren't falsely reported as skipped.
9835
+ */
9836
+ function unsupportedEventNames(params) {
9837
+ const { factory, sharedHooks, effectiveHooks } = params;
9838
+ const supportedEvents = new Set(factory.supportedEvents);
9839
+ const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
9840
+ return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
9841
+ }
8750
9842
  const toolHooksFactories = /* @__PURE__ */ new Map([
8751
9843
  ["antigravity-cli", {
8752
9844
  class: AntigravityCliHooks,
@@ -8880,12 +9972,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8880
9972
  supportsGlobal: true,
8881
9973
  supportsImport: true
8882
9974
  },
8883
- supportedEvents: CLAUDE_HOOK_EVENTS,
8884
- supportedHookTypes: [
8885
- "command",
8886
- "prompt",
8887
- "http"
8888
- ],
9975
+ supportedEvents: HERMESAGENT_HOOK_EVENTS,
9976
+ supportedHookTypes: ["command"],
8889
9977
  supportsMatcher: true
8890
9978
  }],
8891
9979
  ["deepagents", {
@@ -8921,6 +10009,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8921
10009
  supportedHookTypes: ["command"],
8922
10010
  supportsMatcher: true
8923
10011
  }],
10012
+ ["kiro-ide", {
10013
+ class: KiroIdeHooks,
10014
+ meta: {
10015
+ supportsProject: true,
10016
+ supportsGlobal: true,
10017
+ supportsImport: true
10018
+ },
10019
+ supportedEvents: KIRO_IDE_HOOK_EVENTS,
10020
+ supportedHookTypes: ["command", "prompt"],
10021
+ supportsMatcher: true,
10022
+ passthroughOverrideEvents: true
10023
+ }],
8924
10024
  ["devin", {
8925
10025
  class: DevinHooks,
8926
10026
  meta: {
@@ -8929,8 +10029,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8929
10029
  supportsImport: true
8930
10030
  },
8931
10031
  supportedEvents: DEVIN_HOOK_EVENTS,
8932
- supportedHookTypes: ["command"],
8933
- supportsMatcher: false
10032
+ supportedHookTypes: ["command", "prompt"],
10033
+ supportsMatcher: true
8934
10034
  }],
8935
10035
  ["augmentcode", {
8936
10036
  class: AugmentcodeHooks,
@@ -8975,6 +10075,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8975
10075
  supportedEvents: QWENCODE_HOOK_EVENTS,
8976
10076
  supportedHookTypes: ["command"],
8977
10077
  supportsMatcher: true
10078
+ }],
10079
+ ["reasonix", {
10080
+ class: ReasonixHooks,
10081
+ meta: {
10082
+ supportsProject: true,
10083
+ supportsGlobal: true,
10084
+ supportsImport: true
10085
+ },
10086
+ supportedEvents: REASONIX_HOOK_EVENTS,
10087
+ supportedHookTypes: ["command"],
10088
+ supportsMatcher: true
8978
10089
  }]
8979
10090
  ]);
8980
10091
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -9050,8 +10161,11 @@ var HooksProcessor = class extends FeatureProcessor {
9050
10161
  ...overrideHooks
9051
10162
  };
9052
10163
  {
9053
- const supportedEvents = new Set(factory.supportedEvents);
9054
- const skipped = [...new Set(Object.keys(effectiveHooks))].filter((e) => !supportedEvents.has(e));
10164
+ const skipped = unsupportedEventNames({
10165
+ factory,
10166
+ sharedHooks,
10167
+ effectiveHooks
10168
+ });
9055
10169
  if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
9056
10170
  }
9057
10171
  {
@@ -9247,11 +10361,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
9247
10361
  }
9248
10362
  };
9249
10363
  //#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
10364
  //#region src/features/ignore/antigravity-cli-ignore.ts
9256
10365
  /**
9257
10366
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -10483,9 +11592,9 @@ function parseAmpSettingsJsonc(fileContent) {
10483
11592
  }
10484
11593
  function filterMcpServers(mcpServers) {
10485
11594
  const filtered = {};
10486
- if (!isRecord$1(mcpServers)) return filtered;
11595
+ if (!isRecord(mcpServers)) return filtered;
10487
11596
  for (const [name, config] of Object.entries(mcpServers)) {
10488
- if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
11597
+ if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
10489
11598
  const filteredConfig = {};
10490
11599
  for (const [key, value] of Object.entries(config)) {
10491
11600
  if (isPrototypePollutionKey(key)) continue;
@@ -10600,7 +11709,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10600
11709
  success: true,
10601
11710
  error: null
10602
11711
  };
10603
- if (!isRecord$1(mcpServers)) return {
11712
+ if (!isRecord(mcpServers)) return {
10604
11713
  success: false,
10605
11714
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
10606
11715
  };
@@ -10609,7 +11718,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10609
11718
  success: false,
10610
11719
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
10611
11720
  };
10612
- if (!isRecord$1(serverConfig)) return {
11721
+ if (!isRecord(serverConfig)) return {
10613
11722
  success: false,
10614
11723
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
10615
11724
  };
@@ -11130,7 +12239,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
11130
12239
  function convertFromCodexFormat(codexMcp) {
11131
12240
  const result = {};
11132
12241
  for (const [name, config] of Object.entries(codexMcp)) {
11133
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
12242
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11134
12243
  const converted = {};
11135
12244
  for (const [key, value] of Object.entries(config)) {
11136
12245
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11150,7 +12259,7 @@ function convertToCodexFormat(mcpServers) {
11150
12259
  const result = {};
11151
12260
  for (const [name, config] of Object.entries(mcpServers)) {
11152
12261
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
11153
- if (!isRecord$1(config)) continue;
12262
+ if (!isRecord(config)) continue;
11154
12263
  const converted = {};
11155
12264
  for (const [key, value] of Object.entries(config)) {
11156
12265
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11212,19 +12321,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
11212
12321
  const strippedMcpServers = rulesyncMcp.getMcpServers();
11213
12322
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
11214
12323
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
11215
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
12324
+ const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
11216
12325
  return [serverName, {
11217
12326
  ...serverConfig,
11218
- ...isRecord$1(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
12327
+ ...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
11219
12328
  }];
11220
12329
  })));
11221
12330
  const filteredMcpServers = this.removeEmptyEntries(converted);
11222
12331
  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"] : {};
12332
+ const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
11224
12333
  configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
11225
- const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12334
+ const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
11226
12335
  const serverRecord = serverConfig;
11227
- if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
12336
+ if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
11228
12337
  ...serverRecord,
11229
12338
  tools: existingServer["tools"]
11230
12339
  }];
@@ -11734,18 +12843,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
11734
12843
  //#endregion
11735
12844
  //#region src/features/mcp/devin-mcp.ts
11736
12845
  /**
11737
- * MCP generator for Devin (Cascade).
12846
+ * MCP generator for Devin Local (native `.devin/` configuration).
11738
12847
  *
11739
- * Devin reads file-based MCP configuration from:
11740
- * - Project scope: `.windsurf/mcp_config.json`
11741
- * - Global scope: `~/.codeium/windsurf/mcp_config.json`
12848
+ * Devin reads MCP servers from the `mcpServers` key of its native config file:
12849
+ * - Project scope: `.devin/config.json`
12850
+ * - Global scope: `~/.config/devin/config.json`
11742
12851
  *
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.
12852
+ * The config file is shared with the permissions feature (`permissions` key)
12853
+ * and, in global mode, the hooks feature (`hooks` key), so reads and writes
12854
+ * merge into the existing JSON rather than overwriting it, and the file is
12855
+ * never deleted. Each server is a stdio entry ({ command, args, env }) or a
12856
+ * remote entry ({ serverUrl | url, headers }), and may carry an optional
12857
+ * `disabledTools` array.
12858
+ *
12859
+ * @see https://docs.devin.ai/cli/extensibility/configuration
11749
12860
  */
11750
12861
  var DevinMcp = class DevinMcp extends ToolMcp {
11751
12862
  json;
@@ -11763,14 +12874,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
11763
12874
  throw new Error(`Failed to parse Devin MCP config at ${(0, node_path.join)(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
11764
12875
  }
11765
12876
  }
12877
+ /**
12878
+ * config.json may carry the permissions/hooks features' keys, so it is never
12879
+ * deleted; only the managed `mcpServers` key is rewritten.
12880
+ */
12881
+ isDeletable() {
12882
+ return false;
12883
+ }
11766
12884
  static getSettablePaths({ global = false } = {}) {
11767
12885
  if (global) return {
11768
- relativeDirPath: CODEIUM_WINDSURF_DIR,
11769
- relativeFilePath: DEVIN_MCP_FILE_NAME
12886
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
12887
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11770
12888
  };
11771
12889
  return {
11772
- relativeDirPath: WINDSURF_DIR,
11773
- relativeFilePath: DEVIN_MCP_FILE_NAME
12890
+ relativeDirPath: DEVIN_DIR,
12891
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11774
12892
  };
11775
12893
  }
11776
12894
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
@@ -11885,7 +13003,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
11885
13003
  };
11886
13004
  //#endregion
11887
13005
  //#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";
13006
+ const GOOSE_PLUGIN_MCP_RELATIVE_PATH = (0, node_path.join)(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
11889
13007
  function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
11890
13008
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
11891
13009
  let parsed;
@@ -11972,7 +13090,7 @@ function convertServerToGooseExtension(name, config) {
11972
13090
  function convertToGooseFormat(mcpServers) {
11973
13091
  const extensions = {};
11974
13092
  for (const [name, config] of Object.entries(mcpServers)) {
11975
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13093
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11976
13094
  extensions[name] = convertServerToGooseExtension(name, config);
11977
13095
  }
11978
13096
  return extensions;
@@ -11989,7 +13107,7 @@ function convertToGooseFormat(mcpServers) {
11989
13107
  function convertFromGooseFormat(extensions) {
11990
13108
  const result = {};
11991
13109
  for (const [name, ext] of Object.entries(extensions)) {
11992
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
13110
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
11993
13111
  const server = {};
11994
13112
  const type = typeof ext.type === "string" ? ext.type : void 0;
11995
13113
  if (type === "sse") server.type = "sse";
@@ -12007,39 +13125,102 @@ function convertFromGooseFormat(extensions) {
12007
13125
  return result;
12008
13126
  }
12009
13127
  /**
13128
+ * Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
13129
+ * open-plugins `.mcp.json` manifest.
13130
+ */
13131
+ function buildGoosePluginStdioServer(config) {
13132
+ const server = {};
13133
+ const command = config.command;
13134
+ if (Array.isArray(command)) {
13135
+ if (typeof command[0] === "string") server.command = command[0];
13136
+ const rest = command.slice(1).filter((c) => typeof c === "string");
13137
+ const args = isStringArray(config.args) ? config.args : [];
13138
+ if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
13139
+ } else if (typeof command === "string") {
13140
+ server.command = command;
13141
+ if (isStringArray(config.args)) server.args = config.args;
13142
+ }
13143
+ if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
13144
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
13145
+ return server;
13146
+ }
13147
+ /**
13148
+ * Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
13149
+ * used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
13150
+ *
13151
+ * The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
13152
+ * (http/sse/streamable_http) and `builtin` servers cannot be represented. They
13153
+ * are skipped with a warning rather than silently dropped — remote servers stay
13154
+ * global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
13155
+ */
13156
+ function convertToGoosePluginMcpServers(mcpServers, logger) {
13157
+ const result = {};
13158
+ for (const [name, config] of Object.entries(mcpServers)) {
13159
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
13160
+ const gooseType = resolveGooseType(config, resolveGooseUrl(config));
13161
+ if (gooseType !== "stdio") {
13162
+ 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.`);
13163
+ continue;
13164
+ }
13165
+ result[name] = buildGoosePluginStdioServer(config);
13166
+ }
13167
+ return result;
13168
+ }
13169
+ /**
12010
13170
  * Goose MCP servers.
12011
13171
  *
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.
13172
+ * Goose configures MCP servers in two locations:
13173
+ *
13174
+ * - **Global** (`--global`): "extensions" in the shared user config
13175
+ * `~/.config/goose/config.yaml`. That file also holds other Goose settings
13176
+ * (model, provider, ...), so generation merges the `extensions:` block into the
13177
+ * existing config instead of overwriting it, and the file is never deleted.
13178
+ * This location supports both stdio and remote (http/sse) servers.
13179
+ * - **Project**: a stdio-only open-plugin manifest at
13180
+ * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
13181
+ * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
13182
+ * Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
13183
+ * shape and cannot express `url`/`headers`, so remote servers are skipped with
13184
+ * a warning in project mode (use `--global` to sync them instead).
12017
13185
  *
12018
13186
  * @see https://block.github.io/goose/docs/getting-started/using-extensions/
13187
+ * @see https://github.com/block/goose/pull/9471
12019
13188
  */
12020
13189
  var GooseMcp = class GooseMcp extends ToolMcp {
12021
13190
  config;
12022
13191
  constructor(params) {
12023
13192
  super(params);
12024
- if (this.fileContent !== void 0) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
12025
- else this.config = {};
13193
+ if (this.fileContent === void 0) this.config = {};
13194
+ else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
13195
+ else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
13196
+ }
13197
+ parsePluginManifest(fileContent) {
13198
+ try {
13199
+ const parsed = JSON.parse(fileContent);
13200
+ return isRecord(parsed) ? parsed : {};
13201
+ } catch (error) {
13202
+ throw new Error(`Failed to parse Goose MCP manifest at ${(0, node_path.join)(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
13203
+ }
12026
13204
  }
12027
13205
  getConfig() {
12028
13206
  return this.config;
12029
13207
  }
12030
13208
  isDeletable() {
12031
- return false;
13209
+ return !this.global;
12032
13210
  }
12033
- static getSettablePaths(_options) {
12034
- return {
13211
+ static getSettablePaths({ global = false } = {}) {
13212
+ if (global) return {
12035
13213
  relativeDirPath: GOOSE_GLOBAL_DIR,
12036
13214
  relativeFilePath: GOOSE_MCP_FILE_NAME
12037
13215
  };
13216
+ return {
13217
+ relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
13218
+ relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
13219
+ };
12038
13220
  }
12039
13221
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12040
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
12041
13222
  const paths = this.getSettablePaths({ global });
12042
- const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
13223
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
12043
13224
  return new GooseMcp({
12044
13225
  outputRoot,
12045
13226
  relativeDirPath: paths.relativeDirPath,
@@ -12049,9 +13230,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12049
13230
  global
12050
13231
  });
12051
13232
  }
12052
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12053
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
13233
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
12054
13234
  const paths = this.getSettablePaths({ global });
13235
+ if (!global) {
13236
+ const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
13237
+ return new GooseMcp({
13238
+ outputRoot,
13239
+ relativeDirPath: paths.relativeDirPath,
13240
+ relativeFilePath: paths.relativeFilePath,
13241
+ fileContent: JSON.stringify({ mcpServers }, null, 2),
13242
+ validate,
13243
+ global
13244
+ });
13245
+ }
12055
13246
  const merged = {
12056
13247
  ...parseGooseConfig(await readOrInitializeFileContent((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
12057
13248
  extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
@@ -12066,7 +13257,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12066
13257
  });
12067
13258
  }
12068
13259
  toRulesyncMcp() {
12069
- const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
13260
+ if (!this.global) {
13261
+ const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
13262
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
13263
+ }
13264
+ const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
12070
13265
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
12071
13266
  }
12072
13267
  validate() {
@@ -12143,7 +13338,7 @@ function convertToGrokFormat(mcpServers) {
12143
13338
  const result = {};
12144
13339
  for (const [name, config] of Object.entries(mcpServers)) {
12145
13340
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
12146
- if (!isRecord$1(config)) continue;
13341
+ if (!isRecord(config)) continue;
12147
13342
  const converted = {};
12148
13343
  for (const [key, value] of Object.entries(config)) {
12149
13344
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12158,7 +13353,7 @@ function convertToGrokFormat(mcpServers) {
12158
13353
  function convertFromGrokFormat(grokMcp) {
12159
13354
  const result = {};
12160
13355
  for (const [name, config] of Object.entries(grokMcp)) {
12161
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13356
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12162
13357
  const converted = {};
12163
13358
  for (const [key, value] of Object.entries(config)) {
12164
13359
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12321,13 +13516,13 @@ function convertServerToHermes(config) {
12321
13516
  function convertToHermesFormat(mcpServers) {
12322
13517
  const result = {};
12323
13518
  for (const [name, config] of Object.entries(mcpServers)) {
12324
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13519
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12325
13520
  result[name] = convertServerToHermes(config);
12326
13521
  }
12327
13522
  return result;
12328
13523
  }
12329
13524
  function mergeHermesMcpServers(config, mcpServers) {
12330
- const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
13525
+ const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
12331
13526
  return {
12332
13527
  ...config,
12333
13528
  mcp_servers: {
@@ -12345,7 +13540,7 @@ function mergeHermesMcpServers(config, mcpServers) {
12345
13540
  function convertFromHermesFormat(mcpServers) {
12346
13541
  const result = {};
12347
13542
  for (const [name, config] of Object.entries(mcpServers)) {
12348
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13543
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12349
13544
  const server = {};
12350
13545
  if (typeof config.command === "string") server.command = config.command;
12351
13546
  if (isStringArray(config.args)) server.args = config.args;
@@ -12381,7 +13576,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12381
13576
  return true;
12382
13577
  }
12383
13578
  setFileContent(fileContent) {
12384
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13579
+ const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12385
13580
  this.config = merged;
12386
13581
  super.setFileContent(stringifyHermesConfig(merged));
12387
13582
  }
@@ -12421,7 +13616,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12421
13616
  });
12422
13617
  }
12423
13618
  toRulesyncMcp() {
12424
- const servers = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13619
+ const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12425
13620
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
12426
13621
  }
12427
13622
  validate() {
@@ -13256,11 +14451,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
13256
14451
  }
13257
14452
  };
13258
14453
  //#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
14454
  //#region src/features/mcp/reasonix-mcp.ts
13265
14455
  const REASONIX_PLUGIN_FIELDS = [
13266
14456
  "type",
@@ -13268,13 +14458,14 @@ const REASONIX_PLUGIN_FIELDS = [
13268
14458
  "args",
13269
14459
  "env",
13270
14460
  "url",
13271
- "headers"
14461
+ "headers",
14462
+ "trusted_read_only_tools"
13272
14463
  ];
13273
14464
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13274
14465
  toml;
13275
14466
  constructor(params) {
13276
14467
  super(params);
13277
- this.toml = parseReasonixConfig(this.fileContent);
14468
+ this.toml = parseReasonixConfig$1(this.fileContent);
13278
14469
  }
13279
14470
  getToml() {
13280
14471
  return this.toml;
@@ -13298,7 +14489,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13298
14489
  }
13299
14490
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
13300
14491
  const paths = this.getSettablePaths({ global });
13301
- const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
14492
+ const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
13302
14493
  config.plugins = normalizePluginsArray(config.plugins);
13303
14494
  return new ReasonixMcp({
13304
14495
  outputRoot,
@@ -13311,7 +14502,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13311
14502
  }
13312
14503
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13313
14504
  const paths = this.getSettablePaths({ global });
13314
- const config = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
14505
+ const config = parseReasonixConfig$1(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
13315
14506
  config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
13316
14507
  return new ReasonixMcp({
13317
14508
  outputRoot,
@@ -13328,7 +14519,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13328
14519
  }
13329
14520
  validate() {
13330
14521
  try {
13331
- parseReasonixConfig(this.fileContent);
14522
+ parseReasonixConfig$1(this.fileContent);
13332
14523
  return {
13333
14524
  success: true,
13334
14525
  error: null
@@ -13351,7 +14542,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13351
14542
  });
13352
14543
  }
13353
14544
  };
13354
- function parseReasonixConfig(fileContent) {
14545
+ function parseReasonixConfig$1(fileContent) {
13355
14546
  const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
13356
14547
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
13357
14548
  return { ...parsed };
@@ -13491,17 +14682,6 @@ var RooMcp = class RooMcp extends ToolMcp {
13491
14682
  }
13492
14683
  };
13493
14684
  //#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
14685
  //#region src/features/mcp/rovodev-mcp.ts
13506
14686
  function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
13507
14687
  const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
@@ -13595,6 +14775,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
13595
14775
  }
13596
14776
  };
13597
14777
  //#endregion
14778
+ //#region src/features/shared/takt-config.ts
14779
+ /**
14780
+ * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14781
+ *
14782
+ * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14783
+ * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14784
+ * instances are rejected for prototype-pollution hardening; a YAML mapping
14785
+ * always parses to a plain object.
14786
+ */
14787
+ function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14788
+ const configPath = (0, node_path.join)(relativeDirPath, relativeFilePath);
14789
+ let parsed;
14790
+ try {
14791
+ parsed = fileContent.trim() === "" ? {} : (0, js_yaml.load)(fileContent);
14792
+ } catch (error) {
14793
+ throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14794
+ }
14795
+ if (parsed === void 0 || parsed === null) return {};
14796
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14797
+ return parsed;
14798
+ }
14799
+ //#endregion
14800
+ //#region src/features/mcp/takt-mcp.ts
14801
+ /**
14802
+ * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
14803
+ * global).
14804
+ *
14805
+ * IMPORTANT — what this adapter can and cannot represent.
14806
+ *
14807
+ * Takt does NOT have a project- or global-level registry of MCP *server
14808
+ * definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
14809
+ * or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
14810
+ * YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
14811
+ * config loader hard-rejects unknown top-level keys
14812
+ * (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
14813
+ * would therefore both be ignored and break the user's config.
14814
+ *
14815
+ * What `config.yaml` *does* hold is the default-deny transport allowlist
14816
+ * `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
14817
+ * servers are refused regardless of how they are declared. So this adapter emits
14818
+ * the transport allowlist derived from the transports present in
14819
+ * `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
14820
+ *
14821
+ * Lossiness (documented, intentional): the per-server names, commands, env, URLs
14822
+ * and headers are NOT representable in `config.yaml` and are intentionally not
14823
+ * written. Users still declare the concrete servers in their workflow YAML
14824
+ * steps; rulesync only opens the transport gate that permits them. As a
14825
+ * corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
14826
+ * a transport allowlist and yields an empty `mcpServers` map.
14827
+ *
14828
+ * The shared `config.yaml` is merged in place: only the
14829
+ * `workflow_mcp_servers` key is set; every other top-level key (provider,
14830
+ * provider_profiles, etc.) is preserved. The file is never deleted.
14831
+ *
14832
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
14833
+ * @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
14834
+ */
14835
+ var TaktMcp = class TaktMcp extends ToolMcp {
14836
+ constructor(params) {
14837
+ super({
14838
+ ...params,
14839
+ fileContent: params.fileContent ?? ""
14840
+ });
14841
+ }
14842
+ isDeletable() {
14843
+ return false;
14844
+ }
14845
+ static getSettablePaths(_options) {
14846
+ return {
14847
+ relativeDirPath: TAKT_DIR,
14848
+ relativeFilePath: TAKT_CONFIG_FILE_NAME
14849
+ };
14850
+ }
14851
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
14852
+ const paths = TaktMcp.getSettablePaths({ global });
14853
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
14854
+ return new TaktMcp({
14855
+ outputRoot,
14856
+ relativeDirPath: paths.relativeDirPath,
14857
+ relativeFilePath: paths.relativeFilePath,
14858
+ fileContent,
14859
+ validate,
14860
+ global
14861
+ });
14862
+ }
14863
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14864
+ const paths = TaktMcp.getSettablePaths({ global });
14865
+ const config = parseTaktConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
14866
+ const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14867
+ const merged = {
14868
+ ...config,
14869
+ [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14870
+ };
14871
+ return new TaktMcp({
14872
+ outputRoot,
14873
+ relativeDirPath: paths.relativeDirPath,
14874
+ relativeFilePath: paths.relativeFilePath,
14875
+ fileContent: (0, js_yaml.dump)(merged),
14876
+ validate,
14877
+ global
14878
+ });
14879
+ }
14880
+ /**
14881
+ * A transport allowlist cannot reconstruct the per-step server definitions,
14882
+ * so import yields an empty `mcpServers` map. This keeps the round-trip honest
14883
+ * rather than fabricating placeholder servers.
14884
+ */
14885
+ toRulesyncMcp() {
14886
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
14887
+ }
14888
+ validate() {
14889
+ return {
14890
+ success: true,
14891
+ error: null
14892
+ };
14893
+ }
14894
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
14895
+ return new TaktMcp({
14896
+ outputRoot,
14897
+ relativeDirPath,
14898
+ relativeFilePath,
14899
+ fileContent: "",
14900
+ validate: false,
14901
+ global
14902
+ });
14903
+ }
14904
+ };
14905
+ /**
14906
+ * Map a rulesync MCP server onto the single Takt transport its allowlist gates.
14907
+ *
14908
+ * Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
14909
+ * folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
14910
+ * explicit transport is treated as stdio when it carries a `command`, else as a
14911
+ * remote `http` server (it must have a `url`). Returns `undefined` only when the
14912
+ * shape is too ambiguous to classify.
14913
+ */
14914
+ function transportOf(server) {
14915
+ switch (server.type ?? server.transport) {
14916
+ case "stdio":
14917
+ case "local": return "stdio";
14918
+ case "sse": return "sse";
14919
+ case "http":
14920
+ case "streamable-http":
14921
+ case "ws": return "http";
14922
+ default: break;
14923
+ }
14924
+ if (server.command !== void 0) return "stdio";
14925
+ if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
14926
+ }
14927
+ /**
14928
+ * Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
14929
+ * the rulesync servers. All three keys are emitted explicitly (default-deny made
14930
+ * visible): a transport is `true` only when at least one server uses it.
14931
+ *
14932
+ * Server entries are read defensively (record guard); prototype-pollution server
14933
+ * names are irrelevant here because no user-controlled key or value is written —
14934
+ * only the three fixed boolean keys are.
14935
+ */
14936
+ function deriveTransportAllowlist(servers) {
14937
+ const allowlist = {
14938
+ stdio: false,
14939
+ sse: false,
14940
+ http: false
14941
+ };
14942
+ for (const server of Object.values(servers)) {
14943
+ if (!isRecord(server)) continue;
14944
+ const transport = transportOf(server);
14945
+ if (transport) allowlist[transport] = true;
14946
+ }
14947
+ return allowlist;
14948
+ }
14949
+ //#endregion
13598
14950
  //#region src/features/mcp/vibe-mcp.ts
13599
14951
  const VIBE_MCP_SERVER_FIELDS = [
13600
14952
  "transport",
@@ -14055,7 +15407,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14055
15407
  ["goose", {
14056
15408
  class: GooseMcp,
14057
15409
  meta: {
14058
- supportsProject: false,
15410
+ supportsProject: true,
14059
15411
  supportsGlobal: true,
14060
15412
  supportsEnabledTools: false,
14061
15413
  supportsDisabledTools: false
@@ -14169,6 +15521,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14169
15521
  supportsDisabledTools: false
14170
15522
  }
14171
15523
  }],
15524
+ ["takt", {
15525
+ class: TaktMcp,
15526
+ meta: {
15527
+ supportsProject: true,
15528
+ supportsGlobal: true,
15529
+ supportsEnabledTools: false,
15530
+ supportsDisabledTools: false
15531
+ }
15532
+ }],
14172
15533
  ["vibe", {
14173
15534
  class: VibeMcp,
14174
15535
  meta: {
@@ -14297,7 +15658,8 @@ var McpProcessor = class extends FeatureProcessor {
14297
15658
  return await factory.class.fromRulesyncMcp({
14298
15659
  outputRoot: this.outputRoot,
14299
15660
  rulesyncMcp: filteredRulesyncMcp,
14300
- global: this.global
15661
+ global: this.global,
15662
+ logger: this.logger
14301
15663
  });
14302
15664
  }));
14303
15665
  }
@@ -14811,7 +16173,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
14811
16173
  function toAntigravityCliToolName(canonical) {
14812
16174
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
14813
16175
  }
14814
- function toCanonicalToolName$5(cliName) {
16176
+ function toCanonicalToolName$6(cliName) {
14815
16177
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
14816
16178
  }
14817
16179
  /**
@@ -14994,7 +16356,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
14994
16356
  const processEntries = (entries, action) => {
14995
16357
  for (const entry of entries) {
14996
16358
  const { toolName, pattern } = parsePermissionEntry$1(entry);
14997
- const canonical = toCanonicalToolName$5(toolName);
16359
+ const canonical = toCanonicalToolName$6(toolName);
14998
16360
  if (!permission[canonical]) permission[canonical] = {};
14999
16361
  permission[canonical][pattern] = action;
15000
16362
  }
@@ -15033,7 +16395,7 @@ const IDE_ACTION_TO_CANONICAL = {
15033
16395
  function toIdeAction(canonical) {
15034
16396
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
15035
16397
  }
15036
- function toCanonicalCategory$1(ideAction) {
16398
+ function toCanonicalCategory$2(ideAction) {
15037
16399
  return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
15038
16400
  }
15039
16401
  /**
@@ -15207,7 +16569,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
15207
16569
  const processEntries = (entries, action) => {
15208
16570
  for (const entry of entries) {
15209
16571
  const { action: ideAction, pattern } = parsePermissionEntry(entry);
15210
- const canonical = toCanonicalCategory$1(ideAction);
16572
+ const canonical = toCanonicalCategory$2(ideAction);
15211
16573
  if (!permission[canonical]) permission[canonical] = {};
15212
16574
  permission[canonical][pattern] = action;
15213
16575
  }
@@ -15256,7 +16618,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
15256
16618
  function toAugmentToolName(canonical) {
15257
16619
  return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
15258
16620
  }
15259
- function toCanonicalToolName$4(augmentName) {
16621
+ function toCanonicalToolName$5(augmentName) {
15260
16622
  return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
15261
16623
  }
15262
16624
  function actionToAugmentType(action) {
@@ -15600,7 +16962,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
15600
16962
  }
15601
16963
  const type = entry.permission.type;
15602
16964
  if (!isBasicAugmentType(type)) continue;
15603
- const canonical = toCanonicalToolName$4(entry.toolName);
16965
+ const canonical = toCanonicalToolName$5(entry.toolName);
15604
16966
  if (forbiddenMapKeys.has(canonical)) {
15605
16967
  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
16968
  continue;
@@ -15653,7 +17015,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
15653
17015
  function toClaudeToolName(canonical) {
15654
17016
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
15655
17017
  }
15656
- function toCanonicalToolName$3(claudeName) {
17018
+ function toCanonicalToolName$4(claudeName) {
15657
17019
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
15658
17020
  }
15659
17021
  /**
@@ -15827,7 +17189,7 @@ function convertClaudeToRulesyncPermissions(params) {
15827
17189
  const processEntries = (entries, action) => {
15828
17190
  for (const entry of entries) {
15829
17191
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
15830
- const canonical = toCanonicalToolName$3(toolName);
17192
+ const canonical = toCanonicalToolName$4(toolName);
15831
17193
  if (!permission[canonical]) permission[canonical] = {};
15832
17194
  permission[canonical][pattern] = action;
15833
17195
  }
@@ -16454,7 +17816,7 @@ function toCursorPattern(canonical, pattern) {
16454
17816
  }
16455
17817
  return pattern;
16456
17818
  }
16457
- function toCanonicalCategory(cursorType, pattern) {
17819
+ function toCanonicalCategory$1(cursorType, pattern) {
16458
17820
  if (cursorType === "Mcp") {
16459
17821
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
16460
17822
  if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
@@ -16679,7 +18041,7 @@ function convertCursorToRulesyncPermissions(params) {
16679
18041
  const processEntries = (entries, action) => {
16680
18042
  for (const entry of entries) {
16681
18043
  const { type, pattern } = parseCursorPermissionEntry(entry);
16682
- const canonical = toCanonicalCategory(type, pattern);
18044
+ const canonical = toCanonicalCategory$1(type, pattern);
16683
18045
  if (!permission[canonical]) permission[canonical] = {};
16684
18046
  const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
16685
18047
  permission[canonical][canonicalPattern] = action;
@@ -16690,6 +18052,245 @@ function convertCursorToRulesyncPermissions(params) {
16690
18052
  return { permission };
16691
18053
  }
16692
18054
  //#endregion
18055
+ //#region src/features/permissions/devin-permissions.ts
18056
+ /**
18057
+ * Mapping from rulesync canonical tool category names to Devin Local permission
18058
+ * scope matchers.
18059
+ *
18060
+ * Devin expresses permissions with scope-based matchers — `Read(glob)`,
18061
+ * `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
18062
+ * (`mcp__server__tool`). The canonical `edit` and `write` categories both map
18063
+ * onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
18064
+ * `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
18065
+ * names (e.g. `mcp__github__list_issues`) pass through verbatim.
18066
+ *
18067
+ * @see https://docs.devin.ai/cli/reference/permissions
18068
+ */
18069
+ const CANONICAL_TO_DEVIN_SCOPE = {
18070
+ read: "Read",
18071
+ write: "Write",
18072
+ edit: "Write",
18073
+ bash: "Exec",
18074
+ webfetch: "Fetch"
18075
+ };
18076
+ /**
18077
+ * Reverse mapping from Devin scope matchers to rulesync canonical names.
18078
+ */
18079
+ const DEVIN_SCOPE_TO_CANONICAL = {
18080
+ Read: "read",
18081
+ Write: "write",
18082
+ Exec: "bash",
18083
+ Fetch: "webfetch"
18084
+ };
18085
+ function toDevinScope(canonical) {
18086
+ return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
18087
+ }
18088
+ function toCanonicalCategory(devinScope) {
18089
+ return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
18090
+ }
18091
+ /**
18092
+ * Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
18093
+ * Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
18094
+ */
18095
+ function parseDevinPermissionEntry(entry) {
18096
+ const parenIndex = entry.indexOf("(");
18097
+ if (parenIndex === -1) return {
18098
+ scope: entry,
18099
+ pattern: "*"
18100
+ };
18101
+ const scope = entry.slice(0, parenIndex);
18102
+ if (!entry.endsWith(")")) return {
18103
+ scope,
18104
+ pattern: "*"
18105
+ };
18106
+ return {
18107
+ scope,
18108
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
18109
+ };
18110
+ }
18111
+ /**
18112
+ * Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
18113
+ * the bare scope (`Read`), matching the whole scope.
18114
+ */
18115
+ function buildDevinPermissionEntry(scope, pattern) {
18116
+ if (pattern === "*") return scope;
18117
+ return `${scope}(${pattern})`;
18118
+ }
18119
+ /**
18120
+ * Permissions generator for Devin Local (native `.devin/` configuration).
18121
+ *
18122
+ * Maps rulesync permission actions onto Devin's `permissions` block inside its
18123
+ * native config file — `allow` / `deny` / `ask` arrays of scope matchers
18124
+ * (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
18125
+ * `mcp__server__tool` patterns). Devin evaluates the arrays with strict
18126
+ * precedence: `deny` is checked before `ask`, which is checked before `allow`,
18127
+ * so a deny rule always wins.
18128
+ *
18129
+ * - Project scope: `.devin/config.json`
18130
+ * - Global scope: `~/.config/devin/config.json`
18131
+ *
18132
+ * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
18133
+ * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
18134
+ * the file is never deleted; only the managed `permissions` key is rewritten.
18135
+ *
18136
+ * @see https://docs.devin.ai/cli/reference/permissions
18137
+ */
18138
+ var DevinPermissions = class DevinPermissions extends ToolPermissions {
18139
+ constructor(params) {
18140
+ super({
18141
+ ...params,
18142
+ fileContent: params.fileContent ?? "{}"
18143
+ });
18144
+ }
18145
+ /**
18146
+ * config.json may carry the MCP/hooks features' keys, so it is never deleted;
18147
+ * only the managed `permissions` key is rewritten.
18148
+ */
18149
+ isDeletable() {
18150
+ return false;
18151
+ }
18152
+ static getSettablePaths({ global = false } = {}) {
18153
+ if (global) return {
18154
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
18155
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18156
+ };
18157
+ return {
18158
+ relativeDirPath: DEVIN_DIR,
18159
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18160
+ };
18161
+ }
18162
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18163
+ const paths = DevinPermissions.getSettablePaths({ global });
18164
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
18165
+ return new DevinPermissions({
18166
+ outputRoot,
18167
+ relativeDirPath: paths.relativeDirPath,
18168
+ relativeFilePath: paths.relativeFilePath,
18169
+ fileContent,
18170
+ validate
18171
+ });
18172
+ }
18173
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
18174
+ const paths = DevinPermissions.getSettablePaths({ global });
18175
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18176
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
18177
+ let settings;
18178
+ try {
18179
+ const parsed = JSON.parse(existingContent);
18180
+ settings = isRecord(parsed) ? parsed : {};
18181
+ } catch (error) {
18182
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
18183
+ }
18184
+ const config = rulesyncPermissions.getJson();
18185
+ const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
18186
+ const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
18187
+ const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
18188
+ const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
18189
+ const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
18190
+ const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
18191
+ const mergedDeny = (0, es_toolkit.uniq)([...preserve(existingPermissions.deny), ...deny].toSorted());
18192
+ const mergedPermissions = { ...existingPermissions };
18193
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
18194
+ else delete mergedPermissions.allow;
18195
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
18196
+ else delete mergedPermissions.ask;
18197
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
18198
+ else delete mergedPermissions.deny;
18199
+ const merged = {
18200
+ ...settings,
18201
+ permissions: mergedPermissions
18202
+ };
18203
+ return new DevinPermissions({
18204
+ outputRoot,
18205
+ relativeDirPath: paths.relativeDirPath,
18206
+ relativeFilePath: paths.relativeFilePath,
18207
+ fileContent: JSON.stringify(merged, null, 2),
18208
+ validate
18209
+ });
18210
+ }
18211
+ toRulesyncPermissions() {
18212
+ let settings;
18213
+ try {
18214
+ const parsed = JSON.parse(this.getFileContent());
18215
+ settings = isRecord(parsed) ? parsed : {};
18216
+ } catch (error) {
18217
+ throw new Error(`Failed to parse Devin permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18218
+ }
18219
+ const permissions = isRecord(settings.permissions) ? settings.permissions : {};
18220
+ const config = convertDevinToRulesyncPermissions({
18221
+ allow: Array.isArray(permissions.allow) ? permissions.allow : [],
18222
+ ask: Array.isArray(permissions.ask) ? permissions.ask : [],
18223
+ deny: Array.isArray(permissions.deny) ? permissions.deny : []
18224
+ });
18225
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18226
+ }
18227
+ validate() {
18228
+ return {
18229
+ success: true,
18230
+ error: null
18231
+ };
18232
+ }
18233
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18234
+ return new DevinPermissions({
18235
+ outputRoot,
18236
+ relativeDirPath,
18237
+ relativeFilePath,
18238
+ fileContent: JSON.stringify({ permissions: {} }, null, 2),
18239
+ validate: false
18240
+ });
18241
+ }
18242
+ };
18243
+ /**
18244
+ * Convert rulesync permissions config to Devin allow/ask/deny arrays.
18245
+ */
18246
+ function convertRulesyncToDevinPermissions(config) {
18247
+ const allow = [];
18248
+ const ask = [];
18249
+ const deny = [];
18250
+ for (const [category, rules] of Object.entries(config.permission)) {
18251
+ const scope = toDevinScope(category);
18252
+ for (const [pattern, action] of Object.entries(rules)) {
18253
+ const entry = buildDevinPermissionEntry(scope, pattern);
18254
+ switch (action) {
18255
+ case "allow":
18256
+ allow.push(entry);
18257
+ break;
18258
+ case "ask":
18259
+ ask.push(entry);
18260
+ break;
18261
+ case "deny":
18262
+ deny.push(entry);
18263
+ break;
18264
+ }
18265
+ }
18266
+ }
18267
+ return {
18268
+ allow,
18269
+ ask,
18270
+ deny
18271
+ };
18272
+ }
18273
+ /**
18274
+ * Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
18275
+ * are applied allow → ask → deny so the most restrictive action wins for a
18276
+ * given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
18277
+ */
18278
+ function convertDevinToRulesyncPermissions(params) {
18279
+ const permission = {};
18280
+ const processEntries = (entries, action) => {
18281
+ for (const entry of entries) {
18282
+ const { scope, pattern } = parseDevinPermissionEntry(entry);
18283
+ if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
18284
+ const canonical = toCanonicalCategory(scope);
18285
+ (permission[canonical] ??= {})[pattern] = action;
18286
+ }
18287
+ };
18288
+ processEntries(params.allow, "allow");
18289
+ processEntries(params.ask, "ask");
18290
+ processEntries(params.deny, "deny");
18291
+ return { permission };
18292
+ }
18293
+ //#endregion
16693
18294
  //#region src/features/permissions/factorydroid-permissions.ts
16694
18295
  /**
16695
18296
  * Permissions adapter for Factory Droid.
@@ -16949,7 +18550,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16949
18550
  } catch (error) {
16950
18551
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
16951
18552
  }
16952
- const config = isRecord$1(parsed) ? { ...parsed } : {};
18553
+ const config = isRecord(parsed) ? { ...parsed } : {};
16953
18554
  config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
16954
18555
  config: rulesyncPermissions.getJson(),
16955
18556
  logger
@@ -16971,8 +18572,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16971
18572
  } catch (error) {
16972
18573
  throw new Error(`Failed to parse Goose permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16973
18574
  }
16974
- const config = isRecord$1(parsed) ? parsed : {};
16975
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
18575
+ const config = isRecord(parsed) ? parsed : {};
18576
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
16976
18577
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
16977
18578
  }
16978
18579
  validate() {
@@ -17112,7 +18713,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17112
18713
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
17113
18714
  }
17114
18715
  const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
17115
- const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18716
+ const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
17116
18717
  parsed[GROKCLI_UI_KEY] = {
17117
18718
  ...existingUi,
17118
18719
  [GROKCLI_PERMISSION_MODE_KEY]: mode
@@ -17134,7 +18735,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17134
18735
  } catch (error) {
17135
18736
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17136
18737
  }
17137
- const action = (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18738
+ const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
17138
18739
  const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
17139
18740
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
17140
18741
  }
@@ -17216,6 +18817,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
17216
18817
  }
17217
18818
  };
17218
18819
  //#endregion
18820
+ //#region src/features/permissions/junie-permissions.ts
18821
+ /**
18822
+ * JetBrains Junie CLI Action Allowlist (`allowlist.json`).
18823
+ *
18824
+ * Junie gates actions through an allowlist evaluated top-to-bottom (first match
18825
+ * wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
18826
+ * `~/.junie/allowlist.json`.
18827
+ *
18828
+ * ```json
18829
+ * {
18830
+ * "defaultBehavior": "ask",
18831
+ * "allowReadonlyCommands": true,
18832
+ * "rules": {
18833
+ * "executables": [ { "prefix": "git ", "action": "allow" } ],
18834
+ * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18835
+ * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18836
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
18837
+ * }
18838
+ * }
18839
+ * ```
18840
+ *
18841
+ * Each rule carries a literal `prefix` (matches commands that start with it) or
18842
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18843
+ * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
18844
+ *
18845
+ * Category mapping (rulesync canonical <-> Junie rule group):
18846
+ * - `bash` <-> `executables`
18847
+ * - `edit`/`write` -> `fileEditing` (imported back as `edit`)
18848
+ * - `read` <-> `readOutsideProject`
18849
+ * - `mcp` <-> `mcpTools`
18850
+ *
18851
+ * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18852
+ * (with a warning when they carry rules). The top-level `defaultBehavior` and
18853
+ * `allowReadonlyCommands` settings have no canonical equivalent: they are
18854
+ * preserved verbatim on export but not imported into the rulesync model.
18855
+ *
18856
+ * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18857
+ */
18858
+ const JUNIE_RULE_GROUPS = [
18859
+ "executables",
18860
+ "fileEditing",
18861
+ "mcpTools",
18862
+ "readOutsideProject"
18863
+ ];
18864
+ const CANONICAL_TO_JUNIE_GROUP = {
18865
+ bash: "executables",
18866
+ edit: "fileEditing",
18867
+ write: "fileEditing",
18868
+ read: "readOutsideProject",
18869
+ mcp: "mcpTools"
18870
+ };
18871
+ const JUNIE_GROUP_TO_CANONICAL = {
18872
+ executables: "bash",
18873
+ fileEditing: "edit",
18874
+ mcpTools: "mcp",
18875
+ readOutsideProject: "read"
18876
+ };
18877
+ const JUNIE_DEFAULT_BEHAVIOR = "ask";
18878
+ function isPermissionAction$1(value) {
18879
+ return PermissionActionSchema.safeParse(value).success;
18880
+ }
18881
+ /**
18882
+ * Whether a rulesync pattern uses glob syntax. Junie expresses literal
18883
+ * "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
18884
+ * containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
18885
+ */
18886
+ function isGlobPattern(pattern) {
18887
+ return /[*?[]/.test(pattern);
18888
+ }
18889
+ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18890
+ constructor(params) {
18891
+ super({
18892
+ ...params,
18893
+ fileContent: params.fileContent ?? "{}"
18894
+ });
18895
+ }
18896
+ isDeletable() {
18897
+ return false;
18898
+ }
18899
+ static getSettablePaths(_options = {}) {
18900
+ return {
18901
+ relativeDirPath: JUNIE_DIR,
18902
+ relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
18903
+ };
18904
+ }
18905
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18906
+ const paths = JuniePermissions.getSettablePaths({ global });
18907
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
18908
+ return new JuniePermissions({
18909
+ outputRoot,
18910
+ relativeDirPath: paths.relativeDirPath,
18911
+ relativeFilePath: paths.relativeFilePath,
18912
+ fileContent,
18913
+ validate
18914
+ });
18915
+ }
18916
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18917
+ const paths = JuniePermissions.getSettablePaths({ global });
18918
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18919
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
18920
+ let existing;
18921
+ try {
18922
+ const parsed = JSON.parse(existingContent);
18923
+ existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18924
+ } catch (error) {
18925
+ throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18926
+ }
18927
+ const rules = convertRulesyncToJunieRules({
18928
+ config: rulesyncPermissions.getJson(),
18929
+ logger
18930
+ });
18931
+ const merged = {
18932
+ ...existing,
18933
+ defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
18934
+ rules
18935
+ };
18936
+ return new JuniePermissions({
18937
+ outputRoot,
18938
+ relativeDirPath: paths.relativeDirPath,
18939
+ relativeFilePath: paths.relativeFilePath,
18940
+ fileContent: JSON.stringify(merged, null, 2),
18941
+ validate: true
18942
+ });
18943
+ }
18944
+ toRulesyncPermissions() {
18945
+ let allowlist;
18946
+ try {
18947
+ const parsed = JSON.parse(this.getFileContent());
18948
+ allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18949
+ } catch (error) {
18950
+ throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18951
+ }
18952
+ const config = convertJunieToRulesyncPermissions({ allowlist });
18953
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18954
+ }
18955
+ validate() {
18956
+ return {
18957
+ success: true,
18958
+ error: null
18959
+ };
18960
+ }
18961
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18962
+ return new JuniePermissions({
18963
+ outputRoot,
18964
+ relativeDirPath,
18965
+ relativeFilePath,
18966
+ fileContent: "{}",
18967
+ validate: false
18968
+ });
18969
+ }
18970
+ };
18971
+ /**
18972
+ * Convert rulesync permissions config into Junie's `rules` object. Categories
18973
+ * with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
18974
+ * they carry any rule so the gap is surfaced.
18975
+ */
18976
+ function convertRulesyncToJunieRules({ config, logger }) {
18977
+ const rules = {};
18978
+ for (const [category, patterns] of Object.entries(config.permission)) {
18979
+ const group = CANONICAL_TO_JUNIE_GROUP[category];
18980
+ if (!group) {
18981
+ 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.`);
18982
+ continue;
18983
+ }
18984
+ for (const [pattern, action] of Object.entries(patterns)) {
18985
+ const rule = isGlobPattern(pattern) ? {
18986
+ pattern,
18987
+ action
18988
+ } : {
18989
+ prefix: pattern,
18990
+ action
18991
+ };
18992
+ (rules[group] ??= []).push(rule);
18993
+ }
18994
+ }
18995
+ return rules;
18996
+ }
18997
+ /**
18998
+ * Convert a Junie allowlist back into rulesync permissions config. The
18999
+ * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
19000
+ * canonical equivalent and are not imported.
19001
+ */
19002
+ function convertJunieToRulesyncPermissions({ allowlist }) {
19003
+ const permission = {};
19004
+ const rules = allowlist.rules;
19005
+ if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
19006
+ const list = rules[group];
19007
+ if (!Array.isArray(list)) continue;
19008
+ const category = JUNIE_GROUP_TO_CANONICAL[group];
19009
+ for (const rule of list) {
19010
+ if (!rule || typeof rule !== "object") continue;
19011
+ const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
19012
+ if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
19013
+ (permission[category] ??= {})[pattern] = rule.action;
19014
+ }
19015
+ }
19016
+ return { permission };
19017
+ }
19018
+ //#endregion
17219
19019
  //#region src/features/permissions/kilo-permissions.ts
17220
19020
  const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
17221
19021
  "allow",
@@ -17564,7 +19364,11 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
17564
19364
  "ask",
17565
19365
  "deny"
17566
19366
  ]))]);
17567
- const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)) });
19367
+ const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
19368
+ "allow",
19369
+ "ask",
19370
+ "deny"
19371
+ ]), zod_mini.z.record(zod_mini.z.string(), OpencodePermissionSchema)])) });
17568
19372
  var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17569
19373
  json;
17570
19374
  constructor(params) {
@@ -17667,6 +19471,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17667
19471
  }
17668
19472
  normalizePermission(permission) {
17669
19473
  if (!permission) return {};
19474
+ if (typeof permission === "string") return { "*": { "*": permission } };
17670
19475
  return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
17671
19476
  }
17672
19477
  };
@@ -17703,7 +19508,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
17703
19508
  function toQwenToolName(canonical) {
17704
19509
  return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
17705
19510
  }
17706
- function toCanonicalToolName$2(qwenName) {
19511
+ function toCanonicalToolName$3(qwenName) {
17707
19512
  return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
17708
19513
  }
17709
19514
  function parseQwenPermissionEntry(entry, options = {}) {
@@ -17897,13 +19702,242 @@ function convertQwenToRulesyncPermissions(params) {
17897
19702
  const parsed = parseQwenPermissionEntry(entry, { logger });
17898
19703
  if (!parsed.ok) {
17899
19704
  if (action === "deny") {
17900
- const canonical = toCanonicalToolName$2(parsed.toolName);
19705
+ const canonical = toCanonicalToolName$3(parsed.toolName);
17901
19706
  if (!permission[canonical]) permission[canonical] = {};
17902
19707
  permission[canonical]["*"] = action;
17903
19708
  }
17904
19709
  continue;
17905
19710
  }
17906
19711
  const { toolName, pattern } = parsed;
19712
+ const canonical = toCanonicalToolName$3(toolName);
19713
+ if (!permission[canonical]) permission[canonical] = {};
19714
+ permission[canonical][pattern] = action;
19715
+ }
19716
+ };
19717
+ processEntries(params.allow, "allow");
19718
+ processEntries(params.ask, "ask");
19719
+ processEntries(params.deny, "deny");
19720
+ return { permission };
19721
+ }
19722
+ //#endregion
19723
+ //#region src/features/permissions/reasonix-permissions.ts
19724
+ /**
19725
+ * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
19726
+ * permission-rule tool families (PascalCase).
19727
+ *
19728
+ * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
19729
+ * documented as "Claude Code-style": "Bash and file mutation approvals use
19730
+ * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
19731
+ * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
19732
+ * IDs for compatibility, but new rules are saved using these PascalCase
19733
+ * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
19734
+ * uses (the closest documented precedent for this syntax).
19735
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
19736
+ */
19737
+ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
19738
+ bash: "Bash",
19739
+ read: "Read",
19740
+ edit: "Edit",
19741
+ write: "Write",
19742
+ webfetch: "WebFetch",
19743
+ websearch: "WebSearch",
19744
+ grep: "Grep",
19745
+ glob: "Glob",
19746
+ notebookedit: "NotebookEdit",
19747
+ agent: "Agent"
19748
+ };
19749
+ /**
19750
+ * Reverse mapping from Reasonix tool names to rulesync canonical names.
19751
+ */
19752
+ const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
19753
+ function toReasonixToolName(canonical) {
19754
+ return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
19755
+ }
19756
+ function toCanonicalToolName$2(reasonixName) {
19757
+ return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
19758
+ }
19759
+ /**
19760
+ * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
19761
+ * If no parentheses, returns the tool name with "*" as the pattern.
19762
+ */
19763
+ function parseReasonixPermissionEntry(entry) {
19764
+ const parenIndex = entry.indexOf("(");
19765
+ if (parenIndex === -1) return {
19766
+ toolName: entry,
19767
+ pattern: "*"
19768
+ };
19769
+ const toolName = entry.slice(0, parenIndex);
19770
+ if (!entry.endsWith(")")) return {
19771
+ toolName,
19772
+ pattern: "*"
19773
+ };
19774
+ return {
19775
+ toolName,
19776
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
19777
+ };
19778
+ }
19779
+ /**
19780
+ * Build a Reasonix permission entry like "Bash(npm run *)".
19781
+ * If the pattern is "*", returns just the tool name.
19782
+ */
19783
+ function buildReasonixPermissionEntry(toolName, pattern) {
19784
+ if (pattern === "*") return toolName;
19785
+ return `${toolName}(${pattern})`;
19786
+ }
19787
+ function parseReasonixConfig(fileContent) {
19788
+ const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
19789
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
19790
+ return { ...parsed };
19791
+ }
19792
+ function toStringArray$1(value) {
19793
+ if (!Array.isArray(value)) return [];
19794
+ return value.filter((entry) => typeof entry === "string");
19795
+ }
19796
+ function toPermissionsTable(value) {
19797
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19798
+ return { ...value };
19799
+ }
19800
+ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19801
+ toml;
19802
+ constructor(params) {
19803
+ super(params);
19804
+ this.toml = parseReasonixConfig(this.getFileContent());
19805
+ }
19806
+ isDeletable() {
19807
+ return false;
19808
+ }
19809
+ static getSettablePaths({ global } = {}) {
19810
+ if (global) return {
19811
+ relativeDirPath: REASONIX_GLOBAL_DIR,
19812
+ relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
19813
+ };
19814
+ return {
19815
+ relativeDirPath: ".",
19816
+ relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
19817
+ };
19818
+ }
19819
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19820
+ const paths = this.getSettablePaths({ global });
19821
+ const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({});
19822
+ return new ReasonixPermissions({
19823
+ outputRoot,
19824
+ relativeDirPath: paths.relativeDirPath,
19825
+ relativeFilePath: paths.relativeFilePath,
19826
+ fileContent,
19827
+ validate
19828
+ });
19829
+ }
19830
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
19831
+ const paths = this.getSettablePaths({ global });
19832
+ const parsed = parseReasonixConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smol_toml.stringify({}));
19833
+ const config = rulesyncPermissions.getJson();
19834
+ const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
19835
+ const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
19836
+ const existingPermissions = toPermissionsTable(parsed.permissions);
19837
+ const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19838
+ const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19839
+ const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19840
+ if (logger && managedToolNames.has("Read")) {
19841
+ const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
19842
+ const { toolName } = parseReasonixPermissionEntry(entry);
19843
+ return toolName === "Read";
19844
+ });
19845
+ 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.`);
19846
+ }
19847
+ const mergedPermissions = { ...existingPermissions };
19848
+ const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
19849
+ const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
19850
+ const mergedDeny = (0, es_toolkit.uniq)([...preservedDeny, ...deny].toSorted());
19851
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
19852
+ else delete mergedPermissions.allow;
19853
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
19854
+ else delete mergedPermissions.ask;
19855
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19856
+ else delete mergedPermissions.deny;
19857
+ const merged = {
19858
+ ...parsed,
19859
+ permissions: mergedPermissions
19860
+ };
19861
+ const fileContent = smol_toml.stringify(merged);
19862
+ return new ReasonixPermissions({
19863
+ outputRoot,
19864
+ relativeDirPath: paths.relativeDirPath,
19865
+ relativeFilePath: paths.relativeFilePath,
19866
+ fileContent,
19867
+ validate
19868
+ });
19869
+ }
19870
+ toRulesyncPermissions() {
19871
+ const permissions = toPermissionsTable(this.toml.permissions);
19872
+ const config = convertReasonixToRulesyncPermissions({
19873
+ allow: toStringArray$1(permissions.allow),
19874
+ ask: toStringArray$1(permissions.ask),
19875
+ deny: toStringArray$1(permissions.deny)
19876
+ });
19877
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19878
+ }
19879
+ validate() {
19880
+ try {
19881
+ parseReasonixConfig(this.getFileContent());
19882
+ return {
19883
+ success: true,
19884
+ error: null
19885
+ };
19886
+ } catch (error) {
19887
+ return {
19888
+ success: false,
19889
+ error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
19890
+ };
19891
+ }
19892
+ }
19893
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
19894
+ return new ReasonixPermissions({
19895
+ outputRoot,
19896
+ relativeDirPath,
19897
+ relativeFilePath,
19898
+ fileContent: smol_toml.stringify({}),
19899
+ validate: false
19900
+ });
19901
+ }
19902
+ };
19903
+ /**
19904
+ * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
19905
+ */
19906
+ function convertRulesyncToReasonixPermissions(config) {
19907
+ const allow = [];
19908
+ const ask = [];
19909
+ const deny = [];
19910
+ for (const [category, rules] of Object.entries(config.permission)) {
19911
+ const reasonixToolName = toReasonixToolName(category);
19912
+ for (const [pattern, action] of Object.entries(rules)) {
19913
+ const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
19914
+ switch (action) {
19915
+ case "allow":
19916
+ allow.push(entry);
19917
+ break;
19918
+ case "ask":
19919
+ ask.push(entry);
19920
+ break;
19921
+ case "deny":
19922
+ deny.push(entry);
19923
+ break;
19924
+ }
19925
+ }
19926
+ }
19927
+ return {
19928
+ allow,
19929
+ ask,
19930
+ deny
19931
+ };
19932
+ }
19933
+ /**
19934
+ * Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
19935
+ */
19936
+ function convertReasonixToRulesyncPermissions(params) {
19937
+ const permission = {};
19938
+ const processEntries = (entries, action) => {
19939
+ for (const entry of entries) {
19940
+ const { toolName, pattern } = parseReasonixPermissionEntry(entry);
17907
19941
  const canonical = toCanonicalToolName$2(toolName);
17908
19942
  if (!permission[canonical]) permission[canonical] = {};
17909
19943
  permission[canonical][pattern] = action;
@@ -18016,13 +20050,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18016
20050
  } catch (error) {
18017
20051
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
18018
20052
  }
18019
- const config = isRecord$1(parsed) ? { ...parsed } : {};
20053
+ const config = isRecord(parsed) ? { ...parsed } : {};
18020
20054
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
18021
20055
  config: rulesyncPermissions.getJson(),
18022
20056
  logger
18023
20057
  });
18024
20058
  config.toolPermissions = {
18025
- ...isRecord$1(config.toolPermissions) ? { ...config.toolPermissions } : {},
20059
+ ...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
18026
20060
  ...toolPermissions
18027
20061
  };
18028
20062
  return new RovodevPermissions({
@@ -18042,8 +20076,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18042
20076
  } catch (error) {
18043
20077
  throw new Error(`Failed to parse Rovodev permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18044
20078
  }
18045
- const config = isRecord$1(parsed) ? parsed : {};
18046
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
20079
+ const config = isRecord(parsed) ? parsed : {};
20080
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
18047
20081
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18048
20082
  }
18049
20083
  validate() {
@@ -18121,11 +20155,11 @@ function convertBashRules(rules) {
18121
20155
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
18122
20156
  const permission = {};
18123
20157
  const bash = toolPermissions.bash;
18124
- if (isRecord$1(bash)) {
20158
+ if (isRecord(bash)) {
18125
20159
  const bashRules = {};
18126
20160
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
18127
20161
  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;
20162
+ for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
18129
20163
  }
18130
20164
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
18131
20165
  }
@@ -18264,21 +20298,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
18264
20298
  }
18265
20299
  };
18266
20300
  /**
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
20301
  * Resolve the active Takt provider: the top-level `provider:` value, else the
18283
20302
  * sole key in `provider_profiles`, else the `claude` default.
18284
20303
  */
@@ -18600,8 +20619,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18600
20619
  config: rulesyncPermissions.getJson(),
18601
20620
  logger
18602
20621
  });
18603
- const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
18604
- const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
20622
+ const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20623
+ const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
18605
20624
  const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
18606
20625
  const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
18607
20626
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -18626,8 +20645,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18626
20645
  } catch (error) {
18627
20646
  throw new Error(`Failed to parse Warp permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18628
20647
  }
18629
- const agents = isRecord$1(settings.agents) ? settings.agents : {};
18630
- const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
20648
+ const agents = isRecord(settings.agents) ? settings.agents : {};
20649
+ const profiles = isRecord(agents.profiles) ? agents.profiles : {};
18631
20650
  const config = convertWarpToRulesyncPermissions({
18632
20651
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
18633
20652
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -18978,6 +20997,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18978
20997
  supportsImport: true
18979
20998
  }
18980
20999
  }],
21000
+ ["devin", {
21001
+ class: DevinPermissions,
21002
+ meta: {
21003
+ supportsProject: true,
21004
+ supportsGlobal: true,
21005
+ supportsImport: true
21006
+ }
21007
+ }],
18981
21008
  ["factorydroid", {
18982
21009
  class: FactorydroidPermissions,
18983
21010
  meta: {
@@ -19010,6 +21037,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19010
21037
  supportsImport: true
19011
21038
  }
19012
21039
  }],
21040
+ ["junie", {
21041
+ class: JuniePermissions,
21042
+ meta: {
21043
+ supportsProject: true,
21044
+ supportsGlobal: true,
21045
+ supportsImport: true
21046
+ }
21047
+ }],
19013
21048
  ["kilo", {
19014
21049
  class: KiloPermissions,
19015
21050
  meta: {
@@ -19058,6 +21093,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19058
21093
  supportsImport: true
19059
21094
  }
19060
21095
  }],
21096
+ ["reasonix", {
21097
+ class: ReasonixPermissions,
21098
+ meta: {
21099
+ supportsProject: true,
21100
+ supportsGlobal: true,
21101
+ supportsImport: true
21102
+ }
21103
+ }],
19061
21104
  ["rovodev", {
19062
21105
  class: RovodevPermissions,
19063
21106
  meta: {
@@ -19610,7 +21653,7 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
19610
21653
  opencode: zod_mini.z.optional(zod_mini.z.looseObject({
19611
21654
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
19612
21655
  license: zod_mini.z.optional(zod_mini.z.string()),
19613
- compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
21656
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
19614
21657
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
19615
21658
  })),
19616
21659
  kilo: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -19631,7 +21674,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
19631
21674
  })),
19632
21675
  copilotcli: zod_mini.z.optional(zod_mini.z.looseObject({
19633
21676
  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())]))
21677
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
21678
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
19635
21679
  })),
19636
21680
  pi: zod_mini.z.optional(zod_mini.z.looseObject({
19637
21681
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -21385,7 +23429,11 @@ const CopilotSkillFrontmatterSchema = zod_mini.z.looseObject({
21385
23429
  });
21386
23430
  /**
21387
23431
  * Represents a GitHub Copilot skill directory.
21388
- * Skills are stored under the .github/skills directory with SKILL.md files.
23432
+ *
23433
+ * Copilot discovers project skills from `.github/skills/` and personal/global
23434
+ * skills from `~/.copilot/skills/`. Each skill is a directory containing a
23435
+ * `SKILL.md` file with `name`/`description` frontmatter.
23436
+ * https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
21389
23437
  */
21390
23438
  var CopilotSkill = class CopilotSkill extends ToolSkill {
21391
23439
  constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -21407,7 +23455,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
21407
23455
  }
21408
23456
  }
21409
23457
  static getSettablePaths(options) {
21410
- if (options?.global) throw new Error("CopilotSkill does not support global mode.");
23458
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21411
23459
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21412
23460
  }
21413
23461
  getFrontmatter() {
@@ -21522,7 +23570,8 @@ const CopilotcliSkillFrontmatterSchema = zod_mini.z.looseObject({
21522
23570
  name: zod_mini.z.string(),
21523
23571
  description: zod_mini.z.string(),
21524
23572
  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())]))
23573
+ "allowed-tools": zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
23574
+ "argument-hint": zod_mini.z.optional(zod_mini.z.string())
21526
23575
  });
21527
23576
  /**
21528
23577
  * Represents a GitHub Copilot CLI skill directory.
@@ -21552,7 +23601,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21552
23601
  }
21553
23602
  }
21554
23603
  static getSettablePaths(options) {
21555
- if (options?.global) return { relativeDirPath: COPILOTCLI_SKILLS_GLOBAL_DIR_PATH };
23604
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21556
23605
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21557
23606
  }
21558
23607
  getFrontmatter() {
@@ -21580,7 +23629,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21580
23629
  const frontmatter = this.getFrontmatter();
21581
23630
  const copilotcliSection = {
21582
23631
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
21583
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
23632
+ ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
23633
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
21584
23634
  };
21585
23635
  const rulesyncFrontmatter = {
21586
23636
  name: frontmatter.name,
@@ -21606,7 +23656,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21606
23656
  name: rulesyncFrontmatter.name,
21607
23657
  description: rulesyncFrontmatter.description,
21608
23658
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
21609
- ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
23659
+ ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
23660
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
21610
23661
  };
21611
23662
  return new CopilotcliSkill({
21612
23663
  outputRoot,
@@ -22832,8 +24883,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
22832
24883
  if (!result.success) throw result.error;
22833
24884
  }
22834
24885
  }
22835
- static getSettablePaths(options) {
22836
- if (options?.global) throw new Error("KiroSkill does not support global mode.");
24886
+ static getSettablePaths(_options) {
22837
24887
  return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
22838
24888
  }
22839
24889
  getFrontmatter() {
@@ -22975,10 +25025,19 @@ const OpenCodeSkillFrontmatterSchema = zod_mini.z.looseObject({
22975
25025
  name: zod_mini.z.string(),
22976
25026
  description: zod_mini.z.string(),
22977
25027
  license: zod_mini.z.optional(zod_mini.z.string()),
22978
- compatibility: zod_mini.z.optional(zod_mini.z.looseObject({})),
25028
+ compatibility: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.looseObject({})])),
22979
25029
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({})),
22980
25030
  "allowed-tools": zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
22981
25031
  });
25032
+ /**
25033
+ * Reads a top-level `compatibility` value from rulesync frontmatter, accepting
25034
+ * both the documented string form (e.g. `compatibility: opencode`) and the
25035
+ * legacy object form. Returns `undefined` for any other shape.
25036
+ */
25037
+ function readTopLevelCompatibility(value) {
25038
+ if (typeof value === "string") return value;
25039
+ if (typeof value === "object" && value !== null) return value;
25040
+ }
22982
25041
  var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
22983
25042
  constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
22984
25043
  super({
@@ -23055,7 +25114,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
23055
25114
  const opencodeSection = rulesyncFrontmatter.opencode;
23056
25115
  const looseTopLevel = rulesyncFrontmatter;
23057
25116
  const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
23058
- const topLevelCompatibility = typeof looseTopLevel.compatibility === "object" && looseTopLevel.compatibility !== null ? looseTopLevel.compatibility : void 0;
25117
+ const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
23059
25118
  const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
23060
25119
  const license = opencodeSection?.license ?? topLevelLicense;
23061
25120
  const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
@@ -24409,7 +26468,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24409
26468
  meta: {
24410
26469
  supportsProject: true,
24411
26470
  supportsSimulated: false,
24412
- supportsGlobal: false
26471
+ supportsGlobal: true
24413
26472
  }
24414
26473
  }],
24415
26474
  ["copilotcli", {
@@ -24497,7 +26556,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24497
26556
  meta: {
24498
26557
  supportsProject: true,
24499
26558
  supportsSimulated: false,
24500
- supportsGlobal: false
26559
+ supportsGlobal: true
24501
26560
  }
24502
26561
  }],
24503
26562
  ["kiro-ide", {
@@ -24505,7 +26564,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24505
26564
  meta: {
24506
26565
  supportsProject: true,
24507
26566
  supportsSimulated: false,
24508
- supportsGlobal: false
26567
+ supportsGlobal: true
24509
26568
  }
24510
26569
  }],
24511
26570
  ["opencode", {
@@ -28593,7 +30652,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28593
30652
  class: KiroCliSubagent,
28594
30653
  meta: {
28595
30654
  supportsSimulated: false,
28596
- supportsGlobal: false,
30655
+ supportsGlobal: true,
28597
30656
  filePattern: "*.json"
28598
30657
  }
28599
30658
  }],
@@ -28601,7 +30660,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28601
30660
  class: KiroIdeSubagent,
28602
30661
  meta: {
28603
30662
  supportsSimulated: false,
28604
- supportsGlobal: false,
30663
+ supportsGlobal: true,
28605
30664
  filePattern: "*.md"
28606
30665
  }
28607
30666
  }],
@@ -31138,7 +33197,7 @@ const STRATEGIES = [
31138
33197
  * plus companion `globs`/`description` fields. (`.devin/rules/` is the
31139
33198
  * pre-rebrand legacy location the tool still reads.)
31140
33199
  * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
31141
- * `~/.codeium/windsurf/memories/global_rules.md` (unchanged by the rebrand).
33200
+ * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
31142
33201
  *
31143
33202
  * Trigger inference (when no explicit devin trigger is persisted):
31144
33203
  * - Specific globs (non wildcard) → glob
@@ -31165,8 +33224,8 @@ var DevinRule = class DevinRule extends ToolRule {
31165
33224
  }
31166
33225
  static getGlobalRootPath(excludeToolDir) {
31167
33226
  return {
31168
- relativeDirPath: buildToolPath(CODEIUM_DIR, WINDSURF_MEMORIES_SUBDIR, excludeToolDir),
31169
- relativeFilePath: DEVIN_GLOBAL_RULES_FILE_NAME
33227
+ relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
33228
+ relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
31170
33229
  };
31171
33230
  }
31172
33231
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
@@ -32056,7 +34115,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32056
34115
  /**
32057
34116
  * Rule generator for Pi Coding Agent.
32058
34117
  *
32059
- * Pi loads instruction context only from the `AGENTS.md` / `CLAUDE.md` family —
34118
+ * Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
32060
34119
  * the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
32061
34120
  * directory tree from the current working directory. It does NOT resolve
32062
34121
  * `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
@@ -32067,6 +34126,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32067
34126
  * to map onto; their bodies are folded into the single root `AGENTS.md` by the
32068
34127
  * RulesProcessor (there is no separate non-root output location — `nonRoot` is
32069
34128
  * `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
34129
+ *
34130
+ * Pi also loads two system-prompt instruction files that rulesync does NOT emit:
34131
+ * `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
34132
+ * prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
34133
+ * `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
34134
+ * routes a designated `root` rule to a single context file and has no convention
34135
+ * for marking a rule as "replace" vs "append" the system prompt, so these
34136
+ * surfaces are documented in docs/reference/file-formats.md and left to be
34137
+ * authored by hand rather than mapped to a speculative new frontmatter flag.
32070
34138
  */
32071
34139
  var PiRule = class PiRule extends ToolRule {
32072
34140
  constructor({ fileContent, root, ...rest }) {
@@ -34148,63 +36216,215 @@ function warnUnsupportedTargets(params) {
34148
36216
  async function checkRulesyncDirExists(params) {
34149
36217
  return fileExists((0, node_path.join)(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
34150
36218
  }
36219
+ function dependsOnReachable(byId, from, target) {
36220
+ const seen = /* @__PURE__ */ new Set();
36221
+ const stack = [from];
36222
+ while (stack.length > 0) {
36223
+ const current = stack.pop();
36224
+ if (current === void 0 || seen.has(current)) continue;
36225
+ seen.add(current);
36226
+ if (current === target) return true;
36227
+ for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
36228
+ }
36229
+ return false;
36230
+ }
36231
+ function assertSharedFilesOrdered(steps, byId) {
36232
+ const writersByFile = /* @__PURE__ */ new Map();
36233
+ for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
36234
+ for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
36235
+ const a = writers[i];
36236
+ const b = writers[j];
36237
+ 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.`);
36238
+ }
36239
+ }
36240
+ /**
36241
+ * Topologically sort generation steps and reject ordering hazards: a shared file
36242
+ * with two writers not ordered by `dependsOn` (a silent data-loss trap), an
36243
+ * unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
36244
+ *
36245
+ * @throws Error if a shared file has unordered writers, a dependency is unknown,
36246
+ * or the dependency graph contains a cycle.
36247
+ */
36248
+ function resolveExecutionOrder(steps) {
36249
+ const byId = new Map(steps.map((step) => [step.id, step]));
36250
+ assertSharedFilesOrdered(steps, byId);
36251
+ const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
36252
+ const dependents = /* @__PURE__ */ new Map();
36253
+ for (const step of steps) for (const dep of step.dependsOn ?? []) {
36254
+ if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
36255
+ unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
36256
+ dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
36257
+ }
36258
+ const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
36259
+ const ordered = [];
36260
+ while (ready.length > 0) {
36261
+ const id = ready.shift();
36262
+ ordered.push(byId.get(id));
36263
+ for (const dependent of dependents.get(id) ?? []) {
36264
+ const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
36265
+ unresolvedDeps.set(dependent, next);
36266
+ if (next === 0) ready.push(dependent);
36267
+ }
36268
+ }
36269
+ if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36270
+ return ordered;
36271
+ }
36272
+ /**
36273
+ * The static shape of the generation step graph: which steps write which shared
36274
+ * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36275
+ * for those writers. Exported (separately from the `run` closures, which need a
36276
+ * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36277
+ * tested directly against the real graph rather than a hand-copied one. Readonly
36278
+ * so a consumer can't mutate this module-level singleton and affect every
36279
+ * subsequent `generate()` call in the process.
36280
+ */
36281
+ const GENERATION_STEP_GRAPH = [
36282
+ {
36283
+ id: "ignore",
36284
+ writesSharedFile: ["claude-settings", "zed-settings"]
36285
+ },
36286
+ {
36287
+ id: "mcp",
36288
+ writesSharedFile: [
36289
+ "kilo-opencode-config",
36290
+ "zed-settings",
36291
+ "qwencode-settings",
36292
+ "augmentcode-settings",
36293
+ "hermesagent-config",
36294
+ "amp-settings",
36295
+ "codexcli-config",
36296
+ "grokcli-config",
36297
+ "vibe-config",
36298
+ "devin-config",
36299
+ "reasonix-config"
36300
+ ],
36301
+ dependsOn: ["ignore"]
36302
+ },
36303
+ { id: "commands" },
36304
+ { id: "subagents" },
36305
+ { id: "skills" },
36306
+ {
36307
+ id: "hooks",
36308
+ writesSharedFile: [
36309
+ "claude-settings",
36310
+ "qwencode-settings",
36311
+ "augmentcode-settings",
36312
+ "hermesagent-config",
36313
+ "kiro-agent-config",
36314
+ "codexcli-config",
36315
+ "vibe-config",
36316
+ "devin-config"
36317
+ ],
36318
+ dependsOn: ["ignore", "mcp"]
36319
+ },
36320
+ {
36321
+ id: "permissions",
36322
+ writesSharedFile: [
36323
+ "claude-settings",
36324
+ "kilo-opencode-config",
36325
+ "zed-settings",
36326
+ "qwencode-settings",
36327
+ "augmentcode-settings",
36328
+ "hermesagent-config",
36329
+ "kiro-agent-config",
36330
+ "amp-settings",
36331
+ "codexcli-config",
36332
+ "grokcli-config",
36333
+ "vibe-config",
36334
+ "devin-config",
36335
+ "reasonix-config"
36336
+ ],
36337
+ dependsOn: [
36338
+ "ignore",
36339
+ "hooks",
36340
+ "mcp"
36341
+ ]
36342
+ },
36343
+ {
36344
+ id: "rules",
36345
+ writesSharedFile: ["kilo-opencode-config"],
36346
+ dependsOn: [
36347
+ "mcp",
36348
+ "skills",
36349
+ "permissions"
36350
+ ]
36351
+ }
36352
+ ];
34151
36353
  /**
34152
36354
  * Generate configuration files for AI tools.
34153
36355
  * @throws Error if generation fails
34154
36356
  */
34155
36357
  async function generate(params) {
34156
36358
  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;
36359
+ let skillsResult;
36360
+ const runners = {
36361
+ ignore: () => generateIgnoreCore({
36362
+ config,
36363
+ logger
36364
+ }),
36365
+ mcp: () => generateMcpCore({
36366
+ config,
36367
+ logger
36368
+ }),
36369
+ commands: () => generateCommandsCore({
36370
+ config,
36371
+ logger
36372
+ }),
36373
+ subagents: () => generateSubagentsCore({
36374
+ config,
36375
+ logger
36376
+ }),
36377
+ skills: async () => {
36378
+ skillsResult = await generateSkillsCore({
36379
+ config,
36380
+ logger
36381
+ });
36382
+ return skillsResult;
36383
+ },
36384
+ hooks: () => generateHooksCore({
36385
+ config,
36386
+ logger
36387
+ }),
36388
+ permissions: () => generatePermissionsCore({
36389
+ config,
36390
+ logger
36391
+ }),
36392
+ rules: () => generateRulesCore({
36393
+ config,
36394
+ logger,
36395
+ skills: skillsResult?.skills
36396
+ })
36397
+ };
36398
+ const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
36399
+ ...meta,
36400
+ run: runners[meta.id]
36401
+ })));
36402
+ const resultsById = /* @__PURE__ */ new Map();
36403
+ for (const step of orderedSteps) resultsById.set(step.id, await step.run());
36404
+ if (!skillsResult) throw new Error("Skills generation step did not run.");
36405
+ const get = (id) => {
36406
+ const result = resultsById.get(id);
36407
+ if (!result) throw new Error(`Missing generation result for step '${id}'.`);
36408
+ return result;
36409
+ };
36410
+ const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
34191
36411
  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,
36412
+ rulesCount: get("rules").count,
36413
+ rulesPaths: get("rules").paths,
36414
+ ignoreCount: get("ignore").count,
36415
+ ignorePaths: get("ignore").paths,
36416
+ mcpCount: get("mcp").count,
36417
+ mcpPaths: get("mcp").paths,
36418
+ commandsCount: get("commands").count,
36419
+ commandsPaths: get("commands").paths,
36420
+ subagentsCount: get("subagents").count,
36421
+ subagentsPaths: get("subagents").paths,
34202
36422
  skillsCount: skillsResult.count,
34203
36423
  skillsPaths: skillsResult.paths,
34204
- hooksCount: hooksResult.count,
34205
- hooksPaths: hooksResult.paths,
34206
- permissionsCount: permissionsResult.count,
34207
- permissionsPaths: permissionsResult.paths,
36424
+ hooksCount: get("hooks").count,
36425
+ hooksPaths: get("hooks").paths,
36426
+ permissionsCount: get("permissions").count,
36427
+ permissionsPaths: get("permissions").paths,
34208
36428
  skills: skillsResult.skills,
34209
36429
  hasDiff
34210
36430
  };