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.
@@ -198,6 +198,7 @@ const mcpProcessorToolTargetTuple = [
198
198
  "reasonix",
199
199
  "roo",
200
200
  "rovodev",
201
+ "takt",
201
202
  "vibe",
202
203
  "warp",
203
204
  "devin",
@@ -205,6 +206,7 @@ const mcpProcessorToolTargetTuple = [
205
206
  ];
206
207
  const commandsProcessorToolTargetTuple = [
207
208
  "agentsmd",
209
+ "antigravity-cli",
208
210
  "antigravity-ide",
209
211
  "augmentcode",
210
212
  "claudecode",
@@ -224,7 +226,9 @@ const commandsProcessorToolTargetTuple = [
224
226
  "opencode",
225
227
  "pi",
226
228
  "qwencode",
229
+ "reasonix",
227
230
  "roo",
231
+ "rovodev",
228
232
  "takt",
229
233
  "devin"
230
234
  ];
@@ -309,11 +313,13 @@ const hooksProcessorToolTargetTuple = [
309
313
  "deepagents",
310
314
  "kiro",
311
315
  "kiro-cli",
316
+ "kiro-ide",
312
317
  "devin",
313
318
  "augmentcode",
314
319
  "junie",
315
320
  "vibe",
316
- "qwencode"
321
+ "qwencode",
322
+ "reasonix"
317
323
  ];
318
324
  const permissionsProcessorToolTargetTuple = [
319
325
  "amp",
@@ -324,16 +330,19 @@ const permissionsProcessorToolTargetTuple = [
324
330
  "cline",
325
331
  "codexcli",
326
332
  "cursor",
333
+ "devin",
327
334
  "factorydroid",
328
335
  "goose",
329
336
  "grokcli",
330
337
  "hermesagent",
338
+ "junie",
331
339
  "kilo",
332
340
  "kiro",
333
341
  "kiro-cli",
334
342
  "kiro-ide",
335
343
  "opencode",
336
344
  "qwencode",
345
+ "reasonix",
337
346
  "rovodev",
338
347
  "takt",
339
348
  "vibe",
@@ -1373,7 +1382,7 @@ function extractConfigFileTargets(targets) {
1373
1382
  * Type guard to check if a value is a plain object (Record<string, unknown>).
1374
1383
  * This excludes arrays and null values.
1375
1384
  */
1376
- function isRecord$1(value) {
1385
+ function isRecord(value) {
1377
1386
  return typeof value === "object" && value !== null && !Array.isArray(value);
1378
1387
  }
1379
1388
  /**
@@ -1388,7 +1397,7 @@ function isRecord$1(value) {
1388
1397
  * malicious accessor descriptors.
1389
1398
  */
1390
1399
  function isPlainObject(value) {
1391
- if (!isRecord$1(value)) return false;
1400
+ if (!isRecord(value)) return false;
1392
1401
  const proto = Object.getPrototypeOf(value);
1393
1402
  return proto === null || proto === Object.prototype;
1394
1403
  }
@@ -1739,6 +1748,17 @@ var ToolCommand = class extends AiFile {
1739
1748
  throw new Error("Please implement this method in the subclass.");
1740
1749
  }
1741
1750
  /**
1751
+ * Optional hook for tools whose commands are not purely one-file-per-command
1752
+ * (e.g. Rovo Dev's `prompts.yml` manifest that indexes every saved-prompt
1753
+ * content file). Given the full set of ToolCommand instances just generated
1754
+ * for this target, returns any additional shared/aggregate files that must
1755
+ * be written alongside them. Most tools don't need this and inherit the
1756
+ * empty default (mirrors `ToolHooks.getAuxiliaryFiles`).
1757
+ */
1758
+ static async getAuxiliaryFiles(_params) {
1759
+ return [];
1760
+ }
1761
+ /**
1742
1762
  * Convert a RulesyncCommand to the tool-specific command format.
1743
1763
  *
1744
1764
  * This method should:
@@ -1897,16 +1917,25 @@ var AgentsmdCommand = class AgentsmdCommand extends SimulatedCommand {
1897
1917
  }
1898
1918
  };
1899
1919
  //#endregion
1900
- //#region src/constants/antigravity-ide-paths.ts
1901
- const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
1902
- const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = join(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
1903
- const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
1904
- const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
1905
- const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
1906
- const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
1907
- const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
1908
- const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
1909
- const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
1920
+ //#region src/constants/antigravity-paths.ts
1921
+ const ANTIGRAVITY_DIR = ".agents";
1922
+ const ANTIGRAVITY_SKILLS_DIR_PATH = join(ANTIGRAVITY_DIR, "skills");
1923
+ const ANTIGRAVITY_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_DIR, "workflows");
1924
+ const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
1925
+ const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
1926
+ const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
1927
+ const ANTIGRAVITY_GEMINI_DIR = ".gemini";
1928
+ const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
1929
+ const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
1930
+ const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
1931
+ const ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR, "global_workflows");
1932
+ const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
1933
+ const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
1934
+ //#endregion
1935
+ //#region src/constants/antigravity-cli-paths.ts
1936
+ const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
1937
+ const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
1938
+ const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
1910
1939
  //#endregion
1911
1940
  //#region src/features/commands/antigravity-command.ts
1912
1941
  const AntigravityWorkflowFrontmatterSchema = z.looseObject({
@@ -2080,7 +2109,7 @@ var AntigravitySharedCommand = class extends ToolCommand {
2080
2109
  }
2081
2110
  static extractAntigravityConfig(rulesyncCommand) {
2082
2111
  const antigravity = rulesyncCommand.getFrontmatter().antigravity;
2083
- return isRecord$1(antigravity) ? antigravity : void 0;
2112
+ return isRecord(antigravity) ? antigravity : void 0;
2084
2113
  }
2085
2114
  static resolveTrigger(rulesyncCommand, antigravityConfig) {
2086
2115
  const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
@@ -2162,6 +2191,45 @@ var AntigravitySharedCommand = class extends ToolCommand {
2162
2191
  }
2163
2192
  };
2164
2193
  //#endregion
2194
+ //#region src/features/commands/antigravity-cli-command.ts
2195
+ /**
2196
+ * Command (workflow) generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
2197
+ *
2198
+ * Shares all body and frontmatter handling with {@link AntigravitySharedCommand};
2199
+ * the CLI reads project workflows from the same `.agents/workflows/` directory as
2200
+ * the IDE (the shared Antigravity 2.0 harness), but keeps its own global
2201
+ * workflows tree at `~/.gemini/antigravity-cli/global_workflows/` (mirroring the
2202
+ * CLI's global skills tree). It answers to the `antigravity-cli` target.
2203
+ */
2204
+ var AntigravityCliCommand = class extends AntigravitySharedCommand {
2205
+ static getProjectRelativeDirPath() {
2206
+ return ANTIGRAVITY_WORKFLOWS_DIR_PATH;
2207
+ }
2208
+ static getGlobalRelativeDirPath() {
2209
+ return ANTIGRAVITY_CLI_GLOBAL_WORKFLOWS_DIR_PATH;
2210
+ }
2211
+ getToolTargetName() {
2212
+ return "antigravity-cli";
2213
+ }
2214
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
2215
+ return this.isTargetedByRulesyncCommandDefault({
2216
+ rulesyncCommand,
2217
+ toolTarget: "antigravity-cli"
2218
+ });
2219
+ }
2220
+ };
2221
+ //#endregion
2222
+ //#region src/constants/antigravity-ide-paths.ts
2223
+ const ANTIGRAVITY_IDE_AGENTS_DIR = ".agents";
2224
+ const ANTIGRAVITY_IDE_COMMANDS_DIR_PATH = join(ANTIGRAVITY_IDE_AGENTS_DIR, "workflows");
2225
+ const ANTIGRAVITY_IDE_RULE_FILE_NAME = "AGENTS.md";
2226
+ const ANTIGRAVITY_IDE_GEMINI_DIR = ".gemini";
2227
+ const ANTIGRAVITY_IDE_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
2228
+ const ANTIGRAVITY_IDE_GLOBAL_CONFIG_SUBDIR = "config";
2229
+ const ANTIGRAVITY_IDE_GLOBAL_WORKFLOWS_DIR_PATH = join(ANTIGRAVITY_IDE_GEMINI_DIR, "antigravity", "global_workflows");
2230
+ const ANTIGRAVITY_IDE_PERMISSIONS_DIR = ".antigravity";
2231
+ const ANTIGRAVITY_IDE_PERMISSIONS_FILE_NAME = "settings.json";
2232
+ //#endregion
2165
2233
  //#region src/features/commands/antigravity-ide-command.ts
2166
2234
  /**
2167
2235
  * Command (workflow) generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -2546,6 +2614,16 @@ const CodexcliCommandFrontmatterSchema = z.looseObject({
2546
2614
  description: z.optional(z.string()),
2547
2615
  "argument-hint": z.optional(z.string())
2548
2616
  });
2617
+ /**
2618
+ * Generates Codex CLI's global-only custom prompts under `~/.codex/prompts/*.md`.
2619
+ *
2620
+ * Note: upstream Codex docs now state "Custom prompts are deprecated. Use skills for
2621
+ * reusable instructions" (https://developers.openai.com/codex/custom-prompts). No removal
2622
+ * date has been announced and custom prompts remain functional, so this class's
2623
+ * generation behavior is unchanged. Prefer rulesync's `codexcli` skills support
2624
+ * (see `src/features/skills/codexcli-skill.ts`) for new reusable instructions going
2625
+ * forward; this class is kept for users who still rely on custom prompts.
2626
+ */
2549
2627
  var CodexcliCommand = class CodexcliCommand extends ToolCommand {
2550
2628
  frontmatter;
2551
2629
  body;
@@ -2669,7 +2747,7 @@ const COPILOTCLI_PROJECT_MCP_FILE_NAME = "mcp.json";
2669
2747
  const COPILOTCLI_AGENTS_DIR_PATH = join(COPILOT_DIR, "agents");
2670
2748
  const COPILOTCLI_HOOKS_DIR_PATH = join(COPILOT_DIR, "hooks");
2671
2749
  const COPILOTCLI_HOOKS_FILE_NAME = "copilotcli-hooks.json";
2672
- const COPILOTCLI_SKILLS_GLOBAL_DIR_PATH = join(COPILOT_DIR, "skills");
2750
+ const COPILOT_SKILLS_GLOBAL_DIR_PATH = join(COPILOT_DIR, "skills");
2673
2751
  //#endregion
2674
2752
  //#region src/features/commands/copilot-command.ts
2675
2753
  const CopilotCommandFrontmatterSchema = z.looseObject({
@@ -2919,20 +2997,17 @@ var CursorCommand = class CursorCommand extends ToolCommand {
2919
2997
  //#endregion
2920
2998
  //#region src/constants/devin-paths.ts
2921
2999
  const DEVIN_DIR = ".devin";
2922
- const WINDSURF_DIR = ".windsurf";
2923
- const CODEIUM_DIR = ".codeium";
2924
- const WINDSURF_SUBDIR = "windsurf";
2925
- const CODEIUM_WINDSURF_DIR = join(CODEIUM_DIR, WINDSURF_SUBDIR);
2926
- const WINDSURF_MEMORIES_SUBDIR = join(WINDSURF_SUBDIR, "memories");
3000
+ const CODEIUM_WINDSURF_DIR = join(".codeium", "windsurf");
2927
3001
  const DEVIN_WORKFLOWS_DIR_PATH = join(DEVIN_DIR, "workflows");
2928
3002
  const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
2929
3003
  const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
2930
- const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(".config", "devin", "agents");
3004
+ const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
3005
+ const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
2931
3006
  const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
2932
3007
  const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
2933
- const DEVIN_MCP_FILE_NAME = "mcp_config.json";
2934
- const DEVIN_HOOKS_FILE_NAME = "hooks.json";
2935
- const DEVIN_GLOBAL_RULES_FILE_NAME = "global_rules.md";
3008
+ const DEVIN_CONFIG_FILE_NAME = "config.json";
3009
+ const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
3010
+ const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
2936
3011
  const DEVIN_IGNORE_FILE_NAME = ".devinignore";
2937
3012
  const DEVIN_LEGACY_IGNORE_FILE_NAME = ".codeiumignore";
2938
3013
  //#endregion
@@ -3181,6 +3256,8 @@ const GOOSE_MCP_FILE_NAME = "config.yaml";
3181
3256
  const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
3182
3257
  const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
3183
3258
  const GOOSE_HOOKS_FILE_NAME = "hooks.json";
3259
+ const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
3260
+ const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
3184
3261
  const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
3185
3262
  const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
3186
3263
  const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
@@ -3438,6 +3515,7 @@ const JUNIE_ALT_AGENTS_DIR_PATH = ".agents";
3438
3515
  const JUNIE_MCP_DIR_PATH = join(JUNIE_DIR, "mcp");
3439
3516
  const JUNIE_MCP_FILE_NAME = "mcp.json";
3440
3517
  const JUNIE_HOOKS_FILE_NAME = "config.json";
3518
+ const JUNIE_PERMISSIONS_FILE_NAME = "allowlist.json";
3441
3519
  const JUNIE_IGNORE_FILE_NAME = ".aiignore";
3442
3520
  const JUNIE_RULE_FILE_NAME = "AGENTS.md";
3443
3521
  const JUNIE_LEGACY_RULE_FILE_NAME = "guidelines.md";
@@ -3690,6 +3768,15 @@ const KIRO_SKILLS_DIR_PATH = join(KIRO_DIR, "skills");
3690
3768
  const KIRO_SETTINGS_DIR_PATH = join(KIRO_DIR, "settings");
3691
3769
  const KIRO_AGENTS_DIR_PATH = join(KIRO_DIR, "agents");
3692
3770
  const KIRO_HOOKS_FILE_NAME = "default.json";
3771
+ /**
3772
+ * Kiro IDE 1.0 stores hooks as structured JSON files in `.kiro/hooks/`
3773
+ * (workspace) and `~/.kiro/hooks/` (user). A single file may declare multiple
3774
+ * hooks in its `hooks` array, so rulesync emits all generated hooks into one
3775
+ * `rulesync.json` file per scope.
3776
+ * @see https://kiro.dev/docs/hooks/
3777
+ */
3778
+ const KIRO_IDE_HOOKS_DIR_PATH = join(KIRO_DIR, "hooks");
3779
+ const KIRO_IDE_HOOKS_FILE_NAME = "rulesync.json";
3693
3780
  const KIRO_MCP_FILE_NAME = "mcp.json";
3694
3781
  const KIRO_IGNORE_FILE_NAME = ".kiroignore";
3695
3782
  //#endregion
@@ -4269,6 +4356,135 @@ var QwencodeCommand = class QwencodeCommand extends ToolCommand {
4269
4356
  }
4270
4357
  };
4271
4358
  //#endregion
4359
+ //#region src/constants/reasonix-paths.ts
4360
+ const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
4361
+ const REASONIX_GLOBAL_DIR = ".reasonix";
4362
+ const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
4363
+ const REASONIX_PROJECT_PERMISSIONS_FILE_NAME = REASONIX_PROJECT_MCP_FILE_NAME;
4364
+ const REASONIX_GLOBAL_PERMISSIONS_FILE_NAME = REASONIX_GLOBAL_MCP_FILE_NAME;
4365
+ const REASONIX_DIR = REASONIX_GLOBAL_DIR;
4366
+ const REASONIX_SETTINGS_FILE_NAME = "settings.json";
4367
+ const REASONIX_COMMANDS_DIR_PATH = join(REASONIX_DIR, "commands");
4368
+ //#endregion
4369
+ //#region src/features/commands/reasonix-command.ts
4370
+ /**
4371
+ * Reasonix custom slash commands are Markdown files under `.reasonix/commands/`
4372
+ * (project) / `~/.reasonix/commands/` (global) — directly analogous to Claude
4373
+ * Code's `.claude/commands/` (Reasonix explicitly copies Claude Code's
4374
+ * conventions). Frontmatter supports `description` and `argument-hint`; the
4375
+ * body uses the same `$ARGUMENTS` / `$1`…`$N` placeholder syntax rulesync's
4376
+ * universal command-body syntax already targets.
4377
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/GUIDE.md
4378
+ */
4379
+ const ReasonixCommandFrontmatterSchema = z.looseObject({
4380
+ description: z.optional(z.string()),
4381
+ "argument-hint": z.optional(z.string())
4382
+ });
4383
+ var ReasonixCommand = class ReasonixCommand extends ToolCommand {
4384
+ frontmatter;
4385
+ body;
4386
+ constructor({ frontmatter, body, ...rest }) {
4387
+ if (rest.validate) {
4388
+ const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
4389
+ if (!result.success) throw new Error(`Invalid frontmatter in ${join(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}`);
4390
+ }
4391
+ super({
4392
+ ...rest,
4393
+ fileContent: stringifyFrontmatter(body, frontmatter)
4394
+ });
4395
+ this.frontmatter = frontmatter;
4396
+ this.body = body;
4397
+ }
4398
+ static getSettablePaths(_options = {}) {
4399
+ return { relativeDirPath: REASONIX_COMMANDS_DIR_PATH };
4400
+ }
4401
+ getBody() {
4402
+ return this.body;
4403
+ }
4404
+ getFrontmatter() {
4405
+ return this.frontmatter;
4406
+ }
4407
+ toRulesyncCommand() {
4408
+ const { description, ...restFields } = this.frontmatter;
4409
+ const rulesyncFrontmatter = {
4410
+ targets: ["*"],
4411
+ description,
4412
+ ...Object.keys(restFields).length > 0 && { reasonix: restFields }
4413
+ };
4414
+ const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
4415
+ return new RulesyncCommand({
4416
+ outputRoot: ".",
4417
+ frontmatter: rulesyncFrontmatter,
4418
+ body: this.body,
4419
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
4420
+ relativeFilePath: this.relativeFilePath,
4421
+ fileContent,
4422
+ validate: true
4423
+ });
4424
+ }
4425
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
4426
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
4427
+ const reasonixFields = rulesyncFrontmatter.reasonix ?? {};
4428
+ return new ReasonixCommand({
4429
+ outputRoot,
4430
+ frontmatter: {
4431
+ description: rulesyncFrontmatter.description,
4432
+ ...reasonixFields
4433
+ },
4434
+ body: rulesyncCommand.getBody(),
4435
+ relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
4436
+ relativeFilePath: rulesyncCommand.getRelativeFilePath(),
4437
+ validate
4438
+ });
4439
+ }
4440
+ validate() {
4441
+ if (!this.frontmatter) return {
4442
+ success: true,
4443
+ error: null
4444
+ };
4445
+ const result = ReasonixCommandFrontmatterSchema.safeParse(this.frontmatter);
4446
+ if (result.success) return {
4447
+ success: true,
4448
+ error: null
4449
+ };
4450
+ else return {
4451
+ success: false,
4452
+ error: /* @__PURE__ */ new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`)
4453
+ };
4454
+ }
4455
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
4456
+ return this.isTargetedByRulesyncCommandDefault({
4457
+ rulesyncCommand,
4458
+ toolTarget: "reasonix"
4459
+ });
4460
+ }
4461
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
4462
+ const paths = this.getSettablePaths({ global });
4463
+ const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
4464
+ const { frontmatter, body: content } = parseFrontmatter(await readFileContent(filePath), filePath);
4465
+ const result = ReasonixCommandFrontmatterSchema.safeParse(frontmatter);
4466
+ if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
4467
+ return new ReasonixCommand({
4468
+ outputRoot,
4469
+ relativeDirPath: paths.relativeDirPath,
4470
+ relativeFilePath,
4471
+ frontmatter: result.data,
4472
+ body: content.trim(),
4473
+ validate
4474
+ });
4475
+ }
4476
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
4477
+ return new ReasonixCommand({
4478
+ outputRoot,
4479
+ relativeDirPath,
4480
+ relativeFilePath,
4481
+ frontmatter: { description: "" },
4482
+ body: "",
4483
+ validate: false
4484
+ });
4485
+ }
4486
+ };
4487
+ //#endregion
4272
4488
  //#region src/constants/roo-paths.ts
4273
4489
  const ROO_DIR = ".roo";
4274
4490
  const ROO_COMMANDS_DIR_PATH = join(ROO_DIR, "commands");
@@ -4400,6 +4616,217 @@ var RooCommand = class RooCommand extends ToolCommand {
4400
4616
  }
4401
4617
  };
4402
4618
  //#endregion
4619
+ //#region src/constants/rovodev-paths.ts
4620
+ const ROVODEV_DIR = ".rovodev";
4621
+ const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
4622
+ const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
4623
+ const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
4624
+ const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
4625
+ const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
4626
+ const ROVODEV_MCP_FILE_NAME = "mcp.json";
4627
+ const ROVODEV_CONFIG_FILE_NAME = "config.yml";
4628
+ const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
4629
+ const ROVODEV_PROMPTS_FILE_NAME = "prompts.yml";
4630
+ const ROVODEV_PROMPTS_DIR_PATH = join(ROVODEV_DIR, "prompts");
4631
+ //#endregion
4632
+ //#region src/types/tool-file.ts
4633
+ var ToolFile = class extends AiFile {};
4634
+ //#endregion
4635
+ //#region src/features/commands/rovodev-command.ts
4636
+ /**
4637
+ * Rovo Dev CLI "saved prompts": a file-based custom-command surface made of a
4638
+ * `prompts.yml` manifest (`{ name, description, content_file }` entries) plus
4639
+ * per-prompt Markdown content files, discovered in repo-root `.rovodev/`, cwd
4640
+ * `.rovodev/`, and global `~/.rovodev/`, and invoked via `/prompts [title] [extra]`.
4641
+ *
4642
+ * This class represents a single prompt's **content file** — pure Markdown,
4643
+ * no frontmatter — written to `.rovodev/prompts/<name>.md` (project) or
4644
+ * `~/.rovodev/prompts/<name>.md` (global). The `name`/`description` are kept
4645
+ * on the instance (not serialized into the content file itself) so that
4646
+ * {@link RovodevCommand.getAuxiliaryFiles} can build the sibling
4647
+ * `prompts.yml` manifest that indexes every generated prompt.
4648
+ *
4649
+ * @see https://support.atlassian.com/rovo/docs/save-and-reuse-a-prompt-in-rovo-dev-cli/
4650
+ * @see https://support.atlassian.com/rovo/docs/rovo-dev-cli-commands/
4651
+ */
4652
+ var RovodevCommand = class RovodevCommand extends ToolCommand {
4653
+ name;
4654
+ description;
4655
+ body;
4656
+ constructor({ name, description, body, ...rest }) {
4657
+ super({
4658
+ ...rest,
4659
+ fileContent: body
4660
+ });
4661
+ this.name = name;
4662
+ this.description = description;
4663
+ this.body = body;
4664
+ if (rest.validate) {
4665
+ const result = this.validate();
4666
+ if (!result.success) throw result.error;
4667
+ }
4668
+ }
4669
+ static getSettablePaths(_options = {}) {
4670
+ return { relativeDirPath: ROVODEV_PROMPTS_DIR_PATH };
4671
+ }
4672
+ getName() {
4673
+ return this.name;
4674
+ }
4675
+ getDescription() {
4676
+ return this.description;
4677
+ }
4678
+ getBody() {
4679
+ return this.body;
4680
+ }
4681
+ validate() {
4682
+ if (!this.name) return {
4683
+ success: false,
4684
+ error: /* @__PURE__ */ new Error(`${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: Rovo Dev saved-prompt name must not be empty`)
4685
+ };
4686
+ return {
4687
+ success: true,
4688
+ error: null
4689
+ };
4690
+ }
4691
+ toRulesyncCommand() {
4692
+ const rulesyncFrontmatter = {
4693
+ targets: ["*"],
4694
+ description: this.description
4695
+ };
4696
+ const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter);
4697
+ return new RulesyncCommand({
4698
+ outputRoot: ".",
4699
+ frontmatter: rulesyncFrontmatter,
4700
+ body: this.body,
4701
+ relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath,
4702
+ relativeFilePath: this.getRelativeFilePath(),
4703
+ fileContent,
4704
+ validate: true
4705
+ });
4706
+ }
4707
+ static fromRulesyncCommand({ outputRoot = process.cwd(), rulesyncCommand, validate = true, global = false }) {
4708
+ const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();
4709
+ const relativeFilePath = rulesyncCommand.getRelativeFilePath();
4710
+ const name = basename(relativeFilePath, ".md");
4711
+ const paths = this.getSettablePaths({ global });
4712
+ return new RovodevCommand({
4713
+ outputRoot,
4714
+ name,
4715
+ description: rulesyncFrontmatter.description ?? "",
4716
+ body: rulesyncCommand.getBody(),
4717
+ relativeDirPath: paths.relativeDirPath,
4718
+ relativeFilePath,
4719
+ validate,
4720
+ global
4721
+ });
4722
+ }
4723
+ static isTargetedByRulesyncCommand(rulesyncCommand) {
4724
+ return this.isTargetedByRulesyncCommandDefault({
4725
+ rulesyncCommand,
4726
+ toolTarget: "rovodev"
4727
+ });
4728
+ }
4729
+ static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false }) {
4730
+ const paths = this.getSettablePaths({ global });
4731
+ const body = (await readFileContent(join(outputRoot, paths.relativeDirPath, relativeFilePath))).trim();
4732
+ const name = basename(relativeFilePath, ".md");
4733
+ return new RovodevCommand({
4734
+ outputRoot,
4735
+ name,
4736
+ description: await lookupPromptDescription({
4737
+ outputRoot,
4738
+ relativeFilePath,
4739
+ name
4740
+ }),
4741
+ body,
4742
+ relativeDirPath: paths.relativeDirPath,
4743
+ relativeFilePath,
4744
+ validate,
4745
+ global
4746
+ });
4747
+ }
4748
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
4749
+ return new RovodevCommand({
4750
+ outputRoot,
4751
+ name: basename(relativeFilePath, ".md"),
4752
+ description: "",
4753
+ body: "",
4754
+ relativeDirPath,
4755
+ relativeFilePath,
4756
+ validate: false,
4757
+ global
4758
+ });
4759
+ }
4760
+ /**
4761
+ * Rebuilds `.rovodev/prompts.yml` (project) / `~/.rovodev/prompts.yml`
4762
+ * (global) from every `RovodevCommand` generated in this pass. The `prompts`
4763
+ * array is fully replaced with the current set of rulesync-managed prompts
4764
+ * (mirrors `RovodevMcp` fully replacing `mcpServers`); any other top-level
4765
+ * key in an existing manifest is preserved.
4766
+ */
4767
+ static async getAuxiliaryFiles({ toolCommands, outputRoot = process.cwd(), global = false }) {
4768
+ const rovodevCommands = toolCommands.filter((command) => command instanceof RovodevCommand);
4769
+ if (rovodevCommands.length === 0) return [];
4770
+ const existingContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
4771
+ let existing = {};
4772
+ if (existingContent) try {
4773
+ const parsed = load(existingContent);
4774
+ if (isPlainObject(parsed)) existing = parsed;
4775
+ } catch {}
4776
+ const prompts = rovodevCommands.map((command) => ({
4777
+ name: command.getName(),
4778
+ description: command.getDescription(),
4779
+ content_file: toPosixPath(join("prompts", command.getRelativeFilePath()))
4780
+ })).toSorted((a, b) => a.name.localeCompare(b.name));
4781
+ return [new RovodevPromptsManifest({
4782
+ outputRoot,
4783
+ relativeDirPath: ROVODEV_DIR,
4784
+ relativeFilePath: ROVODEV_PROMPTS_FILE_NAME,
4785
+ fileContent: dump({
4786
+ ...existing,
4787
+ prompts
4788
+ }),
4789
+ global
4790
+ })];
4791
+ }
4792
+ };
4793
+ /**
4794
+ * The `prompts` entry shape read back from `content_file`, matched against
4795
+ * either the resolved path (relative to `prompts.yml`, i.e. the `.rovodev/`
4796
+ * directory) or the entry `name`, to recover the `description` that isn't
4797
+ * stored in the content file itself.
4798
+ */
4799
+ async function lookupPromptDescription({ outputRoot, relativeFilePath, name }) {
4800
+ const manifestContent = await readFileContentOrNull(join(outputRoot, ROVODEV_DIR, ROVODEV_PROMPTS_FILE_NAME));
4801
+ if (!manifestContent) return "";
4802
+ let parsed;
4803
+ try {
4804
+ parsed = load(manifestContent);
4805
+ } catch {
4806
+ return "";
4807
+ }
4808
+ if (!isPlainObject(parsed) || !Array.isArray(parsed.prompts)) return "";
4809
+ const expectedContentFile = toPosixPath(join("prompts", relativeFilePath));
4810
+ const entry = parsed.prompts.find((candidate) => isRecord(candidate) && (candidate.content_file === expectedContentFile || candidate.name === name));
4811
+ return isRecord(entry) && typeof entry.description === "string" ? entry.description : "";
4812
+ }
4813
+ /**
4814
+ * The shared `.rovodev/prompts.yml` manifest that indexes every saved prompt.
4815
+ * Never deleted by orphan cleanup: it is regenerated (not individually
4816
+ * discovered) alongside the per-prompt content files it references.
4817
+ */
4818
+ var RovodevPromptsManifest = class extends ToolFile {
4819
+ isDeletable() {
4820
+ return false;
4821
+ }
4822
+ validate() {
4823
+ return {
4824
+ success: true,
4825
+ error: null
4826
+ };
4827
+ }
4828
+ };
4829
+ //#endregion
4403
4830
  //#region src/constants/takt-paths.ts
4404
4831
  const TAKT_DIR = ".takt";
4405
4832
  const TAKT_FACETS_SUBDIR = "facets";
@@ -4417,6 +4844,14 @@ const TAKT_RULE_OVERVIEW_FILE_NAME = "overview.md";
4417
4844
  * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4418
4845
  */
4419
4846
  const TAKT_CONFIG_FILE_NAME = "config.yaml";
4847
+ /**
4848
+ * Top-level key in Takt's `config.yaml` holding the workflow MCP transport
4849
+ * allowlist (`stdio` / `sse` / `http` booleans). Takt is default-deny: a
4850
+ * transport must be explicitly enabled here before any workflow-defined MCP
4851
+ * server using it is permitted to run.
4852
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
4853
+ */
4854
+ const TAKT_WORKFLOW_MCP_SERVERS_KEY = "workflow_mcp_servers";
4420
4855
  //#endregion
4421
4856
  //#region src/features/takt-shared.ts
4422
4857
  /**
@@ -4592,6 +5027,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4592
5027
  supportsSubdirectory: false
4593
5028
  }
4594
5029
  }],
5030
+ ["antigravity-cli", {
5031
+ class: AntigravityCliCommand,
5032
+ meta: {
5033
+ extension: "md",
5034
+ supportsProject: true,
5035
+ supportsGlobal: true,
5036
+ isSimulated: false,
5037
+ supportsSubdirectory: false
5038
+ }
5039
+ }],
4595
5040
  ["antigravity-ide", {
4596
5041
  class: AntigravityIdeCommand,
4597
5042
  meta: {
@@ -4737,7 +5182,7 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4737
5182
  meta: {
4738
5183
  extension: "md",
4739
5184
  supportsProject: true,
4740
- supportsGlobal: false,
5185
+ supportsGlobal: true,
4741
5186
  isSimulated: false,
4742
5187
  supportsSubdirectory: false
4743
5188
  }
@@ -4782,6 +5227,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4782
5227
  supportsSubdirectory: true
4783
5228
  }
4784
5229
  }],
5230
+ ["reasonix", {
5231
+ class: ReasonixCommand,
5232
+ meta: {
5233
+ extension: "md",
5234
+ supportsProject: true,
5235
+ supportsGlobal: true,
5236
+ isSimulated: false,
5237
+ supportsSubdirectory: true
5238
+ }
5239
+ }],
4785
5240
  ["roo", {
4786
5241
  class: RooCommand,
4787
5242
  meta: {
@@ -4792,6 +5247,16 @@ const toolCommandFactories = /* @__PURE__ */ new Map([
4792
5247
  supportsSubdirectory: true
4793
5248
  }
4794
5249
  }],
5250
+ ["rovodev", {
5251
+ class: RovodevCommand,
5252
+ meta: {
5253
+ extension: "md",
5254
+ supportsProject: true,
5255
+ supportsGlobal: true,
5256
+ isSimulated: false,
5257
+ supportsSubdirectory: false
5258
+ }
5259
+ }],
4795
5260
  ["takt", {
4796
5261
  class: TaktCommand,
4797
5262
  meta: {
@@ -4849,7 +5314,7 @@ var CommandsProcessor = class extends FeatureProcessor {
4849
5314
  const rulesyncCommands = rulesyncFiles.filter((file) => file instanceof RulesyncCommand);
4850
5315
  const factory = this.getFactory(this.toolTarget);
4851
5316
  const flattenedPathOrigins = /* @__PURE__ */ new Map();
4852
- return rulesyncCommands.map((rulesyncCommand) => {
5317
+ const toolCommands = rulesyncCommands.map((rulesyncCommand) => {
4853
5318
  if (!factory.class.isTargetedByRulesyncCommand(rulesyncCommand)) return null;
4854
5319
  const originalRelativePath = rulesyncCommand.getRelativeFilePath();
4855
5320
  const commandToConvert = factory.meta.supportsSubdirectory ? rulesyncCommand : this.flattenRelativeFilePath(rulesyncCommand);
@@ -4865,6 +5330,14 @@ var CommandsProcessor = class extends FeatureProcessor {
4865
5330
  global: this.global
4866
5331
  });
4867
5332
  }).filter((command) => command !== null);
5333
+ const auxiliaryFiles = await factory.class.getAuxiliaryFiles?.({
5334
+ toolCommands,
5335
+ outputRoot: this.outputRoot,
5336
+ global: this.global
5337
+ });
5338
+ const result = [...toolCommands];
5339
+ if (auxiliaryFiles && auxiliaryFiles.length > 0) result.push(...auxiliaryFiles);
5340
+ return result;
4868
5341
  }
4869
5342
  async convertToolFilesToRulesyncFiles(toolFiles) {
4870
5343
  return toolFiles.filter((file) => file instanceof ToolCommand).map((toolCommand) => {
@@ -5023,7 +5496,14 @@ const HookDefinitionSchema = z.looseObject({
5023
5496
  name: z.optional(safeString),
5024
5497
  description: z.optional(safeString),
5025
5498
  failClosed: z.optional(z.boolean()),
5026
- sequential: z.optional(z.boolean())
5499
+ sequential: z.optional(z.boolean()),
5500
+ async: z.optional(z.boolean()),
5501
+ env: z.optional(z.record(z.string(), safeString)),
5502
+ shell: z.optional(safeString),
5503
+ statusMessage: z.optional(safeString),
5504
+ headers: z.optional(z.record(z.string(), safeString)),
5505
+ allowedEnvVars: z.optional(z.array(z.string())),
5506
+ once: z.optional(z.boolean())
5027
5507
  });
5028
5508
  /** Hook events supported by Cursor. */
5029
5509
  const CURSOR_HOOK_EVENTS = [
@@ -5087,6 +5567,30 @@ const CLAUDE_HOOK_EVENTS = [
5087
5567
  "elicitation",
5088
5568
  "elicitationResult"
5089
5569
  ];
5570
+ /**
5571
+ * Hook events supported by Devin Local (native `.devin/` hooks).
5572
+ *
5573
+ * Devin Local adopts a Claude-Code-style lifecycle hooks surface. It documents
5574
+ * seven events: `PreToolUse`, `PostToolUse`, `PermissionRequest`,
5575
+ * `UserPromptSubmit`, `Stop`, `SessionStart`, and `SessionEnd`. The
5576
+ * tool/permission events (`PreToolUse`/`PostToolUse`/`PermissionRequest`) carry
5577
+ * a `matcher` (regex against `tool_name`); the session/turn events do not.
5578
+ *
5579
+ * Hooks live in `.devin/hooks.v1.json` (project, standalone — the hooks object
5580
+ * is the entire file) or under the `"hooks"` key of `.devin/config.json` /
5581
+ * `~/.config/devin/config.json`.
5582
+ *
5583
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
5584
+ */
5585
+ const DEVIN_HOOK_EVENTS = [
5586
+ "sessionStart",
5587
+ "sessionEnd",
5588
+ "preToolUse",
5589
+ "postToolUse",
5590
+ "beforeSubmitPrompt",
5591
+ "stop",
5592
+ "permissionRequest"
5593
+ ];
5090
5594
  /** Hook events supported by OpenCode. */
5091
5595
  const OPENCODE_HOOK_EVENTS = [
5092
5596
  "sessionStart",
@@ -5129,7 +5633,11 @@ const COPILOT_HOOK_EVENTS = [
5129
5633
  * `sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
5130
5634
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
5131
5635
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
5132
- * `notification`.
5636
+ * `notification`, `preMcpToolCall` ← `beforeMCPExecution`.
5637
+ *
5638
+ * `preMcpToolCall` (canonical `beforeMCPExecution`) was added in Copilot CLI
5639
+ * v1.0.51 (2026-05-20) for hook providers to control outgoing MCP request
5640
+ * metadata. https://github.com/github/copilot-cli/blob/main/changelog.md
5133
5641
  *
5134
5642
  * @see https://docs.github.com/en/copilot/reference/hooks-configuration
5135
5643
  */
@@ -5146,7 +5654,8 @@ const COPILOTCLI_HOOK_EVENTS = [
5146
5654
  "afterError",
5147
5655
  "preCompact",
5148
5656
  "permissionRequest",
5149
- "notification"
5657
+ "notification",
5658
+ "beforeMCPExecution"
5150
5659
  ];
5151
5660
  /**
5152
5661
  * Hook events supported by Factory Droid.
@@ -5231,6 +5740,25 @@ const KIRO_HOOK_EVENTS = [
5231
5740
  "stop"
5232
5741
  ];
5233
5742
  /**
5743
+ * Hook events supported by the Kiro IDE (`.kiro/hooks/*.json` v1).
5744
+ *
5745
+ * Kiro IDE 1.0 exposes PascalCase triggers. rulesync maps the canonical
5746
+ * lifecycle events that have a clean 1:1 IDE equivalent: `SessionStart`,
5747
+ * `Stop`, `UserPromptSubmit`, `PreToolUse`, and `PostToolUse`. The IDE also
5748
+ * documents file-event (`PostFileCreate`/`PostFileSave`/`PostFileDelete`) and
5749
+ * spec-task (`PreTaskExec`/`PostTaskExec`) triggers that have no canonical
5750
+ * equivalent; those can still be emitted verbatim via a `kiro-ide` override
5751
+ * block (unknown event keys pass through unchanged).
5752
+ * @see https://kiro.dev/docs/hooks/types/
5753
+ */
5754
+ const KIRO_IDE_HOOK_EVENTS = [
5755
+ "sessionStart",
5756
+ "beforeSubmitPrompt",
5757
+ "preToolUse",
5758
+ "postToolUse",
5759
+ "stop"
5760
+ ];
5761
+ /**
5234
5762
  * Hook events supported by Google Antigravity (both the IDE and the CLI).
5235
5763
  *
5236
5764
  * Antigravity exposes a Claude-style hooks surface covering the five
@@ -5281,18 +5809,23 @@ const VIBE_HOOK_EVENTS = [
5281
5809
  /**
5282
5810
  * Hook events supported by JetBrains Junie CLI.
5283
5811
  *
5284
- * Junie CLI exposes four lifecycle events under the `"hooks"` key of
5285
- * `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `Stop`, and
5286
- * `SessionEnd`. Matchers apply only to `SessionStart` / `SessionEnd`
5287
- * (e.g. `startup` / `resume`); `UserPromptSubmit` and `Stop` are
5288
- * matcher-less. Only `type: "command"` hooks are supported. Project-local
5289
- * hooks are ignored for safety.
5812
+ * Junie CLI exposes seven lifecycle events under the `"hooks"` key of
5813
+ * `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
5814
+ * `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`. Matchers apply
5815
+ * to `SessionStart` (source), `PreToolUse` (tool name), `StopFailure` (error
5816
+ * type), `PermissionRequest` (tool name), and `SessionEnd` (reason);
5817
+ * `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
5818
+ * `type: "command"` hooks are supported. Project-local hooks are ignored for
5819
+ * safety.
5290
5820
  * @see https://junie.jetbrains.com/docs/junie-cli-hooks.html
5291
5821
  */
5292
5822
  const JUNIE_HOOK_EVENTS = [
5293
5823
  "sessionStart",
5294
5824
  "beforeSubmitPrompt",
5825
+ "preToolUse",
5295
5826
  "stop",
5827
+ "stopFailure",
5828
+ "permissionRequest",
5296
5829
  "sessionEnd"
5297
5830
  ];
5298
5831
  /**
@@ -5324,6 +5857,69 @@ const QWENCODE_HOOK_EVENTS = [
5324
5857
  "todoCreated",
5325
5858
  "todoCompleted"
5326
5859
  ];
5860
+ /**
5861
+ * Hook events supported by Reasonix.
5862
+ *
5863
+ * Reasonix's `.reasonix/settings.json` (project) / `~/.reasonix/settings.json`
5864
+ * (global) documents a ten-event surface (`PreToolUse`, `PostToolUse`,
5865
+ * `UserPromptSubmit`, `Stop`, `PostLLMCall`, `SessionStart`, `SessionEnd`,
5866
+ * `SubagentStop`, `Notification`, `PreCompact`), but only the four events the
5867
+ * upstream issue scoped in are mapped here: `PreToolUse`, `PostToolUse`,
5868
+ * `UserPromptSubmit` ← `beforeSubmitPrompt`, and `Stop`. `match` (Reasonix's
5869
+ * matcher field name) is honored only on `PreToolUse`/`PostToolUse`, matching
5870
+ * the canonical `matcher` field's tool-event scoping used by other adapters.
5871
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
5872
+ */
5873
+ const REASONIX_HOOK_EVENTS = [
5874
+ "preToolUse",
5875
+ "postToolUse",
5876
+ "beforeSubmitPrompt",
5877
+ "stop"
5878
+ ];
5879
+ /**
5880
+ * Hook events supported by Hermes Agent's native Shell Hooks system.
5881
+ *
5882
+ * Hermes validates hook events against a fixed `VALID_HOOKS` set:
5883
+ * `pre_tool_call`, `post_tool_call`, `pre_llm_call`, `post_llm_call`,
5884
+ * `pre_verify`, `on_session_start`, `on_session_end`, `on_session_finalize`,
5885
+ * `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`,
5886
+ * `pre_approval_request`, `post_approval_response`, `transform_tool_result`,
5887
+ * `transform_terminal_output`, `transform_llm_output`. Only the events with a
5888
+ * clean 1:1 canonical equivalent are mapped here; the remaining `VALID_HOOKS`
5889
+ * entries (`pre_verify`, `on_session_finalize`, `on_session_reset`,
5890
+ * `pre_gateway_dispatch`, `pre_approval_request`, `post_approval_response`, the
5891
+ * `transform_*` result-rewriting hooks) have no canonical rulesync equivalent,
5892
+ * so no canonical event maps to them.
5893
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
5894
+ */
5895
+ const HERMESAGENT_HOOK_EVENTS = [
5896
+ "sessionStart",
5897
+ "sessionEnd",
5898
+ "preToolUse",
5899
+ "postToolUse",
5900
+ "preModelInvocation",
5901
+ "postModelInvocation",
5902
+ "subagentStart",
5903
+ "subagentStop"
5904
+ ];
5905
+ /**
5906
+ * Map canonical camelCase event names to Hermes Agent's native `VALID_HOOKS`
5907
+ * snake_case keys under the `hooks:` block of `~/.hermes/config.yaml`.
5908
+ */
5909
+ const CANONICAL_TO_HERMESAGENT_EVENT_NAMES = {
5910
+ sessionStart: "on_session_start",
5911
+ sessionEnd: "on_session_end",
5912
+ preToolUse: "pre_tool_call",
5913
+ postToolUse: "post_tool_call",
5914
+ preModelInvocation: "pre_llm_call",
5915
+ postModelInvocation: "post_llm_call",
5916
+ subagentStart: "subagent_start",
5917
+ subagentStop: "subagent_stop"
5918
+ };
5919
+ /**
5920
+ * Map Hermes Agent's native `VALID_HOOKS` keys back to canonical camelCase.
5921
+ */
5922
+ const HERMESAGENT_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_HERMESAGENT_EVENT_NAMES).map(([k, v]) => [v, k]));
5327
5923
  const hooksRecordSchema = z.record(z.string(), z.array(HookDefinitionSchema));
5328
5924
  /**
5329
5925
  * Canonical hooks config (canonical event names in camelCase).
@@ -5343,12 +5939,15 @@ const HooksConfigSchema = z.looseObject({
5343
5939
  deepagents: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5344
5940
  kiro: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5345
5941
  "kiro-cli": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5942
+ "kiro-ide": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5346
5943
  devin: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5347
5944
  augmentcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5348
5945
  "antigravity-ide": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5349
5946
  "antigravity-cli": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5947
+ hermesagent: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5350
5948
  junie: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5351
5949
  vibe: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5950
+ reasonix: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
5352
5951
  qwencode: z.optional(z.looseObject({
5353
5952
  hooks: z.optional(hooksRecordSchema),
5354
5953
  disableAllHooks: z.optional(z.boolean())
@@ -5394,6 +5993,26 @@ const CANONICAL_TO_CLAUDE_EVENT_NAMES = {
5394
5993
  */
5395
5994
  const CLAUDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CLAUDE_EVENT_NAMES).map(([k, v]) => [v, k]));
5396
5995
  /**
5996
+ * Map canonical camelCase event names to Devin Local PascalCase.
5997
+ *
5998
+ * Devin Local reuses the same Claude-style PascalCase event names for the
5999
+ * subset of events it supports.
6000
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
6001
+ */
6002
+ const CANONICAL_TO_DEVIN_EVENT_NAMES = {
6003
+ sessionStart: "SessionStart",
6004
+ sessionEnd: "SessionEnd",
6005
+ preToolUse: "PreToolUse",
6006
+ postToolUse: "PostToolUse",
6007
+ beforeSubmitPrompt: "UserPromptSubmit",
6008
+ stop: "Stop",
6009
+ permissionRequest: "PermissionRequest"
6010
+ };
6011
+ /**
6012
+ * Map Devin Local PascalCase event names to canonical camelCase.
6013
+ */
6014
+ const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
6015
+ /**
5397
6016
  * Map canonical camelCase event names to AugmentCode PascalCase.
5398
6017
  * Auggie reuses the same PascalCase names as Claude for the events it supports.
5399
6018
  */
@@ -5524,7 +6143,8 @@ const CANONICAL_TO_COPILOTCLI_EVENT_NAMES = {
5524
6143
  afterError: "errorOccurred",
5525
6144
  preCompact: "preCompact",
5526
6145
  permissionRequest: "permissionRequest",
5527
- notification: "notification"
6146
+ notification: "notification",
6147
+ beforeMCPExecution: "preMcpToolCall"
5528
6148
  };
5529
6149
  /** Map GitHub Copilot CLI event names back to canonical camelCase. */
5530
6150
  const COPILOTCLI_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_COPILOTCLI_EVENT_NAMES).map(([k, v]) => [v, k]));
@@ -5606,13 +6226,35 @@ const CANONICAL_TO_KIRO_EVENT_NAMES = {
5606
6226
  */
5607
6227
  const KIRO_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_EVENT_NAMES).map(([k, v]) => [v, k]));
5608
6228
  /**
6229
+ * Map canonical camelCase event names to Kiro IDE PascalCase triggers.
6230
+ *
6231
+ * Only the canonical lifecycle events with a clean IDE equivalent are mapped.
6232
+ * Unknown keys (e.g. IDE-only `PostFileSave`/`PreTaskExec` set via a `kiro-ide`
6233
+ * override) pass through unchanged.
6234
+ * @see https://kiro.dev/docs/hooks/types/
6235
+ */
6236
+ const CANONICAL_TO_KIRO_IDE_EVENT_NAMES = {
6237
+ sessionStart: "SessionStart",
6238
+ beforeSubmitPrompt: "UserPromptSubmit",
6239
+ preToolUse: "PreToolUse",
6240
+ postToolUse: "PostToolUse",
6241
+ stop: "Stop"
6242
+ };
6243
+ /**
6244
+ * Map Kiro IDE PascalCase trigger names to canonical camelCase.
6245
+ */
6246
+ const KIRO_IDE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_KIRO_IDE_EVENT_NAMES).map(([k, v]) => [v, k]));
6247
+ /**
5609
6248
  * Map canonical camelCase event names to Junie PascalCase.
5610
6249
  * Junie reuses the same PascalCase names as Claude for the events it supports.
5611
6250
  */
5612
6251
  const CANONICAL_TO_JUNIE_EVENT_NAMES = {
5613
6252
  sessionStart: "SessionStart",
5614
6253
  beforeSubmitPrompt: "UserPromptSubmit",
6254
+ preToolUse: "PreToolUse",
5615
6255
  stop: "Stop",
6256
+ stopFailure: "StopFailure",
6257
+ permissionRequest: "PermissionRequest",
5616
6258
  sessionEnd: "SessionEnd"
5617
6259
  };
5618
6260
  /**
@@ -5665,19 +6307,22 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
5665
6307
  * Map Qwen Code PascalCase event names to canonical camelCase.
5666
6308
  */
5667
6309
  const QWENCODE_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_QWENCODE_EVENT_NAMES).map(([k, v]) => [v, k]));
5668
- //#endregion
5669
- //#region src/constants/antigravity-paths.ts
5670
- const ANTIGRAVITY_DIR = ".agents";
5671
- const ANTIGRAVITY_SKILLS_DIR_PATH = join(ANTIGRAVITY_DIR, "skills");
5672
- const ANTIGRAVITY_MCP_FILE_NAME = "mcp_config.json";
5673
- const ANTIGRAVITY_HOOKS_FILE_NAME = "hooks.json";
5674
- const ANTIGRAVITY_IGNORE_FILE_NAME = ".geminiignore";
5675
- const ANTIGRAVITY_GEMINI_DIR = ".gemini";
5676
- const ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR = "antigravity-cli";
5677
- const ANTIGRAVITY_CLI_PERMISSIONS_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_CLI_PERMISSIONS_SUBDIR);
5678
- const ANTIGRAVITY_CLI_PERMISSIONS_FILE_NAME = "settings.json";
5679
- const ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR = "config";
5680
- const ANTIGRAVITY_GLOBAL_CONFIG_DIR_PATH = join(ANTIGRAVITY_GEMINI_DIR, ANTIGRAVITY_GLOBAL_CONFIG_SUBDIR);
6310
+ /**
6311
+ * Map canonical camelCase event names to Reasonix PascalCase.
6312
+ * Reasonix explicitly mirrors Claude Code's hooks model, so it reuses the same
6313
+ * PascalCase names for the four events rulesync maps.
6314
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
6315
+ */
6316
+ const CANONICAL_TO_REASONIX_EVENT_NAMES = {
6317
+ preToolUse: "PreToolUse",
6318
+ postToolUse: "PostToolUse",
6319
+ beforeSubmitPrompt: "UserPromptSubmit",
6320
+ stop: "Stop"
6321
+ };
6322
+ /**
6323
+ * Map Reasonix PascalCase event names to canonical camelCase.
6324
+ */
6325
+ const REASONIX_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_EVENT_NAMES).map(([k, v]) => [v, k]));
5681
6326
  //#endregion
5682
6327
  //#region src/utils/prototype-pollution.ts
5683
6328
  /**
@@ -5725,7 +6370,7 @@ function isToolMatcherEntry(x) {
5725
6370
  /**
5726
6371
  * Filter the shared canonical hooks to the supported events and merge tool overrides on top.
5727
6372
  */
5728
- function buildEffectiveHooks({ config, toolOverrideHooks, supportedEvents }) {
6373
+ function buildEffectiveHooks$1({ config, toolOverrideHooks, supportedEvents }) {
5729
6374
  const supported = new Set(supportedEvents);
5730
6375
  const sharedHooks = {};
5731
6376
  for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
@@ -5754,7 +6399,7 @@ function groupDefinitionsByMatcher(definitions) {
5754
6399
  function applyCommandPrefix({ def, converterConfig }) {
5755
6400
  const commandText = def.command;
5756
6401
  const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
5757
- return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `${converterConfig.projectDirVar}/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
6402
+ return converterConfig.projectDirVar !== "" && typeof trimmedCommand === "string" && !trimmedCommand.startsWith("$") && (!converterConfig.prefixDotRelativeCommandsOnly || trimmedCommand.startsWith(".")) && typeof trimmedCommand === "string" ? `"${converterConfig.projectDirVar}"/${trimmedCommand.replace(/^\.\//, "")}` : def.command;
5758
6403
  }
5759
6404
  /**
5760
6405
  * Convert the definitions of a single matcher group into tool hook entries,
@@ -5787,7 +6432,7 @@ function buildToolHooks({ defs, converterConfig }) {
5787
6432
  * (e.g. beforeSubmitPrompt → UserPromptSubmit).
5788
6433
  */
5789
6434
  function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logger }) {
5790
- const effectiveHooks = buildEffectiveHooks({
6435
+ const effectiveHooks = buildEffectiveHooks$1({
5791
6436
  config,
5792
6437
  toolOverrideHooks,
5793
6438
  supportedEvents: converterConfig.supportedEvents
@@ -5830,7 +6475,13 @@ function canonicalToToolHooks({ config, toolOverrideHooks, converterConfig, logg
5830
6475
  */
5831
6476
  function stripCommandPrefix({ command, converterConfig }) {
5832
6477
  const cmd = typeof command === "string" ? command : void 0;
5833
- if (converterConfig.projectDirVar !== "" && typeof cmd === "string" && cmd.includes(`${converterConfig.projectDirVar}/`)) return cmd.replace(new RegExp(`^${converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/?`), "./");
6478
+ if (converterConfig.projectDirVar === "" || typeof cmd !== "string") return cmd;
6479
+ const quotedPrefix = `"${converterConfig.projectDirVar}"/`;
6480
+ if (cmd.startsWith(quotedPrefix)) return `./${cmd.slice(quotedPrefix.length)}`;
6481
+ if (cmd.includes(`${converterConfig.projectDirVar}/`)) {
6482
+ const escapedVar = converterConfig.projectDirVar.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6483
+ return cmd.replace(new RegExp(`^${escapedVar}\\/?`), "./");
6484
+ }
5834
6485
  return cmd;
5835
6486
  }
5836
6487
  /**
@@ -5883,9 +6534,6 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
5883
6534
  return canonical;
5884
6535
  }
5885
6536
  //#endregion
5886
- //#region src/types/tool-file.ts
5887
- var ToolFile = class extends AiFile {};
5888
- //#endregion
5889
6537
  //#region src/features/hooks/rulesync-hooks.ts
5890
6538
  var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
5891
6539
  json;
@@ -6439,9 +7087,15 @@ const CODEXCLI_CONVERTER_CONFIG = {
6439
7087
  passthroughFields: ["name", "description"]
6440
7088
  };
6441
7089
  /**
6442
- * Build the content for `.codex/config.toml` with `[features] hooks = true`.
6443
- * Reads the existing file (if any), parses TOML, sets the flag, and returns the content
6444
- * without writing to disk. The caller is responsible for writing via the normal write phase.
7090
+ * Build the content for `.codex/config.toml`, cleaning up the deprecated `codex_hooks` key.
7091
+ * Reads the existing file (if any), parses TOML, and returns the content without writing to
7092
+ * disk. The caller is responsible for writing via the normal write phase.
7093
+ *
7094
+ * Hooks are GA and enabled by default in Codex CLI, so `[features] hooks = true` is no longer
7095
+ * required and is intentionally NOT force-written here (doing so used to be harmless/idempotent,
7096
+ * but is now redundant and could mask a user's own `hooks = false` opt-out on a later edit).
7097
+ * See https://developers.openai.com/codex/hooks. Only the legacy `codex_hooks` alias — superseded
7098
+ * by `hooks` — is still cleaned up.
6445
7099
  */
6446
7100
  async function buildCodexConfigTomlContent({ outputRoot }) {
6447
7101
  const configPath = join(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME);
@@ -6452,10 +7106,7 @@ async function buildCodexConfigTomlContent({ outputRoot }) {
6452
7106
  } catch (error) {
6453
7107
  throw new Error(`Failed to parse existing Codex CLI config at ${configPath}: ${formatError(error)}`, { cause: error });
6454
7108
  }
6455
- if (typeof configToml.features !== "object" || configToml.features === null) configToml.features = {};
6456
- const features = configToml.features;
6457
- delete features.codex_hooks;
6458
- features.hooks = true;
7109
+ if (typeof configToml.features === "object" && configToml.features !== null) delete configToml.features.codex_hooks;
6459
7110
  return smolToml.stringify(configToml);
6460
7111
  }
6461
7112
  /**
@@ -6723,6 +7374,20 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6723
7374
  }
6724
7375
  };
6725
7376
  //#endregion
7377
+ //#region src/utils/object.ts
7378
+ /**
7379
+ * Return a shallow copy of `obj` keeping only the entries whose value is
7380
+ * neither `undefined` nor `null`.
7381
+ *
7382
+ * Used to assemble generated config objects without one conditional spread per
7383
+ * optional field (which would otherwise exceed the lint complexity budget).
7384
+ */
7385
+ function compact(obj) {
7386
+ const result = {};
7387
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0 && value !== null) result[key] = value;
7388
+ return result;
7389
+ }
7390
+ //#endregion
6726
7391
  //#region src/features/hooks/copilotcli-hooks.ts
6727
7392
  /**
6728
7393
  * GitHub Copilot CLI hooks.
@@ -6734,7 +7399,7 @@ var CopilotHooks = class CopilotHooks extends ToolHooks {
6734
7399
  * (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
6735
7400
  * `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
6736
7401
  * `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
6737
- * `notification`). Each entry supports three hook types:
7402
+ * `notification`, `preMcpToolCall`). Each entry supports three hook types:
6738
7403
  *
6739
7404
  * - `command` — the `bash` / `powershell` command-field shape with optional
6740
7405
  * `timeoutSec`, plus optional `cwd` / `env`.
@@ -6847,20 +7512,24 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
6847
7512
  } else if (hookType === "http") entries.push({
6848
7513
  type: "http",
6849
7514
  ...matcherPart,
6850
- ...def.url !== void 0 && def.url !== null && { url: def.url },
7515
+ ...compact({
7516
+ url: def.url,
7517
+ headers: def.headers,
7518
+ allowedEnvVars: def.allowedEnvVars
7519
+ }),
7520
+ ...timeoutPart,
7521
+ ...rest
7522
+ });
7523
+ else entries.push({
7524
+ type: "command",
7525
+ ...matcherPart,
7526
+ ...compact({
7527
+ [commandField]: def.command,
7528
+ env: def.env
7529
+ }),
6851
7530
  ...timeoutPart,
6852
7531
  ...rest
6853
7532
  });
6854
- else {
6855
- const command = def.command;
6856
- entries.push({
6857
- type: "command",
6858
- ...matcherPart,
6859
- ...command !== void 0 && command !== null && { [commandField]: command },
6860
- ...timeoutPart,
6861
- ...rest
6862
- });
6863
- }
6864
7533
  }
6865
7534
  return entries;
6866
7535
  }
@@ -7269,148 +7938,63 @@ var DeepagentsHooks = class DeepagentsHooks extends ToolHooks {
7269
7938
  };
7270
7939
  //#endregion
7271
7940
  //#region src/features/hooks/devin-hooks.ts
7941
+ const DEVIN_CONVERTER_CONFIG = {
7942
+ supportedEvents: DEVIN_HOOK_EVENTS,
7943
+ canonicalToToolEventNames: CANONICAL_TO_DEVIN_EVENT_NAMES,
7944
+ toolToCanonicalEventNames: DEVIN_TO_CANONICAL_EVENT_NAMES,
7945
+ projectDirVar: "",
7946
+ supportedHookTypes: /* @__PURE__ */ new Set(["command", "prompt"]),
7947
+ noMatcherEvents: /* @__PURE__ */ new Set([
7948
+ "sessionStart",
7949
+ "sessionEnd",
7950
+ "stop",
7951
+ "beforeSubmitPrompt"
7952
+ ])
7953
+ };
7272
7954
  /**
7273
- * Map canonical camelCase event names to Devin event names.
7274
- *
7275
- * The mapping is bijective for every event with a Devin equivalent, so the
7276
- * conversion round-trips cleanly. Devin splits the generic tool lifecycle
7277
- * into file/command/MCP specific events, so we line them up with the closest
7278
- * file/command/MCP specific canonical events rather than the generic
7279
- * preToolUse/postToolUse pair:
7280
- *
7281
- * - beforeReadFile ⇄ pre_read_code
7282
- * - beforeTabFileRead ⇄ post_read_code
7283
- * - afterTabFileEdit ⇄ pre_write_code
7284
- * - afterFileEdit ⇄ post_write_code
7285
- * - beforeShellExecution⇄ pre_run_command
7286
- * - afterShellExecution ⇄ post_run_command
7287
- * - beforeMCPExecution ⇄ pre_mcp_tool_use
7288
- * - afterMCPExecution ⇄ post_mcp_tool_use
7289
- * - beforeSubmitPrompt ⇄ pre_user_prompt
7290
- * - afterAgentResponse ⇄ post_cascade_response
7291
- * - beforeAgentResponse ⇄ post_cascade_response_with_transcript
7292
- * - worktreeCreate ⇄ post_setup_worktree
7293
- *
7294
- * NOTE on the before/after prefix mismatch: rulesync's canonical vocabulary has
7295
- * no `afterReadFile` or `beforeWriteFile`/`beforeFileEdit` event — the only read
7296
- * events are `beforeReadFile`/`beforeTabFileRead` (both read-side) and the only
7297
- * edit events are `afterFileEdit`/`afterTabFileEdit` (both edit-side). To cover
7298
- * all twelve Devin events bijectively we therefore pair the second read/write
7299
- * event by DOMAIN (read↔read, write↔write) rather than by timing, which inverts
7300
- * the prefix on `post_read_code` (⇄ beforeTabFileRead) and `pre_write_code`
7301
- * (⇄ afterTabFileEdit). The alternative — pairing by timing — would leave two
7302
- * Devin events unmapped and drop user hooks. The round-trip stays lossless;
7303
- * only the human-readable prefix differs, so this is a deliberate trade-off.
7304
- *
7305
- * Canonical events that have no Devin equivalent (e.g. sessionStart, stop)
7306
- * are dropped with a logged warning during export.
7307
- */
7308
- const CANONICAL_TO_DEVIN_EVENT_NAMES = {
7309
- beforeReadFile: "pre_read_code",
7310
- beforeTabFileRead: "post_read_code",
7311
- afterTabFileEdit: "pre_write_code",
7312
- afterFileEdit: "post_write_code",
7313
- beforeShellExecution: "pre_run_command",
7314
- afterShellExecution: "post_run_command",
7315
- beforeMCPExecution: "pre_mcp_tool_use",
7316
- afterMCPExecution: "post_mcp_tool_use",
7317
- beforeSubmitPrompt: "pre_user_prompt",
7318
- afterAgentResponse: "post_cascade_response",
7319
- beforeAgentResponse: "post_cascade_response_with_transcript",
7320
- worktreeCreate: "post_setup_worktree"
7321
- };
7322
- /**
7323
- * Map Devin event names back to canonical camelCase event names.
7324
- */
7325
- const DEVIN_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_DEVIN_EVENT_NAMES).map(([k, v]) => [v, k]));
7326
- /**
7327
- * Canonical hook events supported by Devin (the keys of the bijective map).
7955
+ * Hooks generator for Devin Local (native `.devin/` hooks).
7328
7956
  *
7329
- * Listed explicitly (rather than derived via `Object.keys`) so each value is
7330
- * type-checked against `HookEvent` and stays in sync with
7331
- * `CANONICAL_TO_DEVIN_EVENT_NAMES`.
7332
- */
7333
- const DEVIN_HOOK_EVENTS = [
7334
- "beforeReadFile",
7335
- "beforeTabFileRead",
7336
- "afterTabFileEdit",
7337
- "afterFileEdit",
7338
- "beforeShellExecution",
7339
- "afterShellExecution",
7340
- "beforeMCPExecution",
7341
- "afterMCPExecution",
7342
- "beforeSubmitPrompt",
7343
- "afterAgentResponse",
7344
- "beforeAgentResponse",
7345
- "worktreeCreate"
7346
- ];
7347
- function isRecord(value) {
7348
- return value !== null && typeof value === "object" && !Array.isArray(value);
7349
- }
7350
- /**
7351
- * Read the per-tool `devin` override hooks from the rulesync hooks config.
7957
+ * Devin Local adopts a Claude-Code-style lifecycle hooks model: each event maps
7958
+ * to an array of `{ matcher?, hooks: [{ type, command|prompt, timeout? }] }`
7959
+ * matcher groups.
7352
7960
  *
7353
- * `HooksConfigSchema` surfaces an optional `devin` block (mirroring every
7354
- * other hooks target), so the override hooks are read directly off the typed
7355
- * config without any cast.
7356
- */
7357
- function getDevinOverrideHooks(config) {
7358
- return config.devin?.hooks ?? {};
7359
- }
7360
- /**
7361
- * Convert a canonical hook definition into a Devin hook object, keeping only
7362
- * the Devin-supported fields. Returns null when neither command nor
7363
- * powershell is present (Devin requires at least one of them).
7364
- */
7365
- function canonicalDefToDevinHook(def) {
7366
- const hook = {};
7367
- if (typeof def.command === "string") hook.command = def.command;
7368
- if (typeof def.powershell === "string") hook.powershell = def.powershell;
7369
- if (typeof def.show_output === "boolean") hook.show_output = def.show_output;
7370
- if (typeof def.working_directory === "string") hook.working_directory = def.working_directory;
7371
- if (hook.command === void 0 && hook.powershell === void 0) return null;
7372
- return hook;
7373
- }
7374
- /**
7375
- * Convert a Devin hook object into a canonical hook definition.
7376
- */
7377
- function devinHookToCanonicalDef(hook) {
7378
- const def = { type: "command" };
7379
- if (typeof hook.command === "string") def.command = hook.command;
7380
- if (typeof hook.powershell === "string") def.powershell = hook.powershell;
7381
- if (typeof hook.show_output === "boolean") def.show_output = hook.show_output;
7382
- if (typeof hook.working_directory === "string") def.working_directory = hook.working_directory;
7383
- return def;
7384
- }
7385
- /**
7386
- * Hooks generator for Devin Cascade (GA).
7961
+ * - Project scope: `.devin/hooks.v1.json`. This is a standalone file whose top
7962
+ * level IS the event map (no wrapper key).
7963
+ * - Global scope: `~/.config/devin/config.json` under the `"hooks"` key. This
7964
+ * file is shared with the MCP (`mcpServers`) and permissions (`permissions`)
7965
+ * features, so reads and writes merge into the existing JSON and the file is
7966
+ * never deleted in global mode.
7387
7967
  *
7388
- * Writes a dedicated `hooks.json` whose top-level `hooks` key maps each
7389
- * Devin event name to a flat array of hook objects. Project and global modes
7390
- * share the same shape; only the location differs (`.windsurf/hooks.json` vs
7391
- * `~/.codeium/windsurf/hooks.json`). The harness overrides `outputRoot` with the
7392
- * home directory in global mode.
7968
+ * @see https://docs.devin.ai/cli/extensibility/hooks/overview
7393
7969
  */
7394
7970
  var DevinHooks = class DevinHooks extends ToolHooks {
7395
7971
  constructor(params) {
7396
7972
  super({
7397
7973
  ...params,
7398
- fileContent: params.fileContent ?? JSON.stringify({ hooks: {} }, null, 2)
7974
+ fileContent: params.fileContent ?? "{}"
7399
7975
  });
7400
7976
  }
7977
+ /**
7978
+ * The project standalone `hooks.v1.json` is owned wholesale by rulesync and
7979
+ * may be deleted as an orphan. The global `config.json` is shared with the MCP
7980
+ * and permissions features, so it must never be deleted.
7981
+ */
7982
+ isDeletable() {
7983
+ return this.getRelativeFilePath() !== DEVIN_CONFIG_FILE_NAME;
7984
+ }
7401
7985
  static getSettablePaths({ global = false } = {}) {
7402
7986
  if (global) return {
7403
- relativeDirPath: CODEIUM_WINDSURF_DIR,
7404
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
7987
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
7988
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
7405
7989
  };
7406
7990
  return {
7407
- relativeDirPath: WINDSURF_DIR,
7408
- relativeFilePath: DEVIN_HOOKS_FILE_NAME
7991
+ relativeDirPath: DEVIN_DIR,
7992
+ relativeFilePath: DEVIN_HOOKS_V1_FILE_NAME
7409
7993
  };
7410
7994
  }
7411
7995
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
7412
7996
  const paths = DevinHooks.getSettablePaths({ global });
7413
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ hooks: {} });
7997
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
7414
7998
  return new DevinHooks({
7415
7999
  outputRoot,
7416
8000
  relativeDirPath: paths.relativeDirPath,
@@ -7421,31 +8005,32 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7421
8005
  }
7422
8006
  static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
7423
8007
  const paths = DevinHooks.getSettablePaths({ global });
7424
- await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), JSON.stringify({ hooks: {} }, null, 2));
8008
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
7425
8009
  const config = rulesyncHooks.getJson();
7426
- const supported = new Set(DEVIN_HOOK_EVENTS);
7427
- const sharedHooks = {};
7428
- for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) sharedHooks[event] = defs;
7429
- const overrideHooks = getDevinOverrideHooks(config);
7430
- const effectiveHooks = {
7431
- ...sharedHooks,
7432
- ...overrideHooks
7433
- };
7434
- const devinHooks = {};
7435
- for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
7436
- const devinEvent = CANONICAL_TO_DEVIN_EVENT_NAMES[canonicalEvent];
7437
- if (devinEvent === void 0) {
7438
- logger?.warn(`Skipped hook event "${canonicalEvent}" for devin (no Devin equivalent)`);
7439
- continue;
7440
- }
7441
- const objects = [];
7442
- for (const def of defs) {
7443
- const hook = canonicalDefToDevinHook(def);
7444
- if (hook !== null) objects.push(hook);
8010
+ const devinHooks = canonicalToToolHooks({
8011
+ config,
8012
+ toolOverrideHooks: config.devin?.hooks,
8013
+ converterConfig: DEVIN_CONVERTER_CONFIG,
8014
+ logger
8015
+ });
8016
+ let fileContent;
8017
+ if (global) {
8018
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8019
+ let parsedSettings;
8020
+ try {
8021
+ parsedSettings = JSON.parse(existingContent);
8022
+ } catch (error) {
8023
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
7445
8024
  }
7446
- if (objects.length > 0) devinHooks[devinEvent] = objects;
8025
+ const merged = {
8026
+ ...isRecord(parsedSettings) ? parsedSettings : {},
8027
+ hooks: devinHooks
8028
+ };
8029
+ fileContent = JSON.stringify(merged, null, 2);
8030
+ } else {
8031
+ await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
8032
+ fileContent = JSON.stringify(devinHooks, null, 2);
7447
8033
  }
7448
- const fileContent = JSON.stringify({ hooks: devinHooks }, null, 2);
7449
8034
  return new DevinHooks({
7450
8035
  outputRoot,
7451
8036
  relativeDirPath: paths.relativeDirPath,
@@ -7461,22 +8046,13 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7461
8046
  } catch (error) {
7462
8047
  throw new Error(`Failed to parse Devin hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
7463
8048
  }
7464
- const canonicalHooks = {};
7465
- const devinHooks = parsed.hooks;
7466
- if (isRecord(devinHooks)) for (const [devinEvent, rawObjects] of Object.entries(devinHooks)) {
7467
- if (!Array.isArray(rawObjects)) continue;
7468
- const canonicalEvent = DEVIN_TO_CANONICAL_EVENT_NAMES[devinEvent];
7469
- if (canonicalEvent === void 0) continue;
7470
- const defs = [];
7471
- for (const rawObject of rawObjects) {
7472
- if (!isRecord(rawObject)) continue;
7473
- defs.push(devinHookToCanonicalDef(rawObject));
7474
- }
7475
- if (defs.length > 0) canonicalHooks[canonicalEvent] = defs;
7476
- }
8049
+ const hooks = toolHooksToCanonical({
8050
+ hooks: this.getRelativeFilePath() === "config.json" ? isRecord(parsed) && isRecord(parsed.hooks) ? parsed.hooks : {} : parsed,
8051
+ converterConfig: DEVIN_CONVERTER_CONFIG
8052
+ });
7477
8053
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
7478
8054
  version: 1,
7479
- hooks: canonicalHooks
8055
+ hooks
7480
8056
  }, null, 2) });
7481
8057
  }
7482
8058
  validate() {
@@ -7490,7 +8066,7 @@ var DevinHooks = class DevinHooks extends ToolHooks {
7490
8066
  outputRoot,
7491
8067
  relativeDirPath,
7492
8068
  relativeFilePath,
7493
- fileContent: JSON.stringify({ hooks: {} }, null, 2),
8069
+ fileContent: "{}",
7494
8070
  validate: false
7495
8071
  });
7496
8072
  }
@@ -7717,6 +8293,106 @@ function mergeHermesConfig(fileContent, patch) {
7717
8293
  }
7718
8294
  //#endregion
7719
8295
  //#region src/features/hooks/hermesagent-hooks.ts
8296
+ /**
8297
+ * Canonical events that map to a Hermes tool-call event (`pre_tool_call` /
8298
+ * `post_tool_call`) and therefore carry a `matcher`. Every other supported
8299
+ * canonical event maps to a Hermes lifecycle event, which never accepts a
8300
+ * `matcher`.
8301
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8302
+ */
8303
+ const HERMESAGENT_MATCHER_EVENTS = /* @__PURE__ */ new Set(["preToolUse", "postToolUse"]);
8304
+ /**
8305
+ * Filter the shared canonical hooks to the events Hermes understands and merge
8306
+ * the `hermesagent`-specific override block on top.
8307
+ */
8308
+ function buildEffectiveHooks(config, toolOverrideHooks) {
8309
+ const supported = new Set(HERMESAGENT_HOOK_EVENTS);
8310
+ const shared = {};
8311
+ for (const [event, defs] of Object.entries(config.hooks)) if (supported.has(event)) shared[event] = defs;
8312
+ return {
8313
+ ...shared,
8314
+ ...toolOverrideHooks
8315
+ };
8316
+ }
8317
+ /**
8318
+ * Convert the canonical hooks config into Hermes's native
8319
+ * `hooks: { <event>: [{ matcher?, command, timeout? }] }` shape.
8320
+ *
8321
+ * Only `type: "command"` canonical hooks are emitted — Hermes shell hooks run
8322
+ * via `shlex.split`/`shell=False`, so `prompt`/`http` hooks have no native
8323
+ * equivalent and are skipped (the shared `HooksProcessor` already warns about
8324
+ * unsupported hook types centrally). `matcher` is only carried through for
8325
+ * `pre_tool_call`/`post_tool_call`; on any other event it is dropped with a
8326
+ * warning, mirroring how other adapters (e.g. AugmentCode) handle
8327
+ * matcher-less lifecycle events.
8328
+ */
8329
+ function canonicalToHermesHooks({ config, toolOverrideHooks, logger }) {
8330
+ const effectiveHooks = buildEffectiveHooks(config, toolOverrideHooks);
8331
+ const result = {};
8332
+ for (const [canonicalEvent, defs] of Object.entries(effectiveHooks)) {
8333
+ const nativeEvent = CANONICAL_TO_HERMESAGENT_EVENT_NAMES[canonicalEvent];
8334
+ if (!nativeEvent) continue;
8335
+ const supportsMatcher = HERMESAGENT_MATCHER_EVENTS.has(canonicalEvent);
8336
+ const entries = [];
8337
+ for (const def of defs) {
8338
+ if ((def.type ?? "command") !== "command") continue;
8339
+ if (typeof def.command !== "string" || def.command === "") continue;
8340
+ const entry = { command: def.command };
8341
+ if (typeof def.matcher === "string" && def.matcher !== "") if (supportsMatcher) entry.matcher = def.matcher;
8342
+ else logger?.warn(`matcher "${def.matcher}" on "${canonicalEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
8343
+ if (typeof def.timeout === "number") entry.timeout = def.timeout;
8344
+ entries.push(entry);
8345
+ }
8346
+ if (entries.length > 0) result[nativeEvent] = entries;
8347
+ }
8348
+ return result;
8349
+ }
8350
+ /**
8351
+ * Reverse {@link canonicalToHermesHooks}: parse Hermes's native
8352
+ * `hooks: { <event>: [...] }` map back into a canonical event → definition[]
8353
+ * record. Native events with no canonical equivalent (`pre_verify`,
8354
+ * `transform_tool_result`, ...) are skipped since there is nothing to round
8355
+ * -trip them into.
8356
+ */
8357
+ function hermesHooksToCanonical(hooks) {
8358
+ const canonical = {};
8359
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
8360
+ for (const [nativeEvent, entries] of Object.entries(hooks)) {
8361
+ if (PROTOTYPE_POLLUTION_KEYS.has(nativeEvent) || !Array.isArray(entries)) continue;
8362
+ const canonicalEvent = HERMESAGENT_TO_CANONICAL_EVENT_NAMES[nativeEvent];
8363
+ if (!canonicalEvent) continue;
8364
+ const defs = [];
8365
+ for (const raw of entries) {
8366
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) continue;
8367
+ const entry = raw;
8368
+ if (typeof entry.command !== "string") continue;
8369
+ const def = {
8370
+ type: "command",
8371
+ command: entry.command
8372
+ };
8373
+ if (typeof entry.matcher === "string" && entry.matcher !== "") def.matcher = entry.matcher;
8374
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout;
8375
+ defs.push(def);
8376
+ }
8377
+ if (defs.length > 0) canonical[canonicalEvent] = defs;
8378
+ }
8379
+ return canonical;
8380
+ }
8381
+ /**
8382
+ * Hermes Agent shell hooks.
8383
+ *
8384
+ * Hermes Agent registers shell-command hooks under the `hooks:` key of the
8385
+ * shared user config file `~/.hermes/config.yaml` (the HERMES_HOME directory;
8386
+ * global only — Hermes has no project-scoped hooks location). Hermes only runs
8387
+ * hooks declared under its fixed `VALID_HOOKS` event keys (`pre_tool_call`,
8388
+ * `post_tool_call`, `pre_llm_call`, `post_llm_call`, `on_session_start`,
8389
+ * `on_session_end`, `subagent_start`, `subagent_stop`, ...); any other key is
8390
+ * silently ignored. Generation therefore maps canonical events onto the real
8391
+ * `VALID_HOOKS` keys and merges the resulting `hooks:` block into the existing
8392
+ * config instead of overwriting it, since that file also holds other Hermes
8393
+ * settings (model, `mcp_servers`, `command_allowlist`, ...).
8394
+ * @see https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/features/hooks.md
8395
+ */
7720
8396
  var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7721
8397
  static getSettablePaths() {
7722
8398
  return {
@@ -7736,6 +8412,9 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7736
8412
  error: null
7737
8413
  };
7738
8414
  }
8415
+ isDeletable() {
8416
+ return false;
8417
+ }
7739
8418
  shouldMergeExistingFileContent() {
7740
8419
  return true;
7741
8420
  }
@@ -7743,18 +8422,21 @@ var HermesagentHooks = class HermesagentHooks extends ToolHooks {
7743
8422
  this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
7744
8423
  }
7745
8424
  toRulesyncHooks() {
7746
- const config = parseHermesConfig(this.getFileContent());
7747
- const hooks = config.hooks && typeof config.hooks === "object" ? config.hooks.rulesync : {};
7748
- return new RulesyncHooks({
7749
- relativeDirPath: "",
7750
- relativeFilePath: ".rulesync/hooks.json",
7751
- fileContent: JSON.stringify(hooks ?? {}, null, 2)
7752
- });
8425
+ const hooks = hermesHooksToCanonical(parseHermesConfig(this.getFileContent()).hooks);
8426
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
8427
+ version: 1,
8428
+ hooks
8429
+ }, null, 2) });
7753
8430
  }
7754
- static fromRulesyncHooks({ outputRoot, rulesyncHooks }) {
8431
+ static fromRulesyncHooks({ outputRoot, rulesyncHooks, logger }) {
8432
+ const config = rulesyncHooks.getJson();
7755
8433
  return new HermesagentHooks({
7756
8434
  outputRoot,
7757
- fileContent: stringifyHermesConfig({ hooks: { rulesync: rulesyncHooks.getJson() } })
8435
+ fileContent: stringifyHermesConfig({ hooks: canonicalToHermesHooks({
8436
+ config,
8437
+ toolOverrideHooks: config.hermesagent?.hooks,
8438
+ logger
8439
+ }) })
7758
8440
  });
7759
8441
  }
7760
8442
  };
@@ -8224,9 +8906,8 @@ var KiroHooks = class KiroHooks extends ToolHooks {
8224
8906
  * the tool-specific override key to `kiro-cli` (so `kiro-cli.hooks` overrides in
8225
8907
  * the rulesync hooks config are honored, rather than the legacy `kiro.hooks`).
8226
8908
  *
8227
- * (The Kiro IDE uses multi-file `.kiro/hooks/*.kiro.hook` hooks, which the
8228
- * single-file hooks pipeline does not emit yet, so `kiro-ide` has no hooks
8229
- * adapter.)
8909
+ * (The Kiro IDE uses the structured `.kiro/hooks/*.json` v1 format instead; see
8910
+ * {@link import("./kiro-ide-hooks.js").KiroIdeHooks}.)
8230
8911
  */
8231
8912
  var KiroCliHooks = class extends KiroHooks {
8232
8913
  static getOverrideKey() {
@@ -8234,6 +8915,203 @@ var KiroCliHooks = class extends KiroHooks {
8234
8915
  }
8235
8916
  };
8236
8917
  //#endregion
8918
+ //#region src/features/hooks/kiro-ide-hooks.ts
8919
+ /**
8920
+ * One hook entry inside the Kiro IDE v1 `hooks` array.
8921
+ *
8922
+ * `z.looseObject` keeps unknown fields added by future Kiro IDE versions, so
8923
+ * imports do not drop data they do not yet understand.
8924
+ * @see https://kiro.dev/docs/hooks/types/
8925
+ */
8926
+ const KiroIdeHookActionSchema = z.union([z.looseObject({
8927
+ type: z.literal("command"),
8928
+ command: z.optional(safeString)
8929
+ }), z.looseObject({
8930
+ type: z.literal("agent"),
8931
+ prompt: z.optional(safeString)
8932
+ })]);
8933
+ const KiroIdeHookEntrySchema = z.looseObject({
8934
+ name: z.optional(z.string()),
8935
+ description: z.optional(z.string()),
8936
+ trigger: z.optional(z.string()),
8937
+ matcher: z.optional(z.string()),
8938
+ action: z.optional(KiroIdeHookActionSchema),
8939
+ timeout: z.optional(z.number()),
8940
+ enabled: z.optional(z.boolean())
8941
+ });
8942
+ const KiroIdeHooksFileSchema = z.looseObject({
8943
+ version: z.optional(z.string()),
8944
+ hooks: z.optional(z.array(KiroIdeHookEntrySchema))
8945
+ });
8946
+ /**
8947
+ * Build the Kiro IDE hook entries for a single canonical event's definitions.
8948
+ *
8949
+ * `command`-type definitions become `{ type: "command", command }` actions and
8950
+ * `prompt`-type definitions become `{ type: "agent", prompt }` actions. Other
8951
+ * types are skipped (the {@link import("./hooks-processor.js").HooksProcessor}
8952
+ * already warns about unsupported types).
8953
+ */
8954
+ function buildKiroIdeEntriesForEvent(trigger, definitions) {
8955
+ const entries = [];
8956
+ for (const def of definitions) {
8957
+ const type = def.type ?? "command";
8958
+ let action;
8959
+ if (type === "command") {
8960
+ if (def.command === void 0) continue;
8961
+ action = {
8962
+ type: "command",
8963
+ command: def.command
8964
+ };
8965
+ } else if (type === "prompt") {
8966
+ if (def.prompt === void 0) continue;
8967
+ action = {
8968
+ type: "agent",
8969
+ prompt: def.prompt
8970
+ };
8971
+ } else continue;
8972
+ entries.push({
8973
+ name: def.name ?? trigger,
8974
+ ...def.description !== void 0 && def.description !== null && { description: def.description },
8975
+ trigger,
8976
+ ...def.matcher !== void 0 && def.matcher !== null && def.matcher !== "" && { matcher: def.matcher },
8977
+ action,
8978
+ ...def.timeout !== void 0 && def.timeout !== null && def.timeout >= 0 && { timeout: def.timeout },
8979
+ enabled: true
8980
+ });
8981
+ }
8982
+ return entries;
8983
+ }
8984
+ function canonicalToKiroIdeHooks(config) {
8985
+ const kiroIdeSupported = new Set(KIRO_IDE_HOOK_EVENTS);
8986
+ const sharedHooks = {};
8987
+ for (const [event, defs] of Object.entries(config.hooks)) if (kiroIdeSupported.has(event)) sharedHooks[event] = defs;
8988
+ const effectiveHooks = {
8989
+ ...sharedHooks,
8990
+ ...config["kiro-ide"]?.hooks
8991
+ };
8992
+ const entries = [];
8993
+ for (const [eventName, definitions] of Object.entries(effectiveHooks)) {
8994
+ const trigger = CANONICAL_TO_KIRO_IDE_EVENT_NAMES[eventName] ?? eventName;
8995
+ entries.push(...buildKiroIdeEntriesForEvent(trigger, definitions));
8996
+ }
8997
+ return entries;
8998
+ }
8999
+ function kiroIdeHooksToCanonical(entries) {
9000
+ const canonical = {};
9001
+ for (const entry of entries) {
9002
+ if (entry.trigger === void 0 || entry.action === void 0) continue;
9003
+ const eventName = KIRO_IDE_TO_CANONICAL_EVENT_NAMES[entry.trigger] ?? entry.trigger;
9004
+ if (isPrototypePollutionKey(eventName)) continue;
9005
+ const def = {};
9006
+ if (entry.action.type === "command") {
9007
+ if (!entry.action.command) continue;
9008
+ def.type = "command";
9009
+ def.command = entry.action.command;
9010
+ } else {
9011
+ if (!entry.action.prompt) continue;
9012
+ def.type = "prompt";
9013
+ def.prompt = entry.action.prompt;
9014
+ }
9015
+ if (entry.name !== void 0 && entry.name !== null) def.name = entry.name;
9016
+ if (entry.description !== void 0 && entry.description !== null) def.description = entry.description;
9017
+ if (entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "") def.matcher = entry.matcher;
9018
+ if (entry.timeout !== void 0 && entry.timeout !== null) def.timeout = entry.timeout;
9019
+ (canonical[eventName] ??= []).push(def);
9020
+ }
9021
+ return canonical;
9022
+ }
9023
+ /**
9024
+ * Hooks generator for the **Kiro IDE** (`.kiro/hooks/*.json` v1).
9025
+ *
9026
+ * Kiro IDE 1.0 reads structured JSON hooks from `.kiro/hooks/` (workspace) and
9027
+ * `~/.kiro/hooks/` (user). A single file may declare multiple hooks in its
9028
+ * `hooks` array, so rulesync emits every generated hook into one
9029
+ * `rulesync.json` file per scope (`{ "version": "v1", "hooks": [ ... ] }`),
9030
+ * which keeps it within the single-file hooks architecture.
9031
+ *
9032
+ * This is distinct from the Kiro CLI ({@link import("./kiro-cli-hooks.js").
9033
+ * KiroCliHooks}), which uses the `.kiro/agents/default.json` agent-config shape.
9034
+ *
9035
+ * @see https://kiro.dev/docs/hooks/
9036
+ */
9037
+ var KiroIdeHooks = class KiroIdeHooks extends ToolHooks {
9038
+ constructor(params) {
9039
+ super({
9040
+ ...params,
9041
+ fileContent: params.fileContent ?? JSON.stringify({
9042
+ version: "v1",
9043
+ hooks: []
9044
+ }, null, 2)
9045
+ });
9046
+ }
9047
+ static getSettablePaths(_options = {}) {
9048
+ return {
9049
+ relativeDirPath: KIRO_IDE_HOOKS_DIR_PATH,
9050
+ relativeFilePath: KIRO_IDE_HOOKS_FILE_NAME
9051
+ };
9052
+ }
9053
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9054
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9055
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({
9056
+ version: "v1",
9057
+ hooks: []
9058
+ }, null, 2);
9059
+ return new KiroIdeHooks({
9060
+ outputRoot,
9061
+ relativeDirPath: paths.relativeDirPath,
9062
+ relativeFilePath: paths.relativeFilePath,
9063
+ fileContent,
9064
+ validate
9065
+ });
9066
+ }
9067
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false }) {
9068
+ const paths = KiroIdeHooks.getSettablePaths({ global });
9069
+ const hooks = canonicalToKiroIdeHooks(rulesyncHooks.getJson());
9070
+ const fileContent = JSON.stringify({
9071
+ version: "v1",
9072
+ hooks
9073
+ }, null, 2);
9074
+ return new KiroIdeHooks({
9075
+ outputRoot,
9076
+ relativeDirPath: paths.relativeDirPath,
9077
+ relativeFilePath: paths.relativeFilePath,
9078
+ fileContent,
9079
+ validate
9080
+ });
9081
+ }
9082
+ toRulesyncHooks() {
9083
+ let parsed;
9084
+ try {
9085
+ parsed = KiroIdeHooksFileSchema.parse(JSON.parse(this.getFileContent()));
9086
+ } catch (error) {
9087
+ throw new Error(`Failed to parse Kiro IDE hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9088
+ }
9089
+ const hooks = kiroIdeHooksToCanonical(parsed.hooks ?? []);
9090
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9091
+ version: 1,
9092
+ hooks
9093
+ }, null, 2) });
9094
+ }
9095
+ validate() {
9096
+ return {
9097
+ success: true,
9098
+ error: null
9099
+ };
9100
+ }
9101
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9102
+ return new KiroIdeHooks({
9103
+ outputRoot,
9104
+ relativeDirPath,
9105
+ relativeFilePath,
9106
+ fileContent: JSON.stringify({
9107
+ version: "v1",
9108
+ hooks: []
9109
+ }, null, 2),
9110
+ validate: false
9111
+ });
9112
+ }
9113
+ };
9114
+ //#endregion
8237
9115
  //#region src/features/hooks/opencode-hooks.ts
8238
9116
  var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8239
9117
  constructor(params) {
@@ -8292,6 +9170,35 @@ var OpencodeHooks = class OpencodeHooks extends ToolHooks {
8292
9170
  //#endregion
8293
9171
  //#region src/features/hooks/qwencode-hooks.ts
8294
9172
  /**
9173
+ * Build a single Qwen Code hook object from a canonical hook definition.
9174
+ * Command-only fields (`async`/`env`/`shell`) are emitted only on command hooks
9175
+ * and http-only fields (`headers`/`allowedEnvVars`/`once`) only on http hooks,
9176
+ * matching upstream. `statusMessage` applies to both.
9177
+ * https://github.com/QwenLM/qwen-code/blob/main/docs/users/features/hooks.md
9178
+ */
9179
+ function canonicalDefToQwencodeHook(def) {
9180
+ const type = def.type ?? "command";
9181
+ const isHttp = type === "http";
9182
+ const isCommand = type === "command";
9183
+ return {
9184
+ type,
9185
+ ...compact({
9186
+ command: def.command,
9187
+ url: def.url,
9188
+ timeout: def.timeout,
9189
+ name: def.name,
9190
+ description: def.description,
9191
+ statusMessage: def.statusMessage,
9192
+ async: isCommand ? def.async : void 0,
9193
+ env: isCommand ? def.env : void 0,
9194
+ shell: isCommand ? def.shell : void 0,
9195
+ headers: isHttp ? def.headers : void 0,
9196
+ allowedEnvVars: isHttp ? def.allowedEnvVars : void 0,
9197
+ once: isHttp ? def.once : void 0
9198
+ })
9199
+ };
9200
+ }
9201
+ /**
8295
9202
  * Convert canonical hooks config to Qwen Code format.
8296
9203
  * Filters shared hooks to QWENCODE_HOOK_EVENTS, merges config.qwencode?.hooks,
8297
9204
  * then converts to PascalCase and Qwen Code matcher/hooks structure.
@@ -8319,16 +9226,7 @@ function canonicalToQwencodeHooks(config) {
8319
9226
  }
8320
9227
  const entries = [];
8321
9228
  for (const [matcherKey, defs] of byMatcher) {
8322
- const hooks = defs.map((def) => {
8323
- return {
8324
- type: def.type ?? "command",
8325
- ...def.command !== void 0 && def.command !== null && { command: def.command },
8326
- ...def.url !== void 0 && def.url !== null && { url: def.url },
8327
- ...def.timeout !== void 0 && def.timeout !== null && { timeout: def.timeout },
8328
- ...def.name !== void 0 && def.name !== null && { name: def.name },
8329
- ...def.description !== void 0 && def.description !== null && { description: def.description }
8330
- };
8331
- });
9229
+ const hooks = defs.map(canonicalDefToQwencodeHook);
8332
9230
  const sequential = defs.some((def) => def.sequential === true);
8333
9231
  const group = matcherKey ? {
8334
9232
  matcher: matcherKey,
@@ -8352,7 +9250,14 @@ const QwencodeHookEntrySchema = z.looseObject({
8352
9250
  url: z.optional(z.string()),
8353
9251
  timeout: z.optional(z.number()),
8354
9252
  name: z.optional(z.string()),
8355
- description: z.optional(z.string())
9253
+ description: z.optional(z.string()),
9254
+ statusMessage: z.optional(z.string()),
9255
+ async: z.optional(z.boolean()),
9256
+ env: z.optional(z.record(z.string(), z.string())),
9257
+ shell: z.optional(z.string()),
9258
+ headers: z.optional(z.record(z.string(), z.string())),
9259
+ allowedEnvVars: z.optional(z.array(z.string())),
9260
+ once: z.optional(z.boolean())
8356
9261
  });
8357
9262
  /**
8358
9263
  * A matcher group entry in a Qwen Code event array.
@@ -8371,18 +9276,29 @@ function qwencodeMatcherEntryToCanonical(entry) {
8371
9276
  const defs = [];
8372
9277
  const hooks = entry.hooks ?? [];
8373
9278
  const sequential = entry.sequential === true;
9279
+ const matcher = entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" ? entry.matcher : void 0;
8374
9280
  for (const h of hooks) {
8375
- const command = h.command;
8376
9281
  const hookType = h.type === "command" || h.type === "prompt" || h.type === "http" ? h.type : "command";
9282
+ const isHttp = hookType === "http";
9283
+ const isCommand = hookType === "command";
8377
9284
  defs.push({
8378
9285
  type: hookType,
8379
- ...command !== void 0 && command !== null && { command },
8380
- ...h.url !== void 0 && h.url !== null && { url: h.url },
8381
- ...h.timeout !== void 0 && h.timeout !== null && { timeout: h.timeout },
8382
- ...h.name !== void 0 && h.name !== null && { name: h.name },
8383
- ...h.description !== void 0 && h.description !== null && { description: h.description },
8384
- ...sequential && { sequential: true },
8385
- ...entry.matcher !== void 0 && entry.matcher !== null && entry.matcher !== "" && { matcher: entry.matcher }
9286
+ ...compact({
9287
+ command: h.command,
9288
+ url: h.url,
9289
+ timeout: h.timeout,
9290
+ name: h.name,
9291
+ description: h.description,
9292
+ statusMessage: h.statusMessage,
9293
+ async: isCommand ? h.async : void 0,
9294
+ env: isCommand ? h.env : void 0,
9295
+ shell: isCommand ? h.shell : void 0,
9296
+ headers: isHttp ? h.headers : void 0,
9297
+ allowedEnvVars: isHttp ? h.allowedEnvVars : void 0,
9298
+ once: isHttp ? h.once : void 0,
9299
+ sequential: sequential ? true : void 0,
9300
+ matcher
9301
+ })
8386
9302
  });
8387
9303
  }
8388
9304
  return defs;
@@ -8495,6 +9411,169 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
8495
9411
  }
8496
9412
  };
8497
9413
  //#endregion
9414
+ //#region src/features/hooks/reasonix-hooks.ts
9415
+ /**
9416
+ * Only PreToolUse/PostToolUse honor the `match` field (an anchored regex
9417
+ * against the tool name); it is ignored on every other event.
9418
+ */
9419
+ const REASONIX_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
9420
+ const SUPPORTED_REASONIX_EVENTS = new Set(REASONIX_HOOK_EVENTS);
9421
+ /**
9422
+ * Convert canonical hooks config to the Reasonix `hooks` object.
9423
+ * Filters shared hooks to REASONIX_HOOK_EVENTS, merges `config.reasonix?.hooks`,
9424
+ * then maps event names and emits flat per-event hook-entry arrays.
9425
+ */
9426
+ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
9427
+ const sharedHooks = {};
9428
+ for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_REASONIX_EVENTS.has(event)) sharedHooks[event] = defs;
9429
+ const effectiveHooks = {
9430
+ ...sharedHooks,
9431
+ ...toolOverrideHooks
9432
+ };
9433
+ const result = {};
9434
+ for (const [event, defs] of Object.entries(effectiveHooks)) {
9435
+ if (!SUPPORTED_REASONIX_EVENTS.has(event)) continue;
9436
+ const reasonixEvent = CANONICAL_TO_REASONIX_EVENT_NAMES[event] ?? event;
9437
+ const isMatcherEvent = REASONIX_MATCHER_EVENTS.has(reasonixEvent);
9438
+ const entries = [];
9439
+ for (const def of defs) {
9440
+ if ((def.type ?? "command") !== "command") continue;
9441
+ if (typeof def.command !== "string") continue;
9442
+ const entry = { command: def.command };
9443
+ if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
9444
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
9445
+ if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
9446
+ if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
9447
+ entries.push(entry);
9448
+ }
9449
+ if (entries.length > 0) result[reasonixEvent] = [...result[reasonixEvent] ?? [], ...entries];
9450
+ }
9451
+ return result;
9452
+ }
9453
+ /**
9454
+ * Reverse {@link canonicalToReasonixHooks}: parse the Reasonix `hooks` object
9455
+ * back into a canonical event -> definition[] record.
9456
+ */
9457
+ function reasonixHooksToCanonical(hooks) {
9458
+ const canonical = {};
9459
+ if (hooks === null || hooks === void 0 || typeof hooks !== "object" || Array.isArray(hooks)) return canonical;
9460
+ for (const [reasonixEvent, rawEntries] of Object.entries(hooks)) {
9461
+ if (!Array.isArray(rawEntries)) continue;
9462
+ const canonicalEvent = REASONIX_TO_CANONICAL_EVENT_NAMES[reasonixEvent] ?? reasonixEvent;
9463
+ const defs = [];
9464
+ for (const rawEntry of rawEntries) {
9465
+ if (rawEntry === null || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
9466
+ const entry = rawEntry;
9467
+ if (typeof entry.command !== "string") continue;
9468
+ const def = {
9469
+ type: "command",
9470
+ command: entry.command
9471
+ };
9472
+ if (typeof entry.match === "string" && entry.match !== "") def.matcher = entry.match;
9473
+ if (typeof entry.description === "string" && entry.description !== "") def.description = entry.description;
9474
+ if (typeof entry.timeout === "number") def.timeout = entry.timeout / 1e3;
9475
+ defs.push(def);
9476
+ }
9477
+ if (defs.length > 0) canonical[canonicalEvent] = [...canonical[canonicalEvent] ?? [], ...defs];
9478
+ }
9479
+ return canonical;
9480
+ }
9481
+ /**
9482
+ * Reasonix hooks adapter.
9483
+ *
9484
+ * Reasonix hooks live in a Claude-Code-style but standalone JSON file —
9485
+ * `.reasonix/settings.json` (project) or `~/.reasonix/settings.json`
9486
+ * (global) — separate from the `[permissions]`/`[[plugins]]` TOML config.
9487
+ * Only the four events documented in the upstream issue are mapped:
9488
+ * PreToolUse/PostToolUse/UserPromptSubmit/Stop (see REASONIX_HOOK_EVENTS).
9489
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/DESKTOP_HOOKS.zh-CN.md
9490
+ */
9491
+ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
9492
+ constructor(params) {
9493
+ super({
9494
+ ...params,
9495
+ fileContent: params.fileContent ?? "{}"
9496
+ });
9497
+ }
9498
+ isDeletable() {
9499
+ return false;
9500
+ }
9501
+ static getSettablePaths(_options = {}) {
9502
+ return {
9503
+ relativeDirPath: REASONIX_DIR,
9504
+ relativeFilePath: REASONIX_SETTINGS_FILE_NAME
9505
+ };
9506
+ }
9507
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
9508
+ const paths = ReasonixHooks.getSettablePaths({ global });
9509
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"hooks\":{}}";
9510
+ return new ReasonixHooks({
9511
+ outputRoot,
9512
+ relativeDirPath: paths.relativeDirPath,
9513
+ relativeFilePath: paths.relativeFilePath,
9514
+ fileContent,
9515
+ validate
9516
+ });
9517
+ }
9518
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
9519
+ const paths = ReasonixHooks.getSettablePaths({ global });
9520
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
9521
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
9522
+ let settings;
9523
+ try {
9524
+ settings = JSON.parse(existingContent);
9525
+ } catch (error) {
9526
+ throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
9527
+ }
9528
+ const config = rulesyncHooks.getJson();
9529
+ const reasonixHooks = canonicalToReasonixHooks({
9530
+ config,
9531
+ toolOverrideHooks: config.reasonix?.hooks,
9532
+ logger
9533
+ });
9534
+ const merged = {
9535
+ ...settings,
9536
+ hooks: reasonixHooks
9537
+ };
9538
+ const fileContent = JSON.stringify(merged, null, 2);
9539
+ return new ReasonixHooks({
9540
+ outputRoot,
9541
+ relativeDirPath: paths.relativeDirPath,
9542
+ relativeFilePath: paths.relativeFilePath,
9543
+ fileContent,
9544
+ validate
9545
+ });
9546
+ }
9547
+ toRulesyncHooks() {
9548
+ let settings;
9549
+ try {
9550
+ settings = JSON.parse(this.getFileContent());
9551
+ } catch (error) {
9552
+ throw new Error(`Failed to parse Reasonix hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
9553
+ }
9554
+ const hooks = reasonixHooksToCanonical(settings.hooks);
9555
+ return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
9556
+ version: 1,
9557
+ hooks
9558
+ }, null, 2) });
9559
+ }
9560
+ validate() {
9561
+ return {
9562
+ success: true,
9563
+ error: null
9564
+ };
9565
+ }
9566
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
9567
+ return new ReasonixHooks({
9568
+ outputRoot,
9569
+ relativeDirPath,
9570
+ relativeFilePath,
9571
+ fileContent: JSON.stringify({ hooks: {} }, null, 2),
9572
+ validate: false
9573
+ });
9574
+ }
9575
+ };
9576
+ //#endregion
8498
9577
  //#region src/features/hooks/vibe-hooks.ts
8499
9578
  const VIBE_DIR = ".vibe";
8500
9579
  const VIBE_HOOKS_FILE_NAME = "hooks.toml";
@@ -8720,6 +9799,19 @@ var VibeHooks = class VibeHooks extends ToolHooks {
8720
9799
  //#endregion
8721
9800
  //#region src/features/hooks/hooks-processor.ts
8722
9801
  const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
9802
+ /**
9803
+ * Event names present in the config that the target's adapter cannot emit.
9804
+ *
9805
+ * When the factory passes override-block keys through verbatim
9806
+ * (`passthroughOverrideEvents`), those keys are excluded from the check so
9807
+ * documented passthrough triggers aren't falsely reported as skipped.
9808
+ */
9809
+ function unsupportedEventNames(params) {
9810
+ const { factory, sharedHooks, effectiveHooks } = params;
9811
+ const supportedEvents = new Set(factory.supportedEvents);
9812
+ const eventNames = factory.passthroughOverrideEvents ? Object.keys(sharedHooks) : Object.keys(effectiveHooks);
9813
+ return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
9814
+ }
8723
9815
  const toolHooksFactories = /* @__PURE__ */ new Map([
8724
9816
  ["antigravity-cli", {
8725
9817
  class: AntigravityCliHooks,
@@ -8853,12 +9945,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8853
9945
  supportsGlobal: true,
8854
9946
  supportsImport: true
8855
9947
  },
8856
- supportedEvents: CLAUDE_HOOK_EVENTS,
8857
- supportedHookTypes: [
8858
- "command",
8859
- "prompt",
8860
- "http"
8861
- ],
9948
+ supportedEvents: HERMESAGENT_HOOK_EVENTS,
9949
+ supportedHookTypes: ["command"],
8862
9950
  supportsMatcher: true
8863
9951
  }],
8864
9952
  ["deepagents", {
@@ -8894,6 +9982,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8894
9982
  supportedHookTypes: ["command"],
8895
9983
  supportsMatcher: true
8896
9984
  }],
9985
+ ["kiro-ide", {
9986
+ class: KiroIdeHooks,
9987
+ meta: {
9988
+ supportsProject: true,
9989
+ supportsGlobal: true,
9990
+ supportsImport: true
9991
+ },
9992
+ supportedEvents: KIRO_IDE_HOOK_EVENTS,
9993
+ supportedHookTypes: ["command", "prompt"],
9994
+ supportsMatcher: true,
9995
+ passthroughOverrideEvents: true
9996
+ }],
8897
9997
  ["devin", {
8898
9998
  class: DevinHooks,
8899
9999
  meta: {
@@ -8902,8 +10002,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8902
10002
  supportsImport: true
8903
10003
  },
8904
10004
  supportedEvents: DEVIN_HOOK_EVENTS,
8905
- supportedHookTypes: ["command"],
8906
- supportsMatcher: false
10005
+ supportedHookTypes: ["command", "prompt"],
10006
+ supportsMatcher: true
8907
10007
  }],
8908
10008
  ["augmentcode", {
8909
10009
  class: AugmentcodeHooks,
@@ -8948,6 +10048,17 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
8948
10048
  supportedEvents: QWENCODE_HOOK_EVENTS,
8949
10049
  supportedHookTypes: ["command"],
8950
10050
  supportsMatcher: true
10051
+ }],
10052
+ ["reasonix", {
10053
+ class: ReasonixHooks,
10054
+ meta: {
10055
+ supportsProject: true,
10056
+ supportsGlobal: true,
10057
+ supportsImport: true
10058
+ },
10059
+ supportedEvents: REASONIX_HOOK_EVENTS,
10060
+ supportedHookTypes: ["command"],
10061
+ supportsMatcher: true
8951
10062
  }]
8952
10063
  ]);
8953
10064
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
@@ -9023,8 +10134,11 @@ var HooksProcessor = class extends FeatureProcessor {
9023
10134
  ...overrideHooks
9024
10135
  };
9025
10136
  {
9026
- const supportedEvents = new Set(factory.supportedEvents);
9027
- const skipped = [...new Set(Object.keys(effectiveHooks))].filter((e) => !supportedEvents.has(e));
10137
+ const skipped = unsupportedEventNames({
10138
+ factory,
10139
+ sharedHooks,
10140
+ effectiveHooks
10141
+ });
9028
10142
  if (skipped.length > 0) this.logger.warn(`Skipped hook event(s) for ${this.toolTarget} (not supported): ${skipped.join(", ")}`);
9029
10143
  }
9030
10144
  {
@@ -9220,11 +10334,6 @@ var AiassistantIgnore = class AiassistantIgnore extends ToolIgnore {
9220
10334
  }
9221
10335
  };
9222
10336
  //#endregion
9223
- //#region src/constants/antigravity-cli-paths.ts
9224
- const ANTIGRAVITY_AGENTS_DIR = ANTIGRAVITY_DIR;
9225
- const ANTIGRAVITY_RULE_FILE_NAME = "AGENTS.md";
9226
- const ANTIGRAVITY_GLOBAL_RULE_FILE_NAME = "GEMINI.md";
9227
- //#endregion
9228
10337
  //#region src/features/ignore/antigravity-cli-ignore.ts
9229
10338
  /**
9230
10339
  * Antigravity CLI is the successor to Gemini CLI and is built on the same
@@ -10456,9 +11565,9 @@ function parseAmpSettingsJsonc(fileContent) {
10456
11565
  }
10457
11566
  function filterMcpServers(mcpServers) {
10458
11567
  const filtered = {};
10459
- if (!isRecord$1(mcpServers)) return filtered;
11568
+ if (!isRecord(mcpServers)) return filtered;
10460
11569
  for (const [name, config] of Object.entries(mcpServers)) {
10461
- if (isPrototypePollutionKey(name) || !isRecord$1(config)) continue;
11570
+ if (isPrototypePollutionKey(name) || !isRecord(config)) continue;
10462
11571
  const filteredConfig = {};
10463
11572
  for (const [key, value] of Object.entries(config)) {
10464
11573
  if (isPrototypePollutionKey(key)) continue;
@@ -10573,7 +11682,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10573
11682
  success: true,
10574
11683
  error: null
10575
11684
  };
10576
- if (!isRecord$1(mcpServers)) return {
11685
+ if (!isRecord(mcpServers)) return {
10577
11686
  success: false,
10578
11687
  error: /* @__PURE__ */ new Error(`${AMP_MCP_SERVERS_KEY} must be a JSON object`)
10579
11688
  };
@@ -10582,7 +11691,7 @@ var AmpMcp = class AmpMcp extends ToolMcp {
10582
11691
  success: false,
10583
11692
  error: /* @__PURE__ */ new Error(`Server name "${serverName}" is a prototype pollution key and is not allowed`)
10584
11693
  };
10585
- if (!isRecord$1(serverConfig)) return {
11694
+ if (!isRecord(serverConfig)) return {
10586
11695
  success: false,
10587
11696
  error: /* @__PURE__ */ new Error(`MCP server "${serverName}" must be a JSON object`)
10588
11697
  };
@@ -11103,7 +12212,7 @@ const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
11103
12212
  function convertFromCodexFormat(codexMcp) {
11104
12213
  const result = {};
11105
12214
  for (const [name, config] of Object.entries(codexMcp)) {
11106
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
12215
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11107
12216
  const converted = {};
11108
12217
  for (const [key, value] of Object.entries(config)) {
11109
12218
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11123,7 +12232,7 @@ function convertToCodexFormat(mcpServers) {
11123
12232
  const result = {};
11124
12233
  for (const [name, config] of Object.entries(mcpServers)) {
11125
12234
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
11126
- if (!isRecord$1(config)) continue;
12235
+ if (!isRecord(config)) continue;
11127
12236
  const converted = {};
11128
12237
  for (const [key, value] of Object.entries(config)) {
11129
12238
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -11185,19 +12294,19 @@ var CodexcliMcp = class CodexcliMcp extends ToolMcp {
11185
12294
  const strippedMcpServers = rulesyncMcp.getMcpServers();
11186
12295
  const rawMcpServers = rulesyncMcp.getJson().mcpServers;
11187
12296
  const converted = convertToCodexFormat(Object.fromEntries(Object.entries(strippedMcpServers).map(([serverName, serverConfig]) => {
11188
- const rawServer = isRecord$1(rawMcpServers) ? rawMcpServers[serverName] : void 0;
12297
+ const rawServer = isRecord(rawMcpServers) ? rawMcpServers[serverName] : void 0;
11189
12298
  return [serverName, {
11190
12299
  ...serverConfig,
11191
- ...isRecord$1(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
12300
+ ...isRecord(rawServer) && isStringArray(rawServer.envVars) ? { envVars: rawServer.envVars } : {}
11192
12301
  }];
11193
12302
  })));
11194
12303
  const filteredMcpServers = this.removeEmptyEntries(converted);
11195
12304
  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`);
11196
- const existingMcpServers = isRecord$1(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
12305
+ const existingMcpServers = isRecord(configToml["mcp_servers"]) ? configToml["mcp_servers"] : {};
11197
12306
  configToml["mcp_servers"] = Object.fromEntries(Object.entries(filteredMcpServers).map(([name, serverConfig]) => {
11198
- const existingServer = isRecord$1(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
12307
+ const existingServer = isRecord(existingMcpServers[name]) ? existingMcpServers[name] : void 0;
11199
12308
  const serverRecord = serverConfig;
11200
- if (existingServer && isRecord$1(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
12309
+ if (existingServer && isRecord(existingServer["tools"]) && !("tools" in serverRecord)) return [name, {
11201
12310
  ...serverRecord,
11202
12311
  tools: existingServer["tools"]
11203
12312
  }];
@@ -11707,18 +12816,20 @@ var DeepagentsMcp = class DeepagentsMcp extends ToolMcp {
11707
12816
  //#endregion
11708
12817
  //#region src/features/mcp/devin-mcp.ts
11709
12818
  /**
11710
- * MCP generator for Devin (Cascade).
12819
+ * MCP generator for Devin Local (native `.devin/` configuration).
11711
12820
  *
11712
- * Devin reads file-based MCP configuration from:
11713
- * - Project scope: `.windsurf/mcp_config.json`
11714
- * - Global scope: `~/.codeium/windsurf/mcp_config.json`
12821
+ * Devin reads MCP servers from the `mcpServers` key of its native config file:
12822
+ * - Project scope: `.devin/config.json`
12823
+ * - Global scope: `~/.config/devin/config.json`
11715
12824
  *
11716
- * The official docs document only the global path; the project path mirrors
11717
- * the same `mcp_config.json` filename so both scopes fit the processor
11718
- * framework cleanly. The top-level key is `mcpServers`; each server is a
11719
- * stdio entry ({ command, args, env }) or a remote entry
11720
- * ({ serverUrl | url, headers }), and may carry an optional `disabledTools`
11721
- * array.
12825
+ * The config file is shared with the permissions feature (`permissions` key)
12826
+ * and, in global mode, the hooks feature (`hooks` key), so reads and writes
12827
+ * merge into the existing JSON rather than overwriting it, and the file is
12828
+ * never deleted. Each server is a stdio entry ({ command, args, env }) or a
12829
+ * remote entry ({ serverUrl | url, headers }), and may carry an optional
12830
+ * `disabledTools` array.
12831
+ *
12832
+ * @see https://docs.devin.ai/cli/extensibility/configuration
11722
12833
  */
11723
12834
  var DevinMcp = class DevinMcp extends ToolMcp {
11724
12835
  json;
@@ -11736,14 +12847,21 @@ var DevinMcp = class DevinMcp extends ToolMcp {
11736
12847
  throw new Error(`Failed to parse Devin MCP config at ${join(relativeDirPath, relativeFilePath)}: ${formatError(error)}`, { cause: error });
11737
12848
  }
11738
12849
  }
12850
+ /**
12851
+ * config.json may carry the permissions/hooks features' keys, so it is never
12852
+ * deleted; only the managed `mcpServers` key is rewritten.
12853
+ */
12854
+ isDeletable() {
12855
+ return false;
12856
+ }
11739
12857
  static getSettablePaths({ global = false } = {}) {
11740
12858
  if (global) return {
11741
- relativeDirPath: CODEIUM_WINDSURF_DIR,
11742
- relativeFilePath: DEVIN_MCP_FILE_NAME
12859
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
12860
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11743
12861
  };
11744
12862
  return {
11745
- relativeDirPath: WINDSURF_DIR,
11746
- relativeFilePath: DEVIN_MCP_FILE_NAME
12863
+ relativeDirPath: DEVIN_DIR,
12864
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
11747
12865
  };
11748
12866
  }
11749
12867
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
@@ -11858,7 +12976,7 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
11858
12976
  };
11859
12977
  //#endregion
11860
12978
  //#region src/features/mcp/goose-mcp.ts
11861
- const GOOSE_GLOBAL_ONLY_MESSAGE$1 = "Goose MCP is global-only; use --global to sync ~/.config/goose/config.yaml";
12979
+ const GOOSE_PLUGIN_MCP_RELATIVE_PATH = join(GOOSE_PLUGIN_MCP_DIR_PATH, GOOSE_PLUGIN_MCP_FILE_NAME);
11862
12980
  function parseGooseConfig(fileContent, relativeDirPath, relativeFilePath) {
11863
12981
  const configPath = join(relativeDirPath, relativeFilePath);
11864
12982
  let parsed;
@@ -11945,7 +13063,7 @@ function convertServerToGooseExtension(name, config) {
11945
13063
  function convertToGooseFormat(mcpServers) {
11946
13064
  const extensions = {};
11947
13065
  for (const [name, config] of Object.entries(mcpServers)) {
11948
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13066
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
11949
13067
  extensions[name] = convertServerToGooseExtension(name, config);
11950
13068
  }
11951
13069
  return extensions;
@@ -11962,7 +13080,7 @@ function convertToGooseFormat(mcpServers) {
11962
13080
  function convertFromGooseFormat(extensions) {
11963
13081
  const result = {};
11964
13082
  for (const [name, ext] of Object.entries(extensions)) {
11965
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(ext)) continue;
13083
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(ext)) continue;
11966
13084
  const server = {};
11967
13085
  const type = typeof ext.type === "string" ? ext.type : void 0;
11968
13086
  if (type === "sse") server.type = "sse";
@@ -11980,39 +13098,102 @@ function convertFromGooseFormat(extensions) {
11980
13098
  return result;
11981
13099
  }
11982
13100
  /**
13101
+ * Builds a Claude-style stdio server entry (`command`/`args`/`env`/`cwd`) for the
13102
+ * open-plugins `.mcp.json` manifest.
13103
+ */
13104
+ function buildGoosePluginStdioServer(config) {
13105
+ const server = {};
13106
+ const command = config.command;
13107
+ if (Array.isArray(command)) {
13108
+ if (typeof command[0] === "string") server.command = command[0];
13109
+ const rest = command.slice(1).filter((c) => typeof c === "string");
13110
+ const args = isStringArray(config.args) ? config.args : [];
13111
+ if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
13112
+ } else if (typeof command === "string") {
13113
+ server.command = command;
13114
+ if (isStringArray(config.args)) server.args = config.args;
13115
+ }
13116
+ if (isPlainObject(config.env)) server.env = omitPrototypePollutionKeys(config.env);
13117
+ if (typeof config.cwd === "string") server.cwd = config.cwd;
13118
+ return server;
13119
+ }
13120
+ /**
13121
+ * Converts rulesync canonical MCP servers into the Claude-style `mcpServers` map
13122
+ * used by Goose open-plugin manifests (`.agents/plugins/rulesync/.mcp.json`).
13123
+ *
13124
+ * The open-plugins manifest is **stdio-only** (no `url`/`headers`), so remote
13125
+ * (http/sse/streamable_http) and `builtin` servers cannot be represented. They
13126
+ * are skipped with a warning rather than silently dropped — remote servers stay
13127
+ * global-config-only (sync them with `--global` to `~/.config/goose/config.yaml`).
13128
+ */
13129
+ function convertToGoosePluginMcpServers(mcpServers, logger) {
13130
+ const result = {};
13131
+ for (const [name, config] of Object.entries(mcpServers)) {
13132
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
13133
+ const gooseType = resolveGooseType(config, resolveGooseUrl(config));
13134
+ if (gooseType !== "stdio") {
13135
+ 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.`);
13136
+ continue;
13137
+ }
13138
+ result[name] = buildGoosePluginStdioServer(config);
13139
+ }
13140
+ return result;
13141
+ }
13142
+ /**
11983
13143
  * Goose MCP servers.
11984
13144
  *
11985
- * Goose configures MCP servers as "extensions" in the shared user config file
11986
- * `~/.config/goose/config.yaml` (global only — Goose has no project-scoped MCP
11987
- * location). That file also holds other Goose settings (model, provider, ...),
11988
- * so generation merges the `extensions:` block into the existing config instead
11989
- * of overwriting it, and the file is never deleted.
13145
+ * Goose configures MCP servers in two locations:
13146
+ *
13147
+ * - **Global** (`--global`): "extensions" in the shared user config
13148
+ * `~/.config/goose/config.yaml`. That file also holds other Goose settings
13149
+ * (model, provider, ...), so generation merges the `extensions:` block into the
13150
+ * existing config instead of overwriting it, and the file is never deleted.
13151
+ * This location supports both stdio and remote (http/sse) servers.
13152
+ * - **Project**: a stdio-only open-plugin manifest at
13153
+ * `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
13154
+ * `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
13155
+ * Claude-style `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`
13156
+ * shape and cannot express `url`/`headers`, so remote servers are skipped with
13157
+ * a warning in project mode (use `--global` to sync them instead).
11990
13158
  *
11991
13159
  * @see https://block.github.io/goose/docs/getting-started/using-extensions/
13160
+ * @see https://github.com/block/goose/pull/9471
11992
13161
  */
11993
13162
  var GooseMcp = class GooseMcp extends ToolMcp {
11994
13163
  config;
11995
13164
  constructor(params) {
11996
13165
  super(params);
11997
- if (this.fileContent !== void 0) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
11998
- else this.config = {};
13166
+ if (this.fileContent === void 0) this.config = {};
13167
+ else if (params.global) this.config = parseGooseConfig(this.fileContent, this.relativeDirPath, this.relativeFilePath);
13168
+ else this.config = this.fileContent ? this.parsePluginManifest(this.fileContent) : {};
13169
+ }
13170
+ parsePluginManifest(fileContent) {
13171
+ try {
13172
+ const parsed = JSON.parse(fileContent);
13173
+ return isRecord(parsed) ? parsed : {};
13174
+ } catch (error) {
13175
+ throw new Error(`Failed to parse Goose MCP manifest at ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`, { cause: error });
13176
+ }
11999
13177
  }
12000
13178
  getConfig() {
12001
13179
  return this.config;
12002
13180
  }
12003
13181
  isDeletable() {
12004
- return false;
13182
+ return !this.global;
12005
13183
  }
12006
- static getSettablePaths(_options) {
12007
- return {
13184
+ static getSettablePaths({ global = false } = {}) {
13185
+ if (global) return {
12008
13186
  relativeDirPath: GOOSE_GLOBAL_DIR,
12009
13187
  relativeFilePath: GOOSE_MCP_FILE_NAME
12010
13188
  };
13189
+ return {
13190
+ relativeDirPath: GOOSE_PLUGIN_MCP_DIR_PATH,
13191
+ relativeFilePath: GOOSE_PLUGIN_MCP_FILE_NAME
13192
+ };
12011
13193
  }
12012
13194
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
12013
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
12014
13195
  const paths = this.getSettablePaths({ global });
12015
- const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
13196
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? (global ? "" : JSON.stringify({ mcpServers: {} }, null, 2));
12016
13197
  return new GooseMcp({
12017
13198
  outputRoot,
12018
13199
  relativeDirPath: paths.relativeDirPath,
@@ -12022,9 +13203,19 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12022
13203
  global
12023
13204
  });
12024
13205
  }
12025
- static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
12026
- if (!global) throw new Error(GOOSE_GLOBAL_ONLY_MESSAGE$1);
13206
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
12027
13207
  const paths = this.getSettablePaths({ global });
13208
+ if (!global) {
13209
+ const mcpServers = convertToGoosePluginMcpServers(rulesyncMcp.getMcpServers(), logger);
13210
+ return new GooseMcp({
13211
+ outputRoot,
13212
+ relativeDirPath: paths.relativeDirPath,
13213
+ relativeFilePath: paths.relativeFilePath,
13214
+ fileContent: JSON.stringify({ mcpServers }, null, 2),
13215
+ validate,
13216
+ global
13217
+ });
13218
+ }
12028
13219
  const merged = {
12029
13220
  ...parseGooseConfig(await readOrInitializeFileContent(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath), ""), paths.relativeDirPath, paths.relativeFilePath),
12030
13221
  extensions: convertToGooseFormat(rulesyncMcp.getMcpServers())
@@ -12039,7 +13230,11 @@ var GooseMcp = class GooseMcp extends ToolMcp {
12039
13230
  });
12040
13231
  }
12041
13232
  toRulesyncMcp() {
12042
- const mcpServers = convertFromGooseFormat(isRecord$1(this.config.extensions) ? this.config.extensions : {});
13233
+ if (!this.global) {
13234
+ const mcpServers = isRecord(this.config.mcpServers) ? this.config.mcpServers : {};
13235
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
13236
+ }
13237
+ const mcpServers = convertFromGooseFormat(isRecord(this.config.extensions) ? this.config.extensions : {});
12043
13238
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
12044
13239
  }
12045
13240
  validate() {
@@ -12116,7 +13311,7 @@ function convertToGrokFormat(mcpServers) {
12116
13311
  const result = {};
12117
13312
  for (const [name, config] of Object.entries(mcpServers)) {
12118
13313
  if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
12119
- if (!isRecord$1(config)) continue;
13314
+ if (!isRecord(config)) continue;
12120
13315
  const converted = {};
12121
13316
  for (const [key, value] of Object.entries(config)) {
12122
13317
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12131,7 +13326,7 @@ function convertToGrokFormat(mcpServers) {
12131
13326
  function convertFromGrokFormat(grokMcp) {
12132
13327
  const result = {};
12133
13328
  for (const [name, config] of Object.entries(grokMcp)) {
12134
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13329
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12135
13330
  const converted = {};
12136
13331
  for (const [key, value] of Object.entries(config)) {
12137
13332
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
@@ -12294,13 +13489,13 @@ function convertServerToHermes(config) {
12294
13489
  function convertToHermesFormat(mcpServers) {
12295
13490
  const result = {};
12296
13491
  for (const [name, config] of Object.entries(mcpServers)) {
12297
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13492
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12298
13493
  result[name] = convertServerToHermes(config);
12299
13494
  }
12300
13495
  return result;
12301
13496
  }
12302
13497
  function mergeHermesMcpServers(config, mcpServers) {
12303
- const existingMcpServers = isRecord$1(config.mcp_servers) ? config.mcp_servers : {};
13498
+ const existingMcpServers = isRecord(config.mcp_servers) ? config.mcp_servers : {};
12304
13499
  return {
12305
13500
  ...config,
12306
13501
  mcp_servers: {
@@ -12318,7 +13513,7 @@ function mergeHermesMcpServers(config, mcpServers) {
12318
13513
  function convertFromHermesFormat(mcpServers) {
12319
13514
  const result = {};
12320
13515
  for (const [name, config] of Object.entries(mcpServers)) {
12321
- if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
13516
+ if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord(config)) continue;
12322
13517
  const server = {};
12323
13518
  if (typeof config.command === "string") server.command = config.command;
12324
13519
  if (isStringArray(config.args)) server.args = config.args;
@@ -12354,7 +13549,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12354
13549
  return true;
12355
13550
  }
12356
13551
  setFileContent(fileContent) {
12357
- const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13552
+ const merged = mergeHermesMcpServers(parseHermesConfig(fileContent), isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12358
13553
  this.config = merged;
12359
13554
  super.setFileContent(stringifyHermesConfig(merged));
12360
13555
  }
@@ -12394,7 +13589,7 @@ var HermesagentMcp = class HermesagentMcp extends ToolMcp {
12394
13589
  });
12395
13590
  }
12396
13591
  toRulesyncMcp() {
12397
- const servers = convertFromHermesFormat(isRecord$1(this.config.mcp_servers) ? this.config.mcp_servers : {});
13592
+ const servers = convertFromHermesFormat(isRecord(this.config.mcp_servers) ? this.config.mcp_servers : {});
12398
13593
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: servers }, null, 2) });
12399
13594
  }
12400
13595
  validate() {
@@ -13229,11 +14424,6 @@ var QwencodeMcp = class QwencodeMcp extends ToolMcp {
13229
14424
  }
13230
14425
  };
13231
14426
  //#endregion
13232
- //#region src/constants/reasonix-paths.ts
13233
- const REASONIX_PROJECT_MCP_FILE_NAME = "reasonix.toml";
13234
- const REASONIX_GLOBAL_DIR = ".reasonix";
13235
- const REASONIX_GLOBAL_MCP_FILE_NAME = "config.toml";
13236
- //#endregion
13237
14427
  //#region src/features/mcp/reasonix-mcp.ts
13238
14428
  const REASONIX_PLUGIN_FIELDS = [
13239
14429
  "type",
@@ -13241,13 +14431,14 @@ const REASONIX_PLUGIN_FIELDS = [
13241
14431
  "args",
13242
14432
  "env",
13243
14433
  "url",
13244
- "headers"
14434
+ "headers",
14435
+ "trusted_read_only_tools"
13245
14436
  ];
13246
14437
  var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13247
14438
  toml;
13248
14439
  constructor(params) {
13249
14440
  super(params);
13250
- this.toml = parseReasonixConfig(this.fileContent);
14441
+ this.toml = parseReasonixConfig$1(this.fileContent);
13251
14442
  }
13252
14443
  getToml() {
13253
14444
  return this.toml;
@@ -13271,7 +14462,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13271
14462
  }
13272
14463
  static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
13273
14464
  const paths = this.getSettablePaths({ global });
13274
- const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14465
+ const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
13275
14466
  config.plugins = normalizePluginsArray(config.plugins);
13276
14467
  return new ReasonixMcp({
13277
14468
  outputRoot,
@@ -13284,7 +14475,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13284
14475
  }
13285
14476
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
13286
14477
  const paths = this.getSettablePaths({ global });
13287
- const config = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
14478
+ const config = parseReasonixConfig$1(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
13288
14479
  config.plugins = Object.entries(rulesyncMcp.getMcpServers()).map(([name, server]) => rulesyncMcpServerToReasonix(name, server));
13289
14480
  return new ReasonixMcp({
13290
14481
  outputRoot,
@@ -13301,7 +14492,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13301
14492
  }
13302
14493
  validate() {
13303
14494
  try {
13304
- parseReasonixConfig(this.fileContent);
14495
+ parseReasonixConfig$1(this.fileContent);
13305
14496
  return {
13306
14497
  success: true,
13307
14498
  error: null
@@ -13324,7 +14515,7 @@ var ReasonixMcp = class ReasonixMcp extends ToolMcp {
13324
14515
  });
13325
14516
  }
13326
14517
  };
13327
- function parseReasonixConfig(fileContent) {
14518
+ function parseReasonixConfig$1(fileContent) {
13328
14519
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
13329
14520
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
13330
14521
  return { ...parsed };
@@ -13464,17 +14655,6 @@ var RooMcp = class RooMcp extends ToolMcp {
13464
14655
  }
13465
14656
  };
13466
14657
  //#endregion
13467
- //#region src/constants/rovodev-paths.ts
13468
- const ROVODEV_DIR = ".rovodev";
13469
- const ROVODEV_SKILLS_DIR_PATH = join(ROVODEV_DIR, "skills");
13470
- const ROVODEV_SUBAGENTS_DIR_PATH = join(ROVODEV_DIR, "subagents");
13471
- const ROVODEV_MODULAR_RULES_DIR_PATH = join(ROVODEV_DIR, ".rulesync", "modular-rules");
13472
- const ROVODEV_RULE_FILE_NAME = "AGENTS.md";
13473
- const ROVODEV_LEGACY_RULE_FILE_NAME = "AGENTS.local.md";
13474
- const ROVODEV_MCP_FILE_NAME = "mcp.json";
13475
- const ROVODEV_CONFIG_FILE_NAME = "config.yml";
13476
- const ROVODEV_AGENTS_SKILLS_DIR_PATH = join(".agents", "skills");
13477
- //#endregion
13478
14658
  //#region src/features/mcp/rovodev-mcp.ts
13479
14659
  function parseRovodevMcpJson(fileContent, relativeDirPath, relativeFilePath) {
13480
14660
  const configPath = join(relativeDirPath, relativeFilePath);
@@ -13568,6 +14748,178 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
13568
14748
  }
13569
14749
  };
13570
14750
  //#endregion
14751
+ //#region src/features/shared/takt-config.ts
14752
+ /**
14753
+ * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
14754
+ *
14755
+ * Shared by the Takt adapters that read-modify-write the same `config.yaml`
14756
+ * (mcp, permissions, ...). Uses `isPlainObject` (not `isRecord`) so class
14757
+ * instances are rejected for prototype-pollution hardening; a YAML mapping
14758
+ * always parses to a plain object.
14759
+ */
14760
+ function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
14761
+ const configPath = join(relativeDirPath, relativeFilePath);
14762
+ let parsed;
14763
+ try {
14764
+ parsed = fileContent.trim() === "" ? {} : load(fileContent);
14765
+ } catch (error) {
14766
+ throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
14767
+ }
14768
+ if (parsed === void 0 || parsed === null) return {};
14769
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
14770
+ return parsed;
14771
+ }
14772
+ //#endregion
14773
+ //#region src/features/mcp/takt-mcp.ts
14774
+ /**
14775
+ * MCP adapter for Takt (`.takt/config.yaml` project / `~/.takt/config.yaml`
14776
+ * global).
14777
+ *
14778
+ * IMPORTANT — what this adapter can and cannot represent.
14779
+ *
14780
+ * Takt does NOT have a project- or global-level registry of MCP *server
14781
+ * definitions*. The concrete `mcp_servers` map (a server's `command`/`args`/`env`
14782
+ * or `type`/`url`/`headers`) is declared per-step inside individual *workflow*
14783
+ * YAML files; there is no top-level `mcp_servers` key in `config.yaml`, and the
14784
+ * config loader hard-rejects unknown top-level keys
14785
+ * (`assertNoUnknownGlobalConfigKeys`). Writing a server map into `config.yaml`
14786
+ * would therefore both be ignored and break the user's config.
14787
+ *
14788
+ * What `config.yaml` *does* hold is the default-deny transport allowlist
14789
+ * `workflow_mcp_servers: { stdio, sse, http }`. Without it, workflow-defined MCP
14790
+ * servers are refused regardless of how they are declared. So this adapter emits
14791
+ * the transport allowlist derived from the transports present in
14792
+ * `.rulesync/mcp.json`, enabling exactly the transports the user's servers need.
14793
+ *
14794
+ * Lossiness (documented, intentional): the per-server names, commands, env, URLs
14795
+ * and headers are NOT representable in `config.yaml` and are intentionally not
14796
+ * written. Users still declare the concrete servers in their workflow YAML
14797
+ * steps; rulesync only opens the transport gate that permits them. As a
14798
+ * corollary, import (`toRulesyncMcp`) cannot reconstruct server definitions from
14799
+ * a transport allowlist and yields an empty `mcpServers` map.
14800
+ *
14801
+ * The shared `config.yaml` is merged in place: only the
14802
+ * `workflow_mcp_servers` key is set; every other top-level key (provider,
14803
+ * provider_profiles, etc.) is preserved. The file is never deleted.
14804
+ *
14805
+ * @see https://github.com/nrslib/takt/blob/main/docs/configuration.md
14806
+ * @see https://github.com/nrslib/takt/blob/main/src/core/models/mcp-schemas.ts
14807
+ */
14808
+ var TaktMcp = class TaktMcp extends ToolMcp {
14809
+ constructor(params) {
14810
+ super({
14811
+ ...params,
14812
+ fileContent: params.fileContent ?? ""
14813
+ });
14814
+ }
14815
+ isDeletable() {
14816
+ return false;
14817
+ }
14818
+ static getSettablePaths(_options) {
14819
+ return {
14820
+ relativeDirPath: TAKT_DIR,
14821
+ relativeFilePath: TAKT_CONFIG_FILE_NAME
14822
+ };
14823
+ }
14824
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
14825
+ const paths = TaktMcp.getSettablePaths({ global });
14826
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
14827
+ return new TaktMcp({
14828
+ outputRoot,
14829
+ relativeDirPath: paths.relativeDirPath,
14830
+ relativeFilePath: paths.relativeFilePath,
14831
+ fileContent,
14832
+ validate,
14833
+ global
14834
+ });
14835
+ }
14836
+ static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
14837
+ const paths = TaktMcp.getSettablePaths({ global });
14838
+ const config = parseTaktConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "", paths.relativeDirPath, paths.relativeFilePath);
14839
+ const allowlist = deriveTransportAllowlist(rulesyncMcp.getMcpServers());
14840
+ const merged = {
14841
+ ...config,
14842
+ [TAKT_WORKFLOW_MCP_SERVERS_KEY]: allowlist
14843
+ };
14844
+ return new TaktMcp({
14845
+ outputRoot,
14846
+ relativeDirPath: paths.relativeDirPath,
14847
+ relativeFilePath: paths.relativeFilePath,
14848
+ fileContent: dump(merged),
14849
+ validate,
14850
+ global
14851
+ });
14852
+ }
14853
+ /**
14854
+ * A transport allowlist cannot reconstruct the per-step server definitions,
14855
+ * so import yields an empty `mcpServers` map. This keeps the round-trip honest
14856
+ * rather than fabricating placeholder servers.
14857
+ */
14858
+ toRulesyncMcp() {
14859
+ return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: {} }, null, 2) });
14860
+ }
14861
+ validate() {
14862
+ return {
14863
+ success: true,
14864
+ error: null
14865
+ };
14866
+ }
14867
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
14868
+ return new TaktMcp({
14869
+ outputRoot,
14870
+ relativeDirPath,
14871
+ relativeFilePath,
14872
+ fileContent: "",
14873
+ validate: false,
14874
+ global
14875
+ });
14876
+ }
14877
+ };
14878
+ /**
14879
+ * Map a rulesync MCP server onto the single Takt transport its allowlist gates.
14880
+ *
14881
+ * Takt allows only `stdio` / `sse` / `http`, so the broader rulesync alias set is
14882
+ * folded: `local` ⇒ stdio; `streamable-http` / `ws` ⇒ http. A server with no
14883
+ * explicit transport is treated as stdio when it carries a `command`, else as a
14884
+ * remote `http` server (it must have a `url`). Returns `undefined` only when the
14885
+ * shape is too ambiguous to classify.
14886
+ */
14887
+ function transportOf(server) {
14888
+ switch (server.type ?? server.transport) {
14889
+ case "stdio":
14890
+ case "local": return "stdio";
14891
+ case "sse": return "sse";
14892
+ case "http":
14893
+ case "streamable-http":
14894
+ case "ws": return "http";
14895
+ default: break;
14896
+ }
14897
+ if (server.command !== void 0) return "stdio";
14898
+ if (server.url !== void 0 || server.httpUrl !== void 0) return "http";
14899
+ }
14900
+ /**
14901
+ * Derive Takt's `workflow_mcp_servers` allowlist from the transports present in
14902
+ * the rulesync servers. All three keys are emitted explicitly (default-deny made
14903
+ * visible): a transport is `true` only when at least one server uses it.
14904
+ *
14905
+ * Server entries are read defensively (record guard); prototype-pollution server
14906
+ * names are irrelevant here because no user-controlled key or value is written —
14907
+ * only the three fixed boolean keys are.
14908
+ */
14909
+ function deriveTransportAllowlist(servers) {
14910
+ const allowlist = {
14911
+ stdio: false,
14912
+ sse: false,
14913
+ http: false
14914
+ };
14915
+ for (const server of Object.values(servers)) {
14916
+ if (!isRecord(server)) continue;
14917
+ const transport = transportOf(server);
14918
+ if (transport) allowlist[transport] = true;
14919
+ }
14920
+ return allowlist;
14921
+ }
14922
+ //#endregion
13571
14923
  //#region src/features/mcp/vibe-mcp.ts
13572
14924
  const VIBE_MCP_SERVER_FIELDS = [
13573
14925
  "transport",
@@ -14028,7 +15380,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14028
15380
  ["goose", {
14029
15381
  class: GooseMcp,
14030
15382
  meta: {
14031
- supportsProject: false,
15383
+ supportsProject: true,
14032
15384
  supportsGlobal: true,
14033
15385
  supportsEnabledTools: false,
14034
15386
  supportsDisabledTools: false
@@ -14142,6 +15494,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
14142
15494
  supportsDisabledTools: false
14143
15495
  }
14144
15496
  }],
15497
+ ["takt", {
15498
+ class: TaktMcp,
15499
+ meta: {
15500
+ supportsProject: true,
15501
+ supportsGlobal: true,
15502
+ supportsEnabledTools: false,
15503
+ supportsDisabledTools: false
15504
+ }
15505
+ }],
14145
15506
  ["vibe", {
14146
15507
  class: VibeMcp,
14147
15508
  meta: {
@@ -14270,7 +15631,8 @@ var McpProcessor = class extends FeatureProcessor {
14270
15631
  return await factory.class.fromRulesyncMcp({
14271
15632
  outputRoot: this.outputRoot,
14272
15633
  rulesyncMcp: filteredRulesyncMcp,
14273
- global: this.global
15634
+ global: this.global,
15635
+ logger: this.logger
14274
15636
  });
14275
15637
  }));
14276
15638
  }
@@ -14784,7 +16146,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
14784
16146
  function toAntigravityCliToolName(canonical) {
14785
16147
  return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
14786
16148
  }
14787
- function toCanonicalToolName$5(cliName) {
16149
+ function toCanonicalToolName$6(cliName) {
14788
16150
  return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
14789
16151
  }
14790
16152
  /**
@@ -14967,7 +16329,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
14967
16329
  const processEntries = (entries, action) => {
14968
16330
  for (const entry of entries) {
14969
16331
  const { toolName, pattern } = parsePermissionEntry$1(entry);
14970
- const canonical = toCanonicalToolName$5(toolName);
16332
+ const canonical = toCanonicalToolName$6(toolName);
14971
16333
  if (!permission[canonical]) permission[canonical] = {};
14972
16334
  permission[canonical][pattern] = action;
14973
16335
  }
@@ -15006,7 +16368,7 @@ const IDE_ACTION_TO_CANONICAL = {
15006
16368
  function toIdeAction(canonical) {
15007
16369
  return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
15008
16370
  }
15009
- function toCanonicalCategory$1(ideAction) {
16371
+ function toCanonicalCategory$2(ideAction) {
15010
16372
  return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
15011
16373
  }
15012
16374
  /**
@@ -15180,7 +16542,7 @@ function convertAntigravityIdeToRulesyncPermissions(params) {
15180
16542
  const processEntries = (entries, action) => {
15181
16543
  for (const entry of entries) {
15182
16544
  const { action: ideAction, pattern } = parsePermissionEntry(entry);
15183
- const canonical = toCanonicalCategory$1(ideAction);
16545
+ const canonical = toCanonicalCategory$2(ideAction);
15184
16546
  if (!permission[canonical]) permission[canonical] = {};
15185
16547
  permission[canonical][pattern] = action;
15186
16548
  }
@@ -15229,7 +16591,7 @@ const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONI
15229
16591
  function toAugmentToolName(canonical) {
15230
16592
  return CANONICAL_TO_AUGMENT_TOOL_NAMES[canonical] ?? canonical;
15231
16593
  }
15232
- function toCanonicalToolName$4(augmentName) {
16594
+ function toCanonicalToolName$5(augmentName) {
15233
16595
  return AUGMENT_TO_CANONICAL_TOOL_NAMES[augmentName] ?? augmentName;
15234
16596
  }
15235
16597
  function actionToAugmentType(action) {
@@ -15573,7 +16935,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
15573
16935
  }
15574
16936
  const type = entry.permission.type;
15575
16937
  if (!isBasicAugmentType(type)) continue;
15576
- const canonical = toCanonicalToolName$4(entry.toolName);
16938
+ const canonical = toCanonicalToolName$5(entry.toolName);
15577
16939
  if (forbiddenMapKeys.has(canonical)) {
15578
16940
  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.`);
15579
16941
  continue;
@@ -15626,7 +16988,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
15626
16988
  function toClaudeToolName(canonical) {
15627
16989
  return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
15628
16990
  }
15629
- function toCanonicalToolName$3(claudeName) {
16991
+ function toCanonicalToolName$4(claudeName) {
15630
16992
  return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
15631
16993
  }
15632
16994
  /**
@@ -15800,7 +17162,7 @@ function convertClaudeToRulesyncPermissions(params) {
15800
17162
  const processEntries = (entries, action) => {
15801
17163
  for (const entry of entries) {
15802
17164
  const { toolName, pattern } = parseClaudePermissionEntry(entry);
15803
- const canonical = toCanonicalToolName$3(toolName);
17165
+ const canonical = toCanonicalToolName$4(toolName);
15804
17166
  if (!permission[canonical]) permission[canonical] = {};
15805
17167
  permission[canonical][pattern] = action;
15806
17168
  }
@@ -16427,7 +17789,7 @@ function toCursorPattern(canonical, pattern) {
16427
17789
  }
16428
17790
  return pattern;
16429
17791
  }
16430
- function toCanonicalCategory(cursorType, pattern) {
17792
+ function toCanonicalCategory$1(cursorType, pattern) {
16431
17793
  if (cursorType === "Mcp") {
16432
17794
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
16433
17795
  if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
@@ -16652,7 +18014,7 @@ function convertCursorToRulesyncPermissions(params) {
16652
18014
  const processEntries = (entries, action) => {
16653
18015
  for (const entry of entries) {
16654
18016
  const { type, pattern } = parseCursorPermissionEntry(entry);
16655
- const canonical = toCanonicalCategory(type, pattern);
18017
+ const canonical = toCanonicalCategory$1(type, pattern);
16656
18018
  if (!permission[canonical]) permission[canonical] = {};
16657
18019
  const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
16658
18020
  permission[canonical][canonicalPattern] = action;
@@ -16663,6 +18025,245 @@ function convertCursorToRulesyncPermissions(params) {
16663
18025
  return { permission };
16664
18026
  }
16665
18027
  //#endregion
18028
+ //#region src/features/permissions/devin-permissions.ts
18029
+ /**
18030
+ * Mapping from rulesync canonical tool category names to Devin Local permission
18031
+ * scope matchers.
18032
+ *
18033
+ * Devin expresses permissions with scope-based matchers — `Read(glob)`,
18034
+ * `Write(glob)`, `Exec(prefix)`, and `Fetch(pattern)` — plus MCP tool patterns
18035
+ * (`mcp__server__tool`). The canonical `edit` and `write` categories both map
18036
+ * onto Devin's single `Write` scope; on import `Write` maps back to `write`, so
18037
+ * `edit` rules round-trip as `write` (a lossy but documented collapse). Unknown
18038
+ * names (e.g. `mcp__github__list_issues`) pass through verbatim.
18039
+ *
18040
+ * @see https://docs.devin.ai/cli/reference/permissions
18041
+ */
18042
+ const CANONICAL_TO_DEVIN_SCOPE = {
18043
+ read: "Read",
18044
+ write: "Write",
18045
+ edit: "Write",
18046
+ bash: "Exec",
18047
+ webfetch: "Fetch"
18048
+ };
18049
+ /**
18050
+ * Reverse mapping from Devin scope matchers to rulesync canonical names.
18051
+ */
18052
+ const DEVIN_SCOPE_TO_CANONICAL = {
18053
+ Read: "read",
18054
+ Write: "write",
18055
+ Exec: "bash",
18056
+ Fetch: "webfetch"
18057
+ };
18058
+ function toDevinScope(canonical) {
18059
+ return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
18060
+ }
18061
+ function toCanonicalCategory(devinScope) {
18062
+ return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
18063
+ }
18064
+ /**
18065
+ * Parse a Devin permission entry like `Read(src/**)` into scope and pattern.
18066
+ * Bare entries (e.g. `Read`, or a whole-tool name like `exec`) yield `*`.
18067
+ */
18068
+ function parseDevinPermissionEntry(entry) {
18069
+ const parenIndex = entry.indexOf("(");
18070
+ if (parenIndex === -1) return {
18071
+ scope: entry,
18072
+ pattern: "*"
18073
+ };
18074
+ const scope = entry.slice(0, parenIndex);
18075
+ if (!entry.endsWith(")")) return {
18076
+ scope,
18077
+ pattern: "*"
18078
+ };
18079
+ return {
18080
+ scope,
18081
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
18082
+ };
18083
+ }
18084
+ /**
18085
+ * Build a Devin permission entry like `Read(src/**)`. A `*` pattern collapses to
18086
+ * the bare scope (`Read`), matching the whole scope.
18087
+ */
18088
+ function buildDevinPermissionEntry(scope, pattern) {
18089
+ if (pattern === "*") return scope;
18090
+ return `${scope}(${pattern})`;
18091
+ }
18092
+ /**
18093
+ * Permissions generator for Devin Local (native `.devin/` configuration).
18094
+ *
18095
+ * Maps rulesync permission actions onto Devin's `permissions` block inside its
18096
+ * native config file — `allow` / `deny` / `ask` arrays of scope matchers
18097
+ * (`Read(glob)`, `Write(glob)`, `Exec(prefix)`, `Fetch(pattern)`, plus
18098
+ * `mcp__server__tool` patterns). Devin evaluates the arrays with strict
18099
+ * precedence: `deny` is checked before `ask`, which is checked before `allow`,
18100
+ * so a deny rule always wins.
18101
+ *
18102
+ * - Project scope: `.devin/config.json`
18103
+ * - Global scope: `~/.config/devin/config.json`
18104
+ *
18105
+ * The config file is shared with the MCP (`mcpServers`) and, in global mode, the
18106
+ * hooks (`hooks`) features, so reads and writes merge into the existing JSON and
18107
+ * the file is never deleted; only the managed `permissions` key is rewritten.
18108
+ *
18109
+ * @see https://docs.devin.ai/cli/reference/permissions
18110
+ */
18111
+ var DevinPermissions = class DevinPermissions extends ToolPermissions {
18112
+ constructor(params) {
18113
+ super({
18114
+ ...params,
18115
+ fileContent: params.fileContent ?? "{}"
18116
+ });
18117
+ }
18118
+ /**
18119
+ * config.json may carry the MCP/hooks features' keys, so it is never deleted;
18120
+ * only the managed `permissions` key is rewritten.
18121
+ */
18122
+ isDeletable() {
18123
+ return false;
18124
+ }
18125
+ static getSettablePaths({ global = false } = {}) {
18126
+ if (global) return {
18127
+ relativeDirPath: DEVIN_GLOBAL_CONFIG_DIR_PATH,
18128
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18129
+ };
18130
+ return {
18131
+ relativeDirPath: DEVIN_DIR,
18132
+ relativeFilePath: DEVIN_CONFIG_FILE_NAME
18133
+ };
18134
+ }
18135
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18136
+ const paths = DevinPermissions.getSettablePaths({ global });
18137
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{\"permissions\":{}}";
18138
+ return new DevinPermissions({
18139
+ outputRoot,
18140
+ relativeDirPath: paths.relativeDirPath,
18141
+ relativeFilePath: paths.relativeFilePath,
18142
+ fileContent,
18143
+ validate
18144
+ });
18145
+ }
18146
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
18147
+ const paths = DevinPermissions.getSettablePaths({ global });
18148
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18149
+ const existingContent = await readOrInitializeFileContent(filePath, JSON.stringify({}, null, 2));
18150
+ let settings;
18151
+ try {
18152
+ const parsed = JSON.parse(existingContent);
18153
+ settings = isRecord(parsed) ? parsed : {};
18154
+ } catch (error) {
18155
+ throw new Error(`Failed to parse existing Devin config at ${filePath}: ${formatError(error)}`, { cause: error });
18156
+ }
18157
+ const config = rulesyncPermissions.getJson();
18158
+ const { allow, ask, deny } = convertRulesyncToDevinPermissions(config);
18159
+ const managedScopes = new Set(Object.keys(config.permission).map((category) => toDevinScope(category)));
18160
+ const existingPermissions = isRecord(settings.permissions) ? settings.permissions : {};
18161
+ const preserve = (entries) => (entries ?? []).filter((entry) => !managedScopes.has(parseDevinPermissionEntry(entry).scope));
18162
+ const mergedAllow = uniq([...preserve(existingPermissions.allow), ...allow].toSorted());
18163
+ const mergedAsk = uniq([...preserve(existingPermissions.ask), ...ask].toSorted());
18164
+ const mergedDeny = uniq([...preserve(existingPermissions.deny), ...deny].toSorted());
18165
+ const mergedPermissions = { ...existingPermissions };
18166
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
18167
+ else delete mergedPermissions.allow;
18168
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
18169
+ else delete mergedPermissions.ask;
18170
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
18171
+ else delete mergedPermissions.deny;
18172
+ const merged = {
18173
+ ...settings,
18174
+ permissions: mergedPermissions
18175
+ };
18176
+ return new DevinPermissions({
18177
+ outputRoot,
18178
+ relativeDirPath: paths.relativeDirPath,
18179
+ relativeFilePath: paths.relativeFilePath,
18180
+ fileContent: JSON.stringify(merged, null, 2),
18181
+ validate
18182
+ });
18183
+ }
18184
+ toRulesyncPermissions() {
18185
+ let settings;
18186
+ try {
18187
+ const parsed = JSON.parse(this.getFileContent());
18188
+ settings = isRecord(parsed) ? parsed : {};
18189
+ } catch (error) {
18190
+ throw new Error(`Failed to parse Devin permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18191
+ }
18192
+ const permissions = isRecord(settings.permissions) ? settings.permissions : {};
18193
+ const config = convertDevinToRulesyncPermissions({
18194
+ allow: Array.isArray(permissions.allow) ? permissions.allow : [],
18195
+ ask: Array.isArray(permissions.ask) ? permissions.ask : [],
18196
+ deny: Array.isArray(permissions.deny) ? permissions.deny : []
18197
+ });
18198
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18199
+ }
18200
+ validate() {
18201
+ return {
18202
+ success: true,
18203
+ error: null
18204
+ };
18205
+ }
18206
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18207
+ return new DevinPermissions({
18208
+ outputRoot,
18209
+ relativeDirPath,
18210
+ relativeFilePath,
18211
+ fileContent: JSON.stringify({ permissions: {} }, null, 2),
18212
+ validate: false
18213
+ });
18214
+ }
18215
+ };
18216
+ /**
18217
+ * Convert rulesync permissions config to Devin allow/ask/deny arrays.
18218
+ */
18219
+ function convertRulesyncToDevinPermissions(config) {
18220
+ const allow = [];
18221
+ const ask = [];
18222
+ const deny = [];
18223
+ for (const [category, rules] of Object.entries(config.permission)) {
18224
+ const scope = toDevinScope(category);
18225
+ for (const [pattern, action] of Object.entries(rules)) {
18226
+ const entry = buildDevinPermissionEntry(scope, pattern);
18227
+ switch (action) {
18228
+ case "allow":
18229
+ allow.push(entry);
18230
+ break;
18231
+ case "ask":
18232
+ ask.push(entry);
18233
+ break;
18234
+ case "deny":
18235
+ deny.push(entry);
18236
+ break;
18237
+ }
18238
+ }
18239
+ }
18240
+ return {
18241
+ allow,
18242
+ ask,
18243
+ deny
18244
+ };
18245
+ }
18246
+ /**
18247
+ * Convert Devin allow/ask/deny arrays to rulesync permissions config. Entries
18248
+ * are applied allow → ask → deny so the most restrictive action wins for a
18249
+ * given (scope, pattern), mirroring Devin's deny > ask > allow precedence.
18250
+ */
18251
+ function convertDevinToRulesyncPermissions(params) {
18252
+ const permission = {};
18253
+ const processEntries = (entries, action) => {
18254
+ for (const entry of entries) {
18255
+ const { scope, pattern } = parseDevinPermissionEntry(entry);
18256
+ if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
18257
+ const canonical = toCanonicalCategory(scope);
18258
+ (permission[canonical] ??= {})[pattern] = action;
18259
+ }
18260
+ };
18261
+ processEntries(params.allow, "allow");
18262
+ processEntries(params.ask, "ask");
18263
+ processEntries(params.deny, "deny");
18264
+ return { permission };
18265
+ }
18266
+ //#endregion
16666
18267
  //#region src/features/permissions/factorydroid-permissions.ts
16667
18268
  /**
16668
18269
  * Permissions adapter for Factory Droid.
@@ -16922,7 +18523,7 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16922
18523
  } catch (error) {
16923
18524
  throw new Error(`Failed to parse existing Goose permission.yaml at ${filePath}: ${formatError(error)}`, { cause: error });
16924
18525
  }
16925
- const config = isRecord$1(parsed) ? { ...parsed } : {};
18526
+ const config = isRecord(parsed) ? { ...parsed } : {};
16926
18527
  config[GOOSE_USER_KEY] = convertRulesyncToGoosePermissionConfig({
16927
18528
  config: rulesyncPermissions.getJson(),
16928
18529
  logger
@@ -16944,8 +18545,8 @@ var GoosePermissions = class GoosePermissions extends ToolPermissions {
16944
18545
  } catch (error) {
16945
18546
  throw new Error(`Failed to parse Goose permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
16946
18547
  }
16947
- const config = isRecord$1(parsed) ? parsed : {};
16948
- const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord$1(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
18548
+ const config = isRecord(parsed) ? parsed : {};
18549
+ const rulesyncConfig = convertGoosePermissionConfigToRulesync(isRecord(config[GOOSE_USER_KEY]) ? config[GOOSE_USER_KEY] : {});
16949
18550
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
16950
18551
  }
16951
18552
  validate() {
@@ -17085,7 +18686,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17085
18686
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
17086
18687
  }
17087
18688
  const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
17088
- const existingUi = isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18689
+ const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
17089
18690
  parsed[GROKCLI_UI_KEY] = {
17090
18691
  ...existingUi,
17091
18692
  [GROKCLI_PERMISSION_MODE_KEY]: mode
@@ -17107,7 +18708,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
17107
18708
  } catch (error) {
17108
18709
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
17109
18710
  }
17110
- const action = (isRecord$1(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18711
+ const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
17111
18712
  const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
17112
18713
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
17113
18714
  }
@@ -17189,6 +18790,205 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
17189
18790
  }
17190
18791
  };
17191
18792
  //#endregion
18793
+ //#region src/features/permissions/junie-permissions.ts
18794
+ /**
18795
+ * JetBrains Junie CLI Action Allowlist (`allowlist.json`).
18796
+ *
18797
+ * Junie gates actions through an allowlist evaluated top-to-bottom (first match
18798
+ * wins). Project scope lives in `.junie/allowlist.json`; user scope lives in
18799
+ * `~/.junie/allowlist.json`.
18800
+ *
18801
+ * ```json
18802
+ * {
18803
+ * "defaultBehavior": "ask",
18804
+ * "allowReadonlyCommands": true,
18805
+ * "rules": {
18806
+ * "executables": [ { "prefix": "git ", "action": "allow" } ],
18807
+ * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18808
+ * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18809
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
18810
+ * }
18811
+ * }
18812
+ * ```
18813
+ *
18814
+ * Each rule carries a literal `prefix` (matches commands that start with it) or
18815
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18816
+ * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
18817
+ *
18818
+ * Category mapping (rulesync canonical <-> Junie rule group):
18819
+ * - `bash` <-> `executables`
18820
+ * - `edit`/`write` -> `fileEditing` (imported back as `edit`)
18821
+ * - `read` <-> `readOutsideProject`
18822
+ * - `mcp` <-> `mcpTools`
18823
+ *
18824
+ * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18825
+ * (with a warning when they carry rules). The top-level `defaultBehavior` and
18826
+ * `allowReadonlyCommands` settings have no canonical equivalent: they are
18827
+ * preserved verbatim on export but not imported into the rulesync model.
18828
+ *
18829
+ * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18830
+ */
18831
+ const JUNIE_RULE_GROUPS = [
18832
+ "executables",
18833
+ "fileEditing",
18834
+ "mcpTools",
18835
+ "readOutsideProject"
18836
+ ];
18837
+ const CANONICAL_TO_JUNIE_GROUP = {
18838
+ bash: "executables",
18839
+ edit: "fileEditing",
18840
+ write: "fileEditing",
18841
+ read: "readOutsideProject",
18842
+ mcp: "mcpTools"
18843
+ };
18844
+ const JUNIE_GROUP_TO_CANONICAL = {
18845
+ executables: "bash",
18846
+ fileEditing: "edit",
18847
+ mcpTools: "mcp",
18848
+ readOutsideProject: "read"
18849
+ };
18850
+ const JUNIE_DEFAULT_BEHAVIOR = "ask";
18851
+ function isPermissionAction$1(value) {
18852
+ return PermissionActionSchema.safeParse(value).success;
18853
+ }
18854
+ /**
18855
+ * Whether a rulesync pattern uses glob syntax. Junie expresses literal
18856
+ * "starts-with" matches as `prefix` and glob matches as `pattern`, so a pattern
18857
+ * containing any glob metacharacter (`*`, `?`, `[`) is emitted as `pattern`.
18858
+ */
18859
+ function isGlobPattern(pattern) {
18860
+ return /[*?[]/.test(pattern);
18861
+ }
18862
+ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18863
+ constructor(params) {
18864
+ super({
18865
+ ...params,
18866
+ fileContent: params.fileContent ?? "{}"
18867
+ });
18868
+ }
18869
+ isDeletable() {
18870
+ return false;
18871
+ }
18872
+ static getSettablePaths(_options = {}) {
18873
+ return {
18874
+ relativeDirPath: JUNIE_DIR,
18875
+ relativeFilePath: JUNIE_PERMISSIONS_FILE_NAME
18876
+ };
18877
+ }
18878
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
18879
+ const paths = JuniePermissions.getSettablePaths({ global });
18880
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
18881
+ return new JuniePermissions({
18882
+ outputRoot,
18883
+ relativeDirPath: paths.relativeDirPath,
18884
+ relativeFilePath: paths.relativeFilePath,
18885
+ fileContent,
18886
+ validate
18887
+ });
18888
+ }
18889
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18890
+ const paths = JuniePermissions.getSettablePaths({ global });
18891
+ const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
18892
+ const existingContent = await readOrInitializeFileContent(filePath, "{}");
18893
+ let existing;
18894
+ try {
18895
+ const parsed = JSON.parse(existingContent);
18896
+ existing = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18897
+ } catch (error) {
18898
+ throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18899
+ }
18900
+ const rules = convertRulesyncToJunieRules({
18901
+ config: rulesyncPermissions.getJson(),
18902
+ logger
18903
+ });
18904
+ const merged = {
18905
+ ...existing,
18906
+ defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
18907
+ rules
18908
+ };
18909
+ return new JuniePermissions({
18910
+ outputRoot,
18911
+ relativeDirPath: paths.relativeDirPath,
18912
+ relativeFilePath: paths.relativeFilePath,
18913
+ fileContent: JSON.stringify(merged, null, 2),
18914
+ validate: true
18915
+ });
18916
+ }
18917
+ toRulesyncPermissions() {
18918
+ let allowlist;
18919
+ try {
18920
+ const parsed = JSON.parse(this.getFileContent());
18921
+ allowlist = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18922
+ } catch (error) {
18923
+ throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18924
+ }
18925
+ const config = convertJunieToRulesyncPermissions({ allowlist });
18926
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18927
+ }
18928
+ validate() {
18929
+ return {
18930
+ success: true,
18931
+ error: null
18932
+ };
18933
+ }
18934
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
18935
+ return new JuniePermissions({
18936
+ outputRoot,
18937
+ relativeDirPath,
18938
+ relativeFilePath,
18939
+ fileContent: "{}",
18940
+ validate: false
18941
+ });
18942
+ }
18943
+ };
18944
+ /**
18945
+ * Convert rulesync permissions config into Junie's `rules` object. Categories
18946
+ * with no Junie rule group (e.g. `webfetch`) are skipped, with a warning when
18947
+ * they carry any rule so the gap is surfaced.
18948
+ */
18949
+ function convertRulesyncToJunieRules({ config, logger }) {
18950
+ const rules = {};
18951
+ for (const [category, patterns] of Object.entries(config.permission)) {
18952
+ const group = CANONICAL_TO_JUNIE_GROUP[category];
18953
+ if (!group) {
18954
+ 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.`);
18955
+ continue;
18956
+ }
18957
+ for (const [pattern, action] of Object.entries(patterns)) {
18958
+ const rule = isGlobPattern(pattern) ? {
18959
+ pattern,
18960
+ action
18961
+ } : {
18962
+ prefix: pattern,
18963
+ action
18964
+ };
18965
+ (rules[group] ??= []).push(rule);
18966
+ }
18967
+ }
18968
+ return rules;
18969
+ }
18970
+ /**
18971
+ * Convert a Junie allowlist back into rulesync permissions config. The
18972
+ * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
18973
+ * canonical equivalent and are not imported.
18974
+ */
18975
+ function convertJunieToRulesyncPermissions({ allowlist }) {
18976
+ const permission = {};
18977
+ const rules = allowlist.rules;
18978
+ if (rules && typeof rules === "object") for (const group of JUNIE_RULE_GROUPS) {
18979
+ const list = rules[group];
18980
+ if (!Array.isArray(list)) continue;
18981
+ const category = JUNIE_GROUP_TO_CANONICAL[group];
18982
+ for (const rule of list) {
18983
+ if (!rule || typeof rule !== "object") continue;
18984
+ const pattern = typeof rule.pattern === "string" ? rule.pattern : typeof rule.prefix === "string" ? rule.prefix : void 0;
18985
+ if (pattern === void 0 || !isPermissionAction$1(rule.action)) continue;
18986
+ (permission[category] ??= {})[pattern] = rule.action;
18987
+ }
18988
+ }
18989
+ return { permission };
18990
+ }
18991
+ //#endregion
17192
18992
  //#region src/features/permissions/kilo-permissions.ts
17193
18993
  const KiloPermissionSchema = z.union([z.enum([
17194
18994
  "allow",
@@ -17537,7 +19337,11 @@ const OpencodePermissionSchema = z.union([z.enum([
17537
19337
  "ask",
17538
19338
  "deny"
17539
19339
  ]))]);
17540
- const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodePermissionSchema)) });
19340
+ const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
19341
+ "allow",
19342
+ "ask",
19343
+ "deny"
19344
+ ]), z.record(z.string(), OpencodePermissionSchema)])) });
17541
19345
  var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17542
19346
  json;
17543
19347
  constructor(params) {
@@ -17640,6 +19444,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
17640
19444
  }
17641
19445
  normalizePermission(permission) {
17642
19446
  if (!permission) return {};
19447
+ if (typeof permission === "string") return { "*": { "*": permission } };
17643
19448
  return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
17644
19449
  }
17645
19450
  };
@@ -17676,7 +19481,7 @@ const QWEN_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL
17676
19481
  function toQwenToolName(canonical) {
17677
19482
  return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
17678
19483
  }
17679
- function toCanonicalToolName$2(qwenName) {
19484
+ function toCanonicalToolName$3(qwenName) {
17680
19485
  return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
17681
19486
  }
17682
19487
  function parseQwenPermissionEntry(entry, options = {}) {
@@ -17870,13 +19675,242 @@ function convertQwenToRulesyncPermissions(params) {
17870
19675
  const parsed = parseQwenPermissionEntry(entry, { logger });
17871
19676
  if (!parsed.ok) {
17872
19677
  if (action === "deny") {
17873
- const canonical = toCanonicalToolName$2(parsed.toolName);
19678
+ const canonical = toCanonicalToolName$3(parsed.toolName);
17874
19679
  if (!permission[canonical]) permission[canonical] = {};
17875
19680
  permission[canonical]["*"] = action;
17876
19681
  }
17877
19682
  continue;
17878
19683
  }
17879
19684
  const { toolName, pattern } = parsed;
19685
+ const canonical = toCanonicalToolName$3(toolName);
19686
+ if (!permission[canonical]) permission[canonical] = {};
19687
+ permission[canonical][pattern] = action;
19688
+ }
19689
+ };
19690
+ processEntries(params.allow, "allow");
19691
+ processEntries(params.ask, "ask");
19692
+ processEntries(params.deny, "deny");
19693
+ return { permission };
19694
+ }
19695
+ //#endregion
19696
+ //#region src/features/permissions/reasonix-permissions.ts
19697
+ /**
19698
+ * Mapping from rulesync canonical tool category names (lowercase) to Reasonix
19699
+ * permission-rule tool families (PascalCase).
19700
+ *
19701
+ * Reasonix's `[permissions]` rule syntax (SPEC.md §3.7) is explicitly
19702
+ * documented as "Claude Code-style": "Bash and file mutation approvals use
19703
+ * Claude Code-style families such as `Bash(npm run build)`, `Bash(npm run
19704
+ * test:*)`, and `Edit(docs/**)`." Reasonix also accepts legacy lowercase tool
19705
+ * IDs for compatibility, but new rules are saved using these PascalCase
19706
+ * families, so rulesync reuses the same mapping `claudecode-permissions.ts`
19707
+ * uses (the closest documented precedent for this syntax).
19708
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
19709
+ */
19710
+ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
19711
+ bash: "Bash",
19712
+ read: "Read",
19713
+ edit: "Edit",
19714
+ write: "Write",
19715
+ webfetch: "WebFetch",
19716
+ websearch: "WebSearch",
19717
+ grep: "Grep",
19718
+ glob: "Glob",
19719
+ notebookedit: "NotebookEdit",
19720
+ agent: "Agent"
19721
+ };
19722
+ /**
19723
+ * Reverse mapping from Reasonix tool names to rulesync canonical names.
19724
+ */
19725
+ const REASONIX_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_REASONIX_TOOL_NAMES).map(([k, v]) => [v, k]));
19726
+ function toReasonixToolName(canonical) {
19727
+ return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
19728
+ }
19729
+ function toCanonicalToolName$2(reasonixName) {
19730
+ return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
19731
+ }
19732
+ /**
19733
+ * Parse a Reasonix permission entry like "Bash(npm run *)" into tool name and pattern.
19734
+ * If no parentheses, returns the tool name with "*" as the pattern.
19735
+ */
19736
+ function parseReasonixPermissionEntry(entry) {
19737
+ const parenIndex = entry.indexOf("(");
19738
+ if (parenIndex === -1) return {
19739
+ toolName: entry,
19740
+ pattern: "*"
19741
+ };
19742
+ const toolName = entry.slice(0, parenIndex);
19743
+ if (!entry.endsWith(")")) return {
19744
+ toolName,
19745
+ pattern: "*"
19746
+ };
19747
+ return {
19748
+ toolName,
19749
+ pattern: entry.slice(parenIndex + 1, -1) || "*"
19750
+ };
19751
+ }
19752
+ /**
19753
+ * Build a Reasonix permission entry like "Bash(npm run *)".
19754
+ * If the pattern is "*", returns just the tool name.
19755
+ */
19756
+ function buildReasonixPermissionEntry(toolName, pattern) {
19757
+ if (pattern === "*") return toolName;
19758
+ return `${toolName}(${pattern})`;
19759
+ }
19760
+ function parseReasonixConfig(fileContent) {
19761
+ const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
19762
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
19763
+ return { ...parsed };
19764
+ }
19765
+ function toStringArray$1(value) {
19766
+ if (!Array.isArray(value)) return [];
19767
+ return value.filter((entry) => typeof entry === "string");
19768
+ }
19769
+ function toPermissionsTable(value) {
19770
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19771
+ return { ...value };
19772
+ }
19773
+ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19774
+ toml;
19775
+ constructor(params) {
19776
+ super(params);
19777
+ this.toml = parseReasonixConfig(this.getFileContent());
19778
+ }
19779
+ isDeletable() {
19780
+ return false;
19781
+ }
19782
+ static getSettablePaths({ global } = {}) {
19783
+ if (global) return {
19784
+ relativeDirPath: REASONIX_GLOBAL_DIR,
19785
+ relativeFilePath: REASONIX_GLOBAL_PERMISSIONS_FILE_NAME
19786
+ };
19787
+ return {
19788
+ relativeDirPath: ".",
19789
+ relativeFilePath: REASONIX_PROJECT_PERMISSIONS_FILE_NAME
19790
+ };
19791
+ }
19792
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
19793
+ const paths = this.getSettablePaths({ global });
19794
+ const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({});
19795
+ return new ReasonixPermissions({
19796
+ outputRoot,
19797
+ relativeDirPath: paths.relativeDirPath,
19798
+ relativeFilePath: paths.relativeFilePath,
19799
+ fileContent,
19800
+ validate
19801
+ });
19802
+ }
19803
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
19804
+ const paths = this.getSettablePaths({ global });
19805
+ const parsed = parseReasonixConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? smolToml.stringify({}));
19806
+ const config = rulesyncPermissions.getJson();
19807
+ const { allow, ask, deny } = convertRulesyncToReasonixPermissions(config);
19808
+ const managedToolNames = new Set(Object.keys(config.permission).map((category) => toReasonixToolName(category)));
19809
+ const existingPermissions = toPermissionsTable(parsed.permissions);
19810
+ const preservedAllow = toStringArray$1(existingPermissions.allow).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19811
+ const preservedAsk = toStringArray$1(existingPermissions.ask).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19812
+ const preservedDeny = toStringArray$1(existingPermissions.deny).filter((entry) => !managedToolNames.has(parseReasonixPermissionEntry(entry).toolName));
19813
+ if (logger && managedToolNames.has("Read")) {
19814
+ const droppedReadDenyEntries = toStringArray$1(existingPermissions.deny).filter((entry) => {
19815
+ const { toolName } = parseReasonixPermissionEntry(entry);
19816
+ return toolName === "Read";
19817
+ });
19818
+ 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.`);
19819
+ }
19820
+ const mergedPermissions = { ...existingPermissions };
19821
+ const mergedAllow = uniq([...preservedAllow, ...allow].toSorted());
19822
+ const mergedAsk = uniq([...preservedAsk, ...ask].toSorted());
19823
+ const mergedDeny = uniq([...preservedDeny, ...deny].toSorted());
19824
+ if (mergedAllow.length > 0) mergedPermissions.allow = mergedAllow;
19825
+ else delete mergedPermissions.allow;
19826
+ if (mergedAsk.length > 0) mergedPermissions.ask = mergedAsk;
19827
+ else delete mergedPermissions.ask;
19828
+ if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
19829
+ else delete mergedPermissions.deny;
19830
+ const merged = {
19831
+ ...parsed,
19832
+ permissions: mergedPermissions
19833
+ };
19834
+ const fileContent = smolToml.stringify(merged);
19835
+ return new ReasonixPermissions({
19836
+ outputRoot,
19837
+ relativeDirPath: paths.relativeDirPath,
19838
+ relativeFilePath: paths.relativeFilePath,
19839
+ fileContent,
19840
+ validate
19841
+ });
19842
+ }
19843
+ toRulesyncPermissions() {
19844
+ const permissions = toPermissionsTable(this.toml.permissions);
19845
+ const config = convertReasonixToRulesyncPermissions({
19846
+ allow: toStringArray$1(permissions.allow),
19847
+ ask: toStringArray$1(permissions.ask),
19848
+ deny: toStringArray$1(permissions.deny)
19849
+ });
19850
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19851
+ }
19852
+ validate() {
19853
+ try {
19854
+ parseReasonixConfig(this.getFileContent());
19855
+ return {
19856
+ success: true,
19857
+ error: null
19858
+ };
19859
+ } catch (error) {
19860
+ return {
19861
+ success: false,
19862
+ error: /* @__PURE__ */ new Error(`Failed to parse Reasonix config TOML: ${formatError(error)}`)
19863
+ };
19864
+ }
19865
+ }
19866
+ static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
19867
+ return new ReasonixPermissions({
19868
+ outputRoot,
19869
+ relativeDirPath,
19870
+ relativeFilePath,
19871
+ fileContent: smolToml.stringify({}),
19872
+ validate: false
19873
+ });
19874
+ }
19875
+ };
19876
+ /**
19877
+ * Convert rulesync permissions config to Reasonix allow/ask/deny arrays.
19878
+ */
19879
+ function convertRulesyncToReasonixPermissions(config) {
19880
+ const allow = [];
19881
+ const ask = [];
19882
+ const deny = [];
19883
+ for (const [category, rules] of Object.entries(config.permission)) {
19884
+ const reasonixToolName = toReasonixToolName(category);
19885
+ for (const [pattern, action] of Object.entries(rules)) {
19886
+ const entry = buildReasonixPermissionEntry(reasonixToolName, pattern);
19887
+ switch (action) {
19888
+ case "allow":
19889
+ allow.push(entry);
19890
+ break;
19891
+ case "ask":
19892
+ ask.push(entry);
19893
+ break;
19894
+ case "deny":
19895
+ deny.push(entry);
19896
+ break;
19897
+ }
19898
+ }
19899
+ }
19900
+ return {
19901
+ allow,
19902
+ ask,
19903
+ deny
19904
+ };
19905
+ }
19906
+ /**
19907
+ * Convert Reasonix allow/ask/deny arrays to rulesync permissions config.
19908
+ */
19909
+ function convertReasonixToRulesyncPermissions(params) {
19910
+ const permission = {};
19911
+ const processEntries = (entries, action) => {
19912
+ for (const entry of entries) {
19913
+ const { toolName, pattern } = parseReasonixPermissionEntry(entry);
17880
19914
  const canonical = toCanonicalToolName$2(toolName);
17881
19915
  if (!permission[canonical]) permission[canonical] = {};
17882
19916
  permission[canonical][pattern] = action;
@@ -17989,13 +20023,13 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
17989
20023
  } catch (error) {
17990
20024
  throw new Error(`Failed to parse existing Rovodev config at ${filePath}: ${formatError(error)}`, { cause: error });
17991
20025
  }
17992
- const config = isRecord$1(parsed) ? { ...parsed } : {};
20026
+ const config = isRecord(parsed) ? { ...parsed } : {};
17993
20027
  const toolPermissions = convertRulesyncToRovodevToolPermissions({
17994
20028
  config: rulesyncPermissions.getJson(),
17995
20029
  logger
17996
20030
  });
17997
20031
  config.toolPermissions = {
17998
- ...isRecord$1(config.toolPermissions) ? { ...config.toolPermissions } : {},
20032
+ ...isRecord(config.toolPermissions) ? { ...config.toolPermissions } : {},
17999
20033
  ...toolPermissions
18000
20034
  };
18001
20035
  return new RovodevPermissions({
@@ -18015,8 +20049,8 @@ var RovodevPermissions = class RovodevPermissions extends ToolPermissions {
18015
20049
  } catch (error) {
18016
20050
  throw new Error(`Failed to parse Rovodev permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18017
20051
  }
18018
- const config = isRecord$1(parsed) ? parsed : {};
18019
- const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord$1(config.toolPermissions) ? config.toolPermissions : {});
20052
+ const config = isRecord(parsed) ? parsed : {};
20053
+ const rulesyncConfig = convertRovodevToolPermissionsToRulesync(isRecord(config.toolPermissions) ? config.toolPermissions : {});
18020
20054
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18021
20055
  }
18022
20056
  validate() {
@@ -18094,11 +20128,11 @@ function convertBashRules(rules) {
18094
20128
  function convertRovodevToolPermissionsToRulesync(toolPermissions) {
18095
20129
  const permission = {};
18096
20130
  const bash = toolPermissions.bash;
18097
- if (isRecord$1(bash)) {
20131
+ if (isRecord(bash)) {
18098
20132
  const bashRules = {};
18099
20133
  if (isPermissionAction(bash.default)) bashRules[CATCH_ALL_PATTERN$1] = bash.default;
18100
20134
  if (Array.isArray(bash.commands)) {
18101
- for (const entry of bash.commands) if (isRecord$1(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
20135
+ for (const entry of bash.commands) if (isRecord(entry) && typeof entry.command === "string" && isPermissionAction(entry.permission)) bashRules[entry.command] = entry.permission;
18102
20136
  }
18103
20137
  if (Object.keys(bashRules).length > 0) permission.bash = bashRules;
18104
20138
  }
@@ -18237,21 +20271,6 @@ var TaktPermissions = class TaktPermissions extends ToolPermissions {
18237
20271
  }
18238
20272
  };
18239
20273
  /**
18240
- * Parse a Takt `config.yaml` into a plain object, treating an empty file as `{}`.
18241
- */
18242
- function parseTaktConfig(fileContent, relativeDirPath, relativeFilePath) {
18243
- const configPath = join(relativeDirPath, relativeFilePath);
18244
- let parsed;
18245
- try {
18246
- parsed = fileContent.trim() === "" ? {} : load(fileContent);
18247
- } catch (error) {
18248
- throw new Error(`Failed to parse Takt config at ${configPath}: ${formatError(error)}`, { cause: error });
18249
- }
18250
- if (parsed === void 0 || parsed === null) return {};
18251
- if (!isPlainObject(parsed)) throw new Error(`Failed to parse Takt config at ${configPath}: expected a YAML mapping`);
18252
- return parsed;
18253
- }
18254
- /**
18255
20274
  * Resolve the active Takt provider: the top-level `provider:` value, else the
18256
20275
  * sole key in `provider_profiles`, else the `claude` default.
18257
20276
  */
@@ -18573,8 +20592,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18573
20592
  config: rulesyncPermissions.getJson(),
18574
20593
  logger
18575
20594
  });
18576
- const agents = isRecord$1(settings.agents) ? { ...settings.agents } : {};
18577
- const profiles = isRecord$1(agents.profiles) ? { ...agents.profiles } : {};
20595
+ const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20596
+ const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
18578
20597
  const mergedAllow = uniq(allow.toSorted());
18579
20598
  const mergedDeny = uniq(deny.toSorted());
18580
20599
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -18599,8 +20618,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
18599
20618
  } catch (error) {
18600
20619
  throw new Error(`Failed to parse Warp permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18601
20620
  }
18602
- const agents = isRecord$1(settings.agents) ? settings.agents : {};
18603
- const profiles = isRecord$1(agents.profiles) ? agents.profiles : {};
20621
+ const agents = isRecord(settings.agents) ? settings.agents : {};
20622
+ const profiles = isRecord(agents.profiles) ? agents.profiles : {};
18604
20623
  const config = convertWarpToRulesyncPermissions({
18605
20624
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
18606
20625
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
@@ -18951,6 +20970,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18951
20970
  supportsImport: true
18952
20971
  }
18953
20972
  }],
20973
+ ["devin", {
20974
+ class: DevinPermissions,
20975
+ meta: {
20976
+ supportsProject: true,
20977
+ supportsGlobal: true,
20978
+ supportsImport: true
20979
+ }
20980
+ }],
18954
20981
  ["factorydroid", {
18955
20982
  class: FactorydroidPermissions,
18956
20983
  meta: {
@@ -18983,6 +21010,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
18983
21010
  supportsImport: true
18984
21011
  }
18985
21012
  }],
21013
+ ["junie", {
21014
+ class: JuniePermissions,
21015
+ meta: {
21016
+ supportsProject: true,
21017
+ supportsGlobal: true,
21018
+ supportsImport: true
21019
+ }
21020
+ }],
18986
21021
  ["kilo", {
18987
21022
  class: KiloPermissions,
18988
21023
  meta: {
@@ -19031,6 +21066,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
19031
21066
  supportsImport: true
19032
21067
  }
19033
21068
  }],
21069
+ ["reasonix", {
21070
+ class: ReasonixPermissions,
21071
+ meta: {
21072
+ supportsProject: true,
21073
+ supportsGlobal: true,
21074
+ supportsImport: true
21075
+ }
21076
+ }],
19034
21077
  ["rovodev", {
19035
21078
  class: RovodevPermissions,
19036
21079
  meta: {
@@ -19583,7 +21626,7 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
19583
21626
  opencode: z.optional(z.looseObject({
19584
21627
  "allowed-tools": z.optional(z.array(z.string())),
19585
21628
  license: z.optional(z.string()),
19586
- compatibility: z.optional(z.looseObject({})),
21629
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
19587
21630
  metadata: z.optional(z.looseObject({}))
19588
21631
  })),
19589
21632
  kilo: z.optional(z.looseObject({
@@ -19604,7 +21647,8 @@ const RulesyncSkillFrontmatterSchema = z.looseObject({
19604
21647
  })),
19605
21648
  copilotcli: z.optional(z.looseObject({
19606
21649
  license: z.optional(z.string()),
19607
- "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
21650
+ "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
21651
+ "argument-hint": z.optional(z.string())
19608
21652
  })),
19609
21653
  pi: z.optional(z.looseObject({
19610
21654
  "allowed-tools": z.optional(z.array(z.string())),
@@ -21358,7 +23402,11 @@ const CopilotSkillFrontmatterSchema = z.looseObject({
21358
23402
  });
21359
23403
  /**
21360
23404
  * Represents a GitHub Copilot skill directory.
21361
- * Skills are stored under the .github/skills directory with SKILL.md files.
23405
+ *
23406
+ * Copilot discovers project skills from `.github/skills/` and personal/global
23407
+ * skills from `~/.copilot/skills/`. Each skill is a directory containing a
23408
+ * `SKILL.md` file with `name`/`description` frontmatter.
23409
+ * https://docs.github.com/en/copilot/concepts/agents/about-agent-skills
21362
23410
  */
21363
23411
  var CopilotSkill = class CopilotSkill extends ToolSkill {
21364
23412
  constructor({ outputRoot = process.cwd(), relativeDirPath = COPILOT_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -21380,7 +23428,7 @@ var CopilotSkill = class CopilotSkill extends ToolSkill {
21380
23428
  }
21381
23429
  }
21382
23430
  static getSettablePaths(options) {
21383
- if (options?.global) throw new Error("CopilotSkill does not support global mode.");
23431
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21384
23432
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21385
23433
  }
21386
23434
  getFrontmatter() {
@@ -21495,7 +23543,8 @@ const CopilotcliSkillFrontmatterSchema = z.looseObject({
21495
23543
  name: z.string(),
21496
23544
  description: z.string(),
21497
23545
  license: z.optional(z.string()),
21498
- "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())]))
23546
+ "allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
23547
+ "argument-hint": z.optional(z.string())
21499
23548
  });
21500
23549
  /**
21501
23550
  * Represents a GitHub Copilot CLI skill directory.
@@ -21525,7 +23574,7 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21525
23574
  }
21526
23575
  }
21527
23576
  static getSettablePaths(options) {
21528
- if (options?.global) return { relativeDirPath: COPILOTCLI_SKILLS_GLOBAL_DIR_PATH };
23577
+ if (options?.global) return { relativeDirPath: COPILOT_SKILLS_GLOBAL_DIR_PATH };
21529
23578
  return { relativeDirPath: COPILOT_SKILLS_DIR_PATH };
21530
23579
  }
21531
23580
  getFrontmatter() {
@@ -21553,7 +23602,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21553
23602
  const frontmatter = this.getFrontmatter();
21554
23603
  const copilotcliSection = {
21555
23604
  ...frontmatter.license !== void 0 && { license: frontmatter.license },
21556
- ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] }
23605
+ ...frontmatter["allowed-tools"] !== void 0 && { "allowed-tools": frontmatter["allowed-tools"] },
23606
+ ...frontmatter["argument-hint"] !== void 0 && { "argument-hint": frontmatter["argument-hint"] }
21557
23607
  };
21558
23608
  const rulesyncFrontmatter = {
21559
23609
  name: frontmatter.name,
@@ -21579,7 +23629,8 @@ var CopilotcliSkill = class CopilotcliSkill extends ToolSkill {
21579
23629
  name: rulesyncFrontmatter.name,
21580
23630
  description: rulesyncFrontmatter.description,
21581
23631
  ...rulesyncFrontmatter.copilotcli?.license !== void 0 && { license: rulesyncFrontmatter.copilotcli.license },
21582
- ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] }
23632
+ ...rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== void 0 && { "allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"] },
23633
+ ...rulesyncFrontmatter.copilotcli?.["argument-hint"] !== void 0 && { "argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"] }
21583
23634
  };
21584
23635
  return new CopilotcliSkill({
21585
23636
  outputRoot,
@@ -22805,8 +24856,7 @@ var KiroSkill = class KiroSkill extends ToolSkill {
22805
24856
  if (!result.success) throw result.error;
22806
24857
  }
22807
24858
  }
22808
- static getSettablePaths(options) {
22809
- if (options?.global) throw new Error("KiroSkill does not support global mode.");
24859
+ static getSettablePaths(_options) {
22810
24860
  return { relativeDirPath: KIRO_SKILLS_DIR_PATH };
22811
24861
  }
22812
24862
  getFrontmatter() {
@@ -22948,10 +24998,19 @@ const OpenCodeSkillFrontmatterSchema = z.looseObject({
22948
24998
  name: z.string(),
22949
24999
  description: z.string(),
22950
25000
  license: z.optional(z.string()),
22951
- compatibility: z.optional(z.looseObject({})),
25001
+ compatibility: z.optional(z.union([z.string(), z.looseObject({})])),
22952
25002
  metadata: z.optional(z.looseObject({})),
22953
25003
  "allowed-tools": z.optional(z.array(z.string()))
22954
25004
  });
25005
+ /**
25006
+ * Reads a top-level `compatibility` value from rulesync frontmatter, accepting
25007
+ * both the documented string form (e.g. `compatibility: opencode`) and the
25008
+ * legacy object form. Returns `undefined` for any other shape.
25009
+ */
25010
+ function readTopLevelCompatibility(value) {
25011
+ if (typeof value === "string") return value;
25012
+ if (typeof value === "object" && value !== null) return value;
25013
+ }
22955
25014
  var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
22956
25015
  constructor({ outputRoot = process.cwd(), relativeDirPath = OPENCODE_SKILLS_DIR_PATH, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
22957
25016
  super({
@@ -23028,7 +25087,7 @@ var OpenCodeSkill = class OpenCodeSkill extends ToolSkill {
23028
25087
  const opencodeSection = rulesyncFrontmatter.opencode;
23029
25088
  const looseTopLevel = rulesyncFrontmatter;
23030
25089
  const topLevelLicense = typeof looseTopLevel.license === "string" ? looseTopLevel.license : void 0;
23031
- const topLevelCompatibility = typeof looseTopLevel.compatibility === "object" && looseTopLevel.compatibility !== null ? looseTopLevel.compatibility : void 0;
25090
+ const topLevelCompatibility = readTopLevelCompatibility(looseTopLevel.compatibility);
23032
25091
  const topLevelMetadata = typeof looseTopLevel.metadata === "object" && looseTopLevel.metadata !== null ? looseTopLevel.metadata : void 0;
23033
25092
  const license = opencodeSection?.license ?? topLevelLicense;
23034
25093
  const compatibility = opencodeSection?.compatibility ?? topLevelCompatibility;
@@ -24382,7 +26441,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24382
26441
  meta: {
24383
26442
  supportsProject: true,
24384
26443
  supportsSimulated: false,
24385
- supportsGlobal: false
26444
+ supportsGlobal: true
24386
26445
  }
24387
26446
  }],
24388
26447
  ["copilotcli", {
@@ -24470,7 +26529,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24470
26529
  meta: {
24471
26530
  supportsProject: true,
24472
26531
  supportsSimulated: false,
24473
- supportsGlobal: false
26532
+ supportsGlobal: true
24474
26533
  }
24475
26534
  }],
24476
26535
  ["kiro-ide", {
@@ -24478,7 +26537,7 @@ const toolSkillFactories = /* @__PURE__ */ new Map([
24478
26537
  meta: {
24479
26538
  supportsProject: true,
24480
26539
  supportsSimulated: false,
24481
- supportsGlobal: false
26540
+ supportsGlobal: true
24482
26541
  }
24483
26542
  }],
24484
26543
  ["opencode", {
@@ -28566,7 +30625,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28566
30625
  class: KiroCliSubagent,
28567
30626
  meta: {
28568
30627
  supportsSimulated: false,
28569
- supportsGlobal: false,
30628
+ supportsGlobal: true,
28570
30629
  filePattern: "*.json"
28571
30630
  }
28572
30631
  }],
@@ -28574,7 +30633,7 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
28574
30633
  class: KiroIdeSubagent,
28575
30634
  meta: {
28576
30635
  supportsSimulated: false,
28577
- supportsGlobal: false,
30636
+ supportsGlobal: true,
28578
30637
  filePattern: "*.md"
28579
30638
  }
28580
30639
  }],
@@ -31111,7 +33170,7 @@ const STRATEGIES = [
31111
33170
  * plus companion `globs`/`description` fields. (`.devin/rules/` is the
31112
33171
  * pre-rebrand legacy location the tool still reads.)
31113
33172
  * - Global scope: a single plain-markdown, always-on file (no frontmatter) at
31114
- * `~/.codeium/windsurf/memories/global_rules.md` (unchanged by the rebrand).
33173
+ * `~/.config/devin/AGENTS.md` (Devin Local global always-on rules).
31115
33174
  *
31116
33175
  * Trigger inference (when no explicit devin trigger is persisted):
31117
33176
  * - Specific globs (non wildcard) → glob
@@ -31138,8 +33197,8 @@ var DevinRule = class DevinRule extends ToolRule {
31138
33197
  }
31139
33198
  static getGlobalRootPath(excludeToolDir) {
31140
33199
  return {
31141
- relativeDirPath: buildToolPath(CODEIUM_DIR, WINDSURF_MEMORIES_SUBDIR, excludeToolDir),
31142
- relativeFilePath: DEVIN_GLOBAL_RULES_FILE_NAME
33200
+ relativeDirPath: buildToolPath(DEVIN_GLOBAL_CONFIG_DIR_PATH, ".", excludeToolDir),
33201
+ relativeFilePath: DEVIN_GLOBAL_AGENTS_FILE_NAME
31143
33202
  };
31144
33203
  }
31145
33204
  static getSettablePaths({ global = false, excludeToolDir } = {}) {
@@ -32029,7 +34088,7 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32029
34088
  /**
32030
34089
  * Rule generator for Pi Coding Agent.
32031
34090
  *
32032
- * Pi loads instruction context only from the `AGENTS.md` / `CLAUDE.md` family —
34091
+ * Pi loads instruction context from the `AGENTS.md` / `CLAUDE.md` family —
32033
34092
  * the global `~/.pi/agent/AGENTS.md` plus files discovered by walking up the
32034
34093
  * directory tree from the current working directory. It does NOT resolve
32035
34094
  * `@`-imports or a TOON file list, and has no `.agents/memories/` concept, so
@@ -32040,6 +34099,15 @@ var OpenCodeRule = class OpenCodeRule extends ToolRule {
32040
34099
  * to map onto; their bodies are folded into the single root `AGENTS.md` by the
32041
34100
  * RulesProcessor (there is no separate non-root output location — `nonRoot` is
32042
34101
  * `undefined`). This mirrors the codexcli, grokcli, warp, and deepagents targets.
34102
+ *
34103
+ * Pi also loads two system-prompt instruction files that rulesync does NOT emit:
34104
+ * `.pi/SYSTEM.md` (global `~/.pi/agent/SYSTEM.md`) *replaces* the default system
34105
+ * prompt entirely, and `.pi/APPEND_SYSTEM.md` (global
34106
+ * `~/.pi/agent/APPEND_SYSTEM.md`) *appends* to it. rulesync's rules model only
34107
+ * routes a designated `root` rule to a single context file and has no convention
34108
+ * for marking a rule as "replace" vs "append" the system prompt, so these
34109
+ * surfaces are documented in docs/reference/file-formats.md and left to be
34110
+ * authored by hand rather than mapped to a speculative new frontmatter flag.
32043
34111
  */
32044
34112
  var PiRule = class PiRule extends ToolRule {
32045
34113
  constructor({ fileContent, root, ...rest }) {
@@ -34121,63 +36189,215 @@ function warnUnsupportedTargets(params) {
34121
36189
  async function checkRulesyncDirExists(params) {
34122
36190
  return fileExists(join(params.inputRoot, RULESYNC_RELATIVE_DIR_PATH));
34123
36191
  }
36192
+ function dependsOnReachable(byId, from, target) {
36193
+ const seen = /* @__PURE__ */ new Set();
36194
+ const stack = [from];
36195
+ while (stack.length > 0) {
36196
+ const current = stack.pop();
36197
+ if (current === void 0 || seen.has(current)) continue;
36198
+ seen.add(current);
36199
+ if (current === target) return true;
36200
+ for (const dep of byId.get(current)?.dependsOn ?? []) stack.push(dep);
36201
+ }
36202
+ return false;
36203
+ }
36204
+ function assertSharedFilesOrdered(steps, byId) {
36205
+ const writersByFile = /* @__PURE__ */ new Map();
36206
+ for (const step of steps) for (const file of step.writesSharedFile ?? []) writersByFile.set(file, [...writersByFile.get(file) ?? [], step.id]);
36207
+ for (const [file, writers] of writersByFile) for (let i = 0; i < writers.length; i++) for (let j = i + 1; j < writers.length; j++) {
36208
+ const a = writers[i];
36209
+ const b = writers[j];
36210
+ 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.`);
36211
+ }
36212
+ }
36213
+ /**
36214
+ * Topologically sort generation steps and reject ordering hazards: a shared file
36215
+ * with two writers not ordered by `dependsOn` (a silent data-loss trap), an
36216
+ * unknown dependency, or a cycle. Reordering `steps` stays safe as a result.
36217
+ *
36218
+ * @throws Error if a shared file has unordered writers, a dependency is unknown,
36219
+ * or the dependency graph contains a cycle.
36220
+ */
36221
+ function resolveExecutionOrder(steps) {
36222
+ const byId = new Map(steps.map((step) => [step.id, step]));
36223
+ assertSharedFilesOrdered(steps, byId);
36224
+ const unresolvedDeps = new Map(steps.map((step) => [step.id, 0]));
36225
+ const dependents = /* @__PURE__ */ new Map();
36226
+ for (const step of steps) for (const dep of step.dependsOn ?? []) {
36227
+ if (!byId.has(dep)) throw new Error(`Generation step '${step.id}' depends on unknown step '${dep}'.`);
36228
+ unresolvedDeps.set(step.id, (unresolvedDeps.get(step.id) ?? 0) + 1);
36229
+ dependents.set(dep, [...dependents.get(dep) ?? [], step.id]);
36230
+ }
36231
+ const ready = steps.filter((step) => (unresolvedDeps.get(step.id) ?? 0) === 0).map((step) => step.id);
36232
+ const ordered = [];
36233
+ while (ready.length > 0) {
36234
+ const id = ready.shift();
36235
+ ordered.push(byId.get(id));
36236
+ for (const dependent of dependents.get(id) ?? []) {
36237
+ const next = (unresolvedDeps.get(dependent) ?? 0) - 1;
36238
+ unresolvedDeps.set(dependent, next);
36239
+ if (next === 0) ready.push(dependent);
36240
+ }
36241
+ }
36242
+ if (ordered.length !== steps.length) throw new Error("Generation steps contain a cyclic 'dependsOn' dependency.");
36243
+ return ordered;
36244
+ }
36245
+ /**
36246
+ * The static shape of the generation step graph: which steps write which shared
36247
+ * (read-modify-write) config files, and the `dependsOn` edges that fix a safe order
36248
+ * for those writers. Exported (separately from the `run` closures, which need a
36249
+ * live `config`/`logger`) so `resolveExecutionOrder`'s ordering guarantee can be
36250
+ * tested directly against the real graph rather than a hand-copied one. Readonly
36251
+ * so a consumer can't mutate this module-level singleton and affect every
36252
+ * subsequent `generate()` call in the process.
36253
+ */
36254
+ const GENERATION_STEP_GRAPH = [
36255
+ {
36256
+ id: "ignore",
36257
+ writesSharedFile: ["claude-settings", "zed-settings"]
36258
+ },
36259
+ {
36260
+ id: "mcp",
36261
+ writesSharedFile: [
36262
+ "kilo-opencode-config",
36263
+ "zed-settings",
36264
+ "qwencode-settings",
36265
+ "augmentcode-settings",
36266
+ "hermesagent-config",
36267
+ "amp-settings",
36268
+ "codexcli-config",
36269
+ "grokcli-config",
36270
+ "vibe-config",
36271
+ "devin-config",
36272
+ "reasonix-config"
36273
+ ],
36274
+ dependsOn: ["ignore"]
36275
+ },
36276
+ { id: "commands" },
36277
+ { id: "subagents" },
36278
+ { id: "skills" },
36279
+ {
36280
+ id: "hooks",
36281
+ writesSharedFile: [
36282
+ "claude-settings",
36283
+ "qwencode-settings",
36284
+ "augmentcode-settings",
36285
+ "hermesagent-config",
36286
+ "kiro-agent-config",
36287
+ "codexcli-config",
36288
+ "vibe-config",
36289
+ "devin-config"
36290
+ ],
36291
+ dependsOn: ["ignore", "mcp"]
36292
+ },
36293
+ {
36294
+ id: "permissions",
36295
+ writesSharedFile: [
36296
+ "claude-settings",
36297
+ "kilo-opencode-config",
36298
+ "zed-settings",
36299
+ "qwencode-settings",
36300
+ "augmentcode-settings",
36301
+ "hermesagent-config",
36302
+ "kiro-agent-config",
36303
+ "amp-settings",
36304
+ "codexcli-config",
36305
+ "grokcli-config",
36306
+ "vibe-config",
36307
+ "devin-config",
36308
+ "reasonix-config"
36309
+ ],
36310
+ dependsOn: [
36311
+ "ignore",
36312
+ "hooks",
36313
+ "mcp"
36314
+ ]
36315
+ },
36316
+ {
36317
+ id: "rules",
36318
+ writesSharedFile: ["kilo-opencode-config"],
36319
+ dependsOn: [
36320
+ "mcp",
36321
+ "skills",
36322
+ "permissions"
36323
+ ]
36324
+ }
36325
+ ];
34124
36326
  /**
34125
36327
  * Generate configuration files for AI tools.
34126
36328
  * @throws Error if generation fails
34127
36329
  */
34128
36330
  async function generate(params) {
34129
36331
  const { config, logger } = params;
34130
- const ignoreResult = await generateIgnoreCore({
34131
- config,
34132
- logger
34133
- });
34134
- const mcpResult = await generateMcpCore({
34135
- config,
34136
- logger
34137
- });
34138
- const commandsResult = await generateCommandsCore({
34139
- config,
34140
- logger
34141
- });
34142
- const subagentsResult = await generateSubagentsCore({
34143
- config,
34144
- logger
34145
- });
34146
- const skillsResult = await generateSkillsCore({
34147
- config,
34148
- logger
34149
- });
34150
- const hooksResult = await generateHooksCore({
34151
- config,
34152
- logger
34153
- });
34154
- const permissionsResult = await generatePermissionsCore({
34155
- config,
34156
- logger
34157
- });
34158
- const rulesResult = await generateRulesCore({
34159
- config,
34160
- logger,
34161
- skills: skillsResult.skills
34162
- });
34163
- const hasDiff = ignoreResult.hasDiff || mcpResult.hasDiff || commandsResult.hasDiff || subagentsResult.hasDiff || skillsResult.hasDiff || hooksResult.hasDiff || permissionsResult.hasDiff || rulesResult.hasDiff;
36332
+ let skillsResult;
36333
+ const runners = {
36334
+ ignore: () => generateIgnoreCore({
36335
+ config,
36336
+ logger
36337
+ }),
36338
+ mcp: () => generateMcpCore({
36339
+ config,
36340
+ logger
36341
+ }),
36342
+ commands: () => generateCommandsCore({
36343
+ config,
36344
+ logger
36345
+ }),
36346
+ subagents: () => generateSubagentsCore({
36347
+ config,
36348
+ logger
36349
+ }),
36350
+ skills: async () => {
36351
+ skillsResult = await generateSkillsCore({
36352
+ config,
36353
+ logger
36354
+ });
36355
+ return skillsResult;
36356
+ },
36357
+ hooks: () => generateHooksCore({
36358
+ config,
36359
+ logger
36360
+ }),
36361
+ permissions: () => generatePermissionsCore({
36362
+ config,
36363
+ logger
36364
+ }),
36365
+ rules: () => generateRulesCore({
36366
+ config,
36367
+ logger,
36368
+ skills: skillsResult?.skills
36369
+ })
36370
+ };
36371
+ const orderedSteps = resolveExecutionOrder(GENERATION_STEP_GRAPH.map((meta) => ({
36372
+ ...meta,
36373
+ run: runners[meta.id]
36374
+ })));
36375
+ const resultsById = /* @__PURE__ */ new Map();
36376
+ for (const step of orderedSteps) resultsById.set(step.id, await step.run());
36377
+ if (!skillsResult) throw new Error("Skills generation step did not run.");
36378
+ const get = (id) => {
36379
+ const result = resultsById.get(id);
36380
+ if (!result) throw new Error(`Missing generation result for step '${id}'.`);
36381
+ return result;
36382
+ };
36383
+ const hasDiff = orderedSteps.some((step) => get(step.id).hasDiff);
34164
36384
  return {
34165
- rulesCount: rulesResult.count,
34166
- rulesPaths: rulesResult.paths,
34167
- ignoreCount: ignoreResult.count,
34168
- ignorePaths: ignoreResult.paths,
34169
- mcpCount: mcpResult.count,
34170
- mcpPaths: mcpResult.paths,
34171
- commandsCount: commandsResult.count,
34172
- commandsPaths: commandsResult.paths,
34173
- subagentsCount: subagentsResult.count,
34174
- subagentsPaths: subagentsResult.paths,
36385
+ rulesCount: get("rules").count,
36386
+ rulesPaths: get("rules").paths,
36387
+ ignoreCount: get("ignore").count,
36388
+ ignorePaths: get("ignore").paths,
36389
+ mcpCount: get("mcp").count,
36390
+ mcpPaths: get("mcp").paths,
36391
+ commandsCount: get("commands").count,
36392
+ commandsPaths: get("commands").paths,
36393
+ subagentsCount: get("subagents").count,
36394
+ subagentsPaths: get("subagents").paths,
34175
36395
  skillsCount: skillsResult.count,
34176
36396
  skillsPaths: skillsResult.paths,
34177
- hooksCount: hooksResult.count,
34178
- hooksPaths: hooksResult.paths,
34179
- permissionsCount: permissionsResult.count,
34180
- permissionsPaths: permissionsResult.paths,
36397
+ hooksCount: get("hooks").count,
36398
+ hooksPaths: get("hooks").paths,
36399
+ permissionsCount: get("permissions").count,
36400
+ permissionsPaths: get("permissions").paths,
34181
36401
  skills: skillsResult.skills,
34182
36402
  hasDiff
34183
36403
  };
@@ -34794,4 +37014,4 @@ async function importPermissionsCore(params) {
34794
37014
  //#endregion
34795
37015
  export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
34796
37016
 
34797
- //# sourceMappingURL=import-zmpFGK87.js.map
37017
+ //# sourceMappingURL=import-ylOvQDpE.js.map